feat(FN-204): i18n pricing/subscription/payment surfaces
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:
Fusion
2026-05-12 18:11:25 +00:00
parent 688db7c990
commit ba43df45bc
6 changed files with 135 additions and 78 deletions

View File

@@ -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" />

View File

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

View File

@@ -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",

View File

@@ -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",

View File

@@ -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">

View File

@@ -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">&#10003;</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>