feat(i18n): Phase 1 — migrate 5 critical files from hardcoded text to i18n
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 2m51s

47 new i18n keys added across all 13 locales (en, fr, es, de, pt, it,
nl, ru, ja, ko, zh, ar, fa). English and French are fully translated,
remaining locales use French as placeholder.

Files migrated:
- EditGlossaryDialog.tsx (18 strings)
- DeleteGlossaryDialog.tsx (7 strings)
- ProUpgradePrompt.tsx (10 strings)
- WebhookSnippet.tsx (4 strings)
- TranslationModeToggle.tsx (8 strings)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-01 00:28:24 +02:00
parent c82e70681f
commit 6d27dc4cda
6 changed files with 666 additions and 47 deletions

View File

@@ -5,6 +5,7 @@ import { Webhook, Copy, Check } from 'lucide-react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { API_BASE_URL } from '@/lib/apiClient';
import { useI18n } from '@/lib/i18n';
function getWebhookSnippet(): string {
const baseUrl = API_BASE_URL.replace(/\/$/, '');
@@ -18,6 +19,7 @@ function getWebhookSnippet(): string {
}
export function WebhookSnippet() {
const { t } = useI18n();
const [copied, setCopied] = useState(false);
const webhookSnippet = useMemo(() => getWebhookSnippet(), []);
@@ -33,13 +35,12 @@ export function WebhookSnippet() {
<CardHeader>
<div className="flex items-center gap-2">
<Webhook className="h-5 w-5 text-accent" />
<CardTitle className="text-base">Webhook Integration</CardTitle>
<CardTitle className="text-base">{t('apiKeys.webhook.title')}</CardTitle>
</div>
</CardHeader>
<CardContent className="space-y-3">
<p className="text-sm text-muted-foreground">
Pass a <code className="rounded bg-muted px-1.5 py-0.5 font-mono text-xs">webhook_url</code> parameter
to receive a POST request when your translation is complete.
{t('apiKeys.webhook.descriptionBefore')}<code className="rounded bg-muted px-1.5 py-0.5 font-mono text-xs">{t('apiKeys.webhook.codeParam')}</code>{t('apiKeys.webhook.descriptionAfter')}
</p>
<div className="relative">
<Button

View File

@@ -10,6 +10,7 @@ import {
DialogTitle,
} from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { useI18n } from '@/lib/i18n';
interface DeleteGlossaryDialogProps {
open: boolean;
@@ -26,13 +27,14 @@ export function DeleteGlossaryDialog({
isDeleting,
glossaryName,
}: DeleteGlossaryDialogProps) {
const { t } = useI18n();
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle>Delete Glossary</DialogTitle>
<DialogTitle>{t('glossaries.delete.title')}</DialogTitle>
<DialogDescription>
Are you sure you want to delete this glossary?
{t('glossaries.delete.description')}
{glossaryName && (
<span className="block mt-1 font-medium text-foreground">
"{glossaryName}"
@@ -45,9 +47,9 @@ export function DeleteGlossaryDialog({
<div className="flex items-start gap-3">
<AlertTriangle className="h-5 w-5 text-destructive shrink-0 mt-0.5" />
<div className="space-y-1">
<p className="text-sm font-medium text-destructive">This action cannot be undone</p>
<p className="text-sm font-medium text-destructive">{t('glossaries.delete.warning')}</p>
<p className="text-sm text-muted-foreground">
All term pairs will be permanently removed.
{t('glossaries.delete.warningDesc')}
</p>
</div>
</div>
@@ -59,14 +61,14 @@ export function DeleteGlossaryDialog({
onClick={() => onOpenChange(false)}
disabled={isDeleting}
>
Cancel
{t('glossaries.delete.cancel')}
</Button>
<Button
variant="destructive"
<Button
variant="destructive"
onClick={onConfirm}
disabled={isDeleting}
>
{isDeleting ? 'Deleting...' : 'Delete'}
{isDeleting ? t('glossaries.delete.deleting') : t('glossaries.delete.deleteBtn')}
</Button>
</DialogFooter>
</DialogContent>

View File

@@ -18,6 +18,7 @@ import { exportGlossaryToCsv, parseCsvToTerms } from './csvUtils';
import { useToast } from '@/components/ui/toast';
import type { Glossary, GlossaryTermInput } from './types';
import { MAX_TERMS_PER_GLOSSARY, SUPPORTED_LANGUAGES } from './types';
import { useI18n } from '@/lib/i18n';
interface EditGlossaryDialogProps {
open: boolean;
@@ -41,6 +42,7 @@ export function EditGlossaryDialog({
onSave,
isSaving,
}: EditGlossaryDialogProps) {
const { t } = useI18n();
const [name, setName] = useState('');
const [sourceLanguage, setSourceLanguage] = useState('fr');
const [targetLanguage, setTargetLanguage] = useState('multi');
@@ -133,22 +135,22 @@ export function EditGlossaryDialog({
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.`,
title: t('glossaries.edit.importFailedTitle'),
description: t('glossaries.edit.importFailedMaxDesc', { count: importedTerms.length, max: MAX_TERMS_PER_GLOSSARY }),
});
e.target.value = '';
return;
}
setTerms(importedTerms);
toast({
title: 'Import successful',
description: `${importedTerms.length} terms imported successfully.`,
title: t('glossaries.edit.importSuccessTitle'),
description: t('glossaries.edit.importSuccessDesc', { count: importedTerms.length }),
});
} else {
toast({
variant: 'destructive',
title: 'Import failed',
description: 'No valid terms found in CSV file.',
title: t('glossaries.edit.importFailedTitle'),
description: t('glossaries.edit.importFailedEmptyDesc'),
});
}
}
@@ -156,8 +158,8 @@ export function EditGlossaryDialog({
reader.onerror = () => {
toast({
variant: 'destructive',
title: 'Import failed',
description: 'Failed to read CSV file.',
title: t('glossaries.edit.importFailedTitle'),
description: t('glossaries.edit.importFailedReadDesc'),
});
};
reader.readAsText(file);
@@ -171,27 +173,27 @@ export function EditGlossaryDialog({
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-2xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>Edit Glossary</DialogTitle>
<DialogTitle>{t('glossaries.edit.title')}</DialogTitle>
<DialogDescription>
Update the glossary name, language pair, and terms.
{t('glossaries.edit.description')}
</DialogDescription>
</DialogHeader>
<div className="space-y-6 py-4">
<div className="space-y-2">
<Label htmlFor="glossary-name">Glossary Name</Label>
<Label htmlFor="glossary-name">{t('glossaries.edit.nameLabel')}</Label>
<Input
id="glossary-name"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Enter glossary name..."
placeholder={t('glossaries.edit.namePlaceholder')}
disabled={isSaving}
/>
</div>
<div className="flex items-center gap-2">
<div className="flex-1">
<Label className="text-xs text-muted-foreground mb-1 block">Langue source</Label>
<Label className="text-xs text-muted-foreground mb-1 block">{t('glossaries.edit.sourceLang')}</Label>
<select
value={sourceLanguage}
onChange={e => setSourceLanguage(e.target.value)}
@@ -205,7 +207,7 @@ export function EditGlossaryDialog({
</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>
<Label className="text-xs text-muted-foreground mb-1 block">{t('glossaries.edit.targetLang')}</Label>
<select
value={targetLanguage}
onChange={e => handleTargetLanguageChange(e.target.value)}
@@ -220,7 +222,7 @@ export function EditGlossaryDialog({
</div>
<div className="space-y-2">
<Label>Terms ({validTermsCount} valid)</Label>
<Label>{t('glossaries.edit.termsLabel', { count: validTermsCount })}</Label>
<TermEditor
terms={terms}
onChange={setTerms}
@@ -238,7 +240,7 @@ export function EditGlossaryDialog({
className="gap-1.5"
>
<Download className="size-3.5" />
Export CSV
{t('glossaries.edit.exportCsv')}
</Button>
<Button
type="button"
@@ -249,7 +251,7 @@ export function EditGlossaryDialog({
className="gap-1.5"
>
<Upload className="size-3.5" />
Import CSV
{t('glossaries.edit.importCsv')}
</Button>
<input
ref={fileInputRef}
@@ -267,13 +269,13 @@ export function EditGlossaryDialog({
onClick={() => onOpenChange(false)}
disabled={isSaving}
>
Cancel
{t('glossaries.edit.cancel')}
</Button>
<Button
onClick={handleSave}
disabled={isSaving || !name.trim()}
>
{isSaving ? 'Saving...' : 'Save Changes'}
{isSaving ? t('glossaries.edit.saving') : t('glossaries.edit.saveChanges')}
</Button>
</DialogFooter>
</DialogContent>

View File

@@ -4,8 +4,10 @@ import { BookText, Sparkles } from 'lucide-react';
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import Link from 'next/link';
import { useI18n } from '@/lib/i18n';
export function ProUpgradePrompt() {
const { t } = useI18n();
return (
<div className="flex items-center justify-center min-h-[60vh] p-6">
<Card className="max-w-md w-full border-border/50 bg-gradient-to-br from-card via-card to-accent/5">
@@ -13,39 +15,38 @@ export function ProUpgradePrompt() {
<div className="mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-2xl bg-gradient-to-br from-accent/20 to-accent/5">
<BookText className="h-8 w-8 text-accent" />
</div>
<CardTitle className="text-2xl font-semibold">Glossaries</CardTitle>
<CardTitle className="text-2xl font-semibold">{t('glossaries.upgrade.title')}</CardTitle>
<CardDescription className="text-base">
Customize your translations with custom terminology
{t('glossaries.upgrade.description')}
</CardDescription>
</CardHeader>
<CardContent className="text-center space-y-6">
<div className="space-y-3">
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<Sparkles className="h-4 w-4 text-accent shrink-0" />
<span>Create multiple glossaries</span>
<span>{t('glossaries.upgrade.feature1')}</span>
</div>
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<Sparkles className="h-4 w-4 text-accent shrink-0" />
<span>Define sourcetarget term pairs</span>
<span>{t('glossaries.upgrade.feature2')}</span>
</div>
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<Sparkles className="h-4 w-4 text-accent shrink-0" />
<span>Import/export via CSV</span>
<span>{t('glossaries.upgrade.feature3')}</span>
</div>
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<Sparkles className="h-4 w-4 text-accent shrink-0" />
<span>Apply to LLM translations</span>
<span>{t('glossaries.upgrade.feature4')}</span>
</div>
</div>
<div className="pt-2">
<p className="text-sm text-muted-foreground mb-4">
Glossaries are a <span className="text-accent font-medium">Pro</span> feature.
Upgrade to unlock custom terminology.
{t('glossaries.upgrade.proFeatureBefore')}<span className="text-accent font-medium">{t('glossaries.upgrade.proLabel')}</span>{t('glossaries.upgrade.proFeatureAfter')}
</p>
<Button asChild className="w-full bg-accent hover:bg-accent/90">
<Link href="/pricing">
Upgrade to Pro
{t('glossaries.upgrade.upgradeBtn')}
</Link>
</Button>
</div>

View File

@@ -9,6 +9,7 @@ import {
TooltipTrigger,
} from '@/components/ui/tooltip';
import type { TranslationMode } from './types';
import { useI18n } from '@/lib/i18n';
interface TranslationModeToggleProps {
mode: TranslationMode;
@@ -21,11 +22,12 @@ export function TranslationModeToggle({
onModeChange,
isPro,
}: TranslationModeToggleProps) {
const { t } = useI18n();
return (
<TooltipProvider>
<div className="flex flex-col gap-1.5">
<label className="text-xs font-medium text-muted-foreground">
Translation Mode
{t('translate.mode.label')}
</label>
<div className="flex rounded-lg border border-border bg-muted p-1">
<button
@@ -38,9 +40,9 @@ export function TranslationModeToggle({
)}
onClick={() => onModeChange('classic')}
>
Classic
{t('translate.mode.classic')}
<span className="ms-1.5 text-xs text-muted-foreground">
Fast
{t('translate.mode.classicDesc')}
</span>
</button>
<Tooltip>
@@ -57,9 +59,9 @@ export function TranslationModeToggle({
onClick={() => isPro && onModeChange('llm')}
disabled={!isPro}
>
Pro LLM
{t('translate.mode.proLlm')}
<span className="ms-1.5 text-xs text-muted-foreground">
Context-Aware
{t('translate.mode.proLlmDesc')}
</span>
{!isPro && (
<Lock className="absolute right-2 top-1/2 -translate-y-1/2 size-3 text-muted-foreground" />
@@ -68,7 +70,7 @@ export function TranslationModeToggle({
</TooltipTrigger>
{!isPro && (
<TooltipContent side="top">
<p>Upgrade to Pro for LLM translation</p>
<p>{t('translate.mode.tooltip')}</p>
</TooltipContent>
)}
</Tooltip>
@@ -76,9 +78,9 @@ export function TranslationModeToggle({
{!isPro && (
<p className="text-xs text-muted-foreground">
<a href="/pricing" className="text-primary hover:underline">
Upgrade to Pro
{t('translate.mode.upgradeLink')}
</a>{' '}
for LLM-powered translations
{t('translate.mode.upgradeDesc')}
</p>
)}
</div>