From 07ad846e963be51294a397858370e8868971b5df Mon Sep 17 00:00:00 2001 From: Junyan Qin Date: Sun, 28 Dec 2025 22:38:11 +0800 Subject: [PATCH] feat: update dependencies and enhance account settings dialog with password management and improved UI elements --- web/package.json | 2 +- web/pnpm-lock.yaml | 2 +- web/src/app/auth/space/callback/page.tsx | 6 +- .../AccountSettingsDialog.tsx | 310 +++++++----------- .../ApiIntegrationDialog.tsx | 39 ++- .../components/home-sidebar/HomeSidebar.tsx | 127 ++++--- .../PasswordChangeDialog.tsx | 111 ++++--- web/src/components/ui/item.tsx | 193 +++++++++++ web/src/i18n/locales/en-US.ts | 28 +- web/src/i18n/locales/ja-JP.ts | 27 +- web/src/i18n/locales/zh-Hans.ts | 22 +- web/src/i18n/locales/zh-Hant.ts | 22 +- 12 files changed, 549 insertions(+), 340 deletions(-) create mode 100644 web/src/components/ui/item.tsx diff --git a/web/package.json b/web/package.json index 1b3e04f4..5709fc10 100644 --- a/web/package.json +++ b/web/package.json @@ -34,7 +34,7 @@ "@radix-ui/react-popover": "^1.1.14", "@radix-ui/react-scroll-area": "^1.2.9", "@radix-ui/react-select": "^2.2.4", - "@radix-ui/react-separator": "^1.1.7", + "@radix-ui/react-separator": "^1.1.8", "@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-switch": "^1.2.4", "@radix-ui/react-tabs": "^1.1.11", diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml index f60ecc2d..e73782bc 100644 --- a/web/pnpm-lock.yaml +++ b/web/pnpm-lock.yaml @@ -51,7 +51,7 @@ dependencies: specifier: ^2.2.4 version: 2.2.6(@types/react-dom@19.2.3)(@types/react@19.2.7)(react-dom@19.2.1)(react@19.2.1) '@radix-ui/react-separator': - specifier: ^1.1.7 + specifier: ^1.1.8 version: 1.1.8(@types/react-dom@19.2.3)(@types/react@19.2.7)(react-dom@19.2.1)(react@19.2.1) '@radix-ui/react-slot': specifier: ^1.2.3 diff --git a/web/src/app/auth/space/callback/page.tsx b/web/src/app/auth/space/callback/page.tsx index d0e7ddc4..a9898fbc 100644 --- a/web/src/app/auth/space/callback/page.tsx +++ b/web/src/app/auth/space/callback/page.tsx @@ -33,6 +33,7 @@ export default function SpaceOAuthCallback() { const [isBindMode, setIsBindMode] = useState(false); const [code, setCode] = useState(null); const [isProcessing, setIsProcessing] = useState(false); + const [localEmail, setLocalEmail] = useState(''); const handleOAuthCallback = useCallback( async (authCode: string) => { @@ -115,6 +116,7 @@ export default function SpaceOAuthCallback() { } setBindState(state); setIsBindMode(true); + setLocalEmail(localStorage.getItem('userEmail') || ''); setStatus('confirm'); } else { // Normal login/register mode @@ -169,7 +171,9 @@ export default function SpaceOAuthCallback() { <>

- {t('account.bindSpaceWarning')} + {t('account.bindSpaceWarning', { + localEmail: localEmail || '-', + })}

- - + {loading ? ( +
+
- - {/* Bind Space Account - only for local accounts */} - {accountType === 'local' && ( - <> - -
-

- {t('account.bindSpace')} -

-

- {t('account.bindSpaceDescription')} -

+ ) : ( +
+ {/* Password Item */} + + + + + + {t('account.passwordStatus')} + + {hasPassword + ? t('account.passwordSetDescription') + : t('account.setPasswordHint')} + + + -
- - )} -
- )} - - + + + + {/* Space Account Item */} + + + + + + + + + + {t('account.spaceStatus')} + + {accountType === 'space' + ? t('account.spaceBoundDescription') + : t('account.bindSpaceDescription')} + + + {accountType === 'local' && ( + + + + )} + +
+ )} + + + + + ); } diff --git a/web/src/app/home/components/api-integration-dialog/ApiIntegrationDialog.tsx b/web/src/app/home/components/api-integration-dialog/ApiIntegrationDialog.tsx index 68b36417..b39f9144 100644 --- a/web/src/app/home/components/api-integration-dialog/ApiIntegrationDialog.tsx +++ b/web/src/app/home/components/api-integration-dialog/ApiIntegrationDialog.tsx @@ -5,6 +5,7 @@ import { useState, useEffect } from 'react'; import { useTranslation } from 'react-i18next'; import { toast } from 'sonner'; import { Copy, Trash2, Plus } from 'lucide-react'; +import { useRouter, usePathname, useSearchParams } from 'next/navigation'; import { Dialog, DialogContent, @@ -66,6 +67,9 @@ export default function ApiIntegrationDialog({ onOpenChange, }: ApiIntegrationDialogProps) { const { t } = useTranslation(); + const router = useRouter(); + const pathname = usePathname(); + const searchParams = useSearchParams(); const [activeTab, setActiveTab] = useState('apikeys'); const [apiKeys, setApiKeys] = useState([]); const [webhooks, setWebhooks] = useState([]); @@ -84,6 +88,30 @@ export default function ApiIntegrationDialog({ const [newWebhookEnabled, setNewWebhookEnabled] = useState(true); const [deleteWebhookId, setDeleteWebhookId] = useState(null); + // Sync URL with dialog state + useEffect(() => { + if (open) { + const params = new URLSearchParams(searchParams.toString()); + params.set('action', 'showApiIntegrationSettings'); + router.replace(`${pathname}?${params.toString()}`, { scroll: false }); + } + }, [open]); + + const handleOpenChange = (newOpen: boolean) => { + if (!newOpen && (deleteKeyId || deleteWebhookId)) { + return; + } + if (!newOpen) { + const params = new URLSearchParams(searchParams.toString()); + params.delete('action'); + const newUrl = params.toString() + ? `${pathname}?${params.toString()}` + : pathname; + router.replace(newUrl, { scroll: false }); + } + onOpenChange(newOpen); + }; + // 清理 body 样式,防止对话框关闭后页面无法交互 useEffect(() => { if (!deleteKeyId && !deleteWebhookId) { @@ -231,16 +259,7 @@ export default function ApiIntegrationDialog({ return ( <> - { - // 如果删除确认框是打开的,不允许关闭主对话框 - if (!newOpen && (deleteKeyId || deleteWebhookId)) { - return; - } - onOpenChange(newOpen); - }} - > + {t('common.manageApiIntegration')} diff --git a/web/src/app/home/components/home-sidebar/HomeSidebar.tsx b/web/src/app/home/components/home-sidebar/HomeSidebar.tsx index 6ab5b3a9..bfca8a35 100644 --- a/web/src/app/home/components/home-sidebar/HomeSidebar.tsx +++ b/web/src/app/home/components/home-sidebar/HomeSidebar.tsx @@ -20,7 +20,6 @@ import { Lightbulb, LogOut, KeyRound, - User, } from 'lucide-react'; import { useTheme } from 'next-themes'; @@ -79,6 +78,12 @@ export default function HomeSidebar({ if (searchParams.get('action') === 'showModelSettings') { setModelsDialogOpen(true); } + if (searchParams.get('action') === 'showAccountSettings') { + setAccountSettingsOpen(true); + } + if (searchParams.get('action') === 'showApiIntegrationSettings') { + setApiKeyDialogOpen(true); + } }, [searchParams]); const [selectedChild, setSelectedChild] = useState(); @@ -114,6 +119,23 @@ export default function HomeSidebar({ } } + // 处理账户设置对话框的打开和关闭,同时更新 URL + function handleAccountSettingsChange(open: boolean) { + setAccountSettingsOpen(open); + if (open) { + const params = new URLSearchParams(searchParams.toString()); + params.set('action', 'showAccountSettings'); + router.replace(`${pathname}?${params.toString()}`, { scroll: false }); + } else { + const params = new URLSearchParams(searchParams.toString()); + params.delete('action'); + const newUrl = params.toString() + ? `${pathname}?${params.toString()}` + : pathname; + router.replace(newUrl, { scroll: false }); + } + } + useEffect(() => { initSelect(); if (!localStorage.getItem('token')) { @@ -337,44 +359,47 @@ export default function HomeSidebar({ -
- {t('common.theme')} - { - if (value) setTheme(value); - }} - className="justify-start" - > - - - - - - - - - - +
{ + handleAccountSettingsChange(true); + setPopoverOpen(false); + }} + > +
+ {userEmail ? userEmail.charAt(0).toUpperCase() : 'U'} +
+ + {userEmail || t('account.settings')} +
-
- - {t('common.language')} - - -
+ + { + if (value) setTheme(value); + }} + className="w-full justify-start" + > + + + + + + + + + + -
- - {t('common.integration')} - +
-
- -
- {t('common.account')} - {/* User email display */} -
string) => - z - .object({ - currentPassword: z - .string() - .min(1, { message: t('common.currentPasswordRequired') }), - newPassword: z - .string() - .min(1, { message: t('common.newPasswordRequired') }), - confirmNewPassword: z - .string() - .min(1, { message: t('common.confirmPasswordRequired') }), - }) - .refine((data) => data.newPassword === data.confirmNewPassword, { - message: t('common.passwordsDoNotMatch'), - path: ['confirmNewPassword'], - }); - interface PasswordChangeDialogProps { open: boolean; onOpenChange: (open: boolean) => void; + hasPassword?: boolean; } export default function PasswordChangeDialog({ open, onOpenChange, + hasPassword = true, }: PasswordChangeDialogProps) { const { t } = useTranslation(); const [isSubmitting, setIsSubmitting] = useState(false); - const formSchema = getFormSchema(t); + + const getFormSchema = () => + z + .object({ + currentPassword: hasPassword + ? z.string().min(1, { message: t('common.currentPasswordRequired') }) + : z.string().optional(), + newPassword: z + .string() + .min(1, { message: t('common.newPasswordRequired') }), + confirmNewPassword: z + .string() + .min(1, { message: t('common.confirmPasswordRequired') }), + }) + .refine((data) => data.newPassword === data.confirmNewPassword, { + message: t('common.passwordsDoNotMatch'), + path: ['confirmNewPassword'], + }); + + const formSchema = getFormSchema(); const form = useForm>({ resolver: zodResolver(formSchema), @@ -66,14 +69,30 @@ export default function PasswordChangeDialog({ }, }); + // Reset form when dialog opens/closes or hasPassword changes + useEffect(() => { + if (open) { + form.reset({ + currentPassword: '', + newPassword: '', + confirmNewPassword: '', + }); + } + }, [open, hasPassword, form]); + const onSubmit = async (values: z.infer) => { setIsSubmitting(true); try { - await httpClient.changePassword( - values.currentPassword, - values.newPassword, - ); - toast.success(t('common.changePasswordSuccess')); + if (hasPassword) { + await httpClient.changePassword( + values.currentPassword!, + values.newPassword, + ); + toast.success(t('common.changePasswordSuccess')); + } else { + await httpClient.setPassword(values.newPassword, undefined); + toast.success(t('account.passwordSetSuccess')); + } form.reset(); onOpenChange(false); } catch { @@ -87,27 +106,33 @@ export default function PasswordChangeDialog({ - {t('common.changePassword')} + + {hasPassword + ? t('common.changePassword') + : t('account.setPassword')} +
- ( - - {t('common.currentPassword')} - - - - - - )} - /> + {hasPassword && ( + ( + + {t('common.currentPassword')} + + + + + + )} + /> + )} ) { + return ( +
+ ); +} + +function ItemSeparator({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +const itemVariants = cva( + 'group/item flex items-center border border-transparent text-sm rounded-md transition-colors [a]:hover:bg-accent/50 [a]:transition-colors duration-100 flex-wrap outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]', + { + variants: { + variant: { + default: 'bg-transparent', + outline: 'border-border', + muted: 'bg-muted/50', + }, + size: { + default: 'p-4 gap-4 ', + sm: 'py-3 px-4 gap-2.5', + }, + }, + defaultVariants: { + variant: 'default', + size: 'default', + }, + }, +); + +function Item({ + className, + variant = 'default', + size = 'default', + asChild = false, + ...props +}: React.ComponentProps<'div'> & + VariantProps & { asChild?: boolean }) { + const Comp = asChild ? Slot : 'div'; + return ( + + ); +} + +const itemMediaVariants = cva( + 'flex shrink-0 items-center justify-center gap-2 group-has-[[data-slot=item-description]]/item:self-start [&_svg]:pointer-events-none group-has-[[data-slot=item-description]]/item:translate-y-0.5', + { + variants: { + variant: { + default: 'bg-transparent', + icon: "size-8 border rounded-sm bg-muted [&_svg:not([class*='size-'])]:size-4", + image: + 'size-10 rounded-sm overflow-hidden [&_img]:size-full [&_img]:object-cover', + }, + }, + defaultVariants: { + variant: 'default', + }, + }, +); + +function ItemMedia({ + className, + variant = 'default', + ...props +}: React.ComponentProps<'div'> & VariantProps) { + return ( +
+ ); +} + +function ItemContent({ className, ...props }: React.ComponentProps<'div'>) { + return ( +
+ ); +} + +function ItemTitle({ className, ...props }: React.ComponentProps<'div'>) { + return ( +
+ ); +} + +function ItemDescription({ className, ...props }: React.ComponentProps<'p'>) { + return ( +

a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4', + className, + )} + {...props} + /> + ); +} + +function ItemActions({ className, ...props }: React.ComponentProps<'div'>) { + return ( +

+ ); +} + +function ItemHeader({ className, ...props }: React.ComponentProps<'div'>) { + return ( +
+ ); +} + +function ItemFooter({ className, ...props }: React.ComponentProps<'div'>) { + return ( +
+ ); +} + +export { + Item, + ItemMedia, + ItemContent, + ItemActions, + ItemGroup, + ItemSeparator, + ItemTitle, + ItemDescription, + ItemHeader, + ItemFooter, +}; diff --git a/web/src/i18n/locales/en-US.ts b/web/src/i18n/locales/en-US.ts index 4a85db8a..a04abc67 100644 --- a/web/src/i18n/locales/en-US.ts +++ b/web/src/i18n/locales/en-US.ts @@ -49,7 +49,8 @@ const enUS = { loading: 'Loading...', or: 'or', loginWithSpace: 'Login with Space', - spaceLoginRecommended: 'Recommended: Sync models and credits from Space', + spaceLoginRecommended: + 'Recommended: Use official stable model APIs and cloud services', loginLocal: 'Login with local account', loginWithPassword: 'Login with password', spaceLoginTitle: 'Login with Space', @@ -707,7 +708,8 @@ const enUS = { 'The email and password you fill in will be used as the initial administrator account', register: 'Register', initWithSpace: 'Initialize with Space', - spaceRecommended: 'Recommended: Sync models and credits from Space', + spaceRecommended: + 'Recommended: Use official stable model APIs and cloud services', registerLocal: 'Register local account', registerWithPassword: 'Register with email and password', initSuccess: 'Initialization successful, please login', @@ -756,21 +758,29 @@ const enUS = { settings: 'Account Settings', setPassword: 'Set Password', passwordSetSuccess: 'Password set successfully', + passwordStatus: 'Local Password', + passwordSet: 'Set', + passwordNotSet: 'Not Set', + passwordSetDescription: + 'Password is set, you can login with email and password', + spaceStatus: 'Space Account', + spaceBound: 'Bound', + spaceNotBound: 'Not Bound', + spaceBoundDescription: + 'Space account bound, official model APIs and cloud services available', bindSpace: 'Bind Space Account', - bindSpaceDescription: - 'Link your local account to a Space account to sync models and credits', - bindSpaceButton: 'Bind Space Account', + bindSpaceDescription: 'Bind to use official model APIs and cloud services', + bindSpaceButton: 'Bind', bindSpaceConfirmTitle: 'Confirm Binding', bindSpaceConfirmDescription: - 'You are about to bind your local account to a Space account', + 'You are about to bind your local instance to a Space account', bindSpaceWarning: - 'After binding, your login email will be changed to the Space account email. You can still use email/password login if you have set a password.', + 'After binding, your login email will be changed from {{localEmail}} to the Space account email.', bindSpaceSuccess: 'Space account bound successfully', bindSpaceFailed: 'Failed to bind Space account', bindSpaceInvalidState: 'Invalid bind request. Please try again from account settings.', - setPasswordHint: - 'After setting a password, you can login with email and password', + setPasswordHint: 'Set a password to login with email and password', }, }; diff --git a/web/src/i18n/locales/ja-JP.ts b/web/src/i18n/locales/ja-JP.ts index f78687d2..39067992 100644 --- a/web/src/i18n/locales/ja-JP.ts +++ b/web/src/i18n/locales/ja-JP.ts @@ -50,7 +50,8 @@ const jaJP = { loading: '読み込み中...', or: 'または', loginWithSpace: 'Space でログイン', - spaceLoginRecommended: 'おすすめ:Space からモデルとクレジットを同期', + spaceLoginRecommended: + 'おすすめ:公式の安定したモデル API とクラウドサービスを利用', loginLocal: 'ローカルアカウントでログイン', loginWithPassword: 'パスワードでログイン', spaceLoginTitle: 'Space でログイン', @@ -715,7 +716,8 @@ const jaJP = { '入力したメールアドレスとパスワードが初期管理者アカウントになります', register: '登録', initWithSpace: 'Space で初期化', - spaceRecommended: 'おすすめ:Space からモデルとクレジットを同期', + spaceRecommended: + 'おすすめ:公式の安定したモデル API とクラウドサービスを利用', registerLocal: 'ローカルアカウントを登録', registerWithPassword: 'メールアドレスとパスワードで登録', initSuccess: '初期化に成功しました。ログインしてください', @@ -764,21 +766,30 @@ const jaJP = { settings: 'アカウント設定', setPassword: 'パスワードを設定', passwordSetSuccess: 'パスワードの設定に成功しました', + passwordStatus: 'ローカルパスワード', + passwordSet: '設定済み', + passwordNotSet: '未設定', + passwordSetDescription: + 'パスワードが設定されています。メールとパスワードでログインできます', + spaceStatus: 'Space アカウント', + spaceBound: '連携済み', + spaceNotBound: '未連携', + spaceBoundDescription: + 'Space アカウントと連携済み、公式モデル API とクラウドサービスが利用可能', bindSpace: 'Space アカウントを連携', - bindSpaceDescription: - 'ローカルアカウントを Space アカウントに連携して、モデルとクレジットを同期します', - bindSpaceButton: 'Space アカウントを連携', + bindSpaceDescription: '連携して公式モデル API とクラウドサービスを利用', + bindSpaceButton: '連携', bindSpaceConfirmTitle: '連携を確認', bindSpaceConfirmDescription: - 'ローカルアカウントを Space アカウントに連携しようとしています', + 'ローカルインスタンスを Space アカウントに連携しようとしています', bindSpaceWarning: - '連携後、ログインメールアドレスは Space アカウントのメールアドレスに変更されます。パスワードを設定している場合は、引き続きメールアドレスとパスワードでログインできます。', + '連携後、ログインメールアドレスは {{localEmail}} から Space アカウントのメールアドレスに変更されます。', bindSpaceSuccess: 'Space アカウントの連携に成功しました', bindSpaceFailed: 'Space アカウントの連携に失敗しました', bindSpaceInvalidState: '無効な連携リクエストです。アカウント設定から再度お試しください。', setPasswordHint: - 'パスワードを設定すると、メールアドレスとパスワードでログインできます', + 'パスワードを設定するとメールとパスワードでログインできます', }, }; diff --git a/web/src/i18n/locales/zh-Hans.ts b/web/src/i18n/locales/zh-Hans.ts index c60b9506..f929326f 100644 --- a/web/src/i18n/locales/zh-Hans.ts +++ b/web/src/i18n/locales/zh-Hans.ts @@ -49,7 +49,7 @@ const zhHans = { loading: '加载中...', or: '或', loginWithSpace: '通过 Space 登录', - spaceLoginRecommended: '推荐:从 Space 同步模型和点数', + spaceLoginRecommended: '推荐:使用官方提供的稳定模型 API 和云服务', loginLocal: '使用本地账号登录', loginWithPassword: '通过密码登录', spaceLoginTitle: '通过 Space 登录', @@ -680,7 +680,7 @@ const zhHans = { adminAccountNote: '您填写的邮箱和密码将作为初始管理员账号', register: '注册', initWithSpace: '通过 Space 初始化', - spaceRecommended: '推荐:从 Space 同步模型和点数', + spaceRecommended: '推荐:使用官方提供的稳定模型 API 和云服务', registerLocal: '注册本地账号', registerWithPassword: '通过邮箱密码组合注册', initSuccess: '初始化成功 请登录', @@ -727,17 +727,25 @@ const zhHans = { settings: '账户设置', setPassword: '设置密码', passwordSetSuccess: '密码设置成功', + passwordStatus: '本地密码', + passwordSet: '已设置', + passwordNotSet: '未设置', + passwordSetDescription: '您已设置本地密码,可使用邮箱密码登录', + spaceStatus: 'Space 账户', + spaceBound: '已绑定', + spaceNotBound: '未绑定', + spaceBoundDescription: '已绑定 Space 账户,可使用官方模型 API 和云服务', bindSpace: '绑定 Space 账户', - bindSpaceDescription: '将本地账户关联到 Space 账户,以同步模型和点数', - bindSpaceButton: '绑定 Space 账户', + bindSpaceDescription: '绑定后可使用官方模型 API 和云服务', + bindSpaceButton: '绑定', bindSpaceConfirmTitle: '确认绑定', - bindSpaceConfirmDescription: '您即将把本地账户绑定到 Space 账户', + bindSpaceConfirmDescription: '您即将把本地实例绑定到 Space 账户', bindSpaceWarning: - '绑定后,您的登录邮箱将更改为 Space 账户的邮箱。如果您已设置密码,仍可使用邮箱密码登录。', + '绑定后,您的登录邮箱将从 {{localEmail}} 更改为 Space 账户的邮箱。', bindSpaceSuccess: 'Space 账户绑定成功', bindSpaceFailed: '绑定 Space 账户失败', bindSpaceInvalidState: '无效的绑定请求,请从账户设置重新发起', - setPasswordHint: '设置密码后,您可使用邮箱、密码组合登录', + setPasswordHint: '设置密码后可使用邮箱密码登录', }, }; diff --git a/web/src/i18n/locales/zh-Hant.ts b/web/src/i18n/locales/zh-Hant.ts index 5b8fbb97..e4a8267b 100644 --- a/web/src/i18n/locales/zh-Hant.ts +++ b/web/src/i18n/locales/zh-Hant.ts @@ -49,7 +49,7 @@ const zhHant = { loading: '載入中...', or: '或', loginWithSpace: '透過 Space 登入', - spaceLoginRecommended: '推薦:從 Space 同步模型和點數', + spaceLoginRecommended: '推薦:使用官方提供的穩定模型 API 和雲服務', loginLocal: '使用本地帳號登入', loginWithPassword: '透過密碼登入', spaceLoginTitle: '透過 Space 登入', @@ -677,7 +677,7 @@ const zhHant = { adminAccountNote: '您填寫的電子郵件和密碼將作為初始管理員帳號', register: '註冊', initWithSpace: '透過 Space 初始化', - spaceRecommended: '推薦:從 Space 同步模型和點數', + spaceRecommended: '推薦:使用官方提供的穩定模型 API 和雲服務', registerLocal: '註冊本地帳號', registerWithPassword: '透過電子郵件密碼組合註冊', initSuccess: '初始化成功 請登入', @@ -724,17 +724,25 @@ const zhHant = { settings: '帳戶設定', setPassword: '設定密碼', passwordSetSuccess: '密碼設定成功', + passwordStatus: '本地密碼', + passwordSet: '已設定', + passwordNotSet: '未設定', + passwordSetDescription: '您已設定本地密碼,可使用電子郵件密碼登入', + spaceStatus: 'Space 帳戶', + spaceBound: '已綁定', + spaceNotBound: '未綁定', + spaceBoundDescription: '已綁定 Space 帳戶,可使用官方模型 API 和雲服務', bindSpace: '綁定 Space 帳戶', - bindSpaceDescription: '將本地帳戶關聯到 Space 帳戶,以同步模型和點數', - bindSpaceButton: '綁定 Space 帳戶', + bindSpaceDescription: '綁定後可使用官方模型 API 和雲服務', + bindSpaceButton: '綁定', bindSpaceConfirmTitle: '確認綁定', - bindSpaceConfirmDescription: '您即將把本地帳戶綁定到 Space 帳戶', + bindSpaceConfirmDescription: '您即將把本地實例綁定到 Space 帳戶', bindSpaceWarning: - '綁定後,您的登入電子郵件將更改為 Space 帳戶的電子郵件。如果您已設定密碼,仍可使用電子郵件密碼登入。', + '綁定後,您的登入電子郵件將從 {{localEmail}} 更改為 Space 帳戶的電子郵件。', bindSpaceSuccess: 'Space 帳戶綁定成功', bindSpaceFailed: '綁定 Space 帳戶失敗', bindSpaceInvalidState: '無效的綁定請求,請從帳戶設定重新發起', - setPasswordHint: '設定密碼後,您可使用電子郵件、密碼組合登入', + setPasswordHint: '設定密碼後可使用電子郵件密碼登入', }, };