Implement internationalization support across components
- Integrated `next-intl` for toast messages and locale handling in various components, including `Providers`, `CanvasUserMenu`, and `CreditOverview`. - Replaced hardcoded strings with translation keys to enhance localization capabilities. - Updated `RootLayout` to dynamically set the language attribute based on the user's locale. - Ensured consistent user feedback through localized toast messages in actions such as sign-out, canvas operations, and billing notifications.
This commit is contained in:
176
lib/ai-errors.ts
176
lib/ai-errors.ts
@@ -1,23 +1,22 @@
|
||||
export type AiErrorCategory =
|
||||
| "insufficient_credits"
|
||||
| "rate_limited"
|
||||
| "content_policy"
|
||||
export type ErrorType =
|
||||
| "timeout"
|
||||
| "network"
|
||||
| "server"
|
||||
| "invalid_request"
|
||||
| "daily_cap"
|
||||
| "concurrency"
|
||||
| "unknown";
|
||||
| "insufficientCredits"
|
||||
| "networkError"
|
||||
| "rateLimited"
|
||||
| "modelUnavailable"
|
||||
| "generic"
|
||||
| "contentPolicy"
|
||||
| "invalidRequest"
|
||||
| "dailyCap"
|
||||
| "concurrency";
|
||||
|
||||
export interface AiError {
|
||||
category: AiErrorCategory;
|
||||
message: string;
|
||||
detail?: string;
|
||||
type: ErrorType;
|
||||
retryable: boolean;
|
||||
creditsNotCharged: boolean;
|
||||
showTopUp: boolean;
|
||||
retryCount?: number;
|
||||
rawMessage?: string;
|
||||
}
|
||||
|
||||
type RawErrorObject = {
|
||||
@@ -27,45 +26,50 @@ type RawErrorObject = {
|
||||
retryCount?: unknown;
|
||||
};
|
||||
|
||||
const CATEGORY_ALIASES: Record<string, AiErrorCategory> = {
|
||||
insufficient_credits: "insufficient_credits",
|
||||
insufficientcredits: "insufficient_credits",
|
||||
not_enough_credits: "insufficient_credits",
|
||||
notenoughcredits: "insufficient_credits",
|
||||
credits: "insufficient_credits",
|
||||
payment_required: "insufficient_credits",
|
||||
paymentrequired: "insufficient_credits",
|
||||
rate_limit: "rate_limited",
|
||||
ratelimit: "rate_limited",
|
||||
rate_limited: "rate_limited",
|
||||
ratelimited: "rate_limited",
|
||||
too_many_requests: "rate_limited",
|
||||
toomanyrequests: "rate_limited",
|
||||
content_policy: "content_policy",
|
||||
contentpolicy: "content_policy",
|
||||
safety: "content_policy",
|
||||
const TYPE_ALIASES: Record<string, ErrorType> = {
|
||||
insufficient_credits: "insufficientCredits",
|
||||
insufficientcredits: "insufficientCredits",
|
||||
not_enough_credits: "insufficientCredits",
|
||||
notenoughcredits: "insufficientCredits",
|
||||
credits: "insufficientCredits",
|
||||
payment_required: "insufficientCredits",
|
||||
paymentrequired: "insufficientCredits",
|
||||
rate_limit: "rateLimited",
|
||||
ratelimit: "rateLimited",
|
||||
rate_limited: "rateLimited",
|
||||
ratelimited: "rateLimited",
|
||||
too_many_requests: "rateLimited",
|
||||
toomanyrequests: "rateLimited",
|
||||
content_policy: "contentPolicy",
|
||||
contentpolicy: "contentPolicy",
|
||||
safety: "contentPolicy",
|
||||
timeout: "timeout",
|
||||
timed_out: "timeout",
|
||||
timedout: "timeout",
|
||||
network: "network",
|
||||
connection: "network",
|
||||
server: "server",
|
||||
invalid_request: "invalid_request",
|
||||
invalidrequest: "invalid_request",
|
||||
bad_request: "invalid_request",
|
||||
badrequest: "invalid_request",
|
||||
daily_cap: "daily_cap",
|
||||
dailycap: "daily_cap",
|
||||
daily_limit: "daily_cap",
|
||||
dailylimit: "daily_cap",
|
||||
network: "networkError",
|
||||
connection: "networkError",
|
||||
networkerror: "networkError",
|
||||
server: "modelUnavailable",
|
||||
model_unavailable: "modelUnavailable",
|
||||
modelunavailable: "modelUnavailable",
|
||||
invalid_request: "invalidRequest",
|
||||
invalidrequest: "invalidRequest",
|
||||
bad_request: "invalidRequest",
|
||||
badrequest: "invalidRequest",
|
||||
unknown_model: "invalidRequest",
|
||||
daily_cap: "dailyCap",
|
||||
dailycap: "dailyCap",
|
||||
daily_limit: "dailyCap",
|
||||
dailylimit: "dailyCap",
|
||||
concurrency: "concurrency",
|
||||
concurrent: "concurrency",
|
||||
unknown: "generic",
|
||||
};
|
||||
|
||||
function normalizeCategory(value: string | undefined): AiErrorCategory | undefined {
|
||||
function normalizeType(value: string | undefined): ErrorType | undefined {
|
||||
if (!value) return undefined;
|
||||
const normalized = value.toLowerCase().replace(/[^a-z]/g, "");
|
||||
return CATEGORY_ALIASES[normalized];
|
||||
return TYPE_ALIASES[normalized];
|
||||
}
|
||||
|
||||
function extractRetryCount(rawText: string, rawObj: RawErrorObject | null): number | undefined {
|
||||
@@ -86,15 +90,15 @@ function extractRetryCount(rawText: string, rawObj: RawErrorObject | null): numb
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function cleanPrefixMessage(text: string): { category?: AiErrorCategory; message: string } {
|
||||
function cleanPrefixMessage(text: string): { type?: ErrorType; message: string } {
|
||||
const trimmed = text.trim();
|
||||
|
||||
const bracketPrefix = trimmed.match(/^\[([a-zA-Z_\- ]+)\]\s*[:\-]?\s*(.+)$/);
|
||||
if (bracketPrefix?.[1] && bracketPrefix[2]) {
|
||||
const category = normalizeCategory(bracketPrefix[1]);
|
||||
if (category) {
|
||||
const type = normalizeType(bracketPrefix[1]);
|
||||
if (type) {
|
||||
return {
|
||||
category,
|
||||
type,
|
||||
message: bracketPrefix[2].trim(),
|
||||
};
|
||||
}
|
||||
@@ -102,10 +106,10 @@ function cleanPrefixMessage(text: string): { category?: AiErrorCategory; message
|
||||
|
||||
const plainPrefix = trimmed.match(/^([a-zA-Z_\- ]{3,40})\s*[:|\-]\s*(.+)$/);
|
||||
if (plainPrefix?.[1] && plainPrefix[2]) {
|
||||
const category = normalizeCategory(plainPrefix[1]);
|
||||
if (category) {
|
||||
const type = normalizeType(plainPrefix[1]);
|
||||
if (type) {
|
||||
return {
|
||||
category,
|
||||
type,
|
||||
message: plainPrefix[2].trim(),
|
||||
};
|
||||
}
|
||||
@@ -129,17 +133,17 @@ function splitMessageAndDetail(message: string): { message: string; detail?: str
|
||||
return { message };
|
||||
}
|
||||
|
||||
function inferCategoryFromText(text: string): AiErrorCategory {
|
||||
function inferTypeFromText(text: string): ErrorType {
|
||||
const lower = text.toLowerCase();
|
||||
|
||||
const openRouterStatus = lower.match(/openrouter api error\s*(\d{3})/i);
|
||||
if (openRouterStatus?.[1]) {
|
||||
const status = Number.parseInt(openRouterStatus[1], 10);
|
||||
if (status === 402) return "insufficient_credits";
|
||||
if (status === 402) return "insufficientCredits";
|
||||
if (status === 408 || status === 504) return "timeout";
|
||||
if (status === 429) return "rate_limited";
|
||||
if (status >= 500) return "server";
|
||||
if (status >= 400) return "invalid_request";
|
||||
if (status === 429) return "rateLimited";
|
||||
if (status >= 500) return "modelUnavailable";
|
||||
if (status >= 400) return "invalidRequest";
|
||||
}
|
||||
|
||||
if (
|
||||
@@ -149,7 +153,7 @@ function inferCategoryFromText(text: string): AiErrorCategory {
|
||||
lower.includes("guthaben") ||
|
||||
lower.includes("nicht genug credits")
|
||||
) {
|
||||
return "insufficient_credits";
|
||||
return "insufficientCredits";
|
||||
}
|
||||
|
||||
if (
|
||||
@@ -158,7 +162,7 @@ function inferCategoryFromText(text: string): AiErrorCategory {
|
||||
lower.includes("ratelimit") ||
|
||||
lower.includes("429")
|
||||
) {
|
||||
return "rate_limited";
|
||||
return "rateLimited";
|
||||
}
|
||||
|
||||
if (
|
||||
@@ -166,7 +170,7 @@ function inferCategoryFromText(text: string): AiErrorCategory {
|
||||
lower.includes("tageslimit erreicht") ||
|
||||
lower.includes("daily generation limit")
|
||||
) {
|
||||
return "daily_cap";
|
||||
return "dailyCap";
|
||||
}
|
||||
|
||||
if (
|
||||
@@ -191,7 +195,7 @@ function inferCategoryFromText(text: string): AiErrorCategory {
|
||||
lower.includes("fetch failed") ||
|
||||
lower.includes("econn")
|
||||
) {
|
||||
return "network";
|
||||
return "networkError";
|
||||
}
|
||||
|
||||
if (
|
||||
@@ -200,7 +204,7 @@ function inferCategoryFromText(text: string): AiErrorCategory {
|
||||
lower.includes("refusal") ||
|
||||
lower.includes("modell lehnt ab")
|
||||
) {
|
||||
return "content_policy";
|
||||
return "contentPolicy";
|
||||
}
|
||||
|
||||
if (
|
||||
@@ -209,85 +213,75 @@ function inferCategoryFromText(text: string): AiErrorCategory {
|
||||
lower.includes("unknown model") ||
|
||||
lower.includes("missing")
|
||||
) {
|
||||
return "invalid_request";
|
||||
return "invalidRequest";
|
||||
}
|
||||
|
||||
if (lower.includes("server") || lower.includes("5xx")) {
|
||||
return "server";
|
||||
return "modelUnavailable";
|
||||
}
|
||||
|
||||
return "unknown";
|
||||
return "generic";
|
||||
}
|
||||
|
||||
function defaultsForCategory(category: AiErrorCategory): Omit<AiError, "category" | "detail" | "retryCount"> {
|
||||
switch (category) {
|
||||
case "insufficient_credits":
|
||||
function defaultsForType(type: ErrorType): Omit<AiError, "type" | "retryCount" | "rawMessage"> {
|
||||
switch (type) {
|
||||
case "insufficientCredits":
|
||||
return {
|
||||
message: "Not enough credits for this generation",
|
||||
retryable: false,
|
||||
creditsNotCharged: true,
|
||||
showTopUp: true,
|
||||
};
|
||||
case "rate_limited":
|
||||
case "rateLimited":
|
||||
return {
|
||||
message: "The model is busy right now",
|
||||
retryable: true,
|
||||
creditsNotCharged: true,
|
||||
showTopUp: false,
|
||||
};
|
||||
case "content_policy":
|
||||
case "contentPolicy":
|
||||
return {
|
||||
message: "The request was blocked by model safety rules",
|
||||
retryable: false,
|
||||
creditsNotCharged: true,
|
||||
showTopUp: false,
|
||||
};
|
||||
case "timeout":
|
||||
return {
|
||||
message: "The generation timed out",
|
||||
retryable: true,
|
||||
creditsNotCharged: true,
|
||||
showTopUp: false,
|
||||
};
|
||||
case "network":
|
||||
case "networkError":
|
||||
return {
|
||||
message: "Network issue while contacting the model",
|
||||
retryable: true,
|
||||
creditsNotCharged: true,
|
||||
showTopUp: false,
|
||||
};
|
||||
case "server":
|
||||
case "modelUnavailable":
|
||||
return {
|
||||
message: "The AI service returned a server error",
|
||||
retryable: true,
|
||||
creditsNotCharged: true,
|
||||
showTopUp: false,
|
||||
};
|
||||
case "invalid_request":
|
||||
case "invalidRequest":
|
||||
return {
|
||||
message: "The request could not be processed",
|
||||
retryable: false,
|
||||
creditsNotCharged: true,
|
||||
showTopUp: false,
|
||||
};
|
||||
case "daily_cap":
|
||||
case "dailyCap":
|
||||
return {
|
||||
message: "Tageslimit erreicht",
|
||||
retryable: false,
|
||||
creditsNotCharged: true,
|
||||
showTopUp: false,
|
||||
};
|
||||
case "concurrency":
|
||||
return {
|
||||
message: "Generierung bereits aktiv",
|
||||
retryable: true,
|
||||
creditsNotCharged: true,
|
||||
showTopUp: false,
|
||||
};
|
||||
case "unknown":
|
||||
case "generic":
|
||||
default:
|
||||
return {
|
||||
message: "Generation failed",
|
||||
retryable: true,
|
||||
creditsNotCharged: true,
|
||||
showTopUp: false,
|
||||
@@ -313,22 +307,20 @@ export function classifyError(rawError: unknown): AiError {
|
||||
const rawDetail = typeof rawObj?.detail === "string" ? rawObj.detail.trim() : undefined;
|
||||
|
||||
const prefixed = cleanPrefixMessage(rawMessage);
|
||||
const explicitCategory =
|
||||
normalizeCategory(typeof rawObj?.category === "string" ? rawObj.category : undefined) ??
|
||||
prefixed.category;
|
||||
const category = explicitCategory ?? inferCategoryFromText(prefixed.message);
|
||||
const explicitType =
|
||||
normalizeType(typeof rawObj?.category === "string" ? rawObj.category : undefined) ??
|
||||
prefixed.type;
|
||||
const type = explicitType ?? inferTypeFromText(prefixed.message);
|
||||
|
||||
const defaults = defaultsForCategory(category);
|
||||
const defaults = defaultsForType(type);
|
||||
const split = splitMessageAndDetail(prefixed.message);
|
||||
const message = split.message || defaults.message;
|
||||
|
||||
return {
|
||||
category,
|
||||
message,
|
||||
detail: split.detail ?? rawDetail,
|
||||
type,
|
||||
retryable: defaults.retryable,
|
||||
creditsNotCharged: defaults.creditsNotCharged,
|
||||
showTopUp: defaults.showTopUp,
|
||||
retryCount: extractRetryCount(rawMessage, rawObj),
|
||||
rawMessage: split.message || rawMessage || undefined,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
/**
|
||||
* Formatiert einen Timestamp als relative Zeitangabe.
|
||||
* Beispiele: "Gerade eben", "vor 5 Min.", "vor 3 Std.", "vor 2 Tagen", "12. Mär"
|
||||
*/
|
||||
export function formatRelativeTime(timestamp: number): string {
|
||||
const now = Date.now();
|
||||
const diff = now - timestamp;
|
||||
const minutes = Math.floor(diff / 60000);
|
||||
const hours = Math.floor(diff / 3600000);
|
||||
const days = Math.floor(diff / 86400000);
|
||||
|
||||
if (minutes < 1) return "Gerade eben";
|
||||
if (minutes < 60) return `vor ${minutes} Min.`;
|
||||
if (hours < 24) return `vor ${hours} Std.`;
|
||||
if (days < 7) return days === 1 ? "vor 1 Tag" : `vor ${days} Tagen`;
|
||||
return new Date(timestamp).toLocaleDateString("de-DE", {
|
||||
day: "numeric",
|
||||
month: "short",
|
||||
});
|
||||
}
|
||||
@@ -1,194 +1,207 @@
|
||||
// Zentrales Dictionary für alle Toast-Strings.
|
||||
// Spätere i18n: diese Datei gegen Framework-Lookup ersetzen.
|
||||
'use client';
|
||||
|
||||
/** Grund, warum ein Node-Löschen noch blockiert ist. */
|
||||
export type CanvasNodeDeleteBlockReason = "optimistic";
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { toast, type ToastDurationOverrides } from './toast';
|
||||
import type { CanvasNodeDeleteBlockReason } from './toast';
|
||||
|
||||
const DURATION = {
|
||||
success: 4000,
|
||||
successShort: 2000,
|
||||
error: 6000,
|
||||
warning: 5000,
|
||||
info: 4000,
|
||||
} as const;
|
||||
|
||||
type ToastTranslations = ReturnType<typeof useTranslations<'toasts'>>;
|
||||
|
||||
function canvasNodeDeleteWhy(
|
||||
t: ToastTranslations,
|
||||
reasons: Set<CanvasNodeDeleteBlockReason>,
|
||||
): { title: string; desc: string } {
|
||||
if (reasons.size === 0) {
|
||||
return {
|
||||
title: "Löschen momentan nicht möglich",
|
||||
desc: "Bitte kurz warten und erneut versuchen.",
|
||||
title: t('canvas.nodeDeleteBlockedTitle'),
|
||||
desc: t('canvas.nodeDeleteBlockedDesc'),
|
||||
};
|
||||
}
|
||||
if (reasons.size === 1) {
|
||||
const only = [...reasons][0]!;
|
||||
if (only === "optimistic") {
|
||||
if (only === 'optimistic') {
|
||||
return {
|
||||
title: "Element wird noch angelegt",
|
||||
desc: "Dieses Element ist noch nicht vollständig auf dem Server gespeichert. Sobald die Synchronisierung fertig ist, kannst du es löschen.",
|
||||
title: t('canvas.nodeDeleteOptimisticTitle'),
|
||||
desc: t('canvas.nodeDeleteOptimisticDesc'),
|
||||
};
|
||||
}
|
||||
return {
|
||||
title: "Löschen momentan nicht möglich",
|
||||
desc: "Bitte kurz warten und erneut versuchen.",
|
||||
title: t('canvas.nodeDeleteBlockedTitle'),
|
||||
desc: t('canvas.nodeDeleteBlockedDesc'),
|
||||
};
|
||||
}
|
||||
return {
|
||||
title: "Löschen momentan nicht möglich",
|
||||
desc: "Mindestens ein Element wird noch angelegt. Bitte kurz warten und erneut versuchen.",
|
||||
title: t('canvas.nodeDeleteBlockedTitle'),
|
||||
desc: t('canvas.nodeDeleteBlockedMultiDesc'),
|
||||
};
|
||||
}
|
||||
|
||||
export const msg = {
|
||||
canvas: {
|
||||
imageUploaded: { title: "Bild hochgeladen" },
|
||||
uploadFailed: { title: "Upload fehlgeschlagen" },
|
||||
uploadFormatError: (format: string) => ({
|
||||
title: "Upload fehlgeschlagen",
|
||||
desc: `Format „${format}“ wird nicht unterstützt. Erlaubt: PNG, JPG, WebP.`,
|
||||
imageUploaded: (t: ToastTranslations) => ({
|
||||
title: t('canvas.imageUploaded'),
|
||||
}),
|
||||
uploadSizeError: (maxMb: number) => ({
|
||||
title: "Upload fehlgeschlagen",
|
||||
desc: `Maximale Dateigröße: ${maxMb} MB.`,
|
||||
uploadFailed: (t: ToastTranslations) => ({
|
||||
title: t('canvas.uploadFailed'),
|
||||
}),
|
||||
nodeRemoved: { title: "Element entfernt" },
|
||||
nodesRemoved: (count: number) => ({
|
||||
title: count === 1 ? "Element entfernt" : `${count} Elemente entfernt`,
|
||||
uploadFormatError: (t: ToastTranslations, format: string) => ({
|
||||
title: t('canvas.uploadFailed'),
|
||||
desc: t('canvas.uploadFormatError', { format }),
|
||||
}),
|
||||
/** Warum gerade kein (vollständiges) Löschen möglich ist — aus den gesammelten Gründen der blockierten Nodes. */
|
||||
nodeDeleteBlockedExplain: canvasNodeDeleteWhy,
|
||||
nodeDeleteBlockedPartial: (
|
||||
blockedCount: number,
|
||||
reasons: Set<CanvasNodeDeleteBlockReason>,
|
||||
) => {
|
||||
const why = canvasNodeDeleteWhy(reasons);
|
||||
uploadSizeError: (t: ToastTranslations, maxMb: number) => ({
|
||||
title: t('canvas.uploadFailed'),
|
||||
desc: t('canvas.uploadSizeError', { maxMb }),
|
||||
}),
|
||||
nodeRemoved: (t: ToastTranslations) => ({
|
||||
title: t('canvas.nodeRemoved'),
|
||||
}),
|
||||
nodesRemoved: (t: ToastTranslations, count: number) => ({
|
||||
title: t('canvas.nodesRemoved', { count }),
|
||||
}),
|
||||
nodeDeleteBlockedExplain: (t: ToastTranslations, reasons: Set<CanvasNodeDeleteBlockReason>) => canvasNodeDeleteWhy(t, reasons),
|
||||
nodeDeleteBlockedPartial: (t: ToastTranslations, blockedCount: number, reasons: Set<CanvasNodeDeleteBlockReason>) => {
|
||||
const why = canvasNodeDeleteWhy(t, reasons);
|
||||
const suffix =
|
||||
blockedCount === 1
|
||||
? "Ein Element wurde deshalb nicht gelöscht; die übrige Auswahl wurde entfernt."
|
||||
: `${blockedCount} Elemente wurden deshalb nicht gelöscht; die übrige Auswahl wurde entfernt.`;
|
||||
? t('canvas.nodeDeleteBlockedPartialSuffixOne')
|
||||
: t('canvas.nodeDeleteBlockedPartialSuffixOther', { count: blockedCount });
|
||||
return {
|
||||
title: "Nicht alle Elemente entfernt",
|
||||
title: t('canvas.nodeDeleteBlockedPartialTitle'),
|
||||
desc: `${why.desc} ${suffix}`,
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
ai: {
|
||||
generating: { title: "Bild wird generiert…" },
|
||||
generated: { title: "Bild generiert" },
|
||||
generatedDesc: (credits: number) => `${credits} Credits verbraucht`,
|
||||
generationQueued: { title: "Generierung gestartet" },
|
||||
generationQueuedDesc: "Das Bild erscheint automatisch, sobald es fertig ist.",
|
||||
generationFailed: { title: "Generierung fehlgeschlagen" },
|
||||
creditsNotCharged: "Credits wurden nicht abgebucht",
|
||||
insufficientCredits: (needed: number, available: number) => ({
|
||||
title: "Nicht genügend Credits",
|
||||
desc: `${needed} Credits benötigt, ${available} verfügbar.`,
|
||||
generating: (t: ToastTranslations) => ({ title: t('ai.generating') }),
|
||||
generated: (t: ToastTranslations, credits: number) => ({
|
||||
title: t('ai.generated'),
|
||||
desc: t('ai.generatedDesc', { credits }),
|
||||
}),
|
||||
generatedDesc: (t: ToastTranslations, credits: number) => t('ai.generatedDesc', { credits }),
|
||||
generationQueued: (t: ToastTranslations) => ({ title: t('ai.generationQueued') }),
|
||||
generationQueuedDesc: (t: ToastTranslations) => t('ai.generationQueuedDesc'),
|
||||
generationFailed: (t: ToastTranslations) => ({ title: t('ai.generationFailed') }),
|
||||
creditsNotCharged: (t: ToastTranslations) => t('ai.creditsNotCharged'),
|
||||
insufficientCredits: (t: ToastTranslations, needed: number, available: number) => ({
|
||||
title: t('ai.insufficientCreditsTitle'),
|
||||
desc: t('ai.insufficientCreditsDesc', { needed, available }),
|
||||
}),
|
||||
modelUnavailable: (t: ToastTranslations) => ({
|
||||
title: t('ai.modelUnavailableTitle'),
|
||||
desc: t('ai.modelUnavailableDesc'),
|
||||
}),
|
||||
contentPolicy: (t: ToastTranslations) => ({
|
||||
title: t('ai.contentPolicyTitle'),
|
||||
desc: t('ai.contentPolicyDesc'),
|
||||
}),
|
||||
timeout: (t: ToastTranslations) => ({
|
||||
title: t('ai.timeoutTitle'),
|
||||
desc: t('ai.timeoutDesc'),
|
||||
}),
|
||||
openrouterIssues: (t: ToastTranslations) => ({
|
||||
title: t('ai.openrouterIssuesTitle'),
|
||||
desc: t('ai.openrouterIssuesDesc'),
|
||||
}),
|
||||
concurrentLimitReached: (t: ToastTranslations) => ({
|
||||
title: t('ai.concurrentLimitReachedTitle'),
|
||||
desc: t('ai.concurrentLimitReachedDesc'),
|
||||
}),
|
||||
modelUnavailable: {
|
||||
title: "Modell vorübergehend nicht verfügbar",
|
||||
desc: "Versuche ein anderes Modell oder probiere es später erneut.",
|
||||
},
|
||||
contentPolicy: {
|
||||
title: "Anfrage durch Inhaltsrichtlinie blockiert",
|
||||
desc: "Versuche, den Prompt umzuformulieren.",
|
||||
},
|
||||
timeout: {
|
||||
title: "Generierung abgelaufen",
|
||||
desc: "Credits wurden nicht abgebucht.",
|
||||
},
|
||||
openrouterIssues: {
|
||||
title: "OpenRouter möglicherweise gestört",
|
||||
desc: "Mehrere Generierungen fehlgeschlagen.",
|
||||
},
|
||||
concurrentLimitReached: {
|
||||
title: "Generierung bereits aktiv",
|
||||
desc: "Bitte warte, bis die laufende Generierung abgeschlossen ist.",
|
||||
},
|
||||
},
|
||||
|
||||
export: {
|
||||
frameExported: { title: "Frame exportiert" },
|
||||
exportingFrames: { title: "Frames werden exportiert…" },
|
||||
zipReady: { title: "ZIP bereit" },
|
||||
exportFailed: { title: "Export fehlgeschlagen" },
|
||||
frameEmpty: {
|
||||
title: "Export fehlgeschlagen",
|
||||
desc: "Frame hat keinen sichtbaren Inhalt.",
|
||||
},
|
||||
noFramesOnCanvas: {
|
||||
title: "Export fehlgeschlagen",
|
||||
desc: "Keine Frames auf dem Canvas — zuerst einen Frame anlegen.",
|
||||
},
|
||||
download: "Herunterladen",
|
||||
downloaded: "Heruntergeladen!",
|
||||
frameExported: (t: ToastTranslations) => ({ title: t('export.frameExported') }),
|
||||
exportingFrames: (t: ToastTranslations) => ({ title: t('export.exportingFrames') }),
|
||||
zipReady: (t: ToastTranslations) => ({ title: t('export.zipReady') }),
|
||||
exportFailed: (t: ToastTranslations) => ({ title: t('export.exportFailed') }),
|
||||
frameEmpty: (t: ToastTranslations) => ({
|
||||
title: t('export.frameEmptyTitle'),
|
||||
desc: t('export.frameEmptyDesc'),
|
||||
}),
|
||||
noFramesOnCanvas: (t: ToastTranslations) => ({
|
||||
title: t('export.noFramesOnCanvasTitle'),
|
||||
desc: t('export.noFramesOnCanvasDesc'),
|
||||
}),
|
||||
download: (t: ToastTranslations) => t('export.download'),
|
||||
downloaded: (t: ToastTranslations) => t('export.downloaded'),
|
||||
},
|
||||
|
||||
auth: {
|
||||
welcomeBack: { title: "Willkommen zurück" },
|
||||
welcomeOnDashboard: { title: "Schön, dass du da bist" },
|
||||
checkEmail: (email: string) => ({
|
||||
title: "E-Mail prüfen",
|
||||
desc: `Bestätigungslink an ${email} gesendet.`,
|
||||
welcomeBack: (t: ToastTranslations) => ({ title: t('auth.welcomeBack') }),
|
||||
welcomeOnDashboard: (t: ToastTranslations) => ({ title: t('auth.welcomeOnDashboard') }),
|
||||
checkEmail: (t: ToastTranslations, email: string) => ({
|
||||
title: t('auth.checkEmailTitle'),
|
||||
desc: t('auth.checkEmailDesc', { email }),
|
||||
}),
|
||||
sessionExpired: (t: ToastTranslations) => ({
|
||||
title: t('auth.sessionExpiredTitle'),
|
||||
desc: t('auth.sessionExpiredDesc'),
|
||||
}),
|
||||
signedOut: (t: ToastTranslations) => ({ title: t('auth.signedOut') }),
|
||||
signIn: (t: ToastTranslations) => t('auth.signIn'),
|
||||
initialSetup: (t: ToastTranslations) => ({
|
||||
title: t('auth.initialSetupTitle'),
|
||||
desc: t('auth.initialSetupDesc'),
|
||||
}),
|
||||
sessionExpired: {
|
||||
title: "Sitzung abgelaufen",
|
||||
desc: "Bitte erneut anmelden.",
|
||||
},
|
||||
signedOut: { title: "Abgemeldet" },
|
||||
signIn: "Anmelden",
|
||||
initialSetup: {
|
||||
title: "Startguthaben aktiv",
|
||||
desc: "Du kannst loslegen.",
|
||||
},
|
||||
},
|
||||
|
||||
billing: {
|
||||
subscriptionActivated: (credits: number) => ({
|
||||
title: "Abo aktiviert",
|
||||
desc: `${credits} Credits deinem Guthaben hinzugefügt.`,
|
||||
subscriptionActivated: (t: ToastTranslations, credits: number) => ({
|
||||
title: t('billing.subscriptionActivatedTitle'),
|
||||
desc: t('billing.subscriptionActivatedDesc', { credits }),
|
||||
}),
|
||||
creditsAdded: (credits: number) => ({
|
||||
title: "Credits hinzugefügt",
|
||||
desc: `+${credits} Credits`,
|
||||
creditsAdded: (t: ToastTranslations, credits: number) => ({
|
||||
title: t('billing.creditsAddedTitle'),
|
||||
desc: t('billing.creditsAddedDesc', { credits }),
|
||||
}),
|
||||
subscriptionCancelled: (periodEnd: string) => ({
|
||||
title: "Abo gekündigt",
|
||||
desc: `Deine Credits bleiben bis ${periodEnd} verfügbar.`,
|
||||
subscriptionCancelled: (t: ToastTranslations, periodEnd: string) => ({
|
||||
title: t('billing.subscriptionCancelledTitle'),
|
||||
desc: t('billing.subscriptionCancelledDesc', { periodEnd }),
|
||||
}),
|
||||
paymentFailed: {
|
||||
title: "Zahlung fehlgeschlagen",
|
||||
desc: "Bitte Zahlungsmethode aktualisieren.",
|
||||
},
|
||||
dailyLimitReached: (limit: number) => ({
|
||||
title: "Tageslimit erreicht",
|
||||
desc: `Maximal ${limit} Generierungen pro Tag in deinem Tarif.`,
|
||||
paymentFailed: (t: ToastTranslations) => ({
|
||||
title: t('billing.paymentFailedTitle'),
|
||||
desc: t('billing.paymentFailedDesc'),
|
||||
}),
|
||||
lowCredits: (remaining: number) => ({
|
||||
title: "Credits fast aufgebraucht",
|
||||
desc: `Noch ${remaining} Credits übrig.`,
|
||||
dailyLimitReached: (t: ToastTranslations, limit: number) => ({
|
||||
title: t('billing.dailyLimitReachedTitle'),
|
||||
desc: t('billing.dailyLimitReachedDesc', { limit }),
|
||||
}),
|
||||
topUp: "Aufladen",
|
||||
upgrade: "Upgrade",
|
||||
manage: "Verwalten",
|
||||
redirectingToCheckout: {
|
||||
title: "Weiterleitung…",
|
||||
desc: "Du wirst zum sicheren Checkout weitergeleitet.",
|
||||
},
|
||||
openingPortal: {
|
||||
title: "Portal wird geöffnet…",
|
||||
desc: "Du wirst zur Aboverwaltung weitergeleitet.",
|
||||
},
|
||||
testGrantFailed: { title: "Gutschrift fehlgeschlagen" },
|
||||
lowCredits: (t: ToastTranslations, remaining: number) => ({
|
||||
title: t('billing.lowCreditsTitle'),
|
||||
desc: t('billing.lowCreditsDesc', { remaining }),
|
||||
}),
|
||||
topUp: (t: ToastTranslations) => t('billing.topUp'),
|
||||
upgrade: (t: ToastTranslations) => t('billing.upgrade'),
|
||||
manage: (t: ToastTranslations) => t('billing.manage'),
|
||||
redirectingToCheckout: (t: ToastTranslations) => ({
|
||||
title: t('billing.redirectingToCheckoutTitle'),
|
||||
desc: t('billing.redirectingToCheckoutDesc'),
|
||||
}),
|
||||
openingPortal: (t: ToastTranslations) => ({
|
||||
title: t('billing.openingPortalTitle'),
|
||||
desc: t('billing.openingPortalDesc'),
|
||||
}),
|
||||
testGrantFailed: (t: ToastTranslations) => ({ title: t('billing.testGrantFailedTitle') }),
|
||||
},
|
||||
|
||||
system: {
|
||||
reconnected: { title: "Verbindung wiederhergestellt" },
|
||||
connectionLost: {
|
||||
title: "Verbindung verloren",
|
||||
desc: "Änderungen werden möglicherweise nicht gespeichert.",
|
||||
},
|
||||
copiedToClipboard: { title: "In Zwischenablage kopiert" },
|
||||
reconnected: (t: ToastTranslations) => ({ title: t('system.reconnected') }),
|
||||
connectionLost: (t: ToastTranslations) => ({
|
||||
title: t('system.connectionLostTitle'),
|
||||
desc: t('system.connectionLostDesc'),
|
||||
}),
|
||||
copiedToClipboard: (t: ToastTranslations) => ({ title: t('system.copiedToClipboard') }),
|
||||
},
|
||||
|
||||
dashboard: {
|
||||
renameEmpty: { title: "Name ungültig", desc: "Name darf nicht leer sein." },
|
||||
renameSuccess: { title: "Arbeitsbereich umbenannt" },
|
||||
renameFailed: { title: "Umbenennen fehlgeschlagen" },
|
||||
deleteSuccess: { title: "Arbeitsbereich gelöscht" },
|
||||
deleteFailed: { title: "Löschen fehlgeschlagen" },
|
||||
renameEmpty: (t: ToastTranslations) => ({
|
||||
title: t('dashboard.renameEmptyTitle'),
|
||||
desc: t('dashboard.renameEmptyDesc'),
|
||||
}),
|
||||
renameSuccess: (t: ToastTranslations) => ({ title: t('dashboard.renameSuccess') }),
|
||||
renameFailed: (t: ToastTranslations) => ({ title: t('dashboard.renameFailed') }),
|
||||
deleteSuccess: (t: ToastTranslations) => ({ title: t('dashboard.deleteSuccess') }),
|
||||
deleteFailed: (t: ToastTranslations) => ({ title: t('dashboard.deleteFailed') }),
|
||||
},
|
||||
} as const;
|
||||
|
||||
344
lib/toast.ts
344
lib/toast.ts
@@ -1,4 +1,7 @@
|
||||
import { gooeyToast, type GooeyPromiseData } from "goey-toast";
|
||||
'use client';
|
||||
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { gooeyToast, type GooeyPromiseData } from 'goey-toast';
|
||||
|
||||
const DURATION = {
|
||||
success: 4000,
|
||||
@@ -8,70 +11,48 @@ const DURATION = {
|
||||
info: 4000,
|
||||
} as const;
|
||||
|
||||
type ToastTranslations = ReturnType<typeof useTranslations<'toasts'>>;
|
||||
|
||||
export type ToastDurationOverrides = {
|
||||
duration?: number;
|
||||
};
|
||||
|
||||
export const toast = {
|
||||
success(
|
||||
message: string,
|
||||
description?: string,
|
||||
opts?: ToastDurationOverrides,
|
||||
) {
|
||||
success(message: string, description?: string, opts?: ToastDurationOverrides) {
|
||||
return gooeyToast.success(message, {
|
||||
description,
|
||||
duration: opts?.duration ?? DURATION.success,
|
||||
});
|
||||
},
|
||||
|
||||
error(
|
||||
message: string,
|
||||
description?: string,
|
||||
opts?: ToastDurationOverrides,
|
||||
) {
|
||||
error(message: string, description?: string, opts?: ToastDurationOverrides) {
|
||||
return gooeyToast.error(message, {
|
||||
description,
|
||||
duration: opts?.duration ?? DURATION.error,
|
||||
});
|
||||
},
|
||||
|
||||
warning(
|
||||
message: string,
|
||||
description?: string,
|
||||
opts?: ToastDurationOverrides,
|
||||
) {
|
||||
warning(message: string, description?: string, opts?: ToastDurationOverrides) {
|
||||
return gooeyToast.warning(message, {
|
||||
description,
|
||||
duration: opts?.duration ?? DURATION.warning,
|
||||
});
|
||||
},
|
||||
|
||||
info(
|
||||
message: string,
|
||||
description?: string,
|
||||
opts?: ToastDurationOverrides,
|
||||
) {
|
||||
info(message: string, description?: string, opts?: ToastDurationOverrides) {
|
||||
return gooeyToast.info(message, {
|
||||
description,
|
||||
duration: opts?.duration ?? DURATION.info,
|
||||
});
|
||||
},
|
||||
|
||||
promise<T>(promise: Promise<T>, data: GooeyPromiseData<T>) {
|
||||
return gooeyToast.promise(promise, data);
|
||||
},
|
||||
|
||||
action(
|
||||
message: string,
|
||||
opts: {
|
||||
description?: string;
|
||||
label: string;
|
||||
onClick: () => void;
|
||||
successLabel?: string;
|
||||
type?: "success" | "info" | "warning";
|
||||
duration?: number;
|
||||
},
|
||||
) {
|
||||
action(message: string, opts: {
|
||||
description?: string;
|
||||
label: string;
|
||||
onClick: () => void;
|
||||
successLabel?: string;
|
||||
type?: "success" | "info" | "warning";
|
||||
duration?: number;
|
||||
}) {
|
||||
const t = opts.type ?? "info";
|
||||
return gooeyToast[t](message, {
|
||||
description: opts.description,
|
||||
@@ -83,18 +64,13 @@ export const toast = {
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
update(
|
||||
id: string | number,
|
||||
opts: {
|
||||
title?: string;
|
||||
description?: string;
|
||||
type?: "default" | "success" | "error" | "warning" | "info";
|
||||
},
|
||||
) {
|
||||
update(id: string | number, opts: {
|
||||
title?: string;
|
||||
description?: string;
|
||||
type?: "default" | "success" | "error" | "warning" | "info";
|
||||
}) {
|
||||
gooeyToast.update(id, opts);
|
||||
},
|
||||
|
||||
dismiss(id?: string | number) {
|
||||
gooeyToast.dismiss(id);
|
||||
},
|
||||
@@ -107,3 +83,277 @@ export const toastDuration = {
|
||||
warning: DURATION.warning,
|
||||
info: DURATION.info,
|
||||
} as const;
|
||||
|
||||
export type CanvasNodeDeleteBlockReason = 'optimistic';
|
||||
|
||||
export function showImageUploadedToast(t: ToastTranslations) {
|
||||
toast.success(t('canvas.imageUploaded'));
|
||||
}
|
||||
|
||||
export function showUploadFailedToast(t: ToastTranslations, reason?: string) {
|
||||
if (reason) {
|
||||
toast.error(t('canvas.uploadFailed'), reason);
|
||||
} else {
|
||||
toast.error(t('canvas.uploadFailed'));
|
||||
}
|
||||
}
|
||||
|
||||
export function showUploadFormatError(t: ToastTranslations, format: string) {
|
||||
toast.error(t('canvas.uploadFailed'), t('canvas.uploadFormatError', { format }));
|
||||
}
|
||||
|
||||
export function showUploadSizeError(t: ToastTranslations, maxMb: number) {
|
||||
toast.error(t('canvas.uploadFailed'), t('canvas.uploadSizeError', { maxMb }));
|
||||
}
|
||||
|
||||
export function showNodeRemovedToast(t: ToastTranslations) {
|
||||
toast.success(t('canvas.nodeRemoved'));
|
||||
}
|
||||
|
||||
export function showNodesRemovedToast(t: ToastTranslations, count: number) {
|
||||
const title = t('canvas.nodesRemoved', { count });
|
||||
toast.success(title);
|
||||
}
|
||||
|
||||
export function canvasNodeDeleteWhy(
|
||||
t: ToastTranslations,
|
||||
reasons: Set<CanvasNodeDeleteBlockReason>,
|
||||
) {
|
||||
if (reasons.size === 0) {
|
||||
return {
|
||||
title: t('canvas.nodeDeleteBlockedTitle'),
|
||||
desc: t('canvas.nodeDeleteBlockedDesc'),
|
||||
};
|
||||
}
|
||||
if (reasons.size === 1) {
|
||||
const only = [...reasons][0]!;
|
||||
if (only === 'optimistic') {
|
||||
return {
|
||||
title: t('canvas.nodeDeleteOptimisticTitle'),
|
||||
desc: t('canvas.nodeDeleteOptimisticDesc'),
|
||||
};
|
||||
}
|
||||
return {
|
||||
title: t('canvas.nodeDeleteBlockedTitle'),
|
||||
desc: t('canvas.nodeDeleteBlockedDesc'),
|
||||
};
|
||||
}
|
||||
return {
|
||||
title: t('canvas.nodeDeleteBlockedTitle'),
|
||||
desc: t('canvas.nodeDeleteBlockedMultiDesc'),
|
||||
};
|
||||
}
|
||||
|
||||
export function canvasNodeDeleteBlockedPartial(
|
||||
t: ToastTranslations,
|
||||
blockedCount: number,
|
||||
reasons: Set<CanvasNodeDeleteBlockReason>,
|
||||
) {
|
||||
const why = canvasNodeDeleteWhy(t, reasons);
|
||||
const suffix =
|
||||
blockedCount === 1
|
||||
? t('canvas.nodeDeleteBlockedPartialSuffixOne')
|
||||
: t('canvas.nodeDeleteBlockedPartialSuffixOther', { count: blockedCount });
|
||||
return {
|
||||
title: t('canvas.nodeDeleteBlockedPartialTitle'),
|
||||
desc: `${why.desc} ${suffix}`,
|
||||
};
|
||||
}
|
||||
|
||||
export function showGeneratingToast(t: ToastTranslations) {
|
||||
gooeyToast.info(t('ai.generating'), { duration: Infinity });
|
||||
}
|
||||
|
||||
export function showGeneratedToast(
|
||||
t: ToastTranslations,
|
||||
credits: number,
|
||||
) {
|
||||
toast.success(t('ai.generated'), t('ai.generatedDesc', { credits }));
|
||||
}
|
||||
|
||||
export function showGenerationQueuedToast(t: ToastTranslations) {
|
||||
toast.success(t('ai.generationQueued'), t('ai.generationQueuedDesc'));
|
||||
}
|
||||
|
||||
export function showGenerationFailedToast(t: ToastTranslations) {
|
||||
toast.error(t('ai.generationFailed'));
|
||||
}
|
||||
|
||||
export function showCreditsNotChargedToast(t: ToastTranslations) {
|
||||
toast.warning(t('ai.creditsNotCharged'));
|
||||
}
|
||||
|
||||
export function showInsufficientCreditsToast(
|
||||
t: ToastTranslations,
|
||||
needed: number,
|
||||
available: number,
|
||||
) {
|
||||
toast.error(t('ai.insufficientCreditsTitle'), t('ai.insufficientCreditsDesc', { needed, available }));
|
||||
}
|
||||
|
||||
export function showModelUnavailableToast(t: ToastTranslations) {
|
||||
toast.error(t('ai.modelUnavailableTitle'), t('ai.modelUnavailableDesc'));
|
||||
}
|
||||
|
||||
export function showContentPolicyBlockedToast(t: ToastTranslations) {
|
||||
toast.error(t('ai.contentPolicyTitle'), t('ai.contentPolicyDesc'));
|
||||
}
|
||||
|
||||
export function showTimeoutToast(t: ToastTranslations) {
|
||||
toast.error(t('ai.timeoutTitle'), t('ai.timeoutDesc'));
|
||||
}
|
||||
|
||||
export function showOpenrouterIssuesToast(t: ToastTranslations) {
|
||||
toast.error(t('ai.openrouterIssuesTitle'), t('ai.openrouterIssuesDesc'));
|
||||
}
|
||||
|
||||
export function showConcurrentLimitReachedToast(t: ToastTranslations) {
|
||||
toast.error(t('ai.concurrentLimitReachedTitle'), t('ai.concurrentLimitReachedDesc'));
|
||||
}
|
||||
|
||||
export function showFrameExportedToast(t: ToastTranslations) {
|
||||
toast.success(t('export.frameExported'));
|
||||
}
|
||||
|
||||
export function showExportingFramesToast(t: ToastTranslations) {
|
||||
gooeyToast.info(t('export.exportingFrames'), { duration: Infinity });
|
||||
}
|
||||
|
||||
export function showZipReadyToast(t: ToastTranslations) {
|
||||
toast.success(t('export.zipReady'));
|
||||
}
|
||||
|
||||
export function showExportFailedToast(t: ToastTranslations) {
|
||||
toast.error(t('export.exportFailed'));
|
||||
}
|
||||
|
||||
export function showFrameEmptyToast(t: ToastTranslations) {
|
||||
toast.error(t('export.frameEmptyTitle'), t('export.frameEmptyDesc'));
|
||||
}
|
||||
|
||||
export function showNoFramesOnCanvasToast(t: ToastTranslations) {
|
||||
toast.error(t('export.noFramesOnCanvasTitle'), t('export.noFramesOnCanvasDesc'));
|
||||
}
|
||||
|
||||
export function showDownloadToast(t: ToastTranslations) {
|
||||
toast.success(t('export.downloaded'), t('export.download'));
|
||||
}
|
||||
|
||||
export function showWelcomeBackToast(t: ToastTranslations) {
|
||||
toast.success(t('auth.welcomeBack'));
|
||||
}
|
||||
|
||||
export function showWelcomeOnDashboardToast(t: ToastTranslations) {
|
||||
toast.success(t('auth.welcomeOnDashboard'));
|
||||
}
|
||||
|
||||
export function showCheckEmailToast(t: ToastTranslations, email: string) {
|
||||
toast.success(t('auth.checkEmailTitle'), t('auth.checkEmailDesc', { email }));
|
||||
}
|
||||
|
||||
export function showSessionExpiredToast(t: ToastTranslations) {
|
||||
toast.error(t('auth.sessionExpiredTitle'), t('auth.sessionExpiredDesc'));
|
||||
}
|
||||
|
||||
export function showSignedOutToast(t: ToastTranslations) {
|
||||
toast.success(t('auth.signedOut'));
|
||||
}
|
||||
|
||||
export function showSignInToast(t: ToastTranslations) {
|
||||
toast.success(t('auth.signIn'));
|
||||
}
|
||||
|
||||
export function showInitialSetupToast(t: ToastTranslations) {
|
||||
toast.success(t('auth.initialSetupTitle'), t('auth.initialSetupDesc'));
|
||||
}
|
||||
|
||||
export function showSubscriptionActivatedToast(
|
||||
t: ToastTranslations,
|
||||
credits: number,
|
||||
) {
|
||||
toast.success(t('billing.subscriptionActivatedTitle'), t('billing.subscriptionActivatedDesc', { credits }));
|
||||
}
|
||||
|
||||
export function showCreditsAddedToast(t: ToastTranslations, credits: number) {
|
||||
toast.success(t('billing.creditsAddedTitle'), t('billing.creditsAddedDesc', { credits }));
|
||||
}
|
||||
|
||||
export function showSubscriptionCancelledToast(
|
||||
t: ToastTranslations,
|
||||
periodEnd: string,
|
||||
) {
|
||||
gooeyToast.info(t('billing.subscriptionCancelledTitle'), { description: t('billing.subscriptionCancelledDesc', { periodEnd }) });
|
||||
}
|
||||
|
||||
export function showPaymentFailedToast(t: ToastTranslations) {
|
||||
toast.error(t('billing.paymentFailedTitle'), t('billing.paymentFailedDesc'));
|
||||
}
|
||||
|
||||
export function showDailyLimitReachedToast(t: ToastTranslations, limit: number) {
|
||||
toast.error(t('billing.dailyLimitReachedTitle'), t('billing.dailyLimitReachedDesc', { limit }));
|
||||
}
|
||||
|
||||
export function showLowCreditsToast(t: ToastTranslations, remaining: number) {
|
||||
toast.warning(t('billing.lowCreditsTitle'), t('billing.lowCreditsDesc', { remaining }));
|
||||
}
|
||||
|
||||
export function showTopUpToast(t: ToastTranslations) {
|
||||
toast.success(t('billing.topUp'));
|
||||
}
|
||||
|
||||
export function showUpgradeToast(t: ToastTranslations) {
|
||||
toast.success(t('billing.upgrade'));
|
||||
}
|
||||
|
||||
export function showManageToast(t: ToastTranslations) {
|
||||
toast.success(t('billing.manage'));
|
||||
}
|
||||
|
||||
export function showRedirectingToCheckoutToast(t: ToastTranslations) {
|
||||
gooeyToast.info(t('billing.redirectingToCheckoutTitle'), { description: t('billing.redirectingToCheckoutDesc') });
|
||||
}
|
||||
|
||||
export function showOpeningPortalToast(t: ToastTranslations) {
|
||||
gooeyToast.info(t('billing.openingPortalTitle'), { description: t('billing.openingPortalDesc') });
|
||||
}
|
||||
|
||||
export function showTestGrantFailedToast(t: ToastTranslations) {
|
||||
toast.error(t('billing.testGrantFailedTitle'));
|
||||
}
|
||||
|
||||
export function showReconnectedToast(t: ToastTranslations) {
|
||||
toast.success(t('system.reconnected'));
|
||||
}
|
||||
|
||||
export function showConnectionLostToast(t: ToastTranslations) {
|
||||
toast.error(t('system.connectionLostTitle'), t('system.connectionLostDesc'));
|
||||
}
|
||||
|
||||
export function showCopiedToClipboardToast(t: ToastTranslations) {
|
||||
toast.success(t('system.copiedToClipboard'));
|
||||
}
|
||||
|
||||
export function showRenameEmptyToast(t: ToastTranslations) {
|
||||
toast.error(t('dashboard.renameEmptyTitle'), t('dashboard.renameEmptyDesc'));
|
||||
}
|
||||
|
||||
export function showRenameSuccessToast(t: ToastTranslations) {
|
||||
toast.success(t('dashboard.renameSuccess'));
|
||||
}
|
||||
|
||||
export function showRenameFailedToast(t: ToastTranslations) {
|
||||
toast.error(t('dashboard.renameFailed'));
|
||||
}
|
||||
|
||||
export function showDeleteSuccessToast(t: ToastTranslations) {
|
||||
toast.success(t('dashboard.deleteSuccess'));
|
||||
}
|
||||
|
||||
export function showDeleteFailedToast(t: ToastTranslations) {
|
||||
toast.error(t('dashboard.deleteFailed'));
|
||||
}
|
||||
|
||||
export function getToastTranslations() {
|
||||
const t = useTranslations('toasts');
|
||||
return t as ToastTranslations;
|
||||
}
|
||||
|
||||
@@ -4,11 +4,3 @@ import { twMerge } from "tailwind-merge"
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
|
||||
/** Credits / Preise: Werte sind Euro-Cent (siehe PRD, Manifest). */
|
||||
export function formatEurFromCents(cents: number) {
|
||||
return new Intl.NumberFormat("de-DE", {
|
||||
style: "currency",
|
||||
currency: "EUR",
|
||||
}).format(cents / 100)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user