feat(i18n): add billing namespace, fully internationalize billing pages

billing/page.tsx, billing/plans/page.tsx, billing/invoices/page.tsx
were hardcoded in English. Added zh/billing.json and en/billing.json
covering overview, plans, and invoices sections. Registered billing
namespace in i18n config.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
hailin 2026-03-07 01:46:05 -08:00
parent c1fb39c3c0
commit af1cae9da8
6 changed files with 187 additions and 57 deletions

View File

@ -2,6 +2,7 @@
import { useState } from 'react'; import { useState } from 'react';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { useTranslation } from 'react-i18next';
import Link from 'next/link'; import Link from 'next/link';
import { apiClient } from '@/infrastructure/api/api-client'; import { apiClient } from '@/infrastructure/api/api-client';
import { FileText, Download } from 'lucide-react'; import { FileText, Download } from 'lucide-react';
@ -38,6 +39,7 @@ const PAGE_SIZE = 20;
export default function InvoicesPage() { export default function InvoicesPage() {
const [page, setPage] = useState(0); const [page, setPage] = useState(0);
const { t } = useTranslation('billing');
const { data, isLoading } = useQuery<{ data: Invoice[]; total: number }>({ const { data, isLoading } = useQuery<{ data: Invoice[]; total: number }>({
queryKey: ['billing', 'invoices', page], queryKey: ['billing', 'invoices', page],
@ -51,7 +53,7 @@ export default function InvoicesPage() {
return ( return (
<div className="space-y-6"> <div className="space-y-6">
<h1 className="text-2xl font-bold flex items-center gap-2"> <h1 className="text-2xl font-bold flex items-center gap-2">
<FileText className="h-6 w-6" /> Invoices <FileText className="h-6 w-6" /> {t('invoices.title')}
</h1> </h1>
<div className="rounded-lg border bg-card overflow-hidden"> <div className="rounded-lg border bg-card overflow-hidden">
@ -59,22 +61,22 @@ export default function InvoicesPage() {
<table className="w-full text-sm"> <table className="w-full text-sm">
<thead className="bg-muted/50 text-muted-foreground"> <thead className="bg-muted/50 text-muted-foreground">
<tr> <tr>
<th className="text-left p-4 font-medium">Invoice #</th> <th className="text-left p-4 font-medium">{t('invoices.invoiceNumber')}</th>
<th className="text-left p-4 font-medium">Period</th> <th className="text-left p-4 font-medium">{t('invoices.period')}</th>
<th className="text-left p-4 font-medium">Amount</th> <th className="text-left p-4 font-medium">{t('invoices.amount')}</th>
<th className="text-left p-4 font-medium">Due Date</th> <th className="text-left p-4 font-medium">{t('invoices.dueDate')}</th>
<th className="text-left p-4 font-medium">Status</th> <th className="text-left p-4 font-medium">{t('invoices.status')}</th>
<th className="p-4" /> <th className="p-4" />
</tr> </tr>
</thead> </thead>
<tbody className="divide-y"> <tbody className="divide-y">
{isLoading ? ( {isLoading ? (
<tr> <tr>
<td colSpan={6} className="p-8 text-center text-muted-foreground">Loading...</td> <td colSpan={6} className="p-8 text-center text-muted-foreground">{t('invoices.loading')}</td>
</tr> </tr>
) : invoices.length === 0 ? ( ) : invoices.length === 0 ? (
<tr> <tr>
<td colSpan={6} className="p-8 text-center text-muted-foreground">No invoices found</td> <td colSpan={6} className="p-8 text-center text-muted-foreground">{t('invoices.noInvoices')}</td>
</tr> </tr>
) : ( ) : (
invoices.map((inv) => ( invoices.map((inv) => (
@ -102,7 +104,7 @@ export default function InvoicesPage() {
href={`/billing/invoices/${inv.id}?pay=1`} href={`/billing/invoices/${inv.id}?pay=1`}
className="text-xs bg-primary text-primary-foreground px-3 py-1.5 rounded-md hover:bg-primary/90" className="text-xs bg-primary text-primary-foreground px-3 py-1.5 rounded-md hover:bg-primary/90"
> >
Pay Now {t('invoices.payNow')}
</Link> </Link>
) : ( ) : (
<button className="p-1.5 rounded hover:bg-muted transition-colors" title="Download"> <button className="p-1.5 rounded hover:bg-muted transition-colors" title="Download">
@ -120,7 +122,7 @@ export default function InvoicesPage() {
{totalPages > 1 && ( {totalPages > 1 && (
<div className="flex items-center justify-between p-4 border-t"> <div className="flex items-center justify-between p-4 border-t">
<span className="text-sm text-muted-foreground"> <span className="text-sm text-muted-foreground">
Page {page + 1} of {totalPages} ({total} total) {t('invoices.pagination', { current: page + 1, total: totalPages, count: total })}
</span> </span>
<div className="flex gap-2"> <div className="flex gap-2">
<button <button
@ -128,14 +130,14 @@ export default function InvoicesPage() {
disabled={page === 0} disabled={page === 0}
className="px-3 py-1.5 rounded border text-sm disabled:opacity-50" className="px-3 py-1.5 rounded border text-sm disabled:opacity-50"
> >
Previous {t('invoices.previous')}
</button> </button>
<button <button
onClick={() => setPage((p) => p + 1)} onClick={() => setPage((p) => p + 1)}
disabled={page >= totalPages - 1} disabled={page >= totalPages - 1}
className="px-3 py-1.5 rounded border text-sm disabled:opacity-50" className="px-3 py-1.5 rounded border text-sm disabled:opacity-50"
> >
Next {t('invoices.next')}
</button> </button>
</div> </div>
</div> </div>

View File

@ -74,7 +74,7 @@ function formatTokens(n: number) {
} }
export default function BillingPage() { export default function BillingPage() {
const { t } = useTranslation(); const { t } = useTranslation('billing');
const { data: sub } = useQuery<Subscription>({ const { data: sub } = useQuery<Subscription>({
queryKey: ['billing', 'subscription'], queryKey: ['billing', 'subscription'],
@ -100,18 +100,18 @@ export default function BillingPage() {
return ( return (
<div className="space-y-6"> <div className="space-y-6">
<h1 className="text-2xl font-bold">Billing &amp; Subscription</h1> <h1 className="text-2xl font-bold">{t('overview.title')}</h1>
{/* Usage Warning Banner */} {/* Usage Warning Banner */}
{usedPct >= 80 && ( {usedPct >= 80 && (
<div className="flex items-center gap-3 p-4 rounded-lg bg-yellow-50 border border-yellow-200 dark:bg-yellow-900/20 dark:border-yellow-800"> <div className="flex items-center gap-3 p-4 rounded-lg bg-yellow-50 border border-yellow-200 dark:bg-yellow-900/20 dark:border-yellow-800">
<AlertCircle className="h-5 w-5 text-yellow-600 flex-shrink-0" /> <AlertCircle className="h-5 w-5 text-yellow-600 flex-shrink-0" />
<span className="text-sm text-yellow-800 dark:text-yellow-300"> <span className="text-sm text-yellow-800 dark:text-yellow-300">
You have used {usedPct.toFixed(0)}% of your monthly token quota.{' '} {t('overview.usageWarning', { pct: usedPct.toFixed(0) })}{' '}
{usedPct >= 100 ? 'Limit reached — agent tasks are blocked.' : 'Consider upgrading your plan.'} {usedPct >= 100 ? t('overview.limitReached') : t('overview.considerUpgrade')}
</span> </span>
<Link href="/billing/plans" className="ml-auto text-sm font-medium text-yellow-800 dark:text-yellow-300 underline whitespace-nowrap"> <Link href="/billing/plans" className="ml-auto text-sm font-medium text-yellow-800 dark:text-yellow-300 underline whitespace-nowrap">
Upgrade {t('overview.upgrade')}
</Link> </Link>
</div> </div>
)} )}
@ -122,10 +122,10 @@ export default function BillingPage() {
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<CreditCard className="h-5 w-5 text-muted-foreground" /> <CreditCard className="h-5 w-5 text-muted-foreground" />
<h2 className="font-semibold">Current Plan</h2> <h2 className="font-semibold">{t('overview.currentPlan')}</h2>
</div> </div>
<Link href="/billing/plans" className="text-sm text-primary flex items-center gap-1 hover:underline"> <Link href="/billing/plans" className="text-sm text-primary flex items-center gap-1 hover:underline">
Change <ChevronRight className="h-4 w-4" /> {t('overview.change')} <ChevronRight className="h-4 w-4" />
</Link> </Link>
</div> </div>
@ -137,25 +137,31 @@ export default function BillingPage() {
</div> </div>
{sub.trialEndsAt && sub.status === 'trialing' && ( {sub.trialEndsAt && sub.status === 'trialing' && (
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground">
Trial ends {new Date(sub.trialEndsAt).toLocaleDateString()} {t('overview.trialEnds', { date: new Date(sub.trialEndsAt).toLocaleDateString() })}
</p> </p>
)} )}
{sub.cancelAtPeriodEnd && ( {sub.cancelAtPeriodEnd && (
<p className="text-sm text-yellow-600 dark:text-yellow-400"> <p className="text-sm text-yellow-600 dark:text-yellow-400">
Cancels on {new Date(sub.currentPeriodEnd).toLocaleDateString()} {t('overview.cancelsOn', { date: new Date(sub.currentPeriodEnd).toLocaleDateString() })}
</p> </p>
)} )}
{sub.nextPlan && ( {sub.nextPlan && (
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground">
Downgrade to <strong>{sub.nextPlan.displayName}</strong> on {new Date(sub.currentPeriodEnd).toLocaleDateString()} {t('overview.downgradeTo', {
plan: sub.nextPlan.displayName,
date: new Date(sub.currentPeriodEnd).toLocaleDateString(),
})}
</p> </p>
)} )}
<p className="text-xs text-muted-foreground"> <p className="text-xs text-muted-foreground">
Period: {new Date(sub.currentPeriodStart).toLocaleDateString()} {new Date(sub.currentPeriodEnd).toLocaleDateString()} {t('overview.period', {
start: new Date(sub.currentPeriodStart).toLocaleDateString(),
end: new Date(sub.currentPeriodEnd).toLocaleDateString(),
})}
</p> </p>
</div> </div>
) : ( ) : (
<p className="text-muted-foreground text-sm">No active subscription</p> <p className="text-muted-foreground text-sm">{t('overview.noSubscription')}</p>
)} )}
</div> </div>
@ -163,15 +169,17 @@ export default function BillingPage() {
<div className="rounded-lg border bg-card p-6 space-y-4"> <div className="rounded-lg border bg-card p-6 space-y-4">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Zap className="h-5 w-5 text-muted-foreground" /> <Zap className="h-5 w-5 text-muted-foreground" />
<h2 className="font-semibold">This Month&apos;s Token Usage</h2> <h2 className="font-semibold">{t('overview.tokenUsage')}</h2>
</div> </div>
{quota ? ( {quota ? (
<div className="space-y-3"> <div className="space-y-3">
<div className="flex justify-between text-sm"> <div className="flex justify-between text-sm">
<span className="font-medium">{formatTokens(quota.usedTokens)} used</span> <span className="font-medium">{t('overview.tokensUsed', { used: formatTokens(quota.usedTokens) })}</span>
<span className="text-muted-foreground"> <span className="text-muted-foreground">
{quota.limitTokens === -1 ? 'Unlimited' : `${formatTokens(quota.limitTokens)} limit`} {quota.limitTokens === -1
? t('overview.unlimited')
: t('overview.tokensLimit', { limit: formatTokens(quota.limitTokens) })}
</span> </span>
</div> </div>
{quota.limitTokens !== -1 && ( {quota.limitTokens !== -1 && (
@ -180,12 +188,12 @@ export default function BillingPage() {
<div className="flex justify-between text-xs text-muted-foreground"> <div className="flex justify-between text-xs text-muted-foreground">
<span>{usedPct.toFixed(1)}%</span> <span>{usedPct.toFixed(1)}%</span>
{quota.overageAllowed && usedPct > 100 && ( {quota.overageAllowed && usedPct > 100 && (
<span className="text-yellow-600">Overage charges apply</span> <span className="text-yellow-600">{t('overview.overageApply')}</span>
)} )}
</div> </div>
</div> </div>
) : ( ) : (
<p className="text-muted-foreground text-sm">Loading usage data...</p> <p className="text-muted-foreground text-sm">{t('overview.loadingUsage')}</p>
)} )}
</div> </div>
</div> </div>
@ -195,15 +203,15 @@ export default function BillingPage() {
<div className="flex items-center justify-between p-6 border-b"> <div className="flex items-center justify-between p-6 border-b">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<TrendingUp className="h-5 w-5 text-muted-foreground" /> <TrendingUp className="h-5 w-5 text-muted-foreground" />
<h2 className="font-semibold">Recent Invoices</h2> <h2 className="font-semibold">{t('overview.recentInvoices')}</h2>
</div> </div>
<Link href="/billing/invoices" className="text-sm text-primary hover:underline flex items-center gap-1"> <Link href="/billing/invoices" className="text-sm text-primary hover:underline flex items-center gap-1">
View all <ChevronRight className="h-4 w-4" /> {t('overview.viewAll')} <ChevronRight className="h-4 w-4" />
</Link> </Link>
</div> </div>
{recentInvoices.length === 0 ? ( {recentInvoices.length === 0 ? (
<div className="p-6 text-center text-muted-foreground text-sm">No invoices yet</div> <div className="p-6 text-center text-muted-foreground text-sm">{t('overview.noInvoices')}</div>
) : ( ) : (
<div className="divide-y"> <div className="divide-y">
{recentInvoices.map((inv) => ( {recentInvoices.map((inv) => (

View File

@ -3,6 +3,7 @@
import { useState } from 'react'; import { useState } from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useTranslation } from 'react-i18next';
import { apiClient } from '@/infrastructure/api/api-client'; import { apiClient } from '@/infrastructure/api/api-client';
import { Check, Zap } from 'lucide-react'; import { Check, Zap } from 'lucide-react';
@ -26,25 +27,16 @@ interface Subscription {
status: string; status: string;
} }
function formatLimit(n: number) {
return n === -1 ? 'Unlimited' : n.toLocaleString();
}
function formatTokens(n: number) { function formatTokens(n: number) {
if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(0)}M`; if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(0)}M`;
if (n >= 1_000) return `${(n / 1_000).toFixed(0)}K`; if (n >= 1_000) return `${(n / 1_000).toFixed(0)}K`;
return `${n}`; return `${n}`;
} }
const PLAN_FEATURES: Record<string, string[]> = {
free: ['100K tokens/month', 'Up to 5 servers', 'Up to 3 users', 'Up to 10 standing orders', 'Hard limit at 100%'],
pro: ['1M tokens/month', 'Up to 50 servers', 'Up to 20 users', 'Up to 100 standing orders', 'Soft limit at 150%', 'Overage at $8/MTok', '14-day trial'],
enterprise: ['10M tokens/month', 'Unlimited servers', 'Unlimited users', 'Unlimited standing orders', 'No hard limit', 'Overage at $5/MTok', '14-day trial'],
};
export default function PlansPage() { export default function PlansPage() {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const router = useRouter(); const router = useRouter();
const { t } = useTranslation('billing');
const [currency, setCurrency] = useState<'USD' | 'CNY'>('USD'); const [currency, setCurrency] = useState<'USD' | 'CNY'>('USD');
const [selectedPlan, setSelectedPlan] = useState<string | null>(null); const [selectedPlan, setSelectedPlan] = useState<string | null>(null);
@ -86,7 +78,7 @@ export default function PlansPage() {
return ( return (
<div className="space-y-6"> <div className="space-y-6">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<h1 className="text-2xl font-bold">Choose a Plan</h1> <h1 className="text-2xl font-bold">{t('plans.title')}</h1>
<div className="flex items-center gap-2 text-sm"> <div className="flex items-center gap-2 text-sm">
<button <button
onClick={() => setCurrency('USD')} onClick={() => setCurrency('USD')}
@ -109,7 +101,7 @@ export default function PlansPage() {
const isSelected = plan.name === selectedPlan; const isSelected = plan.name === selectedPlan;
const price = currency === 'CNY' ? plan.monthlyPriceCny : plan.monthlyPriceUsd; const price = currency === 'CNY' ? plan.monthlyPriceCny : plan.monthlyPriceUsd;
const symbol = currency === 'CNY' ? '¥' : '$'; const symbol = currency === 'CNY' ? '¥' : '$';
const features = PLAN_FEATURES[plan.name] ?? []; const features = t(`plans.features.${plan.name}`, { returnObjects: true }) as string[];
const isEnterprise = plan.name === 'enterprise'; const isEnterprise = plan.name === 'enterprise';
return ( return (
@ -125,7 +117,7 @@ export default function PlansPage() {
{isEnterprise && ( {isEnterprise && (
<div className="absolute -top-3 left-1/2 -translate-x-1/2"> <div className="absolute -top-3 left-1/2 -translate-x-1/2">
<span className="bg-primary text-primary-foreground text-xs font-bold px-3 py-1 rounded-full"> <span className="bg-primary text-primary-foreground text-xs font-bold px-3 py-1 rounded-full">
MOST POPULAR {t('plans.mostPopular')}
</span> </span>
</div> </div>
)} )}
@ -133,7 +125,7 @@ export default function PlansPage() {
{isCurrent && ( {isCurrent && (
<div className="absolute top-3 right-3"> <div className="absolute top-3 right-3">
<span className="bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400 text-xs font-medium px-2 py-0.5 rounded-full"> <span className="bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400 text-xs font-medium px-2 py-0.5 rounded-full">
Current {t('plans.current')}
</span> </span>
</div> </div>
)} )}
@ -142,29 +134,31 @@ export default function PlansPage() {
<h3 className="text-xl font-bold">{plan.displayName}</h3> <h3 className="text-xl font-bold">{plan.displayName}</h3>
<div className="flex items-end gap-1 mt-2"> <div className="flex items-end gap-1 mt-2">
<span className="text-3xl font-bold">{symbol}{price.toFixed(2)}</span> <span className="text-3xl font-bold">{symbol}{price.toFixed(2)}</span>
<span className="text-muted-foreground text-sm mb-1">/month</span> <span className="text-muted-foreground text-sm mb-1">{t('plans.perMonth')}</span>
</div> </div>
{plan.trialDays > 0 && ( {plan.trialDays > 0 && (
<p className="text-xs text-primary mt-1">{plan.trialDays}-day free trial</p> <p className="text-xs text-primary mt-1">{t('plans.freeTrial', { days: plan.trialDays })}</p>
)} )}
</div> </div>
<div className="border-t pt-4 space-y-2"> <div className="border-t pt-4 space-y-2">
<div className="flex items-center gap-2 text-sm font-medium"> <div className="flex items-center gap-2 text-sm font-medium">
<Zap className="h-4 w-4 text-primary" /> <Zap className="h-4 w-4 text-primary" />
{formatTokens(plan.includedTokensPerMonth)} tokens/month {formatTokens(plan.includedTokensPerMonth)} {t('plans.tokensPerMonth')}
</div> </div>
{plan.overageRateUsdPerMToken > 0 && ( {plan.overageRateUsdPerMToken > 0 && (
<p className="text-xs text-muted-foreground pl-6"> <p className="text-xs text-muted-foreground pl-6">
Overage: {currency === 'CNY' {t('plans.overage', {
? `¥${(plan.overageRateUsdPerMToken * 7.2).toFixed(2)}` rate: currency === 'CNY'
: `$${plan.overageRateUsdPerMToken.toFixed(2)}`}/MTok ? `¥${(plan.overageRateUsdPerMToken * 7.2).toFixed(2)}`
: `$${plan.overageRateUsdPerMToken.toFixed(2)}`,
})}
</p> </p>
)} )}
</div> </div>
<ul className="space-y-1.5"> <ul className="space-y-1.5">
{features.map((f) => ( {Array.isArray(features) && features.map((f) => (
<li key={f} className="flex items-center gap-2 text-sm"> <li key={f} className="flex items-center gap-2 text-sm">
<Check className="h-4 w-4 text-green-500 flex-shrink-0" /> <Check className="h-4 w-4 text-green-500 flex-shrink-0" />
{f} {f}
@ -181,7 +175,7 @@ export default function PlansPage() {
: 'bg-muted hover:bg-primary/10' : 'bg-muted hover:bg-primary/10'
}`} }`}
> >
{isSelected ? 'Selected' : plan.name === 'free' ? 'Downgrade' : 'Upgrade'} {isSelected ? t('plans.selected') : plan.name === 'free' ? t('plans.downgrade') : t('plans.upgrade')}
</button> </button>
)} )}
</div> </div>
@ -192,21 +186,21 @@ export default function PlansPage() {
{selectedPlan && ( {selectedPlan && (
<div className="flex justify-end gap-3 pt-4"> <div className="flex justify-end gap-3 pt-4">
<button onClick={() => setSelectedPlan(null)} className="px-4 py-2 rounded-lg border text-sm"> <button onClick={() => setSelectedPlan(null)} className="px-4 py-2 rounded-lg border text-sm">
Cancel {t('plans.cancel')}
</button> </button>
<button <button
onClick={handleConfirm} onClick={handleConfirm}
disabled={upgradeMutation.isPending} disabled={upgradeMutation.isPending}
className="px-6 py-2 rounded-lg bg-primary text-primary-foreground text-sm font-medium disabled:opacity-50" className="px-6 py-2 rounded-lg bg-primary text-primary-foreground text-sm font-medium disabled:opacity-50"
> >
{upgradeMutation.isPending ? 'Processing...' : `Switch to ${selectedPlan}`} {upgradeMutation.isPending ? t('plans.processing') : t('plans.switchTo', { plan: selectedPlan })}
</button> </button>
</div> </div>
)} )}
{upgradeMutation.isError && ( {upgradeMutation.isError && (
<p className="text-sm text-red-500 text-center"> <p className="text-sm text-red-500 text-center">
Failed to change plan. Please try again. {t('plans.changePlanFailed')}
</p> </p>
)} )}
</div> </div>

View File

@ -21,6 +21,7 @@ import enTerminal from './locales/en/terminal.json';
import enStandingOrders from './locales/en/standing-orders.json'; import enStandingOrders from './locales/en/standing-orders.json';
import enRunbooks from './locales/en/runbooks.json'; import enRunbooks from './locales/en/runbooks.json';
import enSessions from './locales/en/sessions.json'; import enSessions from './locales/en/sessions.json';
import enBilling from './locales/en/billing.json';
// Chinese // Chinese
import zhCommon from './locales/zh/common.json'; import zhCommon from './locales/zh/common.json';
@ -41,6 +42,7 @@ import zhTerminal from './locales/zh/terminal.json';
import zhStandingOrders from './locales/zh/standing-orders.json'; import zhStandingOrders from './locales/zh/standing-orders.json';
import zhRunbooks from './locales/zh/runbooks.json'; import zhRunbooks from './locales/zh/runbooks.json';
import zhSessions from './locales/zh/sessions.json'; import zhSessions from './locales/zh/sessions.json';
import zhBilling from './locales/zh/billing.json';
export const supportedLngs = ['en', 'zh'] as const; export const supportedLngs = ['en', 'zh'] as const;
export type SupportedLocale = (typeof supportedLngs)[number]; export type SupportedLocale = (typeof supportedLngs)[number];
@ -69,6 +71,7 @@ i18n
'standing-orders': enStandingOrders, 'standing-orders': enStandingOrders,
runbooks: enRunbooks, runbooks: enRunbooks,
sessions: enSessions, sessions: enSessions,
billing: enBilling,
}, },
zh: { zh: {
common: zhCommon, common: zhCommon,
@ -89,6 +92,7 @@ i18n
'standing-orders': zhStandingOrders, 'standing-orders': zhStandingOrders,
runbooks: zhRunbooks, runbooks: zhRunbooks,
sessions: zhSessions, sessions: zhSessions,
billing: zhBilling,
}, },
}, },
fallbackLng: 'en', fallbackLng: 'en',

View File

@ -0,0 +1,61 @@
{
"plans": {
"title": "Choose a Plan",
"mostPopular": "MOST POPULAR",
"current": "Current",
"perMonth": "/month",
"freeTrial": "{{days}}-day free trial",
"tokensPerMonth": "tokens/month",
"overage": "Overage: {{rate}}/MTok",
"selected": "Selected",
"downgrade": "Downgrade",
"upgrade": "Upgrade",
"cancel": "Cancel",
"processing": "Processing...",
"switchTo": "Switch to {{plan}}",
"changePlanFailed": "Failed to change plan. Please try again.",
"unlimited": "Unlimited",
"features": {
"free": ["100K tokens/month", "Up to 5 servers", "Up to 3 users", "Up to 10 standing orders", "Hard limit at 100%"],
"pro": ["1M tokens/month", "Up to 50 servers", "Up to 20 users", "Up to 100 standing orders", "Soft limit at 150%", "Overage at $8/MTok", "14-day trial"],
"enterprise": ["10M tokens/month", "Unlimited servers", "Unlimited users", "Unlimited standing orders", "No hard limit", "Overage at $5/MTok", "14-day trial"]
}
},
"overview": {
"title": "Billing & Subscription",
"usageWarning": "You have used {{pct}}% of your monthly token quota.",
"limitReached": "Limit reached — agent tasks are blocked.",
"considerUpgrade": "Consider upgrading your plan.",
"upgrade": "Upgrade",
"currentPlan": "Current Plan",
"change": "Change",
"trialEnds": "Trial ends {{date}}",
"cancelsOn": "Cancels on {{date}}",
"downgradeTo": "Downgrade to {{plan}} on {{date}}",
"period": "Period: {{start}} {{end}}",
"noSubscription": "No active subscription",
"tokenUsage": "This Month's Token Usage",
"tokensUsed": "{{used}} used",
"tokensLimit": "{{limit}} limit",
"unlimited": "Unlimited",
"overageApply": "Overage charges apply",
"loadingUsage": "Loading usage data...",
"recentInvoices": "Recent Invoices",
"viewAll": "View all",
"noInvoices": "No invoices yet"
},
"invoices": {
"title": "Invoices",
"invoiceNumber": "Invoice #",
"period": "Period",
"amount": "Amount",
"dueDate": "Due Date",
"status": "Status",
"loading": "Loading...",
"noInvoices": "No invoices found",
"payNow": "Pay Now",
"pagination": "Page {{current}} of {{total}} ({{count}} total)",
"previous": "Previous",
"next": "Next"
}
}

View File

@ -0,0 +1,61 @@
{
"plans": {
"title": "选择套餐",
"mostPopular": "最受欢迎",
"current": "当前",
"perMonth": "/月",
"freeTrial": "{{days}} 天免费试用",
"tokensPerMonth": "tokens/月",
"overage": "超额: {{rate}}/MTok",
"selected": "已选择",
"downgrade": "降级",
"upgrade": "升级",
"cancel": "取消",
"processing": "处理中...",
"switchTo": "切换到 {{plan}}",
"changePlanFailed": "套餐切换失败,请重试",
"unlimited": "不限",
"features": {
"free": ["每月 100K tokens", "最多 5 台服务器", "最多 3 个用户", "最多 10 条常驻指令", "硬限制 100%"],
"pro": ["每月 1M tokens", "最多 50 台服务器", "最多 20 个用户", "最多 100 条常驻指令", "软限制 150%", "超额 $8/MTok", "14 天试用"],
"enterprise": ["每月 10M tokens", "不限服务器", "不限用户", "不限常驻指令", "无硬限制", "超额 $5/MTok", "14 天试用"]
}
},
"overview": {
"title": "账单与订阅",
"usageWarning": "您已使用本月 token 配额的 {{pct}}%。",
"limitReached": "已达上限 — 智能体任务已暂停。",
"considerUpgrade": "建议升级套餐。",
"upgrade": "升级",
"currentPlan": "当前套餐",
"change": "更改",
"trialEnds": "试用期至 {{date}}",
"cancelsOn": "将于 {{date}} 取消",
"downgradeTo": "将于 {{date}} 降级为 {{plan}}",
"period": "周期:{{start}} {{end}}",
"noSubscription": "暂无活跃订阅",
"tokenUsage": "本月 Token 用量",
"tokensUsed": "已用 {{used}}",
"tokensLimit": "上限 {{limit}}",
"unlimited": "不限",
"overageApply": "超额费用适用",
"loadingUsage": "加载用量数据...",
"recentInvoices": "近期账单",
"viewAll": "查看全部",
"noInvoices": "暂无账单"
},
"invoices": {
"title": "账单列表",
"invoiceNumber": "账单号",
"period": "周期",
"amount": "金额",
"dueDate": "到期日",
"status": "状态",
"loading": "加载中...",
"noInvoices": "暂无账单",
"payNow": "立即支付",
"pagination": "第 {{current}} / {{total}} 页(共 {{count}} 条)",
"previous": "上一页",
"next": "下一页"
}
}