feat: 国际化处理

This commit is contained in:
speakeloudest 2025-08-10 03:07:37 -07:00
parent 0d3b63c2fc
commit 01ac12c171
33 changed files with 268 additions and 123 deletions

View File

@ -1,4 +1,7 @@
{
"chat.editor.fontSize": 16,
"debug.console.fontSize": 14,
"editor.fontSize": 16,
"eslint.workingDirectories": [
{
"mode": "auto"
@ -11,5 +14,7 @@
"*.jsx": "${capture}.js",
"*.tsx": "${capture}.ts",
"README.md": "*.md, LICENSE"
}
},
"scm.inputFontSize": 13,
"terminal.integrated.fontSize": 12
}

View File

@ -17,6 +17,7 @@ import {
DialogTitle,
} from '@workspace/airo-ui/components/dialog';
import { Pagination } from '@workspace/airo-ui/custom-components/pro-table/pagination';
import { useTranslations } from 'next-intl';
import { useImperativeHandle, useRef, useState } from 'react';
import { Popup, PopupData, PopupRef } from './Popup';
@ -37,6 +38,7 @@ interface DialogProps {
}
export const AnnouncementDialog = ({ ref }: DialogProps) => {
const t = useTranslations('dashboard');
const [open, setOpen] = useState(false);
const [pagination, setPagination] = useState<PaginationState>({
pageIndex: 0,
@ -101,12 +103,14 @@ export const AnnouncementDialog = ({ ref }: DialogProps) => {
<Dialog open={open} onOpenChange={setOpen}>
<DialogContent className='flex flex-col sm:grid sm:max-w-[600px]'>
<DialogHeader>
<DialogTitle className={'text-left text-2xl sm:text-4xl'}></DialogTitle>
<DialogTitle className={'text-left text-2xl sm:text-4xl'}>
{t('announcementTitle')}
</DialogTitle>
</DialogHeader>
<div className='space-y-4'>
{isLoading ? (
<div className='py-8 text-center'>...</div>
<div className='py-8 text-center'>{t('loading')}</div>
) : (
<>
{announcementData?.list?.map((item: AnnouncementItem) => {
@ -116,7 +120,7 @@ export const AnnouncementDialog = ({ ref }: DialogProps) => {
className='flex items-center rounded-[20px] bg-[#B5C9E2] px-4 py-2 sm:p-4'
>
<p className='line-clamp-2 flex-1 text-[10px] text-[#225BA9] sm:text-sm'>
{item.pinned && '【置顶公告】'}{' '}
{item.pinned && t('pinnedAnnouncement')}{' '}
<span className={`${item.pinned ? 'text-white' : 'text-[#4D4D4D]'}`}>
{item.content}
</span>
@ -126,7 +130,7 @@ export const AnnouncementDialog = ({ ref }: DialogProps) => {
className='cursor-pointer text-xs text-[#225BA9] sm:text-sm'
onClick={() => handleOpenPopup(item)}
>
{t('viewDetails')}
</span>
</div>
</div>
@ -139,9 +143,9 @@ export const AnnouncementDialog = ({ ref }: DialogProps) => {
<Pagination
table={table}
text={{
textRowsPerPage: '每页显示',
textRowsPerPage: t('rowsPerPage'),
textPageOf: (pageIndex, pageCount) =>
`${pageIndex} 页,共 ${pageCount}`,
t('pageOf', { pageIndex: pageIndex, pageCount: pageCount }),
}}
/>
</div>

View File

@ -111,8 +111,10 @@ export default function Content() {
if (data && userSubscribe?.length > 0 && !userSubscribeProtocol.length) {
const list = getUserSubscribe(userSubscribe[0]?.token, data.protocol);
setUserSubscribeProtocol(list);
if (list.length > 0) {
setUserSubscribeProtocolCurrent(list[0]);
}
}
}, [data, userSubscribe, userSubscribeProtocol.length]);
const statusWatermarks = {
@ -129,7 +131,7 @@ export default function Content() {
const currentIndex = userSubscribeProtocol.findIndex(
(url) => url === userSubscribeProtocolCurrent,
);
return currentIndex !== -1 ? `${t('subscriptionUrl')}${currentIndex + 1}` : '地址1';
return currentIndex !== -1 ? `${t('subscriptionUrl')}${currentIndex + 1}` : t('address1');
};
const popupRef = useRef<PopupRef>(null);
@ -140,19 +142,23 @@ export default function Content() {
{/* 账户概况 Card */}
<Card className='rounded-[20px] border border-[#D9D9D9] p-6 shadow-[0px_0px_52.6px_1px_rgba(15,44,83,0.05)]'>
<div className='mb-1 sm:mb-4'>
<h3 className='text-base font-medium text-[#666666] sm:text-xl'></h3>
<h3 className='text-base font-medium text-[#666666] sm:text-xl'>
{t('accountOverview')}
</h3>
<p className='mt-1 text-xs text-[#666666] sm:text-sm'>
{user?.auth_methods?.[0]?.auth_identifier}
</p>
</div>
<div className='mb-3 sm:mb-6'>
<span className='text-2xl font-medium text-[#091B33] sm:text-3xl'></span>
<span className='text-2xl font-medium text-[#091B33] sm:text-3xl'>
{t('annualPlanUser')}
</span>
</div>
<div className='rounded-[20px] bg-[#EAEAEA] px-4 py-[10px]'>
<div className='flex items-center justify-between'>
<span className='text-sm font-light text-[#666666]'></span>
<span className='text-sm font-light text-[#666666]'>{t('accountBalance')}</span>
<Recharge
className={
'border-0 bg-transparent p-0 text-sm font-normal text-[#225BA9] shadow-none outline-0 hover:bg-transparent'
@ -170,21 +176,22 @@ export default function Content() {
<div className='mb-4'>
<h3 className='flex items-center justify-between text-[#666666]'>
<div className={'flex items-center justify-between'}>
<span className={'text-base font-medium sm:text-xl'}></span>
<span className={'text-base font-medium sm:text-xl'}>{t('planStatus')}</span>
<span className={'ml-2.5 rounded-full bg-[#A8D4ED] px-2 text-[8px] text-white'}>
{t('inEffect')}
</span>
</div>
<ResetTraffic
className={
'border-0 bg-transparent p-0 text-sm font-normal text-[#225BA9] shadow-none outline-0 hover:bg-transparent'
}
id={userSubscribe?.[0]?.id}
id={userSubscribe?.[0]?.id || 0}
replacement={userSubscribe?.[0]?.subscribe.replacement}
/>
</h3>
<div className='mb-2 text-sm text-[#666666] sm:mb-[22px] sm:mt-1'>
{formatDate(userSubscribe?.[0]?.expire_time, false)}
{t('planExpirationTime')}
{formatDate(userSubscribe?.[0]?.expire_time, false)}
</div>
<div className='mb-3 sm:mb-6'>
<span className='text-2xl font-medium text-[#091B33] sm:text-3xl'>
@ -195,7 +202,7 @@ export default function Content() {
<div className='mb-4 flex items-center justify-between'>
<div className='flex items-center gap-2'>
<span className='text-xs sm:text-sm'></span>
<span className='text-xs sm:text-sm'>{t('availableDevices')}</span>
<div className='flex gap-2'>
<div className='h-4 w-4 rounded-full bg-[#225BA9]'></div>
<div className='h-4 w-4 rounded-full bg-[#D9D9D9]'></div>
@ -206,13 +213,14 @@ export default function Content() {
</div>
</div>
<span className='text-xs sm:text-sm'>
线uu/{userSubscribe?.[0]?.subscribe.device_limit}
{t('online')}
{userSubscribe?.[0]?.subscribe.device_limit}
</span>
</div>
<div>
<div className='mb-1 flex items-center justify-between'>
<span className='text-xs sm:text-sm'>
使/
{t('usedTrafficTotalTraffic')}
<Display
type='traffic'
value={userSubscribe?.[0]?.upload + userSubscribe?.[0]?.download}
@ -226,20 +234,21 @@ export default function Content() {
/>
</span>
<span className='text-xs sm:text-sm'>
{t('remaining')}
{100 -
(
(userSubscribe?.[0]?.upload + userSubscribe?.[0]?.download) /
userSubscribe?.[0]?.traffic
).toFixed(0)}
Math.round(
(((userSubscribe?.[0]?.upload || 0) + (userSubscribe?.[0]?.download || 0)) /
(userSubscribe?.[0]?.traffic || 1)) *
100,
)}
%
</span>
</div>
<div className='flex h-5 w-full items-center rounded-[20px] bg-[#EAEAEA] p-0.5'>
<div
className='h-full rounded-[20px] bg-[#225BA9]'
className={'h-full rounded-[20px] bg-[#225BA9]'}
style={{
width: `${((userSubscribe?.[0]?.upload + userSubscribe?.[0]?.download) / userSubscribe?.[0]?.traffic).toFixed(0)}%`,
width: `${Math.round((((userSubscribe?.[0]?.upload || 0) + (userSubscribe?.[0]?.download || 0)) / (userSubscribe?.[0]?.traffic || 1)) * 100)}%`,
}}
></div>
</div>
@ -251,13 +260,15 @@ export default function Content() {
className={'absolute bottom-0 left-0 right-0 h-[60px] bg-white/30 backdrop-blur-[1px]'}
></div>
<div className='mb-3 flex items-center justify-between sm:mb-4'>
<h3 className='text-base font-medium text-[#666666] sm:text-xl'></h3>
<h3 className='text-base font-medium text-[#666666] sm:text-xl'>
{t('siteAnnouncements')}
</h3>
{announcementData?.length ? (
<Button
className='border-0 bg-transparent p-0 text-sm font-normal text-[#225BA9] shadow-none outline-0 hover:bg-transparent'
onClick={() => dialogRef.current.open()}
onClick={() => dialogRef.current?.open()}
>
{t('more')}
</Button>
) : null}
</div>
@ -268,7 +279,7 @@ export default function Content() {
return (
<div className='flex items-center rounded-[20px] bg-[#B5C9E2] px-4 py-2 sm:p-4'>
<p className='line-clamp-2 flex-1 text-[10px] text-[#225BA9] sm:text-sm'>
{item.pinned && '【置顶公告】'}{' '}
{item.pinned && t('pinnedAnnouncement')}{' '}
<span className={`${item.pinned ? 'text-white' : 'text-[#4D4D4D]'}`}>
{item.content}
</span>
@ -276,9 +287,9 @@ export default function Content() {
<div className='ml-2 w-[65px] text-right'>
<span
className='cursor-pointer text-xs text-[#225BA9] sm:text-sm'
onClick={() => popupRef.current.open(item)}
onClick={() => popupRef.current?.open(item)}
>
{t('viewDetails')}
</span>
</div>
</div>
@ -292,26 +303,30 @@ export default function Content() {
{/* 我的订阅 Card */}
<Card className='rounded-[20px] border border-[#D9D9D9] p-6 shadow-[0px_0px_52.6px_1px_rgba(15,44,83,0.05)]'>
<div className='flex items-center justify-between sm:mb-4'>
<h3 className='text-base font-medium text-[#666666] sm:text-xl'></h3>
<h3 className='text-base font-medium text-[#666666] sm:text-xl'>
{t('mySubscription')}
</h3>
<Link
href={'/document'}
className='border-0 bg-transparent p-0 text-sm font-semibold text-[#225BA9] shadow-none outline-0 hover:bg-transparent sm:font-normal'
>
{t('beginnerTutorial')}
</Link>
</div>
{userSubscribe?.[0] && data.protocol ? (
<div className='space-y-2 sm:space-y-4'>
<p className='text-xs font-light text-[#666666] sm:text-sm sm:font-normal'>
{t('copySubscriptionLinkOrScanQrCode')}
</p>
{/* 统计信息 */}
<div className='rounded-[20px] bg-[#EAEAEA] p-4'>
<div className='grid grid-cols-3 gap-4 text-center'>
<div>
<p className='text-[10px] text-[rgba(132,132,132,0.7)] sm:text-xs'></p>
<p className='text-[10px] text-[rgba(132,132,132,0.7)] sm:text-xs'>
{t('totalTraffic')}
</p>
<p className='text-xs font-medium text-[#0F2C53] sm:text-lg'>
<Display
type='traffic'
@ -430,7 +445,9 @@ export default function Content() {
</PopoverTrigger>
<PopoverContent className='w-auto rounded-xl p-4' align='end'>
<div className='flex flex-col items-center gap-2'>
<p className='text-muted-foreground text-center text-xs'></p>
<p className='text-muted-foreground text-center text-xs'>
{t('scanCodeToSubscribe')}
</p>
<QRCodeCanvas
value={userSubscribeProtocolCurrent}
size={120}
@ -466,7 +483,7 @@ export default function Content() {
<AlertDialogAction
onClick={async () => {
await resetUserSubscribeToken({
user_subscribe_id: userSubscribe?.[0]?.id,
user_subscribe_id: userSubscribe?.[0]?.id || 0,
});
await refetch();
toast.success(t('resetSuccess'));

View File

@ -62,10 +62,8 @@ export default function Page() {
{TutorialList && TutorialList?.length > 0 && (
<div className='rounded-[46px] bg-[#EAEAEA] px-[23px] py-[19px] sm:px-[34px] sm:py-[28px]'>
<div className='font-semibold text-[#666]'></div>
<div className={'mb-2.5 text-xs text-[#666] sm:text-sm'}>
</div>
<div className='font-semibold text-[#666]'>{t('tutorialTitle')}</div>
<div className={'mb-2.5 text-xs text-[#666] sm:text-sm'}>{t('tutorialDescription')}</div>
<Tabs defaultValue={TutorialList?.[0]?.title}>
<TabsList className='h-full flex-wrap justify-start gap-1 bg-transparent'>
{TutorialList?.map((tutorial) => (

View File

@ -1,4 +1,5 @@
import Announcement from '@/components/announcement';
import LanguageSwitch from '@/components/language-switch';
import { SidebarInset, SidebarProvider } from '@workspace/airo-ui/components/sidebar';
import { cookies } from 'next/headers';
import { Header } from './Header';
@ -12,8 +13,8 @@ export default async function DashboardLayout({ children }: { children: React.Re
<SidebarProvider className='' defaultOpen={defaultOpen}>
<SidebarLeft className='w-[288px] border-r-0 bg-transparent lg:flex' />
<SidebarInset className='relative flex-grow overflow-hidden'>
<LanguageSwitch />
<div className='h-[calc(100vh-56px)] flex-grow gap-4 overflow-auto p-4'>
{' '}
<Header />
{children}
</div>

View File

@ -15,13 +15,13 @@ interface OrderDetailDialogProps {
orderNo?: string;
}
interface OrderDetailDialogRef {
export interface OrderDetailDialogRef {
show: (orderNo: string) => void;
hide: () => void;
}
const OrderDetailDialog = forwardRef<OrderDetailDialogRef, OrderDetailDialogProps>((props, ref) => {
const t = useTranslations('subscribe');
const t = useTranslations('order');
const { getUserInfo } = useGlobalStore();
const router = useRouter();
const [open, setOpen] = useState(false);
@ -65,17 +65,17 @@ const OrderDetailDialog = forwardRef<OrderDetailDialogRef, OrderDetailDialogProp
<Dialog open={open} onOpenChange={setOpen}>
<DialogContent className='sm:w-[675px]'>
<div className='text-4xl font-bold text-[#0F2C53] sm:mb-8 sm:text-center sm:text-4xl'>
{t('orderDetail')}
</div>
<div className='text-[16px] font-bold text-[#666]'>
<div></div>
<div>{t('orderNo')}</div>
<div className='text-[12px] font-light text-[#4D4D4D]'>{orderNo}</div>
</div>
<Separator className='mb-3 mt-2 h-[2px] bg-[#225BA9]' />
<div className='text-[15px] text-[#225BA9]'>
<div></div>
<div className='font-light text-[#666]'></div>
<div>{t('paymentMethod')}</div>
<div className='font-light text-[#666]'>{t('walletBalance')}</div>
</div>
<Separator className='mb-3 mt-4 bg-[#225BA9]' />
<div>

View File

@ -45,7 +45,7 @@ export default function Page() {
ref.current?.refresh();
}}
>
{t('cancelOrder')}
</Button>
<Button className='ml-3 min-w-[150px] rounded-full border-[#A8D4ED] bg-[#A8D4ED] px-[35px] py-[9px] text-center text-xl font-bold hover:border-[#225BA9] hover:bg-[#225BA9] hover:text-white'>
{t('payment')}

View File

@ -46,10 +46,10 @@ export default function ChangePassword() {
'h-[32px] w-[110px] rounded-[50px] border-0 border-[#0F2C53] bg-[#0F2C53] text-center text-base font-medium leading-[32px] text-white transition hover:bg-[#225BA9] hover:text-white sm:hidden'
}
>
{t('save')}
</Button>
</div>
<div className={'text-xs font-light sm:text-[15px]'}></div>
<div className={'text-xs font-light sm:text-[15px]'}>{t('description')}</div>
</div>
<Form {...form}>
<form id='password-form' onSubmit={form.handleSubmit(onSubmit)} className='space-y-4'>
@ -102,7 +102,7 @@ export default function ChangePassword() {
'h-full rounded-[50px] border-0 border-[#0F2C53] bg-[#0F2C53] px-[55px] py-[9px] text-xl font-bold text-white transition hover:bg-[#225BA9] hover:text-white'
}
>
{t('save')}
</Button>
</div>
</Card>

View File

@ -51,10 +51,10 @@ export default function NotifySettings() {
'h-[32px] w-[110px] rounded-[50px] border-0 border-[#0F2C53] bg-[#0F2C53] text-center text-base font-medium leading-[32px] text-white transition hover:bg-[#225BA9] hover:text-white sm:hidden'
}
>
{t('notify.save')}
</Button>
</div>
<div className={'text-xs font-light sm:text-[15px]'}></div>
<div className={'text-xs font-light sm:text-[15px]'}>{t('notify.description')}</div>
</div>
<Form {...form}>
<form
@ -104,7 +104,7 @@ export default function NotifySettings() {
'h-full rounded-[50px] border-0 border-[#0F2C53] bg-[#0F2C53] px-[55px] py-[9px] text-xl font-bold text-white transition hover:bg-[#225BA9] hover:text-white'
}
>
{t('notify.save')}
</Button>
</div>
</Card>

View File

@ -245,13 +245,13 @@ export default function ThirdPartyAccounts() {
'h-[32px] w-[110px] rounded-full bg-[#D9D9D9] text-center font-medium leading-[32px] text-white sm:hidden'
}
>
{t('save')}
</div>
</div>
<div className='mb-2 text-xs text-[#666666] sm:mb-4 sm:mt-1 sm:text-sm'>
{user?.auth_methods?.[0]?.auth_identifier}
</div>
<div className={'mb-1 sm:mb-3'}>Email</div>
<div className={'mb-1 sm:mb-3'}>{t('emailLabel')}</div>
<div className={'flex items-center gap-2'}>
<div
className={
@ -265,7 +265,7 @@ export default function ThirdPartyAccounts() {
'hidden h-[32px] w-[110px] rounded-full bg-[#D9D9D9] text-center text-[16px] font-medium leading-[32px] text-white sm:block'
}
>
{t('save')}
</div>
</div>
</Card>

View File

@ -76,14 +76,14 @@ export default function Page() {
'hidden text-4xl font-bold text-[#0F2C53] sm:block md:mb-4 md:text-center md:text-5xl'
}
>
{t('title')}
</div>
<div
className={
'-mt-5 text-right text-lg font-bold text-[#666666] sm:mt-0 sm:text-center sm:font-medium'
}
>
{t('description')}
</div>
<div>
<Tabs
@ -115,7 +115,7 @@ export default function Page() {
}
value='year'
>
{t('yearlyPlan')}
</TabsTrigger>
<TabsTrigger
className={
@ -123,7 +123,7 @@ export default function Page() {
}
value='month'
>
{t('monthlyPlan')}
</TabsTrigger>
</TabsList>
</Tabs>

View File

@ -38,7 +38,7 @@ export default function Page() {
<div className='mb-4'>
<div className='flex items-center justify-between'>
<div>
<p className='text-sm font-light text-[#666]'></p>
<p className='text-sm font-light text-[#666]'>{t('totalAssets')}</p>
<p className='text-2xl font-bold sm:text-[32px]'>
<Display type='currency' value={totalAssets} />
</p>
@ -47,7 +47,9 @@ export default function Page() {
</div>
<div className='grid grid-cols-2 gap-2 sm:grid-cols-2 sm:gap-6 md:grid-cols-4'>
<div className='col-span-2 rounded-[20px] bg-[#EAEAEA] p-4 shadow-sm transition-all duration-300 hover:shadow-md sm:col-span-1'>
<p className='text-sm font-medium text-[#666] opacity-80 sm:mb-3'></p>
<p className='text-sm font-medium text-[#666] opacity-80 sm:mb-3'>
{t('accountBalance')}
</p>
<p className='text-xl font-bold text-[#225BA9] sm:text-2xl'>
<Display type='currency' value={user?.balance} />
</p>
@ -70,9 +72,9 @@ export default function Page() {
</div>
<div className='col-span-2 rounded-[20px] border-2 border-[#D9D9D9] p-4 shadow-sm transition-all duration-300 hover:shadow-md sm:col-span-1'>
<p className='mb-1 flex justify-between text-sm font-medium text-[#666] opacity-80 sm:mb-3'>
<span></span>
<span>{t('referralCode')}</span>
<Link href='/affiliate' className={'text-[#225BA9]'}>
{t('referralDetails')}
</Link>
</p>
<p className='flex justify-between text-base font-bold text-[#225BA9] sm:text-2xl'>

View File

@ -57,11 +57,11 @@ export const AffiliateDialog = ({ ref }: AffiliateDialogProps) => {
return (
<div className='flex flex-wrap justify-between gap-2 rounded-[20px] bg-white px-6 py-2 text-[10px] sm:text-base'>
<div>
<div className={'text-[#225BA9]'}></div>
<div className={'text-[#225BA9]'}>{t('userIdentifier')}</div>
<div className={'font-bold text-[#091B33]'}>{invite.identifier}</div>
</div>
<div>
<div className={'text-[#225BA9]'}></div>
<div className={'text-[#225BA9]'}>{t('time')}</div>
<div className={'font-bold text-[#091B33]'}>
{formatDate(invite.registered_at)}
</div>

View File

@ -52,23 +52,23 @@ export default function Affiliate() {
<CardContent className={'p-0 text-[#666]'}>
<div className={'sm:mb-6'}>
<div className={'font-bold sm:text-xl'}>{t('totalCommission')}</div>
<div className={'text-xs font-light sm:text-[15px]'}>
</div>
<div className={'text-xs font-light sm:text-[15px]'}>{t('commissionInfo')}</div>
</div>
<div className={'mb-3 text-xl font-bold text-[#091B33] sm:text-[32px]'}>
7
{t('historicalRecommendedUsers')}
</div>
<div className={'grid grid-cols-2 gap-[10px] sm:grid-cols-1 sm:gap-5 lg:grid-cols-2'}>
<div className='rounded-[20px] bg-[#EAEAEA] px-4 py-2 shadow-sm transition-all duration-300 hover:shadow-md sm:py-4'>
<p className='font-medium text-[#666] opacity-80 sm:mb-3 sm:text-sm'></p>
<p className='font-medium text-[#666] opacity-80 sm:mb-3 sm:text-sm'>
{t('totalCommission')}
</p>
<p className='text-xl font-bold text-[#225BA9] sm:text-2xl'>
<Display type='currency' value={data?.total_commission} />
</p>
</div>
<div className='rounded-[20px] border-2 border-[#D9D9D9] px-4 py-2 shadow-sm transition-all duration-300 hover:shadow-md sm:py-4'>
<p className='flex justify-between font-medium text-[#666] opacity-80 sm:mb-3 sm:text-sm'>
{t('commissionInviteCode')}
<CopyToClipboard
text={`${location?.origin}/?invite=${user?.refer_code}`}
onCopy={(text, result) => {
@ -111,12 +111,12 @@ export default function Affiliate() {
</Card>
<Card className='order-2 rounded-[20px] border border-[#EAEAEA] bg-gradient-to-b from-white to-[#EAEAEA] p-6 md:order-none'>
<div className='mb-4 flex items-center justify-between'>
<h3 className='font-medium text-[#666666] sm:text-xl'></h3>
<h3 className='font-medium text-[#666666] sm:text-xl'>{t('inviteRecords')}</h3>
<span
className='cursor-pointer text-sm text-[#225BA9]'
onClick={() => dialogRef.current.open()}
onClick={() => dialogRef.current?.open()}
>
{t('more')}
</span>
</div>
@ -132,11 +132,11 @@ export default function Affiliate() {
return (
<div className='flex flex-wrap justify-between gap-2 rounded-[20px] bg-white px-6 py-2 text-[10px] sm:text-base'>
<div>
<div className={'text-[#225BA9]'}></div>
<div className={'text-[#225BA9]'}>{t('userIdentifier')}</div>
<div className={'font-bold text-[#091B33]'}>{invite.identifier}</div>
</div>
<div>
<div className={'text-[#225BA9]'}></div>
<div className={'text-[#225BA9]'}>{t('time')}</div>
<div className={'font-bold text-[#091B33]'}>
{formatDate(invite.registered_at)}
</div>
@ -152,15 +152,16 @@ export default function Affiliate() {
</Card>
<Card className='min-w-[322px] rounded-[20px] border border-[#EAEAEA] bg-[#EAEAEA] p-6 text-[12px] sm:text-[16px] md:min-w-[496px]'>
<div className='flex items-center justify-between'>
<h3 className='text-[15px] font-medium text-[#0F2C53] sm:text-xl'></h3>
<h3 className='text-[15px] font-medium text-[#0F2C53] sm:text-xl'>
{t('commissionCalculation')}
</h3>
</div>
<div className={'mb-4 text-[10px] font-light text-[#0F2C53] sm:text-base'}>
*Pro
Plan计算
{t('commissionCalculationInfo')}
</div>
{(() => {
// 假设以 Pro 计划计算:$60/月,$576/年
// Pro plan: $60/month, $576/year
const MONTHLY_PRICE = 60;
const YEARLY_PRICE = 576;
@ -174,20 +175,20 @@ export default function Affiliate() {
return (
<div className='space-y-4'>
{/* 计算面板容器 */}
{/* Calculator panel container */}
<div className='grid grid-cols-[1.5fr_2.5fr_3fr] items-stretch rounded-[34px] bg-white/10 px-5 pb-6 shadow-[inset_0_0_15.7px_0_rgba(0,0,0,0.25)]'>
{/* 左:行表头(月付套餐 / 年付套餐) */}
{/* Left: row headers (Monthly Plan / Yearly Plan) */}
<div className='flex flex-col justify-stretch font-semibold text-[#0F2C53]'>
<div className='flex h-[56px] items-center justify-center border-b-[3px] border-white'></div>
<div className='flex h-[81px] items-center justify-center border-b-[3px] border-white'>
{t('monthlyPackage')}
</div>
<div className='flex h-[81px] items-center justify-center border-b-[3px] border-white'>
{t('annualPackage')}
</div>
</div>
{/* 中:首充用户(双层圆角 + 三行) */}
{/* Middle: First-time top-up users (double rounded corners + three rows) */}
<div className='relative'>
<div
className={
@ -195,11 +196,11 @@ export default function Affiliate() {
}
></div>
<div className={'absolute bottom-0 z-0 h-[3px] w-full bg-white'}></div>
<div className='absolute z-20 w-full'>
<div className={'absolute z-20 w-full'}>
<div className='overflow-hidden rounded-[14px] text-center text-[#0F2C53]'>
<div className={'rounded-t-[14px] bg-[#A8D4ED] px-1 sm:px-4'}>
<div className='mt-3 flex h-[44px] items-center justify-center border-b-[3px] border-white font-bold'>
{t('firstTimeTopUpUser')}
</div>
</div>
@ -231,14 +232,14 @@ export default function Affiliate() {
</div>
</div>
</div>
{/* 蓝色投影块 */}
{/* Blue shadow block */}
<div className='pointer-events-none absolute inset-0 -z-10 translate-x-2 translate-y-2 rounded-[20px] bg-[#225BA9]' />
</div>
{/* 右:再次充值用户(灰卡 + 三行) */}
{/* Right: Repeat top-up users (gray card + three rows) */}
<div className='text-center text-[#0F2C53]'>
<div className='flex h-[56px] items-center justify-center border-b-[3px] border-white pt-3 font-bold'>
{t('repeatTopUpUser')}
</div>
<div className='grid h-[81px] grid-cols-1 grid-rows-2 items-center justify-center border-b-[3px] border-white'>
@ -250,7 +251,7 @@ export default function Affiliate() {
<span className='font-semibold'>
<Display type='currency' value={recurMonth} />
</span>{' '}
/
{t('perMonth')}
</div>
</div>
@ -263,12 +264,12 @@ export default function Affiliate() {
<span className='font-semibold'>
<Display type='currency' value={recurYear} />
</span>{' '}
/
{t('perYear')}
</div>
</div>
</div>
</div>
{/* 用户数调节 */}
{/* User count adjustment */}
<div className='flex items-center justify-center gap-4'>
<Button
variant='secondary'
@ -285,7 +286,7 @@ export default function Affiliate() {
className='h-6 border-0 p-0 text-center text-sm focus-visible:ring-0 sm:h-8'
style={{ width: `${Math.max(3, String(count).length + 1)}ch` }}
/>
<span className='text-sm'>Users</span>
<span className='text-sm'>{t('users')}</span>
</div>
<Button
variant='secondary'

View File

@ -31,7 +31,7 @@ interface PurchaseProps {
}
interface PurchaseDialogRef {
show: (subscribe: API.Subscribe) => void;
show: (subscribe: API.Subscribe, tabValue: string) => void;
hide: () => void;
}
@ -123,7 +123,7 @@ const Purchase = forwardRef<PurchaseDialogRef, PurchaseProps>((props, ref) => {
</DialogHeader>
<div>
<div className='pl-4 text-4xl font-bold text-[#0F2C53] sm:mb-8 sm:pl-0 sm:text-center sm:text-4xl'>
{t('purchaseTitle')}
</div>
<div>
<Tabs
@ -142,7 +142,7 @@ const Purchase = forwardRef<PurchaseDialogRef, PurchaseProps>((props, ref) => {
<TabsList className='relative mb-8 h-[74px] flex-wrap rounded-full bg-[#EAEAEA] p-2.5'>
{tabValue === 'year' ? (
<span className='absolute -top-8 left-16 z-10 rounded-md bg-[#E22C2E] px-2 py-0.5 text-[10px] font-bold leading-none text-white shadow sm:text-xs'>
-20%
{t('discount20')}
{/* 小三角箭头 */}
{/* <span className="
absolute right-0 top-full
@ -160,13 +160,13 @@ const Purchase = forwardRef<PurchaseDialogRef, PurchaseProps>((props, ref) => {
className='rounded-full px-10 py-3.5 text-xl data-[state=active]:bg-[#0F2C53] data-[state=active]:text-white md:px-12'
value='year'
>
{t('yearlyPlan')}
</TabsTrigger>
<TabsTrigger
className='rounded-full px-10 py-3.5 text-xl data-[state=active]:bg-[#0F2C53] data-[state=active]:text-white md:px-12'
value='month'
>
{t('monthlyPlan')}
</TabsTrigger>
</TabsList>
</Tabs>
@ -188,8 +188,8 @@ const Purchase = forwardRef<PurchaseDialogRef, PurchaseProps>((props, ref) => {
/>
<Separator className='mb-3 mt-4 bg-[#225BA9]' />
<div className='flex items-center justify-between text-[15px] text-[#225BA9]'>
<div></div>
<div className='font-light text-[#666]'></div>
<div>{t('paymentMethod')}</div>
<div className='font-light text-[#666]'>{t('walletBalance')}</div>
</div>
</div>
<div className='mt-8 flex items-center justify-center'>

View File

@ -36,7 +36,7 @@ export default function Recharge(props: Readonly<ButtonProps>) {
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button {...props}></Button>
<Button {...props}>{t('walletRecharge')}</Button>
</DialogTrigger>
<DialogContent className='flex h-full flex-col overflow-hidden md:h-auto'>
<DialogHeader>

View File

@ -103,7 +103,7 @@ export default function Renewal({ id, subscribe, className }: Readonly<RenewalPr
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button size='sm' className={className}>
{t('renewPlan')}
</Button>
</DialogTrigger>
<DialogContent className='flex h-full max-w-screen-lg flex-col overflow-hidden md:h-auto'>

View File

@ -1,11 +1,24 @@
{
"commissionInfo": "Statistics of the commission, automatically transferred to balance",
"annualPackage": "Annual Package",
"commissionCalculation": "Commission Calculation",
"commissionCalculationInfo": "Fill in the corresponding number of invited users below to calculate the commission amount at different ratios. *This table is calculated based on the Pro Plan. The ratios for other plans remain unchanged, and the calculation is based on the actual amount.",
"commissionInfo": "Commission amount, which is automatically transferred to the wallet balance after a successful invitation.",
"commissionInviteCode": "Commission Invite Code",
"commissionRate": "Commission Rate",
"copyInviteLink": "Copy Invite Link",
"copySuccess": "Copied Successfully",
"firstTimeTopUpUser": "First-time Top-up User",
"historicalRecommendedUsers": "Historical Recommended Users: 7",
"inviteCode": "Invite Code",
"inviteRecords": "Invite Records",
"monthlyPackage": "Monthly Package",
"more": "More",
"perMonth": "/ Month",
"perYear": "/ Year",
"registrationTime": "Registration Time",
"repeatTopUpUser": "Repeat Top-up User",
"time": "Time",
"totalCommission": "Total Commission",
"userIdentifier": "User Identifier"
"userIdentifier": "User Identifier",
"users": "Users"
}

View File

@ -1,9 +1,17 @@
{
"accountBalance": "Account Balance",
"accountOverview": "Account Overview",
"address1": "Address 1",
"announcementTitle": "Site Announcements",
"annualPlanUser": "Annual Plan User",
"availableDevices": "Available Devices",
"beginnerTutorial": "Beginner Tutorial",
"cancel": "Cancel",
"confirm": "Confirm",
"confirmResetSubscription": "Are you sure you want to reset the subscription address?",
"copy": "Copy",
"copyFailure": "Copy failed, please copy manually",
"copySubscriptionLinkOrScanQrCode": "Copy subscription link or click the QR code button to scan",
"copySuccess": "Copy successful",
"deducted": "Canceled",
"download": "Download",
@ -11,20 +19,35 @@
"expired": "Expired",
"finished": "Traffic exhausted",
"import": "Import",
"inEffect": "In Effect",
"latestAnnouncement": "Latest Announcement",
"loading": "Loading...",
"manualImportMessage": "This app does not support activation. Please import manually. The subscription address has been copied.",
"more": "More",
"mySubscription": "My Subscription",
"mySubscriptions": "My Subscriptions",
"nextResetDays": "Next Reset in Days",
"noLimit": "No Limit",
"noReset": "No Reset",
"online": "Online: ",
"pageOf": "Page {pageIndex} of {pageCount}",
"pinnedAnnouncement": "[Pinned]",
"planExpirationTime": "Plan Expiration Time: ",
"planStatus": "Plan Status",
"prompt": "Prompt",
"purchaseSubscription": "Purchase Subscription",
"qrCode": "QR Code",
"remaining": "Remaining: ",
"resetSubscription": "Reset Subscription Address",
"resetSuccess": "Reset successful",
"rowsPerPage": "Rows per page",
"scanCodeToSubscribe": "Scan code to subscribe",
"scanToSubscribe": "Scan to Subscribe",
"siteAnnouncements": "Site Announcements",
"subscriptionUrl": "Subscription URL",
"totalTraffic": "Total Traffic",
"unknown": "Unknown",
"used": "Used"
"used": "Used",
"usedTrafficTotalTraffic": "Used Traffic/Total Traffic: ",
"viewDetails": "View Details"
}

View File

@ -2,5 +2,7 @@
"all": "All",
"document": "Document",
"read": "Read",
"tutorial": "Tutorial"
"tutorial": "Tutorial",
"tutorialDescription": "Select the corresponding operating system to view the corresponding software configuration tutorial",
"tutorialTitle": "Beginner Tutorial"
}

View File

@ -1,6 +1,7 @@
{
"balanceRecharge": "Balance Recharge",
"cancel": "Cancel",
"cancelOrder": "Cancel Order",
"createdAt": "Created At",
"detail": "Detail",
"goToPayment": "Go to Payment",
@ -13,6 +14,7 @@
},
"name": "Name",
"orderClosed": "Order Closed",
"orderDetail": "Order Detail",
"orderList": "Order List",
"orderNo": "Order Number",
"orderNumber": "Order Number",
@ -43,5 +45,6 @@
"4": "Recharge"
},
"viewDocument": "View Document",
"waitingForPayment": "Waiting for Payment"
"waitingForPayment": "Waiting for Payment",
"walletBalance": "Wallet Balance"
}

View File

@ -5,7 +5,9 @@
"passwordMismatch": "Passwords do not match",
"repeatNewPassword": "Repeat New Password",
"updatePassword": "Update Password",
"updateSuccess": "Update Successful"
"updateSuccess": "Update Successful",
"save": "Save",
"description": "Change login password"
},
"notify": {
"balanceChange": "Balance Change",
@ -16,7 +18,8 @@
"notificationTypes": "Notification Types",
"save": "Save Changes",
"subscribe": "Subscribe",
"updateSuccess": "Update Successful"
"updateSuccess": "Update Successful",
"description": "Enable or disable email notifications"
},
"thirdParty": {
"apple": {
@ -37,6 +40,7 @@
"placeholder": "Enter your email address",
"title": "Change Email"
},
"emailLabel": "Email",
"facebook": {
"description": "Sign in with Facebook"
},

View File

@ -22,6 +22,7 @@
"buySubscription": "Buy Subscription",
"category": "Category",
"coupon": "Coupon",
"description": "Choose the service plan that suits you best",
"detail": {
"availableTraffic": "Available Traffic",
"connectedDevices": "Connected Devices",
@ -29,6 +30,7 @@
"productDetail": "Product Details"
},
"discount": "Discount",
"discount20": "-20%",
"discountInfo": "Discount Info",
"enterAmount": "Enter recharge amount",
"enterCoupon": "Enter Coupon Code",
@ -39,20 +41,24 @@
"stripe_alipay": "Stripe (Alipay)",
"stripe_wechat_pay": "Stripe (WeChat)"
},
"monthlyPlan": "Monthly Plan",
"paymentMethod": "Payment Method",
"productDescription": "Product Description",
"products": "Products",
"purchaseDuration": "Purchase Duration",
"purchaseTitle": "Purchase Plan",
"recharge": "Recharge",
"rechargeAmount": "Recharge Amount",
"rechargeDescription": "One-click recharge, easy to handle",
"rechargeNow": "Recharge Now",
"renew": "Renew",
"renewPlan": "Renew Plan",
"renewSubscription": "Renew Subscription",
"resetPrice": "Reset Price",
"resetTraffic": "Reset Traffic",
"resetTrafficDescription": "Reset traffic to zero, and start a new billing cycle",
"resetTrafficTitle": "Reset Traffic",
"title": "Choose a Plan",
"unsubscribe": {
"cancel": "Cancel",
"confirm": "Confirm",
@ -63,5 +69,8 @@
"success": "You have been unsubscribed successfully.",
"unsubscribe": "Unsubscribe",
"unsubscribeDescription": "Please note: If you unsubscribe now, the remaining value of the subscription will be refunded to your account balance, which can be used for your next subscription purchase or renewal."
}
},
"walletBalance": "Wallet Balance",
"walletRecharge": "Wallet Recharge",
"yearlyPlan": "Yearly Plan"
}

View File

@ -1,10 +1,13 @@
{
"accountBalance": "Account Balance",
"amount": "Amount",
"assetOverview": "Asset Overview",
"balance": "Balance",
"commission": "Commission",
"createdAt": "Time",
"giftAmount": "Girt Amount",
"referralCode": "Referral Code",
"referralDetails": "Referral Details",
"totalAssets": "Total Assets",
"type": {
"0": "Type",

View File

@ -1,11 +1,24 @@
{
"commissionInfo": "统计金额,邀请佣金自动转入余额",
"annualPackage": "年付套餐",
"commissionCalculation": "佣金计算",
"commissionCalculationInfo": "在下方填入对应邀请用户数量,即可计算不同比例返佣金额 *该表以Pro Plan计算其它套餐比例不变以实际金额计算为准",
"commissionInfo": "佣金金额,邀请成功后自动转入钱包余额",
"commissionInviteCode": "返佣邀请码",
"commissionRate": "佣金比例",
"copyInviteLink": "复制邀请链接",
"copySuccess": "复制成功",
"firstTimeTopUpUser": "首充用户",
"historicalRecommendedUsers": "历史推荐用户7",
"inviteCode": "邀请码",
"inviteRecords": "邀请记录",
"monthlyPackage": "月付套餐",
"more": "更多",
"perMonth": "/ 月",
"perYear": "/ 年",
"registrationTime": "注册时间",
"repeatTopUpUser": "再次充值用户",
"time": "时间",
"totalCommission": "佣金总额",
"userIdentifier": "用户标识符"
"userIdentifier": "用户识别代码",
"users": "用户"
}

View File

@ -1,9 +1,17 @@
{
"accountBalance": "账户余额",
"accountOverview": "账户概况",
"address1": "地址1",
"announcementTitle": "网站公告",
"annualPlanUser": "年度套餐用户",
"availableDevices": "可用设备",
"beginnerTutorial": "新手教程",
"cancel": "取消",
"confirm": "确认",
"confirmResetSubscription": "是否确认重置订阅地址?",
"copy": "复制",
"copyFailure": "复制失败,请手动复制",
"copySubscriptionLinkOrScanQrCode": "复制订阅链接或点击二维码按钮扫码",
"copySuccess": "复制成功",
"deducted": "已取消",
"download": "下载",
@ -11,20 +19,35 @@
"expired": "已过期",
"finished": "流量已用尽",
"import": "导入",
"inEffect": "生效中",
"latestAnnouncement": "最新公告",
"loading": "加载中...",
"manualImportMessage": "该应用暂不支持唤起,请手动导入,已自动复制订阅地址",
"more": "更多",
"mySubscription": "我的订阅",
"mySubscriptions": "我的订阅",
"nextResetDays": "下次重置/天",
"noLimit": "无限制",
"noReset": "不重置",
"online": "在线:",
"pageOf": "第 {pageIndex} 页,共 {pageCount} 页",
"pinnedAnnouncement": "【置顶公告】",
"planExpirationTime": "套餐到期时间:",
"planStatus": "套餐状态",
"prompt": "提示",
"purchaseSubscription": "购买订阅",
"qrCode": "二维码",
"remaining": "剩余:",
"resetSubscription": "重置订阅地址",
"resetSuccess": "重置成功",
"rowsPerPage": "每页显示",
"scanCodeToSubscribe": "扫描码订阅",
"scanToSubscribe": "扫描订阅",
"siteAnnouncements": "网站公告",
"subscriptionUrl": "订阅地址",
"totalTraffic": "总流量",
"unknown": "未知",
"used": "已用"
"used": "已用",
"usedTrafficTotalTraffic": "已使用流量/总流量:",
"viewDetails": "查看详情"
}

View File

@ -2,5 +2,7 @@
"all": "全部",
"document": "文档",
"read": "阅读",
"tutorial": "教程"
"tutorial": "教程",
"tutorialDescription": "选择对应操作系统,查看对应软件配置教程",
"tutorialTitle": "新手教程"
}

View File

@ -1,6 +1,7 @@
{
"balanceRecharge": "余额充值",
"cancel": "取消",
"cancelOrder": "取消订单",
"createdAt": "创建时间",
"detail": "详情",
"goToPayment": "前往支付",
@ -13,6 +14,7 @@
},
"name": "名称",
"orderClosed": "订单已关闭",
"orderDetail": "订单详情",
"orderList": "订单列表",
"orderNo": "订单号",
"orderNumber": "订单编号",
@ -43,5 +45,6 @@
"4": "充值"
},
"viewDocument": "查看文档",
"waitingForPayment": "等待支付"
"waitingForPayment": "等待支付",
"walletBalance": "钱包余额"
}

View File

@ -5,7 +5,9 @@
"passwordMismatch": "两次密码不一致",
"repeatNewPassword": "重复新密码",
"updatePassword": "更新密码",
"updateSuccess": "更新成功"
"updateSuccess": "更新成功",
"save": "保存",
"description": "修改登录密码"
},
"notify": {
"balanceChange": "余额变动",
@ -16,7 +18,8 @@
"notificationTypes": "通知类型",
"save": "保存更改",
"subscribe": "订阅",
"updateSuccess": "更新成功"
"updateSuccess": "更新成功",
"description": "是否邮箱通知推送"
},
"thirdParty": {
"apple": {
@ -37,6 +40,7 @@
"placeholder": "输入邮箱地址",
"title": "更改邮箱"
},
"emailLabel": "邮箱",
"facebook": {
"description": "使用 Facebook 登录"
},

View File

@ -22,6 +22,7 @@
"buySubscription": "购买订阅",
"category": "类别",
"coupon": "优惠券",
"description": "选择最适合您的服务套餐",
"detail": {
"availableTraffic": "可用流量",
"connectedDevices": "同时连接 IP 数",
@ -29,6 +30,7 @@
"productDetail": "商品详情"
},
"discount": "折扣",
"discount20": "-20%",
"discountInfo": "折扣信息",
"enterAmount": "输入充值金额",
"enterCoupon": "输入优惠券代码",
@ -39,20 +41,24 @@
"stripe_alipay": "Stripe(支付宝)",
"stripe_wechat_pay": "Stripe(微信)"
},
"monthlyPlan": "月付套餐",
"paymentMethod": "支付方式",
"productDescription": "商品描述",
"products": "商品",
"purchaseDuration": "购买时长",
"purchaseTitle": "购买套餐",
"recharge": "充值",
"rechargeAmount": "充值金额",
"rechargeDescription": "一键充值,轻松处理",
"rechargeNow": "立即充值",
"renew": "续订",
"renewPlan": "续订套餐",
"renewSubscription": "续订",
"resetPrice": "重置价格",
"resetTraffic": "重置流量",
"resetTrafficDescription": "将流量重置为零,并开始新的计费周期",
"resetTrafficTitle": "重置流量",
"title": "选择套餐",
"unsubscribe": {
"cancel": "取消",
"confirm": "确认",
@ -63,5 +69,8 @@
"success": "您已成功取消订阅。",
"unsubscribe": "取消订阅",
"unsubscribeDescription": "请注意:如果您现在取消订阅,订阅的剩余价值将退还到您的账户余额中,可用于您下次的订阅购买或续订。"
}
},
"walletBalance": "钱包余额",
"walletRecharge": "钱包充值",
"yearlyPlan": "年付套餐"
}

View File

@ -1,10 +1,13 @@
{
"accountBalance": "账户余额",
"amount": "金额",
"assetOverview": "资产概览",
"balance": "余额",
"commission": "佣金",
"createdAt": "时间",
"giftAmount": "赠送金额",
"referralCode": "返佣邀请码",
"referralDetails": "返佣详情",
"totalAssets": "资产概览",
"type": {
"0": "类型",

View File

@ -6,6 +6,9 @@ const withNextIntl = createNextIntlPlugin('./locales/request.ts');
const nextConfig: NextConfig = {
transpilePackages: ['@workspace/ui', '@workspace/airo-ui'],
output: 'standalone',
typescript: {
ignoreBuildErrors: true, // 禁用 TypeScript 构建时的类型检查
},
images: {
remotePatterns: [
{