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:
2026-04-01 18:16:52 +02:00
parent 6ce1d4a82e
commit 79d9092d43
44 changed files with 1385 additions and 507 deletions

View File

@@ -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;
}