feature: localization's first implmentation done. all frontend hardcoded text is now localized.

This commit is contained in:
2026-06-20 14:51:43 +03:30
parent 284fbd08aa
commit b2f4dfa4ca
52 changed files with 3306 additions and 1041 deletions

View File

@@ -1,6 +1,7 @@
'use client';
import { useEffect, useState } from 'react';
import { useTranslations } from 'next-intl';
import { Link, useRouter } from '@/i18n/navigation';
import { useAuth } from '@/lib/hooks/useAuth';
import { authApi } from '@/lib/api/auth';
@@ -9,14 +10,16 @@ import { Toast } from '@/components/ui/shared/Toast';
import type { SubscriptionAlertData } from '@/types/subscription';
const PLAN_OPTIONS = [
{ id: 'solo', name: 'Solo', maxUsers: 1, price: 19 },
{ id: 'small', name: 'Small', maxUsers: 5, price: 49 },
{ id: 'medium', name: 'Medium', maxUsers: 10, price: 89 },
{ id: 'large', name: 'Large', maxUsers: 15, price: 129 },
{ id: 'enterprise', name: 'Enterprise', maxUsers: null, price: 199 },
{ id: 'solo', nameKey: 'planSolo' as const, maxUsers: 1, price: 19 },
{ id: 'small', nameKey: 'planSmall' as const, maxUsers: 5, price: 49 },
{ id: 'medium', nameKey: 'planMedium' as const, maxUsers: 10, price: 89 },
{ id: 'large', nameKey: 'planLarge' as const, maxUsers: 15, price: 129 },
{ id: 'enterprise', nameKey: 'planEnterprise' as const, maxUsers: null, price: 199 },
] as const;
export default function SubscriptionsSettingsPage() {
const t = useTranslations('settings');
const tCommon = useTranslations('common');
const { currentOrganization } = useAuth();
const router = useRouter();
const [alert, setAlert] = useState<SubscriptionAlertData | null>(null);
@@ -38,13 +41,13 @@ export default function SubscriptionsSettingsPage() {
if (!currentOrganization) {
return (
<p className="text-text-secondary text-sm">Loading...</p>
<p className="text-text-secondary text-sm">{tCommon('loadingEllipsis')}</p>
);
}
if (!currentOrganization.isOwner) {
return (
<p className="text-text-secondary text-sm">Redirecting...</p>
<p className="text-text-secondary text-sm">{tCommon('redirecting')}</p>
);
}
@@ -75,55 +78,51 @@ export default function SubscriptionsSettingsPage() {
href="/today"
className="text-sm text-primary hover:opacity-90"
>
Back to app
{tCommon('backToApp')}
</Link>
<h1 className="text-2xl font-semibold text-text-primary mt-4">Subscriptions</h1>
<h1 className="text-2xl font-semibold text-text-primary mt-4">{t('subscriptionsTitle')}</h1>
<p className="text-text-secondary text-sm mt-2">
Your DyoLink workspace plan and seats for{' '}
<span className="text-text-primary font-medium">{currentOrganization.name}</span>.
Clinic and lab income tracking stays under the sidebar{' '}
<span className="text-text-primary">Billing</span> tab.
{t('subscriptionsSubtitle', { orgName: currentOrganization.name })}
</p>
</div>
<div className="surface-card p-6 space-y-4">
{!hasActiveSubscription && (
<div className="rounded-[var(--radius-md)] border border-amber-500/30 bg-amber-500/10 p-4">
<p className="text-sm text-amber-200">
This organization has no active subscription. Select a plan below to start
the purchase process.
</p>
<p className="text-sm text-amber-200">{t('noSubscriptionNotice')}</p>
</div>
)}
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-5">
<div>
<p className="text-xs text-text-muted uppercase tracking-wide">Current plan</p>
<p className="text-xs text-text-muted uppercase tracking-wide">{t('currentPlan')}</p>
<p className="text-lg font-medium text-text-primary capitalize">
{plan?.name ?? '—'}
</p>
</div>
<div>
<p className="text-xs text-text-muted uppercase tracking-wide">Plan price</p>
<p className="text-xs text-text-muted uppercase tracking-wide">{t('planPrice')}</p>
<p className="text-lg font-medium text-text-primary">
{typeof plan?.price === 'number' ? `$${plan.price}` : '—'}
</p>
</div>
<div>
<p className="text-xs text-text-muted uppercase tracking-wide">Seats used</p>
<p className="text-xs text-text-muted uppercase tracking-wide">{t('seatsUsed')}</p>
<p className="text-lg font-medium text-text-primary">
{typeof seatsUsed === 'number' ? seatsUsed : '—'}
{typeof maxUsers === 'number' ? ` / ${isUnlimited ? 'Unlimited' : maxUsers}` : ''}
{typeof maxUsers === 'number'
? ` / ${isUnlimited ? t('unlimited') : maxUsers}`
: ''}
</p>
</div>
<div>
<p className="text-xs text-text-muted uppercase tracking-wide">Seats remaining</p>
<p className="text-xs text-text-muted uppercase tracking-wide">{t('seatsRemaining')}</p>
<p className="text-lg font-medium text-text-primary">
{isUnlimited ? 'Unlimited' : seatsRemaining ?? '—'}
{isUnlimited ? t('unlimited') : seatsRemaining ?? '—'}
</p>
</div>
<div>
<p className="text-xs text-text-muted uppercase tracking-wide">Days remaining</p>
<p className="text-xs text-text-muted uppercase tracking-wide">{t('daysRemaining')}</p>
<p className={`text-lg font-medium ${planDayTone}`}>
{daysUntilPlanEnd ?? '—'}
</p>
@@ -133,27 +132,24 @@ export default function SubscriptionsSettingsPage() {
{alert?.showWarning && (
<div className="text-sm text-text-secondary space-y-1">
{alert.noActiveSubscription && (
<p>No active subscription for this organization.</p>
<p>{t('noActiveSubscription')}</p>
)}
{alert.trialExpired && (
<p>Trial period has ended. Choose a plan when checkout is available.</p>
<p>{t('trialEnded')}</p>
)}
{!alert.trialExpired && alert.trialEndingSoon && (
<p>
Trial ends in {alert.daysUntilTrialEnd ?? '—'} day(s).
{t('trialEndsIn', { days: alert.daysUntilTrialEnd ?? '—' })}
</p>
)}
{!alert.trialExpired && !alert.trialEndingSoon && alert.seatsLow && (
<p>Seat usage is high for this organization.</p>
<p>{t('seatsLow')}</p>
)}
</div>
)}
<div className="space-y-3 pt-2">
<p className="text-sm text-text-secondary">
Choose a plan to continue. Purchase integration is not active yet, so this
currently prepares the selection step only.
</p>
<p className="text-sm text-text-secondary">{t('choosePlanIntro')}</p>
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
{PLAN_OPTIONS.map((option) => {
const selected = selectedPlanId === option.id;
@@ -168,11 +164,15 @@ export default function SubscriptionsSettingsPage() {
: 'border-border hover:border-border-strong'
}`}
>
<p className="text-base font-medium text-text-primary">{option.name}</p>
<p className="text-base font-medium text-text-primary">{t(option.nameKey)}</p>
<p className="text-sm text-text-secondary mt-1">
{option.maxUsers == null ? 'Unlimited seats' : `${option.maxUsers} seats`}
{option.maxUsers == null
? t('unlimitedSeats')
: t('seatsCount', { n: option.maxUsers })}
</p>
<p className="text-sm text-text-secondary mt-1">
{t('pricePerMonth', { price: option.price })}
</p>
<p className="text-sm text-text-secondary mt-1">${option.price} / month</p>
</button>
);
})}
@@ -181,13 +181,13 @@ export default function SubscriptionsSettingsPage() {
type="button"
variant="primary"
onClick={() => {
const selectedPlanLabel = selectedPlan?.name ?? 'the selected plan';
setPurchaseNotice(
`Purchase flow will be enabled soon. ${selectedPlanLabel} is selected and ready for checkout setup.`,
);
const selectedPlanLabel = selectedPlan
? t(selectedPlan.nameKey)
: t('planSolo');
setPurchaseNotice(t('purchaseNotice', { plan: selectedPlanLabel }));
}}
>
Start purchase process
{t('startPurchase')}
</Button>
</div>
</div>