feat(FN-204): i18n pricing/subscription/payment surfaces
Some checks failed
Sync dev → Gitea / Mirror dev to Gitea (push) Has been cancelled
Some checks failed
Sync dev → Gitea / Mirror dev to Gitea (push) Has been cancelled
Commits merged: - feat(FN-204): i18n pricing/subscription/payment surfaces Files changed: .../web/src/components/payment/payment-content.tsx | 11 +- apps/web/src/lib/i18n.ts | 17 ++- apps/web/src/messages/en.json | 27 ++++- apps/web/src/messages/tr.json | 27 ++++- .../src/routes/dashboard/subscription/index.tsx | 4 +- apps/web/src/routes/pricing.tsx | 127 +++++++++++---------- 6 files changed, 135 insertions(+), 78 deletions(-) Fusion-Task-Id: FN-204
This commit is contained in:
@@ -363,10 +363,7 @@ export function PaymentContent({ planKey, period, brandIds }: PaymentContentProp
|
||||
{/* iyzico Credit Card Tab */}
|
||||
<TabsContent value="iyzico" className="space-y-4 pt-4">
|
||||
<div className="rounded-lg border bg-muted/30 p-4">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
3D Secure ile guvenli odeme. iyzico altyapisi kullanilmaktadir. Butona
|
||||
tikladiginizda iyzico odeme sayfasina yonlendirileceksiniz.
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground">{t("payment.iyzicoTrustNotice")}</p>
|
||||
</div>
|
||||
<Button
|
||||
className="w-full"
|
||||
@@ -444,7 +441,7 @@ export function PaymentContent({ planKey, period, brandIds }: PaymentContentProp
|
||||
onClick={handleEftProceed}
|
||||
disabled={eftMutation.isPending}
|
||||
>
|
||||
{eftMutation.isPending ? t("payment.processingPayment") : "EFT/Havale Yaptim"}
|
||||
{eftMutation.isPending ? t("payment.processingPayment") : t("payment.eftPaid")}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
@@ -522,7 +519,7 @@ export function PaymentContent({ planKey, period, brandIds }: PaymentContentProp
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<CheckCircle2 className="h-5 w-5 text-green-500" />
|
||||
<span className="text-sm">EFT/Havale kaydı oluşturuldu</span>
|
||||
<span className="text-sm">{t("payment.eftStatus.created")}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
{uploadedFile ? (
|
||||
@@ -530,7 +527,7 @@ export function PaymentContent({ planKey, period, brandIds }: PaymentContentProp
|
||||
) : (
|
||||
<Clock className="h-5 w-5 text-muted-foreground" />
|
||||
)}
|
||||
<span className="text-sm">Dekont yüklendi</span>
|
||||
<span className="text-sm">{t("payment.eftStatus.receiptUploaded")}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<Clock className="h-5 w-5 text-muted-foreground" />
|
||||
|
||||
@@ -52,17 +52,26 @@ function getNestedValue(obj: unknown, path: string): string {
|
||||
return path;
|
||||
}
|
||||
|
||||
export function t(key: string): string {
|
||||
function interpolate(template: string, params?: Record<string, string | number>): string {
|
||||
if (!params) return template;
|
||||
return template.replace(/\{(\w+)\}/g, (_, key) => {
|
||||
return key in params ? String(params[key]) : `{${key}}`;
|
||||
});
|
||||
}
|
||||
|
||||
export function t(key: string, params?: Record<string, string | number>): string {
|
||||
const locale = useI18nStore.getState().locale;
|
||||
return getNestedValue(messages[locale], key);
|
||||
const template = getNestedValue(messages[locale], key);
|
||||
return interpolate(template, params);
|
||||
}
|
||||
|
||||
export function useTranslation() {
|
||||
const locale = useI18nStore((state) => state.locale);
|
||||
const setLocale = useI18nStore((state) => state.setLocale);
|
||||
|
||||
const translate = (key: string): string => {
|
||||
return getNestedValue(messages[locale], key);
|
||||
const translate = (key: string, params?: Record<string, string | number>): string => {
|
||||
const template = getNestedValue(messages[locale], key);
|
||||
return interpolate(template, params);
|
||||
};
|
||||
|
||||
return { t: translate, locale, setLocale };
|
||||
|
||||
@@ -25,7 +25,9 @@
|
||||
"yearly": "Yearly",
|
||||
"trySymbol": "₺",
|
||||
"perMonth": "/mo",
|
||||
"perYear": "/yr"
|
||||
"perYear": "/yr",
|
||||
"login": "Log In",
|
||||
"register": "Sign Up"
|
||||
},
|
||||
"nav": {
|
||||
"search": "Search",
|
||||
@@ -149,7 +151,8 @@
|
||||
"schemaViewer": "Schema viewer",
|
||||
"prioritySupport": "Priority support",
|
||||
"oemSearch": "OEM parts search",
|
||||
"allBrands": "All brands"
|
||||
"allBrands": "All brands",
|
||||
"brandSelection": "{count} brands selection"
|
||||
},
|
||||
"plans": {
|
||||
"brand1": {
|
||||
@@ -206,7 +209,8 @@
|
||||
"trustRefund": "7-day refund guarantee",
|
||||
"paymentTrustSSL": "256-bit SSL",
|
||||
"paymentTrustProvider": "Iyzico infrastructure",
|
||||
"paymentTrustKVKK": "KVKK compliant"
|
||||
"paymentTrustKVKK": "KVKK compliant",
|
||||
"trialDaysLeft": "{days} days left"
|
||||
},
|
||||
"payment": {
|
||||
"title": "Payment",
|
||||
@@ -243,7 +247,13 @@
|
||||
"eftConfirmationDescription": "Your EFT/Wire receipt has been received. Your subscription will be activated after approval.",
|
||||
"goToDashboard": "Go to Dashboard",
|
||||
"initializeFailed": "Payment initialization failed. Please try again.",
|
||||
"processingPayment": "Processing payment..."
|
||||
"processingPayment": "Processing payment...",
|
||||
"iyzicoTrustNotice": "Secure payment with 3D Secure. Powered by iyzico. You will be redirected to the iyzico payment page when you click the button.",
|
||||
"eftPaid": "I Made EFT/Wire Transfer",
|
||||
"eftStatus": {
|
||||
"created": "EFT/Wire transfer record created",
|
||||
"receiptUploaded": "Receipt uploaded"
|
||||
}
|
||||
},
|
||||
"billing": {
|
||||
"title": "Payment History",
|
||||
@@ -356,6 +366,15 @@
|
||||
"fileTooBig": "File size too large. Maximum 5MB allowed.",
|
||||
"invalidFileType": "Invalid file type."
|
||||
},
|
||||
"pricing": {
|
||||
"title": "Pricing",
|
||||
"subtitle": "Choose the plan that fits your needs.",
|
||||
"metaDescription": "Chassis number and OEM parts search plans starting from 200 TL/month. Try free for 30 days.",
|
||||
"popular": "Popular",
|
||||
"orYearly": "or {price} TL/year",
|
||||
"getStarted": "Get Started",
|
||||
"pageTitle": "Pricing — Sase.tr | Chassis Search Plans"
|
||||
},
|
||||
"language": {
|
||||
"tr": "Türkçe",
|
||||
"en": "English",
|
||||
|
||||
@@ -25,7 +25,9 @@
|
||||
"yearly": "Yıllık",
|
||||
"trySymbol": "₺",
|
||||
"perMonth": "/ay",
|
||||
"perYear": "/yıl"
|
||||
"perYear": "/yıl",
|
||||
"login": "Giriş Yap",
|
||||
"register": "Kayıt Ol"
|
||||
},
|
||||
"nav": {
|
||||
"search": "Arama",
|
||||
@@ -149,7 +151,8 @@
|
||||
"schemaViewer": "Şema görüntüleyici",
|
||||
"prioritySupport": "Öncelikli destek",
|
||||
"oemSearch": "OEM parça arama",
|
||||
"allBrands": "Tüm markalar"
|
||||
"allBrands": "Tüm markalar",
|
||||
"brandSelection": "{count} marka seçimi"
|
||||
},
|
||||
"plans": {
|
||||
"brand1": {
|
||||
@@ -206,7 +209,8 @@
|
||||
"trustRefund": "7 gün iade garantisi",
|
||||
"paymentTrustSSL": "256-bit SSL",
|
||||
"paymentTrustProvider": "Iyzico altyapısı",
|
||||
"paymentTrustKVKK": "KVKK uyumlu"
|
||||
"paymentTrustKVKK": "KVKK uyumlu",
|
||||
"trialDaysLeft": "{days} gün kaldı"
|
||||
},
|
||||
"payment": {
|
||||
"title": "Ödeme",
|
||||
@@ -243,7 +247,13 @@
|
||||
"eftConfirmationDescription": "EFT/Havale dekontunuz alındı. Onay sonrası aboneliğiniz aktifleştirilecektir.",
|
||||
"goToDashboard": "Panele Git",
|
||||
"initializeFailed": "Ödeme başlatılamadı. Lütfen tekrar deneyin.",
|
||||
"processingPayment": "Ödeme işleniyor..."
|
||||
"processingPayment": "Ödeme işleniyor...",
|
||||
"iyzicoTrustNotice": "3D Secure ile güvenli ödeme. iyzico altyapısı kullanılmaktadır. Butona tıkladığınızda iyzico ödeme sayfasına yönlendirileceksiniz.",
|
||||
"eftPaid": "EFT/Havale Yaptım",
|
||||
"eftStatus": {
|
||||
"created": "EFT/Havale kaydı oluşturuldu",
|
||||
"receiptUploaded": "Dekont yüklendi"
|
||||
}
|
||||
},
|
||||
"billing": {
|
||||
"title": "Ödeme Geçmişi",
|
||||
@@ -356,6 +366,15 @@
|
||||
"fileTooBig": "Dosya boyutu çok büyük. Maksimum 5MB yükleyebilirsiniz.",
|
||||
"invalidFileType": "Geçersiz dosya türü."
|
||||
},
|
||||
"pricing": {
|
||||
"title": "Fiyatlandırma",
|
||||
"subtitle": "İhtiyacınıza uygun planı seçin.",
|
||||
"metaDescription": "200 TL/ay'dan başlayan şase numarası ve OEM parça sorgulama planları. 30 gün ücretsiz deneyin.",
|
||||
"popular": "Popüler",
|
||||
"orYearly": "veya {price} TL/yıl",
|
||||
"getStarted": "Başla",
|
||||
"pageTitle": "Fiyatlandırma — Sase.tr | Şase Sorgulama Planları"
|
||||
},
|
||||
"language": {
|
||||
"tr": "Türkçe",
|
||||
"en": "English",
|
||||
|
||||
@@ -381,7 +381,9 @@ function SubscriptionPage() {
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{t("subscription.currentPlan")}
|
||||
</span>
|
||||
<Badge className="bg-brand text-brand-foreground">Full Paket</Badge>
|
||||
<Badge className="bg-brand text-brand-foreground">
|
||||
{t("subscription.plans.full.name")}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm text-muted-foreground">
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { usePageMeta } from "@/hooks/use-page-meta";
|
||||
import { useTranslation } from "@/lib/i18n";
|
||||
import { Button } from "@sase/ui";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@sase/ui";
|
||||
import { Badge } from "@sase/ui";
|
||||
import { Link, createFileRoute } from "@tanstack/react-router";
|
||||
import { Check } from "lucide-react";
|
||||
|
||||
export const Route = createFileRoute("/pricing")({
|
||||
component: PricingPage,
|
||||
@@ -10,60 +12,57 @@ export const Route = createFileRoute("/pricing")({
|
||||
|
||||
const plans = [
|
||||
{
|
||||
name: "1 Marka",
|
||||
description: "Tek marka için yedek parça erişimi",
|
||||
priceMonthly: "200",
|
||||
priceYearly: "2.000",
|
||||
features: ["1 marka seçimi", "Sınırsız VIN arama", "Parça kataloğu", "Şema görüntüleyici"],
|
||||
key: "brand1",
|
||||
brandLimit: 1,
|
||||
priceMonthly: 20000,
|
||||
priceYearly: 200000,
|
||||
features: ["brandSelection", "vinSearch", "partsCatalog", "schemaViewer"],
|
||||
},
|
||||
{
|
||||
name: "2 Marka",
|
||||
description: "İki farklı marka için erişim",
|
||||
priceMonthly: "350",
|
||||
priceYearly: "3.500",
|
||||
key: "brand2",
|
||||
brandLimit: 2,
|
||||
priceMonthly: 35000,
|
||||
priceYearly: 350000,
|
||||
popular: true,
|
||||
features: [
|
||||
"2 marka seçimi",
|
||||
"Sınırsız VIN arama",
|
||||
"Parça kataloğu",
|
||||
"Şema görüntüleyici",
|
||||
"Öncelikli destek",
|
||||
],
|
||||
features: ["brandSelection", "vinSearch", "partsCatalog", "schemaViewer", "prioritySupport"],
|
||||
},
|
||||
{
|
||||
name: "3 Marka",
|
||||
description: "Üç marka için kapsamlı erişim",
|
||||
priceMonthly: "500",
|
||||
priceYearly: "5.000",
|
||||
features: [
|
||||
"3 marka seçimi",
|
||||
"Sınırsız VIN arama",
|
||||
"Parça kataloğu",
|
||||
"Şema görüntüleyici",
|
||||
"Öncelikli destek",
|
||||
],
|
||||
key: "brand3",
|
||||
brandLimit: 3,
|
||||
priceMonthly: 50000,
|
||||
priceYearly: 500000,
|
||||
features: ["brandSelection", "vinSearch", "partsCatalog", "schemaViewer", "prioritySupport"],
|
||||
},
|
||||
{
|
||||
name: "Full Paket",
|
||||
description: "Tüm markalara sınırsız erişim",
|
||||
priceMonthly: "999",
|
||||
priceYearly: "9.990",
|
||||
key: "full",
|
||||
brandLimit: 999,
|
||||
priceMonthly: 99900,
|
||||
priceYearly: 999000,
|
||||
features: [
|
||||
"Tüm markalar",
|
||||
"Sınırsız VIN arama",
|
||||
"Parça kataloğu",
|
||||
"Şema görüntüleyici",
|
||||
"Öncelikli destek",
|
||||
"OEM parça arama",
|
||||
"allBrands",
|
||||
"vinSearch",
|
||||
"partsCatalog",
|
||||
"schemaViewer",
|
||||
"prioritySupport",
|
||||
"oemSearch",
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
function formatTRY(amount: number): string {
|
||||
return new Intl.NumberFormat("tr-TR", {
|
||||
style: "currency",
|
||||
currency: "TRY",
|
||||
minimumFractionDigits: 2,
|
||||
}).format(amount / 100);
|
||||
}
|
||||
|
||||
function PricingPage() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
usePageMeta({
|
||||
title: "Fiyatlandırma — Sase.tr | Şase Sorgulama Planları",
|
||||
description:
|
||||
"200 TL/ay'dan başlayan şase numarası ve OEM parça sorgulama planları. 30 gün ücretsiz deneyin.",
|
||||
title: t("pricing.pageTitle"),
|
||||
description: t("pricing.metaDescription"),
|
||||
canonical: "https://sase.tr/pricing",
|
||||
});
|
||||
|
||||
@@ -76,10 +75,10 @@ function PricingPage() {
|
||||
</Link>
|
||||
<div className="flex items-center gap-4">
|
||||
<Link to="/login">
|
||||
<Button variant="ghost">Giriş Yap</Button>
|
||||
<Button variant="ghost">{t("common.login")}</Button>
|
||||
</Link>
|
||||
<Link to="/register">
|
||||
<Button>Kayıt Ol</Button>
|
||||
<Button>{t("common.register")}</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
@@ -87,40 +86,52 @@ function PricingPage() {
|
||||
|
||||
<main id="main-content" className="container mx-auto px-4 py-24">
|
||||
<div className="text-center">
|
||||
<h1 className="text-4xl font-bold">Fiyatlandırma</h1>
|
||||
<p className="mt-4 text-lg text-muted-foreground">İhtiyacınıza uygun planı seçin.</p>
|
||||
<h1 className="text-4xl font-bold">{t("pricing.title")}</h1>
|
||||
<p className="mt-4 text-lg text-muted-foreground">{t("pricing.subtitle")}</p>
|
||||
</div>
|
||||
|
||||
<div className="mt-12 grid gap-6 md:grid-cols-2 lg:grid-cols-4">
|
||||
{plans.map((plan) => (
|
||||
<Card
|
||||
key={plan.name}
|
||||
key={plan.key}
|
||||
className={plan.popular ? "border-primary shadow-lg relative" : ""}
|
||||
>
|
||||
{plan.popular && (
|
||||
<Badge className="absolute -top-3 left-1/2 -translate-x-1/2">Popüler</Badge>
|
||||
<Badge className="absolute -top-3 left-1/2 -translate-x-1/2">
|
||||
{t("subscription.popular")}
|
||||
</Badge>
|
||||
)}
|
||||
<CardHeader>
|
||||
<CardTitle>{plan.name}</CardTitle>
|
||||
<CardDescription>{plan.description}</CardDescription>
|
||||
<CardTitle>{t(`subscription.plans.${plan.key}.name`)}</CardTitle>
|
||||
<CardDescription>{t(`subscription.plans.${plan.key}.description`)}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div>
|
||||
<span className="text-3xl font-bold">{plan.priceMonthly} TL</span>
|
||||
<span className="text-muted-foreground">/ay</span>
|
||||
<span className="text-3xl font-bold">{formatTRY(plan.priceMonthly)}</span>
|
||||
<span className="text-muted-foreground">{t("common.perMonth")}</span>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">veya {plan.priceYearly} TL/yıl</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t("pricing.orYearly", {
|
||||
price: formatTRY(plan.priceYearly).replace(/[₺\s]/g, "").trim(),
|
||||
})}
|
||||
</p>
|
||||
<ul className="space-y-2 text-sm">
|
||||
{plan.features.map((f) => (
|
||||
<li key={f} className="flex items-center gap-2">
|
||||
<span className="text-primary">✓</span>
|
||||
{f}
|
||||
</li>
|
||||
))}
|
||||
{plan.features.map((f) => {
|
||||
const featureLabel =
|
||||
f === "brandSelection"
|
||||
? t(`subscription.features.${f}`, { count: plan.brandLimit })
|
||||
: t(`subscription.features.${f}`);
|
||||
return (
|
||||
<li key={f} className="flex items-center gap-2">
|
||||
<Check className="h-4 w-4 text-primary" />
|
||||
{featureLabel}
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
<Link to="/register">
|
||||
<Button className="w-full" variant={plan.popular ? "default" : "outline"}>
|
||||
Başla
|
||||
{t("pricing.getStarted")}
|
||||
</Button>
|
||||
</Link>
|
||||
</CardContent>
|
||||
|
||||
Reference in New Issue
Block a user