feat(admin): canaux IA par palier (z.ai/DeepSeek directs), emails generes par IA, reductions Stripe
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 2m40s

- Chaque modele d'un palier devient une route {canal, modele} : openrouter,
  deepseek direct, zhipu (z.ai, nouveau canal), minimax, openai, xai ;
  repli automatique sur la route suivante si la cle manque, compatibilite
  ascendante avec les anciens reglages a chaines
- Page Fournisseurs refondue en sections ; interrupteurs et chaine de
  secours retablis ; tests de connexion avec delai d'attente
- Emails de relance generes par IA (canal au choix, prompt construit depuis
  le plan marketing, HTML sane, RTL arabe/persan, jamais d'envoi automatique)
- Codes promo : validation locale avant Stripe, Coupon+PromotionCode avec
  double plafond, comptage par session (webhook+sync=1 fois), ecriture
  atomique, relier-a-Stripe, suppression, validation publique au checkout
- Page Tarifs : champ code promo (?promo=) avec verification traduite
- Tests : 1357 verts (facturation par palier testee dans le worker,
  point de contact providers/available, promos, generation)
This commit is contained in:
2026-09-05 19:46:33 +02:00
parent 6c26712687
commit 0a013c679d
47 changed files with 7210 additions and 679 deletions

View File

@@ -17,3 +17,20 @@ export const adminNavItems: AdminNavItem[] = [
{ labelKey: 'admin.nav.system', href: '/admin/system', icon: Settings },
{ labelKey: 'admin.nav.logs', href: '/admin/logs', icon: FileText },
];
/* ── Canaux LLM (source unique côté client) ────────────────────────────────
* Miroir de services/providers/config.py::LLM_CHANNELS. Utilisé par la page
* « Modèles & abonnements » (routes de palier) et la page Marketing
* (génération d'emails par IA) — ne pas dupliquer ailleurs. */
export const LLM_CHANNEL_OPTIONS: { id: string; labelKey: string }[] = [
{ id: "openrouter", labelKey: "admin.models.channelOpenrouter" },
{ id: "openrouter_premium", labelKey: "admin.models.channelOpenrouterPremium" },
{ id: "deepseek", labelKey: "admin.models.channelDeepseek" },
{ id: "zhipu", labelKey: "admin.models.channelZhipu" },
{ id: "minimax", labelKey: "admin.models.channelMinimax" },
{ id: "openai", labelKey: "admin.models.channelOpenai" },
{ id: "zai", labelKey: "admin.models.channelZai" },
];
export const channelLabelKey = (id: string): string =>
LLM_CHANNEL_OPTIONS.find((c) => c.id === id)?.labelKey ?? id;

View File

@@ -10,6 +10,11 @@ import {
AlertCircle,
CheckCircle2,
Eye,
Sparkles,
Tag,
Ban,
Link2,
Trash2,
} from "lucide-react";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
@@ -22,10 +27,15 @@ import { useNotification } from "@/components/ui/notification";
import { useTranslationStore } from "@/lib/store";
import { API_BASE } from "@/lib/config";
import { useI18n } from "@/lib/i18n";
import { LLM_CHANNEL_OPTIONS } from "../constants";
import type {
MarketingAudiencesResponse,
MarketingEmailGenerateRequest,
MarketingEmailGenerateResponse,
MarketingEmailHistoryEntry,
MarketingEmailSendResponse,
PromoCode,
PromoListResponse,
} from "../types";
const getToken = () => useTranslationStore.getState().settings.adminToken ?? "";
@@ -42,6 +52,33 @@ const AUDIENCE_LABEL_KEYS: Record<string, string> = {
all_users: "admin.marketing.audienceAllUsers",
};
/** Langues proposées pour l'email généré. */
const EMAIL_LANGUAGES: { id: string; labelKey: string }[] = [
{ id: "Français", labelKey: "admin.marketing.langFr" },
{ id: "English", labelKey: "admin.marketing.langEn" },
{ id: "Deutsch", labelKey: "admin.marketing.langDe" },
{ id: "Español", labelKey: "admin.marketing.langEs" },
{ id: "Italiano", labelKey: "admin.marketing.langIt" },
{ id: "Português", labelKey: "admin.marketing.langPt" },
{ id: "Nederlands", labelKey: "admin.marketing.langNl" },
{ id: "العربية", labelKey: "admin.marketing.langAr" },
{ id: "فارسی", labelKey: "admin.marketing.langFa" },
{ id: "日本語", labelKey: "admin.marketing.langJa" },
{ id: "한국어", labelKey: "admin.marketing.langKo" },
{ id: "Русский", labelKey: "admin.marketing.langRu" },
{ id: "中文", labelKey: "admin.marketing.langZh" },
];
const TONES: { id: string; labelKey: string }[] = [
{ id: "friendly", labelKey: "admin.marketing.toneFriendly" },
{ id: "professional", labelKey: "admin.marketing.toneProfessional" },
{ id: "urgent", labelKey: "admin.marketing.toneUrgent" },
{ id: "festive", labelKey: "admin.marketing.toneFestive" },
];
/** Forfaits éligibles à un code promo (le forfait gratuit n'est pas éligible). */
const PROMO_PLANS = ["all", "starter", "pro", "business", "enterprise"];
export default function AdminMarketingPage() {
const { t } = useI18n();
const { success, error } = useNotification();
@@ -62,6 +99,28 @@ export default function AdminMarketingPage() {
* l'envoi reste possible : le serveur valide l'envoi test). */
const [hashSupported, setHashSupported] = useState(true);
// ── Génération par IA ──
const [genProvider, setGenProvider] = useState("openrouter");
const [genModel, setGenModel] = useState("");
const [brief, setBrief] = useState("");
const [genLanguage, setGenLanguage] = useState("Français");
const [genTone, setGenTone] = useState("friendly");
const [genPromoCode, setGenPromoCode] = useState("");
const [isGenerating, setIsGenerating] = useState(false);
// ── Réductions ──
const [promos, setPromos] = useState<PromoCode[]>([]);
const [promoCode, setPromoCode] = useState("");
const [promoType, setPromoType] = useState<"percent" | "amount">("percent");
const [promoValue, setPromoValue] = useState("10");
const [promoPlans, setPromoPlans] = useState<string[]>(["all"]);
const [promoMaxRedemptions, setPromoMaxRedemptions] = useState("");
const [promoExpiresAt, setPromoExpiresAt] = useState("");
const [isCreatingPromo, setIsCreatingPromo] = useState(false);
const [deactivatingCode, setDeactivatingCode] = useState<string | null>(null);
const [deletingCode, setDeletingCode] = useState<string | null>(null);
const [linkingCode, setLinkingCode] = useState<string | null>(null);
useEffect(() => {
if (!crypto?.subtle) {
setHashSupported(false);
@@ -105,9 +164,18 @@ export default function AdminMarketingPage() {
setHistory(json.data ?? []);
};
const loadPromos = async () => {
const response = await fetch(`${API_BASE}/api/v1/admin/marketing/promos`, {
headers: { Authorization: `Bearer ${getToken()}` },
});
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const json: PromoListResponse = await response.json();
setPromos(json.data.promos ?? []);
};
useEffect(() => {
setIsLoading(true);
Promise.all([loadAudiences(), loadHistory()])
Promise.all([loadAudiences(), loadHistory(), loadPromos()])
.catch((e) =>
error({ title: t("admin.marketing.loadErrorTitle"), description: String(e.message ?? e) })
)
@@ -120,6 +188,11 @@ export default function AdminMarketingPage() {
return labelKey ? t(labelKey) : key;
};
const channelLabel = (id: string): string => {
const entry = LLM_CHANNEL_OPTIONS.find((c) => c.id === id);
return entry ? t(entry.labelKey) : id;
};
const recipientCount = audiences[audience] ?? 0;
/** Simple indicateur (non bloquant) : un test réussi de ce contenu exact
@@ -131,6 +204,52 @@ export default function AdminMarketingPage() {
(h) => h.test_mode && h.sent_count > 0 && h.content_hash === contentHash
);
// ── Génération IA : remplit sujet + HTML, n'envoie RIEN ──
const generateEmail = async () => {
if (!brief.trim()) {
error({ title: t("admin.marketing.generateFailedTitle"), description: t("admin.marketing.briefRequired") });
return;
}
setIsGenerating(true);
try {
const payload: MarketingEmailGenerateRequest = {
provider: genProvider,
model: genModel.trim() || undefined,
brief: brief.trim(),
language: genLanguage,
tone: genTone || undefined,
promo_code: genPromoCode || undefined,
};
const response = await fetch(`${API_BASE}/api/v1/admin/marketing/email/generate`, {
method: "POST",
headers: {
Authorization: `Bearer ${getToken()}`,
"Content-Type": "application/json",
},
body: JSON.stringify(payload),
});
const json = await response.json().catch(() => null);
const d = json?.data as MarketingEmailGenerateResponse["data"] | undefined;
if (response.ok && d?.subject && d?.html) {
setSubject(d.subject);
setHtml(d.html);
success({ title: t("admin.marketing.generatedTitle"), description: t("admin.marketing.generatedDesc", { provider: channelLabel(d.provider) }) });
} else if (response.ok) {
// 200 sans contenu exploitable : message d'erreur, jamais d'exception.
error({ title: t("admin.marketing.generateFailedTitle"), description: t("admin.marketing.generateEmpty") });
} else {
error({
title: t("admin.marketing.generateFailedTitle"),
description: json?.message || `HTTP ${response.status}`,
});
}
} catch {
error({ title: t("admin.marketing.generateFailedTitle"), description: t("admin.marketing.networkDesc") });
} finally {
setIsGenerating(false);
}
};
const sendTest = async () => {
if (!subject.trim() || !html.trim()) {
error({ title: t("admin.marketing.incompleteTitle"), description: t("admin.marketing.incompleteDesc") });
@@ -224,6 +343,153 @@ export default function AdminMarketingPage() {
}
};
// ── Réductions ──
const createPromo = async () => {
const code = promoCode.trim().toUpperCase();
const value = parseFloat(promoValue);
if (!code || !Number.isFinite(value)) {
error({ title: t("admin.marketing.promoCreateFailedTitle"), description: t("admin.marketing.promoInvalidInput") });
return;
}
setIsCreatingPromo(true);
try {
const body: Record<string, unknown> = {
code,
type: promoType,
value,
plans: promoPlans.length ? promoPlans : ["all"],
};
if (promoMaxRedemptions.trim()) body.max_redemptions = parseInt(promoMaxRedemptions, 10);
if (promoExpiresAt.trim()) {
// « jusqu'au 10 » = valable tout le 10 : 23:59:59 UTC du jour choisi.
body.expires_at = new Date(`${promoExpiresAt.trim()}T23:59:59Z`).toISOString();
}
const response = await fetch(`${API_BASE}/api/v1/admin/marketing/promos`, {
method: "POST",
headers: {
Authorization: `Bearer ${getToken()}`,
"Content-Type": "application/json",
},
body: JSON.stringify(body),
});
const json = await response.json().catch(() => ({}));
if (response.ok) {
success({ title: t("admin.marketing.promoCreatedTitle"), description: t("admin.marketing.promoCreatedDesc", { code }) });
setPromoCode("");
setPromoMaxRedemptions("");
setPromoExpiresAt("");
await loadPromos();
} else {
error({
title: t("admin.marketing.promoCreateFailedTitle"),
description: json.message || `HTTP ${response.status}`,
});
}
} catch {
error({ title: t("admin.marketing.promoCreateFailedTitle"), description: t("admin.marketing.networkDesc") });
} finally {
setIsCreatingPromo(false);
}
};
const deactivatePromo = async (code: string) => {
setDeactivatingCode(code);
try {
const response = await fetch(
`${API_BASE}/api/v1/admin/marketing/promos/${encodeURIComponent(code)}/deactivate`,
{
method: "POST",
headers: { Authorization: `Bearer ${getToken()}` },
}
);
const json = await response.json().catch(() => ({}));
if (response.ok) {
success({ title: t("admin.marketing.promoDeactivatedTitle"), description: t("admin.marketing.promoDeactivatedDesc", { code }) });
await loadPromos();
} else {
error({
title: t("admin.marketing.promoDeactivateFailedTitle"),
description: json.message || `HTTP ${response.status}`,
});
}
} catch {
error({ title: t("admin.marketing.promoDeactivateFailedTitle"), description: t("admin.marketing.networkDesc") });
} finally {
setDeactivatingCode(null);
}
};
const deletePromo = async (code: string) => {
if (!window.confirm(t("admin.marketing.promoDeleteConfirm", { code }))) return;
setDeletingCode(code);
try {
const response = await fetch(
`${API_BASE}/api/v1/admin/marketing/promos/${encodeURIComponent(code)}`,
{
method: "DELETE",
headers: { Authorization: `Bearer ${getToken()}` },
}
);
const json = await response.json().catch(() => ({}));
if (response.ok) {
success({ title: t("admin.marketing.promoDeletedTitle"), description: t("admin.marketing.promoDeletedDesc", { code }) });
await loadPromos();
} else {
error({
title: t("admin.marketing.promoDeleteFailedTitle"),
description: json.message || `HTTP ${response.status}`,
});
}
} catch {
error({ title: t("admin.marketing.promoDeleteFailedTitle"), description: t("admin.marketing.networkDesc") });
} finally {
setDeletingCode(null);
}
};
const linkPromoToStripe = async (code: string) => {
setLinkingCode(code);
try {
const response = await fetch(
`${API_BASE}/api/v1/admin/marketing/promos/${encodeURIComponent(code)}/link-stripe`,
{
method: "POST",
headers: { Authorization: `Bearer ${getToken()}` },
}
);
const json = await response.json().catch(() => ({}));
if (response.ok) {
success({ title: t("admin.marketing.promoLinkedTitle"), description: t("admin.marketing.promoLinkedDesc", { code }) });
await loadPromos();
} else {
error({
title: t("admin.marketing.promoLinkFailedTitle"),
description: json.message || `HTTP ${response.status}`,
});
}
} catch {
error({ title: t("admin.marketing.promoLinkFailedTitle"), description: t("admin.marketing.networkDesc") });
} finally {
setLinkingCode(null);
}
};
const togglePromoPlan = (plan: string) => {
setPromoPlans((prev) => {
if (plan === "all") return prev.includes("all") ? [] : ["all"];
const next = prev.filter((p) => p !== "all");
return next.includes(plan) ? next.filter((p) => p !== plan) : [...next, plan];
});
};
const activePromos = useMemo(() => promos.filter((p) => p.active), [promos]);
const promoLabel = (p: PromoCode): string => {
const value =
p.type === "percent" ? `-${p.value} %` : `-${p.value}`;
return `${p.code} (${value})`;
};
const previewDoc = useMemo(
() =>
html.trim() ||
@@ -250,176 +516,490 @@ export default function AdminMarketingPage() {
<Loader2 className="size-8 animate-spin text-muted-foreground" />
</div>
) : (
<div className="grid gap-6 lg:grid-cols-3">
{/* ── Composition ── */}
<Card className="lg:col-span-2 overflow-visible">
<CardHeader>
<CardTitle className="text-base">{t("admin.marketing.newCampaign")}</CardTitle>
<CardDescription>
{t("admin.marketing.unsubscribedNote", { count: unsubscribedCount })}
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid gap-4 sm:grid-cols-2">
<div className="space-y-2">
<Label>{t("admin.marketing.audience")}</Label>
<Select value={audience} onValueChange={setAudience}>
<SelectTrigger>
<SelectValue placeholder={t("admin.marketing.audiencePlaceholder")} />
</SelectTrigger>
<SelectContent>
{Object.keys(audiences)
.sort((a, b) => (audiences[b] ?? 0) - (audiences[a] ?? 0))
.map((key) => (
<SelectItem key={key} value={key}>
{audienceLabel(key)} ({audiences[key]})
</SelectItem>
))}
</SelectContent>
</Select>
<p className="text-xs text-muted-foreground">
{t("admin.marketing.recipientsAvailable", { count: recipientCount })}
</p>
</div>
<div className="space-y-2">
<Label htmlFor="marketing-test-email">{t("admin.marketing.testEmail")}</Label>
<Input
id="marketing-test-email"
type="email"
placeholder={t("admin.marketing.testEmailPlaceholder")}
value={testEmail}
onChange={(e) => setTestEmail(e.target.value)}
/>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="marketing-subject">{t("admin.marketing.subject")}</Label>
<Input
id="marketing-subject"
placeholder={t("admin.marketing.subjectPlaceholder")}
value={subject}
onChange={(e) => setSubject(e.target.value)}
maxLength={200}
/>
</div>
<div className="space-y-2">
<div className="flex items-center justify-between">
<Label htmlFor="marketing-html">{t("admin.marketing.html")}</Label>
<Button
variant="ghost"
size="sm"
className="h-7 gap-1.5 text-xs"
onClick={() => setShowPreview((v) => !v)}
>
<Eye className="size-3" />
{showPreview ? t("admin.marketing.hidePreview") : t("admin.marketing.preview")}
</Button>
</div>
<Textarea
id="marketing-html"
className="min-h-48 font-mono text-xs"
placeholder={t("admin.marketing.htmlPlaceholder")}
value={html}
onChange={(e) => setHtml(e.target.value)}
/>
<p className="text-xs text-muted-foreground">{t("admin.marketing.footerNote")}</p>
{showPreview && (
<iframe
title={t("admin.marketing.previewTitle")}
srcDoc={previewDoc}
className="h-72 w-full rounded-lg border border-border bg-white"
sandbox=""
/>
)}
</div>
<div className="flex flex-wrap items-center gap-3 border-t border-border/60 pt-4">
<Button variant="outline" onClick={sendTest} disabled={isSendingTest} className="gap-2">
{isSendingTest ? (
<Loader2 className="size-4 animate-spin" />
) : (
<FlaskConical className="size-4" />
)}
{t("admin.marketing.sendTest")}
</Button>
<Button
onClick={sendReal}
disabled={isSending || recipientCount === 0}
className="gap-2"
>
{isSending ? (
<Loader2 className="size-4 animate-spin" />
) : (
<Send className="size-4" />
)}
{t("admin.marketing.sendReal", { count: recipientCount })}
</Button>
{!hashSupported && (
<span className="flex items-center gap-1.5 text-xs text-muted-foreground">
<AlertCircle className="size-3.5" />
{t("admin.marketing.localCheckUnavailable")}
</span>
)}
{hashSupported && testSeenInHistory && (
<span className="flex items-center gap-1.5 text-xs text-green-500">
<CheckCircle2 className="size-3.5" />
{t("admin.marketing.testValidated")}
</span>
)}
{hashSupported && !testSeenInHistory && (
<span className="flex items-center gap-1.5 text-xs text-amber-500">
<AlertCircle className="size-3.5" />
{t("admin.marketing.testRequiredHint")}
</span>
)}
</div>
</CardContent>
</Card>
{/* ── Historique ── */}
<Card>
<CardHeader className="flex flex-row items-center gap-2 pb-3">
<History className="size-4 text-muted-foreground" />
<CardTitle className="text-base">{t("admin.marketing.history")}</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
{history.length === 0 && (
<p className="text-sm text-muted-foreground">{t("admin.marketing.noHistory")}</p>
)}
{history.map((h) => (
<div key={h.id} className="rounded-lg border border-border/60 p-3 text-xs">
<div className="mb-1 flex flex-wrap items-center gap-2">
{h.test_mode ? (
<Badge variant="outline" className="text-[10px]">
{t("admin.marketing.badgeTest")}
</Badge>
) : (
<Badge className="bg-accent/10 text-accent border-accent/30 text-[10px]">
{t("admin.marketing.badgeReal")}
</Badge>
)}
<span className="text-muted-foreground">
{new Date(h.sent_at).toLocaleString("fr-FR")}
</span>
<>
<div className="grid gap-6 lg:grid-cols-3">
{/* ── Composition ── */}
<Card className="lg:col-span-2 overflow-visible">
<CardHeader>
<CardTitle className="text-base">{t("admin.marketing.newCampaign")}</CardTitle>
<CardDescription>
{t("admin.marketing.unsubscribedNote", { count: unsubscribedCount })}
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
{/* ── Générer avec l'IA ── */}
<div className="rounded-lg border border-border/60 bg-muted/30 p-4 space-y-3">
<div className="flex items-center gap-2">
<Sparkles className="size-4 text-accent" />
<span className="text-sm font-semibold">{t("admin.marketing.generateTitle")}</span>
</div>
<p className="text-xs text-muted-foreground">{t("admin.marketing.generateDesc")}</p>
<div className="grid gap-3 sm:grid-cols-3">
<div className="space-y-1.5">
<Label htmlFor="gen-provider">{t("admin.marketing.channel")}</Label>
<Select value={genProvider} onValueChange={setGenProvider}>
<SelectTrigger id="gen-provider">
<SelectValue />
</SelectTrigger>
<SelectContent>
{LLM_CHANNEL_OPTIONS.map((c) => (
<SelectItem key={c.id} value={c.id}>
{t(c.labelKey)}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-1.5">
<Label htmlFor="gen-model">{t("admin.marketing.model")}</Label>
<Input
id="gen-model"
placeholder={t("admin.marketing.modelPlaceholder")}
value={genModel}
onChange={(e) => setGenModel(e.target.value)}
/>
</div>
<div className="space-y-1.5">
<Label htmlFor="gen-promo">{t("admin.marketing.promoOptional")}</Label>
<Select value={genPromoCode || "none"} onValueChange={(v) => setGenPromoCode(v === "none" ? "" : v)}>
<SelectTrigger id="gen-promo">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="none">{t("admin.marketing.promoNone")}</SelectItem>
{activePromos.map((p) => (
<SelectItem key={p.code} value={p.code}>
{promoLabel(p)}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
<div className="space-y-1.5">
<Label htmlFor="gen-brief">{t("admin.marketing.brief")}</Label>
<Textarea
id="gen-brief"
className="min-h-20"
placeholder={t("admin.marketing.briefPlaceholder")}
value={brief}
onChange={(e) => setBrief(e.target.value)}
maxLength={4000}
/>
</div>
<div className="grid gap-3 sm:grid-cols-3">
<div className="space-y-1.5">
<Label htmlFor="gen-language">{t("admin.marketing.language")}</Label>
<Select value={genLanguage} onValueChange={setGenLanguage}>
<SelectTrigger id="gen-language">
<SelectValue />
</SelectTrigger>
<SelectContent>
{EMAIL_LANGUAGES.map((l) => (
<SelectItem key={l.id} value={l.id}>
{t(l.labelKey)}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-1.5">
<Label htmlFor="gen-tone">{t("admin.marketing.tone")}</Label>
<Select value={genTone || "none"} onValueChange={(v) => setGenTone(v === "none" ? "" : v)}>
<SelectTrigger id="gen-tone">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="none">{t("admin.marketing.toneNone")}</SelectItem>
{TONES.map((tone) => (
<SelectItem key={tone.id} value={tone.id}>
{t(tone.labelKey)}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="flex items-end">
<Button
variant="outline"
onClick={generateEmail}
disabled={isGenerating || !brief.trim()}
className="gap-2 w-full"
>
{isGenerating ? (
<Loader2 className="size-4 animate-spin" />
) : (
<Sparkles className="size-4" />
)}
{isGenerating ? t("admin.marketing.generating") : t("admin.marketing.generateBtn")}
</Button>
</div>
</div>
<p className="mb-1 font-medium text-foreground">{h.subject}</p>
<p className="text-muted-foreground">
{audienceLabel(h.audience)} {" "}
{t("admin.marketing.historySent", {
sent: h.sent_count,
total: h.recipients_total,
})}
{h.failed_count > 0
? t("admin.marketing.historyFailedSuffix", { count: h.failed_count })
: ""}
</p>
</div>
))}
</CardContent>
</Card>
</div>
<div className="grid gap-4 sm:grid-cols-2">
<div className="space-y-2">
<Label>{t("admin.marketing.audience")}</Label>
<Select value={audience} onValueChange={setAudience}>
<SelectTrigger>
<SelectValue placeholder={t("admin.marketing.audiencePlaceholder")} />
</SelectTrigger>
<SelectContent>
{Object.keys(audiences)
.sort((a, b) => (audiences[b] ?? 0) - (audiences[a] ?? 0))
.map((key) => (
<SelectItem key={key} value={key}>
{audienceLabel(key)} ({audiences[key]})
</SelectItem>
))}
</SelectContent>
</Select>
<p className="text-xs text-muted-foreground">
{t("admin.marketing.recipientsAvailable", { count: recipientCount })}
</p>
</div>
<div className="space-y-2">
<Label htmlFor="marketing-test-email">{t("admin.marketing.testEmail")}</Label>
<Input
id="marketing-test-email"
type="email"
placeholder={t("admin.marketing.testEmailPlaceholder")}
value={testEmail}
onChange={(e) => setTestEmail(e.target.value)}
/>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="marketing-subject">{t("admin.marketing.subject")}</Label>
<Input
id="marketing-subject"
placeholder={t("admin.marketing.subjectPlaceholder")}
value={subject}
onChange={(e) => setSubject(e.target.value)}
maxLength={200}
/>
</div>
<div className="space-y-2">
<div className="flex items-center justify-between">
<Label htmlFor="marketing-html">{t("admin.marketing.html")}</Label>
<Button
variant="ghost"
size="sm"
className="h-7 gap-1.5 text-xs"
onClick={() => setShowPreview((v) => !v)}
>
<Eye className="size-3" />
{showPreview ? t("admin.marketing.hidePreview") : t("admin.marketing.preview")}
</Button>
</div>
<Textarea
id="marketing-html"
className="min-h-48 font-mono text-xs"
placeholder={t("admin.marketing.htmlPlaceholder")}
value={html}
onChange={(e) => setHtml(e.target.value)}
/>
<p className="text-xs text-muted-foreground">{t("admin.marketing.footerNote")}</p>
{showPreview && (
<iframe
title={t("admin.marketing.previewTitle")}
srcDoc={previewDoc}
className="h-72 w-full rounded-lg border border-border bg-white"
sandbox=""
/>
)}
</div>
<div className="flex flex-wrap items-center gap-3 border-t border-border/60 pt-4">
<Button variant="outline" onClick={sendTest} disabled={isSendingTest} className="gap-2">
{isSendingTest ? (
<Loader2 className="size-4 animate-spin" />
) : (
<FlaskConical className="size-4" />
)}
{t("admin.marketing.sendTest")}
</Button>
<Button
onClick={sendReal}
disabled={isSending || recipientCount === 0}
className="gap-2"
>
{isSending ? (
<Loader2 className="size-4 animate-spin" />
) : (
<Send className="size-4" />
)}
{t("admin.marketing.sendReal", { count: recipientCount })}
</Button>
{!hashSupported && (
<span className="flex items-center gap-1.5 text-xs text-muted-foreground">
<AlertCircle className="size-3.5" />
{t("admin.marketing.localCheckUnavailable")}
</span>
)}
{hashSupported && testSeenInHistory && (
<span className="flex items-center gap-1.5 text-xs text-green-500">
<CheckCircle2 className="size-3.5" />
{t("admin.marketing.testValidated")}
</span>
)}
{hashSupported && !testSeenInHistory && (
<span className="flex items-center gap-1.5 text-xs text-amber-500">
<AlertCircle className="size-3.5" />
{t("admin.marketing.testRequiredHint")}
</span>
)}
</div>
</CardContent>
</Card>
{/* ── Historique ── */}
<Card>
<CardHeader className="flex flex-row items-center gap-2 pb-3">
<History className="size-4 text-muted-foreground" />
<CardTitle className="text-base">{t("admin.marketing.history")}</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
{history.length === 0 && (
<p className="text-sm text-muted-foreground">{t("admin.marketing.noHistory")}</p>
)}
{history.map((h) => (
<div key={h.id} className="rounded-lg border border-border/60 p-3 text-xs">
<div className="mb-1 flex flex-wrap items-center gap-2">
{h.test_mode ? (
<Badge variant="outline" className="text-[10px]">
{t("admin.marketing.badgeTest")}
</Badge>
) : (
<Badge className="bg-accent/10 text-accent border-accent/30 text-[10px]">
{t("admin.marketing.badgeReal")}
</Badge>
)}
<span className="text-muted-foreground">
{new Date(h.sent_at).toLocaleString("fr-FR")}
</span>
</div>
<p className="mb-1 font-medium text-foreground">{h.subject}</p>
<p className="text-muted-foreground">
{audienceLabel(h.audience)} {" "}
{t("admin.marketing.historySent", {
sent: h.sent_count,
total: h.recipients_total,
})}
{h.failed_count > 0
? t("admin.marketing.historyFailedSuffix", { count: h.failed_count })
: ""}
</p>
</div>
))}
</CardContent>
</Card>
</div>
{/* ── Réductions (codes promo) ── */}
<div className="grid gap-6 lg:grid-cols-3">
<Card className="lg:col-span-2">
<CardHeader className="flex flex-row items-center gap-2 pb-3">
<Tag className="size-4 text-muted-foreground" />
<div>
<CardTitle className="text-base">{t("admin.marketing.promoTitle")}</CardTitle>
<CardDescription>{t("admin.marketing.promoDesc")}</CardDescription>
</div>
</CardHeader>
<CardContent className="space-y-3">
{promos.length === 0 && (
<p className="text-sm text-muted-foreground">{t("admin.marketing.promoEmpty")}</p>
)}
{promos.map((p) => (
<div key={p.code} className="rounded-lg border border-border/60 p-3 text-xs">
<div className="mb-1 flex flex-wrap items-center gap-2">
<span className="font-mono font-semibold text-foreground">{p.code}</span>
<Badge variant="secondary" className="text-[10px]">
{p.type === "percent" ? `-${p.value} %` : `-${p.value}`}
</Badge>
{p.active ? (
<Badge className="bg-green-500/10 text-green-600 border-green-500/30 text-[10px]">
{t("admin.marketing.promoActive")}
</Badge>
) : (
<Badge variant="outline" className="text-[10px]">
{t("admin.marketing.promoInactive")}
</Badge>
)}
{p.stripe_mode === "local_only" && (
<Badge variant="outline" className="text-[10px] text-amber-500 border-amber-500/40">
{t("admin.marketing.promoLocalOnly")}
</Badge>
)}
{p.active && p.stripe_mode === "local_only" && (
<Button
variant="ghost"
size="sm"
className="ms-auto h-6 gap-1 px-2 text-[11px] text-blue-500"
onClick={() => linkPromoToStripe(p.code)}
disabled={linkingCode === p.code}
>
{linkingCode === p.code ? (
<Loader2 className="size-3 animate-spin" />
) : (
<Link2 className="size-3" />
)}
{t("admin.marketing.promoLinkStripe")}
</Button>
)}
{p.active && (
<Button
variant="ghost"
size="sm"
className={"h-6 gap-1 px-2 text-[11px] text-red-500 " + (p.stripe_mode === "local_only" ? "" : "ms-auto")}
onClick={() => deactivatePromo(p.code)}
disabled={deactivatingCode === p.code}
>
{deactivatingCode === p.code ? (
<Loader2 className="size-3 animate-spin" />
) : (
<Ban className="size-3" />
)}
{t("admin.marketing.promoDeactivate")}
</Button>
)}
<Button
variant="ghost"
size="sm"
className="h-6 w-6 p-0 text-red-500"
onClick={() => deletePromo(p.code)}
disabled={deletingCode === p.code}
aria-label={t("admin.marketing.promoDelete")}
>
{deletingCode === p.code ? (
<Loader2 className="size-3 animate-spin" />
) : (
<Trash2 className="size-3" />
)}
</Button>
</div>
<p className="text-muted-foreground">
{t("admin.marketing.promoUsage", {
used: p.times_used,
max: p.max_redemptions ?? "∞",
})}
{" · "}
{t("admin.marketing.promoPlansLabel")}{" "}
{(p.plans ?? []).map((plan) =>
plan === "all" ? t("admin.marketing.promoAllPlans") : plan
).join(", ")}
{" · "}
{p.expires_at
? t("admin.marketing.promoExpiresOn", {
date: new Date(p.expires_at).toLocaleDateString("fr-FR"),
})
: t("admin.marketing.promoNeverExpires")}
</p>
</div>
))}
</CardContent>
</Card>
<Card className="overflow-visible">
<CardHeader className="pb-3">
<CardTitle className="text-base">{t("admin.marketing.promoCreateTitle")}</CardTitle>
<CardDescription>{t("admin.marketing.promoCreateDesc")}</CardDescription>
</CardHeader>
<CardContent className="space-y-3">
<div className="space-y-1.5">
<Label htmlFor="promo-code">{t("admin.marketing.promoCode")}</Label>
<Input
id="promo-code"
placeholder="LANCEMENT10"
value={promoCode}
onChange={(e) => setPromoCode(e.target.value.toUpperCase())}
maxLength={24}
/>
</div>
<div className="grid grid-cols-2 gap-2">
<div className="space-y-1.5">
<Label htmlFor="promo-type">{t("admin.marketing.promoType")}</Label>
<Select value={promoType} onValueChange={(v) => setPromoType(v as "percent" | "amount")}>
<SelectTrigger id="promo-type">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="percent">{t("admin.marketing.promoTypePercent")}</SelectItem>
<SelectItem value="amount">{t("admin.marketing.promoTypeAmount")}</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-1.5">
<Label htmlFor="promo-value">{t("admin.marketing.promoValue")}</Label>
<Input
id="promo-value"
type="number"
min={promoType === "percent" ? 1 : 0.01}
max={promoType === "percent" ? 100 : 10000}
value={promoValue}
onChange={(e) => setPromoValue(e.target.value)}
/>
</div>
</div>
<div className="space-y-1.5">
<Label>{t("admin.marketing.promoPlans")}</Label>
<div className="flex flex-wrap gap-1.5">
{PROMO_PLANS.map((plan) => (
<button
key={plan}
type="button"
onClick={() => togglePromoPlan(plan)}
className={
"rounded-full border px-2.5 py-1 text-[11px] transition-colors " +
(promoPlans.includes(plan)
? "border-accent bg-accent/10 text-accent"
: "border-border text-muted-foreground hover:text-foreground")
}
>
{plan === "all" ? t("admin.marketing.promoAllPlans") : plan}
</button>
))}
</div>
</div>
<div className="grid grid-cols-2 gap-2">
<div className="space-y-1.5">
<Label htmlFor="promo-max">{t("admin.marketing.promoMaxRedemptions")}</Label>
<Input
id="promo-max"
type="number"
min={1}
placeholder={t("admin.marketing.promoMaxPlaceholder")}
value={promoMaxRedemptions}
onChange={(e) => setPromoMaxRedemptions(e.target.value)}
/>
</div>
<div className="space-y-1.5">
<Label htmlFor="promo-expires">{t("admin.marketing.promoExpires")}</Label>
<Input
id="promo-expires"
type="date"
value={promoExpiresAt}
onChange={(e) => setPromoExpiresAt(e.target.value)}
/>
<p className="text-[11px] text-muted-foreground">
{t("admin.marketing.promoExpiryHint")}
</p>
</div>
</div>
<Button onClick={createPromo} disabled={isCreatingPromo || !promoCode.trim()} className="w-full gap-2">
{isCreatingPromo ? (
<Loader2 className="size-4 animate-spin" />
) : (
<Tag className="size-4" />
)}
{isCreatingPromo ? t("admin.marketing.promoCreating") : t("admin.marketing.promoCreate")}
</Button>
</CardContent>
</Card>
</div>
</>
)}
</div>
);

View File

@@ -5,12 +5,14 @@ import { Cpu, Save, Loader2, ArrowUp, ArrowDown, X, Plus, RefreshCw } from "luci
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { useNotification } from "@/components/ui/notification";
import { ModelCombobox, ModelOption } from "@/components/ui/model-combobox";
import { useTranslationStore } from "@/lib/store";
import { API_BASE } from "@/lib/config";
import { useI18n } from "@/lib/i18n";
import type { AiTierSettings, AiTiersSettings } from "../types";
import { LLM_CHANNEL_OPTIONS, channelLabelKey } from "../constants";
import type { AiTierRoute, AiTierSettings, AiTiersSettings } from "../types";
interface FullSettings {
[key: string]: unknown;
@@ -18,7 +20,33 @@ interface FullSettings {
const getToken = () => useTranslationStore.getState().settings.adminToken ?? "";
/** Éditeur d'un palier : liste ordonnée (ordre = priorité de secours) + radio « par défaut ». */
/** L'ancien format (chaînes OpenRouter) est converti en routes à l'affichage. */
function toRoutes(tier: AiTierSettings): AiTierRoute[] {
if (tier.routes && tier.routes.length > 0) return tier.routes;
const legacy: AiTierRoute[] = [];
if (tier.default_model) legacy.push({ provider: "openrouter", model: tier.default_model });
for (const m of tier.models || []) {
if (m && !legacy.some((r) => r.model === m)) legacy.push({ provider: "openrouter", model: m });
}
return legacy;
}
/** À la sauvegarde : routes multi-canaux + miroir hérité (modèles openrouter). */
function fromRoutes(routes: AiTierRoute[], previous: AiTierSettings): AiTierSettings {
const openrouterModels = routes.filter((r) => r.provider === "openrouter").map((r) => r.model);
const defaultRoute = routes[0];
const legacyDefault =
defaultRoute?.provider === "openrouter"
? defaultRoute.model
: (previous.default_model || "");
return {
routes,
models: openrouterModels,
default_model: legacyDefault,
};
}
/** Éditeur d'un palier : liste ordonnée de routes (ordre = priorité de secours). */
function TierEditor({
tier,
onChange,
@@ -33,42 +61,63 @@ function TierEditor({
catalogLoading: boolean;
}) {
const { t } = useI18n();
const { error } = useNotification();
const [adding, setAdding] = useState(false);
const [newProvider, setNewProvider] = useState("openrouter");
const [newModel, setNewModel] = useState("");
const routes = toRoutes(tier);
const setRoutes = (next: AiTierRoute[]) => onChange(fromRoutes(next, tier));
/** Vrai si un couple canal+modèle existe déjà ailleurs dans la liste. */
const isDuplicate = (provider: string, model: string, exceptIndex = -1): boolean =>
routes.some(
(r, i) =>
i !== exceptIndex && r.provider === provider && r.model === model
);
const move = (index: number, dir: -1 | 1) => {
const models = [...tier.models];
const next = [...routes];
const target = index + dir;
if (target < 0 || target >= models.length) return;
[models[index], models[target]] = [models[target], models[index]];
onChange({ ...tier, models });
if (target < 0 || target >= next.length) return;
[next[index], next[target]] = [next[target], next[index]];
setRoutes(next);
};
const remove = (index: number) => {
const models = tier.models.filter((_, i) => i !== index);
onChange({ ...tier, models });
setRoutes(routes.filter((_, i) => i !== index));
};
const add = (id: string) => {
const idClean = id.trim();
if (!idClean || tier.models.includes(idClean)) {
setAdding(false);
/** Par défaut = première route essayée : cliquer le radio met la route en tête. */
const setDefault = (index: number) => {
if (index === 0) return;
const next = [...routes];
const [route] = next.splice(index, 1);
setRoutes([route, ...next]);
};
const add = () => {
const model = newModel.trim();
if (!model) return;
if (isDuplicate(newProvider, model)) {
error({ title: t("admin.models.duplicateRouteTitle"), description: t("admin.models.duplicateRouteDesc") });
return;
}
onChange({ ...tier, models: [...tier.models, idClean] });
setRoutes([...routes, { provider: newProvider, model }]);
setNewModel("");
setAdding(false);
};
const defaultModel = tier.default_model || tier.models[0] || "";
return (
<div className="space-y-2">
{tier.models.map((model, i) => {
const isDefault = model === defaultModel;
{routes.map((route, i) => {
const isDefault = i === 0;
return (
<div
key={model}
key={`${route.provider}/${route.model}`}
className={
"flex items-center gap-2 rounded-lg border px-3 py-2 " +
"flex flex-wrap items-center gap-2 rounded-lg border px-3 py-2 " +
(isDefault ? "border-accent/50 bg-accent/5" : "border-border/60 bg-card")
}
>
@@ -76,8 +125,8 @@ function TierEditor({
type="button"
role="radio"
aria-checked={isDefault}
aria-label={t("admin.models.setDefault", { model })}
onClick={() => onChange({ ...tier, default_model: model })}
aria-label={t("admin.models.setDefault", { model: `${route.provider}/${route.model}` })}
onClick={() => setDefault(i)}
className={
"flex size-4 shrink-0 items-center justify-center rounded-full border " +
(isDefault ? "border-accent" : "border-muted-foreground/40")
@@ -85,13 +134,37 @@ function TierEditor({
>
{isDefault && <span className="size-2 rounded-full bg-accent" />}
</button>
<span className="flex-1 cursor-pointer font-mono text-xs" onClick={() => onChange({ ...tier, default_model: model })}>
{model}
<Select
value={route.provider}
onValueChange={(value) => {
if (isDuplicate(value, route.model, i)) {
error({ title: t("admin.models.duplicateRouteTitle"), description: t("admin.models.duplicateRouteDesc") });
return;
}
const next = [...routes];
next[i] = { ...route, provider: value };
setRoutes(next);
}}
>
<SelectTrigger className="h-7 w-44 shrink-0 text-xs">
<SelectValue />
</SelectTrigger>
<SelectContent>
{LLM_CHANNEL_OPTIONS.map((p) => (
<SelectItem key={p.id} value={p.id} className="text-xs">
{t(p.labelKey)}
</SelectItem>
))}
</SelectContent>
</Select>
<span className="flex-1 cursor-pointer font-mono text-xs" onClick={() => setDefault(i)}>
{route.model}
{isDefault ? (
<Badge variant="secondary" className="ms-2 font-sans text-[10px]">
{t("admin.models.defaultBadge")}
</Badge>
) : i === 0 ? (
) : null}
{isDefault ? (
<span className="ms-2 font-sans text-[10px] font-bold uppercase tracking-wider text-muted-foreground">
{t("admin.models.fallbackFirst")}
</span>
@@ -112,7 +185,7 @@ function TierEditor({
size="sm"
className="h-6 w-6 p-0"
onClick={() => move(i, 1)}
disabled={i === tier.models.length - 1}
disabled={i === routes.length - 1}
aria-label={t("admin.models.moveDown")}
>
<ArrowDown className="size-3" />
@@ -130,24 +203,56 @@ function TierEditor({
);
})}
{tier.models.length === 0 && (
{routes.length === 0 && (
<p className="text-xs text-muted-foreground">{t("admin.models.emptyTier")}</p>
)}
{adding ? (
<div className="space-y-2 rounded-lg border border-dashed border-border p-3">
<ModelCombobox
value=""
onChange={add}
models={catalog}
isLoading={catalogLoading}
onFetchModels={onLoadCatalog}
providerLabel="OpenRouter"
placeholder="provider/nom-du-modèle"
/>
<Button variant="ghost" size="sm" onClick={() => setAdding(false)}>
{t("admin.models.cancel")}
</Button>
<div className="grid gap-2 sm:grid-cols-[auto_1fr]">
<Select value={newProvider} onValueChange={setNewProvider}>
<SelectTrigger className="w-44 text-xs">
<SelectValue />
</SelectTrigger>
<SelectContent>
{LLM_CHANNEL_OPTIONS.map((p) => (
<SelectItem key={p.id} value={p.id} className="text-xs">
{t(p.labelKey)}
</SelectItem>
))}
</SelectContent>
</Select>
{newProvider === "openrouter" ? (
<ModelCombobox
value={newModel}
onChange={setNewModel}
models={catalog}
isLoading={catalogLoading}
onFetchModels={onLoadCatalog}
providerLabel="OpenRouter"
placeholder="provider/nom-du-modèle"
/>
) : (
<ModelCombobox
value={newModel}
onChange={setNewModel}
models={[]}
isLoading={false}
onFetchModels={() => {}}
providerLabel={t(channelLabelKey(newProvider))}
placeholder={t("admin.models.customModelPlaceholder")}
/>
)}
</div>
<p className="text-xs text-muted-foreground">{t("admin.models.routeHint")}</p>
<div className="flex gap-2">
<Button variant="outline" size="sm" onClick={add} disabled={!newModel.trim()}>
{t("admin.models.addRouteConfirm")}
</Button>
<Button variant="ghost" size="sm" onClick={() => setAdding(false)}>
{t("admin.models.cancel")}
</Button>
</div>
</div>
) : (
<div className="flex items-center gap-2 pt-1">
@@ -200,7 +305,10 @@ export default function AdminModelsPage() {
const payload = envelope.data ?? envelope;
setFullSettings(payload);
if (payload.ai_tiers) {
setAiTiers(payload.ai_tiers);
setAiTiers({
essential: { models: [], default_model: "", routes: [], ...payload.ai_tiers.essential },
premium: { models: [], default_model: "", routes: [], ...payload.ai_tiers.premium },
});
}
} else {
error({
@@ -256,7 +364,12 @@ export default function AdminModelsPage() {
if (response.ok) {
const envelope = await response.json();
const payload = envelope.data ?? envelope;
if (payload?.ai_tiers) setAiTiers(payload.ai_tiers);
if (payload?.ai_tiers) {
setAiTiers({
essential: { models: [], default_model: "", routes: [], ...payload.ai_tiers.essential },
premium: { models: [], default_model: "", routes: [], ...payload.ai_tiers.premium },
});
}
success({
title: t("admin.models.savedTitle"),
description: t("admin.models.savedDesc"),

File diff suppressed because it is too large Load Diff

View File

@@ -81,11 +81,20 @@ export interface TranslationStatsResponse {
/* ── Modèles & abonnements (paliers IA) ─────────────────────── */
/** Route d'un palier : un modèle via un canal donné (multi-canaux). */
export interface AiTierRoute {
provider: string; // openrouter | openrouter_premium | deepseek | zhipu | minimax | openai | zai
model: string;
label?: string;
}
export interface AiTierSettings {
/** Modèles actifs — l'ordre est la priorité de secours. */
/** Ancien format : modèles OpenRouter (l'ordre est la priorité de secours). */
models: string[];
/** Modèle par défaut ; vide = premier de la liste = défaut du plan. */
default_model: string;
/** Routes multi-canaux {provider, model} — priment sur models/default_model. */
routes: AiTierRoute[];
}
export interface AiTiersSettings {
@@ -165,3 +174,48 @@ export interface MarketingEmailSendResponse {
};
meta: Record<string, unknown>;
}
/* ── Marketing : génération IA + réductions ─────────────────── */
export interface MarketingEmailGenerateRequest {
provider: string;
model?: string;
brief: string;
language?: string;
tone?: string;
promo_code?: string;
}
export interface MarketingEmailGenerateResponse {
data: {
subject: string;
html: string;
provider: string;
model: string;
language: string;
};
meta: Record<string, unknown>;
}
/** Code promo (réduction) — miroir de data/promo_codes.json côté serveur. */
export interface PromoCode {
code: string;
active: boolean;
type: "percent" | "amount";
value: number;
plans: string[];
max_redemptions: number | null;
times_used: number;
expires_at: string | null;
stripe_coupon_id: string | null;
stripe_promotion_code_id: string | null;
stripe_mode: "test" | "live" | "local_only";
created_at: string;
deactivated_at: string | null;
last_used_at?: string;
}
export interface PromoListResponse {
data: { promos: PromoCode[] };
meta: Record<string, unknown>;
}

View File

@@ -11,6 +11,8 @@ import {
} from "lucide-react";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import {
Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle,
} from "@/components/ui/dialog";
@@ -275,7 +277,15 @@ export default function PricingPage() {
const searchParams = useSearchParams();
const planFromUrl = searchParams.get("plan");
const billingFromUrl = searchParams.get("billing");
const promoFromUrl = (searchParams.get("promo") || "").toUpperCase();
const [isYearly, setIsYearly] = useState(billingFromUrl === "yearly");
/** Code promo (lu depuis ?promo=, modifiable dans la confirmation). */
const [promoCode, setPromoCode] = useState(promoFromUrl);
/** Résultat de la vérification du code (GET /promos/validate). */
const [promoStatus, setPromoStatus] = useState<{
state: "idle" | "checking" | "valid" | "invalid";
text?: string;
}>({ state: "idle" });
const [plans, setPlans] = useState<Plan[]>(STATIC_PLANS);
const [credits, setCredits] = useState<CreditPackage[]>(STATIC_CREDITS);
const [currentPlan, setCurrentPlan] = useState<string | null>(null);
@@ -375,6 +385,7 @@ export default function PricingPage() {
body: JSON.stringify({
plan: planId,
billing_period: isYearly ? "yearly" : "monthly",
promo_code: promoCode.trim() || undefined,
}),
});
@@ -383,10 +394,15 @@ export default function PricingPage() {
try { data = await res.json(); } catch { /* ignore */ }
const url = data.data?.url ?? data.url;
const promoReason = data.promo_reason ?? data.data?.promo_reason;
const backendMessage = data.message ?? data.error ?? data.data?.error;
if (!res.ok) {
throw new Error(backendMessage || t('pricing.error.server', { status: res.status }));
// Refus d'un code promo : cause traduite si elle est connue.
const message = promoReason
? (t(`pricing.promoError.${promoReason}`) || backendMessage)
: (backendMessage || t('pricing.error.server', { status: res.status }));
throw new Error(message);
}
if (url) {
@@ -406,8 +422,39 @@ export default function PricingPage() {
}
};
const handleBuyCredits = async (packageIndex: number) => {
const token = localStorage.getItem("token");
/** Vérifie le code promo AVANT l'ouverture de Stripe (endpoint public). */
const applyPromoCode = async () => {
const code = promoCode.trim();
if (!code || !confirmPlan) return;
setPromoStatus({ state: "checking" });
try {
const res = await fetch(
`${API_BASE}/api/v1/promos/validate?code=${encodeURIComponent(code)}&plan=${encodeURIComponent(confirmPlan.id)}`,
{ cache: "no-store" }
);
const json = await res.json().catch(() => null);
const d = json?.data;
if (d?.valid) {
const text =
typeof d.percent_off === "number"
? `${d.percent_off} %`
: typeof d.amount_off === "number"
? `${d.amount_off}`
: t('pricing.promo.valid');
setPromoStatus({ state: "valid", text });
} else {
const reason = d?.reason_code;
setPromoStatus({
state: "invalid",
text: (reason ? t(`pricing.promoError.${reason}`) : "") || d?.message || t('pricing.promoError.unknown'),
});
}
} catch {
setPromoStatus({ state: "invalid", text: t('pricing.toast.networkError') });
}
};
const handleBuyCredits = async (packageIndex: number) => { const token = localStorage.getItem("token");
if (!token) {
router.push(`/auth/login?redirect=/pricing`);
return;
@@ -499,6 +546,41 @@ export default function PricingPage() {
{t('pricing.confirm.monthlyEquivalent', { price: (confirmPlan.price_yearly / 12).toFixed(2) })}
</p>
)}
{/* Code promo : validé côté serveur avant l'ouverture de Stripe. */}
<div className="space-y-1.5">
<Label htmlFor="promo-code-input">{t('pricing.promo.label')}</Label>
<div className="flex gap-2">
<Input
id="promo-code-input"
className="uppercase"
placeholder={t('pricing.promo.placeholder')}
value={promoCode}
onChange={(e) => {
setPromoCode(e.target.value.toUpperCase());
if (promoStatus.state !== "idle") setPromoStatus({ state: "idle" });
}}
maxLength={24}
/>
<Button
variant="outline"
onClick={applyPromoCode}
disabled={!promoCode.trim() || promoStatus.state === "checking"}
>
{promoStatus.state === "checking"
? t('pricing.promo.checking')
: t('pricing.promo.apply')}
</Button>
</div>
{promoStatus.state === "valid" && promoStatus.text && (
<p className="text-xs font-medium text-emerald-600">{promoStatus.text}</p>
)}
{promoStatus.state === "invalid" && promoStatus.text && (
<p className="text-xs text-red-500">{promoStatus.text}</p>
)}
<p className="text-xs text-muted-foreground">
{t('pricing.promo.subscriptionOnly')}
</p>
</div>
<p className="text-xs text-muted-foreground leading-relaxed">
{t('pricing.confirm.secureNote')}
</p>

View File

@@ -145,5 +145,180 @@
"admin.stats.tiersSub": "أساسي / متميز · كلاسيكي {classic} · آخر {other}",
"admin.stats.unavailable": "إحصائيات الأعمال غير متاحة ({error}).",
"admin.stats.waitlist": "قائمة الانتظار",
"admin.stats.waitlistSub": "مُدرجون في قائمة الانتظار"
"admin.stats.waitlistSub": "مُدرجون في قائمة الانتظار",
"admin.settings.title": "المزودون",
"admin.settings.subtitle": "اضبط صلاحيات المزودين. يمكن أيضًا تعيين مفاتيح API في ملف .env.",
"admin.settings.sectionClassic": "الترجمة الكلاسيكية",
"admin.settings.sectionClassicDesc": "محركات Google للترجمة العادية.",
"admin.settings.sectionDirect": "مزودو الذكاء الاصطناعي المباشرون",
"admin.settings.sectionDirectDesc": "وصول مباشر متوافق مع OpenAI — يُستخدم كقناة لمستوى الذكاء الاصطناعي (صفحة «النماذج والاشتراكات») أو لتوليد الرسائل.",
"admin.settings.sectionGateway": "بوابات متعددة النماذج",
"admin.settings.sectionGatewayDesc": "مفتاح واحد ومئات النماذج. تُدار النماذج لكل مستوى من صفحة «النماذج والاشتراكات».",
"admin.settings.sectionOcr": "التعرف الضوئي (PDF ممسوحة)",
"admin.settings.sectionOcrDesc": "استخراج النص من صفحات الصور قبل الترجمة.",
"admin.settings.sectionEmails": "الرسائل",
"admin.settings.sectionEmailsDesc": "خادم الإرسال للإشعارات ورسائل المتابعة.",
"admin.settings.enabled": "مفعّل",
"admin.settings.disabled": "معطّل",
"admin.settings.envBadge": "المفتاح في .env",
"admin.settings.envConfigured": "مُعيَّن في .env",
"admin.settings.envKeyPlaceholder": "مفتاح معيَّن في .env (اتركه فارغًا لاستخدامه)",
"admin.settings.apiKey": "مفتاح API",
"admin.settings.model": "النموذج",
"admin.settings.baseUrl": "العنوان الأساسي",
"admin.settings.test": "اختبار",
"admin.settings.testing": "جارٍ الاختبار...",
"admin.settings.ok": "تم",
"admin.settings.error": "خطأ",
"admin.settings.failure": "فشل",
"admin.settings.sending": "جارٍ الإرسال...",
"admin.settings.save": "حفظ الإعدادات",
"admin.settings.saving": "جارٍ الحفظ...",
"admin.settings.savedTitle": "✅ تم حفظ الإعدادات",
"admin.settings.savedDesc": "تم حفظ الإعدادات بنجاح.",
"admin.settings.saveErrorTitle": "خطأ في الحفظ",
"admin.settings.loadErrorTitle": "خطأ في التحميل",
"admin.settings.loadErrorDesc": "HTTP {status} — تحقق من رمز المدير.",
"admin.settings.networkErrorTitle": "خطأ في الشبكة",
"admin.settings.networkErrorDesc": "لا يمكن الوصول إلى الخادم الخلفي.",
"admin.settings.networkSaveErrorDesc": "لا يمكن الوصول إلى الخادم الخلفي للحفظ.",
"admin.settings.connectionOk": "الاتصال ناجح",
"admin.settings.modelsFound": "تم العثور على {count} نماذج لـ {provider}",
"admin.settings.providerError": "خطأ {provider}",
"admin.settings.google.title": "Google Translate",
"admin.settings.google.desc": "وصول ويب غير رسمي عبر deep_translator. لا يحتاج مفتاحًا؛ يُنصح بالاستخدام المعقول.",
"admin.settings.googleCloud.title": "Google Cloud Translation (رسمي)",
"admin.settings.googleCloud.desc": "واجهة Google Cloud الرسمية. 500 ألف حرف/شهر مجانًا ثم نحو 20 $ لكل مليون حرف. مفتاح API من console.cloud.google.com.",
"admin.settings.googleCloud.hint": "فعّل Cloud Translation API في Google Cloud Console ثم أنشئ مفتاح API مقيَّدًا بهذه الواجهة.",
"admin.settings.deepseek.title": "DeepSeek (مباشر)",
"admin.settings.deepseek.desc": "واجهة DeepSeek المباشرة المتوافقة مع OpenAI. المفتاح من platform.deepseek.com — صالح كقناة لمستوى الذكاء الاصطناعي.",
"admin.settings.zhipu.title": "z.ai / Zhipu (GLM)",
"admin.settings.zhipu.desc": "وصول مباشر إلى نماذج GLM من z.ai، متوافق مع OpenAI. المفتاح من z.ai — صالح كقناة لمستوى الذكاء الاصطناعي.",
"admin.settings.zhipu.urlHint": "الافتراضي: https://api.z.ai/api/paas/v4 — غيّره فقط إذا كنت تستخدم وسيطًا.",
"admin.settings.minimax.title": "MiniMax (مباشر)",
"admin.settings.minimax.desc": "واجهة MiniMax العامة المتوافقة مع OpenAI. المفتاح من minimax.io — صالح كقناة لمستوى الذكاء الاصطناعي.",
"admin.settings.openai.title": "OpenAI",
"admin.settings.openai.desc": "ترجمات GPT. احصل على مفتاح من platform.openai.com.",
"admin.settings.zai.title": "xAI (Grok)",
"admin.settings.zai.desc": "نماذج Grok من xAI. واجهة متوافقة مع OpenAI. احصل على مفتاحك من x.ai.",
"admin.settings.zai.urlHint": "الافتراضي: https://api.x.ai/v1 — غيّره فقط إذا كنت تستخدم وسيطًا.",
"admin.settings.openrouter.title": "OpenRouter — مفتاح البوابة",
"admin.settings.openrouter.desc": "المستوى الأساسي (خطة Pro). المجموعة الحالية: deepseek/deepseek-v4-flash، z-ai/glm-5.3-flash، minimax/minimax-m3.",
"admin.settings.openrouter.keyLabel": "مفتاح OpenRouter API (مشترك مع Premium)",
"admin.settings.openrouter.envSharedPlaceholder": "مفتاح معيَّن في .env (مشترك مع Premium)",
"admin.settings.openrouter.reminder": "النماذج تُدار من صفحة «النماذج والاشتراكات».",
"admin.settings.openrouterPremium.title": "OpenRouter Premium — مفتاح البوابة",
"admin.settings.openrouterPremium.desc": "المستوى المتقدم (خطة Business). المجموعة الحالية: anthropic/claude-sonnet-5، deepseek/deepseek-v4-pro، z-ai/glm-5.3.",
"admin.settings.openrouterPremium.reminder": "النماذج تُدار من صفحة «النماذج والاشتراكات». يشارك نفس مفتاح OpenRouter.",
"admin.settings.mistral.title": "OCR Mistral (PDF ممسوحة)",
"admin.settings.mistral.desc": "يستخرج النص من ملفات PDF الممسوحة (صفحات الصور) قبل الترجمة. بدون مفتاح تُرفض هذه الملفات برسالة واضحة. نحو 1 € / 1000 صفحة. المفتاح: console.mistral.ai.",
"admin.settings.mistral.keyLabel": "مفتاح API الخاص بـ Mistral",
"admin.settings.mistral.modelLabel": "نموذج OCR",
"admin.settings.mistral.modelHint": "الموصى به: mistral-ocr-latest",
"admin.settings.smtp.title": "البريد عبر SMTP",
"admin.settings.smtp.desc": "إعدادات خادم SMTP لإرسال الرسائل (استعادة كلمة المرور، الإشعارات، رسائل المتابعة).",
"admin.settings.smtp.host": "خادم SMTP",
"admin.settings.smtp.port": "المنفذ",
"admin.settings.smtp.username": "اسم المستخدم",
"admin.settings.smtp.password": "كلمة المرور",
"admin.settings.smtp.fromEmail": "عنوان المرسِل",
"admin.settings.smtp.useTls": "استخدام TLS",
"admin.settings.smtp.sendTest": "إرسال رسالة تجريبية",
"admin.settings.smtp.sent": "أُرسلت",
"admin.settings.smtp.sendFailed": "فشل الإرسال",
"admin.models.channelOpenrouter": "OpenRouter",
"admin.models.channelOpenrouterPremium": "OpenRouter (Premium)",
"admin.models.channelDeepseek": "DeepSeek (مباشر)",
"admin.models.channelZhipu": "z.ai (GLM)",
"admin.models.channelMinimax": "MiniMax (مباشر)",
"admin.models.channelOpenai": "OpenAI",
"admin.models.channelZai": "xAI (Grok)",
"admin.models.customModelPlaceholder": "معرّف-النموذج",
"admin.models.routeHint": "القناة تختار الوصول المباشر أو البوابة؛ معرّفات النماذج تختلف من قناة إلى أخرى.",
"admin.models.addRouteConfirm": "إضافة",
"admin.marketing.generateTitle": "التوليد بالذكاء الاصطناعي",
"admin.marketing.generateDesc": "يُكتب النص استنادًا إلى الخطة التسويقية (التموضع والأرقام المسموح بها). لا يُرسل شيء تلقائيًا: راجع ثم أرسل.",
"admin.marketing.channel": "القناة",
"admin.marketing.model": "النموذج",
"admin.marketing.modelPlaceholder": "افتراضي القناة",
"admin.marketing.promoOptional": "خصم (اختياري)",
"admin.marketing.promoNone": "بدون",
"admin.marketing.brief": "الملخص",
"admin.marketing.briefPlaceholder": "مثال: متابعة المسجلين في قائمة الانتظار والتنبيه لمزايا خطة Pro.",
"admin.marketing.language": "لغة الرسالة",
"admin.marketing.tone": "النبرة",
"admin.marketing.toneNone": "محايدة",
"admin.marketing.generateBtn": "توليد",
"admin.marketing.generating": "جارٍ التوليد...",
"admin.marketing.generatedTitle": "تم توليد الرسالة",
"admin.marketing.generatedDesc": "أُدرج الموضوع وHTML في النموذج — راجعه قبل الإرسال ({provider}).",
"admin.marketing.generateFailedTitle": "تعذّر التوليد",
"admin.marketing.briefRequired": "صِف أولًا المحتوى المطلوب في الملخص.",
"admin.marketing.langFr": "الفرنسية",
"admin.marketing.langEn": "الإنجليزية",
"admin.marketing.langDe": "الألمانية",
"admin.marketing.langEs": "الإسبانية",
"admin.marketing.langIt": "الإيطالية",
"admin.marketing.langPt": "البرتغالية",
"admin.marketing.langNl": "الهولندية",
"admin.marketing.langAr": "العربية",
"admin.marketing.langFa": "الفارسية",
"admin.marketing.langJa": "اليابانية",
"admin.marketing.langKo": "الكورية",
"admin.marketing.langRu": "الروسية",
"admin.marketing.langZh": "الصينية",
"admin.marketing.toneFriendly": "ودّية",
"admin.marketing.toneProfessional": "مهنية",
"admin.marketing.toneUrgent": "عاجلة",
"admin.marketing.toneFestive": "احتفالية",
"admin.marketing.promoTitle": "الخصومات",
"admin.marketing.promoDesc": "أكواد الخصم تُطبَّق على الدفع عبر Stripe؛ يرتفع العداد بعد اكتمال الاشتراك فعليًا.",
"admin.marketing.promoEmpty": "لا توجد أكواد خصم بعد.",
"admin.marketing.promoActive": "نشط",
"admin.marketing.promoInactive": "معطّل",
"admin.marketing.promoLocalOnly": "خارج Stripe",
"admin.marketing.promoDeactivate": "تعطيل",
"admin.marketing.promoUsage": "{used}/{max} استخدامًا",
"admin.marketing.promoPlansLabel": "الخطط:",
"admin.marketing.promoAllPlans": "الكل",
"admin.marketing.promoExpiresOn": "تنتهي في {date}",
"admin.marketing.promoNeverExpires": "بدون انتهاء",
"admin.marketing.promoCreateTitle": "إنشاء خصم",
"admin.marketing.promoCreateDesc": "يُنشأ الكود أيضًا في Stripe (كوبون + كود ترويجي) عندما يكون Stripe مضبوطًا.",
"admin.marketing.promoCode": "الكود",
"admin.marketing.promoType": "النوع",
"admin.marketing.promoTypePercent": "نسبة مئوية",
"admin.marketing.promoTypeAmount": "مبلغ (EUR)",
"admin.marketing.promoValue": "القيمة",
"admin.marketing.promoPlans": "الخطط المشمولة",
"admin.marketing.promoMaxRedemptions": "حد الاستخدام",
"admin.marketing.promoMaxPlaceholder": "غير محدود",
"admin.marketing.promoExpires": "تاريخ الانتهاء",
"admin.marketing.promoCreate": "إنشاء الكود",
"admin.marketing.promoCreating": "جارٍ الإنشاء...",
"admin.marketing.promoCreatedTitle": "تم إنشاء كود الخصم",
"admin.marketing.promoCreatedDesc": "الكود {code} جاهز.",
"admin.marketing.promoCreateFailedTitle": "رُفض الإنشاء",
"admin.marketing.promoInvalidInput": "تحقق من الكود والقيمة.",
"admin.marketing.promoDeactivatedTitle": "تم تعطيل الكود",
"admin.marketing.promoDeactivatedDesc": "الكود {code} لم يعد صالحًا.",
"admin.settings.fallback.title": "سلسلة الاحتياط",
"admin.settings.fallback.desc": "ترتيب الأولوية للاختيار التلقائي للمزودين.",
"admin.settings.fallback.classic": "الوضع الكلاسيكي (Google)",
"admin.settings.fallback.llm": "وضع LLM (البوابات والوصول المباشر)",
"admin.settings.fallback.note": "القنوات المسجلة فقط (google وopenai وdeepseek وminimax) تشارك في هذه السلسلة؛ مستويات الذكاء الاصطناعي والقنوات الأخرى لا تشارك.",
"admin.models.duplicateRouteTitle": "رُفض التكرار",
"admin.models.duplicateRouteDesc": "هذا الثنائي قناة + نموذج موجود في القائمة بالفعل.",
"admin.marketing.promoDeactivateFailedTitle": "رُفض التعطيل",
"admin.marketing.promoDelete": "حذف",
"admin.marketing.promoDeleteConfirm": "حذف الكود {code} نهائيًا؟",
"admin.marketing.promoDeletedTitle": "تم حذف الكود",
"admin.marketing.promoDeletedDesc": "تم حذف الكود {code}.",
"admin.marketing.promoDeleteFailedTitle": "رُفض الحذف",
"admin.marketing.promoLinkStripe": "ربط بـ Stripe",
"admin.marketing.promoLinkedTitle": "تم ربط الكود بـ Stripe",
"admin.marketing.promoLinkedDesc": "الكود {code} صار قابلًا للتطبيق على الدفع.",
"admin.marketing.promoLinkFailedTitle": "تعذّر الربط",
"admin.marketing.promoExpiryHint": "الكود «حتى العاشر» يبقى صالحًا طوال اليوم العاشر (حتى 23:59:59 بالتوقيت العالمي).",
"admin.marketing.generateEmpty": "كان رد الخادم فارغًا أو غير قابل للاستخدام."
}

View File

@@ -167,5 +167,17 @@
"pricing.header.titleBase": "خطة لكل",
"pricing.header.titleAccent": "احتياج",
"pricing.toast.close": "إغلاق",
"pricing.aiModels.premium.price": "2 $ / 10 $ لكل مليون رمز"
"pricing.aiModels.premium.price": "2 $ / 10 $ لكل مليون رمز",
"pricing.promo.label": "كود الخصم",
"pricing.promo.placeholder": "كودك",
"pricing.promo.apply": "تطبيق",
"pricing.promo.checking": "جارٍ التحقق...",
"pricing.promo.valid": "كود صالح",
"pricing.promo.subscriptionOnly": "صالح على الاشتراكات، لا على الرصيد.",
"pricing.promoError.unknown": "كود الخصم هذا غير موجود.",
"pricing.promoError.inactive": "كود الخصم هذا لم يعد نشطًا.",
"pricing.promoError.expired": "انتهت صلاحية كود الخصم هذا.",
"pricing.promoError.exhausted": "بلغ كود الخصم هذا حده الأقصى للاستخدام.",
"pricing.promoError.plan": "كود الخصم هذا لا ينطبق على هذه الخطة.",
"pricing.promoError.local_only": "كود الخصم هذا غير مرتبط بالدفع عبر الإنترنت."
}

View File

@@ -145,5 +145,180 @@
"admin.stats.tiersSub": "Basis / Premium · klassisch {classic} · sonstige {other}",
"admin.stats.unavailable": "Geschäftszahlen nicht verfügbar ({error}).",
"admin.stats.waitlist": "Warteliste",
"admin.stats.waitlistSub": "Einträge auf der Warteliste"
"admin.stats.waitlistSub": "Einträge auf der Warteliste",
"admin.settings.title": "Anbieter",
"admin.settings.subtitle": "Anbieterzugänge konfigurieren. API-Schlüssel können auch in der .env-Datei hinterlegt werden.",
"admin.settings.sectionClassic": "Klassische Übersetzung",
"admin.settings.sectionClassicDesc": "Google-Maschinen für die Standardübersetzung.",
"admin.settings.sectionDirect": "Direkte KI-Anbieter",
"admin.settings.sectionDirectDesc": "Direkte OpenAI-kompatible Zugänge — als Kanal einer KI-Stufe (Seite „Modelle & Abos“) oder zum Erstellen von E-Mails nutzbar.",
"admin.settings.sectionGateway": "Multi-Modell-Gateways",
"admin.settings.sectionGatewayDesc": "Ein Schlüssel, hunderte Modelle. Die Modelle pro Stufe werden auf der Seite „Modelle & Abos“ verwaltet.",
"admin.settings.sectionOcr": "OCR (gescannte PDFs)",
"admin.settings.sectionOcrDesc": "Text aus Bildseiten vor der Übersetzung erfassen.",
"admin.settings.sectionEmails": "E-Mails",
"admin.settings.sectionEmailsDesc": "Versandserver für Benachrichtigungen und Erinnerungen.",
"admin.settings.enabled": "Aktiviert",
"admin.settings.disabled": "Deaktiviert",
"admin.settings.envBadge": "Schlüssel in .env",
"admin.settings.envConfigured": "In .env konfiguriert",
"admin.settings.envKeyPlaceholder": "Schlüssel in .env konfiguriert (leer lassen, um ihn zu nutzen)",
"admin.settings.apiKey": "API-Schlüssel",
"admin.settings.model": "Modell",
"admin.settings.baseUrl": "Basis-URL",
"admin.settings.test": "Testen",
"admin.settings.testing": "Test...",
"admin.settings.ok": "OK",
"admin.settings.error": "Fehler",
"admin.settings.failure": "Fehlgeschlagen",
"admin.settings.sending": "Wird gesendet...",
"admin.settings.save": "Konfiguration speichern",
"admin.settings.saving": "Speichern...",
"admin.settings.savedTitle": "✅ Konfiguration gespeichert",
"admin.settings.savedDesc": "Die Einstellungen wurden erfolgreich gespeichert.",
"admin.settings.saveErrorTitle": "Fehler beim Speichern",
"admin.settings.loadErrorTitle": "Fehler beim Laden",
"admin.settings.loadErrorDesc": "HTTP {status} — prüfen Sie Ihren Admin-Token.",
"admin.settings.networkErrorTitle": "Netzwerkfehler",
"admin.settings.networkErrorDesc": "Backend nicht erreichbar.",
"admin.settings.networkSaveErrorDesc": "Backend zum Speichern nicht erreichbar.",
"admin.settings.connectionOk": "Verbindung OK",
"admin.settings.modelsFound": "{count} {provider}-Modelle gefunden",
"admin.settings.providerError": "Fehler {provider}",
"admin.settings.google.title": "Google Translate",
"admin.settings.google.desc": "Inoffizieller Webzugriff über deep_translator. Kein Schlüssel nötig, maßvolle Nutzung empfohlen.",
"admin.settings.googleCloud.title": "Google Cloud Translation (offiziell)",
"admin.settings.googleCloud.desc": "Offizielle Google-Cloud-API. 500.000 Zeichen/Monat kostenlos, danach ca. 20 $ pro Million Zeichen. API-Schlüssel auf console.cloud.google.com.",
"admin.settings.googleCloud.hint": "Aktivieren Sie die Cloud Translation API in der Google Cloud Console und erstellen Sie einen darauf beschränkten API-Schlüssel.",
"admin.settings.deepseek.title": "DeepSeek (direkt)",
"admin.settings.deepseek.desc": "Direkte DeepSeek-API, OpenAI-kompatibel. Schlüssel auf platform.deepseek.com — als Kanal einer KI-Stufe nutzbar.",
"admin.settings.zhipu.title": "z.ai / Zhipu (GLM)",
"admin.settings.zhipu.desc": "Direkter Zugriff auf die GLM-Modelle von z.ai, OpenAI-kompatibel. Schlüssel auf z.ai — als Kanal einer KI-Stufe nutzbar.",
"admin.settings.zhipu.urlHint": "Standard: https://api.z.ai/api/paas/v4 — nur bei Nutzung eines Proxys ändern.",
"admin.settings.minimax.title": "MiniMax (direkt)",
"admin.settings.minimax.desc": "Öffentliche MiniMax-API, OpenAI-kompatibel. Schlüssel auf minimax.io — als Kanal einer KI-Stufe nutzbar.",
"admin.settings.openai.title": "OpenAI",
"admin.settings.openai.desc": "GPT-Übersetzungen. Schlüssel auf platform.openai.com erhalten.",
"admin.settings.zai.title": "xAI (Grok)",
"admin.settings.zai.desc": "Grok-Modelle von xAI. OpenAI-kompatible API. Schlüssel auf x.ai erhalten.",
"admin.settings.zai.urlHint": "Standard: https://api.x.ai/v1 — nur bei Nutzung eines Proxys ändern.",
"admin.settings.openrouter.title": "OpenRouter — Gateway-Schlüssel",
"admin.settings.openrouter.desc": "Essential-Stufe (Pro-Abo). Aktuelle Auswahl: deepseek/deepseek-v4-flash, z-ai/glm-5.3-flash, minimax/minimax-m3.",
"admin.settings.openrouter.keyLabel": "OpenRouter-API-Schlüssel (mit Premium geteilt)",
"admin.settings.openrouter.envSharedPlaceholder": "Schlüssel in .env konfiguriert (mit Premium geteilt)",
"admin.settings.openrouter.reminder": "Modelle werden auf der Seite „Modelle & Abos“ verwaltet.",
"admin.settings.openrouterPremium.title": "OpenRouter Premium — Gateway-Schlüssel",
"admin.settings.openrouterPremium.desc": "Premium-Stufe (Business-Abo). Aktuelle Auswahl: anthropic/claude-sonnet-5, deepseek/deepseek-v4-pro, z-ai/glm-5.3.",
"admin.settings.openrouterPremium.reminder": "Modelle werden auf der Seite „Modelle & Abos“ verwaltet. Nutzt denselben OpenRouter-Schlüssel.",
"admin.settings.mistral.title": "Mistral OCR (gescannte PDFs)",
"admin.settings.mistral.desc": "Extrahiert Text aus gescannten PDFs (Bildseiten) vor der Übersetzung. Ohne Schlüssel werden diese Dateien mit klarer Meldung abgelehnt. Ca. 1 € / 1.000 Seiten. Schlüssel: console.mistral.ai.",
"admin.settings.mistral.keyLabel": "Mistral-API-Schlüssel",
"admin.settings.mistral.modelLabel": "OCR-Modell",
"admin.settings.mistral.modelHint": "Empfohlen: mistral-ocr-latest",
"admin.settings.smtp.title": "SMTP-E-Mail",
"admin.settings.smtp.desc": "SMTP-Servereinstellungen für den E-Mail-Versand (Passwort vergessen, Benachrichtigungen, Erinnerungen).",
"admin.settings.smtp.host": "SMTP-Host",
"admin.settings.smtp.port": "Port",
"admin.settings.smtp.username": "Benutzername",
"admin.settings.smtp.password": "Passwort",
"admin.settings.smtp.fromEmail": "Absenderadresse",
"admin.settings.smtp.useTls": "TLS verwenden",
"admin.settings.smtp.sendTest": "Test-E-Mail senden",
"admin.settings.smtp.sent": "Gesendet",
"admin.settings.smtp.sendFailed": "Senden fehlgeschlagen",
"admin.models.channelOpenrouter": "OpenRouter",
"admin.models.channelOpenrouterPremium": "OpenRouter (Premium)",
"admin.models.channelDeepseek": "DeepSeek (direkt)",
"admin.models.channelZhipu": "z.ai (GLM)",
"admin.models.channelMinimax": "MiniMax (direkt)",
"admin.models.channelOpenai": "OpenAI",
"admin.models.channelZai": "xAI (Grok)",
"admin.models.customModelPlaceholder": "modell-id",
"admin.models.routeHint": "Der Kanal wählt direkten Zugang oder Gateway; Modell-IDs unterscheiden sich je Kanal.",
"admin.models.addRouteConfirm": "Hinzufügen",
"admin.marketing.generateTitle": "Mit KI erstellen",
"admin.marketing.generateDesc": "Der Text wird anhand des Marketingplans verfasst (Positionierung und zugelassene Zahlen). Nichts wird automatisch gesendet: erst prüfen, dann senden.",
"admin.marketing.channel": "Kanal",
"admin.marketing.model": "Modell",
"admin.marketing.modelPlaceholder": "Kanal-Standard",
"admin.marketing.promoOptional": "Rabatt (optional)",
"admin.marketing.promoNone": "Keine",
"admin.marketing.brief": "Briefing",
"admin.marketing.briefPlaceholder": "Z. B.: Wartelisten-Anmeldungen an die Vorteile des Pro-Abos erinnern.",
"admin.marketing.language": "E-Mail-Sprache",
"admin.marketing.tone": "Tonalität",
"admin.marketing.toneNone": "Neutral",
"admin.marketing.generateBtn": "Erstellen",
"admin.marketing.generating": "Wird erstellt...",
"admin.marketing.generatedTitle": "E-Mail erstellt",
"admin.marketing.generatedDesc": "Betreff und HTML ins Formular eingefügt — vor dem Versand prüfen ({provider}).",
"admin.marketing.generateFailedTitle": "Erstellung fehlgeschlagen",
"admin.marketing.briefRequired": "Beschreiben Sie zuerst den gewünschten Inhalt im Briefing.",
"admin.marketing.langFr": "Französisch",
"admin.marketing.langEn": "Englisch",
"admin.marketing.langDe": "Deutsch",
"admin.marketing.langEs": "Spanisch",
"admin.marketing.langIt": "Italienisch",
"admin.marketing.langPt": "Portugiesisch",
"admin.marketing.langNl": "Niederländisch",
"admin.marketing.langAr": "Arabisch",
"admin.marketing.langFa": "Persisch",
"admin.marketing.langJa": "Japanisch",
"admin.marketing.langKo": "Koreanisch",
"admin.marketing.langRu": "Russisch",
"admin.marketing.langZh": "Chinesisch",
"admin.marketing.toneFriendly": "Freundlich",
"admin.marketing.toneProfessional": "Professionell",
"admin.marketing.toneUrgent": "Dringend",
"admin.marketing.toneFestive": "Festlich",
"admin.marketing.promoTitle": "Rabatte",
"admin.marketing.promoDesc": "Promo-Codes werden auf Stripe-Zahlungen angewendet; der Zähler steigt erst, wenn das Abo wirklich abgeschlossen ist.",
"admin.marketing.promoEmpty": "Noch keine Promo-Codes.",
"admin.marketing.promoActive": "Aktiv",
"admin.marketing.promoInactive": "Deaktiviert",
"admin.marketing.promoLocalOnly": "Ohne Stripe",
"admin.marketing.promoDeactivate": "Deaktivieren",
"admin.marketing.promoUsage": "{used}/{max} Einlösungen",
"admin.marketing.promoPlansLabel": "Abos:",
"admin.marketing.promoAllPlans": "Alle",
"admin.marketing.promoExpiresOn": "läuft am {date} ab",
"admin.marketing.promoNeverExpires": "ohne Ablauf",
"admin.marketing.promoCreateTitle": "Rabatt erstellen",
"admin.marketing.promoCreateDesc": "Der Code wird auch in Stripe angelegt (Coupon + Promo-Code), wenn Stripe konfiguriert ist.",
"admin.marketing.promoCode": "Code",
"admin.marketing.promoType": "Typ",
"admin.marketing.promoTypePercent": "Prozent",
"admin.marketing.promoTypeAmount": "Betrag (EUR)",
"admin.marketing.promoValue": "Wert",
"admin.marketing.promoPlans": "Berechtigte Abos",
"admin.marketing.promoMaxRedemptions": "Einlöse-Limit",
"admin.marketing.promoMaxPlaceholder": "Unbegrenzt",
"admin.marketing.promoExpires": "Ablauf",
"admin.marketing.promoCreate": "Code erstellen",
"admin.marketing.promoCreating": "Wird erstellt...",
"admin.marketing.promoCreatedTitle": "Promo-Code erstellt",
"admin.marketing.promoCreatedDesc": "Der Code {code} ist bereit.",
"admin.marketing.promoCreateFailedTitle": "Erstellung abgelehnt",
"admin.marketing.promoInvalidInput": "Prüfen Sie Code und Wert.",
"admin.marketing.promoDeactivatedTitle": "Code deaktiviert",
"admin.marketing.promoDeactivatedDesc": "Der Code {code} gilt nicht mehr.",
"admin.settings.fallback.title": "Fallback-Kette",
"admin.settings.fallback.desc": "Prioritätsreihenfolge für die automatische Anbieterauswahl.",
"admin.settings.fallback.classic": "Klassischer Modus (Google)",
"admin.settings.fallback.llm": "LLM-Modus (Gateways und direkte Zugänge)",
"admin.settings.fallback.note": "Nur registrierte Kanäle (google, openai, deepseek, minimax) nehmen an dieser Kette teil; KI-Stufen und andere Kanäle nicht.",
"admin.models.duplicateRouteTitle": "Duplikat abgelehnt",
"admin.models.duplicateRouteDesc": "Dieses Kanal-+-Modell-Paar steht bereits in der Liste.",
"admin.marketing.promoDeactivateFailedTitle": "Deaktivierung abgelehnt",
"admin.marketing.promoDelete": "Löschen",
"admin.marketing.promoDeleteConfirm": "Code {code} endgültig löschen?",
"admin.marketing.promoDeletedTitle": "Code gelöscht",
"admin.marketing.promoDeletedDesc": "Der Code {code} wurde gelöscht.",
"admin.marketing.promoDeleteFailedTitle": "Löschen abgelehnt",
"admin.marketing.promoLinkStripe": "Mit Stripe verknüpfen",
"admin.marketing.promoLinkedTitle": "Code mit Stripe verknüpft",
"admin.marketing.promoLinkedDesc": "Der Code {code} ist jetzt beim Bezahlvorgang anwendbar.",
"admin.marketing.promoLinkFailedTitle": "Verknüpfung fehlgeschlagen",
"admin.marketing.promoExpiryHint": "Ein Code „bis zum 10.“ gilt den ganzen 10. (Ende: 23:59:59 UTC).",
"admin.marketing.generateEmpty": "Die Antwort des Servers war leer oder unbrauchbar."
}

View File

@@ -167,5 +167,17 @@
"pricing.header.titleBase": "Ein Plan für",
"pricing.header.titleAccent": "jeden Bedarf",
"pricing.toast.close": "Schließen",
"pricing.aiModels.premium.price": "2 $ / 10 $ pro 1M Token"
"pricing.aiModels.premium.price": "2 $ / 10 $ pro 1M Token",
"pricing.promo.label": "Promo-Code",
"pricing.promo.placeholder": "DEINCODE",
"pricing.promo.apply": "Anwenden",
"pricing.promo.checking": "Prüfung...",
"pricing.promo.valid": "Code gültig",
"pricing.promo.subscriptionOnly": "Nur für Abos gültig, nicht für Credits.",
"pricing.promoError.unknown": "Dieser Promo-Code existiert nicht.",
"pricing.promoError.inactive": "Dieser Promo-Code ist nicht mehr aktiv.",
"pricing.promoError.expired": "Dieser Promo-Code ist abgelaufen.",
"pricing.promoError.exhausted": "Dieser Promo-Code hat sein Einlöse-Limit erreicht.",
"pricing.promoError.plan": "Dieser Promo-Code gilt nicht für dieses Abo.",
"pricing.promoError.local_only": "Dieser Promo-Code ist nicht für die Online-Zahlung freigeschaltet."
}

View File

@@ -145,5 +145,180 @@
"admin.stats.tiersSub": "Essential / Premium · classic {classic} · other {other}",
"admin.stats.unavailable": "Business statistics unavailable ({error}).",
"admin.stats.waitlist": "Waitlist",
"admin.stats.waitlistSub": "people waiting"
"admin.stats.waitlistSub": "people waiting",
"admin.settings.title": "Providers",
"admin.settings.subtitle": "Configure provider access. API keys can also be set in the .env file.",
"admin.settings.sectionClassic": "Classic translation",
"admin.settings.sectionClassicDesc": "Google engines for standard translation.",
"admin.settings.sectionDirect": "Direct AI providers",
"admin.settings.sectionDirectDesc": "Direct OpenAI-compatible endpoints — usable as an AI tier channel (see the “Models & subscriptions” page) or for email generation.",
"admin.settings.sectionGateway": "Multi-model gateways",
"admin.settings.sectionGatewayDesc": "One key, hundreds of models. Per-tier models are managed on the “Models & subscriptions” page.",
"admin.settings.sectionOcr": "OCR (scanned PDFs)",
"admin.settings.sectionOcrDesc": "Extract text from image-only pages before translation.",
"admin.settings.sectionEmails": "Emails",
"admin.settings.sectionEmailsDesc": "Sending server for notifications and follow-up emails.",
"admin.settings.enabled": "Enabled",
"admin.settings.disabled": "Disabled",
"admin.settings.envBadge": "Key in .env",
"admin.settings.envConfigured": "Configured in .env",
"admin.settings.envKeyPlaceholder": "Key configured in .env (leave empty to use it)",
"admin.settings.apiKey": "API key",
"admin.settings.model": "Model",
"admin.settings.baseUrl": "Base URL",
"admin.settings.test": "Test",
"admin.settings.testing": "Testing...",
"admin.settings.ok": "OK",
"admin.settings.error": "Error",
"admin.settings.failure": "Failed",
"admin.settings.sending": "Sending...",
"admin.settings.save": "Save configuration",
"admin.settings.saving": "Saving...",
"admin.settings.savedTitle": "✅ Configuration saved",
"admin.settings.savedDesc": "Your settings have been saved successfully.",
"admin.settings.saveErrorTitle": "Save failed",
"admin.settings.loadErrorTitle": "Loading error",
"admin.settings.loadErrorDesc": "HTTP {status} — check your admin token.",
"admin.settings.networkErrorTitle": "Network error",
"admin.settings.networkErrorDesc": "Cannot reach the backend.",
"admin.settings.networkSaveErrorDesc": "Cannot reach the backend to save.",
"admin.settings.connectionOk": "Connection OK",
"admin.settings.modelsFound": "{count} {provider} models found",
"admin.settings.providerError": "{provider} error",
"admin.settings.google.title": "Google Translate",
"admin.settings.google.desc": "Unofficial web access via deep_translator. No key required, reasonable use recommended.",
"admin.settings.googleCloud.title": "Google Cloud Translation (official)",
"admin.settings.googleCloud.desc": "Official Google Cloud API. 500,000 characters/month free, then about $20 per million characters. API key from console.cloud.google.com.",
"admin.settings.googleCloud.hint": "Enable the Cloud Translation API in Google Cloud Console, then create an API key restricted to that API.",
"admin.settings.deepseek.title": "DeepSeek (direct)",
"admin.settings.deepseek.desc": "Direct DeepSeek API, OpenAI-compatible. Key from platform.deepseek.com — usable as an AI tier channel.",
"admin.settings.zhipu.title": "z.ai / Zhipu (GLM)",
"admin.settings.zhipu.desc": "Direct access to z.ai GLM models, OpenAI-compatible. Key from z.ai — usable as an AI tier channel.",
"admin.settings.zhipu.urlHint": "Default: https://api.z.ai/api/paas/v4 — change only if you use a proxy.",
"admin.settings.minimax.title": "MiniMax (direct)",
"admin.settings.minimax.desc": "Public MiniMax API, OpenAI-compatible. Key from minimax.io — usable as an AI tier channel.",
"admin.settings.openai.title": "OpenAI",
"admin.settings.openai.desc": "GPT translations. Get a key at platform.openai.com.",
"admin.settings.zai.title": "xAI (Grok)",
"admin.settings.zai.desc": "Grok models by xAI. OpenAI-compatible API. Get your key at x.ai.",
"admin.settings.zai.urlHint": "Default: https://api.x.ai/v1 — change only if you use a proxy.",
"admin.settings.openrouter.title": "OpenRouter — gateway key",
"admin.settings.openrouter.desc": "Essential tier (Pro plan). Current range: deepseek/deepseek-v4-flash, z-ai/glm-5.3-flash, minimax/minimax-m3.",
"admin.settings.openrouter.keyLabel": "OpenRouter API key (shared with Premium)",
"admin.settings.openrouter.envSharedPlaceholder": "Key configured in .env (shared with Premium)",
"admin.settings.openrouter.reminder": "Models are managed on the “Models & subscriptions” page.",
"admin.settings.openrouterPremium.title": "OpenRouter Premium — gateway key",
"admin.settings.openrouterPremium.desc": "Premium tier (Business plan). Current range: anthropic/claude-sonnet-5, deepseek/deepseek-v4-pro, z-ai/glm-5.3.",
"admin.settings.openrouterPremium.reminder": "Models are managed on the “Models & subscriptions” page. Shares the same OpenRouter key.",
"admin.settings.mistral.title": "Mistral OCR (scanned PDFs)",
"admin.settings.mistral.desc": "Extracts text from scanned PDFs (image pages) before translation. Without a key, such files are rejected with a clear message. About €1 / 1,000 pages. Key: console.mistral.ai.",
"admin.settings.mistral.keyLabel": "Mistral API key",
"admin.settings.mistral.modelLabel": "OCR model",
"admin.settings.mistral.modelHint": "Recommended: mistral-ocr-latest",
"admin.settings.smtp.title": "SMTP email",
"admin.settings.smtp.desc": "SMTP server settings for sending emails (password reset, notifications, follow-ups).",
"admin.settings.smtp.host": "SMTP host",
"admin.settings.smtp.port": "Port",
"admin.settings.smtp.username": "Username",
"admin.settings.smtp.password": "Password",
"admin.settings.smtp.fromEmail": "From address",
"admin.settings.smtp.useTls": "Use TLS",
"admin.settings.smtp.sendTest": "Send a test email",
"admin.settings.smtp.sent": "Sent",
"admin.settings.smtp.sendFailed": "Send failed",
"admin.models.channelOpenrouter": "OpenRouter",
"admin.models.channelOpenrouterPremium": "OpenRouter (Premium)",
"admin.models.channelDeepseek": "DeepSeek (direct)",
"admin.models.channelZhipu": "z.ai (GLM)",
"admin.models.channelMinimax": "MiniMax (direct)",
"admin.models.channelOpenai": "OpenAI",
"admin.models.channelZai": "xAI (Grok)",
"admin.models.customModelPlaceholder": "model-id",
"admin.models.routeHint": "The channel picks a direct access or the gateway; model IDs differ from one channel to another.",
"admin.models.addRouteConfirm": "Add",
"admin.marketing.generateTitle": "Generate with AI",
"admin.marketing.generateDesc": "The copy is written from the marketing plan (positioning and approved figures). Nothing is sent automatically: review, then send.",
"admin.marketing.channel": "Channel",
"admin.marketing.model": "Model",
"admin.marketing.modelPlaceholder": "Channel default",
"admin.marketing.promoOptional": "Discount (optional)",
"admin.marketing.promoNone": "None",
"admin.marketing.brief": "Brief",
"admin.marketing.briefPlaceholder": "E.g.: follow up with waitlist sign-ups, reminding them of the Pro plan benefits.",
"admin.marketing.language": "Email language",
"admin.marketing.tone": "Tone",
"admin.marketing.toneNone": "Neutral",
"admin.marketing.generateBtn": "Generate",
"admin.marketing.generating": "Generating...",
"admin.marketing.generatedTitle": "Email generated",
"admin.marketing.generatedDesc": "Subject and HTML inserted into the form — review before sending ({provider}).",
"admin.marketing.generateFailedTitle": "Generation failed",
"admin.marketing.briefRequired": "Describe the desired content in the brief first.",
"admin.marketing.langFr": "French",
"admin.marketing.langEn": "English",
"admin.marketing.langDe": "German",
"admin.marketing.langEs": "Spanish",
"admin.marketing.langIt": "Italian",
"admin.marketing.langPt": "Portuguese",
"admin.marketing.langNl": "Dutch",
"admin.marketing.langAr": "Arabic",
"admin.marketing.langFa": "Persian",
"admin.marketing.langJa": "Japanese",
"admin.marketing.langKo": "Korean",
"admin.marketing.langRu": "Russian",
"admin.marketing.langZh": "Chinese",
"admin.marketing.toneFriendly": "Friendly",
"admin.marketing.toneProfessional": "Professional",
"admin.marketing.toneUrgent": "Urgent",
"admin.marketing.toneFestive": "Festive",
"admin.marketing.promoTitle": "Discounts",
"admin.marketing.promoDesc": "Promo codes applied to Stripe payments; the counter increases once the subscription is actually completed.",
"admin.marketing.promoEmpty": "No promo codes yet.",
"admin.marketing.promoActive": "Active",
"admin.marketing.promoInactive": "Deactivated",
"admin.marketing.promoLocalOnly": "Outside Stripe",
"admin.marketing.promoDeactivate": "Deactivate",
"admin.marketing.promoUsage": "{used}/{max} redemptions",
"admin.marketing.promoPlansLabel": "Plans:",
"admin.marketing.promoAllPlans": "All",
"admin.marketing.promoExpiresOn": "expires {date}",
"admin.marketing.promoNeverExpires": "no expiry",
"admin.marketing.promoCreateTitle": "Create a discount",
"admin.marketing.promoCreateDesc": "The code is also created in Stripe (coupon + promotion code) when Stripe is configured.",
"admin.marketing.promoCode": "Code",
"admin.marketing.promoType": "Type",
"admin.marketing.promoTypePercent": "Percentage",
"admin.marketing.promoTypeAmount": "Amount (EUR)",
"admin.marketing.promoValue": "Value",
"admin.marketing.promoPlans": "Eligible plans",
"admin.marketing.promoMaxRedemptions": "Redemption limit",
"admin.marketing.promoMaxPlaceholder": "Unlimited",
"admin.marketing.promoExpires": "Expiry",
"admin.marketing.promoCreate": "Create code",
"admin.marketing.promoCreating": "Creating...",
"admin.marketing.promoCreatedTitle": "Promo code created",
"admin.marketing.promoCreatedDesc": "Code {code} is ready.",
"admin.marketing.promoCreateFailedTitle": "Creation refused",
"admin.marketing.promoInvalidInput": "Check the code and the value.",
"admin.marketing.promoDeactivatedTitle": "Code deactivated",
"admin.marketing.promoDeactivatedDesc": "Code {code} no longer applies.",
"admin.settings.fallback.title": "Fallback chain",
"admin.settings.fallback.desc": "Priority order for automatic provider selection.",
"admin.settings.fallback.classic": "Classic mode (Google)",
"admin.settings.fallback.llm": "LLM mode (gateways and direct accesses)",
"admin.settings.fallback.note": "Only registered engines (google, openai, deepseek, minimax) take part in this chain; AI tiers and other channels do not.",
"admin.models.duplicateRouteTitle": "Duplicate refused",
"admin.models.duplicateRouteDesc": "This channel + model pair is already in the list.",
"admin.marketing.promoDeactivateFailedTitle": "Deactivation refused",
"admin.marketing.promoDelete": "Delete",
"admin.marketing.promoDeleteConfirm": "Permanently delete code {code}?",
"admin.marketing.promoDeletedTitle": "Code deleted",
"admin.marketing.promoDeletedDesc": "Code {code} has been deleted.",
"admin.marketing.promoDeleteFailedTitle": "Deletion refused",
"admin.marketing.promoLinkStripe": "Link to Stripe",
"admin.marketing.promoLinkedTitle": "Code linked to Stripe",
"admin.marketing.promoLinkedDesc": "Code {code} can now be applied at checkout.",
"admin.marketing.promoLinkFailedTitle": "Linking failed",
"admin.marketing.promoExpiryHint": "A code “until the 10th” stays valid through the 10th (ends 23:59:59 UTC).",
"admin.marketing.generateEmpty": "The server response was empty or unusable."
}

View File

@@ -167,5 +167,17 @@
"pricing.aiModels.essential.price": "From $0.09 per million tokens",
"pricing.aiModels.premium.modelName": "Claude 5",
"pricing.aiModels.premium.alternatives": "Alternatives: DeepSeek V4 Pro and GLM-5.3",
"pricing.aiModels.premium.price": "$2 / $10 per 1M tokens"
"pricing.aiModels.premium.price": "$2 / $10 per 1M tokens",
"pricing.promo.label": "Promo code",
"pricing.promo.placeholder": "YOURCODE",
"pricing.promo.apply": "Apply",
"pricing.promo.checking": "Checking...",
"pricing.promo.valid": "Valid code",
"pricing.promo.subscriptionOnly": "Valid on subscriptions only, not on credits.",
"pricing.promoError.unknown": "This promo code does not exist.",
"pricing.promoError.inactive": "This promo code is no longer active.",
"pricing.promoError.expired": "This promo code has expired.",
"pricing.promoError.exhausted": "This promo code has reached its redemption limit.",
"pricing.promoError.plan": "This promo code does not apply to this plan.",
"pricing.promoError.local_only": "This promo code is not valid for online payment."
}

View File

@@ -145,5 +145,180 @@
"admin.stats.tiersSub": "Esencial / Premium · clásico {classic} · otro {other}",
"admin.stats.unavailable": "Estadísticas de negocio no disponibles ({error}).",
"admin.stats.waitlist": "Lista de espera",
"admin.stats.waitlistSub": "inscritos en espera"
"admin.stats.waitlistSub": "inscritos en espera",
"admin.settings.title": "Proveedores",
"admin.settings.subtitle": "Configura los accesos de proveedores. Las claves API también pueden definirse en el archivo .env.",
"admin.settings.sectionClassic": "Traducción clásica",
"admin.settings.sectionClassicDesc": "Motores de Google para la traducción estándar.",
"admin.settings.sectionDirect": "Proveedores de IA directos",
"admin.settings.sectionDirectDesc": "Accesos directos compatibles con OpenAI — utilizables como canal de un nivel de IA (página «Modelos y suscripciones») o para generar correos.",
"admin.settings.sectionGateway": "Pasarelas multiplano",
"admin.settings.sectionGatewayDesc": "Una sola clave, cientos de modelos. Los modelos por nivel se gestionan en la página «Modelos y suscripciones».",
"admin.settings.sectionOcr": "OCR (PDF escaneados)",
"admin.settings.sectionOcrDesc": "Extracción del texto de las páginas de imagen antes de traducir.",
"admin.settings.sectionEmails": "Correos",
"admin.settings.sectionEmailsDesc": "Servidor de envío para notificaciones y reenganches.",
"admin.settings.enabled": "Activado",
"admin.settings.disabled": "Desactivado",
"admin.settings.envBadge": "Clave en .env",
"admin.settings.envConfigured": "Configurado en .env",
"admin.settings.envKeyPlaceholder": "Clave configurada en .env (déjela vacía para usarla)",
"admin.settings.apiKey": "Clave API",
"admin.settings.model": "Modelo",
"admin.settings.baseUrl": "URL base",
"admin.settings.test": "Probar",
"admin.settings.testing": "Prueba...",
"admin.settings.ok": "OK",
"admin.settings.error": "Error",
"admin.settings.failure": "Fallo",
"admin.settings.sending": "Enviando...",
"admin.settings.save": "Guardar la configuración",
"admin.settings.saving": "Guardando...",
"admin.settings.savedTitle": "✅ Configuración guardada",
"admin.settings.savedDesc": "Los parámetros se han guardado correctamente.",
"admin.settings.saveErrorTitle": "Error al guardar",
"admin.settings.loadErrorTitle": "Error de carga",
"admin.settings.loadErrorDesc": "HTTP {status} — verifique su token de administrador.",
"admin.settings.networkErrorTitle": "Error de red",
"admin.settings.networkErrorDesc": "No se puede contactar con el backend.",
"admin.settings.networkSaveErrorDesc": "No se puede contactar con el backend para guardar.",
"admin.settings.connectionOk": "Conexión correcta",
"admin.settings.modelsFound": "{count} modelos de {provider} encontrados",
"admin.settings.providerError": "Error {provider}",
"admin.settings.google.title": "Google Translate",
"admin.settings.google.desc": "Acceso web no oficial vía deep_translator. No requiere clave; se recomienda un uso razonable.",
"admin.settings.googleCloud.title": "Google Cloud Translation (oficial)",
"admin.settings.googleCloud.desc": "API oficial de Google Cloud. 500 000 caracteres/mes gratis, luego unos 20 $ por millón de caracteres. Clave API en console.cloud.google.com.",
"admin.settings.googleCloud.hint": "Active Cloud Translation API en Google Cloud Console y cree una clave API restringida a esa API.",
"admin.settings.deepseek.title": "DeepSeek (directo)",
"admin.settings.deepseek.desc": "API directa de DeepSeek, compatible con OpenAI. Clave en platform.deepseek.com — utilizable como canal de un nivel de IA.",
"admin.settings.zhipu.title": "z.ai / Zhipu (GLM)",
"admin.settings.zhipu.desc": "Acceso directo a los modelos GLM de z.ai, compatible con OpenAI. Clave en z.ai — utilizable como canal de un nivel de IA.",
"admin.settings.zhipu.urlHint": "Por defecto: https://api.z.ai/api/paas/v4 — cámbielo solo si usa un proxy.",
"admin.settings.minimax.title": "MiniMax (directo)",
"admin.settings.minimax.desc": "API pública de MiniMax, compatible con OpenAI. Clave en minimax.io — utilizable como canal de un nivel de IA.",
"admin.settings.openai.title": "OpenAI",
"admin.settings.openai.desc": "Traducciones GPT. Obtenga una clave en platform.openai.com.",
"admin.settings.zai.title": "xAI (Grok)",
"admin.settings.zai.desc": "Modelos Grok de xAI. API compatible con OpenAI. Obtenga su clave en x.ai.",
"admin.settings.zai.urlHint": "Por defecto: https://api.x.ai/v1 — cámbielo solo si usa un proxy.",
"admin.settings.openrouter.title": "OpenRouter — clave de pasarela",
"admin.settings.openrouter.desc": "Nivel Esencial (plan Pro). Gama actual: deepseek/deepseek-v4-flash, z-ai/glm-5.3-flash, minimax/minimax-m3.",
"admin.settings.openrouter.keyLabel": "Clave API de OpenRouter (compartida con Premium)",
"admin.settings.openrouter.envSharedPlaceholder": "Clave configurada en .env (compartida con Premium)",
"admin.settings.openrouter.reminder": "Modelos gestionados en la página «Modelos y suscripciones».",
"admin.settings.openrouterPremium.title": "OpenRouter Premium — clave de pasarela",
"admin.settings.openrouterPremium.desc": "Nivel Premium (plan Business). Gama actual: anthropic/claude-sonnet-5, deepseek/deepseek-v4-pro, z-ai/glm-5.3.",
"admin.settings.openrouterPremium.reminder": "Modelos gestionados en la página «Modelos y suscripciones». Comparte la misma clave de OpenRouter.",
"admin.settings.mistral.title": "OCR Mistral (PDF escaneados)",
"admin.settings.mistral.desc": "Extrae el texto de los PDF escaneados (páginas de imagen) antes de traducir. Sin clave, estos archivos se rechazan con un mensaje claro. Unos 1 € / 1 000 páginas. Clave: console.mistral.ai.",
"admin.settings.mistral.keyLabel": "Clave API de Mistral",
"admin.settings.mistral.modelLabel": "Modelo OCR",
"admin.settings.mistral.modelHint": "Recomendado: mistral-ocr-latest",
"admin.settings.smtp.title": "Correo SMTP",
"admin.settings.smtp.desc": "Configuración del servidor SMTP para el envío de correos (contraseña olvidada, notificaciones, reenganches).",
"admin.settings.smtp.host": "Host SMTP",
"admin.settings.smtp.port": "Puerto",
"admin.settings.smtp.username": "Nombre de usuario",
"admin.settings.smtp.password": "Contraseña",
"admin.settings.smtp.fromEmail": "Dirección de envío",
"admin.settings.smtp.useTls": "Usar TLS",
"admin.settings.smtp.sendTest": "Enviar un correo de prueba",
"admin.settings.smtp.sent": "Enviado",
"admin.settings.smtp.sendFailed": "Fallo en el envío",
"admin.models.channelOpenrouter": "OpenRouter",
"admin.models.channelOpenrouterPremium": "OpenRouter (Premium)",
"admin.models.channelDeepseek": "DeepSeek (directo)",
"admin.models.channelZhipu": "z.ai (GLM)",
"admin.models.channelMinimax": "MiniMax (directo)",
"admin.models.channelOpenai": "OpenAI",
"admin.models.channelZai": "xAI (Grok)",
"admin.models.customModelPlaceholder": "id-del-modelo",
"admin.models.routeHint": "El canal elige el acceso directo o la pasarela; los identificadores de modelos difieren según el canal.",
"admin.models.addRouteConfirm": "Añadir",
"admin.marketing.generateTitle": "Generar con IA",
"admin.marketing.generateDesc": "El texto se redacta a partir del plan de marketing (posicionamiento y cifras autorizadas). No se envía nada automáticamente: revise y envíe.",
"admin.marketing.channel": "Canal",
"admin.marketing.model": "Modelo",
"admin.marketing.modelPlaceholder": "Predeterminado del canal",
"admin.marketing.promoOptional": "Descuento (opcional)",
"admin.marketing.promoNone": "Ninguno",
"admin.marketing.brief": "Brief",
"admin.marketing.briefPlaceholder": "Ej.: reenganchear a los inscritos en la lista de espera recordando las ventajas del plan Pro.",
"admin.marketing.language": "Idioma del correo",
"admin.marketing.tone": "Tono",
"admin.marketing.toneNone": "Neutro",
"admin.marketing.generateBtn": "Generar",
"admin.marketing.generating": "Generando...",
"admin.marketing.generatedTitle": "Correo generado",
"admin.marketing.generatedDesc": "Asunto y HTML insertados en el formulario — revise antes de enviar ({provider}).",
"admin.marketing.generateFailedTitle": "No se pudo generar",
"admin.marketing.briefRequired": "Describa primero el contenido deseado en el brief.",
"admin.marketing.langFr": "Francés",
"admin.marketing.langEn": "Inglés",
"admin.marketing.langDe": "Alemán",
"admin.marketing.langEs": "Español",
"admin.marketing.langIt": "Italiano",
"admin.marketing.langPt": "Portugués",
"admin.marketing.langNl": "Neerlandés",
"admin.marketing.langAr": "Árabe",
"admin.marketing.langFa": "Persa",
"admin.marketing.langJa": "Japonés",
"admin.marketing.langKo": "Coreano",
"admin.marketing.langRu": "Ruso",
"admin.marketing.langZh": "Chino",
"admin.marketing.toneFriendly": "Cercano",
"admin.marketing.toneProfessional": "Profesional",
"admin.marketing.toneUrgent": "Urgente",
"admin.marketing.toneFestive": "Festivo",
"admin.marketing.promoTitle": "Descuentos",
"admin.marketing.promoDesc": "Códigos promo aplicados al pago mediante Stripe; el contador aumenta cuando la suscripción se completa de verdad.",
"admin.marketing.promoEmpty": "Aún no hay códigos promo.",
"admin.marketing.promoActive": "Activo",
"admin.marketing.promoInactive": "Desactivado",
"admin.marketing.promoLocalOnly": "Fuera de Stripe",
"admin.marketing.promoDeactivate": "Desactivar",
"admin.marketing.promoUsage": "{used}/{max} usos",
"admin.marketing.promoPlansLabel": "Planes:",
"admin.marketing.promoAllPlans": "Todos",
"admin.marketing.promoExpiresOn": "caduca el {date}",
"admin.marketing.promoNeverExpires": "sin caducidad",
"admin.marketing.promoCreateTitle": "Crear un descuento",
"admin.marketing.promoCreateDesc": "El código también se crea en Stripe (cupón + código promocional) cuando Stripe está configurado.",
"admin.marketing.promoCode": "Código",
"admin.marketing.promoType": "Tipo",
"admin.marketing.promoTypePercent": "Porcentaje",
"admin.marketing.promoTypeAmount": "Importe (EUR)",
"admin.marketing.promoValue": "Valor",
"admin.marketing.promoPlans": "Planes elegibles",
"admin.marketing.promoMaxRedemptions": "Límite de usos",
"admin.marketing.promoMaxPlaceholder": "Ilimitado",
"admin.marketing.promoExpires": "Caducidad",
"admin.marketing.promoCreate": "Crear el código",
"admin.marketing.promoCreating": "Creando...",
"admin.marketing.promoCreatedTitle": "Código promo creado",
"admin.marketing.promoCreatedDesc": "El código {code} está listo.",
"admin.marketing.promoCreateFailedTitle": "Creación rechazada",
"admin.marketing.promoInvalidInput": "Verifique el código y el valor.",
"admin.marketing.promoDeactivatedTitle": "Código desactivado",
"admin.marketing.promoDeactivatedDesc": "El código {code} ya no se aplica.",
"admin.settings.fallback.title": "Cadena de respaldo",
"admin.settings.fallback.desc": "Orden de prioridad para la selección automática de proveedores.",
"admin.settings.fallback.classic": "Modo clásico (Google)",
"admin.settings.fallback.llm": "Modo LLM (pasarelas y accesos directos)",
"admin.settings.fallback.note": "Solo los canales registrados (google, openai, deepseek, minimax) participan en esta cadena; los niveles de IA y los demás canales no.",
"admin.models.duplicateRouteTitle": "Duplicado rechazado",
"admin.models.duplicateRouteDesc": "Ese par canal + modelo ya está en la lista.",
"admin.marketing.promoDeactivateFailedTitle": "Desactivación rechazada",
"admin.marketing.promoDelete": "Eliminar",
"admin.marketing.promoDeleteConfirm": "¿Eliminar definitivamente el código {code}?",
"admin.marketing.promoDeletedTitle": "Código eliminado",
"admin.marketing.promoDeletedDesc": "El código {code} ha sido eliminado.",
"admin.marketing.promoDeleteFailedTitle": "Eliminación rechazada",
"admin.marketing.promoLinkStripe": "Vincular a Stripe",
"admin.marketing.promoLinkedTitle": "Código vinculado a Stripe",
"admin.marketing.promoLinkedDesc": "El código {code} ya se puede aplicar al pago.",
"admin.marketing.promoLinkFailedTitle": "No se pudo vincular",
"admin.marketing.promoExpiryHint": "Un código «hasta el 10» sigue siendo válido todo el 10 (fin: 23:59:59 UTC).",
"admin.marketing.generateEmpty": "La respuesta del servidor estaba vacía o no era utilizable."
}

View File

@@ -167,5 +167,17 @@
"pricing.header.titleBase": "Un plan para",
"pricing.header.titleAccent": "cada necesidad",
"pricing.toast.close": "Cerrar",
"pricing.aiModels.premium.price": "2 $ / 10 $ por 1M de tokens"
"pricing.aiModels.premium.price": "2 $ / 10 $ por 1M de tokens",
"pricing.promo.label": "Código promo",
"pricing.promo.placeholder": "TUCODIGO",
"pricing.promo.apply": "Aplicar",
"pricing.promo.checking": "Verificando...",
"pricing.promo.valid": "Código válido",
"pricing.promo.subscriptionOnly": "Válido en suscripciones, no en créditos.",
"pricing.promoError.unknown": "Este código promo no existe.",
"pricing.promoError.inactive": "Este código promo ya no está activo.",
"pricing.promoError.expired": "Este código promo ha caducado.",
"pricing.promoError.exhausted": "Este código promo ha alcanzado su límite de usos.",
"pricing.promoError.plan": "Este código promo no es válido para este plan.",
"pricing.promoError.local_only": "Este código promo no está habilitado para el pago en línea."
}

View File

@@ -145,5 +145,180 @@
"admin.stats.tiersSub": "پایه / پیشرفته · کلاسیک {classic} · سایر {other}",
"admin.stats.unavailable": "آمار کسب‌وکار در دسترس نیست ({error}).",
"admin.stats.waitlist": "فهرست انتظار",
"admin.stats.waitlistSub": "نفر در انتظار"
"admin.stats.waitlistSub": "نفر در انتظار",
"admin.settings.title": "ارائه‌دهندگان",
"admin.settings.subtitle": "دسترسی ارائه‌دهندگان را تنظیم کنید. کلیدهای API را می‌توان در فایل .env نیز قرار داد.",
"admin.settings.sectionClassic": "ترجمه کلاسیک",
"admin.settings.sectionClassicDesc": "موتورهای Google برای ترجمه استاندارد.",
"admin.settings.sectionDirect": "ارائه‌دهندگان مستقیم هوش مصنوعی",
"admin.settings.sectionDirectDesc": "دسترسی‌های مستقیم سازگار با OpenAI — به‌عنوان کانال یک سطح هوش مصنوعی (صفحه «مدل‌ها و اشتراک‌ها») یا برای تولید ایمیل قابل استفاده‌اند.",
"admin.settings.sectionGateway": "درگاه‌های چندمدلی",
"admin.settings.sectionGatewayDesc": "یک کلید، صدها مدل. مدل‌های هر سطح در صفحه «مدل‌ها و اشتراک‌ها» مدیریت می‌شوند.",
"admin.settings.sectionOcr": "OCR (PDFهای اسکن‌شده)",
"admin.settings.sectionOcrDesc": "استخراج متن صفحات تصویری پیش از ترجمه.",
"admin.settings.sectionEmails": "ایمیل‌ها",
"admin.settings.sectionEmailsDesc": "سرور ارسال برای اعلان‌ها و ایمیل‌های پیگیری.",
"admin.settings.enabled": "فعال",
"admin.settings.disabled": "غیرفعال",
"admin.settings.envBadge": "کلید در .env",
"admin.settings.envConfigured": "تنظیم‌شده در .env",
"admin.settings.envKeyPlaceholder": "کلید در .env تنظیم شده (برای استفاده خالی بگذارید)",
"admin.settings.apiKey": "کلید API",
"admin.settings.model": "مدل",
"admin.settings.baseUrl": "آدرس پایه",
"admin.settings.test": "آزمایش",
"admin.settings.testing": "در حال آزمایش...",
"admin.settings.ok": "انجام شد",
"admin.settings.error": "خطا",
"admin.settings.failure": "ناموفق",
"admin.settings.sending": "در حال ارسال...",
"admin.settings.save": "ذخیره پیکربندی",
"admin.settings.saving": "در حال ذخیره...",
"admin.settings.savedTitle": "✅ پیکربندی ذخیره شد",
"admin.settings.savedDesc": "تنظیمات با موفقیت ذخیره شدند.",
"admin.settings.saveErrorTitle": "خطا در ذخیره",
"admin.settings.loadErrorTitle": "خطا در بارگیری",
"admin.settings.loadErrorDesc": "HTTP {status} — توکن مدیر خود را بررسی کنید.",
"admin.settings.networkErrorTitle": "خطای شبکه",
"admin.settings.networkErrorDesc": "دسترسی به سرور ممکن نیست.",
"admin.settings.networkSaveErrorDesc": "برای ذخیره، دسترسی به سرور ممکن نیست.",
"admin.settings.connectionOk": "اتصال برقرار است",
"admin.settings.modelsFound": "{count} مدل {provider} یافت شد",
"admin.settings.providerError": "خطای {provider}",
"admin.settings.google.title": "Google Translate",
"admin.settings.google.desc": "دسترسی وب غیررسمی از طریق deep_translator. کلید لازم ندارد؛ استفاده معقول توصیه می‌شود.",
"admin.settings.googleCloud.title": "Google Cloud Translation (رسمی)",
"admin.settings.googleCloud.desc": "رسمی Google Cloud. ۵۰۰٬۰۰۰ نویسه/ماه رایگان، سپس حدود ۲۰ دلار برای هر میلیون نویسه. کلید API از console.cloud.google.com.",
"admin.settings.googleCloud.hint": "در Google Cloud Console سرویس Cloud Translation API را فعال کنید و یک کلید API محدود به همان سرویس بسازید.",
"admin.settings.deepseek.title": "DeepSeek (مستقیم)",
"admin.settings.deepseek.desc": "API مستقیم DeepSeek، سازگار با OpenAI. کلید از platform.deepseek.com — به‌عنوان کانال سطح هوش مصنوعی قابل استفاده است.",
"admin.settings.zhipu.title": "z.ai / Zhipu (GLM)",
"admin.settings.zhipu.desc": "دسترسی مستقیم به مدل‌های GLM از z.ai، سازگار با OpenAI. کلید از z.ai — به‌عنوان کانال سطح هوش مصنوعی قابل استفاده است.",
"admin.settings.zhipu.urlHint": "پیش‌فرض: https://api.z.ai/api/paas/v4 — فقط در صورت استفاده از پروکسی تغییر دهید.",
"admin.settings.minimax.title": "MiniMax (مستقیم)",
"admin.settings.minimax.desc": "API عمومی MiniMax، سازگار با OpenAI. کلید از minimax.io — به‌عنوان کانال سطح هوش مصنوعی قابل استفاده است.",
"admin.settings.openai.title": "OpenAI",
"admin.settings.openai.desc": "ترجمه‌های GPT. کلید خود را از platform.openai.com بگیرید.",
"admin.settings.zai.title": "xAI (Grok)",
"admin.settings.zai.desc": "مدل‌های Grok از xAI. API سازگار با OpenAI. کلید خود را از x.ai بگیرید.",
"admin.settings.zai.urlHint": "پیش‌فرض: https://api.x.ai/v1 — فقط در صورت استفاده از پروکسی تغییر دهید.",
"admin.settings.openrouter.title": "OpenRouter — کلید درگاه",
"admin.settings.openrouter.desc": "سطح Essential (طرح Pro). فهرست کنونی: deepseek/deepseek-v4-flash، z-ai/glm-5.3-flash، minimax/minimax-m3.",
"admin.settings.openrouter.keyLabel": "کلید API OpenRouter (مشترک با Premium)",
"admin.settings.openrouter.envSharedPlaceholder": "کلید در .env تنظیم شده (مشترک با Premium)",
"admin.settings.openrouter.reminder": "مدل‌ها در صفحه «مدل‌ها و اشتراک‌ها» مدیریت می‌شوند.",
"admin.settings.openrouterPremium.title": "OpenRouter Premium — کلید درگاه",
"admin.settings.openrouterPremium.desc": "سطح Premium (طرح Business). فهرست کنونی: anthropic/claude-sonnet-5، deepseek/deepseek-v4-pro، z-ai/glm-5.3.",
"admin.settings.openrouterPremium.reminder": "مدل‌ها در صفحه «مدل‌ها و اشتراک‌ها» مدیریت می‌شوند. همان کلید OpenRouter را به اشتراک می‌گذارد.",
"admin.settings.mistral.title": "OCR Mistral (PDFهای اسکن‌شده)",
"admin.settings.mistral.desc": "متن PDFهای اسکن‌شده (صفحات تصویری) را پیش از ترجمه استخراج می‌کند. بدون کلید، این فایل‌ها با پیامی روشن رد می‌شوند. حدود ۱ یورو / ۱۰۰۰ صفحه. کلید: console.mistral.ai.",
"admin.settings.mistral.keyLabel": "کلید API Mistral",
"admin.settings.mistral.modelLabel": "مدل OCR",
"admin.settings.mistral.modelHint": "پیشنهادی: mistral-ocr-latest",
"admin.settings.smtp.title": "ایمیل SMTP",
"admin.settings.smtp.desc": "تنظیمات سرور SMTP برای ارسال ایمیل (فراموشی رمز، اعلان‌ها، ایمیل‌های پیگیری).",
"admin.settings.smtp.host": "میزبان SMTP",
"admin.settings.smtp.port": "درگاه",
"admin.settings.smtp.username": "نام کاربری",
"admin.settings.smtp.password": "رمز عبور",
"admin.settings.smtp.fromEmail": "نشانی فرستنده",
"admin.settings.smtp.useTls": "استفاده از TLS",
"admin.settings.smtp.sendTest": "ارسال ایمیل آزمایشی",
"admin.settings.smtp.sent": "ارسال شد",
"admin.settings.smtp.sendFailed": "ارسال ناموفق",
"admin.models.channelOpenrouter": "OpenRouter",
"admin.models.channelOpenrouterPremium": "OpenRouter (Premium)",
"admin.models.channelDeepseek": "DeepSeek (مستقیم)",
"admin.models.channelZhipu": "z.ai (GLM)",
"admin.models.channelMinimax": "MiniMax (مستقیم)",
"admin.models.channelOpenai": "OpenAI",
"admin.models.channelZai": "xAI (Grok)",
"admin.models.customModelPlaceholder": "شناسه-مدل",
"admin.models.routeHint": "کانال، دسترسی مستقیم یا درگاه را برمی‌گزیند؛ شناسه مدل‌ها در هر کانال متفاوت است.",
"admin.models.addRouteConfirm": "افزودن",
"admin.marketing.generateTitle": "تولید با هوش مصنوعی",
"admin.marketing.generateDesc": "متن بر اساس برنامه بازاریابی نوشته می‌شود (جایگاه و ارقام مجاز). هیچ‌چیز به‌طور خودکار ارسال نمی‌شود: بازبینی کنید و سپس بفرستید.",
"admin.marketing.channel": "کانال",
"admin.marketing.model": "مدل",
"admin.marketing.modelPlaceholder": "پیش‌فرض کانال",
"admin.marketing.promoOptional": "تخفیف (اختیاری)",
"admin.marketing.promoNone": "بدون تخفیف",
"admin.marketing.brief": "شرح درخواست",
"admin.marketing.briefPlaceholder": "مثال: یادآوری به ثبت‌نام‌شدگان لیست انتظار درباره مزایای طرح Pro.",
"admin.marketing.language": "زبان ایمیل",
"admin.marketing.tone": "لحن",
"admin.marketing.toneNone": "بی‌طرف",
"admin.marketing.generateBtn": "تولید",
"admin.marketing.generating": "در حال تولید...",
"admin.marketing.generatedTitle": "ایمیل تولید شد",
"admin.marketing.generatedDesc": "موضوع و HTML در فرم درج شدند — پیش از ارسال بازبینی کنید ({provider}).",
"admin.marketing.generateFailedTitle": "تولید ناموفق بود",
"admin.marketing.briefRequired": "نخست محتوای موردنظر را در شرح درخواست بنویسید.",
"admin.marketing.langFr": "فرانسوی",
"admin.marketing.langEn": "انگلیسی",
"admin.marketing.langDe": "آلمانی",
"admin.marketing.langEs": "اسپانیایی",
"admin.marketing.langIt": "ایتالیایی",
"admin.marketing.langPt": "پرتغالی",
"admin.marketing.langNl": "هلندی",
"admin.marketing.langAr": "عربی",
"admin.marketing.langFa": "فارسی",
"admin.marketing.langJa": "ژاپنی",
"admin.marketing.langKo": "کره‌ای",
"admin.marketing.langRu": "روسی",
"admin.marketing.langZh": "چینی",
"admin.marketing.toneFriendly": "صمیمی",
"admin.marketing.toneProfessional": "حرفه‌ای",
"admin.marketing.toneUrgent": "فوری",
"admin.marketing.toneFestive": "جشنواره‌ای",
"admin.marketing.promoTitle": "تخفیف‌ها",
"admin.marketing.promoDesc": "کدهای تخفیف از طریق Stripe بر پرداخت اعمال می‌شوند؛ شمارنده پس از تکمیل واقعی اشتراک بالا می‌رود.",
"admin.marketing.promoEmpty": "هنوز کد تخفیفی وجود ندارد.",
"admin.marketing.promoActive": "فعال",
"admin.marketing.promoInactive": "غیرفعال",
"admin.marketing.promoLocalOnly": "خارج از Stripe",
"admin.marketing.promoDeactivate": "غیرفعال‌سازی",
"admin.marketing.promoUsage": "{used}/{max} بار استفاده",
"admin.marketing.promoPlansLabel": "طرح‌ها:",
"admin.marketing.promoAllPlans": "همه",
"admin.marketing.promoExpiresOn": "تاریخ انقضا {date}",
"admin.marketing.promoNeverExpires": "بدون انقضا",
"admin.marketing.promoCreateTitle": "ایجاد تخفیف",
"admin.marketing.promoCreateDesc": "اگر Stripe تنظیم شده باشد، کد در Stripe نیز ساخته می‌شود (کوپن + کد تبلیغاتی).",
"admin.marketing.promoCode": "کد",
"admin.marketing.promoType": "نوع",
"admin.marketing.promoTypePercent": "درصد",
"admin.marketing.promoTypeAmount": "مبلغ (EUR)",
"admin.marketing.promoValue": "مقدار",
"admin.marketing.promoPlans": "طرح‌های واجد شرایط",
"admin.marketing.promoMaxRedemptions": "سقف استفاده",
"admin.marketing.promoMaxPlaceholder": "نامحدود",
"admin.marketing.promoExpires": "انقضا",
"admin.marketing.promoCreate": "ایجاد کد",
"admin.marketing.promoCreating": "در حال ایجاد...",
"admin.marketing.promoCreatedTitle": "کد تخفیف ایجاد شد",
"admin.marketing.promoCreatedDesc": "کد {code} آماده است.",
"admin.marketing.promoCreateFailedTitle": "ایجاد رد شد",
"admin.marketing.promoInvalidInput": "کد و مقدار را بررسی کنید.",
"admin.marketing.promoDeactivatedTitle": "کد غیرفعال شد",
"admin.marketing.promoDeactivatedDesc": "کد {code} دیگر اعمال نمی‌شود.",
"admin.settings.fallback.title": "زنجیره پشتیبان",
"admin.settings.fallback.desc": "ترتیب اولویت برای انتخاب خودکار ارائه‌دهندگان.",
"admin.settings.fallback.classic": "حالت کلاسیک (Google)",
"admin.settings.fallback.llm": "حالت LLM (درگاه‌ها و دسترسی مستقیم)",
"admin.settings.fallback.note": "فقط کانال‌های ثبت‌شده (google، openai، deepseek، minimax) در این زنجیره شرکت می‌کنند؛ سطوح هوش مصنوعی و کانال‌های دیگر نه.",
"admin.models.duplicateRouteTitle": "تکرار رد شد",
"admin.models.duplicateRouteDesc": "این جفت کانال + مدل از قبل در فهرست هست.",
"admin.marketing.promoDeactivateFailedTitle": "غیرفعال‌سازی رد شد",
"admin.marketing.promoDelete": "حذف",
"admin.marketing.promoDeleteConfirm": "کد {code} برای همیشه حذف شود؟",
"admin.marketing.promoDeletedTitle": "کد حذف شد",
"admin.marketing.promoDeletedDesc": "کد {code} حذف شد.",
"admin.marketing.promoDeleteFailedTitle": "حذف رد شد",
"admin.marketing.promoLinkStripe": "پیوند به Stripe",
"admin.marketing.promoLinkedTitle": "کد به Stripe پیوند خورد",
"admin.marketing.promoLinkedDesc": "کد {code} اکنون در پرداخت قابل اعمال است.",
"admin.marketing.promoLinkFailedTitle": "پیوند ناموفق بود",
"admin.marketing.promoExpiryHint": "کد «تا ۱۰» تمام روز دهم معتبر است (پایان: ۲۳:۵۹:۵۹ UTC).",
"admin.marketing.generateEmpty": "پاسخ سرور خالی یا غیرقابل استفاده بود."
}

View File

@@ -167,5 +167,17 @@
"pricing.header.titleBase": "برنامه‌ای برای",
"pricing.header.titleAccent": "هر نیاز",
"pricing.toast.close": "بستن",
"pricing.aiModels.premium.price": "۲ دلار / ۱۰ دلار به ازای هر میلیون توکن"
"pricing.aiModels.premium.price": "۲ دلار / ۱۰ دلار به ازای هر میلیون توکن",
"pricing.promo.label": "کد تخفیف",
"pricing.promo.placeholder": "کدشما",
"pricing.promo.apply": "اعمال",
"pricing.promo.checking": "در حال بررسی...",
"pricing.promo.valid": "کد معتبر است",
"pricing.promo.subscriptionOnly": "فقط روی اشتراک‌ها معتبر است، نه روی اعتبارها.",
"pricing.promoError.unknown": "این کد تخفیف وجود ندارد.",
"pricing.promoError.inactive": "این کد تخفیف دیگر فعال نیست.",
"pricing.promoError.expired": "این کد تخفیف منقضی شده است.",
"pricing.promoError.exhausted": "این کد تخفیف به سقف استفاده رسیده است.",
"pricing.promoError.plan": "این کد تخفیف برای این طرح معتبر نیست.",
"pricing.promoError.local_only": "این کد تخفیف برای پرداخت آنلاین فعال نشده است."
}

View File

@@ -145,5 +145,180 @@
"admin.stats.tiersSub": "Essentielle / Premium · classique {classic} · autre {other}",
"admin.stats.unavailable": "Statistiques business indisponibles ({error}).",
"admin.stats.waitlist": "Liste d'attente",
"admin.stats.waitlistSub": "inscrits en attente"
"admin.stats.waitlistSub": "inscrits en attente",
"admin.settings.title": "Fournisseurs",
"admin.settings.subtitle": "Configurez les accès fournisseurs. Les clés API peuvent aussi être définies dans le fichier .env.",
"admin.settings.sectionClassic": "Traduction classique",
"admin.settings.sectionClassicDesc": "Moteurs Google pour la traduction standard.",
"admin.settings.sectionDirect": "Fournisseurs IA directs",
"admin.settings.sectionDirectDesc": "Accès directs compatibles OpenAI — utilisables comme canal d'un palier IA (page « Modèles & abonnements ») ou pour la génération d'emails.",
"admin.settings.sectionGateway": "Passerelles multi-modèles",
"admin.settings.sectionGatewayDesc": "Une seule clé, des centaines de modèles. Les modèles par palier se règlent dans « Modèles & abonnements ».",
"admin.settings.sectionOcr": "OCR (PDF scannés)",
"admin.settings.sectionOcrDesc": "Extraction du texte des pages image avant traduction.",
"admin.settings.sectionEmails": "Emails",
"admin.settings.sectionEmailsDesc": "Serveur d'envoi pour les notifications et les relances.",
"admin.settings.enabled": "Activé",
"admin.settings.disabled": "Désactivé",
"admin.settings.envBadge": "Clé dans .env",
"admin.settings.envConfigured": "Configuré dans .env",
"admin.settings.envKeyPlaceholder": "Clé configurée dans .env (laisser vide pour l'utiliser)",
"admin.settings.apiKey": "Clé API",
"admin.settings.model": "Modèle",
"admin.settings.baseUrl": "URL de base",
"admin.settings.test": "Tester",
"admin.settings.testing": "Test...",
"admin.settings.ok": "OK",
"admin.settings.error": "Erreur",
"admin.settings.failure": "Échec",
"admin.settings.sending": "Envoi...",
"admin.settings.save": "Sauvegarder la configuration",
"admin.settings.saving": "Sauvegarde...",
"admin.settings.savedTitle": "✅ Configuration sauvegardée",
"admin.settings.savedDesc": "Les paramètres ont été enregistrés avec succès.",
"admin.settings.saveErrorTitle": "Erreur de sauvegarde",
"admin.settings.loadErrorTitle": "Erreur de chargement",
"admin.settings.loadErrorDesc": "HTTP {status} — vérifiez votre token admin.",
"admin.settings.networkErrorTitle": "Erreur réseau",
"admin.settings.networkErrorDesc": "Impossible de contacter le backend.",
"admin.settings.networkSaveErrorDesc": "Impossible de contacter le backend pour la sauvegarde.",
"admin.settings.connectionOk": "Connexion OK",
"admin.settings.modelsFound": "{count} modèles {provider} trouvés",
"admin.settings.providerError": "Erreur {provider}",
"admin.settings.google.title": "Google Translate",
"admin.settings.google.desc": "Accès web non officiel via deep_translator. Aucune clé requise, usage raisonnable recommandé.",
"admin.settings.googleCloud.title": "Google Cloud Translation (officiel)",
"admin.settings.googleCloud.desc": "API officielle Google Cloud. 500 000 caractères/mois offerts, puis environ 20 $ par million de caractères. Clé API sur console.cloud.google.com.",
"admin.settings.googleCloud.hint": "Activez Cloud Translation API dans Google Cloud Console, puis créez une clé API restreinte à cette API.",
"admin.settings.deepseek.title": "DeepSeek (accès direct)",
"admin.settings.deepseek.desc": "API DeepSeek directe, compatible OpenAI. Clé sur platform.deepseek.com — utilisable comme canal d'un palier IA.",
"admin.settings.zhipu.title": "z.ai / Zhipu (GLM)",
"admin.settings.zhipu.desc": "Accès direct aux modèles GLM de z.ai, compatible OpenAI. Clé sur z.ai — utilisable comme canal d'un palier IA.",
"admin.settings.zhipu.urlHint": "Par défaut : https://api.z.ai/api/paas/v4 — à changer uniquement si vous utilisez un proxy.",
"admin.settings.minimax.title": "MiniMax (accès direct)",
"admin.settings.minimax.desc": "API publique MiniMax, compatible OpenAI. Clé sur minimax.io — utilisable comme canal d'un palier IA.",
"admin.settings.openai.title": "OpenAI",
"admin.settings.openai.desc": "Traductions GPT. Obtenez une clé sur platform.openai.com.",
"admin.settings.zai.title": "xAI (Grok)",
"admin.settings.zai.desc": "Modèles Grok par xAI. API compatible OpenAI. Obtenez votre clé sur x.ai.",
"admin.settings.zai.urlHint": "Par défaut : https://api.x.ai/v1 — à changer uniquement si vous utilisez un proxy.",
"admin.settings.openrouter.title": "OpenRouter — clé de passerelle",
"admin.settings.openrouter.desc": "Palier Essentielle (forfait Pro). Gamme actuelle : deepseek/deepseek-v4-flash, z-ai/glm-5.3-flash, minimax/minimax-m3.",
"admin.settings.openrouter.keyLabel": "Clé API OpenRouter (partagée avec Premium)",
"admin.settings.openrouter.envSharedPlaceholder": "Clé configurée dans .env (partagée avec Premium)",
"admin.settings.openrouter.reminder": "Modèles gérés dans la page « Modèles & abonnements ».",
"admin.settings.openrouterPremium.title": "OpenRouter Premium — clé de passerelle",
"admin.settings.openrouterPremium.desc": "Palier Premium (forfait Business). Gamme actuelle : anthropic/claude-sonnet-5, deepseek/deepseek-v4-pro, z-ai/glm-5.3.",
"admin.settings.openrouterPremium.reminder": "Modèles gérés dans la page « Modèles & abonnements ». Partage la même clé OpenRouter.",
"admin.settings.mistral.title": "OCR Mistral (PDF scannés)",
"admin.settings.mistral.desc": "Extraction du texte des PDF scannés (pages image) avant traduction. Sans clé, ces fichiers sont refusés avec un message clair. Environ 1 € / 1 000 pages. Clé : console.mistral.ai.",
"admin.settings.mistral.keyLabel": "Clé API Mistral",
"admin.settings.mistral.modelLabel": "Modèle OCR",
"admin.settings.mistral.modelHint": "Recommandé : mistral-ocr-latest",
"admin.settings.smtp.title": "Email SMTP",
"admin.settings.smtp.desc": "Configuration du serveur SMTP pour l'envoi d'emails (mot de passe oublié, notifications, relances).",
"admin.settings.smtp.host": "Hôte SMTP",
"admin.settings.smtp.port": "Port",
"admin.settings.smtp.username": "Nom d'utilisateur",
"admin.settings.smtp.password": "Mot de passe",
"admin.settings.smtp.fromEmail": "Adresse d'expédition",
"admin.settings.smtp.useTls": "Utiliser TLS",
"admin.settings.smtp.sendTest": "Envoyer un email de test",
"admin.settings.smtp.sent": "Envoyé",
"admin.settings.smtp.sendFailed": "Échec de l'envoi",
"admin.models.channelOpenrouter": "OpenRouter",
"admin.models.channelOpenrouterPremium": "OpenRouter (Premium)",
"admin.models.channelDeepseek": "DeepSeek (accès direct)",
"admin.models.channelZhipu": "z.ai (GLM)",
"admin.models.channelMinimax": "MiniMax (accès direct)",
"admin.models.channelOpenai": "OpenAI",
"admin.models.channelZai": "xAI (Grok)",
"admin.models.customModelPlaceholder": "identifiant-du-modèle",
"admin.models.routeHint": "Le canal choisit l'accès direct ou la passerelle ; les identifiants de modèles diffèrent d'un canal à l'autre.",
"admin.models.addRouteConfirm": "Ajouter",
"admin.marketing.generateTitle": "Générer avec l'IA",
"admin.marketing.generateDesc": "Le texte est rédigé à partir du plan marketing (positionnement et chiffres autorisés). Rien n'est envoyé automatiquement : relisez puis envoyez.",
"admin.marketing.channel": "Canal",
"admin.marketing.model": "Modèle",
"admin.marketing.modelPlaceholder": "Par défaut du canal",
"admin.marketing.promoOptional": "Réduction (facultatif)",
"admin.marketing.promoNone": "Aucune",
"admin.marketing.brief": "Brief",
"admin.marketing.briefPlaceholder": "Ex. : relancer les inscrits de la liste d'attente avec un rappel des avantages du forfait Pro.",
"admin.marketing.language": "Langue de l'email",
"admin.marketing.tone": "Ton",
"admin.marketing.toneNone": "Neutre",
"admin.marketing.generateBtn": "Générer",
"admin.marketing.generating": "Génération...",
"admin.marketing.generatedTitle": "Email généré",
"admin.marketing.generatedDesc": "Sujet et HTML insérés dans le formulaire — relisez avant envoi ({provider}).",
"admin.marketing.generateFailedTitle": "Génération impossible",
"admin.marketing.briefRequired": "Décrivez d'abord le contenu voulu dans le brief.",
"admin.marketing.langFr": "Français",
"admin.marketing.langEn": "Anglais",
"admin.marketing.langDe": "Allemand",
"admin.marketing.langEs": "Espagnol",
"admin.marketing.langIt": "Italien",
"admin.marketing.langPt": "Portugais",
"admin.marketing.langNl": "Néerlandais",
"admin.marketing.langAr": "Arabe",
"admin.marketing.langFa": "Persan",
"admin.marketing.langJa": "Japonais",
"admin.marketing.langKo": "Coréen",
"admin.marketing.langRu": "Russe",
"admin.marketing.langZh": "Chinois",
"admin.marketing.toneFriendly": "Amical",
"admin.marketing.toneProfessional": "Professionnel",
"admin.marketing.toneUrgent": "Urgent",
"admin.marketing.toneFestive": "Festif",
"admin.marketing.promoTitle": "Réductions",
"admin.marketing.promoDesc": "Codes promo appliqués au paiement par Stripe ; le compteur augmente quand l'abonnement est réellement souscrit.",
"admin.marketing.promoEmpty": "Aucun code promo pour le moment.",
"admin.marketing.promoActive": "Actif",
"admin.marketing.promoInactive": "Désactivé",
"admin.marketing.promoLocalOnly": "Hors Stripe",
"admin.marketing.promoDeactivate": "Désactiver",
"admin.marketing.promoUsage": "{used}/{max} utilisations",
"admin.marketing.promoPlansLabel": "Forfaits :",
"admin.marketing.promoAllPlans": "Tous",
"admin.marketing.promoExpiresOn": "expire le {date}",
"admin.marketing.promoNeverExpires": "sans expiration",
"admin.marketing.promoCreateTitle": "Créer une réduction",
"admin.marketing.promoCreateDesc": "Le code est créé aussi chez Stripe (coupon + code promotionnel) quand Stripe est configuré.",
"admin.marketing.promoCode": "Code",
"admin.marketing.promoType": "Type",
"admin.marketing.promoTypePercent": "Pourcentage",
"admin.marketing.promoTypeAmount": "Montant (EUR)",
"admin.marketing.promoValue": "Valeur",
"admin.marketing.promoPlans": "Forfaits éligibles",
"admin.marketing.promoMaxRedemptions": "Plafond d'utilisations",
"admin.marketing.promoMaxPlaceholder": "Illimité",
"admin.marketing.promoExpires": "Expiration",
"admin.marketing.promoCreate": "Créer le code",
"admin.marketing.promoCreating": "Création...",
"admin.marketing.promoCreatedTitle": "Code promo créé",
"admin.marketing.promoCreatedDesc": "Le code {code} est prêt.",
"admin.marketing.promoCreateFailedTitle": "Création refusée",
"admin.marketing.promoInvalidInput": "Vérifiez le code et la valeur.",
"admin.marketing.promoDeactivatedTitle": "Code désactivé",
"admin.marketing.promoDeactivatedDesc": "Le code {code} n'est plus applicable.",
"admin.settings.fallback.title": "Chaîne de secours",
"admin.settings.fallback.desc": "Ordre de priorité pour la sélection automatique des fournisseurs.",
"admin.settings.fallback.classic": "Mode classique (Google)",
"admin.settings.fallback.llm": "Mode LLM (passerelles et accès directs)",
"admin.settings.fallback.note": "Seuls les canaux enregistrés (google, openai, deepseek, minimax) participent à cette chaîne ; les paliers IA et les autres canaux n'y participent pas.",
"admin.models.duplicateRouteTitle": "Doublon refusé",
"admin.models.duplicateRouteDesc": "Ce couple canal + modèle existe déjà dans la liste.",
"admin.marketing.promoDeactivateFailedTitle": "Désactivation refusée",
"admin.marketing.promoDelete": "Supprimer",
"admin.marketing.promoDeleteConfirm": "Supprimer définitivement le code {code} ?",
"admin.marketing.promoDeletedTitle": "Code supprimé",
"admin.marketing.promoDeletedDesc": "Le code {code} a été supprimé.",
"admin.marketing.promoDeleteFailedTitle": "Suppression refusée",
"admin.marketing.promoLinkStripe": "Relier à Stripe",
"admin.marketing.promoLinkedTitle": "Code relié à Stripe",
"admin.marketing.promoLinkedDesc": "Le code {code} est maintenant applicable au paiement.",
"admin.marketing.promoLinkFailedTitle": "Reliage impossible",
"admin.marketing.promoExpiryHint": "Un code « jusqu'au 10 » reste valable tout le 10 (fin : 23:59:59 UTC).",
"admin.marketing.generateEmpty": "La réponse du serveur était vide ou inexploitable."
}

View File

@@ -167,5 +167,17 @@
"pricing.aiModels.essential.price": "À partir de 0,09 $ par million de jetons",
"pricing.aiModels.premium.modelName": "Claude 5",
"pricing.aiModels.premium.alternatives": "Alternatives : DeepSeek V4 Pro et GLM-5.3",
"pricing.aiModels.premium.price": "2 $ / 10 $ par 1M de jetons"
"pricing.aiModels.premium.price": "2 $ / 10 $ par 1M de jetons",
"pricing.promo.label": "Code promo",
"pricing.promo.placeholder": "VOTRCODE",
"pricing.promo.apply": "Appliquer",
"pricing.promo.checking": "Vérification...",
"pricing.promo.valid": "Code valable",
"pricing.promo.subscriptionOnly": "Valable sur les abonnements, pas sur les crédits.",
"pricing.promoError.unknown": "Ce code promo n'existe pas.",
"pricing.promoError.inactive": "Ce code promo n'est plus actif.",
"pricing.promoError.expired": "Ce code promo a expiré.",
"pricing.promoError.exhausted": "Ce code promo a atteint sa limite d'utilisations.",
"pricing.promoError.plan": "Ce code promo n'est pas valable pour ce forfait.",
"pricing.promoError.local_only": "Ce code promo n'est pas relié au paiement en ligne."
}

View File

@@ -145,5 +145,180 @@
"admin.stats.tiersSub": "Essenziale / Premium · classico {classic} · altro {other}",
"admin.stats.unavailable": "Statistiche business non disponibili ({error}).",
"admin.stats.waitlist": "Lista d'attesa",
"admin.stats.waitlistSub": "iscritti in attesa"
"admin.stats.waitlistSub": "iscritti in attesa",
"admin.settings.title": "Fornitori",
"admin.settings.subtitle": "Configura gli accessi ai fornitori. Le chiavi API possono anche essere definite nel file .env.",
"admin.settings.sectionClassic": "Traduzione classica",
"admin.settings.sectionClassicDesc": "Motori Google per la traduzione standard.",
"admin.settings.sectionDirect": "Fornitori IA diretti",
"admin.settings.sectionDirectDesc": "Accessi diretti compatibili OpenAI — utilizzabili come canale di un livello IA (pagina «Modelli e abbonamenti») o per la generazione delle email.",
"admin.settings.sectionGateway": "Gateway multi-modello",
"admin.settings.sectionGatewayDesc": "Una sola chiave, centinaia di modelli. I modelli per livello si gestiscono nella pagina «Modelli e abbonamenti».",
"admin.settings.sectionOcr": "OCR (PDF scansionati)",
"admin.settings.sectionOcrDesc": "Estrazione del testo dalle pagine immagine prima della traduzione.",
"admin.settings.sectionEmails": "Email",
"admin.settings.sectionEmailsDesc": "Server di invio per notifiche e promemoria.",
"admin.settings.enabled": "Attivato",
"admin.settings.disabled": "Disattivato",
"admin.settings.envBadge": "Chiave in .env",
"admin.settings.envConfigured": "Configurato in .env",
"admin.settings.envKeyPlaceholder": "Chiave configurata in .env (lasciare vuoto per usarla)",
"admin.settings.apiKey": "Chiave API",
"admin.settings.model": "Modello",
"admin.settings.baseUrl": "URL di base",
"admin.settings.test": "Prova",
"admin.settings.testing": "Test...",
"admin.settings.ok": "OK",
"admin.settings.error": "Errore",
"admin.settings.failure": "Fallito",
"admin.settings.sending": "Invio...",
"admin.settings.save": "Salva la configurazione",
"admin.settings.saving": "Salvataggio...",
"admin.settings.savedTitle": "✅ Configurazione salvata",
"admin.settings.savedDesc": "Le impostazioni sono state salvate correttamente.",
"admin.settings.saveErrorTitle": "Errore di salvataggio",
"admin.settings.loadErrorTitle": "Errore di caricamento",
"admin.settings.loadErrorDesc": "HTTP {status} — verifica il tuo token amministratore.",
"admin.settings.networkErrorTitle": "Errore di rete",
"admin.settings.networkErrorDesc": "Impossibile contattare il backend.",
"admin.settings.networkSaveErrorDesc": "Impossibile contattare il backend per salvare.",
"admin.settings.connectionOk": "Connessione OK",
"admin.settings.modelsFound": "{count} modelli {provider} trovati",
"admin.settings.providerError": "Errore {provider}",
"admin.settings.google.title": "Google Translate",
"admin.settings.google.desc": "Accesso web non ufficiale tramite deep_translator. Nessuna chiave richiesta, uso ragionevole consigliato.",
"admin.settings.googleCloud.title": "Google Cloud Translation (ufficiale)",
"admin.settings.googleCloud.desc": "API ufficiale di Google Cloud. 500 000 caratteri/mese gratuiti, poi circa 20 $ per milione di caratteri. Chiave API su console.cloud.google.com.",
"admin.settings.googleCloud.hint": "Attiva la Cloud Translation API nella Google Cloud Console, poi crea una chiave API limitata a questa API.",
"admin.settings.deepseek.title": "DeepSeek (diretto)",
"admin.settings.deepseek.desc": "API diretta DeepSeek, compatibile OpenAI. Chiave su platform.deepseek.com — utilizzabile come canale di un livello IA.",
"admin.settings.zhipu.title": "z.ai / Zhipu (GLM)",
"admin.settings.zhipu.desc": "Accesso diretto ai modelli GLM di z.ai, compatibile OpenAI. Chiave su z.ai — utilizzabile come canale di un livello IA.",
"admin.settings.zhipu.urlHint": "Predefinito: https://api.z.ai/api/paas/v4 — da cambiare solo se usi un proxy.",
"admin.settings.minimax.title": "MiniMax (diretto)",
"admin.settings.minimax.desc": "API pubblica MiniMax, compatibile OpenAI. Chiave su minimax.io — utilizzabile come canale di un livello IA.",
"admin.settings.openai.title": "OpenAI",
"admin.settings.openai.desc": "Traduzioni GPT. Ottieni una chiave su platform.openai.com.",
"admin.settings.zai.title": "xAI (Grok)",
"admin.settings.zai.desc": "Modelli Grok di xAI. API compatibile OpenAI. Ottieni la chiave su x.ai.",
"admin.settings.zai.urlHint": "Predefinito: https://api.x.ai/v1 — da cambiare solo se usi un proxy.",
"admin.settings.openrouter.title": "OpenRouter — chiave gateway",
"admin.settings.openrouter.desc": "Livello Essenziale (piano Pro). Gamma attuale: deepseek/deepseek-v4-flash, z-ai/glm-5.3-flash, minimax/minimax-m3.",
"admin.settings.openrouter.keyLabel": "Chiave API OpenRouter (condivisa con Premium)",
"admin.settings.openrouter.envSharedPlaceholder": "Chiave configurata in .env (condivisa con Premium)",
"admin.settings.openrouter.reminder": "Modelli gestiti nella pagina «Modelli e abbonamenti».",
"admin.settings.openrouterPremium.title": "OpenRouter Premium — chiave gateway",
"admin.settings.openrouterPremium.desc": "Livello Premium (piano Business). Gamma attuale: anthropic/claude-sonnet-5, deepseek/deepseek-v4-pro, z-ai/glm-5.3.",
"admin.settings.openrouterPremium.reminder": "Modelli gestiti nella pagina «Modelli e abbonamenti». Condivide la stessa chiave OpenRouter.",
"admin.settings.mistral.title": "OCR Mistral (PDF scansionati)",
"admin.settings.mistral.desc": "Estrae il testo dai PDF scansionati (pagine immagine) prima della traduzione. Senza chiave questi file vengono rifiutati con un messaggio chiaro. Circa 1 € / 1 000 pagine. Chiave: console.mistral.ai.",
"admin.settings.mistral.keyLabel": "Chiave API Mistral",
"admin.settings.mistral.modelLabel": "Modello OCR",
"admin.settings.mistral.modelHint": "Consigliato: mistral-ocr-latest",
"admin.settings.smtp.title": "Email SMTP",
"admin.settings.smtp.desc": "Configurazione del server SMTP per l'invio delle email (password dimenticata, notifiche, promemoria).",
"admin.settings.smtp.host": "Host SMTP",
"admin.settings.smtp.port": "Porta",
"admin.settings.smtp.username": "Nome utente",
"admin.settings.smtp.password": "Password",
"admin.settings.smtp.fromEmail": "Indirizzo mittente",
"admin.settings.smtp.useTls": "Usa TLS",
"admin.settings.smtp.sendTest": "Invia un'email di prova",
"admin.settings.smtp.sent": "Inviata",
"admin.settings.smtp.sendFailed": "Invio non riuscito",
"admin.models.channelOpenrouter": "OpenRouter",
"admin.models.channelOpenrouterPremium": "OpenRouter (Premium)",
"admin.models.channelDeepseek": "DeepSeek (diretto)",
"admin.models.channelZhipu": "z.ai (GLM)",
"admin.models.channelMinimax": "MiniMax (diretto)",
"admin.models.channelOpenai": "OpenAI",
"admin.models.channelZai": "xAI (Grok)",
"admin.models.customModelPlaceholder": "id-del-modello",
"admin.models.routeHint": "Il canale sceglie l'accesso diretto o il gateway; gli identificativi dei modelli cambiano da un canale all'altro.",
"admin.models.addRouteConfirm": "Aggiungi",
"admin.marketing.generateTitle": "Genera con l'IA",
"admin.marketing.generateDesc": "Il testo è scritto a partire dal piano marketing (posizionamento e cifre autorizzate). Nulla viene inviato automaticamente: rileggi e poi invia.",
"admin.marketing.channel": "Canale",
"admin.marketing.model": "Modello",
"admin.marketing.modelPlaceholder": "Predefinito del canale",
"admin.marketing.promoOptional": "Sconto (facoltativo)",
"admin.marketing.promoNone": "Nessuno",
"admin.marketing.brief": "Brief",
"admin.marketing.briefPlaceholder": "Es.: ricontatta gli iscritti alla lista d'attente ricordando i vantaggi del piano Pro.",
"admin.marketing.language": "Lingua dell'email",
"admin.marketing.tone": "Tono",
"admin.marketing.toneNone": "Neutro",
"admin.marketing.generateBtn": "Genera",
"admin.marketing.generating": "Generazione...",
"admin.marketing.generatedTitle": "Email generata",
"admin.marketing.generatedDesc": "Oggetto e HTML inseriti nel modulo — rileggi prima di inviare ({provider}).",
"admin.marketing.generateFailedTitle": "Generazione non riuscita",
"admin.marketing.briefRequired": "Descrivi prima il contenuto desiderato nel brief.",
"admin.marketing.langFr": "Francese",
"admin.marketing.langEn": "Inglese",
"admin.marketing.langDe": "Tedesco",
"admin.marketing.langEs": "Spagnolo",
"admin.marketing.langIt": "Italiano",
"admin.marketing.langPt": "Portoghese",
"admin.marketing.langNl": "Olandese",
"admin.marketing.langAr": "Arabo",
"admin.marketing.langFa": "Persiano",
"admin.marketing.langJa": "Giapponese",
"admin.marketing.langKo": "Coreano",
"admin.marketing.langRu": "Russo",
"admin.marketing.langZh": "Cinese",
"admin.marketing.toneFriendly": "Amichevole",
"admin.marketing.toneProfessional": "Professionale",
"admin.marketing.toneUrgent": "Urgente",
"admin.marketing.toneFestive": "Festivo",
"admin.marketing.promoTitle": "Sconti",
"admin.marketing.promoDesc": "Codici promo applicati al pagamento tramite Stripe; il contatore aumenta quando l'abbonamento è davvero completato.",
"admin.marketing.promoEmpty": "Ancora nessun codice promo.",
"admin.marketing.promoActive": "Attivo",
"admin.marketing.promoInactive": "Disattivato",
"admin.marketing.promoLocalOnly": "Fuori Stripe",
"admin.marketing.promoDeactivate": "Disattiva",
"admin.marketing.promoUsage": "{used}/{max} utilizzi",
"admin.marketing.promoPlansLabel": "Piani:",
"admin.marketing.promoAllPlans": "Tutti",
"admin.marketing.promoExpiresOn": "scade il {date}",
"admin.marketing.promoNeverExpires": "senza scadenza",
"admin.marketing.promoCreateTitle": "Crea uno sconto",
"admin.marketing.promoCreateDesc": "Il codice viene creato anche su Stripe (coupon + codice promozionale) quando Stripe è configurato.",
"admin.marketing.promoCode": "Codice",
"admin.marketing.promoType": "Tipo",
"admin.marketing.promoTypePercent": "Percentuale",
"admin.marketing.promoTypeAmount": "Importo (EUR)",
"admin.marketing.promoValue": "Valore",
"admin.marketing.promoPlans": "Piani idonei",
"admin.marketing.promoMaxRedemptions": "Limite di utilizzi",
"admin.marketing.promoMaxPlaceholder": "Illimitato",
"admin.marketing.promoExpires": "Scadenza",
"admin.marketing.promoCreate": "Crea il codice",
"admin.marketing.promoCreating": "Creazione...",
"admin.marketing.promoCreatedTitle": "Codice promo creato",
"admin.marketing.promoCreatedDesc": "Il codice {code} è pronto.",
"admin.marketing.promoCreateFailedTitle": "Creazione rifiutata",
"admin.marketing.promoInvalidInput": "Verifica il codice e il valore.",
"admin.marketing.promoDeactivatedTitle": "Codice disattivato",
"admin.marketing.promoDeactivatedDesc": "Il codice {code} non è più applicabile.",
"admin.settings.fallback.title": "Catena di soccorso",
"admin.settings.fallback.desc": "Ordine di priorità per la selezione automatica dei fornitori.",
"admin.settings.fallback.classic": "Modalità classica (Google)",
"admin.settings.fallback.llm": "Modalità LLM (gateway e accessi diretti)",
"admin.settings.fallback.note": "Solo i canali registrati (google, openai, deepseek, minimax) partecipano a questa catena; i livelli IA e gli altri canali no.",
"admin.models.duplicateRouteTitle": "Duplicato rifiutato",
"admin.models.duplicateRouteDesc": "Questa coppia canale + modello è già nell'elenco.",
"admin.marketing.promoDeactivateFailedTitle": "Disattivazione rifiutata",
"admin.marketing.promoDelete": "Elimina",
"admin.marketing.promoDeleteConfirm": "Eliminare definitivamente il codice {code}?",
"admin.marketing.promoDeletedTitle": "Codice eliminato",
"admin.marketing.promoDeletedDesc": "Il codice {code} è stato eliminato.",
"admin.marketing.promoDeleteFailedTitle": "Eliminazione rifiutata",
"admin.marketing.promoLinkStripe": "Collega a Stripe",
"admin.marketing.promoLinkedTitle": "Codice collegato a Stripe",
"admin.marketing.promoLinkedDesc": "Il codice {code} ora è applicabile al pagamento.",
"admin.marketing.promoLinkFailedTitle": "Collegamento non riuscito",
"admin.marketing.promoExpiryHint": "Un codice «fino al 10» resta valido tutto il 10 (fine: 23:59:59 UTC).",
"admin.marketing.generateEmpty": "La risposta del server era vuota o inutilizzabile."
}

View File

@@ -167,5 +167,17 @@
"pricing.header.titleBase": "Un piano per",
"pricing.header.titleAccent": "ogni esigenza",
"pricing.toast.close": "Chiudi",
"pricing.aiModels.premium.price": "2 $ / 10 $ per 1M di token"
"pricing.aiModels.premium.price": "2 $ / 10 $ per 1M di token",
"pricing.promo.label": "Codice promo",
"pricing.promo.placeholder": "TUOCODICE",
"pricing.promo.apply": "Applica",
"pricing.promo.checking": "Verifica...",
"pricing.promo.valid": "Codice valido",
"pricing.promo.subscriptionOnly": "Valido sugli abbonamenti, non sui crediti.",
"pricing.promoError.unknown": "Questo codice promo non esiste.",
"pricing.promoError.inactive": "Questo codice promo non è più attivo.",
"pricing.promoError.expired": "Questo codice promo è scaduto.",
"pricing.promoError.exhausted": "Questo codice promo ha raggiunto il limite di utilizzi.",
"pricing.promoError.plan": "Questo codice promo non è valido per questo piano.",
"pricing.promoError.local_only": "Questo codice promo non è abilitato al pagamento online."
}

View File

@@ -145,5 +145,180 @@
"admin.stats.tiersSub": "エッセンシャル / プレミアム · クラシック {classic} · その他 {other}",
"admin.stats.unavailable": "ビジネス統計を利用できません({error})。",
"admin.stats.waitlist": "ウェイトリスト",
"admin.stats.waitlistSub": "待機中の登録者"
"admin.stats.waitlistSub": "待機中の登録者",
"admin.settings.title": "プロバイダー",
"admin.settings.subtitle": "プロバイダーへのアクセスを設定します。APIキーは.envファイルにも設定できます。",
"admin.settings.sectionClassic": "クラシック翻訳",
"admin.settings.sectionClassicDesc": "標準翻訳用のGoogleエンジン。",
"admin.settings.sectionDirect": "直接AIプロバイダー",
"admin.settings.sectionDirectDesc": "OpenAI互換の直接アクセス — AIティアのチャネル「モデルとサブスクリプション」ページやメール生成に使用できます。",
"admin.settings.sectionGateway": "マルチモデルゲートウェイ",
"admin.settings.sectionGatewayDesc": "鍵は1つで、モデルは数百。ティアごとのモデルは「モデルとサブスクリプション」ページで管理します。",
"admin.settings.sectionOcr": "OCRスキャンPDF",
"admin.settings.sectionOcrDesc": "翻訳前に画像ページからテキストを抽出します。",
"admin.settings.sectionEmails": "メール",
"admin.settings.sectionEmailsDesc": "通知やフォローアップメールを送るサーバー。",
"admin.settings.enabled": "有効",
"admin.settings.disabled": "無効",
"admin.settings.envBadge": ".envのキー",
"admin.settings.envConfigured": ".envに設定済み",
"admin.settings.envKeyPlaceholder": ".envに設定済みのキー使う場合は空のまま",
"admin.settings.apiKey": "APIキー",
"admin.settings.model": "モデル",
"admin.settings.baseUrl": "ベースURL",
"admin.settings.test": "テスト",
"admin.settings.testing": "テスト中...",
"admin.settings.ok": "OK",
"admin.settings.error": "エラー",
"admin.settings.failure": "失敗",
"admin.settings.sending": "送信中...",
"admin.settings.save": "設定を保存",
"admin.settings.saving": "保存中...",
"admin.settings.savedTitle": "✅ 設定を保存しました",
"admin.settings.savedDesc": "設定は正常に保存されました。",
"admin.settings.saveErrorTitle": "保存エラー",
"admin.settings.loadErrorTitle": "読み込みエラー",
"admin.settings.loadErrorDesc": "HTTP {status} — 管理者トークンを確認してください。",
"admin.settings.networkErrorTitle": "ネットワークエラー",
"admin.settings.networkErrorDesc": "バックエンドに接続できません。",
"admin.settings.networkSaveErrorDesc": "保存のためバックエンドに接続できません。",
"admin.settings.connectionOk": "接続OK",
"admin.settings.modelsFound": "{provider}のモデルが{count}件見つかりました",
"admin.settings.providerError": "{provider}エラー",
"admin.settings.google.title": "Google Translate",
"admin.settings.google.desc": "deep_translator経由の非公式ウェブアクセス。キー不要、適度な利用を推奨。",
"admin.settings.googleCloud.title": "Google Cloud Translation公式",
"admin.settings.googleCloud.desc": "Google Cloudの公式API。毎月50万文字まで無料、その後は約100万文字あたり20ドル。キーはconsole.cloud.google.comで取得。",
"admin.settings.googleCloud.hint": "Google Cloud ConsoleでCloud Translation APIを有効にし、そのAPIに限定したAPIキーを作成してください。",
"admin.settings.deepseek.title": "DeepSeek直接",
"admin.settings.deepseek.desc": "OpenAI互換のDeepSeek直接API。キーはplatform.deepseek.comで取得 — AIティアのチャネルとして使用可能。",
"admin.settings.zhipu.title": "z.ai / ZhipuGLM",
"admin.settings.zhipu.desc": "z.aiのGLMモデルへの直接アクセス、OpenAI互換。キーはz.aiで取得 — AIティアのチャネルとして使用可能。",
"admin.settings.zhipu.urlHint": "既定https://api.z.ai/api/paas/v4 — プロキシを使う場合のみ変更。",
"admin.settings.minimax.title": "MiniMax直接",
"admin.settings.minimax.desc": "OpenAI互換のMiniMax公開API。キーはminimax.ioで取得 — AIティアのチャネルとして使用可能。",
"admin.settings.openai.title": "OpenAI",
"admin.settings.openai.desc": "GPT翻訳。platform.openai.comでキーを取得してください。",
"admin.settings.zai.title": "xAIGrok",
"admin.settings.zai.desc": "xAIのGrokモデル。OpenAI互換API。キーはx.aiで取得してください。",
"admin.settings.zai.urlHint": "既定https://api.x.ai/v1 — プロキシを使う場合のみ変更。",
"admin.settings.openrouter.title": "OpenRouter — ゲートウェイキー",
"admin.settings.openrouter.desc": "EssentialティアProプラン。現在のラインナップdeepseek/deepseek-v4-flash、z-ai/glm-5.3-flash、minimax/minimax-m3。",
"admin.settings.openrouter.keyLabel": "OpenRouter APIキーPremiumと共有",
"admin.settings.openrouter.envSharedPlaceholder": ".envに設定済みのキーPremiumと共有",
"admin.settings.openrouter.reminder": "モデルは「モデルとサブスクリプション」ページで管理します。",
"admin.settings.openrouterPremium.title": "OpenRouter Premium — ゲートウェイキー",
"admin.settings.openrouterPremium.desc": "PremiumティアBusinessプラン。現在のラインナップanthropic/claude-sonnet-5、deepseek/deepseek-v4-pro、z-ai/glm-5.3。",
"admin.settings.openrouterPremium.reminder": "モデルは「モデルとサブスクリプション」ページで管理します。同じOpenRouterキーを共有します。",
"admin.settings.mistral.title": "Mistral OCRスキャンPDF",
"admin.settings.mistral.desc": "スキャンPDF画像ページから翻訳前にテキストを抽出します。キーがない場合、これらのファイルは明確なメッセージ付きで拒否されます。約1ユーロ/1,000ページ。キーconsole.mistral.ai。",
"admin.settings.mistral.keyLabel": "Mistral APIキー",
"admin.settings.mistral.modelLabel": "OCRモデル",
"admin.settings.mistral.modelHint": "推奨mistral-ocr-latest",
"admin.settings.smtp.title": "SMTPメール",
"admin.settings.smtp.desc": "メール送信のためのSMTPサーバー設定パスワード再設定、通知、フォローアップ。",
"admin.settings.smtp.host": "SMTPホスト",
"admin.settings.smtp.port": "ポート",
"admin.settings.smtp.username": "ユーザー名",
"admin.settings.smtp.password": "パスワード",
"admin.settings.smtp.fromEmail": "送信元アドレス",
"admin.settings.smtp.useTls": "TLSを使用",
"admin.settings.smtp.sendTest": "テストメールを送信",
"admin.settings.smtp.sent": "送信済み",
"admin.settings.smtp.sendFailed": "送信に失敗しました",
"admin.models.channelOpenrouter": "OpenRouter",
"admin.models.channelOpenrouterPremium": "OpenRouterPremium",
"admin.models.channelDeepseek": "DeepSeek直接",
"admin.models.channelZhipu": "z.aiGLM",
"admin.models.channelMinimax": "MiniMax直接",
"admin.models.channelOpenai": "OpenAI",
"admin.models.channelZai": "xAIGrok",
"admin.models.customModelPlaceholder": "モデルID",
"admin.models.routeHint": "チャネルで直接アクセスかゲートウェイかを選びます。モデルIDはチャネルごとに異なります。",
"admin.models.addRouteConfirm": "追加",
"admin.marketing.generateTitle": "AIで生成",
"admin.marketing.generateDesc": "文章はマーケティングプラン(ポジショニングと許可された数値)に基づいて書かれます。自動送信は一切ありません。確認してから送信してください。",
"admin.marketing.channel": "チャネル",
"admin.marketing.model": "モデル",
"admin.marketing.modelPlaceholder": "チャネルの既定",
"admin.marketing.promoOptional": "割引(任意)",
"admin.marketing.promoNone": "なし",
"admin.marketing.brief": "概要",
"admin.marketing.briefPlaceholder": "例ウェイティングリスト登録者にProプランの特典を思い出してもらう。",
"admin.marketing.language": "メールの言語",
"admin.marketing.tone": "トーン",
"admin.marketing.toneNone": "標準",
"admin.marketing.generateBtn": "生成",
"admin.marketing.generating": "生成中...",
"admin.marketing.generatedTitle": "メールを生成しました",
"admin.marketing.generatedDesc": "件名とHTMLをフォームに挿入しました — 送信前に確認してください({provider})。",
"admin.marketing.generateFailedTitle": "生成できませんでした",
"admin.marketing.briefRequired": "まず概要に希望する内容を記入してください。",
"admin.marketing.langFr": "フランス語",
"admin.marketing.langEn": "英語",
"admin.marketing.langDe": "ドイツ語",
"admin.marketing.langEs": "スペイン語",
"admin.marketing.langIt": "イタリア語",
"admin.marketing.langPt": "ポルトガル語",
"admin.marketing.langNl": "オランダ語",
"admin.marketing.langAr": "アラビア語",
"admin.marketing.langFa": "ペルシャ語",
"admin.marketing.langJa": "日本語",
"admin.marketing.langKo": "韓国語",
"admin.marketing.langRu": "ロシア語",
"admin.marketing.langZh": "中国語",
"admin.marketing.toneFriendly": "親しみやすく",
"admin.marketing.toneProfessional": "プロフェッショナル",
"admin.marketing.toneUrgent": "緊急",
"admin.marketing.toneFestive": "お祝い",
"admin.marketing.promoTitle": "割引",
"admin.marketing.promoDesc": "プロモコードはStripeの支払いに適用されます。カウンターはサブスクリプションが実際に完了したときだけ増えます。",
"admin.marketing.promoEmpty": "プロモコードはまだありません。",
"admin.marketing.promoActive": "有効",
"admin.marketing.promoInactive": "無効",
"admin.marketing.promoLocalOnly": "Stripe外",
"admin.marketing.promoDeactivate": "無効化",
"admin.marketing.promoUsage": "{used}/{max}回使用",
"admin.marketing.promoPlansLabel": "プラン:",
"admin.marketing.promoAllPlans": "すべて",
"admin.marketing.promoExpiresOn": "{date}に期限切れ",
"admin.marketing.promoNeverExpires": "期限なし",
"admin.marketing.promoCreateTitle": "割引を作成",
"admin.marketing.promoCreateDesc": "Stripeが設定されている場合、コードはStripeにも作成されますクーポンプロモコード。",
"admin.marketing.promoCode": "コード",
"admin.marketing.promoType": "種類",
"admin.marketing.promoTypePercent": "パーセント",
"admin.marketing.promoTypeAmount": "金額EUR",
"admin.marketing.promoValue": "値",
"admin.marketing.promoPlans": "対象プラン",
"admin.marketing.promoMaxRedemptions": "使用上限",
"admin.marketing.promoMaxPlaceholder": "無制限",
"admin.marketing.promoExpires": "有効期限",
"admin.marketing.promoCreate": "コードを作成",
"admin.marketing.promoCreating": "作成中...",
"admin.marketing.promoCreatedTitle": "プロモコードを作成しました",
"admin.marketing.promoCreatedDesc": "コード{code}は準備できました。",
"admin.marketing.promoCreateFailedTitle": "作成できませんでした",
"admin.marketing.promoInvalidInput": "コードと値を確認してください。",
"admin.marketing.promoDeactivatedTitle": "コードを無効化しました",
"admin.marketing.promoDeactivatedDesc": "コード{code}はもう適用されません。",
"admin.settings.fallback.title": "フォールバックチェーン",
"admin.settings.fallback.desc": "プロバイダー自動選択の優先順位。",
"admin.settings.fallback.classic": "クラシックモードGoogle",
"admin.settings.fallback.llm": "LLMモードゲートウェイと直接アクセス",
"admin.settings.fallback.note": "このチェーンに参加するのは登録済みチャネルgoogle、openai、deepseek、minimaxのみです。AIティアや他のチャネルは参加しません。",
"admin.models.duplicateRouteTitle": "重複は拒否されました",
"admin.models.duplicateRouteDesc": "そのチャネル+モデルの組み合わせは既にリストにあります。",
"admin.marketing.promoDeactivateFailedTitle": "無効化できませんでした",
"admin.marketing.promoDelete": "削除",
"admin.marketing.promoDeleteConfirm": "コード{code}を完全に削除しますか?",
"admin.marketing.promoDeletedTitle": "コードを削除しました",
"admin.marketing.promoDeletedDesc": "コード{code}を削除しました。",
"admin.marketing.promoDeleteFailedTitle": "削除できませんでした",
"admin.marketing.promoLinkStripe": "Stripeにリンク",
"admin.marketing.promoLinkedTitle": "コードをStripeにリンクしました",
"admin.marketing.promoLinkedDesc": "コード{code}は決済で使えるようになりました。",
"admin.marketing.promoLinkFailedTitle": "リンクできませんでした",
"admin.marketing.promoExpiryHint": "「10日まで」のコードは10日いっぱい23:59:59 UTCまで有効です。",
"admin.marketing.generateEmpty": "サーバーの応答が空か、利用できませんでした。"
}

View File

@@ -167,5 +167,17 @@
"pricing.header.titleBase": "あらゆるニーズに",
"pricing.header.titleAccent": "ぴったりのプラン",
"pricing.toast.close": "閉じる",
"pricing.aiModels.premium.price": "100万トークンあたり $2 / $10"
"pricing.aiModels.premium.price": "100万トークンあたり $2 / $10",
"pricing.promo.label": "プロモコード",
"pricing.promo.placeholder": "あなたのコード",
"pricing.promo.apply": "適用",
"pricing.promo.checking": "確認中...",
"pricing.promo.valid": "コードは有効です",
"pricing.promo.subscriptionOnly": "サブスクリプションのみ有効。クレジットには使えません。",
"pricing.promoError.unknown": "このプロモコードは存在しません。",
"pricing.promoError.inactive": "このプロモコードは無効になっています。",
"pricing.promoError.expired": "このプロモコードは期限切れです。",
"pricing.promoError.exhausted": "このプロモコードは使用上限に達しました。",
"pricing.promoError.plan": "このプロモコードはこのプランには使えません。",
"pricing.promoError.local_only": "このプロモコードはオンライン決済に対応していません。"
}

View File

@@ -145,5 +145,180 @@
"admin.stats.tiersSub": "에센셜 / 프리미엄 · 클래식 {classic} · 기타 {other}",
"admin.stats.unavailable": "비즈니스 통계를 사용할 수 없습니다 ({error}).",
"admin.stats.waitlist": "대기 명단",
"admin.stats.waitlistSub": "대기 중인 등록자"
"admin.stats.waitlistSub": "대기 중인 등록자",
"admin.settings.title": "공급자",
"admin.settings.subtitle": "공급자 접근을 설정합니다. API 키는 .env 파일에서도 설정할 수 있습니다.",
"admin.settings.sectionClassic": "클래식 번역",
"admin.settings.sectionClassicDesc": "표준 번역을 위한 Google 엔진.",
"admin.settings.sectionDirect": "직접 AI 공급자",
"admin.settings.sectionDirectDesc": "OpenAI 호환 직접 접근 — AI 등급의 채널(「모델 및 구독」 페이지)이나 이메일 생성에 사용할 수 있습니다.",
"admin.settings.sectionGateway": "멀티 모델 게이트웨이",
"admin.settings.sectionGatewayDesc": "키 하나로 수백 가지 모델. 등급별 모델은 「모델 및 구독」 페이지에서 관리합니다.",
"admin.settings.sectionOcr": "OCR (스캔 PDF)",
"admin.settings.sectionOcrDesc": "번역 전에 이미지 페이지에서 텍스트를 추출합니다.",
"admin.settings.sectionEmails": "이메일",
"admin.settings.sectionEmailsDesc": "알림과 후속 이메일을 보내는 서버.",
"admin.settings.enabled": "사용 함",
"admin.settings.disabled": "사용 안 함",
"admin.settings.envBadge": ".env의 키",
"admin.settings.envConfigured": ".env에 설정됨",
"admin.settings.envKeyPlaceholder": ".env에 설정된 키(사용하려면 비워 두세요)",
"admin.settings.apiKey": "API 키",
"admin.settings.model": "모델",
"admin.settings.baseUrl": "기본 URL",
"admin.settings.test": "테스트",
"admin.settings.testing": "테스트 중...",
"admin.settings.ok": "확인",
"admin.settings.error": "오류",
"admin.settings.failure": "실패",
"admin.settings.sending": "보내는 중...",
"admin.settings.save": "설정 저장",
"admin.settings.saving": "저장 중...",
"admin.settings.savedTitle": "✅ 설정이 저장되었습니다",
"admin.settings.savedDesc": "설정이 성공적으로 저장되었습니다.",
"admin.settings.saveErrorTitle": "저장 오류",
"admin.settings.loadErrorTitle": "불러오기 오류",
"admin.settings.loadErrorDesc": "HTTP {status} — 관리자 토큰을 확인하세요.",
"admin.settings.networkErrorTitle": "네트워크 오류",
"admin.settings.networkErrorDesc": "백엔드에 연결할 수 없습니다.",
"admin.settings.networkSaveErrorDesc": "저장을 위해 백엔드에 연결할 수 없습니다.",
"admin.settings.connectionOk": "연결 정상",
"admin.settings.modelsFound": "{provider} 모델 {count}개를 찾았습니다",
"admin.settings.providerError": "{provider} 오류",
"admin.settings.google.title": "Google Translate",
"admin.settings.google.desc": "deep_translator를 통한 비공식 웹 접근. 키가 필요 없으며 적당한 사용을 권장합니다.",
"admin.settings.googleCloud.title": "Google Cloud Translation (공식)",
"admin.settings.googleCloud.desc": "Google Cloud 공식 API. 매월 50만 자 무료, 이후 백만 자당 약 20달러. 키는 console.cloud.google.com에서 발급.",
"admin.settings.googleCloud.hint": "Google Cloud Console에서 Cloud Translation API를 활성화하고 해당 API로 제한된 API 키를 만드세요.",
"admin.settings.deepseek.title": "DeepSeek (직접)",
"admin.settings.deepseek.desc": "OpenAI 호환 DeepSeek 직접 API. 키는 platform.deepseek.com에서 — AI 등급 채널로 사용 가능.",
"admin.settings.zhipu.title": "z.ai / Zhipu (GLM)",
"admin.settings.zhipu.desc": "z.ai의 GLM 모델 직접 접근, OpenAI 호환. 키는 z.ai에서 — AI 등급 채널로 사용 가능.",
"admin.settings.zhipu.urlHint": "기본값: https://api.z.ai/api/paas/v4 — 프록시를 사용할 때만 변경하세요.",
"admin.settings.minimax.title": "MiniMax (직접)",
"admin.settings.minimax.desc": "OpenAI 호환 MiniMax 공개 API. 키는 minimax.io에서 — AI 등급 채널로 사용 가능.",
"admin.settings.openai.title": "OpenAI",
"admin.settings.openai.desc": "GPT 번역. platform.openai.com에서 키를 받으세요.",
"admin.settings.zai.title": "xAI (Grok)",
"admin.settings.zai.desc": "xAI의 Grok 모델. OpenAI 호환 API. x.ai에서 키를 받으세요.",
"admin.settings.zai.urlHint": "기본값: https://api.x.ai/v1 — 프록시를 사용할 때만 변경하세요.",
"admin.settings.openrouter.title": "OpenRouter — 게이트웨이 키",
"admin.settings.openrouter.desc": "Essential 등급 (Pro 플랜). 현재 라인업: deepseek/deepseek-v4-flash, z-ai/glm-5.3-flash, minimax/minimax-m3.",
"admin.settings.openrouter.keyLabel": "OpenRouter API 키 (Premium과 공유)",
"admin.settings.openrouter.envSharedPlaceholder": ".env에 설정된 키(Premium과 공유)",
"admin.settings.openrouter.reminder": "모델은 「모델 및 구독」 페이지에서 관리합니다.",
"admin.settings.openrouterPremium.title": "OpenRouter Premium — 게이트웨이 키",
"admin.settings.openrouterPremium.desc": "Premium 등급 (Business 플랜). 현재 라인업: anthropic/claude-sonnet-5, deepseek/deepseek-v4-pro, z-ai/glm-5.3.",
"admin.settings.openrouterPremium.reminder": "모델은 「모델 및 구독」 페이지에서 관리합니다. 같은 OpenRouter 키를 공유합니다.",
"admin.settings.mistral.title": "Mistral OCR (스캔 PDF)",
"admin.settings.mistral.desc": "번역 전에 스캔된 PDF(이미지 페이지)에서 텍스트를 추출합니다. 키가 없으면 이런 파일은 명확한 안내와 함께 거부됩니다. 약 1유로/1,000페이지. 키: console.mistral.ai.",
"admin.settings.mistral.keyLabel": "Mistral API 키",
"admin.settings.mistral.modelLabel": "OCR 모델",
"admin.settings.mistral.modelHint": "권장: mistral-ocr-latest",
"admin.settings.smtp.title": "SMTP 이메일",
"admin.settings.smtp.desc": "이메일 발송을 위한 SMTP 서버 설정(비밀번호 찾기, 알림, 후속 이메일).",
"admin.settings.smtp.host": "SMTP 호스트",
"admin.settings.smtp.port": "포트",
"admin.settings.smtp.username": "사용자 이름",
"admin.settings.smtp.password": "비밀번호",
"admin.settings.smtp.fromEmail": "발신 주소",
"admin.settings.smtp.useTls": "TLS 사용",
"admin.settings.smtp.sendTest": "테스트 이메일 보내기",
"admin.settings.smtp.sent": "보냄",
"admin.settings.smtp.sendFailed": "보내기 실패",
"admin.models.channelOpenrouter": "OpenRouter",
"admin.models.channelOpenrouterPremium": "OpenRouter (Premium)",
"admin.models.channelDeepseek": "DeepSeek (직접)",
"admin.models.channelZhipu": "z.ai (GLM)",
"admin.models.channelMinimax": "MiniMax (직접)",
"admin.models.channelOpenai": "OpenAI",
"admin.models.channelZai": "xAI (Grok)",
"admin.models.customModelPlaceholder": "모델 ID",
"admin.models.routeHint": "채널은 직접 접근과 게이트웨이 중 하나를 고릅니다. 모델 ID는 채널마다 다릅니다.",
"admin.models.addRouteConfirm": "추가",
"admin.marketing.generateTitle": "AI로 생성",
"admin.marketing.generateDesc": "문안은 마케팅 계획(포지셔닝과 허용된 수치)에 따라 작성됩니다. 아무것도 자동 발송되지 않습니다. 확인 후 보내세요.",
"admin.marketing.channel": "채널",
"admin.marketing.model": "모델",
"admin.marketing.modelPlaceholder": "채널 기본값",
"admin.marketing.promoOptional": "할인 (선택)",
"admin.marketing.promoNone": "없음",
"admin.marketing.brief": "브리프",
"admin.marketing.briefPlaceholder": "예: 대기자 명단 등록자에게 Pro 플랜의 장점을 상기시키기.",
"admin.marketing.language": "이메일 언어",
"admin.marketing.tone": "어조",
"admin.marketing.toneNone": "중립",
"admin.marketing.generateBtn": "생성",
"admin.marketing.generating": "생성 중...",
"admin.marketing.generatedTitle": "이메일이 생성되었습니다",
"admin.marketing.generatedDesc": "제목과 HTML이 폼에 삽입되었습니다 — 보내기 전에 확인하세요({provider}).",
"admin.marketing.generateFailedTitle": "생성 실패",
"admin.marketing.briefRequired": "먼저 브리프에 원하는 내용을 적어주세요.",
"admin.marketing.langFr": "프랑스어",
"admin.marketing.langEn": "영어",
"admin.marketing.langDe": "독일어",
"admin.marketing.langEs": "스페인어",
"admin.marketing.langIt": "이탈리아어",
"admin.marketing.langPt": "포르투갈어",
"admin.marketing.langNl": "네덜란드어",
"admin.marketing.langAr": "아랍어",
"admin.marketing.langFa": "페르시아어",
"admin.marketing.langJa": "일본어",
"admin.marketing.langKo": "한국어",
"admin.marketing.langRu": "러시아어",
"admin.marketing.langZh": "중국어",
"admin.marketing.toneFriendly": "친근함",
"admin.marketing.toneProfessional": "전문적",
"admin.marketing.toneUrgent": "긴급",
"admin.marketing.toneFestive": "축제",
"admin.marketing.promoTitle": "할인",
"admin.marketing.promoDesc": "프로모 코드는 Stripe 결제에 적용됩니다. 카운터는 구독이 실제로 완료되었을 때만 올라갑니다.",
"admin.marketing.promoEmpty": "아직 프로모 코드가 없습니다.",
"admin.marketing.promoActive": "활성",
"admin.marketing.promoInactive": "비활성",
"admin.marketing.promoLocalOnly": "Stripe 외부",
"admin.marketing.promoDeactivate": "비활성화",
"admin.marketing.promoUsage": "{used}/{max}회 사용",
"admin.marketing.promoPlansLabel": "플랜:",
"admin.marketing.promoAllPlans": "전체",
"admin.marketing.promoExpiresOn": "{date}에 만료",
"admin.marketing.promoNeverExpires": "만료 없음",
"admin.marketing.promoCreateTitle": "할인 만들기",
"admin.marketing.promoCreateDesc": "Stripe가 설정되어 있으면 코드는 Stripe에도 생성됩니다(쿠폰 + 프로모 코드).",
"admin.marketing.promoCode": "코드",
"admin.marketing.promoType": "유형",
"admin.marketing.promoTypePercent": "비율",
"admin.marketing.promoTypeAmount": "금액 (EUR)",
"admin.marketing.promoValue": "값",
"admin.marketing.promoPlans": "대상 플랜",
"admin.marketing.promoMaxRedemptions": "사용 한도",
"admin.marketing.promoMaxPlaceholder": "무제한",
"admin.marketing.promoExpires": "만료",
"admin.marketing.promoCreate": "코드 만들기",
"admin.marketing.promoCreating": "만드는 중...",
"admin.marketing.promoCreatedTitle": "프로모 코드 생성됨",
"admin.marketing.promoCreatedDesc": "코드 {code}이(가) 준비되었습니다.",
"admin.marketing.promoCreateFailedTitle": "생성이 거부되었습니다",
"admin.marketing.promoInvalidInput": "코드와 값을 확인하세요.",
"admin.marketing.promoDeactivatedTitle": "코드가 비활성화되었습니다",
"admin.marketing.promoDeactivatedDesc": "코드 {code}은(는) 더 이상 적용되지 않습니다.",
"admin.settings.fallback.title": "폴백 체인",
"admin.settings.fallback.desc": "공급자 자동 선택의 우선순위.",
"admin.settings.fallback.classic": "클래식 모드 (Google)",
"admin.settings.fallback.llm": "LLM 모드 (게이트웨이 및 직접 접근)",
"admin.settings.fallback.note": "등록된 채널(google, openai, deepseek, minimax)만 이 체인에 참여합니다. AI 등급과 다른 채널은 참여하지 않습니다.",
"admin.models.duplicateRouteTitle": "중복 거부됨",
"admin.models.duplicateRouteDesc": "그 채널 + 모델 조합은 이미 목록에 있습니다.",
"admin.marketing.promoDeactivateFailedTitle": "비활성화 거부됨",
"admin.marketing.promoDelete": "삭제",
"admin.marketing.promoDeleteConfirm": "코드 {code}을(를) 완전히 삭제할까요?",
"admin.marketing.promoDeletedTitle": "코드 삭제됨",
"admin.marketing.promoDeletedDesc": "코드 {code}이(가) 삭제되었습니다.",
"admin.marketing.promoDeleteFailedTitle": "삭제 거부됨",
"admin.marketing.promoLinkStripe": "Stripe에 연결",
"admin.marketing.promoLinkedTitle": "코드가 Stripe에 연결됨",
"admin.marketing.promoLinkedDesc": "코드 {code}은(는) 이제 결제에 적용할 수 있습니다.",
"admin.marketing.promoLinkFailedTitle": "연결 실패",
"admin.marketing.promoExpiryHint": "«10일까지» 코드는 10일 하루 종일 유효합니다(23:59:59 UTC 종료).",
"admin.marketing.generateEmpty": "서버 응답이 비어 있거나 사용할 수 없습니다."
}

View File

@@ -167,5 +167,17 @@
"pricing.header.titleBase": "모든 필요를 위한",
"pricing.header.titleAccent": "하나의 플랜",
"pricing.toast.close": "닫기",
"pricing.aiModels.premium.price": "백만 토큰당 $2 / $10"
"pricing.aiModels.premium.price": "백만 토큰당 $2 / $10",
"pricing.promo.label": "프로모 코드",
"pricing.promo.placeholder": "내코드",
"pricing.promo.apply": "적용",
"pricing.promo.checking": "확인 중...",
"pricing.promo.valid": "유효한 코드",
"pricing.promo.subscriptionOnly": "구독에만 유효하며 크레딧에는 사용할 수 없습니다.",
"pricing.promoError.unknown": "이 프로모 코드는 존재하지 않습니다.",
"pricing.promoError.inactive": "이 프로모 코드는 더 이상 활성 상태가 아닙니다.",
"pricing.promoError.expired": "이 프로모 코드는 만료되었습니다.",
"pricing.promoError.exhausted": "이 프로모 코드는 사용 한도에 도달했습니다.",
"pricing.promoError.plan": "이 프로모 코드는 이 플랜에 적용되지 않습니다.",
"pricing.promoError.local_only": "이 프로모 코드는 온라인 결제에 연결되어 있지 않습니다."
}

View File

@@ -145,5 +145,180 @@
"admin.stats.tiersSub": "Basis / Premium · klassiek {classic} · overig {other}",
"admin.stats.unavailable": "Bedrijfsstatistieken niet beschikbaar ({error}).",
"admin.stats.waitlist": "Wachtlijst",
"admin.stats.waitlistSub": "inschrijvingen op de wachtlijst"
"admin.stats.waitlistSub": "inschrijvingen op de wachtlijst",
"admin.settings.title": "Aanbieders",
"admin.settings.subtitle": "Configureer de aanbieders Toegangen. API-sleutels kunnen ook in het .env-bestand worden gezet.",
"admin.settings.sectionClassic": "Klassieke vertaling",
"admin.settings.sectionClassicDesc": "Google-motoren voor de standaardvertaling.",
"admin.settings.sectionDirect": "Directe AI-aanbieders",
"admin.settings.sectionDirectDesc": "Directe OpenAI-compatibele toegangen — bruikbaar als kanaal van een AI-niveau (pagina «Modellen & abonnementen») of voor het genereren van e-mails.",
"admin.settings.sectionGateway": "Multi-model gateways",
"admin.settings.sectionGatewayDesc": "Eén sleutel, honderden modellen. De modellen per niveau worden beheerd op de pagina «Modellen & abonnementen».",
"admin.settings.sectionOcr": "OCR (gescande PDF's)",
"admin.settings.sectionOcrDesc": "Tekst uit afbeeldingspagina's halen vóór de vertaling.",
"admin.settings.sectionEmails": "E-mails",
"admin.settings.sectionEmailsDesc": "Verzendserver voor meldingen en opvolgmails.",
"admin.settings.enabled": "Ingeschakeld",
"admin.settings.disabled": "Uitgeschakeld",
"admin.settings.envBadge": "Sleutel in .env",
"admin.settings.envConfigured": "Ingesteld in .env",
"admin.settings.envKeyPlaceholder": "Sleutel ingesteld in .env (leeg laten om te gebruiken)",
"admin.settings.apiKey": "API-sleutel",
"admin.settings.model": "Model",
"admin.settings.baseUrl": "Basis-URL",
"admin.settings.test": "Testen",
"admin.settings.testing": "Testen...",
"admin.settings.ok": "OK",
"admin.settings.error": "Fout",
"admin.settings.failure": "Mislukt",
"admin.settings.sending": "Verzenden...",
"admin.settings.save": "Configuratie opslaan",
"admin.settings.saving": "Opslaan...",
"admin.settings.savedTitle": "✅ Configuratie opgeslagen",
"admin.settings.savedDesc": "De instellingen zijn succesvol opgeslagen.",
"admin.settings.saveErrorTitle": "Fout bij opslaan",
"admin.settings.loadErrorTitle": "Laadfout",
"admin.settings.loadErrorDesc": "HTTP {status} — controleer uw admin-token.",
"admin.settings.networkErrorTitle": "Netwerkfout",
"admin.settings.networkErrorDesc": "Kan de backend niet bereiken.",
"admin.settings.networkSaveErrorDesc": "Kan de backend niet bereiken om op te slaan.",
"admin.settings.connectionOk": "Verbinding OK",
"admin.settings.modelsFound": "{count} {provider}-modellen gevonden",
"admin.settings.providerError": "Fout {provider}",
"admin.settings.google.title": "Google Translate",
"admin.settings.google.desc": "Officiële webtoegang via deep_translator. Geen sleutel nodig, gematigd gebruik aanbevolen.",
"admin.settings.googleCloud.title": "Google Cloud Translation (officieel)",
"admin.settings.googleCloud.desc": "Officiële Google Cloud API. 500.000 tekens/maand gratis, daarna ca. $20 per miljoen tekens. API-sleutel op console.cloud.google.com.",
"admin.settings.googleCloud.hint": "Schakel de Cloud Translation API in Google Cloud Console in en maak een API-sleutel die tot die API beperkt is.",
"admin.settings.deepseek.title": "DeepSeek (direct)",
"admin.settings.deepseek.desc": "Directe DeepSeek-API, OpenAI-compatibel. Sleutel op platform.deepseek.com — bruikbaar als kanaal van een AI-niveau.",
"admin.settings.zhipu.title": "z.ai / Zhipu (GLM)",
"admin.settings.zhipu.desc": "Directe toegang tot de GLM-modellen van z.ai, OpenAI-compatibel. Sleutel op z.ai — bruikbaar als kanaal van een AI-niveau.",
"admin.settings.zhipu.urlHint": "Standaard: https://api.z.ai/api/paas/v4 — wijzig dit alleen als u een proxy gebruikt.",
"admin.settings.minimax.title": "MiniMax (direct)",
"admin.settings.minimax.desc": "Publieke MiniMax-API, OpenAI-compatibel. Sleutel op minimax.io — bruikbaar als kanaal van een AI-niveau.",
"admin.settings.openai.title": "OpenAI",
"admin.settings.openai.desc": "GPT-vertalingen. Vraag een sleutel aan op platform.openai.com.",
"admin.settings.zai.title": "xAI (Grok)",
"admin.settings.zai.desc": "Grok-modellen van xAI. OpenAI-compatibele API. Vraag uw sleutel aan op x.ai.",
"admin.settings.zai.urlHint": "Standaard: https://api.x.ai/v1 — wijzig dit alleen als u een proxy gebruikt.",
"admin.settings.openrouter.title": "OpenRouter — gateway-sleutel",
"admin.settings.openrouter.desc": "Essential-niveau (Pro-abonnement). Huidige reeks: deepseek/deepseek-v4-flash, z-ai/glm-5.3-flash, minimax/minimax-m3.",
"admin.settings.openrouter.keyLabel": "OpenRouter API-sleutel (gedeeld met Premium)",
"admin.settings.openrouter.envSharedPlaceholder": "Sleutel ingesteld in .env (gedeeld met Premium)",
"admin.settings.openrouter.reminder": "Modellen worden beheerd op de pagina «Modellen & abonnementen».",
"admin.settings.openrouterPremium.title": "OpenRouter Premium — gateway-sleutel",
"admin.settings.openrouterPremium.desc": "Premium-niveau (Business-abonnement). Huidige reeks: anthropic/claude-sonnet-5, deepseek/deepseek-v4-pro, z-ai/glm-5.3.",
"admin.settings.openrouterPremium.reminder": "Modellen worden beheerd op de pagina «Modellen & abonnementen». Deelt dezelfde OpenRouter-sleutel.",
"admin.settings.mistral.title": "Mistral OCR (gescande PDF's)",
"admin.settings.mistral.desc": "Haalt tekst uit gescande PDF's (afbeeldingspagina's) vóór de vertaling. Zonder sleutel worden deze bestanden geweigerd met een duidelijke melding. Ca. €1 / 1.000 pagina's. Sleutel: console.mistral.ai.",
"admin.settings.mistral.keyLabel": "Mistral API-sleutel",
"admin.settings.mistral.modelLabel": "OCR-model",
"admin.settings.mistral.modelHint": "Aanbevolen: mistral-ocr-latest",
"admin.settings.smtp.title": "SMTP-e-mail",
"admin.settings.smtp.desc": "SMTP-serverinstellingen voor het versturen van e-mails (wachtwoord vergeten, meldingen, opvolgmails).",
"admin.settings.smtp.host": "SMTP-host",
"admin.settings.smtp.port": "Poort",
"admin.settings.smtp.username": "Gebruikersnaam",
"admin.settings.smtp.password": "Wachtwoord",
"admin.settings.smtp.fromEmail": "Afzenderadres",
"admin.settings.smtp.useTls": "TLS gebruiken",
"admin.settings.smtp.sendTest": "Stuur een testmail",
"admin.settings.smtp.sent": "Verzonden",
"admin.settings.smtp.sendFailed": "Verzenden mislukt",
"admin.models.channelOpenrouter": "OpenRouter",
"admin.models.channelOpenrouterPremium": "OpenRouter (Premium)",
"admin.models.channelDeepseek": "DeepSeek (direct)",
"admin.models.channelZhipu": "z.ai (GLM)",
"admin.models.channelMinimax": "MiniMax (direct)",
"admin.models.channelOpenai": "OpenAI",
"admin.models.channelZai": "xAI (Grok)",
"admin.models.customModelPlaceholder": "model-id",
"admin.models.routeHint": "Het kanaal kiest directe toegang of gateway; model-id's verschillen per kanaal.",
"admin.models.addRouteConfirm": "Toevoegen",
"admin.marketing.generateTitle": "Genereren met AI",
"admin.marketing.generateDesc": "De tekst wordt geschreven op basis van het marketingplan (positionering en toegestane cijfers). Niets wordt automatisch verstuurd: eerst lezen, dan versturen.",
"admin.marketing.channel": "Kanaal",
"admin.marketing.model": "Model",
"admin.marketing.modelPlaceholder": "Standaard van het kanaal",
"admin.marketing.promoOptional": "Korting (optioneel)",
"admin.marketing.promoNone": "Geen",
"admin.marketing.brief": "Briefing",
"admin.marketing.briefPlaceholder": "Bijv.: wachtlijstaanmeldingen herinneren aan de voordelen van het Pro-abonnement.",
"admin.marketing.language": "Taal van de e-mail",
"admin.marketing.tone": "Toon",
"admin.marketing.toneNone": "Neutraal",
"admin.marketing.generateBtn": "Genereren",
"admin.marketing.generating": "Genereren...",
"admin.marketing.generatedTitle": "E-mail gegenereerd",
"admin.marketing.generatedDesc": "Onderwerp en HTML in het formulier geplaatst — eerst controleren voordat u verstuurt ({provider}).",
"admin.marketing.generateFailedTitle": "Genereren mislukt",
"admin.marketing.briefRequired": "Beschrijf eerst de gewenste inhoud in de briefing.",
"admin.marketing.langFr": "Frans",
"admin.marketing.langEn": "Engels",
"admin.marketing.langDe": "Duits",
"admin.marketing.langEs": "Spaans",
"admin.marketing.langIt": "Italiaans",
"admin.marketing.langPt": "Portugees",
"admin.marketing.langNl": "Nederlands",
"admin.marketing.langAr": "Arabisch",
"admin.marketing.langFa": "Perzisch",
"admin.marketing.langJa": "Japans",
"admin.marketing.langKo": "Koreaans",
"admin.marketing.langRu": "Russisch",
"admin.marketing.langZh": "Chinees",
"admin.marketing.toneFriendly": "Vriendelijk",
"admin.marketing.toneProfessional": "Professioneel",
"admin.marketing.toneUrgent": "Dringend",
"admin.marketing.toneFestive": "Festief",
"admin.marketing.promoTitle": "Kortingen",
"admin.marketing.promoDesc": "Promocodes worden via Stripe op de betaling toegepast; de teller stijgt pas als het abonnement echt is afgerond.",
"admin.marketing.promoEmpty": "Nog geen promocodes.",
"admin.marketing.promoActive": "Actief",
"admin.marketing.promoInactive": "Uitgeschakeld",
"admin.marketing.promoLocalOnly": "Buiten Stripe",
"admin.marketing.promoDeactivate": "Uitschakelen",
"admin.marketing.promoUsage": "{used}/{max} keer gebruikt",
"admin.marketing.promoPlansLabel": "Abonnementen:",
"admin.marketing.promoAllPlans": "Alle",
"admin.marketing.promoExpiresOn": "vervalt op {date}",
"admin.marketing.promoNeverExpires": "geen vervaldatum",
"admin.marketing.promoCreateTitle": "Korting aanmaken",
"admin.marketing.promoCreateDesc": "De code wordt ook in Stripe aangemaakt (coupon + promotiecode) wanneer Stripe is ingesteld.",
"admin.marketing.promoCode": "Code",
"admin.marketing.promoType": "Type",
"admin.marketing.promoTypePercent": "Percentage",
"admin.marketing.promoTypeAmount": "Bedrag (EUR)",
"admin.marketing.promoValue": "Waarde",
"admin.marketing.promoPlans": "Geschikte abonnementen",
"admin.marketing.promoMaxRedemptions": "Inwissellimiet",
"admin.marketing.promoMaxPlaceholder": "Onbeperkt",
"admin.marketing.promoExpires": "Vervaldatum",
"admin.marketing.promoCreate": "Code aanmaken",
"admin.marketing.promoCreating": "Aanmaken...",
"admin.marketing.promoCreatedTitle": "Promocode aangemaakt",
"admin.marketing.promoCreatedDesc": "De code {code} is klaar.",
"admin.marketing.promoCreateFailedTitle": "Aanmaak geweigerd",
"admin.marketing.promoInvalidInput": "Controleer de code en de waarde.",
"admin.marketing.promoDeactivatedTitle": "Code uitgeschakeld",
"admin.marketing.promoDeactivatedDesc": "De code {code} geldt niet meer.",
"admin.settings.fallback.title": "Fallback-keten",
"admin.settings.fallback.desc": "Prioriteitsvolgorde voor de automatische selectie van aanbieders.",
"admin.settings.fallback.classic": "Klassieke modus (Google)",
"admin.settings.fallback.llm": "LLM-modus (gateways en directe toegangen)",
"admin.settings.fallback.note": "Alleen geregistreerde kanalen (google, openai, deepseek, minimax) doen mee aan deze keten; AI-niveaus en andere kanalen niet.",
"admin.models.duplicateRouteTitle": "Duplicaat geweigerd",
"admin.models.duplicateRouteDesc": "Dit kanaal-+-model paar staat al in de lijst.",
"admin.marketing.promoDeactivateFailedTitle": "Uitschakelen geweigerd",
"admin.marketing.promoDelete": "Verwijderen",
"admin.marketing.promoDeleteConfirm": "Code {code} definitief verwijderen?",
"admin.marketing.promoDeletedTitle": "Code verwijderd",
"admin.marketing.promoDeletedDesc": "De code {code} is verwijderd.",
"admin.marketing.promoDeleteFailedTitle": "Verwijderen geweigerd",
"admin.marketing.promoLinkStripe": "Koppelen aan Stripe",
"admin.marketing.promoLinkedTitle": "Code gekoppeld aan Stripe",
"admin.marketing.promoLinkedDesc": "De code {code} is nu toepasbaar bij de betaling.",
"admin.marketing.promoLinkFailedTitle": "Koppelen mislukt",
"admin.marketing.promoExpiryHint": "Een code «tot en met de 10e» blijft de hele 10e geldig (einde: 23:59:59 UTC).",
"admin.marketing.generateEmpty": "Het antwoord van de server was leeg of onbruikbaar."
}

View File

@@ -167,5 +167,17 @@
"pricing.header.titleBase": "Een plan voor",
"pricing.header.titleAccent": "elke behoefte",
"pricing.toast.close": "Sluiten",
"pricing.aiModels.premium.price": "$2 / $10 per 1M tokens"
"pricing.aiModels.premium.price": "$2 / $10 per 1M tokens",
"pricing.promo.label": "Promocode",
"pricing.promo.placeholder": "JOUWCODE",
"pricing.promo.apply": "Toepassen",
"pricing.promo.checking": "Controleren...",
"pricing.promo.valid": "Code geldig",
"pricing.promo.subscriptionOnly": "Geldig op abonnementen, niet op credits.",
"pricing.promoError.unknown": "Deze promocode bestaat niet.",
"pricing.promoError.inactive": "Deze promocode is niet meer actief.",
"pricing.promoError.expired": "Deze promocode is verlopen.",
"pricing.promoError.exhausted": "Deze promocode heeft zijn inwissellimiet bereikt.",
"pricing.promoError.plan": "Deze promocode geldt niet voor dit abonnement.",
"pricing.promoError.local_only": "Deze promocode is niet geschakeld voor online betaling."
}

View File

@@ -145,5 +145,180 @@
"admin.stats.tiersSub": "Essencial / Premium · clássico {classic} · outro {other}",
"admin.stats.unavailable": "Estatísticas de negócio indisponíveis ({error}).",
"admin.stats.waitlist": "Lista de espera",
"admin.stats.waitlistSub": "inscritos na espera"
"admin.stats.waitlistSub": "inscritos na espera",
"admin.settings.title": "Fornecedores",
"admin.settings.subtitle": "Configure os acessos dos fornecedores. As chaves API também podem ser definidas no ficheiro .env.",
"admin.settings.sectionClassic": "Tradução clássica",
"admin.settings.sectionClassicDesc": "Motores Google para a tradução padrão.",
"admin.settings.sectionDirect": "Fornecedores de IA diretos",
"admin.settings.sectionDirectDesc": "Acessos diretos compatíveis com OpenAI — utilizáveis como canal de um nível de IA (página «Modelos e assinaturas») ou para gerar emails.",
"admin.settings.sectionGateway": "Gateways multi-modelos",
"admin.settings.sectionGatewayDesc": "Uma chave, centenas de modelos. Os modelos por nível gerem-se na página «Modelos e assinaturas».",
"admin.settings.sectionOcr": "OCR (PDF digitalizados)",
"admin.settings.sectionOcrDesc": "Extração do texto das páginas de imagem antes da tradução.",
"admin.settings.sectionEmails": "Emails",
"admin.settings.sectionEmailsDesc": "Servidor de envio para notificações e retomas de contato.",
"admin.settings.enabled": "Ativado",
"admin.settings.disabled": "Desativado",
"admin.settings.envBadge": "Chave no .env",
"admin.settings.envConfigured": "Configurado no .env",
"admin.settings.envKeyPlaceholder": "Chave configurada no .env (deixe vazio para a utilizar)",
"admin.settings.apiKey": "Chave API",
"admin.settings.model": "Modelo",
"admin.settings.baseUrl": "URL de base",
"admin.settings.test": "Testar",
"admin.settings.testing": "Teste...",
"admin.settings.ok": "OK",
"admin.settings.error": "Erro",
"admin.settings.failure": "Falha",
"admin.settings.sending": "A enviar...",
"admin.settings.save": "Guardar a configuração",
"admin.settings.saving": "A guardar...",
"admin.settings.savedTitle": "✅ Configuração guardada",
"admin.settings.savedDesc": "Os parâmetros foram guardados com sucesso.",
"admin.settings.saveErrorTitle": "Erro ao guardar",
"admin.settings.loadErrorTitle": "Erro de carregamento",
"admin.settings.loadErrorDesc": "HTTP {status} — verifique o seu token de administrador.",
"admin.settings.networkErrorTitle": "Erro de rede",
"admin.settings.networkErrorDesc": "Não é possível contactar o backend.",
"admin.settings.networkSaveErrorDesc": "Não é possível contactar o backend para guardar.",
"admin.settings.connectionOk": "Ligação OK",
"admin.settings.modelsFound": "{count} modelos {provider} encontrados",
"admin.settings.providerError": "Erro {provider}",
"admin.settings.google.title": "Google Translate",
"admin.settings.google.desc": "Acesso web não oficial via deep_translator. Não requer chave, uso razoável recomendado.",
"admin.settings.googleCloud.title": "Google Cloud Translation (oficial)",
"admin.settings.googleCloud.desc": "API oficial da Google Cloud. 500 000 carateres/mês grátis, depois cerca de 20 $ por milhão de carateres. Chave API em console.cloud.google.com.",
"admin.settings.googleCloud.hint": "Ative a Cloud Translation API na Google Cloud Console e crie uma chave API restrita a essa API.",
"admin.settings.deepseek.title": "DeepSeek (direto)",
"admin.settings.deepseek.desc": "API direta da DeepSeek, compatível com OpenAI. Chave em platform.deepseek.com — utilizável como canal de um nível de IA.",
"admin.settings.zhipu.title": "z.ai / Zhipu (GLM)",
"admin.settings.zhipu.desc": "Acesso direto aos modelos GLM da z.ai, compatível com OpenAI. Chave em z.ai — utilizável como canal de um nível de IA.",
"admin.settings.zhipu.urlHint": "Por omissão: https://api.z.ai/api/paas/v4 — altere apenas se usar um proxy.",
"admin.settings.minimax.title": "MiniMax (direto)",
"admin.settings.minimax.desc": "API pública da MiniMax, compatível com OpenAI. Chave em minimax.io — utilizável como canal de um nível de IA.",
"admin.settings.openai.title": "OpenAI",
"admin.settings.openai.desc": "Traduções GPT. Obtenha uma chave em platform.openai.com.",
"admin.settings.zai.title": "xAI (Grok)",
"admin.settings.zai.desc": "Modelos Grok da xAI. API compatível com OpenAI. Obtenha a chave em x.ai.",
"admin.settings.zai.urlHint": "Por omissão: https://api.x.ai/v1 — altere apenas se usar um proxy.",
"admin.settings.openrouter.title": "OpenRouter — chave de gateway",
"admin.settings.openrouter.desc": "Nível Essencial (plano Pro). Gama atual: deepseek/deepseek-v4-flash, z-ai/glm-5.3-flash, minimax/minimax-m3.",
"admin.settings.openrouter.keyLabel": "Chave API OpenRouter (partilhada com Premium)",
"admin.settings.openrouter.envSharedPlaceholder": "Chave configurada no .env (partilhada com Premium)",
"admin.settings.openrouter.reminder": "Modelos geridos na página «Modelos e assinaturas».",
"admin.settings.openrouterPremium.title": "OpenRouter Premium — chave de gateway",
"admin.settings.openrouterPremium.desc": "Nível Premium (plano Business). Gama atual: anthropic/claude-sonnet-5, deepseek/deepseek-v4-pro, z-ai/glm-5.3.",
"admin.settings.openrouterPremium.reminder": "Modelos geridos na página «Modelos e assinaturas». Partilha a mesma chave OpenRouter.",
"admin.settings.mistral.title": "OCR Mistral (PDF digitalizados)",
"admin.settings.mistral.desc": "Extrai o texto dos PDF digitalizados (páginas de imagem) antes da tradução. Sem chave, esses ficheiros são recusados com uma mensagem clara. Cerca de 1 € / 1 000 páginas. Chave: console.mistral.ai.",
"admin.settings.mistral.keyLabel": "Chave API Mistral",
"admin.settings.mistral.modelLabel": "Modelo OCR",
"admin.settings.mistral.modelHint": "Recomendado: mistral-ocr-latest",
"admin.settings.smtp.title": "Email SMTP",
"admin.settings.smtp.desc": "Configuração do servidor SMTP para o envio de emails (palavra-passe esquecida, notificações, retomas de contato).",
"admin.settings.smtp.host": "Anfitrião SMTP",
"admin.settings.smtp.port": "Porta",
"admin.settings.smtp.username": "Nome de utilizador",
"admin.settings.smtp.password": "Palavra-passe",
"admin.settings.smtp.fromEmail": "Endereço de envio",
"admin.settings.smtp.useTls": "Utilizar TLS",
"admin.settings.smtp.sendTest": "Enviar um email de teste",
"admin.settings.smtp.sent": "Enviado",
"admin.settings.smtp.sendFailed": "Falha no envio",
"admin.models.channelOpenrouter": "OpenRouter",
"admin.models.channelOpenrouterPremium": "OpenRouter (Premium)",
"admin.models.channelDeepseek": "DeepSeek (direto)",
"admin.models.channelZhipu": "z.ai (GLM)",
"admin.models.channelMinimax": "MiniMax (direto)",
"admin.models.channelOpenai": "OpenAI",
"admin.models.channelZai": "xAI (Grok)",
"admin.models.customModelPlaceholder": "id-do-modelo",
"admin.models.routeHint": "O canal escolhe o acesso direto ou o gateway; os identificadores de modelos diferem de canal para canal.",
"admin.models.addRouteConfirm": "Adicionar",
"admin.marketing.generateTitle": "Gerar com IA",
"admin.marketing.generateDesc": "O texto é redigido a partir do plano de marketing (posicionamento e números autorizados). Nada é enviado automaticamente: reveja e depois envie.",
"admin.marketing.channel": "Canal",
"admin.marketing.model": "Modelo",
"admin.marketing.modelPlaceholder": "Predefinição do canal",
"admin.marketing.promoOptional": "Desconto (facultativo)",
"admin.marketing.promoNone": "Nenhum",
"admin.marketing.brief": "Briefing",
"admin.marketing.briefPlaceholder": "Ex.: retomar os inscritos na lista de espera lembrando as vantagens do plano Pro.",
"admin.marketing.language": "Língua do email",
"admin.marketing.tone": "Tom",
"admin.marketing.toneNone": "Neutro",
"admin.marketing.generateBtn": "Gerar",
"admin.marketing.generating": "A gerar...",
"admin.marketing.generatedTitle": "Email gerado",
"admin.marketing.generatedDesc": "Assunto e HTML inseridos no formulário — reveja antes de enviar ({provider}).",
"admin.marketing.generateFailedTitle": "Geração impossível",
"admin.marketing.briefRequired": "Descreva primeiro o conteúdo pretendido no briefing.",
"admin.marketing.langFr": "Francês",
"admin.marketing.langEn": "Inglês",
"admin.marketing.langDe": "Alemão",
"admin.marketing.langEs": "Espanhol",
"admin.marketing.langIt": "Italiano",
"admin.marketing.langPt": "Português",
"admin.marketing.langNl": "Neerlandês",
"admin.marketing.langAr": "Árabe",
"admin.marketing.langFa": "Persa",
"admin.marketing.langJa": "Japonês",
"admin.marketing.langKo": "Coreano",
"admin.marketing.langRu": "Russo",
"admin.marketing.langZh": "Chinês",
"admin.marketing.toneFriendly": "Amigável",
"admin.marketing.toneProfessional": "Profissional",
"admin.marketing.toneUrgent": "Urgente",
"admin.marketing.toneFestive": "Festivo",
"admin.marketing.promoTitle": "Descontos",
"admin.marketing.promoDesc": "Códigos promo aplicados ao pagamento via Stripe; o contador aumenta quando a assinatura é realmente concluída.",
"admin.marketing.promoEmpty": "Ainda não há códigos promo.",
"admin.marketing.promoActive": "Ativo",
"admin.marketing.promoInactive": "Desativado",
"admin.marketing.promoLocalOnly": "Fora do Stripe",
"admin.marketing.promoDeactivate": "Desativar",
"admin.marketing.promoUsage": "{used}/{max} utilizações",
"admin.marketing.promoPlansLabel": "Planos:",
"admin.marketing.promoAllPlans": "Todos",
"admin.marketing.promoExpiresOn": "expira em {date}",
"admin.marketing.promoNeverExpires": "sem expiração",
"admin.marketing.promoCreateTitle": "Criar um desconto",
"admin.marketing.promoCreateDesc": "O código também é criado no Stripe (cupão + código promocional) quando o Stripe está configurado.",
"admin.marketing.promoCode": "Código",
"admin.marketing.promoType": "Tipo",
"admin.marketing.promoTypePercent": "Percentagem",
"admin.marketing.promoTypeAmount": "Montante (EUR)",
"admin.marketing.promoValue": "Valor",
"admin.marketing.promoPlans": "Planos elegíveis",
"admin.marketing.promoMaxRedemptions": "Limite de utilizações",
"admin.marketing.promoMaxPlaceholder": "Ilimitado",
"admin.marketing.promoExpires": "Expiração",
"admin.marketing.promoCreate": "Criar o código",
"admin.marketing.promoCreating": "A criar...",
"admin.marketing.promoCreatedTitle": "Código promo criado",
"admin.marketing.promoCreatedDesc": "O código {code} está pronto.",
"admin.marketing.promoCreateFailedTitle": "Criação recusada",
"admin.marketing.promoInvalidInput": "Verifique o código e o valor.",
"admin.marketing.promoDeactivatedTitle": "Código desativado",
"admin.marketing.promoDeactivatedDesc": "O código {code} já não é aplicável.",
"admin.settings.fallback.title": "Cadeia de recurso",
"admin.settings.fallback.desc": "Ordem de prioridade para a seleção automática de fornecedores.",
"admin.settings.fallback.classic": "Modo clássico (Google)",
"admin.settings.fallback.llm": "Modo LLM (gateways e acessos diretos)",
"admin.settings.fallback.note": "Apenas os canais registados (google, openai, deepseek, minimax) participam nesta cadeia; os níveis de IA e os outros canais não.",
"admin.models.duplicateRouteTitle": "Duplicado recusado",
"admin.models.duplicateRouteDesc": "Esse par canal + modelo já está na lista.",
"admin.marketing.promoDeactivateFailedTitle": "Desativação recusada",
"admin.marketing.promoDelete": "Eliminar",
"admin.marketing.promoDeleteConfirm": "Eliminar definitivamente o código {code}?",
"admin.marketing.promoDeletedTitle": "Código eliminado",
"admin.marketing.promoDeletedDesc": "O código {code} foi eliminado.",
"admin.marketing.promoDeleteFailedTitle": "Eliminação recusada",
"admin.marketing.promoLinkStripe": "Ligar ao Stripe",
"admin.marketing.promoLinkedTitle": "Código ligado ao Stripe",
"admin.marketing.promoLinkedDesc": "O código {code} já é aplicável ao pagamento.",
"admin.marketing.promoLinkFailedTitle": "Ligação impossível",
"admin.marketing.promoExpiryHint": "Um código «até dia 10» continua válido todo o dia 10 (fim: 23:59:59 UTC).",
"admin.marketing.generateEmpty": "A resposta do servidor estava vazia ou era inutilizável."
}

View File

@@ -167,5 +167,17 @@
"pricing.header.titleBase": "Um plano para",
"pricing.header.titleAccent": "cada necessidade",
"pricing.toast.close": "Fechar",
"pricing.aiModels.premium.price": "2 US$ / 10 US$ por 1M de tokens"
"pricing.aiModels.premium.price": "2 US$ / 10 US$ por 1M de tokens",
"pricing.promo.label": "Código promo",
"pricing.promo.placeholder": "SEUCODIGO",
"pricing.promo.apply": "Aplicar",
"pricing.promo.checking": "A verificar...",
"pricing.promo.valid": "Código válido",
"pricing.promo.subscriptionOnly": "Válido em assinaturas, não em créditos.",
"pricing.promoError.unknown": "Este código promo não existe.",
"pricing.promoError.inactive": "Este código promo já não está ativo.",
"pricing.promoError.expired": "Este código promo expirou.",
"pricing.promoError.exhausted": "Este código promo atingiu o limite de utilizações.",
"pricing.promoError.plan": "Este código promo não é válido para este plano.",
"pricing.promoError.local_only": "Este código promo não está habilitado para o pagamento online."
}

View File

@@ -145,5 +145,180 @@
"admin.stats.tiersSub": "Базовый / Премиум · классика {classic} · прочее {other}",
"admin.stats.unavailable": "Бизнес-статистика недоступна ({error}).",
"admin.stats.waitlist": "Лист ожидания",
"admin.stats.waitlistSub": "записей в листе ожидания"
"admin.stats.waitlistSub": "записей в листе ожидания",
"admin.settings.title": "Поставщики",
"admin.settings.subtitle": "Настройте доступы поставщиков. Ключи API также можно задать в файле .env.",
"admin.settings.sectionClassic": "Классический перевод",
"admin.settings.sectionClassicDesc": "Механизмы Google для стандартного перевода.",
"admin.settings.sectionDirect": "Прямые ИИ-поставщики",
"admin.settings.sectionDirectDesc": "Прямые совместимые с OpenAI доступы — используются как канал ИИ-уровня (страница «Модели и подписки») или для генерации писем.",
"admin.settings.sectionGateway": "Мульти модельные шлюзы",
"admin.settings.sectionGatewayDesc": "Один ключ, сотни моделей. Модели по уровням настраиваются на странице «Модели и подписки».",
"admin.settings.sectionOcr": "OCR (сканированные PDF)",
"admin.settings.sectionOcrDesc": "Извлечение текста со страниц-изображений перед переводом.",
"admin.settings.sectionEmails": "Письма",
"admin.settings.sectionEmailsDesc": "Сервер отправки для уведомлений и повторных писем.",
"admin.settings.enabled": "Включено",
"admin.settings.disabled": "Отключено",
"admin.settings.envBadge": "Ключ в .env",
"admin.settings.envConfigured": "Задано в .env",
"admin.settings.envKeyPlaceholder": "Ключ задан в .env (оставьте пустым, чтобы использовать)",
"admin.settings.apiKey": "Ключ API",
"admin.settings.model": "Модель",
"admin.settings.baseUrl": "Базовый URL",
"admin.settings.test": "Проверить",
"admin.settings.testing": "Проверка...",
"admin.settings.ok": "OK",
"admin.settings.error": "Ошибка",
"admin.settings.failure": "Сбой",
"admin.settings.sending": "Отправка...",
"admin.settings.save": "Сохранить конфигурацию",
"admin.settings.saving": "Сохранение...",
"admin.settings.savedTitle": "✅ Конфигурация сохранена",
"admin.settings.savedDesc": "Параметры успешно сохранены.",
"admin.settings.saveErrorTitle": "Ошибка сохранения",
"admin.settings.loadErrorTitle": "Ошибка загрузки",
"admin.settings.loadErrorDesc": "HTTP {status} — проверьте административный токен.",
"admin.settings.networkErrorTitle": "Сетевая ошибка",
"admin.settings.networkErrorDesc": "Не удалось связаться с сервером.",
"admin.settings.networkSaveErrorDesc": "Не удалось связаться с сервером для сохранения.",
"admin.settings.connectionOk": "Соединение в порядке",
"admin.settings.modelsFound": "Найдено моделей {provider}: {count}",
"admin.settings.providerError": "Ошибка {provider}",
"admin.settings.google.title": "Google Translate",
"admin.settings.google.desc": "Неофициальный веб-доступ через deep_translator. Ключ не нужен, разумное использование приветствуется.",
"admin.settings.googleCloud.title": "Google Cloud Translation (официальный)",
"admin.settings.googleCloud.desc": "Официальный API Google Cloud. 500 000 знаков/месяц бесплатно, далее около 20 $ за миллион знаков. Ключ API на console.cloud.google.com.",
"admin.settings.googleCloud.hint": "Включите Cloud Translation API в Google Cloud Console и создайте ключ API с ограничением этим API.",
"admin.settings.deepseek.title": "DeepSeek (напрямую)",
"admin.settings.deepseek.desc": "Прямой API DeepSeek, совместим с OpenAI. Ключ на platform.deepseek.com — подходит как канал ИИ-уровня.",
"admin.settings.zhipu.title": "z.ai / Zhipu (GLM)",
"admin.settings.zhipu.desc": "Прямой доступ к моделям GLM от z.ai, совместим с OpenAI. Ключ на z.ai — подходит как канал ИИ-уровня.",
"admin.settings.zhipu.urlHint": "По умолчанию: https://api.z.ai/api/paas/v4 — меняйте только при использовании прокси.",
"admin.settings.minimax.title": "MiniMax (напрямую)",
"admin.settings.minimax.desc": "Публичный API MiniMax, совместим с OpenAI. Ключ на minimax.io — подходит как канал ИИ-уровня.",
"admin.settings.openai.title": "OpenAI",
"admin.settings.openai.desc": "Переводы GPT. Получите ключ на platform.openai.com.",
"admin.settings.zai.title": "xAI (Grok)",
"admin.settings.zai.desc": "Модели Grok от xAI. API совместим с OpenAI. Получите ключ на x.ai.",
"admin.settings.zai.urlHint": "По умолчанию: https://api.x.ai/v1 — меняйте только при использовании прокси.",
"admin.settings.openrouter.title": "OpenRouter — ключ шлюза",
"admin.settings.openrouter.desc": "Уровень Essential (тариф Pro). Текущая линейка: deepseek/deepseek-v4-flash, z-ai/glm-5.3-flash, minimax/minimax-m3.",
"admin.settings.openrouter.keyLabel": "Ключ API OpenRouter (общий с Premium)",
"admin.settings.openrouter.envSharedPlaceholder": "Ключ задан в .env (общий с Premium)",
"admin.settings.openrouter.reminder": "Модели управляются на странице «Модели и подписки».",
"admin.settings.openrouterPremium.title": "OpenRouter Premium — ключ шлюза",
"admin.settings.openrouterPremium.desc": "Уровень Premium (тариф Business). Текущая линейка: anthropic/claude-sonnet-5, deepseek/deepseek-v4-pro, z-ai/glm-5.3.",
"admin.settings.openrouterPremium.reminder": "Модели управляются на странице «Модели и подписки». Используется тот же ключ OpenRouter.",
"admin.settings.mistral.title": "OCR Mistral (сканированные PDF)",
"admin.settings.mistral.desc": "Извлекает текст из сканированных PDF (страницы-изображения) перед переводом. Без ключа такие файлы отклоняются с понятным сообщением. Около 1 € / 1000 страниц. Ключ: console.mistral.ai.",
"admin.settings.mistral.keyLabel": "Ключ API Mistral",
"admin.settings.mistral.modelLabel": "OCR-модель",
"admin.settings.mistral.modelHint": "Рекомендуется: mistral-ocr-latest",
"admin.settings.smtp.title": "Почта SMTP",
"admin.settings.smtp.desc": "Настройки SMTP-сервера для отправки писем (восстановление пароля, уведомления, повторные письма).",
"admin.settings.smtp.host": "SMTP-сервер",
"admin.settings.smtp.port": "Порт",
"admin.settings.smtp.username": "Имя пользователя",
"admin.settings.smtp.password": "Пароль",
"admin.settings.smtp.fromEmail": "Адрес отправителя",
"admin.settings.smtp.useTls": "Использовать TLS",
"admin.settings.smtp.sendTest": "Отправить тестовое письмо",
"admin.settings.smtp.sent": "Отправлено",
"admin.settings.smtp.sendFailed": "Не удалось отправить",
"admin.models.channelOpenrouter": "OpenRouter",
"admin.models.channelOpenrouterPremium": "OpenRouter (Premium)",
"admin.models.channelDeepseek": "DeepSeek (напрямую)",
"admin.models.channelZhipu": "z.ai (GLM)",
"admin.models.channelMinimax": "MiniMax (напрямую)",
"admin.models.channelOpenai": "OpenAI",
"admin.models.channelZai": "xAI (Grok)",
"admin.models.customModelPlaceholder": "идентификатор-модели",
"admin.models.routeHint": "Канал выбирает прямой доступ или шлюз; идентификаторы моделей различаются между каналами.",
"admin.models.addRouteConfirm": "Добавить",
"admin.marketing.generateTitle": "Создать с помощью ИИ",
"admin.marketing.generateDesc": "Текст составляется по маркетинговому плану (позиционирование и разрешённые цифры). Ничего не отправляется автоматически: проверьте и отправьте.",
"admin.marketing.channel": "Канал",
"admin.marketing.model": "Модель",
"admin.marketing.modelPlaceholder": "По умолчанию канала",
"admin.marketing.promoOptional": "Скидка (необязательно)",
"admin.marketing.promoNone": "Без скидки",
"admin.marketing.brief": "Задание",
"admin.marketing.briefPlaceholder": "Напр.: напомнить записавшимся в лист ожидания о преимуществах тарифа Pro.",
"admin.marketing.language": "Язык письма",
"admin.marketing.tone": "Тон",
"admin.marketing.toneNone": "Нейтральный",
"admin.marketing.generateBtn": "Создать",
"admin.marketing.generating": "Создание...",
"admin.marketing.generatedTitle": "Письмо создано",
"admin.marketing.generatedDesc": "Тема и HTML вставлены в форму — проверьте перед отправкой ({provider}).",
"admin.marketing.generateFailedTitle": "Не удалось создать",
"admin.marketing.briefRequired": "Сначала опишите желаемое содержание в задании.",
"admin.marketing.langFr": "Французский",
"admin.marketing.langEn": "Английский",
"admin.marketing.langDe": "Немецкий",
"admin.marketing.langEs": "Испанский",
"admin.marketing.langIt": "Итальянский",
"admin.marketing.langPt": "Португальский",
"admin.marketing.langNl": "Нидерландский",
"admin.marketing.langAr": "Арабский",
"admin.marketing.langFa": "Персидский",
"admin.marketing.langJa": "Японский",
"admin.marketing.langKo": "Корейский",
"admin.marketing.langRu": "Русский",
"admin.marketing.langZh": "Китайский",
"admin.marketing.toneFriendly": "Дружелюбный",
"admin.marketing.toneProfessional": "Профессиональный",
"admin.marketing.toneUrgent": "Срочный",
"admin.marketing.toneFestive": "Праздничный",
"admin.marketing.promoTitle": "Скидки",
"admin.marketing.promoDesc": "Промокоды применяются к оплате через Stripe; счётчик растёт только после реального оформления подписки.",
"admin.marketing.promoEmpty": "Промокодов пока нет.",
"admin.marketing.promoActive": "Активен",
"admin.marketing.promoInactive": "Отключён",
"admin.marketing.promoLocalOnly": "Вне Stripe",
"admin.marketing.promoDeactivate": "Отключить",
"admin.marketing.promoUsage": "использований: {used}/{max}",
"admin.marketing.promoPlansLabel": "Тарифы:",
"admin.marketing.promoAllPlans": "Все",
"admin.marketing.promoExpiresOn": "истекает {date}",
"admin.marketing.promoNeverExpires": "без срока",
"admin.marketing.promoCreateTitle": "Создать скидку",
"admin.marketing.promoCreateDesc": "Код также создаётся в Stripe (купон + промокод), если Stripe настроен.",
"admin.marketing.promoCode": "Код",
"admin.marketing.promoType": "Тип",
"admin.marketing.promoTypePercent": "Процент",
"admin.marketing.promoTypeAmount": "Сумма (EUR)",
"admin.marketing.promoValue": "Значение",
"admin.marketing.promoPlans": "Подходящие тарифы",
"admin.marketing.promoMaxRedemptions": "Лимит применений",
"admin.marketing.promoMaxPlaceholder": "Без лимита",
"admin.marketing.promoExpires": "Срок действия",
"admin.marketing.promoCreate": "Создать код",
"admin.marketing.promoCreating": "Создание...",
"admin.marketing.promoCreatedTitle": "Промокод создан",
"admin.marketing.promoCreatedDesc": "Код {code} готов.",
"admin.marketing.promoCreateFailedTitle": "Создание отклонено",
"admin.marketing.promoInvalidInput": "Проверьте код и значение.",
"admin.marketing.promoDeactivatedTitle": "Код отключён",
"admin.marketing.promoDeactivatedDesc": "Код {code} больше не применяется.",
"admin.settings.fallback.title": "Цепочка переключения",
"admin.settings.fallback.desc": "Порядок приоритета для автоматического выбора поставщиков.",
"admin.settings.fallback.classic": "Классический режим (Google)",
"admin.settings.fallback.llm": "Режим LLM (шлюзы и прямые доступы)",
"admin.settings.fallback.note": "В этой цепочке участвуют только зарегистрированные каналы (google, openai, deepseek, minimax); ИИ-уровни и прочие каналы — нет.",
"admin.models.duplicateRouteTitle": "Дубль отклонён",
"admin.models.duplicateRouteDesc": "Такая пара канал + модель уже есть в списке.",
"admin.marketing.promoDeactivateFailedTitle": "Отключение отклонено",
"admin.marketing.promoDelete": "Удалить",
"admin.marketing.promoDeleteConfirm": "Окончательно удалить код {code}?",
"admin.marketing.promoDeletedTitle": "Код удалён",
"admin.marketing.promoDeletedDesc": "Код {code} удалён.",
"admin.marketing.promoDeleteFailedTitle": "Удаление отклонено",
"admin.marketing.promoLinkStripe": "Привязать к Stripe",
"admin.marketing.promoLinkedTitle": "Код привязан к Stripe",
"admin.marketing.promoLinkedDesc": "Код {code} теперь применим к оплате.",
"admin.marketing.promoLinkFailedTitle": "Не удалось привязать",
"admin.marketing.promoExpiryHint": "Код «до 10-го» действует весь 10-й день (до 23:59:59 UTC).",
"admin.marketing.generateEmpty": "Ответ сервера был пустым или непригодным."
}

View File

@@ -167,5 +167,17 @@
"pricing.header.titleBase": "Тариф для",
"pricing.header.titleAccent": "каждой задачи",
"pricing.toast.close": "Закрыть",
"pricing.aiModels.premium.price": "$2 / $10 за 1 млн токенов"
"pricing.aiModels.premium.price": "$2 / $10 за 1 млн токенов",
"pricing.promo.label": "Промокод",
"pricing.promo.placeholder": "ВАШКОД",
"pricing.promo.apply": "Применить",
"pricing.promo.checking": "Проверка...",
"pricing.promo.valid": "Код действителен",
"pricing.promo.subscriptionOnly": "Действует на подписки, но не на кредиты.",
"pricing.promoError.unknown": "Такого промокода не существует.",
"pricing.promoError.inactive": "Этот промокод больше не активен.",
"pricing.promoError.expired": "Срок действия этого промокода истёк.",
"pricing.promoError.exhausted": "Этот промокод достиг лимита применений.",
"pricing.promoError.plan": "Этот промокод не действует для этого тарифа.",
"pricing.promoError.local_only": "Этот промокод не привязан к онлайн-оплате."
}

View File

@@ -145,5 +145,180 @@
"admin.stats.tiersSub": "基础 / 高级 · 经典 {classic} · 其他 {other}",
"admin.stats.unavailable": "业务统计不可用({error})。",
"admin.stats.waitlist": "等候名单",
"admin.stats.waitlistSub": "位用户等候中"
"admin.stats.waitlistSub": "位用户等候中",
"admin.settings.title": "服务商",
"admin.settings.subtitle": "配置服务商的访问。API 密钥也可以在 .env 文件中设置。",
"admin.settings.sectionClassic": "经典翻译",
"admin.settings.sectionClassicDesc": "用于标准翻译的 Google 引擎。",
"admin.settings.sectionDirect": "直连 AI 服务商",
"admin.settings.sectionDirectDesc": "兼容 OpenAI 的直连接入 —— 可作为 AI 层级的通道(见「模型与订阅」页面)或用于生成邮件。",
"admin.settings.sectionGateway": "多模型网关",
"admin.settings.sectionGatewayDesc": "一个密钥,数百个模型。每个层级的模型在「模型与订阅」页面管理。",
"admin.settings.sectionOcr": "OCR扫描版 PDF",
"admin.settings.sectionOcrDesc": "翻译前从图片页面提取文字。",
"admin.settings.sectionEmails": "邮件",
"admin.settings.sectionEmailsDesc": "用于通知和跟进邮件的发送服务器。",
"admin.settings.enabled": "已启用",
"admin.settings.disabled": "已停用",
"admin.settings.envBadge": "密钥在 .env",
"admin.settings.envConfigured": "已在 .env 中配置",
"admin.settings.envKeyPlaceholder": "密钥已在 .env 中配置(留空即可使用)",
"admin.settings.apiKey": "API 密钥",
"admin.settings.model": "模型",
"admin.settings.baseUrl": "基础 URL",
"admin.settings.test": "测试",
"admin.settings.testing": "测试中...",
"admin.settings.ok": "正常",
"admin.settings.error": "错误",
"admin.settings.failure": "失败",
"admin.settings.sending": "发送中...",
"admin.settings.save": "保存配置",
"admin.settings.saving": "保存中...",
"admin.settings.savedTitle": "✅ 配置已保存",
"admin.settings.savedDesc": "设置已成功保存。",
"admin.settings.saveErrorTitle": "保存出错",
"admin.settings.loadErrorTitle": "加载出错",
"admin.settings.loadErrorDesc": "HTTP {status} — 请检查您的管理员令牌。",
"admin.settings.networkErrorTitle": "网络错误",
"admin.settings.networkErrorDesc": "无法连接后端。",
"admin.settings.networkSaveErrorDesc": "无法连接后端进行保存。",
"admin.settings.connectionOk": "连接正常",
"admin.settings.modelsFound": "找到 {count} 个 {provider} 模型",
"admin.settings.providerError": "{provider} 错误",
"admin.settings.google.title": "Google 翻译",
"admin.settings.google.desc": "通过 deep_translator 的非官方网页访问。无需密钥,建议适度使用。",
"admin.settings.googleCloud.title": "Google Cloud 翻译(官方)",
"admin.settings.googleCloud.desc": "Google Cloud 官方 API。每月 50 万字符免费,之后约每百万字符 20 美元。密钥来自 console.cloud.google.com。",
"admin.settings.googleCloud.hint": "在 Google Cloud Console 中启用 Cloud Translation API然后创建仅限该 API 的密钥。",
"admin.settings.deepseek.title": "DeepSeek直连",
"admin.settings.deepseek.desc": "DeepSeek 直连 API兼容 OpenAI。密钥来自 platform.deepseek.com —— 可作为 AI 层级通道。",
"admin.settings.zhipu.title": "z.ai / ZhipuGLM",
"admin.settings.zhipu.desc": "直连 z.ai 的 GLM 模型,兼容 OpenAI。密钥来自 z.ai —— 可作为 AI 层级通道。",
"admin.settings.zhipu.urlHint": "默认https://api.z.ai/api/paas/v4 — 仅在使用代理时修改。",
"admin.settings.minimax.title": "MiniMax直连",
"admin.settings.minimax.desc": "MiniMax 公开 API兼容 OpenAI。密钥来自 minimax.io —— 可作为 AI 层级通道。",
"admin.settings.openai.title": "OpenAI",
"admin.settings.openai.desc": "GPT 翻译。请在 platform.openai.com 获取密钥。",
"admin.settings.zai.title": "xAIGrok",
"admin.settings.zai.desc": "xAI 的 Grok 模型。兼容 OpenAI 的 API。请在 x.ai 获取密钥。",
"admin.settings.zai.urlHint": "默认https://api.x.ai/v1 — 仅在使用代理时修改。",
"admin.settings.openrouter.title": "OpenRouter — 网关密钥",
"admin.settings.openrouter.desc": "Essential 层Pro 套餐。当前型号deepseek/deepseek-v4-flash、z-ai/glm-5.3-flash、minimax/minimax-m3。",
"admin.settings.openrouter.keyLabel": "OpenRouter API 密钥(与 Premium 共用)",
"admin.settings.openrouter.envSharedPlaceholder": "密钥已在 .env 中配置(与 Premium 共用)",
"admin.settings.openrouter.reminder": "模型在「模型与订阅」页面管理。",
"admin.settings.openrouterPremium.title": "OpenRouter Premium — 网关密钥",
"admin.settings.openrouterPremium.desc": "Premium 层Business 套餐。当前型号anthropic/claude-sonnet-5、deepseek/deepseek-v4-pro、z-ai/glm-5.3。",
"admin.settings.openrouterPremium.reminder": "模型在「模型与订阅」页面管理。共用同一个 OpenRouter 密钥。",
"admin.settings.mistral.title": "Mistral OCR扫描版 PDF",
"admin.settings.mistral.desc": "翻译前从扫描版 PDF图片页面提取文字。没有密钥时这类文件会被明确提示后拒绝。约 1 欧元 / 1000 页。密钥console.mistral.ai。",
"admin.settings.mistral.keyLabel": "Mistral API 密钥",
"admin.settings.mistral.modelLabel": "OCR 模型",
"admin.settings.mistral.modelHint": "推荐mistral-ocr-latest",
"admin.settings.smtp.title": "SMTP 邮件",
"admin.settings.smtp.desc": "用于发送邮件的 SMTP 服务器设置(忘记密码、通知、跟进邮件)。",
"admin.settings.smtp.host": "SMTP 主机",
"admin.settings.smtp.port": "端口",
"admin.settings.smtp.username": "用户名",
"admin.settings.smtp.password": "密码",
"admin.settings.smtp.fromEmail": "发件地址",
"admin.settings.smtp.useTls": "使用 TLS",
"admin.settings.smtp.sendTest": "发送测试邮件",
"admin.settings.smtp.sent": "已发送",
"admin.settings.smtp.sendFailed": "发送失败",
"admin.models.channelOpenrouter": "OpenRouter",
"admin.models.channelOpenrouterPremium": "OpenRouterPremium",
"admin.models.channelDeepseek": "DeepSeek直连",
"admin.models.channelZhipu": "z.aiGLM",
"admin.models.channelMinimax": "MiniMax直连",
"admin.models.channelOpenai": "OpenAI",
"admin.models.channelZai": "xAIGrok",
"admin.models.customModelPlaceholder": "模型 ID",
"admin.models.routeHint": "通道决定使用直连接入还是网关;模型标识因通道而异。",
"admin.models.addRouteConfirm": "添加",
"admin.marketing.generateTitle": "用 AI 生成",
"admin.marketing.generateDesc": "文案依据营销方案撰写(定位与允许使用的数字)。不会自动发送任何内容:请先审阅再发送。",
"admin.marketing.channel": "通道",
"admin.marketing.model": "模型",
"admin.marketing.modelPlaceholder": "通道默认",
"admin.marketing.promoOptional": "折扣(可选)",
"admin.marketing.promoNone": "无",
"admin.marketing.brief": "内容简报",
"admin.marketing.briefPlaceholder": "例如:提醒等候名单中的注册用户 Pro 套餐的优势。",
"admin.marketing.language": "邮件语言",
"admin.marketing.tone": "语气",
"admin.marketing.toneNone": "中性",
"admin.marketing.generateBtn": "生成",
"admin.marketing.generating": "生成中...",
"admin.marketing.generatedTitle": "邮件已生成",
"admin.marketing.generatedDesc": "主题和 HTML 已填入表单 —— 发送前请审阅({provider})。",
"admin.marketing.generateFailedTitle": "无法生成",
"admin.marketing.briefRequired": "请先在内容简报中描述所需内容。",
"admin.marketing.langFr": "法语",
"admin.marketing.langEn": "英语",
"admin.marketing.langDe": "德语",
"admin.marketing.langEs": "西班牙语",
"admin.marketing.langIt": "意大利语",
"admin.marketing.langPt": "葡萄牙语",
"admin.marketing.langNl": "荷兰语",
"admin.marketing.langAr": "阿拉伯语",
"admin.marketing.langFa": "波斯语",
"admin.marketing.langJa": "日语",
"admin.marketing.langKo": "韩语",
"admin.marketing.langRu": "俄语",
"admin.marketing.langZh": "中文",
"admin.marketing.toneFriendly": "亲切",
"admin.marketing.toneProfessional": "专业",
"admin.marketing.toneUrgent": "紧迫",
"admin.marketing.toneFestive": "喜庆",
"admin.marketing.promoTitle": "折扣",
"admin.marketing.promoDesc": "优惠码通过 Stripe 应用于付款;计数器只在订阅真正完成后增加。",
"admin.marketing.promoEmpty": "暂无优惠码。",
"admin.marketing.promoActive": "启用",
"admin.marketing.promoInactive": "已停用",
"admin.marketing.promoLocalOnly": "不在 Stripe",
"admin.marketing.promoDeactivate": "停用",
"admin.marketing.promoUsage": "已使用 {used}/{max} 次",
"admin.marketing.promoPlansLabel": "套餐:",
"admin.marketing.promoAllPlans": "全部",
"admin.marketing.promoExpiresOn": "{date} 到期",
"admin.marketing.promoNeverExpires": "不过期",
"admin.marketing.promoCreateTitle": "创建折扣",
"admin.marketing.promoCreateDesc": "如果配置了 Stripe优惠码也会在 Stripe 中创建(优惠券 + 促销码)。",
"admin.marketing.promoCode": "代码",
"admin.marketing.promoType": "类型",
"admin.marketing.promoTypePercent": "百分比",
"admin.marketing.promoTypeAmount": "金额EUR",
"admin.marketing.promoValue": "数值",
"admin.marketing.promoPlans": "适用套餐",
"admin.marketing.promoMaxRedemptions": "使用上限",
"admin.marketing.promoMaxPlaceholder": "不限",
"admin.marketing.promoExpires": "到期日",
"admin.marketing.promoCreate": "创建代码",
"admin.marketing.promoCreating": "创建中...",
"admin.marketing.promoCreatedTitle": "优惠码已创建",
"admin.marketing.promoCreatedDesc": "代码 {code} 已就绪。",
"admin.marketing.promoCreateFailedTitle": "创建被拒绝",
"admin.marketing.promoInvalidInput": "请检查代码和数值。",
"admin.marketing.promoDeactivatedTitle": "代码已停用",
"admin.marketing.promoDeactivatedDesc": "代码 {code} 不再适用。",
"admin.settings.fallback.title": "备用链",
"admin.settings.fallback.desc": "自动选择服务商的优先顺序。",
"admin.settings.fallback.classic": "经典模式Google",
"admin.settings.fallback.llm": "LLM 模式(网关与直连接入)",
"admin.settings.fallback.note": "只有已注册的通道google、openai、deepseek、minimax参与这条链AI 层级和其他通道不参与。",
"admin.models.duplicateRouteTitle": "已拒绝重复项",
"admin.models.duplicateRouteDesc": "该通道 + 模型组合已在列表中。",
"admin.marketing.promoDeactivateFailedTitle": "停用被拒绝",
"admin.marketing.promoDelete": "删除",
"admin.marketing.promoDeleteConfirm": "确定永久删除代码 {code} 吗?",
"admin.marketing.promoDeletedTitle": "代码已删除",
"admin.marketing.promoDeletedDesc": "代码 {code} 已删除。",
"admin.marketing.promoDeleteFailedTitle": "删除被拒绝",
"admin.marketing.promoLinkStripe": "关联到 Stripe",
"admin.marketing.promoLinkedTitle": "代码已关联到 Stripe",
"admin.marketing.promoLinkedDesc": "代码 {code} 现在可用于付款。",
"admin.marketing.promoLinkFailedTitle": "关联失败",
"admin.marketing.promoExpiryHint": "「至 10 日」的代码在整个 10 日当天有效(至 23:59:59 UTC。",
"admin.marketing.generateEmpty": "服务器返回的内容为空或无法使用。"
}

View File

@@ -167,5 +167,17 @@
"pricing.header.titleBase": "总有一款套餐适合",
"pricing.header.titleAccent": "您的需求",
"pricing.toast.close": "关闭",
"pricing.aiModels.premium.price": "每百万 Token $2 / $10"
"pricing.aiModels.premium.price": "每百万 Token $2 / $10",
"pricing.promo.label": "优惠码",
"pricing.promo.placeholder": "您的代码",
"pricing.promo.apply": "应用",
"pricing.promo.checking": "检查中...",
"pricing.promo.valid": "代码有效",
"pricing.promo.subscriptionOnly": "仅适用于订阅,不适用于积分。",
"pricing.promoError.unknown": "该优惠码不存在。",
"pricing.promoError.inactive": "该优惠码已停用。",
"pricing.promoError.expired": "该优惠码已过期。",
"pricing.promoError.exhausted": "该优惠码已达到使用上限。",
"pricing.promoError.plan": "该优惠码不适用于此套餐。",
"pricing.promoError.local_only": "该优惠码未关联在线支付。"
}