fixed paddle locale

This commit is contained in:
Codex Agent
2025-10-27 21:05:06 +01:00
parent 5432456ffd
commit f29067f570
6 changed files with 146 additions and 66 deletions

View File

@@ -379,8 +379,9 @@
"title": "Konto", "title": "Konto",
"subtitle": "Anmelden oder Registrieren", "subtitle": "Anmelden oder Registrieren",
"description": "Erstellen Sie ein Konto oder melden Sie sich an, um mit dem Kauf fortzufahren.", "description": "Erstellen Sie ein Konto oder melden Sie sich an, um mit dem Kauf fortzufahren.",
"already_logged_in_title": "Bereits eingeloggt", "already_logged_in_title": "Willkommen zurück!",
"already_logged_in_desc": "Sie sind bereits als {email} eingeloggt.", "already_logged_in_body": "Du bist bereits mit <strong>{{email}}</strong> angemeldet. Wir haben deine Daten übernommen, damit du ohne Umwege weitermachen kannst.",
"already_logged_in_hint": "Möchtest du ein anderes Konto nutzen? Melde dich kurz ab und starte den Checkout anschließend erneut.",
"next_to_payment": "Weiter zur Zahlung", "next_to_payment": "Weiter zur Zahlung",
"switch_to_register": "Registrieren", "switch_to_register": "Registrieren",
"switch_to_login": "Anmelden", "switch_to_login": "Anmelden",
@@ -400,7 +401,7 @@
"activate_package": "Paket aktivieren", "activate_package": "Paket aktivieren",
"loading_payment": "Zahlungsdaten werden geladen...", "loading_payment": "Zahlungsdaten werden geladen...",
"secure_payment_desc": "Sichere Zahlung über Paddle.", "secure_payment_desc": "Sichere Zahlung über Paddle.",
"paddle_intro": "Wir öffnen den Paddle-Checkout direkt hier im Wizard, damit du im Ablauf bleibst.", "paddle_intro": "Starte den Paddle-Checkout direkt hier im Wizard ganz ohne Seitenwechsel.",
"paddle_preparing": "Paddle-Checkout wird vorbereitet…", "paddle_preparing": "Paddle-Checkout wird vorbereitet…",
"paddle_overlay_ready": "Der Paddle-Checkout läuft jetzt in einem Overlay. Schließe die Zahlung dort ab und kehre anschließend hierher zurück.", "paddle_overlay_ready": "Der Paddle-Checkout läuft jetzt in einem Overlay. Schließe die Zahlung dort ab und kehre anschließend hierher zurück.",
"paddle_ready": "Paddle-Checkout wurde in einem neuen Tab geöffnet. Schließe die Zahlung dort ab und kehre dann hierher zurück.", "paddle_ready": "Paddle-Checkout wurde in einem neuen Tab geöffnet. Schließe die Zahlung dort ab und kehre dann hierher zurück.",
@@ -441,22 +442,24 @@
"confirmation_step": { "confirmation_step": {
"title": "Bestätigung", "title": "Bestätigung",
"subtitle": "Alles erledigt!", "subtitle": "Alles erledigt!",
"description": "Ihr Paket ist aktiviert. Überprüfen Sie Ihre E-Mail für Details.", "description": "Dein Paket ist aktiviert. Prüfe deine E-Mails für Details.",
"welcome": "Willkommen bei Fotospiel", "welcome": "Danke, dass du FotoSpiel gewählt hast!",
"package_summary": "Dein Paket <strong>{name}</strong> ist jetzt freigeschaltet. Du kannst sofort mit der Einrichtung loslegen.",
"email_followup": "Wir haben dir gerade alle Details per E-Mail geschickt inklusive Rechnung und den nächsten Schritten.",
"package_activated": "Ihr Paket '{name}' ist aktiviert.", "package_activated": "Ihr Paket '{name}' ist aktiviert.",
"email_sent": "Wir haben Ihnen eine Bestätigungs-E-Mail gesendet.", "email_sent": "Wir haben Ihnen eine Bestätigungs-E-Mail gesendet.",
"open_profile": "Profil öffnen", "open_profile": "Profil öffnen",
"to_admin": "Zum Admin-Bereich" "to_admin": "Zum Admin-Bereich"
}, },
"confirmation": { "confirmation": {
"welcome": "Willkommen bei Fotospiel", "welcome": "Danke, dass du FotoSpiel gewählt hast!",
"package_activated": "Ihr Paket '{name}' ist aktiviert.", "package_activated": "Dein Paket <strong>{name}</strong> ist jetzt freigeschaltet.",
"email_sent": "Wir haben Ihnen eine Bestätigungs-E-Mail gesendet.", "email_sent": "Wir haben dir alle Details per E-Mail geschickt.",
"open_profile": "Profil öffnen", "open_profile": "Profil öffnen",
"to_admin": "Zum Admin-Bereich" "to_admin": "Zum Admin-Bereich"
}, },
"auth": { "auth": {
"already_logged_in": "Sie sind bereits als {email} eingeloggt.", "already_logged_in": "Du bist bereits mit {{email}} angemeldet.",
"switch_to_register": "Registrieren", "switch_to_register": "Registrieren",
"switch_to_login": "Anmelden", "switch_to_login": "Anmelden",
"continue_with_google": "Mit Google fortfahren", "continue_with_google": "Mit Google fortfahren",

View File

@@ -373,8 +373,9 @@
"title": "Account", "title": "Account",
"subtitle": "Login or Register", "subtitle": "Login or Register",
"description": "Create an account or log in to continue with your purchase.", "description": "Create an account or log in to continue with your purchase.",
"already_logged_in_title": "Already Logged In", "already_logged_in_title": "Welcome back!",
"already_logged_in_desc": "You are already logged in as {email}.", "already_logged_in_body": "You're already signed in as <strong>{{email}}</strong>. Your details are all set so you can continue without interruption.",
"already_logged_in_hint": "Need to switch accounts? Sign out briefly and restart the checkout.",
"next_to_payment": "Next to Payment", "next_to_payment": "Next to Payment",
"switch_to_register": "Register", "switch_to_register": "Register",
"switch_to_login": "Login", "switch_to_login": "Login",
@@ -394,7 +395,7 @@
"activate_package": "Activate Package", "activate_package": "Activate Package",
"loading_payment": "Payment data is loading...", "loading_payment": "Payment data is loading...",
"secure_payment_desc": "Secure payment with Paddle.", "secure_payment_desc": "Secure payment with Paddle.",
"paddle_intro": "We open Paddle's secure checkout directly inside this wizard so you never leave the flow.", "paddle_intro": "Launch the Paddle checkout right here in the wizard—no page changes required.",
"paddle_preparing": "Preparing Paddle checkout…", "paddle_preparing": "Preparing Paddle checkout…",
"paddle_overlay_ready": "Paddle checkout is running in a secure overlay. Complete the payment there and then continue here.", "paddle_overlay_ready": "Paddle checkout is running in a secure overlay. Complete the payment there and then continue here.",
"paddle_ready": "Paddle checkout opened in a new tab. Complete the payment and then continue here.", "paddle_ready": "Paddle checkout opened in a new tab. Complete the payment and then continue here.",
@@ -436,21 +437,23 @@
"title": "Confirmation", "title": "Confirmation",
"subtitle": "All Done!", "subtitle": "All Done!",
"description": "Your package is activated. Check your email for details.", "description": "Your package is activated. Check your email for details.",
"welcome": "Welcome to FotoSpiel", "welcome": "Thank you for choosing FotoSpiel!",
"package_summary": "Your <strong>{name}</strong> package is now active. You're ready to get everything set up.",
"email_followup": "We've just sent a confirmation email with your receipt and the next steps.",
"package_activated": "Your package '{name}' is activated.", "package_activated": "Your package '{name}' is activated.",
"email_sent": "We have sent you a confirmation email.", "email_sent": "We have sent you a confirmation email.",
"open_profile": "Open Profile", "open_profile": "Open Profile",
"to_admin": "To Admin Area" "to_admin": "To Admin Area"
}, },
"confirmation": { "confirmation": {
"welcome": "Welcome to FotoSpiel", "welcome": "Thank you for choosing FotoSpiel!",
"package_activated": "Your package '{name}' is activated.", "package_activated": "Your <strong>{name}</strong> package is active.",
"email_sent": "We have sent you a confirmation email.", "email_sent": "We've emailed you all the details.",
"open_profile": "Open Profile", "open_profile": "Open Profile",
"to_admin": "To Admin Area" "to_admin": "To Admin Area"
}, },
"auth": { "auth": {
"already_logged_in": "You are already logged in as {email}.", "already_logged_in": "You're already signed in as {{email}}.",
"switch_to_register": "Register", "switch_to_register": "Register",
"switch_to_login": "Login", "switch_to_login": "Login",
"continue_with_google": "Continue with Google", "continue_with_google": "Continue with Google",

View File

@@ -0,0 +1,20 @@
import { describe, expect, it } from 'vitest';
import { resolvePaddleLocale } from '../steps/PaymentStep';
describe('resolvePaddleLocale', () => {
it('returns short locale when given region-specific tag', () => {
expect(resolvePaddleLocale('de-DE')).toBe('de');
expect(resolvePaddleLocale('en-US')).toBe('en');
});
it('falls back to english when locale unsupported', () => {
expect(resolvePaddleLocale('jp')).toBe('en');
expect(resolvePaddleLocale('xx-YY')).toBe('en');
expect(resolvePaddleLocale(undefined)).toBe('en');
});
it('keeps supported locale codes untouched', () => {
expect(resolvePaddleLocale('fr')).toBe('fr');
expect(resolvePaddleLocale('es')).toBe('es');
});
});

View File

@@ -6,7 +6,7 @@ import { useCheckoutWizard } from "../WizardContext";
import type { GoogleProfilePrefill } from '../types'; import type { GoogleProfilePrefill } from '../types';
import LoginForm, { AuthUserPayload } from "../../../auth/LoginForm"; import LoginForm, { AuthUserPayload } from "../../../auth/LoginForm";
import RegisterForm, { RegisterSuccessPayload } from "../../../auth/RegisterForm"; import RegisterForm, { RegisterSuccessPayload } from "../../../auth/RegisterForm";
import { useTranslation } from 'react-i18next'; import { Trans, useTranslation } from 'react-i18next';
import toast from 'react-hot-toast'; import toast from 'react-hot-toast';
import { LoaderCircle } from "lucide-react"; import { LoaderCircle } from "lucide-react";
@@ -104,10 +104,20 @@ export const AuthStep: React.FC<AuthStepProps> = ({ privacyHtml, googleProfile,
if (isAuthenticated && authUser) { if (isAuthenticated && authUser) {
return ( return (
<div className="space-y-6"> <div className="space-y-6">
<Alert> <Alert className="border-primary/40 bg-primary/5">
<AlertTitle>{t('checkout.auth_step.already_logged_in_title')}</AlertTitle> <AlertTitle className="text-xl font-semibold">
<AlertDescription> {t('checkout.auth_step.already_logged_in_title')}
{t('checkout.auth_step.already_logged_in_desc', { email: authUser?.email || '' })} </AlertTitle>
<AlertDescription className="space-y-2 text-base leading-relaxed">
<Trans
t={t}
i18nKey="checkout.auth_step.already_logged_in_body"
components={{ strong: <span className="font-semibold" /> }}
values={{ email: authUser?.email || '' }}
/>
<p className="text-sm text-muted-foreground">
{t('checkout.auth_step.already_logged_in_hint')}
</p>
</AlertDescription> </AlertDescription>
</Alert> </Alert>
<div className="flex justify-end"> <div className="flex justify-end">

View File

@@ -2,7 +2,7 @@ import React from "react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { useCheckoutWizard } from "../WizardContext"; import { useCheckoutWizard } from "../WizardContext";
import { useTranslation } from 'react-i18next'; import { Trans, useTranslation } from 'react-i18next';
interface ConfirmationStepProps { interface ConfirmationStepProps {
onViewProfile?: () => void; onViewProfile?: () => void;
@@ -28,13 +28,24 @@ export const ConfirmationStep: React.FC<ConfirmationStepProps> = ({ onViewProfil
window.location.href = '/event-admin'; window.location.href = '/event-admin';
}, [onGoToAdmin]); }, [onGoToAdmin]);
const packageName = selectedPackage?.name ?? '';
return ( return (
<div className="space-y-6"> <div className="space-y-6">
<Alert> <Alert className="border-primary/40 bg-primary/5">
<AlertTitle>{t('checkout.confirmation_step.welcome')}</AlertTitle> <AlertTitle className="text-xl font-semibold">
<AlertDescription> {t('checkout.confirmation_step.welcome')}
{t('checkout.confirmation_step.package_activated', { name: selectedPackage?.name || '' })} </AlertTitle>
{t('checkout.confirmation_step.email_sent')} <AlertDescription className="space-y-2 text-base leading-relaxed">
<Trans
t={t}
i18nKey="checkout.confirmation_step.package_summary"
components={{ strong: <span className="font-semibold" /> }}
values={{ name: packageName }}
/>
<p className="text-sm text-muted-foreground">
{t('checkout.confirmation_step.email_followup')}
</p>
</AlertDescription> </AlertDescription>
</Alert> </Alert>
<div className="flex flex-wrap gap-3 justify-end"> <div className="flex flex-wrap gap-3 justify-end">

View File

@@ -22,6 +22,26 @@ declare global {
} }
const PADDLE_SCRIPT_URL = 'https://cdn.paddle.com/paddle/v2/paddle.js'; const PADDLE_SCRIPT_URL = 'https://cdn.paddle.com/paddle/v2/paddle.js';
const PADDLE_SUPPORTED_LOCALES = ['en', 'de', 'fr', 'es', 'it', 'nl', 'pt', 'sv', 'da', 'fi', 'no'];
export function resolvePaddleLocale(rawLocale?: string | null): string {
if (!rawLocale) {
return 'en';
}
const normalized = rawLocale.toLowerCase();
if (PADDLE_SUPPORTED_LOCALES.includes(normalized)) {
return normalized;
}
const short = normalized.split('-')[0];
if (short && PADDLE_SUPPORTED_LOCALES.includes(short)) {
return short;
}
return 'en';
}
type PaddleEnvironment = 'sandbox' | 'production'; type PaddleEnvironment = 'sandbox' | 'production';
@@ -74,7 +94,7 @@ const PaddleCta: React.FC<{ onCheckout: () => Promise<void>; disabled: boolean;
const { t } = useTranslation('marketing'); const { t } = useTranslation('marketing');
return ( return (
<Button size="lg" className="w-full" disabled={disabled} onClick={onCheckout}> <Button size="lg" className="w-full sm:w-auto" disabled={disabled} onClick={onCheckout}>
{isProcessing && <LoaderCircle className="mr-2 h-4 w-4 animate-spin" />} {isProcessing && <LoaderCircle className="mr-2 h-4 w-4 animate-spin" />}
{t('checkout.payment_step.pay_with_paddle')} {t('checkout.payment_step.pay_with_paddle')}
</Button> </Button>
@@ -82,7 +102,7 @@ const PaddleCta: React.FC<{ onCheckout: () => Promise<void>; disabled: boolean;
}; };
export const PaymentStep: React.FC = () => { export const PaymentStep: React.FC = () => {
const { t } = useTranslation('marketing'); const { t, i18n } = useTranslation('marketing');
const { selectedPackage, nextStep, paddleConfig, authUser, setPaymentCompleted } = useCheckoutWizard(); const { selectedPackage, nextStep, paddleConfig, authUser, setPaymentCompleted } = useCheckoutWizard();
const [status, setStatus] = useState<PaymentStatus>('idle'); const [status, setStatus] = useState<PaymentStatus>('idle');
const [message, setMessage] = useState<string>(''); const [message, setMessage] = useState<string>('');
@@ -92,6 +112,11 @@ export const PaymentStep: React.FC = () => {
const eventCallbackRef = useRef<(event: any) => void>(); const eventCallbackRef = useRef<(event: any) => void>();
const checkoutContainerClass = 'paddle-checkout-container'; const checkoutContainerClass = 'paddle-checkout-container';
const paddleLocale = useMemo(() => {
const sourceLocale = i18n.language || (typeof document !== 'undefined' ? document.documentElement.lang : null);
return resolvePaddleLocale(sourceLocale ?? undefined);
}, [i18n.language]);
const isFree = useMemo(() => (selectedPackage ? Number(selectedPackage.price) <= 0 : false), [selectedPackage]); const isFree = useMemo(() => (selectedPackage ? Number(selectedPackage.price) <= 0 : false), [selectedPackage]);
const handleFreeActivation = async () => { const handleFreeActivation = async () => {
@@ -149,10 +174,11 @@ export const PaymentStep: React.FC = () => {
frameInitialHeight: '550', frameInitialHeight: '550',
frameStyle: 'width: 100%; min-width: 320px; background-color: transparent; border: none;', frameStyle: 'width: 100%; min-width: 320px; background-color: transparent; border: none;',
theme: 'light', theme: 'light',
locale: typeof document !== 'undefined' ? document.documentElement.lang ?? 'de' : 'de', locale: paddleLocale,
}, },
customData: { customData: {
package_id: String(selectedPackage.id), package_id: String(selectedPackage.id),
locale: paddleLocale,
}, },
}; };
@@ -182,6 +208,7 @@ export const PaymentStep: React.FC = () => {
}, },
body: JSON.stringify({ body: JSON.stringify({
package_id: selectedPackage.id, package_id: selectedPackage.id,
locale: paddleLocale,
}), }),
}); });
@@ -283,6 +310,7 @@ export const PaymentStep: React.FC = () => {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.info('[Checkout] Initializing Paddle.js', { environment, hasToken: Boolean(clientToken) }); console.info('[Checkout] Initializing Paddle.js', { environment, hasToken: Boolean(clientToken) });
} }
paddle.Initialize({ paddle.Initialize({
token: clientToken, token: clientToken,
checkout: { checkout: {
@@ -291,11 +319,12 @@ export const PaymentStep: React.FC = () => {
frameTarget: checkoutContainerClass, frameTarget: checkoutContainerClass,
frameInitialHeight: '550', frameInitialHeight: '550',
frameStyle: 'width: 100%; min-width: 320px; background-color: transparent; border: none;', frameStyle: 'width: 100%; min-width: 320px; background-color: transparent; border: none;',
locale: typeof document !== 'undefined' ? document.documentElement.lang ?? 'de' : 'de', locale: paddleLocale,
}, },
}, },
eventCallback: (event: any) => eventCallbackRef.current?.(event), eventCallback: (event: any) => eventCallbackRef.current?.(event),
}); });
inlineReady = true; inlineReady = true;
} }
@@ -313,7 +342,7 @@ export const PaymentStep: React.FC = () => {
return () => { return () => {
cancelled = true; cancelled = true;
}; };
}, [paddleConfig?.environment, paddleConfig?.client_token, setPaymentCompleted, t]); }, [paddleConfig?.environment, paddleConfig?.client_token, paddleLocale, setPaymentCompleted, t]);
useEffect(() => { useEffect(() => {
setPaymentCompleted(false); setPaymentCompleted(false);
@@ -345,42 +374,46 @@ export const PaymentStep: React.FC = () => {
} }
return ( return (
<div className="space-y-6"> <div className="space-y-4">
<p className="text-sm text-muted-foreground"> <div className="rounded-lg border bg-card p-6 shadow-sm">
{t('checkout.payment_step.paddle_intro')} <div className="space-y-4">
</p> {!inlineActive && (
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<p className="text-sm text-muted-foreground">
{t('checkout.payment_step.paddle_intro')}
</p>
<PaddleCta
onCheckout={startPaddleCheckout}
disabled={status === 'processing'}
isProcessing={status === 'processing'}
/>
</div>
)}
{status !== 'idle' && ( {status !== 'idle' && (
<Alert variant={status === 'error' ? 'destructive' : 'secondary'}> <Alert variant={status === 'error' ? 'destructive' : 'secondary'}>
<AlertTitle> <AlertTitle>
{status === 'processing' {status === 'processing'
? t('checkout.payment_step.status_processing_title') ? t('checkout.payment_step.status_processing_title')
: status === 'ready' : status === 'ready'
? t('checkout.payment_step.status_ready_title') ? t('checkout.payment_step.status_ready_title')
: status === 'error' : status === 'error'
? t('checkout.payment_step.status_error_title') ? t('checkout.payment_step.status_error_title')
: t('checkout.payment_step.status_info_title')} : t('checkout.payment_step.status_info_title')}
</AlertTitle> </AlertTitle>
<AlertDescription className="flex items-center gap-3"> <AlertDescription className="flex items-center gap-3">
<span>{message}</span> <span>{message}</span>
{status === 'processing' && <LoaderCircle className="h-4 w-4 animate-spin" />} {status === 'processing' && <LoaderCircle className="h-4 w-4 animate-spin" />}
</AlertDescription> </AlertDescription>
</Alert> </Alert>
)} )}
<div className="rounded-lg border bg-card p-6 shadow-sm space-y-4"> <div className={`${checkoutContainerClass} min-h-[360px]`} />
<div className={`${checkoutContainerClass} min-h-[200px]`} />
{!inlineActive && (
<PaddleCta
onCheckout={startPaddleCheckout}
disabled={status === 'processing'}
isProcessing={status === 'processing'}
/>
)}
<p className="text-xs text-muted-foreground"> <p className={`text-xs text-muted-foreground ${inlineActive ? 'text-center' : 'sm:text-right'}`}>
{t('checkout.payment_step.paddle_disclaimer')} {t('checkout.payment_step.paddle_disclaimer')}
</p> </p>
</div>
</div> </div>
</div> </div>
); );