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
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:
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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
@@ -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>;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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": "كان رد الخادم فارغًا أو غير قابل للاستخدام."
|
||||
}
|
||||
|
||||
@@ -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": "كود الخصم هذا غير مرتبط بالدفع عبر الإنترنت."
|
||||
}
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
|
||||
@@ -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": "پاسخ سرور خالی یا غیرقابل استفاده بود."
|
||||
}
|
||||
|
||||
@@ -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": "این کد تخفیف برای پرداخت آنلاین فعال نشده است."
|
||||
}
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
|
||||
@@ -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 / 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": "サーバーの応答が空か、利用できませんでした。"
|
||||
}
|
||||
|
||||
@@ -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": "このプロモコードはオンライン決済に対応していません。"
|
||||
}
|
||||
|
||||
@@ -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": "서버 응답이 비어 있거나 사용할 수 없습니다."
|
||||
}
|
||||
|
||||
@@ -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": "이 프로모 코드는 온라인 결제에 연결되어 있지 않습니다."
|
||||
}
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
|
||||
@@ -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": "Ответ сервера был пустым или непригодным."
|
||||
}
|
||||
|
||||
@@ -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": "Этот промокод не привязан к онлайн-оплате."
|
||||
}
|
||||
|
||||
@@ -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 / 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": "MiniMax 公开 API,兼容 OpenAI。密钥来自 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 欧元 / 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": "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": "通道决定使用直连接入还是网关;模型标识因通道而异。",
|
||||
"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": "服务器返回的内容为空或无法使用。"
|
||||
}
|
||||
|
||||
@@ -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": "该优惠码未关联在线支付。"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user