From af1cae9da8fb22f23077421ee68ac7af645d0f04 Mon Sep 17 00:00:00 2001 From: hailin Date: Sat, 7 Mar 2026 01:46:05 -0800 Subject: [PATCH] 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 --- .../src/app/(admin)/billing/invoices/page.tsx | 26 ++++---- .../src/app/(admin)/billing/page.tsx | 48 +++++++++------ .../src/app/(admin)/billing/plans/page.tsx | 44 ++++++------- it0-web-admin/src/i18n/config.ts | 4 ++ .../src/i18n/locales/en/billing.json | 61 +++++++++++++++++++ .../src/i18n/locales/zh/billing.json | 61 +++++++++++++++++++ 6 files changed, 187 insertions(+), 57 deletions(-) create mode 100644 it0-web-admin/src/i18n/locales/en/billing.json create mode 100644 it0-web-admin/src/i18n/locales/zh/billing.json diff --git a/it0-web-admin/src/app/(admin)/billing/invoices/page.tsx b/it0-web-admin/src/app/(admin)/billing/invoices/page.tsx index b3c315a..51242e7 100644 --- a/it0-web-admin/src/app/(admin)/billing/invoices/page.tsx +++ b/it0-web-admin/src/app/(admin)/billing/invoices/page.tsx @@ -2,6 +2,7 @@ import { useState } from 'react'; import { useQuery } from '@tanstack/react-query'; +import { useTranslation } from 'react-i18next'; import Link from 'next/link'; import { apiClient } from '@/infrastructure/api/api-client'; import { FileText, Download } from 'lucide-react'; @@ -38,6 +39,7 @@ const PAGE_SIZE = 20; export default function InvoicesPage() { const [page, setPage] = useState(0); + const { t } = useTranslation('billing'); const { data, isLoading } = useQuery<{ data: Invoice[]; total: number }>({ queryKey: ['billing', 'invoices', page], @@ -51,7 +53,7 @@ export default function InvoicesPage() { return (

- Invoices + {t('invoices.title')}

@@ -59,22 +61,22 @@ export default function InvoicesPage() { - - - - - + + + + + {isLoading ? ( - + ) : invoices.length === 0 ? ( - + ) : ( invoices.map((inv) => ( @@ -102,7 +104,7 @@ export default function InvoicesPage() { 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" > - Pay Now + {t('invoices.payNow')} ) : ( diff --git a/it0-web-admin/src/app/(admin)/billing/page.tsx b/it0-web-admin/src/app/(admin)/billing/page.tsx index 17cdf3a..f75e4c8 100644 --- a/it0-web-admin/src/app/(admin)/billing/page.tsx +++ b/it0-web-admin/src/app/(admin)/billing/page.tsx @@ -74,7 +74,7 @@ function formatTokens(n: number) { } export default function BillingPage() { - const { t } = useTranslation(); + const { t } = useTranslation('billing'); const { data: sub } = useQuery({ queryKey: ['billing', 'subscription'], @@ -100,18 +100,18 @@ export default function BillingPage() { return (
-

Billing & Subscription

+

{t('overview.title')}

{/* Usage Warning Banner */} {usedPct >= 80 && (
- You have used {usedPct.toFixed(0)}% of your monthly token quota.{' '} - {usedPct >= 100 ? 'Limit reached — agent tasks are blocked.' : 'Consider upgrading your plan.'} + {t('overview.usageWarning', { pct: usedPct.toFixed(0) })}{' '} + {usedPct >= 100 ? t('overview.limitReached') : t('overview.considerUpgrade')} - Upgrade + {t('overview.upgrade')}
)} @@ -122,10 +122,10 @@ export default function BillingPage() {
-

Current Plan

+

{t('overview.currentPlan')}

- Change + {t('overview.change')}
@@ -137,25 +137,31 @@ export default function BillingPage() {
{sub.trialEndsAt && sub.status === 'trialing' && (

- Trial ends {new Date(sub.trialEndsAt).toLocaleDateString()} + {t('overview.trialEnds', { date: new Date(sub.trialEndsAt).toLocaleDateString() })}

)} {sub.cancelAtPeriodEnd && (

- Cancels on {new Date(sub.currentPeriodEnd).toLocaleDateString()} + {t('overview.cancelsOn', { date: new Date(sub.currentPeriodEnd).toLocaleDateString() })}

)} {sub.nextPlan && (

- Downgrade to {sub.nextPlan.displayName} on {new Date(sub.currentPeriodEnd).toLocaleDateString()} + {t('overview.downgradeTo', { + plan: sub.nextPlan.displayName, + date: new Date(sub.currentPeriodEnd).toLocaleDateString(), + })}

)}

- 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(), + })}

) : ( -

No active subscription

+

{t('overview.noSubscription')}

)} @@ -163,15 +169,17 @@ export default function BillingPage() {
-

This Month's Token Usage

+

{t('overview.tokenUsage')}

{quota ? (
- {formatTokens(quota.usedTokens)} used + {t('overview.tokensUsed', { used: formatTokens(quota.usedTokens) })} - {quota.limitTokens === -1 ? 'Unlimited' : `${formatTokens(quota.limitTokens)} limit`} + {quota.limitTokens === -1 + ? t('overview.unlimited') + : t('overview.tokensLimit', { limit: formatTokens(quota.limitTokens) })}
{quota.limitTokens !== -1 && ( @@ -180,12 +188,12 @@ export default function BillingPage() {
{usedPct.toFixed(1)}% {quota.overageAllowed && usedPct > 100 && ( - Overage charges apply + {t('overview.overageApply')} )}
) : ( -

Loading usage data...

+

{t('overview.loadingUsage')}

)}
@@ -195,15 +203,15 @@ export default function BillingPage() {
-

Recent Invoices

+

{t('overview.recentInvoices')}

- View all + {t('overview.viewAll')}
{recentInvoices.length === 0 ? ( -
No invoices yet
+
{t('overview.noInvoices')}
) : (
{recentInvoices.map((inv) => ( diff --git a/it0-web-admin/src/app/(admin)/billing/plans/page.tsx b/it0-web-admin/src/app/(admin)/billing/plans/page.tsx index b02a9b4..990dee6 100644 --- a/it0-web-admin/src/app/(admin)/billing/plans/page.tsx +++ b/it0-web-admin/src/app/(admin)/billing/plans/page.tsx @@ -3,6 +3,7 @@ import { useState } from 'react'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useRouter } from 'next/navigation'; +import { useTranslation } from 'react-i18next'; import { apiClient } from '@/infrastructure/api/api-client'; import { Check, Zap } from 'lucide-react'; @@ -26,25 +27,16 @@ interface Subscription { status: string; } -function formatLimit(n: number) { - return n === -1 ? 'Unlimited' : n.toLocaleString(); -} - function formatTokens(n: number) { if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(0)}M`; if (n >= 1_000) return `${(n / 1_000).toFixed(0)}K`; return `${n}`; } -const PLAN_FEATURES: Record = { - 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() { const queryClient = useQueryClient(); const router = useRouter(); + const { t } = useTranslation('billing'); const [currency, setCurrency] = useState<'USD' | 'CNY'>('USD'); const [selectedPlan, setSelectedPlan] = useState(null); @@ -86,7 +78,7 @@ export default function PlansPage() { return (
-

Choose a Plan

+

{t('plans.title')}

- {formatTokens(plan.includedTokensPerMonth)} tokens/month + {formatTokens(plan.includedTokensPerMonth)} {t('plans.tokensPerMonth')}
{plan.overageRateUsdPerMToken > 0 && (

- Overage: {currency === 'CNY' - ? `¥${(plan.overageRateUsdPerMToken * 7.2).toFixed(2)}` - : `$${plan.overageRateUsdPerMToken.toFixed(2)}`}/MTok + {t('plans.overage', { + rate: currency === 'CNY' + ? `¥${(plan.overageRateUsdPerMToken * 7.2).toFixed(2)}` + : `$${plan.overageRateUsdPerMToken.toFixed(2)}`, + })}

)}
    - {features.map((f) => ( + {Array.isArray(features) && features.map((f) => (
  • {f} @@ -181,7 +175,7 @@ export default function PlansPage() { : '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')} )}
@@ -192,21 +186,21 @@ export default function PlansPage() { {selectedPlan && (
)} {upgradeMutation.isError && (

- Failed to change plan. Please try again. + {t('plans.changePlanFailed')}

)}
diff --git a/it0-web-admin/src/i18n/config.ts b/it0-web-admin/src/i18n/config.ts index 73ea8ab..be82c82 100644 --- a/it0-web-admin/src/i18n/config.ts +++ b/it0-web-admin/src/i18n/config.ts @@ -21,6 +21,7 @@ import enTerminal from './locales/en/terminal.json'; import enStandingOrders from './locales/en/standing-orders.json'; import enRunbooks from './locales/en/runbooks.json'; import enSessions from './locales/en/sessions.json'; +import enBilling from './locales/en/billing.json'; // Chinese 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 zhRunbooks from './locales/zh/runbooks.json'; import zhSessions from './locales/zh/sessions.json'; +import zhBilling from './locales/zh/billing.json'; export const supportedLngs = ['en', 'zh'] as const; export type SupportedLocale = (typeof supportedLngs)[number]; @@ -69,6 +71,7 @@ i18n 'standing-orders': enStandingOrders, runbooks: enRunbooks, sessions: enSessions, + billing: enBilling, }, zh: { common: zhCommon, @@ -89,6 +92,7 @@ i18n 'standing-orders': zhStandingOrders, runbooks: zhRunbooks, sessions: zhSessions, + billing: zhBilling, }, }, fallbackLng: 'en', diff --git a/it0-web-admin/src/i18n/locales/en/billing.json b/it0-web-admin/src/i18n/locales/en/billing.json new file mode 100644 index 0000000..0dabd75 --- /dev/null +++ b/it0-web-admin/src/i18n/locales/en/billing.json @@ -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" + } +} diff --git a/it0-web-admin/src/i18n/locales/zh/billing.json b/it0-web-admin/src/i18n/locales/zh/billing.json new file mode 100644 index 0000000..4e8f867 --- /dev/null +++ b/it0-web-admin/src/i18n/locales/zh/billing.json @@ -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": "下一页" + } +}
Invoice #PeriodAmountDue DateStatus{t('invoices.invoiceNumber')}{t('invoices.period')}{t('invoices.amount')}{t('invoices.dueDate')}{t('invoices.status')}
Loading...{t('invoices.loading')}
No invoices found{t('invoices.noInvoices')}