Amélioration de l'ergonomie de l'interface et de la table de relecture
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 3m11s

This commit is contained in:
2026-08-31 20:52:55 +02:00
parent d008baf81b
commit 1c6c91d5ea
206 changed files with 74199 additions and 213 deletions

View File

@@ -4,7 +4,7 @@ import { useState, useEffect, useMemo } from 'react';
import {
Library, Calendar, Hash, MessageSquare, Save, Trash2, Loader2,
CheckCircle2, AlertCircle, ArrowRight, Info, Search, Plus,
Sparkles, Zap
Sparkles,
} from 'lucide-react';
import Link from 'next/link';
import { useRouter } from 'next/navigation';
@@ -19,7 +19,6 @@ import { SUPPORTED_LANGUAGES } from './types';
import { useTranslationStore } from '@/lib/store';
// ── Chips de suggestions pour les consignes de contexte ─────────────────────
// Prompt bodies are LLM content — resolved through t() so they follow the UI locale.
const CONTEXT_SUGGESTIONS: { labelKey: string; valueKey: string }[] = [
{ labelKey: 'glossaries.suggestion.formal', valueKey: 'glossaries.suggestion.formal.value' },
{ labelKey: 'glossaries.suggestion.proprietary', valueKey: 'glossaries.suggestion.proprietary.value' },
@@ -62,8 +61,8 @@ export default function GlossariesPage() {
setIsSavingPrompt(true);
updateSettings({ systemPrompt });
setPromptSaved(true);
toast({ title: t('context.saved'), description: t('context.savedDesc') });
setTimeout(() => setPromptSaved(false), 3000);
toast({ title: t('context.saved'), description: t('context.savedDesc') });
setTimeout(() => setPromptSaved(false), 3000);
setIsSavingPrompt(false);
};
@@ -90,7 +89,7 @@ export default function GlossariesPage() {
<div className="flex items-center justify-center min-h-[60vh]">
<div className="text-center space-y-4">
<div className="animate-spin rounded-full h-8 w-8 border-4 border-brand-muted border-t-brand-accent mx-auto"></div>
<p className="text-xs text-[#555555] dark:text-white/40 font-light">{t('glossaries.loading')}</p>
<p className="text-xs text-muted-foreground font-light">{t('glossaries.loading')}</p>
</div>
</div>
);
@@ -112,14 +111,14 @@ export default function GlossariesPage() {
<h1 className="text-4xl md:text-5xl mb-3 leading-tight text-brand-dark dark:text-white font-serif font-medium tracking-tight">
{t('glossaries.titleBase')} <span className="italic">{t('glossaries.titleAccent')}</span>
</h1>
<p className="text-[#555555] dark:text-white/60 text-sm font-light leading-relaxed">
<p className="text-muted-foreground text-sm font-light leading-relaxed max-w-xl">
{t('glossaries.description') || 'Gérez vos glossaires et instructions de contexte pour des traductions plus précises.'}
</p>
</div>
{/* Bouton principal — unique point d'entrée pour créer */}
{/* Bouton principal — création de glossaire */}
<Link
href="/dashboard/glossaries/new"
className="shrink-0 flex items-center gap-2 px-5 py-2.5 rounded-xl bg-[#1A1A1A] hover:bg-[#333333] text-white text-xs font-bold uppercase tracking-widest transition-all"
className="shrink-0 flex items-center gap-2 px-5 py-2.5 rounded-xl bg-brand-dark hover:bg-brand-accent text-white dark:bg-brand-accent dark:text-brand-dark text-xs font-bold uppercase tracking-widest transition-all shadow-sm active:scale-98"
>
<Plus size={14} />
{t('glossaries.new')}
@@ -127,17 +126,17 @@ export default function GlossariesPage() {
</div>
{/* ── Bandeau informatif ────────────────────────────────────── */}
<div className="mb-8 flex items-center gap-3 p-4 rounded-xl bg-[#F5F0EA] dark:bg-brand-accent/10 border border-[#D4B896] dark:border-brand-accent/20">
<div className="w-7 h-7 rounded-full bg-[#8B6F47] text-white flex items-center justify-center shrink-0">
<div className="mb-8 flex items-center gap-3 p-4 rounded-2xl bg-brand-accent/10 border border-brand-accent/20">
<div className="w-7 h-7 rounded-full bg-brand-goldink dark:bg-brand-accent text-white dark:text-brand-dark flex items-center justify-center shrink-0">
<Info size={13} />
</div>
<p className="text-[11px] text-[#2D2D2D] dark:text-white/70 font-medium leading-relaxed flex-1">
<strong className="font-bold text-[#1A1A1A] dark:text-white">{t('glossaries.info.title')}</strong>{' '}
<p className="text-xs text-brand-dark/85 dark:text-white/80 font-medium leading-relaxed flex-1">
<strong className="font-bold text-brand-dark dark:text-white">{t('glossaries.info.title')}</strong>{' '}
{t('glossaries.info.body')}
</p>
<Link
href="/dashboard/translate"
className="shrink-0 flex items-center gap-1.5 text-[10px] font-bold uppercase tracking-wider text-[#7A5C35] dark:text-brand-accent hover:underline whitespace-nowrap"
className="shrink-0 flex items-center gap-1.5 text-[10px] font-bold uppercase tracking-wider text-brand-goldink dark:text-brand-accent hover:underline whitespace-nowrap"
>
{t('glossaries.info.cta')} <ArrowRight size={11} />
</Link>
@@ -152,8 +151,8 @@ export default function GlossariesPage() {
className={cn(
'pb-4 px-6 text-xs uppercase tracking-widest font-bold border-b-2 transition-all cursor-pointer',
activeTab === 'glossaries'
? 'border-brand-accent text-[#1A1A1A] dark:text-white'
: 'border-transparent text-[#555555] dark:text-white/40 hover:text-[#1A1A1A] dark:hover:text-white/80'
? 'border-brand-accent text-brand-dark dark:text-white'
: 'border-transparent text-muted-foreground hover:text-brand-dark dark:hover:text-white'
)}
>
{t('glossaries.tabs.glossaries') || 'Glossaires terminologiques'}
@@ -165,8 +164,8 @@ export default function GlossariesPage() {
className={cn(
'pb-4 px-6 text-xs uppercase tracking-widest font-bold border-b-2 transition-all cursor-pointer',
activeTab === 'context'
? 'border-brand-accent text-[#1A1A1A] dark:text-white'
: 'border-transparent text-[#555555] dark:text-white/40 hover:text-[#1A1A1A] dark:hover:text-white/80'
? 'border-brand-accent text-brand-dark dark:text-white'
: 'border-transparent text-muted-foreground hover:text-brand-dark dark:hover:text-white'
)}
>
{t('glossaries.tabs.context') || 'Consignes de contexte (IA)'}
@@ -177,19 +176,19 @@ export default function GlossariesPage() {
{activeTab === 'context' ? (
/* ── Onglet Consignes de Contexte ─────────────────────── */
<section className="editorial-card p-8 lg:p-10 bg-white dark:bg-[#141414] border border-black/5 dark:border-white/5 rounded-2xl shadow-sm animate-fade-in">
<section className="editorial-card p-8 lg:p-10 bg-white dark:bg-[#141414] border border-black/5 dark:border-white/5 rounded-2xl shadow-editorial animate-fade-in">
{/* En-tête section */}
<div className="flex items-start justify-between mb-6 gap-4">
<div className="flex items-start gap-3">
<div className="w-9 h-9 rounded-xl bg-brand-accent/10 flex items-center justify-center text-[#8B6F47] shrink-0">
<div className="w-9 h-9 rounded-xl bg-brand-accent/10 flex items-center justify-center text-brand-goldink dark:text-brand-accent shrink-0">
<MessageSquare size={18} />
</div>
<div>
<h2 className="text-base font-serif font-semibold text-[#1A1A1A] dark:text-white tracking-tight">
<h2 className="text-base font-serif font-semibold text-brand-dark dark:text-white tracking-tight">
{t('glossaries.context.title')}
</h2>
<p className="text-[11px] text-[#555555] dark:text-white/50 font-light mt-0.5">
<p className="text-xs text-muted-foreground font-light mt-0.5">
{t('glossaries.context.desc')}
</p>
</div>
@@ -206,7 +205,7 @@ export default function GlossariesPage() {
{t('glossaries.context.active')}
</span>
) : (
<span className="shrink-0 flex items-center gap-1.5 text-[10px] font-bold uppercase tracking-wider text-[#555555] bg-[#EBEBEB] dark:text-white/30 dark:bg-white/5 px-3 py-1.5 rounded-full border border-[#CCCCCC] dark:border-white/10">
<span className="shrink-0 flex items-center gap-1.5 text-[10px] font-bold uppercase tracking-wider text-muted-foreground bg-brand-muted dark:bg-white/5 px-3 py-1.5 rounded-full border border-black/5 dark:border-white/10">
{t('glossaries.context.inactive')}
</span>
)}
@@ -215,8 +214,8 @@ export default function GlossariesPage() {
{/* Chips de suggestions rapides */}
<div className="mb-5">
<div className="flex items-center gap-2 mb-3">
<Sparkles size={13} className="text-[#8B6F47]" />
<span className="text-[10px] font-bold uppercase tracking-widest text-[#555555] dark:text-white/40">
<Sparkles size={13} className="text-brand-goldink dark:text-brand-accent" />
<span className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground">
{t('glossaries.context.suggestions')}
</span>
</div>
@@ -224,8 +223,9 @@ export default function GlossariesPage() {
{CONTEXT_SUGGESTIONS.map((s) => (
<button
key={s.labelKey}
type="button"
onClick={() => handleAddSuggestion(s.valueKey)}
className="px-3 py-1.5 rounded-lg text-[11px] font-semibold text-[#3D3D3D] dark:text-white/70 bg-[#F0EDE8] dark:bg-white/5 border border-[#D9D5CE] dark:border-white/10 hover:bg-[#E8E2DA] hover:border-[#C5A17A] dark:hover:bg-brand-accent/10 dark:hover:border-brand-accent/30 transition-all cursor-pointer"
className="px-3 py-1.5 rounded-lg text-[11px] font-semibold text-brand-dark/80 dark:text-white/70 bg-brand-muted/70 dark:bg-white/5 border border-black/5 dark:border-white/10 hover:bg-brand-muted hover:border-brand-accent/40 dark:hover:bg-brand-accent/10 dark:hover:border-brand-accent/30 transition-all cursor-pointer"
>
+ {t(s.labelKey)}
</button>
@@ -239,29 +239,31 @@ export default function GlossariesPage() {
onChange={e => { setSystemPrompt(e.target.value); setPromptSaved(false); }}
placeholder={t('glossaries.context.placeholder')}
aria-label={t('glossaries.context.title')}
className="w-full h-44 p-4 bg-[#FAFAF8] dark:bg-white/[0.02] rounded-xl border border-[#D9D6D0] dark:border-white/10 text-[#1A1A1A] dark:text-white text-xs leading-relaxed focus:ring-2 focus:ring-brand-accent/30 focus:border-brand-accent/50 transition-all outline-none resize-y placeholder:text-[#AAAAAA] dark:placeholder:text-white/25"
className="w-full h-44 p-4 bg-brand-muted/20 dark:bg-black/20 rounded-xl border border-black/10 dark:border-white/10 text-brand-dark dark:text-white text-xs leading-relaxed focus:ring-2 focus:ring-brand-accent/30 focus:border-brand-accent/50 transition-all outline-none resize-y placeholder:text-muted-foreground/50"
/>
{/* Pied de section */}
<div className="flex justify-between items-center mt-4">
<p className="text-[10px] text-[#777777] dark:text-white/25 font-light">
<p className="text-[10px] text-muted-foreground font-light">
{systemPrompt.length > 0
? t('glossaries.context.chars', { count: systemPrompt.length })
: t('glossaries.context.noPrompt')}
</p>
<div className="flex gap-3">
<button
type="button"
onClick={handleClearPrompt}
disabled={!systemPrompt}
className="px-5 py-2.5 bg-[#EBEBEB] dark:bg-white/5 text-[#444444] dark:text-white/40 rounded-lg text-xs font-bold uppercase tracking-wider hover:text-[#1A1A1A] hover:bg-[#E0E0E0] dark:hover:text-white transition-all cursor-pointer disabled:opacity-40 disabled:cursor-not-allowed"
className="px-5 py-2.5 bg-brand-muted dark:bg-white/5 text-muted-foreground rounded-xl text-xs font-bold uppercase tracking-wider hover:text-brand-dark hover:bg-brand-muted/90 dark:hover:text-white transition-all cursor-pointer disabled:opacity-40 disabled:cursor-not-allowed"
>
<Trash2 size={12} className="inline mr-1.5" />
{t('glossaries.context.clear')}
</button>
<button
type="button"
onClick={handleSavePrompt}
disabled={isSavingPrompt || !promptHasUnsavedChanges}
className="premium-button px-8 py-2.5 text-xs uppercase tracking-widest !rounded-lg flex items-center gap-2 disabled:opacity-50 cursor-pointer font-bold"
className="premium-button px-8 py-2.5 text-xs uppercase tracking-widest !rounded-xl flex items-center gap-2 disabled:opacity-50 cursor-pointer font-bold"
>
{isSavingPrompt
? <><Loader2 size={14} className="animate-spin" /> {t('glossaries.context.saving')}</>
@@ -282,18 +284,18 @@ export default function GlossariesPage() {
{/* En-tête de section + compteur */}
<div className="flex items-center justify-between gap-4">
<div>
<h2 className="text-lg font-serif font-medium text-[#1A1A1A] dark:text-white tracking-tight">
<h2 className="text-lg font-serif font-medium text-brand-dark dark:text-white tracking-tight">
{t('glossaries.grid.title') || 'Vos'}{' '}
<span className="italic">{t('glossaries.grid.titleHighlight') || 'glossaires'}</span>
</h2>
<p className="text-[11px] text-[#555555] dark:text-white/40 font-light mt-1">
<p className="text-xs text-muted-foreground font-light mt-1">
{glossaries.length > 0
? t('glossaries.countMany', { count: glossaries.length })
: t('glossaries.countNone')}
</p>
</div>
{currentTargetInfo && (
<span className="flex items-center gap-1.5 text-[10px] font-bold text-[#444444] dark:text-white/40 bg-[#EBEBEB] dark:bg-white/5 border border-[#CCCCCC] dark:border-white/5 px-3 py-1.5 rounded-full shrink-0">
<span className="flex items-center gap-1.5 text-[10px] font-bold text-muted-foreground bg-brand-muted dark:bg-white/5 border border-black/5 dark:border-white/10 px-3 py-1.5 rounded-full shrink-0">
<span>{t('glossaries.activeTarget')}</span>
<span>{currentTargetInfo.flag} {currentTargetInfo.label}</span>
</span>
@@ -303,33 +305,33 @@ export default function GlossariesPage() {
{/* Barre de recherche (si > 3 glossaires) */}
{glossaries.length > 3 && (
<div className="relative">
<Search size={14} className="absolute left-3 top-1/2 -translate-y-1/2 text-[#888888] dark:text-white/30" />
<Search size={14} className="absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground" />
<input
type="text"
value={searchQuery}
onChange={e => setSearchQuery(e.target.value)}
placeholder={t('glossaries.searchPlaceholder')}
aria-label={t('glossaries.searchPlaceholder')}
className="w-full pl-9 pr-3 py-2.5 text-xs rounded-lg border border-[#D9D6D0] dark:border-white/10 bg-white dark:bg-[#141414] text-[#1A1A1A] dark:text-white placeholder:text-[#AAAAAA] dark:placeholder:text-white/25 focus:outline-none focus:ring-2 focus:ring-brand-accent/20 focus:border-brand-accent/30"
className="w-full pl-9 pr-3 py-2.5 text-xs rounded-xl border border-black/10 dark:border-white/10 bg-white dark:bg-[#141414] text-brand-dark dark:text-white placeholder:text-muted-foreground/50 focus:outline-none focus:ring-2 focus:ring-brand-accent/30 focus:border-brand-accent/50"
/>
</div>
)}
{/* État vide */}
{glossaries.length === 0 ? (
<div className="editorial-card p-16 bg-white dark:bg-[#141414] border border-black/5 dark:border-white/5 rounded-2xl shadow-sm text-center">
<div className="w-14 h-14 bg-[#F0EDE8] dark:bg-white/10 rounded-2xl flex items-center justify-center text-[#8B6F47] mx-auto mb-5">
<div className="editorial-card p-16 bg-white dark:bg-[#141414] border border-black/5 dark:border-white/5 rounded-2xl shadow-editorial text-center">
<div className="w-14 h-14 bg-brand-muted/70 dark:bg-white/10 rounded-2xl flex items-center justify-center text-brand-goldink dark:text-brand-accent mx-auto mb-5">
<Library size={28} />
</div>
<p className="text-base font-serif font-semibold text-[#1A1A1A] dark:text-white mb-2">
<p className="text-base font-serif font-semibold text-brand-dark dark:text-white mb-2">
{t('glossaries.empty') || 'Aucun glossaire'}
</p>
<p className="text-sm text-[#555555] dark:text-white/40 font-light mb-6 max-w-xs mx-auto">
<p className="text-xs text-muted-foreground font-light mb-6 max-w-xs mx-auto leading-relaxed">
{t('glossaries.emptyDesc') || 'Créez votre premier glossaire pour garantir une terminologie cohérente dans vos traductions.'}
</p>
<Link
href="/dashboard/glossaries/new"
className="inline-flex items-center gap-2 px-6 py-2.5 rounded-xl bg-[#1A1A1A] hover:bg-[#333333] text-white text-xs font-bold uppercase tracking-widest transition-all"
className="inline-flex items-center gap-2 px-6 py-2.5 rounded-xl bg-brand-dark hover:bg-brand-accent text-white dark:bg-brand-accent dark:text-brand-dark text-xs font-bold uppercase tracking-widest transition-all shadow-sm"
>
<Plus size={12} />
{t('glossaries.createFirst')}
@@ -337,7 +339,7 @@ export default function GlossariesPage() {
</div>
) : filteredGlossaries.length === 0 ? (
<div className="p-8 bg-white dark:bg-[#141414] border border-black/5 dark:border-white/5 rounded-2xl text-center">
<p className="text-sm text-[#555555] dark:text-white/40 font-light">
<p className="text-xs text-muted-foreground font-light">
{t('glossaries.noResults', { query: searchQuery })}
</p>
</div>
@@ -360,7 +362,7 @@ export default function GlossariesPage() {
? 'border-brand-accent/50 ring-1 ring-brand-accent/20 hover:border-brand-accent/70'
: mismatch
? 'border-amber-300/50 dark:border-amber-500/20 hover:border-amber-400/70 opacity-80 hover:opacity-100'
: 'border-[#E5E3DF] dark:border-white/5 hover:border-[#C5A17A]/40'
: 'border-black/5 dark:border-white/5 hover:border-brand-accent/40'
)}
>
{/* Badge compatible / autre langue */}
@@ -383,17 +385,17 @@ export default function GlossariesPage() {
aria-label={`${t('glossaries.card.edit')}${glossary.name}`}
>
<div className="flex items-start gap-3 mb-4">
<div className="w-10 h-10 bg-[#F0EDE8] dark:bg-white/10 rounded-xl flex items-center justify-center text-[#8B6F47] shrink-0">
<div className="w-10 h-10 bg-brand-muted/70 dark:bg-white/10 rounded-xl flex items-center justify-center text-brand-goldink dark:text-brand-accent shrink-0">
<Library size={18} />
</div>
<div className="flex-1 min-w-0 pt-0.5">
<h3 className="text-sm font-serif font-semibold text-[#1A1A1A] dark:text-white tracking-tight leading-snug line-clamp-2 group-hover:text-[#8B6F47] transition-colors">
<h3 className="text-sm font-serif font-semibold text-brand-dark dark:text-white tracking-tight leading-snug line-clamp-2 group-hover:text-brand-goldink dark:group-hover:text-brand-accent transition-colors">
{glossary.name}
</h3>
<p className="text-[11px] text-[#555555] dark:text-white/50 font-medium flex items-center gap-1 mt-0.5">
<p className="text-xs text-muted-foreground font-medium flex items-center gap-1 mt-0.5">
<span>{srcInfo?.flag ?? '🌐'}</span>
<span>{srcInfo?.label ?? glossary.source_language}</span>
<span className="text-[#7A5C35] dark:text-brand-accent font-bold"></span>
<span className="text-brand-goldink dark:text-brand-accent font-bold"></span>
<span>{tgtInfo?.flag ?? '🌐'}</span>
<span>{tgtInfo?.label ?? glossary.target_language}</span>
</p>
@@ -401,11 +403,11 @@ export default function GlossariesPage() {
</div>
<div className="flex justify-between items-center pb-4">
<span className="flex items-center gap-1 text-xs font-semibold text-[#333333] dark:text-white/65">
<Hash size={12} className="text-[#8B6F47] dark:text-brand-accent" />
<span className="flex items-center gap-1 text-xs font-semibold text-brand-dark/80 dark:text-white/70">
<Hash size={12} className="text-brand-goldink dark:text-brand-accent" />
{termCount > 1 ? t('glossaries.termsMany', { count: termCount }) : t('glossaries.termsOne')}
</span>
<span className="flex items-center gap-1 font-mono text-[10px] text-[#666666] dark:text-white/40">
<span className="flex items-center gap-1 font-mono text-[10px] text-muted-foreground">
<Calendar size={11} />
{formatDate(new Date(glossary.created_at), locale)}
</span>
@@ -413,16 +415,16 @@ export default function GlossariesPage() {
</button>
{/* Actions */}
<div className="flex gap-2 pt-4 border-t border-[#F0EDE8] dark:border-white/10 mt-auto shrink-0">
<div className="flex gap-2 pt-4 border-t border-black/5 dark:border-white/5 mt-auto shrink-0">
<Link
href={`/dashboard/glossaries/${glossary.id}`}
className="flex-none px-3 py-2 rounded-lg bg-[#EBEBEB] dark:bg-white/5 hover:bg-[#E0D9D1] text-[#333333] dark:text-white/50 hover:text-[#1A1A1A] text-[10px] font-bold uppercase tracking-wider transition-all border border-[#CCCCCC] dark:border-white/10"
className="flex-none px-3 py-2 rounded-xl bg-brand-muted dark:bg-white/5 hover:bg-brand-muted/80 text-brand-dark/70 dark:text-white/60 hover:text-brand-dark dark:hover:text-white text-[10px] font-bold uppercase tracking-wider transition-all border border-black/5 dark:border-white/10"
>
{t('glossaries.card.edit')}
</Link>
<Link
href={`/dashboard/translate?glossaryId=${glossary.id}`}
className="flex-1 text-center py-2 rounded-lg bg-[#1A1A1A] hover:bg-[#333333] text-white text-[10px] font-bold uppercase tracking-wider transition-all flex items-center justify-center gap-1.5"
className="flex-1 text-center py-2 rounded-xl bg-brand-dark hover:bg-brand-accent text-white dark:bg-brand-accent dark:text-brand-dark text-[10px] font-bold uppercase tracking-wider transition-all flex items-center justify-center gap-1.5 shadow-sm"
>
{t('glossaries.card.use')} <ArrowRight size={11} />
</Link>

View File

@@ -10,6 +10,7 @@ import {
FileUp,
Loader2,
Pencil,
Search,
Undo2,
} from 'lucide-react';
import { apiClient, API_BASE_URL } from '@/lib/apiClient';
@@ -17,7 +18,6 @@ import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { useToast } from '@/components/ui/toast';
import { useI18n } from '@/lib/i18n';
import { PageHeader } from '@/components/layout/page-header';
import { cn } from '@/lib/utils';
interface Segment {
@@ -90,7 +90,10 @@ export default function ReviewPage() {
const [savingId, setSavingId] = useState<string | null>(null);
const [isRebuilding, setIsRebuilding] = useState(false);
const [approveArmed, setApproveArmed] = useState(false);
const [approvingProgress, setApprovingProgress] = useState<{ current: number; total: number } | null>(null);
const [isImporting, setIsImporting] = useState(false);
const [statusFilter, setStatusFilter] = useState<'all' | 'pending' | 'approved' | 'edited'>('all');
const [searchQuery, setSearchQuery] = useState('');
const fileInputRef = useRef<HTMLInputElement>(null);
const load = useCallback(async () => {
@@ -107,7 +110,7 @@ export default function ReviewPage() {
} finally {
setIsLoading(false);
}
}, [jobId]);
}, [jobId, t]);
useEffect(() => {
if (jobId) load();
@@ -120,6 +123,19 @@ export default function ReviewPage() {
return { total: segments.length, pending, approved, edited };
}, [segments]);
const filteredSegments = useMemo(() => {
return segments.filter((seg) => {
if (statusFilter !== 'all' && seg.status !== statusFilter) return false;
if (searchQuery.trim()) {
const q = searchQuery.toLowerCase();
const src = (seg.source_text || '').toLowerCase();
const tgt = (seg.reviewed_text || seg.translated_text || '').toLowerCase();
return src.includes(q) || tgt.includes(q);
}
return true;
});
}, [segments, statusFilter, searchQuery]);
const patchSegment = async (
id: string,
body: { reviewed_text?: string; status?: string }
@@ -157,10 +173,16 @@ export default function ReviewPage() {
}
setApproveArmed(false);
const pending = segments.filter((s) => s.status === 'pending');
for (const seg of pending) {
// sequential is fine — server-side each is a tiny PATCH
if (pending.length === 0) return;
setApprovingProgress({ current: 0, total: pending.length });
for (let i = 0; i < pending.length; i++) {
const seg = pending[i];
await patchSegment(seg.id, { status: 'approved' });
setApprovingProgress({ current: i + 1, total: pending.length });
}
setApprovingProgress(null);
notify.success({
title: t('reviews.approvedTitle'),
description: t('reviews.approvedDesc', { count: pending.length }),
@@ -252,7 +274,7 @@ export default function ReviewPage() {
return (
<div className="space-y-6">
<div className="flex flex-wrap items-end justify-between gap-3">
<div className="flex flex-wrap items-end justify-between gap-4">
<div>
<span className="accent-pill mb-3 block w-fit">{t('reviews.pill')}</span>
<h1 className="mb-2 text-3xl font-serif font-medium leading-tight tracking-tight text-brand-dark dark:text-white">
@@ -267,16 +289,34 @@ export default function ReviewPage() {
edited: counts.edited,
})}
</p>
<p className="max-w-xl text-xs font-light text-brand-dark/50 dark:text-white/50">
<p className="max-w-xl text-xs font-light text-brand-dark/50 dark:text-white/50 mt-1">
{t('reviews.xliffHint')}
</p>
</div>
<div className="flex flex-wrap gap-2">
<Button variant={approveArmed ? 'default' : 'outline'} size="sm" onClick={approveAll} disabled={counts.pending === 0}>
<CheckCheck className="size-3.5" />
{approveArmed
? t('reviews.approveAllConfirm', { count: counts.pending })
: t('reviews.approveAll')}
<Button
variant={approveArmed ? 'default' : 'outline'}
size="sm"
onClick={approveAll}
disabled={counts.pending === 0 || !!approvingProgress}
>
{approvingProgress ? (
<>
<Loader2 className="size-3.5 animate-spin" />
{t('reviews.approvingProgress', {
current: approvingProgress.current,
total: approvingProgress.total,
}) || `Approbation (${approvingProgress.current}/${approvingProgress.total})…`}
</>
) : (
<>
<CheckCheck className="size-3.5" />
{approveArmed
? t('reviews.approveAllConfirm', { count: counts.pending })
: t('reviews.approveAll')}
</>
)}
</Button>
<Button variant="outline" size="sm" onClick={exportXliff} disabled={!segments.length}>
<FileDown className="size-3.5" /> XLIFF
@@ -313,119 +353,193 @@ export default function ReviewPage() {
</div>
)}
{/* ── Filtering and Search bar ──────────────────────────────── */}
{!isLoading && segments.length > 0 && (
<div className="flex flex-col sm:flex-row items-stretch sm:items-center justify-between gap-3 pt-2">
{/* Status tabs */}
<div className="flex flex-wrap gap-1.5 p-1 bg-brand-muted/70 dark:bg-white/5 rounded-xl border border-black/5 dark:border-white/10">
{[
{ id: 'all', label: t('reviews.filter.all') || 'Tous', count: counts.total },
{ id: 'pending', label: t('reviews.filter.pending') || 'À relire', count: counts.pending },
{ id: 'approved', label: t('reviews.filter.approved') || 'Approuvés', count: counts.approved },
{ id: 'edited', label: t('reviews.filter.edited') || 'Modifiés', count: counts.edited },
].map((tab) => (
<button
key={tab.id}
type="button"
onClick={() => setStatusFilter(tab.id as any)}
className={cn(
'px-3 py-1.5 rounded-lg text-xs font-bold transition-all flex items-center gap-2 cursor-pointer',
statusFilter === tab.id
? 'bg-white dark:bg-[#1a1a1a] text-brand-dark dark:text-white shadow-sm'
: 'text-brand-dark/50 dark:text-white/50 hover:text-brand-dark dark:hover:text-white'
)}
>
<span>{tab.label}</span>
<span className={cn(
'text-[10px] px-1.5 py-0.5 rounded-full font-semibold',
statusFilter === tab.id
? 'bg-brand-accent/20 text-brand-goldink dark:text-brand-accent'
: 'bg-black/5 dark:bg-white/10'
)}>
{tab.count}
</span>
</button>
))}
</div>
{/* Search box */}
<div className="relative min-w-[220px] max-w-sm">
<input
type="text"
placeholder={t('reviews.searchPlaceholder') || 'Rechercher dans les segments...'}
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="w-full text-xs px-3 py-2 pl-8 rounded-xl bg-white dark:bg-[#141414] border border-black/10 dark:border-white/10 text-brand-dark dark:text-white placeholder:text-brand-dark/30 dark:placeholder:text-white/30 outline-none focus:border-brand-accent/60"
/>
<Search size={14} className="absolute left-2.5 top-1/2 -translate-y-1/2 text-brand-dark/40 dark:text-white/40 pointer-events-none" />
</div>
</div>
)}
{isLoading ? (
<div className="flex items-center justify-center py-16">
<Loader2 className="size-6 animate-spin text-muted-foreground" />
</div>
) : (
<div className="overflow-hidden rounded-lg border border-border">
<div className="overflow-hidden rounded-xl border border-black/10 dark:border-white/10 bg-white dark:bg-[#141414] shadow-sm">
<table className="w-full text-sm">
<thead className="bg-muted/50 text-left text-xs uppercase tracking-wide text-muted-foreground">
<thead className="bg-brand-muted/40 dark:bg-white/5 text-left text-xs uppercase tracking-wider text-brand-dark/50 dark:text-white/50 border-b border-black/5 dark:border-white/5">
<tr>
<th className="w-1/2 px-4 py-2.5 font-medium">{t('reviews.col.source')}</th>
<th className="w-1/2 px-4 py-2.5 font-medium">{t('reviews.col.target')}</th>
<th className="w-40 px-4 py-2.5 font-medium">{t('reviews.col.status')}</th>
<th className="w-1/2 px-4 py-3 font-semibold">{t('reviews.col.source')}</th>
<th className="w-1/2 px-4 py-3 font-semibold">{t('reviews.col.target')}</th>
<th className="w-40 px-4 py-3 font-semibold">{t('reviews.col.status')}</th>
</tr>
</thead>
<tbody>
{segments.map((seg) => {
const isEditing = editingId === seg.id;
const finalText = seg.status === 'edited' && seg.reviewed_text
? seg.reviewed_text
: seg.translated_text;
return (
<tr key={seg.id} className="border-t border-border align-top">
<td className="px-4 py-3 text-muted-foreground">
{seg.source_text}
</td>
<td className="px-4 py-3">
{isEditing ? (
<div className="space-y-2">
<textarea
className="min-h-20 w-full rounded-md border border-input bg-background px-3 py-2 text-foreground focus:outline-none focus:ring-2 focus:ring-ring"
value={draft}
onChange={(e) => setDraft(e.target.value)}
/>
<div className="flex gap-2">
<Button
size="sm"
disabled={savingId === seg.id || !draft.trim()}
onClick={async () => {
await patchSegment(seg.id, {
reviewed_text: draft,
status: 'edited',
});
setEditingId(null);
{filteredSegments.length === 0 ? (
<tr>
<td colSpan={3} className="px-4 py-12 text-center text-xs text-brand-dark/40 dark:text-white/40 italic">
{t('reviews.noMatchingSegments') || 'Aucun segment ne correspond à votre filtre.'}
</td>
</tr>
) : (
filteredSegments.map((seg) => {
const isEditing = editingId === seg.id;
const finalText = seg.status === 'edited' && seg.reviewed_text
? seg.reviewed_text
: seg.translated_text;
return (
<tr key={seg.id} className="border-t border-black/5 dark:border-white/5 align-top hover:bg-brand-muted/20 dark:hover:bg-white/[0.02] transition-colors">
<td className="px-4 py-3 text-brand-dark/70 dark:text-white/70 leading-relaxed">
{seg.source_text}
</td>
<td className="px-4 py-3">
{isEditing ? (
<div className="space-y-2">
<textarea
className="min-h-20 w-full rounded-lg border border-black/10 dark:border-white/10 bg-brand-muted/20 dark:bg-black/20 px-3 py-2 text-foreground focus:outline-none focus:ring-2 focus:ring-brand-accent/40 text-sm leading-relaxed"
value={draft}
onChange={(e) => setDraft(e.target.value)}
onKeyDown={(e) => {
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
e.preventDefault();
if (draft.trim() && savingId !== seg.id) {
patchSegment(seg.id, {
reviewed_text: draft,
status: 'edited',
});
setEditingId(null);
}
}
}}
>
{savingId === seg.id ? (
<Loader2 className="size-3.5 animate-spin" />
) : (
<Check className="size-3.5" />
)}
{t('reviews.save')}
</Button>
<Button
size="sm"
variant="ghost"
onClick={() => setEditingId(null)}
>
{t('reviews.cancel')}
</Button>
/>
<div className="flex items-center justify-between">
<div className="flex gap-2">
<Button
size="sm"
disabled={savingId === seg.id || !draft.trim()}
onClick={async () => {
await patchSegment(seg.id, {
reviewed_text: draft,
status: 'edited',
});
setEditingId(null);
}}
>
{savingId === seg.id ? (
<Loader2 className="size-3.5 animate-spin" />
) : (
<Check className="size-3.5" />
)}
{t('reviews.save')}
</Button>
<Button
size="sm"
variant="ghost"
onClick={() => setEditingId(null)}
>
{t('reviews.cancel')}
</Button>
</div>
<span className="text-[10px] text-brand-dark/40 dark:text-white/40 italic hidden sm:inline">
{t('reviews.shortcutHint') || 'Ctrl + Entrée pour enregistrer'}
</span>
</div>
</div>
</div>
) : (
<div
className={cn(
'whitespace-pre-wrap',
seg.status === 'edited' && 'text-blue-600 dark:text-blue-400'
)}
>
{finalText || <span className="italic text-muted-foreground">{t('reviews.emptyCell')}</span>}
</div>
)}
</td>
<td className="px-4 py-3">
<div className="flex flex-col items-start gap-2">
{statusBadge(seg.status)}
{!isEditing && (
<div className="flex flex-wrap gap-1">
<Button
size="sm"
variant="ghost"
className="h-7 gap-1 px-2 text-[11px]"
onClick={() => {
setEditingId(seg.id);
setDraft(finalText);
}}
>
<Pencil className="size-3" /> {t('reviews.action.edit')}
</Button>
<Button
size="sm"
variant="ghost"
className="h-7 gap-1 px-2 text-[11px]"
disabled={seg.status === 'approved' || savingId === seg.id}
onClick={() => patchSegment(seg.id, { status: 'approved' })}
>
<Check className="size-3" /> {t('reviews.action.approveShort')}
</Button>
<Button
size="sm"
variant="ghost"
className="h-7 gap-1 px-2 text-[11px]"
disabled={seg.status === 'pending'}
onClick={() => patchSegment(seg.id, { status: 'pending' })}
>
<Undo2 className="size-3" /> {t('reviews.action.resetShort')}
</Button>
) : (
<div
className={cn(
'whitespace-pre-wrap leading-relaxed',
seg.status === 'edited' ? 'text-blue-600 dark:text-blue-400 font-medium' : 'text-brand-dark dark:text-white'
)}
>
{finalText || <span className="italic text-muted-foreground">{t('reviews.emptyCell')}</span>}
</div>
)}
</div>
</td>
</tr>
);
})}
</td>
<td className="px-4 py-3">
<div className="flex flex-col items-start gap-2">
{statusBadge(seg.status)}
{!isEditing && (
<div className="flex flex-wrap gap-1">
<Button
size="sm"
variant="ghost"
className="h-7 gap-1 px-2 text-[11px]"
onClick={() => {
setEditingId(seg.id);
setDraft(finalText);
}}
>
<Pencil className="size-3" /> {t('reviews.action.edit')}
</Button>
<Button
size="sm"
variant="ghost"
className="h-7 gap-1 px-2 text-[11px]"
disabled={seg.status === 'approved' || savingId === seg.id}
onClick={() => patchSegment(seg.id, { status: 'approved' })}
>
<Check className="size-3" /> {t('reviews.action.approveShort')}
</Button>
<Button
size="sm"
variant="ghost"
className="h-7 gap-1 px-2 text-[11px]"
disabled={seg.status === 'pending'}
onClick={() => patchSegment(seg.id, { status: 'pending' })}
>
<Undo2 className="size-3" /> {t('reviews.action.resetShort')}
</Button>
</div>
)}
</div>
</td>
</tr>
);
})
)}
</tbody>
</table>
</div>

View File

@@ -46,6 +46,7 @@ export default function TeamsPage() {
const [inviteEmail, setInviteEmail] = useState<Record<string, string>>({});
const [inviteRole, setInviteRole] = useState<Record<string, string>>({});
const [busyWs, setBusyWs] = useState<string | null>(null);
const [confirmDeleteMember, setConfirmDeleteMember] = useState<{ wsId: string; userId: string } | null>(null);
const load = useCallback(async () => {
setIsLoading(true);
@@ -185,26 +186,57 @@ export default function TeamsPage() {
</div>
<ul className="divide-y divide-border rounded-md border border-border">
{ws.members.map((m) => (
<li key={m.id} className="flex items-center justify-between px-3 py-2 text-sm">
<span className="flex items-center gap-2">
<span className="text-muted-foreground">{m.user_id.slice(0, 8)}</span>
<Badge variant="outline">{m.role}</Badge>
</span>
{m.role !== 'owner' && (
<Button
size="sm"
variant="ghost"
className="h-7 px-2 text-red-500"
disabled={busyWs === ws.id || ws.my_role === 'member'}
onClick={() => removeMember(ws.id, m.user_id)}
title={t('teams.removeMember')}
>
<Trash2 className="size-3.5" />
</Button>
)}
</li>
))}
{ws.members.map((m) => {
const isConfirming = confirmDeleteMember?.wsId === ws.id && confirmDeleteMember?.userId === m.user_id;
return (
<li key={m.id} className="flex items-center justify-between px-3 py-2 text-sm">
<span className="flex items-center gap-2">
<span className="text-muted-foreground">{m.user_id.slice(0, 8)}</span>
<Badge variant="outline">{m.role}</Badge>
</span>
{m.role !== 'owner' && (
isConfirming ? (
<div className="flex items-center gap-1.5 animate-fade-in">
<span className="text-xs text-red-500 font-medium mr-1">
{t('teams.removeConfirm')}
</span>
<Button
size="sm"
variant="destructive"
className="h-6 px-2 text-[10px]"
disabled={busyWs === ws.id}
onClick={async () => {
await removeMember(ws.id, m.user_id);
setConfirmDeleteMember(null);
}}
>
{t('teams.confirm')}
</Button>
<Button
size="sm"
variant="ghost"
className="h-6 px-2 text-[10px]"
onClick={() => setConfirmDeleteMember(null)}
>
{t('teams.cancel')}
</Button>
</div>
) : (
<Button
size="sm"
variant="ghost"
className="h-7 px-2 text-red-500"
disabled={busyWs === ws.id || ws.my_role === 'member'}
onClick={() => setConfirmDeleteMember({ wsId: ws.id, userId: m.user_id })}
title={t('teams.removeMember')}
>
<Trash2 className="size-3.5" />
</Button>
)
)}
</li>
);
})}
</ul>
{(ws.my_role === 'owner' || ws.my_role === 'admin') && (

View File

@@ -118,7 +118,7 @@ function Combobox({
<div
role="listbox"
aria-label={ariaLabel}
className="absolute top-[102%] right-0 left-0 bg-white dark:bg-[#1a1a1a] border border-black/10 dark:border-white/10 rounded-xl shadow-2xl p-2 z-50 max-h-48 overflow-y-auto animate-fade-in"
className="absolute top-[102%] right-0 left-0 bg-white dark:bg-[#1a1a1a] border border-black/10 dark:border-white/10 rounded-xl shadow-2xl p-2 z-50 animate-fade-in"
>
<div className="border-b border-black/5 dark:border-white/5 px-2 py-1.5">
<input
@@ -131,7 +131,7 @@ function Combobox({
className="w-full bg-transparent px-1 py-1 text-xs outline-none placeholder:text-brand-dark/30 dark:placeholder:text-white/30 text-brand-dark dark:text-white"
/>
</div>
<div className="max-h-[160px] overflow-y-auto p-1 mt-1 space-y-0.5">
<div className="max-h-52 overflow-y-auto p-1 mt-1 space-y-0.5">
{filtered.length === 0 && (
<div className="px-3 py-3 text-center text-xs text-brand-dark/40 dark:text-white/40">{t('langSelector.noResults')}</div>
)}

View File

@@ -623,6 +623,9 @@ export default function TranslatePage() {
{upload.files.length > 0 && !config.targetLang && (
<p className="text-center text-[10px] text-brand-dark/50 dark:text-white/50 font-semibold uppercase tracking-wider">{t('translate.chooseTargetLang')}</p>
)}
{upload.files.length > 0 && config.targetLang && config.sourceLang !== 'auto' && config.sourceLang === config.targetLang && (
<p className="text-center text-[10px] text-amber-600 dark:text-amber-400 font-semibold uppercase tracking-wider">{t('translate.sameLanguageError')}</p>
)}
<div className="flex justify-between gap-4 text-[11px] font-semibold uppercase tracking-[0.06em] text-brand-dark/55 dark:text-white/55 border-t border-black/5 dark:border-white/5 pt-4">
<span className="flex items-center gap-1.5">
@@ -1063,6 +1066,9 @@ export default function TranslatePage() {
<>{t('translate.startTranslation')} <ArrowRight size={13} className={upload.file ? 'text-brand-goldink dark:text-brand-accent' : 'opacity-20'} /></>
)}
</button>
{upload.files.length > 0 && config.targetLang && config.sourceLang !== 'auto' && config.sourceLang === config.targetLang && (
<p className="text-center text-[10px] text-amber-600 dark:text-amber-400 font-semibold uppercase tracking-wider mt-2">{t('translate.sameLanguageError')}</p>
)}
</div>
)}
</div>

View File

@@ -428,7 +428,7 @@ const PricingSection = () => {
</ul>
<Link
href={p.popular ? "/auth/register" : `/pricing?plan=${p.name.toLowerCase()}${isAnnual ? '&billing=yearly' : ''}`}
href={p.popular ? "/auth/register" : `/pricing?plan=${p.id}${isAnnual ? '&billing=yearly' : ''}`}
className={`w-full py-4 rounded-2xl text-xs font-bold uppercase tracking-widest transition-all leading-none text-center block ${p.popular ? 'bg-brand-dark dark:bg-brand-accent text-white dark:text-brand-dark shadow-xl' : 'bg-transparent text-brand-dark dark:text-white border border-black/10 dark:border-white/10 hover:bg-black/5 dark:hover:bg-white/5'}`}
>
{p.cta}

View File

@@ -20,8 +20,10 @@ import profile from "./profile.json";
import providerSelector from "./providerSelector.json";
import providerTheme from "./providerTheme.json";
import register from "./register.json";
import reviews from "./reviews.json";
import services from "./services.json";
import settings from "./settings.json";
import teams from "./teams.json";
import translate from "./translate.json";
import translateComplete from "./translateComplete.json";
@@ -45,8 +47,10 @@ const messages: Record<string, string> = {
...providerSelector,
...providerTheme,
...register,
...reviews,
...services,
...settings,
...teams,
...translate,
...translateComplete,
};

View File

@@ -0,0 +1,51 @@
{
"reviews.title": "Review",
"reviews.subtitle": "{file} — {total} segments ({pending} to review, {approved} approved, {edited} edited)",
"reviews.approveAll": "Approve all",
"reviews.importXliff": "Import XLIFF",
"reviews.rebuild": "Rebuild & download",
"reviews.col.source": "Source",
"reviews.col.target": "Translation",
"reviews.col.status": "Status",
"reviews.status.approved": "Approved",
"reviews.status.edited": "Edited",
"reviews.status.pending": "To review",
"reviews.save": "Save",
"reviews.cancel": "Cancel",
"reviews.emptyCell": "— empty —",
"reviews.action.edit": "Edit translation",
"reviews.action.approve": "Approve as-is",
"reviews.action.reset": "Reset to pending",
"reviews.approvedTitle": "Segments approved",
"reviews.approvedDesc": "{count} segment(s) approved.",
"reviews.rebuiltTitle": "Document rebuilt",
"reviews.rebuiltDesc": "{count} reviewed segment(s) applied.",
"reviews.rebuildFailed": "Rebuild failed",
"reviews.xliffExportFailed": "XLIFF export failed",
"reviews.xliffImported": "XLIFF imported",
"reviews.xliffImportedDesc": "{count} segment(s) updated.",
"reviews.xliffImportFailed": "XLIFF import failed",
"reviews.error.title": "Error",
"reviews.error.load": "Load error",
"reviews.error.update": "Update failed",
"reviews.error.download": "Download failed (HTTP {status})",
"reviews.pill": "Quality control",
"reviews.titleBase": "Translation",
"reviews.titleAccent": "review",
"reviews.approveAllConfirm": "Confirm ({count})",
"reviews.action.approveShort": "OK",
"reviews.action.resetShort": "To review",
"reviews.xliffHint": "XLIFF is the standard exchange format for translation reviews — export segments, edit in your CAT tool, then reimport.",
"reviews.error.sessionExpired": "Session expired. Sign in again and refresh.",
"reviews.error.notFound": "Job not found.",
"reviews.error.rateLimited": "Rate limited. Wait a few seconds.",
"reviews.error.server": "Server error. Try again shortly.",
"reviews.filter.all": "All",
"reviews.filter.pending": "To review",
"reviews.filter.approved": "Approved",
"reviews.filter.edited": "Edited",
"reviews.searchPlaceholder": "Search in segments...",
"reviews.shortcutHint": "Ctrl + Enter to save",
"reviews.approvingProgress": "Approving ({current}/{total})…",
"reviews.noMatchingSegments": "No segments match your filter."
}

View File

@@ -0,0 +1,32 @@
{
"teams.title": "Workspaces",
"teams.subtitle": "Share your glossaries with your team (Business plan — 5 seats).",
"teams.needsBusiness": "Workspaces require the Business plan. Upgrade to collaborate with your team.",
"teams.newWorkspace": "New workspace",
"teams.newPlaceholder": "e.g. Translation team",
"teams.create": "Create",
"teams.created": "Workspace created",
"teams.createdDesc": "You are the owner.",
"teams.createFailed": "Could not create",
"teams.memberAdded": "Member added",
"teams.addFailed": "Could not add",
"teams.removeFailed": "Could not remove",
"teams.removeMember": "Remove from workspace",
"teams.owner": "Owner",
"teams.seats": "{count} / {limit} seats",
"teams.seatsUnlimited": "{count} seats (unlimited)",
"teams.role.member": "Member",
"teams.role.admin": "Admin",
"teams.addMember": "Add a member (email)",
"teams.emailPlaceholder": "colleague@company.com",
"teams.invite": "Invite",
"teams.empty": "No workspace yet — create the first one above.",
"teams.error.title": "Error",
"teams.error.load": "Could not load workspaces",
"teams.pill": "Collaboration",
"teams.titleBase": "Workspaces",
"teams.titleAccent": "team",
"teams.removeConfirm": "Are you sure you want to remove this member?",
"teams.cancel": "Cancel",
"teams.confirm": "Confirm"
}

View File

@@ -88,5 +88,6 @@
"translate.preserveLayoutDesc": "الحفاظ على التخطيط",
"translate.textOnly": "نص فقط",
"translate.textOnlyDesc": "ترجمة سريعة للنص فقط",
"translate.unavailableStandard": "غير متاح في الوضع القياسي (AI فقط)"
"translate.unavailableStandard": "غير متاح في الوضع القياسي (AI فقط)",
"translate.sameLanguageError": "يجب أن تختلف اللغة المصدر عن اللغة الهدف"
}

View File

@@ -22,8 +22,10 @@ import providerSelector from "./providerSelector.json";
import providerTheme from "./providerTheme.json";
import register from "./register.json";
import resetPassword from "./resetPassword.json";
import reviews from "./reviews.json";
import services from "./services.json";
import settings from "./settings.json";
import teams from "./teams.json";
import translate from "./translate.json";
import translateComplete from "./translateComplete.json";
@@ -49,8 +51,10 @@ const messages: Record<string, string> = {
...providerTheme,
...register,
...resetPassword,
...reviews,
...services,
...settings,
...teams,
...translate,
...translateComplete,
};

View File

@@ -0,0 +1,51 @@
{
"reviews.title": "Review",
"reviews.subtitle": "{file} — {total} segments ({pending} to review, {approved} approved, {edited} edited)",
"reviews.approveAll": "Approve all",
"reviews.importXliff": "Import XLIFF",
"reviews.rebuild": "Rebuild & download",
"reviews.col.source": "Source",
"reviews.col.target": "Translation",
"reviews.col.status": "Status",
"reviews.status.approved": "Approved",
"reviews.status.edited": "Edited",
"reviews.status.pending": "To review",
"reviews.save": "Save",
"reviews.cancel": "Cancel",
"reviews.emptyCell": "— empty —",
"reviews.action.edit": "Edit translation",
"reviews.action.approve": "Approve as-is",
"reviews.action.reset": "Reset to pending",
"reviews.approvedTitle": "Segments approved",
"reviews.approvedDesc": "{count} segment(s) approved.",
"reviews.rebuiltTitle": "Document rebuilt",
"reviews.rebuiltDesc": "{count} reviewed segment(s) applied.",
"reviews.rebuildFailed": "Rebuild failed",
"reviews.xliffExportFailed": "XLIFF export failed",
"reviews.xliffImported": "XLIFF imported",
"reviews.xliffImportedDesc": "{count} segment(s) updated.",
"reviews.xliffImportFailed": "XLIFF import failed",
"reviews.error.title": "Error",
"reviews.error.load": "Load error",
"reviews.error.update": "Update failed",
"reviews.error.download": "Download failed (HTTP {status})",
"reviews.pill": "Quality control",
"reviews.titleBase": "Translation",
"reviews.titleAccent": "review",
"reviews.approveAllConfirm": "Confirm ({count})",
"reviews.action.approveShort": "OK",
"reviews.action.resetShort": "To review",
"reviews.xliffHint": "XLIFF is the standard exchange format for translation reviews — export segments, edit in your CAT tool, then reimport.",
"reviews.error.sessionExpired": "Session expired. Sign in again and refresh.",
"reviews.error.notFound": "Job not found.",
"reviews.error.rateLimited": "Rate limited. Wait a few seconds.",
"reviews.error.server": "Server error. Try again shortly.",
"reviews.filter.all": "All",
"reviews.filter.pending": "To review",
"reviews.filter.approved": "Approved",
"reviews.filter.edited": "Edited",
"reviews.searchPlaceholder": "Search in segments...",
"reviews.shortcutHint": "Ctrl + Enter to save",
"reviews.approvingProgress": "Approving ({current}/{total})…",
"reviews.noMatchingSegments": "No segments match your filter."
}

View File

@@ -0,0 +1,32 @@
{
"teams.title": "Workspaces",
"teams.subtitle": "Share your glossaries with your team (Business plan — 5 seats).",
"teams.needsBusiness": "Workspaces require the Business plan. Upgrade to collaborate with your team.",
"teams.newWorkspace": "New workspace",
"teams.newPlaceholder": "e.g. Translation team",
"teams.create": "Create",
"teams.created": "Workspace created",
"teams.createdDesc": "You are the owner.",
"teams.createFailed": "Could not create",
"teams.memberAdded": "Member added",
"teams.addFailed": "Could not add",
"teams.removeFailed": "Could not remove",
"teams.removeMember": "Remove from workspace",
"teams.owner": "Owner",
"teams.seats": "{count} / {limit} seats",
"teams.seatsUnlimited": "{count} seats (unlimited)",
"teams.role.member": "Member",
"teams.role.admin": "Admin",
"teams.addMember": "Add a member (email)",
"teams.emailPlaceholder": "colleague@company.com",
"teams.invite": "Invite",
"teams.empty": "No workspace yet — create the first one above.",
"teams.error.title": "Error",
"teams.error.load": "Could not load workspaces",
"teams.pill": "Collaboration",
"teams.titleBase": "Workspaces",
"teams.titleAccent": "team",
"teams.removeConfirm": "Are you sure you want to remove this member?",
"teams.cancel": "Cancel",
"teams.confirm": "Confirm"
}

View File

@@ -88,5 +88,6 @@
"translate.preserveLayoutDesc": "Layout beibehalten",
"translate.textOnly": "Nur Text",
"translate.textOnlyDesc": "Schnelle Übersetzung nur des Textes",
"translate.unavailableStandard": "Im Standardmodus nicht verfügbar (nur KI)"
"translate.unavailableStandard": "Im Standardmodus nicht verfügbar (nur KI)",
"translate.sameLanguageError": "Ausgangs- und Zielsprache müssen sich unterscheiden"
}

View File

@@ -13,9 +13,9 @@
"reviews.save": "Save",
"reviews.cancel": "Cancel",
"reviews.emptyCell": "— empty —",
"reviews.action.edit": "Edit the translation",
"reviews.action.approve": "Approve as is",
"reviews.action.reset": "Set back to review",
"reviews.action.edit": "Edit translation",
"reviews.action.approve": "Approve as-is",
"reviews.action.reset": "Reset to pending",
"reviews.approvedTitle": "Segments approved",
"reviews.approvedDesc": "{count} segment(s) approved.",
"reviews.rebuiltTitle": "Document rebuilt",
@@ -26,18 +26,26 @@
"reviews.xliffImportedDesc": "{count} segment(s) updated.",
"reviews.xliffImportFailed": "XLIFF import failed",
"reviews.error.title": "Error",
"reviews.error.load": "Loading failed",
"reviews.error.update": "Could not update",
"reviews.error.load": "Load error",
"reviews.error.update": "Update failed",
"reviews.error.download": "Download failed (HTTP {status})",
"reviews.pill": "Quality control",
"reviews.titleBase": "Translation",
"reviews.titleAccent": "review",
"reviews.approveAllConfirm": "Confirm ({count})",
"reviews.action.approveShort": "OK",
"reviews.action.resetShort": "Re-review",
"reviews.xliffHint": "XLIFF is the standard exchange format for translation reviews — export the segments, correct them in your own tool or send them to a reviewer, then import them back.",
"reviews.error.sessionExpired": "Your session has expired. Log in again and reload this page.",
"reviews.error.notFound": "This job no longer exists. It may have been cleaned up.",
"reviews.error.rateLimited": "Too many requests — wait a few seconds and retry.",
"reviews.error.server": "The server could not process this. Try again in a moment."
"reviews.action.resetShort": "To review",
"reviews.xliffHint": "XLIFF is the standard exchange format for translation reviews — export segments, edit in your CAT tool, then reimport.",
"reviews.error.sessionExpired": "Session expired. Sign in again and refresh.",
"reviews.error.notFound": "Job not found.",
"reviews.error.rateLimited": "Rate limited. Wait a few seconds.",
"reviews.error.server": "Server error. Try again shortly.",
"reviews.filter.all": "All",
"reviews.filter.pending": "To review",
"reviews.filter.approved": "Approved",
"reviews.filter.edited": "Edited",
"reviews.searchPlaceholder": "Search in segments...",
"reviews.shortcutHint": "Ctrl + Enter to save",
"reviews.approvingProgress": "Approving ({current}/{total})…",
"reviews.noMatchingSegments": "No segments match your filter."
}

View File

@@ -22,8 +22,11 @@
"teams.invite": "Invite",
"teams.empty": "No workspace yet — create the first one above.",
"teams.error.title": "Error",
"teams.error.load": "Could not load",
"teams.error.load": "Could not load workspaces",
"teams.pill": "Collaboration",
"teams.titleBase": "Team",
"teams.titleAccent": "workspaces"
"teams.titleBase": "Workspaces",
"teams.titleAccent": "team",
"teams.removeConfirm": "Are you sure you want to remove this member?",
"teams.cancel": "Cancel",
"teams.confirm": "Confirm"
}

View File

@@ -108,5 +108,6 @@
"translate.batch.doneTitle": "{done}/{total} translated",
"translate.batch.stop": "Stop the queue",
"translate.batch.downloadAll": "Download all",
"translate.batch.newSession": "New session"
"translate.batch.newSession": "New session",
"translate.sameLanguageError": "Source and target languages must be different"
}

View File

@@ -22,8 +22,10 @@ import providerSelector from "./providerSelector.json";
import providerTheme from "./providerTheme.json";
import register from "./register.json";
import resetPassword from "./resetPassword.json";
import reviews from "./reviews.json";
import services from "./services.json";
import settings from "./settings.json";
import teams from "./teams.json";
import translate from "./translate.json";
import translateComplete from "./translateComplete.json";
@@ -49,8 +51,10 @@ const messages: Record<string, string> = {
...providerTheme,
...register,
...resetPassword,
...reviews,
...services,
...settings,
...teams,
...translate,
...translateComplete,
};

View File

@@ -0,0 +1,51 @@
{
"reviews.title": "Review",
"reviews.subtitle": "{file} — {total} segments ({pending} to review, {approved} approved, {edited} edited)",
"reviews.approveAll": "Approve all",
"reviews.importXliff": "Import XLIFF",
"reviews.rebuild": "Rebuild & download",
"reviews.col.source": "Source",
"reviews.col.target": "Translation",
"reviews.col.status": "Status",
"reviews.status.approved": "Approved",
"reviews.status.edited": "Edited",
"reviews.status.pending": "To review",
"reviews.save": "Save",
"reviews.cancel": "Cancel",
"reviews.emptyCell": "— empty —",
"reviews.action.edit": "Edit translation",
"reviews.action.approve": "Approve as-is",
"reviews.action.reset": "Reset to pending",
"reviews.approvedTitle": "Segments approved",
"reviews.approvedDesc": "{count} segment(s) approved.",
"reviews.rebuiltTitle": "Document rebuilt",
"reviews.rebuiltDesc": "{count} reviewed segment(s) applied.",
"reviews.rebuildFailed": "Rebuild failed",
"reviews.xliffExportFailed": "XLIFF export failed",
"reviews.xliffImported": "XLIFF imported",
"reviews.xliffImportedDesc": "{count} segment(s) updated.",
"reviews.xliffImportFailed": "XLIFF import failed",
"reviews.error.title": "Error",
"reviews.error.load": "Load error",
"reviews.error.update": "Update failed",
"reviews.error.download": "Download failed (HTTP {status})",
"reviews.pill": "Quality control",
"reviews.titleBase": "Translation",
"reviews.titleAccent": "review",
"reviews.approveAllConfirm": "Confirm ({count})",
"reviews.action.approveShort": "OK",
"reviews.action.resetShort": "To review",
"reviews.xliffHint": "XLIFF is the standard exchange format for translation reviews — export segments, edit in your CAT tool, then reimport.",
"reviews.error.sessionExpired": "Session expired. Sign in again and refresh.",
"reviews.error.notFound": "Job not found.",
"reviews.error.rateLimited": "Rate limited. Wait a few seconds.",
"reviews.error.server": "Server error. Try again shortly.",
"reviews.filter.all": "All",
"reviews.filter.pending": "To review",
"reviews.filter.approved": "Approved",
"reviews.filter.edited": "Edited",
"reviews.searchPlaceholder": "Search in segments...",
"reviews.shortcutHint": "Ctrl + Enter to save",
"reviews.approvingProgress": "Approving ({current}/{total})…",
"reviews.noMatchingSegments": "No segments match your filter."
}

View File

@@ -0,0 +1,32 @@
{
"teams.title": "Workspaces",
"teams.subtitle": "Share your glossaries with your team (Business plan — 5 seats).",
"teams.needsBusiness": "Workspaces require the Business plan. Upgrade to collaborate with your team.",
"teams.newWorkspace": "New workspace",
"teams.newPlaceholder": "e.g. Translation team",
"teams.create": "Create",
"teams.created": "Workspace created",
"teams.createdDesc": "You are the owner.",
"teams.createFailed": "Could not create",
"teams.memberAdded": "Member added",
"teams.addFailed": "Could not add",
"teams.removeFailed": "Could not remove",
"teams.removeMember": "Remove from workspace",
"teams.owner": "Owner",
"teams.seats": "{count} / {limit} seats",
"teams.seatsUnlimited": "{count} seats (unlimited)",
"teams.role.member": "Member",
"teams.role.admin": "Admin",
"teams.addMember": "Add a member (email)",
"teams.emailPlaceholder": "colleague@company.com",
"teams.invite": "Invite",
"teams.empty": "No workspace yet — create the first one above.",
"teams.error.title": "Error",
"teams.error.load": "Could not load workspaces",
"teams.pill": "Collaboration",
"teams.titleBase": "Workspaces",
"teams.titleAccent": "team",
"teams.removeConfirm": "Are you sure you want to remove this member?",
"teams.cancel": "Cancel",
"teams.confirm": "Confirm"
}

View File

@@ -88,5 +88,6 @@
"translate.preserveLayoutDesc": "Conservar el diseño",
"translate.textOnly": "Solo texto",
"translate.textOnlyDesc": "Traducción rápida solo de texto",
"translate.unavailableStandard": "No disponible en modo Estándar (solo IA)"
"translate.unavailableStandard": "No disponible en modo Estándar (solo IA)",
"translate.sameLanguageError": "El idioma de origen y el de destino deben ser diferentes"
}

View File

@@ -20,8 +20,10 @@ import profile from "./profile.json";
import providerSelector from "./providerSelector.json";
import providerTheme from "./providerTheme.json";
import register from "./register.json";
import reviews from "./reviews.json";
import services from "./services.json";
import settings from "./settings.json";
import teams from "./teams.json";
import translate from "./translate.json";
import translateComplete from "./translateComplete.json";
@@ -45,8 +47,10 @@ const messages: Record<string, string> = {
...providerSelector,
...providerTheme,
...register,
...reviews,
...services,
...settings,
...teams,
...translate,
...translateComplete,
};

View File

@@ -0,0 +1,51 @@
{
"reviews.title": "Review",
"reviews.subtitle": "{file} — {total} segments ({pending} to review, {approved} approved, {edited} edited)",
"reviews.approveAll": "Approve all",
"reviews.importXliff": "Import XLIFF",
"reviews.rebuild": "Rebuild & download",
"reviews.col.source": "Source",
"reviews.col.target": "Translation",
"reviews.col.status": "Status",
"reviews.status.approved": "Approved",
"reviews.status.edited": "Edited",
"reviews.status.pending": "To review",
"reviews.save": "Save",
"reviews.cancel": "Cancel",
"reviews.emptyCell": "— empty —",
"reviews.action.edit": "Edit translation",
"reviews.action.approve": "Approve as-is",
"reviews.action.reset": "Reset to pending",
"reviews.approvedTitle": "Segments approved",
"reviews.approvedDesc": "{count} segment(s) approved.",
"reviews.rebuiltTitle": "Document rebuilt",
"reviews.rebuiltDesc": "{count} reviewed segment(s) applied.",
"reviews.rebuildFailed": "Rebuild failed",
"reviews.xliffExportFailed": "XLIFF export failed",
"reviews.xliffImported": "XLIFF imported",
"reviews.xliffImportedDesc": "{count} segment(s) updated.",
"reviews.xliffImportFailed": "XLIFF import failed",
"reviews.error.title": "Error",
"reviews.error.load": "Load error",
"reviews.error.update": "Update failed",
"reviews.error.download": "Download failed (HTTP {status})",
"reviews.pill": "Quality control",
"reviews.titleBase": "Translation",
"reviews.titleAccent": "review",
"reviews.approveAllConfirm": "Confirm ({count})",
"reviews.action.approveShort": "OK",
"reviews.action.resetShort": "To review",
"reviews.xliffHint": "XLIFF is the standard exchange format for translation reviews — export segments, edit in your CAT tool, then reimport.",
"reviews.error.sessionExpired": "Session expired. Sign in again and refresh.",
"reviews.error.notFound": "Job not found.",
"reviews.error.rateLimited": "Rate limited. Wait a few seconds.",
"reviews.error.server": "Server error. Try again shortly.",
"reviews.filter.all": "All",
"reviews.filter.pending": "To review",
"reviews.filter.approved": "Approved",
"reviews.filter.edited": "Edited",
"reviews.searchPlaceholder": "Search in segments...",
"reviews.shortcutHint": "Ctrl + Enter to save",
"reviews.approvingProgress": "Approving ({current}/{total})…",
"reviews.noMatchingSegments": "No segments match your filter."
}

View File

@@ -0,0 +1,32 @@
{
"teams.title": "Workspaces",
"teams.subtitle": "Share your glossaries with your team (Business plan — 5 seats).",
"teams.needsBusiness": "Workspaces require the Business plan. Upgrade to collaborate with your team.",
"teams.newWorkspace": "New workspace",
"teams.newPlaceholder": "e.g. Translation team",
"teams.create": "Create",
"teams.created": "Workspace created",
"teams.createdDesc": "You are the owner.",
"teams.createFailed": "Could not create",
"teams.memberAdded": "Member added",
"teams.addFailed": "Could not add",
"teams.removeFailed": "Could not remove",
"teams.removeMember": "Remove from workspace",
"teams.owner": "Owner",
"teams.seats": "{count} / {limit} seats",
"teams.seatsUnlimited": "{count} seats (unlimited)",
"teams.role.member": "Member",
"teams.role.admin": "Admin",
"teams.addMember": "Add a member (email)",
"teams.emailPlaceholder": "colleague@company.com",
"teams.invite": "Invite",
"teams.empty": "No workspace yet — create the first one above.",
"teams.error.title": "Error",
"teams.error.load": "Could not load workspaces",
"teams.pill": "Collaboration",
"teams.titleBase": "Workspaces",
"teams.titleAccent": "team",
"teams.removeConfirm": "Are you sure you want to remove this member?",
"teams.cancel": "Cancel",
"teams.confirm": "Confirm"
}

View File

@@ -88,5 +88,6 @@
"translate.preserveLayoutDesc": "حفظ چیدمان",
"translate.textOnly": "فقط متن",
"translate.textOnlyDesc": "ترجمه سریع فقط متن",
"translate.unavailableStandard": "در حالت استاندارد موجود نیست (فقط AI)"
"translate.unavailableStandard": "در حالت استاندارد موجود نیست (فقط AI)",
"translate.sameLanguageError": "زبان مبدأ و مقصد باید متفاوت باشند"
}

View File

@@ -39,5 +39,13 @@
"reviews.error.sessionExpired": "Votre session a expiré. Reconnectez-vous puis rechargez cette page.",
"reviews.error.notFound": "Ce job n'existe plus. Il a peut-être été nettoyé.",
"reviews.error.rateLimited": "Trop de requêtes — attendez quelques secondes et réessayez.",
"reviews.error.server": "Le serveur n'a pas pu traiter la demande. Réessayez dans un instant."
"reviews.error.server": "Le serveur n'a pas pu traiter la demande. Réessayez dans un instant.",
"reviews.filter.all": "Tous",
"reviews.filter.pending": "À relire",
"reviews.filter.approved": "Approuvés",
"reviews.filter.edited": "Modifiés",
"reviews.searchPlaceholder": "Rechercher dans les segments...",
"reviews.shortcutHint": "Ctrl + Entrée pour enregistrer",
"reviews.approvingProgress": "Approbation ({current}/{total})…",
"reviews.noMatchingSegments": "Aucun segment ne correspond à votre filtre."
}

View File

@@ -25,5 +25,8 @@
"teams.error.load": "Chargement impossible",
"teams.pill": "Collaboration",
"teams.titleBase": "Espaces de",
"teams.titleAccent": "travail"
"teams.titleAccent": "travail",
"teams.removeConfirm": "Êtes-vous sûr de vouloir retirer ce membre ?",
"teams.cancel": "Annuler",
"teams.confirm": "Confirmer"
}

View File

@@ -108,5 +108,6 @@
"translate.batch.doneTitle": "{done}/{total} traduits",
"translate.batch.stop": "Arrêter la file",
"translate.batch.downloadAll": "Tout télécharger",
"translate.batch.newSession": "Nouvelle session"
"translate.batch.newSession": "Nouvelle session",
"translate.sameLanguageError": "La langue source et la langue cible doivent être différentes"
}

View File

@@ -22,8 +22,10 @@ import providerSelector from "./providerSelector.json";
import providerTheme from "./providerTheme.json";
import register from "./register.json";
import resetPassword from "./resetPassword.json";
import reviews from "./reviews.json";
import services from "./services.json";
import settings from "./settings.json";
import teams from "./teams.json";
import translate from "./translate.json";
import translateComplete from "./translateComplete.json";
@@ -49,8 +51,10 @@ const messages: Record<string, string> = {
...providerTheme,
...register,
...resetPassword,
...reviews,
...services,
...settings,
...teams,
...translate,
...translateComplete,
};

View File

@@ -0,0 +1,51 @@
{
"reviews.title": "Review",
"reviews.subtitle": "{file} — {total} segments ({pending} to review, {approved} approved, {edited} edited)",
"reviews.approveAll": "Approve all",
"reviews.importXliff": "Import XLIFF",
"reviews.rebuild": "Rebuild & download",
"reviews.col.source": "Source",
"reviews.col.target": "Translation",
"reviews.col.status": "Status",
"reviews.status.approved": "Approved",
"reviews.status.edited": "Edited",
"reviews.status.pending": "To review",
"reviews.save": "Save",
"reviews.cancel": "Cancel",
"reviews.emptyCell": "— empty —",
"reviews.action.edit": "Edit translation",
"reviews.action.approve": "Approve as-is",
"reviews.action.reset": "Reset to pending",
"reviews.approvedTitle": "Segments approved",
"reviews.approvedDesc": "{count} segment(s) approved.",
"reviews.rebuiltTitle": "Document rebuilt",
"reviews.rebuiltDesc": "{count} reviewed segment(s) applied.",
"reviews.rebuildFailed": "Rebuild failed",
"reviews.xliffExportFailed": "XLIFF export failed",
"reviews.xliffImported": "XLIFF imported",
"reviews.xliffImportedDesc": "{count} segment(s) updated.",
"reviews.xliffImportFailed": "XLIFF import failed",
"reviews.error.title": "Error",
"reviews.error.load": "Load error",
"reviews.error.update": "Update failed",
"reviews.error.download": "Download failed (HTTP {status})",
"reviews.pill": "Quality control",
"reviews.titleBase": "Translation",
"reviews.titleAccent": "review",
"reviews.approveAllConfirm": "Confirm ({count})",
"reviews.action.approveShort": "OK",
"reviews.action.resetShort": "To review",
"reviews.xliffHint": "XLIFF is the standard exchange format for translation reviews — export segments, edit in your CAT tool, then reimport.",
"reviews.error.sessionExpired": "Session expired. Sign in again and refresh.",
"reviews.error.notFound": "Job not found.",
"reviews.error.rateLimited": "Rate limited. Wait a few seconds.",
"reviews.error.server": "Server error. Try again shortly.",
"reviews.filter.all": "All",
"reviews.filter.pending": "To review",
"reviews.filter.approved": "Approved",
"reviews.filter.edited": "Edited",
"reviews.searchPlaceholder": "Search in segments...",
"reviews.shortcutHint": "Ctrl + Enter to save",
"reviews.approvingProgress": "Approving ({current}/{total})…",
"reviews.noMatchingSegments": "No segments match your filter."
}

View File

@@ -0,0 +1,32 @@
{
"teams.title": "Workspaces",
"teams.subtitle": "Share your glossaries with your team (Business plan — 5 seats).",
"teams.needsBusiness": "Workspaces require the Business plan. Upgrade to collaborate with your team.",
"teams.newWorkspace": "New workspace",
"teams.newPlaceholder": "e.g. Translation team",
"teams.create": "Create",
"teams.created": "Workspace created",
"teams.createdDesc": "You are the owner.",
"teams.createFailed": "Could not create",
"teams.memberAdded": "Member added",
"teams.addFailed": "Could not add",
"teams.removeFailed": "Could not remove",
"teams.removeMember": "Remove from workspace",
"teams.owner": "Owner",
"teams.seats": "{count} / {limit} seats",
"teams.seatsUnlimited": "{count} seats (unlimited)",
"teams.role.member": "Member",
"teams.role.admin": "Admin",
"teams.addMember": "Add a member (email)",
"teams.emailPlaceholder": "colleague@company.com",
"teams.invite": "Invite",
"teams.empty": "No workspace yet — create the first one above.",
"teams.error.title": "Error",
"teams.error.load": "Could not load workspaces",
"teams.pill": "Collaboration",
"teams.titleBase": "Workspaces",
"teams.titleAccent": "team",
"teams.removeConfirm": "Are you sure you want to remove this member?",
"teams.cancel": "Cancel",
"teams.confirm": "Confirm"
}

View File

@@ -88,5 +88,6 @@
"translate.preserveLayoutDesc": "Mantieni il layout",
"translate.textOnly": "Solo testo",
"translate.textOnlyDesc": "Traduzione rapida solo del testo",
"translate.unavailableStandard": "Non disponibile in modalità Standard (solo IA)"
"translate.unavailableStandard": "Non disponibile in modalità Standard (solo IA)",
"translate.sameLanguageError": "La lingua di origine e quella di destinazione devono essere diverse"
}

View File

@@ -22,8 +22,10 @@ import providerSelector from "./providerSelector.json";
import providerTheme from "./providerTheme.json";
import register from "./register.json";
import resetPassword from "./resetPassword.json";
import reviews from "./reviews.json";
import services from "./services.json";
import settings from "./settings.json";
import teams from "./teams.json";
import translate from "./translate.json";
import translateComplete from "./translateComplete.json";
@@ -49,8 +51,10 @@ const messages: Record<string, string> = {
...providerTheme,
...register,
...resetPassword,
...reviews,
...services,
...settings,
...teams,
...translate,
...translateComplete,
};

View File

@@ -0,0 +1,51 @@
{
"reviews.title": "Review",
"reviews.subtitle": "{file} — {total} segments ({pending} to review, {approved} approved, {edited} edited)",
"reviews.approveAll": "Approve all",
"reviews.importXliff": "Import XLIFF",
"reviews.rebuild": "Rebuild & download",
"reviews.col.source": "Source",
"reviews.col.target": "Translation",
"reviews.col.status": "Status",
"reviews.status.approved": "Approved",
"reviews.status.edited": "Edited",
"reviews.status.pending": "To review",
"reviews.save": "Save",
"reviews.cancel": "Cancel",
"reviews.emptyCell": "— empty —",
"reviews.action.edit": "Edit translation",
"reviews.action.approve": "Approve as-is",
"reviews.action.reset": "Reset to pending",
"reviews.approvedTitle": "Segments approved",
"reviews.approvedDesc": "{count} segment(s) approved.",
"reviews.rebuiltTitle": "Document rebuilt",
"reviews.rebuiltDesc": "{count} reviewed segment(s) applied.",
"reviews.rebuildFailed": "Rebuild failed",
"reviews.xliffExportFailed": "XLIFF export failed",
"reviews.xliffImported": "XLIFF imported",
"reviews.xliffImportedDesc": "{count} segment(s) updated.",
"reviews.xliffImportFailed": "XLIFF import failed",
"reviews.error.title": "Error",
"reviews.error.load": "Load error",
"reviews.error.update": "Update failed",
"reviews.error.download": "Download failed (HTTP {status})",
"reviews.pill": "Quality control",
"reviews.titleBase": "Translation",
"reviews.titleAccent": "review",
"reviews.approveAllConfirm": "Confirm ({count})",
"reviews.action.approveShort": "OK",
"reviews.action.resetShort": "To review",
"reviews.xliffHint": "XLIFF is the standard exchange format for translation reviews — export segments, edit in your CAT tool, then reimport.",
"reviews.error.sessionExpired": "Session expired. Sign in again and refresh.",
"reviews.error.notFound": "Job not found.",
"reviews.error.rateLimited": "Rate limited. Wait a few seconds.",
"reviews.error.server": "Server error. Try again shortly.",
"reviews.filter.all": "All",
"reviews.filter.pending": "To review",
"reviews.filter.approved": "Approved",
"reviews.filter.edited": "Edited",
"reviews.searchPlaceholder": "Search in segments...",
"reviews.shortcutHint": "Ctrl + Enter to save",
"reviews.approvingProgress": "Approving ({current}/{total})…",
"reviews.noMatchingSegments": "No segments match your filter."
}

View File

@@ -0,0 +1,32 @@
{
"teams.title": "Workspaces",
"teams.subtitle": "Share your glossaries with your team (Business plan — 5 seats).",
"teams.needsBusiness": "Workspaces require the Business plan. Upgrade to collaborate with your team.",
"teams.newWorkspace": "New workspace",
"teams.newPlaceholder": "e.g. Translation team",
"teams.create": "Create",
"teams.created": "Workspace created",
"teams.createdDesc": "You are the owner.",
"teams.createFailed": "Could not create",
"teams.memberAdded": "Member added",
"teams.addFailed": "Could not add",
"teams.removeFailed": "Could not remove",
"teams.removeMember": "Remove from workspace",
"teams.owner": "Owner",
"teams.seats": "{count} / {limit} seats",
"teams.seatsUnlimited": "{count} seats (unlimited)",
"teams.role.member": "Member",
"teams.role.admin": "Admin",
"teams.addMember": "Add a member (email)",
"teams.emailPlaceholder": "colleague@company.com",
"teams.invite": "Invite",
"teams.empty": "No workspace yet — create the first one above.",
"teams.error.title": "Error",
"teams.error.load": "Could not load workspaces",
"teams.pill": "Collaboration",
"teams.titleBase": "Workspaces",
"teams.titleAccent": "team",
"teams.removeConfirm": "Are you sure you want to remove this member?",
"teams.cancel": "Cancel",
"teams.confirm": "Confirm"
}

View File

@@ -88,5 +88,6 @@
"translate.preserveLayoutDesc": "レイアウトを保持",
"translate.textOnly": "テキストのみ",
"translate.textOnlyDesc": "テキストのみの高速翻訳",
"translate.unavailableStandard": "標準モードでは利用できません (AI のみ)"
"translate.unavailableStandard": "標準モードでは利用できません (AI のみ)",
"translate.sameLanguageError": "翻訳元と翻訳先の言語は異なる必要があります"
}

View File

@@ -22,8 +22,10 @@ import providerSelector from "./providerSelector.json";
import providerTheme from "./providerTheme.json";
import register from "./register.json";
import resetPassword from "./resetPassword.json";
import reviews from "./reviews.json";
import services from "./services.json";
import settings from "./settings.json";
import teams from "./teams.json";
import translate from "./translate.json";
import translateComplete from "./translateComplete.json";
@@ -49,8 +51,10 @@ const messages: Record<string, string> = {
...providerTheme,
...register,
...resetPassword,
...reviews,
...services,
...settings,
...teams,
...translate,
...translateComplete,
};

View File

@@ -0,0 +1,51 @@
{
"reviews.title": "Review",
"reviews.subtitle": "{file} — {total} segments ({pending} to review, {approved} approved, {edited} edited)",
"reviews.approveAll": "Approve all",
"reviews.importXliff": "Import XLIFF",
"reviews.rebuild": "Rebuild & download",
"reviews.col.source": "Source",
"reviews.col.target": "Translation",
"reviews.col.status": "Status",
"reviews.status.approved": "Approved",
"reviews.status.edited": "Edited",
"reviews.status.pending": "To review",
"reviews.save": "Save",
"reviews.cancel": "Cancel",
"reviews.emptyCell": "— empty —",
"reviews.action.edit": "Edit translation",
"reviews.action.approve": "Approve as-is",
"reviews.action.reset": "Reset to pending",
"reviews.approvedTitle": "Segments approved",
"reviews.approvedDesc": "{count} segment(s) approved.",
"reviews.rebuiltTitle": "Document rebuilt",
"reviews.rebuiltDesc": "{count} reviewed segment(s) applied.",
"reviews.rebuildFailed": "Rebuild failed",
"reviews.xliffExportFailed": "XLIFF export failed",
"reviews.xliffImported": "XLIFF imported",
"reviews.xliffImportedDesc": "{count} segment(s) updated.",
"reviews.xliffImportFailed": "XLIFF import failed",
"reviews.error.title": "Error",
"reviews.error.load": "Load error",
"reviews.error.update": "Update failed",
"reviews.error.download": "Download failed (HTTP {status})",
"reviews.pill": "Quality control",
"reviews.titleBase": "Translation",
"reviews.titleAccent": "review",
"reviews.approveAllConfirm": "Confirm ({count})",
"reviews.action.approveShort": "OK",
"reviews.action.resetShort": "To review",
"reviews.xliffHint": "XLIFF is the standard exchange format for translation reviews — export segments, edit in your CAT tool, then reimport.",
"reviews.error.sessionExpired": "Session expired. Sign in again and refresh.",
"reviews.error.notFound": "Job not found.",
"reviews.error.rateLimited": "Rate limited. Wait a few seconds.",
"reviews.error.server": "Server error. Try again shortly.",
"reviews.filter.all": "All",
"reviews.filter.pending": "To review",
"reviews.filter.approved": "Approved",
"reviews.filter.edited": "Edited",
"reviews.searchPlaceholder": "Search in segments...",
"reviews.shortcutHint": "Ctrl + Enter to save",
"reviews.approvingProgress": "Approving ({current}/{total})…",
"reviews.noMatchingSegments": "No segments match your filter."
}

View File

@@ -0,0 +1,32 @@
{
"teams.title": "Workspaces",
"teams.subtitle": "Share your glossaries with your team (Business plan — 5 seats).",
"teams.needsBusiness": "Workspaces require the Business plan. Upgrade to collaborate with your team.",
"teams.newWorkspace": "New workspace",
"teams.newPlaceholder": "e.g. Translation team",
"teams.create": "Create",
"teams.created": "Workspace created",
"teams.createdDesc": "You are the owner.",
"teams.createFailed": "Could not create",
"teams.memberAdded": "Member added",
"teams.addFailed": "Could not add",
"teams.removeFailed": "Could not remove",
"teams.removeMember": "Remove from workspace",
"teams.owner": "Owner",
"teams.seats": "{count} / {limit} seats",
"teams.seatsUnlimited": "{count} seats (unlimited)",
"teams.role.member": "Member",
"teams.role.admin": "Admin",
"teams.addMember": "Add a member (email)",
"teams.emailPlaceholder": "colleague@company.com",
"teams.invite": "Invite",
"teams.empty": "No workspace yet — create the first one above.",
"teams.error.title": "Error",
"teams.error.load": "Could not load workspaces",
"teams.pill": "Collaboration",
"teams.titleBase": "Workspaces",
"teams.titleAccent": "team",
"teams.removeConfirm": "Are you sure you want to remove this member?",
"teams.cancel": "Cancel",
"teams.confirm": "Confirm"
}

View File

@@ -88,5 +88,6 @@
"translate.preserveLayoutDesc": "레이아웃 유지",
"translate.textOnly": "텍스트만",
"translate.textOnlyDesc": "텍스트만 빠른 번역",
"translate.unavailableStandard": "표준 모드에서 사용 불가 (AI만)"
"translate.unavailableStandard": "표준 모드에서 사용 불가 (AI만)",
"translate.sameLanguageError": "출발어와 도착어가 달라야 합니다"
}

View File

@@ -22,8 +22,10 @@ import providerSelector from "./providerSelector.json";
import providerTheme from "./providerTheme.json";
import register from "./register.json";
import resetPassword from "./resetPassword.json";
import reviews from "./reviews.json";
import services from "./services.json";
import settings from "./settings.json";
import teams from "./teams.json";
import translate from "./translate.json";
import translateComplete from "./translateComplete.json";
@@ -49,8 +51,10 @@ const messages: Record<string, string> = {
...providerTheme,
...register,
...resetPassword,
...reviews,
...services,
...settings,
...teams,
...translate,
...translateComplete,
};

View File

@@ -0,0 +1,51 @@
{
"reviews.title": "Review",
"reviews.subtitle": "{file} — {total} segments ({pending} to review, {approved} approved, {edited} edited)",
"reviews.approveAll": "Approve all",
"reviews.importXliff": "Import XLIFF",
"reviews.rebuild": "Rebuild & download",
"reviews.col.source": "Source",
"reviews.col.target": "Translation",
"reviews.col.status": "Status",
"reviews.status.approved": "Approved",
"reviews.status.edited": "Edited",
"reviews.status.pending": "To review",
"reviews.save": "Save",
"reviews.cancel": "Cancel",
"reviews.emptyCell": "— empty —",
"reviews.action.edit": "Edit translation",
"reviews.action.approve": "Approve as-is",
"reviews.action.reset": "Reset to pending",
"reviews.approvedTitle": "Segments approved",
"reviews.approvedDesc": "{count} segment(s) approved.",
"reviews.rebuiltTitle": "Document rebuilt",
"reviews.rebuiltDesc": "{count} reviewed segment(s) applied.",
"reviews.rebuildFailed": "Rebuild failed",
"reviews.xliffExportFailed": "XLIFF export failed",
"reviews.xliffImported": "XLIFF imported",
"reviews.xliffImportedDesc": "{count} segment(s) updated.",
"reviews.xliffImportFailed": "XLIFF import failed",
"reviews.error.title": "Error",
"reviews.error.load": "Load error",
"reviews.error.update": "Update failed",
"reviews.error.download": "Download failed (HTTP {status})",
"reviews.pill": "Quality control",
"reviews.titleBase": "Translation",
"reviews.titleAccent": "review",
"reviews.approveAllConfirm": "Confirm ({count})",
"reviews.action.approveShort": "OK",
"reviews.action.resetShort": "To review",
"reviews.xliffHint": "XLIFF is the standard exchange format for translation reviews — export segments, edit in your CAT tool, then reimport.",
"reviews.error.sessionExpired": "Session expired. Sign in again and refresh.",
"reviews.error.notFound": "Job not found.",
"reviews.error.rateLimited": "Rate limited. Wait a few seconds.",
"reviews.error.server": "Server error. Try again shortly.",
"reviews.filter.all": "All",
"reviews.filter.pending": "To review",
"reviews.filter.approved": "Approved",
"reviews.filter.edited": "Edited",
"reviews.searchPlaceholder": "Search in segments...",
"reviews.shortcutHint": "Ctrl + Enter to save",
"reviews.approvingProgress": "Approving ({current}/{total})…",
"reviews.noMatchingSegments": "No segments match your filter."
}

View File

@@ -0,0 +1,32 @@
{
"teams.title": "Workspaces",
"teams.subtitle": "Share your glossaries with your team (Business plan — 5 seats).",
"teams.needsBusiness": "Workspaces require the Business plan. Upgrade to collaborate with your team.",
"teams.newWorkspace": "New workspace",
"teams.newPlaceholder": "e.g. Translation team",
"teams.create": "Create",
"teams.created": "Workspace created",
"teams.createdDesc": "You are the owner.",
"teams.createFailed": "Could not create",
"teams.memberAdded": "Member added",
"teams.addFailed": "Could not add",
"teams.removeFailed": "Could not remove",
"teams.removeMember": "Remove from workspace",
"teams.owner": "Owner",
"teams.seats": "{count} / {limit} seats",
"teams.seatsUnlimited": "{count} seats (unlimited)",
"teams.role.member": "Member",
"teams.role.admin": "Admin",
"teams.addMember": "Add a member (email)",
"teams.emailPlaceholder": "colleague@company.com",
"teams.invite": "Invite",
"teams.empty": "No workspace yet — create the first one above.",
"teams.error.title": "Error",
"teams.error.load": "Could not load workspaces",
"teams.pill": "Collaboration",
"teams.titleBase": "Workspaces",
"teams.titleAccent": "team",
"teams.removeConfirm": "Are you sure you want to remove this member?",
"teams.cancel": "Cancel",
"teams.confirm": "Confirm"
}

View File

@@ -88,5 +88,6 @@
"translate.preserveLayoutDesc": "Opmaak behouden",
"translate.textOnly": "Alleen tekst",
"translate.textOnlyDesc": "Snelle vertaling van alleen tekst",
"translate.unavailableStandard": "Niet beschikbaar in Standaardmodus (alleen AI)"
"translate.unavailableStandard": "Niet beschikbaar in Standaardmodus (alleen AI)",
"translate.sameLanguageError": "Bron- en doeltaal moeten verschillend zijn"
}

View File

@@ -22,8 +22,10 @@ import providerSelector from "./providerSelector.json";
import providerTheme from "./providerTheme.json";
import register from "./register.json";
import resetPassword from "./resetPassword.json";
import reviews from "./reviews.json";
import services from "./services.json";
import settings from "./settings.json";
import teams from "./teams.json";
import translate from "./translate.json";
import translateComplete from "./translateComplete.json";
@@ -49,8 +51,10 @@ const messages: Record<string, string> = {
...providerTheme,
...register,
...resetPassword,
...reviews,
...services,
...settings,
...teams,
...translate,
...translateComplete,
};

View File

@@ -0,0 +1,51 @@
{
"reviews.title": "Review",
"reviews.subtitle": "{file} — {total} segments ({pending} to review, {approved} approved, {edited} edited)",
"reviews.approveAll": "Approve all",
"reviews.importXliff": "Import XLIFF",
"reviews.rebuild": "Rebuild & download",
"reviews.col.source": "Source",
"reviews.col.target": "Translation",
"reviews.col.status": "Status",
"reviews.status.approved": "Approved",
"reviews.status.edited": "Edited",
"reviews.status.pending": "To review",
"reviews.save": "Save",
"reviews.cancel": "Cancel",
"reviews.emptyCell": "— empty —",
"reviews.action.edit": "Edit translation",
"reviews.action.approve": "Approve as-is",
"reviews.action.reset": "Reset to pending",
"reviews.approvedTitle": "Segments approved",
"reviews.approvedDesc": "{count} segment(s) approved.",
"reviews.rebuiltTitle": "Document rebuilt",
"reviews.rebuiltDesc": "{count} reviewed segment(s) applied.",
"reviews.rebuildFailed": "Rebuild failed",
"reviews.xliffExportFailed": "XLIFF export failed",
"reviews.xliffImported": "XLIFF imported",
"reviews.xliffImportedDesc": "{count} segment(s) updated.",
"reviews.xliffImportFailed": "XLIFF import failed",
"reviews.error.title": "Error",
"reviews.error.load": "Load error",
"reviews.error.update": "Update failed",
"reviews.error.download": "Download failed (HTTP {status})",
"reviews.pill": "Quality control",
"reviews.titleBase": "Translation",
"reviews.titleAccent": "review",
"reviews.approveAllConfirm": "Confirm ({count})",
"reviews.action.approveShort": "OK",
"reviews.action.resetShort": "To review",
"reviews.xliffHint": "XLIFF is the standard exchange format for translation reviews — export segments, edit in your CAT tool, then reimport.",
"reviews.error.sessionExpired": "Session expired. Sign in again and refresh.",
"reviews.error.notFound": "Job not found.",
"reviews.error.rateLimited": "Rate limited. Wait a few seconds.",
"reviews.error.server": "Server error. Try again shortly.",
"reviews.filter.all": "All",
"reviews.filter.pending": "To review",
"reviews.filter.approved": "Approved",
"reviews.filter.edited": "Edited",
"reviews.searchPlaceholder": "Search in segments...",
"reviews.shortcutHint": "Ctrl + Enter to save",
"reviews.approvingProgress": "Approving ({current}/{total})…",
"reviews.noMatchingSegments": "No segments match your filter."
}

View File

@@ -0,0 +1,32 @@
{
"teams.title": "Workspaces",
"teams.subtitle": "Share your glossaries with your team (Business plan — 5 seats).",
"teams.needsBusiness": "Workspaces require the Business plan. Upgrade to collaborate with your team.",
"teams.newWorkspace": "New workspace",
"teams.newPlaceholder": "e.g. Translation team",
"teams.create": "Create",
"teams.created": "Workspace created",
"teams.createdDesc": "You are the owner.",
"teams.createFailed": "Could not create",
"teams.memberAdded": "Member added",
"teams.addFailed": "Could not add",
"teams.removeFailed": "Could not remove",
"teams.removeMember": "Remove from workspace",
"teams.owner": "Owner",
"teams.seats": "{count} / {limit} seats",
"teams.seatsUnlimited": "{count} seats (unlimited)",
"teams.role.member": "Member",
"teams.role.admin": "Admin",
"teams.addMember": "Add a member (email)",
"teams.emailPlaceholder": "colleague@company.com",
"teams.invite": "Invite",
"teams.empty": "No workspace yet — create the first one above.",
"teams.error.title": "Error",
"teams.error.load": "Could not load workspaces",
"teams.pill": "Collaboration",
"teams.titleBase": "Workspaces",
"teams.titleAccent": "team",
"teams.removeConfirm": "Are you sure you want to remove this member?",
"teams.cancel": "Cancel",
"teams.confirm": "Confirm"
}

View File

@@ -88,5 +88,6 @@
"translate.preserveLayoutDesc": "Manter o layout",
"translate.textOnly": "Apenas texto",
"translate.textOnlyDesc": "Tradução rápida apenas do texto",
"translate.unavailableStandard": "Indisponível no modo Padrão (apenas IA)"
"translate.unavailableStandard": "Indisponível no modo Padrão (apenas IA)",
"translate.sameLanguageError": "Os idiomas de origem e de destino devem ser diferentes"
}

View File

@@ -22,8 +22,10 @@ import providerSelector from "./providerSelector.json";
import providerTheme from "./providerTheme.json";
import register from "./register.json";
import resetPassword from "./resetPassword.json";
import reviews from "./reviews.json";
import services from "./services.json";
import settings from "./settings.json";
import teams from "./teams.json";
import translate from "./translate.json";
import translateComplete from "./translateComplete.json";
@@ -49,8 +51,10 @@ const messages: Record<string, string> = {
...providerTheme,
...register,
...resetPassword,
...reviews,
...services,
...settings,
...teams,
...translate,
...translateComplete,
};

View File

@@ -0,0 +1,51 @@
{
"reviews.title": "Review",
"reviews.subtitle": "{file} — {total} segments ({pending} to review, {approved} approved, {edited} edited)",
"reviews.approveAll": "Approve all",
"reviews.importXliff": "Import XLIFF",
"reviews.rebuild": "Rebuild & download",
"reviews.col.source": "Source",
"reviews.col.target": "Translation",
"reviews.col.status": "Status",
"reviews.status.approved": "Approved",
"reviews.status.edited": "Edited",
"reviews.status.pending": "To review",
"reviews.save": "Save",
"reviews.cancel": "Cancel",
"reviews.emptyCell": "— empty —",
"reviews.action.edit": "Edit translation",
"reviews.action.approve": "Approve as-is",
"reviews.action.reset": "Reset to pending",
"reviews.approvedTitle": "Segments approved",
"reviews.approvedDesc": "{count} segment(s) approved.",
"reviews.rebuiltTitle": "Document rebuilt",
"reviews.rebuiltDesc": "{count} reviewed segment(s) applied.",
"reviews.rebuildFailed": "Rebuild failed",
"reviews.xliffExportFailed": "XLIFF export failed",
"reviews.xliffImported": "XLIFF imported",
"reviews.xliffImportedDesc": "{count} segment(s) updated.",
"reviews.xliffImportFailed": "XLIFF import failed",
"reviews.error.title": "Error",
"reviews.error.load": "Load error",
"reviews.error.update": "Update failed",
"reviews.error.download": "Download failed (HTTP {status})",
"reviews.pill": "Quality control",
"reviews.titleBase": "Translation",
"reviews.titleAccent": "review",
"reviews.approveAllConfirm": "Confirm ({count})",
"reviews.action.approveShort": "OK",
"reviews.action.resetShort": "To review",
"reviews.xliffHint": "XLIFF is the standard exchange format for translation reviews — export segments, edit in your CAT tool, then reimport.",
"reviews.error.sessionExpired": "Session expired. Sign in again and refresh.",
"reviews.error.notFound": "Job not found.",
"reviews.error.rateLimited": "Rate limited. Wait a few seconds.",
"reviews.error.server": "Server error. Try again shortly.",
"reviews.filter.all": "All",
"reviews.filter.pending": "To review",
"reviews.filter.approved": "Approved",
"reviews.filter.edited": "Edited",
"reviews.searchPlaceholder": "Search in segments...",
"reviews.shortcutHint": "Ctrl + Enter to save",
"reviews.approvingProgress": "Approving ({current}/{total})…",
"reviews.noMatchingSegments": "No segments match your filter."
}

View File

@@ -0,0 +1,32 @@
{
"teams.title": "Workspaces",
"teams.subtitle": "Share your glossaries with your team (Business plan — 5 seats).",
"teams.needsBusiness": "Workspaces require the Business plan. Upgrade to collaborate with your team.",
"teams.newWorkspace": "New workspace",
"teams.newPlaceholder": "e.g. Translation team",
"teams.create": "Create",
"teams.created": "Workspace created",
"teams.createdDesc": "You are the owner.",
"teams.createFailed": "Could not create",
"teams.memberAdded": "Member added",
"teams.addFailed": "Could not add",
"teams.removeFailed": "Could not remove",
"teams.removeMember": "Remove from workspace",
"teams.owner": "Owner",
"teams.seats": "{count} / {limit} seats",
"teams.seatsUnlimited": "{count} seats (unlimited)",
"teams.role.member": "Member",
"teams.role.admin": "Admin",
"teams.addMember": "Add a member (email)",
"teams.emailPlaceholder": "colleague@company.com",
"teams.invite": "Invite",
"teams.empty": "No workspace yet — create the first one above.",
"teams.error.title": "Error",
"teams.error.load": "Could not load workspaces",
"teams.pill": "Collaboration",
"teams.titleBase": "Workspaces",
"teams.titleAccent": "team",
"teams.removeConfirm": "Are you sure you want to remove this member?",
"teams.cancel": "Cancel",
"teams.confirm": "Confirm"
}

View File

@@ -88,5 +88,6 @@
"translate.preserveLayoutDesc": "Сохранить макет",
"translate.textOnly": "Только текст",
"translate.textOnlyDesc": "Быстрый перевод только текста",
"translate.unavailableStandard": "Недоступно в стандартном режиме (только ИИ)"
"translate.unavailableStandard": "Недоступно в стандартном режиме (только ИИ)",
"translate.sameLanguageError": "Исходный и целевой языки должны различаться"
}

View File

@@ -20,8 +20,10 @@ import profile from "./profile.json";
import providerSelector from "./providerSelector.json";
import providerTheme from "./providerTheme.json";
import register from "./register.json";
import reviews from "./reviews.json";
import services from "./services.json";
import settings from "./settings.json";
import teams from "./teams.json";
import translate from "./translate.json";
import translateComplete from "./translateComplete.json";
@@ -45,8 +47,10 @@ const messages: Record<string, string> = {
...providerSelector,
...providerTheme,
...register,
...reviews,
...services,
...settings,
...teams,
...translate,
...translateComplete,
};

View File

@@ -0,0 +1,51 @@
{
"reviews.title": "Review",
"reviews.subtitle": "{file} — {total} segments ({pending} to review, {approved} approved, {edited} edited)",
"reviews.approveAll": "Approve all",
"reviews.importXliff": "Import XLIFF",
"reviews.rebuild": "Rebuild & download",
"reviews.col.source": "Source",
"reviews.col.target": "Translation",
"reviews.col.status": "Status",
"reviews.status.approved": "Approved",
"reviews.status.edited": "Edited",
"reviews.status.pending": "To review",
"reviews.save": "Save",
"reviews.cancel": "Cancel",
"reviews.emptyCell": "— empty —",
"reviews.action.edit": "Edit translation",
"reviews.action.approve": "Approve as-is",
"reviews.action.reset": "Reset to pending",
"reviews.approvedTitle": "Segments approved",
"reviews.approvedDesc": "{count} segment(s) approved.",
"reviews.rebuiltTitle": "Document rebuilt",
"reviews.rebuiltDesc": "{count} reviewed segment(s) applied.",
"reviews.rebuildFailed": "Rebuild failed",
"reviews.xliffExportFailed": "XLIFF export failed",
"reviews.xliffImported": "XLIFF imported",
"reviews.xliffImportedDesc": "{count} segment(s) updated.",
"reviews.xliffImportFailed": "XLIFF import failed",
"reviews.error.title": "Error",
"reviews.error.load": "Load error",
"reviews.error.update": "Update failed",
"reviews.error.download": "Download failed (HTTP {status})",
"reviews.pill": "Quality control",
"reviews.titleBase": "Translation",
"reviews.titleAccent": "review",
"reviews.approveAllConfirm": "Confirm ({count})",
"reviews.action.approveShort": "OK",
"reviews.action.resetShort": "To review",
"reviews.xliffHint": "XLIFF is the standard exchange format for translation reviews — export segments, edit in your CAT tool, then reimport.",
"reviews.error.sessionExpired": "Session expired. Sign in again and refresh.",
"reviews.error.notFound": "Job not found.",
"reviews.error.rateLimited": "Rate limited. Wait a few seconds.",
"reviews.error.server": "Server error. Try again shortly.",
"reviews.filter.all": "All",
"reviews.filter.pending": "To review",
"reviews.filter.approved": "Approved",
"reviews.filter.edited": "Edited",
"reviews.searchPlaceholder": "Search in segments...",
"reviews.shortcutHint": "Ctrl + Enter to save",
"reviews.approvingProgress": "Approving ({current}/{total})…",
"reviews.noMatchingSegments": "No segments match your filter."
}

View File

@@ -0,0 +1,32 @@
{
"teams.title": "Workspaces",
"teams.subtitle": "Share your glossaries with your team (Business plan — 5 seats).",
"teams.needsBusiness": "Workspaces require the Business plan. Upgrade to collaborate with your team.",
"teams.newWorkspace": "New workspace",
"teams.newPlaceholder": "e.g. Translation team",
"teams.create": "Create",
"teams.created": "Workspace created",
"teams.createdDesc": "You are the owner.",
"teams.createFailed": "Could not create",
"teams.memberAdded": "Member added",
"teams.addFailed": "Could not add",
"teams.removeFailed": "Could not remove",
"teams.removeMember": "Remove from workspace",
"teams.owner": "Owner",
"teams.seats": "{count} / {limit} seats",
"teams.seatsUnlimited": "{count} seats (unlimited)",
"teams.role.member": "Member",
"teams.role.admin": "Admin",
"teams.addMember": "Add a member (email)",
"teams.emailPlaceholder": "colleague@company.com",
"teams.invite": "Invite",
"teams.empty": "No workspace yet — create the first one above.",
"teams.error.title": "Error",
"teams.error.load": "Could not load workspaces",
"teams.pill": "Collaboration",
"teams.titleBase": "Workspaces",
"teams.titleAccent": "team",
"teams.removeConfirm": "Are you sure you want to remove this member?",
"teams.cancel": "Cancel",
"teams.confirm": "Confirm"
}

View File

@@ -88,5 +88,6 @@
"translate.preserveLayoutDesc": "保留布局",
"translate.textOnly": "仅文本",
"translate.textOnlyDesc": "仅文本快速翻译",
"translate.unavailableStandard": "标准模式下不可用(仅 AI)"
"translate.unavailableStandard": "标准模式下不可用(仅 AI)",
"translate.sameLanguageError": "源语言和目标语言必须不同"
}