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
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 3m11s
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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') && (
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
51
frontend/src/lib/i18n/messages/ar/reviews.json
Normal file
51
frontend/src/lib/i18n/messages/ar/reviews.json
Normal 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."
|
||||
}
|
||||
32
frontend/src/lib/i18n/messages/ar/teams.json
Normal file
32
frontend/src/lib/i18n/messages/ar/teams.json
Normal 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"
|
||||
}
|
||||
@@ -88,5 +88,6 @@
|
||||
"translate.preserveLayoutDesc": "الحفاظ على التخطيط",
|
||||
"translate.textOnly": "نص فقط",
|
||||
"translate.textOnlyDesc": "ترجمة سريعة للنص فقط",
|
||||
"translate.unavailableStandard": "غير متاح في الوضع القياسي (AI فقط)"
|
||||
"translate.unavailableStandard": "غير متاح في الوضع القياسي (AI فقط)",
|
||||
"translate.sameLanguageError": "يجب أن تختلف اللغة المصدر عن اللغة الهدف"
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
51
frontend/src/lib/i18n/messages/de/reviews.json
Normal file
51
frontend/src/lib/i18n/messages/de/reviews.json
Normal 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."
|
||||
}
|
||||
32
frontend/src/lib/i18n/messages/de/teams.json
Normal file
32
frontend/src/lib/i18n/messages/de/teams.json
Normal 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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
51
frontend/src/lib/i18n/messages/es/reviews.json
Normal file
51
frontend/src/lib/i18n/messages/es/reviews.json
Normal 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."
|
||||
}
|
||||
32
frontend/src/lib/i18n/messages/es/teams.json
Normal file
32
frontend/src/lib/i18n/messages/es/teams.json
Normal 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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
51
frontend/src/lib/i18n/messages/fa/reviews.json
Normal file
51
frontend/src/lib/i18n/messages/fa/reviews.json
Normal 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."
|
||||
}
|
||||
32
frontend/src/lib/i18n/messages/fa/teams.json
Normal file
32
frontend/src/lib/i18n/messages/fa/teams.json
Normal 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"
|
||||
}
|
||||
@@ -88,5 +88,6 @@
|
||||
"translate.preserveLayoutDesc": "حفظ چیدمان",
|
||||
"translate.textOnly": "فقط متن",
|
||||
"translate.textOnlyDesc": "ترجمه سریع فقط متن",
|
||||
"translate.unavailableStandard": "در حالت استاندارد موجود نیست (فقط AI)"
|
||||
"translate.unavailableStandard": "در حالت استاندارد موجود نیست (فقط AI)",
|
||||
"translate.sameLanguageError": "زبان مبدأ و مقصد باید متفاوت باشند"
|
||||
}
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
51
frontend/src/lib/i18n/messages/it/reviews.json
Normal file
51
frontend/src/lib/i18n/messages/it/reviews.json
Normal 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."
|
||||
}
|
||||
32
frontend/src/lib/i18n/messages/it/teams.json
Normal file
32
frontend/src/lib/i18n/messages/it/teams.json
Normal 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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
51
frontend/src/lib/i18n/messages/ja/reviews.json
Normal file
51
frontend/src/lib/i18n/messages/ja/reviews.json
Normal 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."
|
||||
}
|
||||
32
frontend/src/lib/i18n/messages/ja/teams.json
Normal file
32
frontend/src/lib/i18n/messages/ja/teams.json
Normal 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"
|
||||
}
|
||||
@@ -88,5 +88,6 @@
|
||||
"translate.preserveLayoutDesc": "レイアウトを保持",
|
||||
"translate.textOnly": "テキストのみ",
|
||||
"translate.textOnlyDesc": "テキストのみの高速翻訳",
|
||||
"translate.unavailableStandard": "標準モードでは利用できません (AI のみ)"
|
||||
"translate.unavailableStandard": "標準モードでは利用できません (AI のみ)",
|
||||
"translate.sameLanguageError": "翻訳元と翻訳先の言語は異なる必要があります"
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
51
frontend/src/lib/i18n/messages/ko/reviews.json
Normal file
51
frontend/src/lib/i18n/messages/ko/reviews.json
Normal 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."
|
||||
}
|
||||
32
frontend/src/lib/i18n/messages/ko/teams.json
Normal file
32
frontend/src/lib/i18n/messages/ko/teams.json
Normal 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"
|
||||
}
|
||||
@@ -88,5 +88,6 @@
|
||||
"translate.preserveLayoutDesc": "레이아웃 유지",
|
||||
"translate.textOnly": "텍스트만",
|
||||
"translate.textOnlyDesc": "텍스트만 빠른 번역",
|
||||
"translate.unavailableStandard": "표준 모드에서 사용 불가 (AI만)"
|
||||
"translate.unavailableStandard": "표준 모드에서 사용 불가 (AI만)",
|
||||
"translate.sameLanguageError": "출발어와 도착어가 달라야 합니다"
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
51
frontend/src/lib/i18n/messages/nl/reviews.json
Normal file
51
frontend/src/lib/i18n/messages/nl/reviews.json
Normal 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."
|
||||
}
|
||||
32
frontend/src/lib/i18n/messages/nl/teams.json
Normal file
32
frontend/src/lib/i18n/messages/nl/teams.json
Normal 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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
51
frontend/src/lib/i18n/messages/pt/reviews.json
Normal file
51
frontend/src/lib/i18n/messages/pt/reviews.json
Normal 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."
|
||||
}
|
||||
32
frontend/src/lib/i18n/messages/pt/teams.json
Normal file
32
frontend/src/lib/i18n/messages/pt/teams.json
Normal 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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
51
frontend/src/lib/i18n/messages/ru/reviews.json
Normal file
51
frontend/src/lib/i18n/messages/ru/reviews.json
Normal 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."
|
||||
}
|
||||
32
frontend/src/lib/i18n/messages/ru/teams.json
Normal file
32
frontend/src/lib/i18n/messages/ru/teams.json
Normal 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"
|
||||
}
|
||||
@@ -88,5 +88,6 @@
|
||||
"translate.preserveLayoutDesc": "Сохранить макет",
|
||||
"translate.textOnly": "Только текст",
|
||||
"translate.textOnlyDesc": "Быстрый перевод только текста",
|
||||
"translate.unavailableStandard": "Недоступно в стандартном режиме (только ИИ)"
|
||||
"translate.unavailableStandard": "Недоступно в стандартном режиме (только ИИ)",
|
||||
"translate.sameLanguageError": "Исходный и целевой языки должны различаться"
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
51
frontend/src/lib/i18n/messages/zh/reviews.json
Normal file
51
frontend/src/lib/i18n/messages/zh/reviews.json
Normal 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."
|
||||
}
|
||||
32
frontend/src/lib/i18n/messages/zh/teams.json
Normal file
32
frontend/src/lib/i18n/messages/zh/teams.json
Normal 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"
|
||||
}
|
||||
@@ -88,5 +88,6 @@
|
||||
"translate.preserveLayoutDesc": "保留布局",
|
||||
"translate.textOnly": "仅文本",
|
||||
"translate.textOnlyDesc": "仅文本快速翻译",
|
||||
"translate.unavailableStandard": "标准模式下不可用(仅 AI)"
|
||||
"translate.unavailableStandard": "标准模式下不可用(仅 AI)",
|
||||
"translate.sameLanguageError": "源语言和目标语言必须不同"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user