diff --git a/frontend/src/i18n/locales/en.ts b/frontend/src/i18n/locales/en.ts deleted file mode 100644 index 6761311469..0000000000 --- a/frontend/src/i18n/locales/en.ts +++ /dev/null @@ -1,7554 +0,0 @@ -export default { - batchImageGuide: { - title: 'Batch Image Generation', - description: 'Submit multiple prompts in one job and download the generated images when complete' - }, - // Home Page - home: { - viewOnGithub: 'View on GitHub', - viewDocs: 'View Documentation', - docs: 'Docs', - switchToLight: 'Switch to Light Mode', - switchToDark: 'Switch to Dark Mode', - dashboard: 'Dashboard', - login: 'Login', - getStarted: 'Get Started', - goToDashboard: 'Go to Dashboard', - // User-focused value proposition - heroSubtitle: 'One Key, All AI Models', - heroDescription: 'No need to manage multiple subscriptions. Access Claude, GPT, Gemini and more with a single API key', - tags: { - subscriptionToApi: 'Subscription to API', - stickySession: 'Session Persistence', - realtimeBilling: 'Pay As You Go' - }, - // Pain points section - painPoints: { - title: 'Sound Familiar?', - items: { - expensive: { - title: 'High Subscription Costs', - desc: 'Paying for multiple AI subscriptions that add up every month' - }, - complex: { - title: 'Account Chaos', - desc: 'Managing scattered accounts and API keys across different platforms' - }, - unstable: { - title: 'Service Interruptions', - desc: 'Single accounts hitting rate limits and disrupting your workflow' - }, - noControl: { - title: 'No Usage Control', - desc: "Can't track where your money goes or limit team member usage" - } - } - }, - // Solutions section - solutions: { - title: 'We Solve These Problems', - subtitle: 'Three simple steps to stress-free AI access' - }, - features: { - unifiedGateway: 'One-Click Access', - unifiedGatewayDesc: 'Get a single API key to call all connected AI models. No separate applications needed.', - multiAccount: 'Always Reliable', - multiAccountDesc: 'Smart routing across multiple upstream accounts with automatic failover. Say goodbye to errors.', - balanceQuota: 'Pay What You Use', - balanceQuotaDesc: 'Usage-based billing with quota limits. Full visibility into team consumption.' - }, - // Comparison section - comparison: { - title: 'Why Choose Us?', - headers: { - feature: 'Comparison', - official: 'Official Subscriptions', - us: 'Our Platform' - }, - items: { - pricing: { - feature: 'Pricing', - official: 'Fixed monthly fee, pay even if unused', - us: 'Pay only for what you use' - }, - models: { - feature: 'Model Selection', - official: 'Single provider only', - us: 'Switch between models freely' - }, - management: { - feature: 'Account Management', - official: 'Manage each service separately', - us: 'Unified key, one dashboard' - }, - stability: { - feature: 'Stability', - official: 'Single account rate limits', - us: 'Multi-account pool, auto-failover' - }, - control: { - feature: 'Usage Control', - official: 'Not available', - us: 'Quotas & detailed analytics' - } - } - }, - providers: { - title: 'Supported AI Models', - description: 'One API, Multiple Choices', - supported: 'Supported', - soon: 'Soon', - claude: 'Claude', - gemini: 'Gemini', - antigravity: 'Antigravity', - more: 'More' - }, - // CTA section - cta: { - title: 'Ready to Get Started?', - description: 'Sign up now and get free trial credits to experience seamless AI access', - button: 'Sign Up Free' - }, - footer: { - allRightsReserved: 'All rights reserved.' - } - }, - - // Key Usage Query Page - keyUsage: { - title: 'API Key Usage', - subtitle: 'Enter your API Key to view real-time spending and usage status', - placeholder: 'sk-ant-mirror-xxxxxxxxxxxx', - query: 'Query', - querying: 'Querying...', - privacyNote: 'Your Key is processed locally in the browser and will not be stored', - dateRange: 'Date Range:', - dateRangeToday: 'Today', - dateRange7d: '7 Days', - dateRange30d: '30 Days', - dateRange90d: '90 Days', - dateRangeCustom: 'Custom', - apply: 'Apply', - used: 'Used', - detailInfo: 'Detail Information', - tokenStats: 'Token Statistics', - dailyDetail: 'Daily Detail', - modelStats: 'Model Usage Statistics', - // Table headers - date: 'Date', - model: 'Model', - requests: 'Requests', - inputTokens: 'Input Tokens', - outputTokens: 'Output Tokens', - cacheCreationTokens: 'Cache Creation', - cacheReadTokens: 'Cache Read', - cacheWriteTokens: 'Cache Write', - totalTokens: 'Total Tokens', - cost: 'Cost', - // Status - quotaMode: 'Key Quota Mode', - walletBalance: 'Wallet Balance', - // Ring card titles - totalQuota: 'Total Quota', - limit5h: '5-Hour Limit', - limitDaily: 'Daily Limit', - limit7d: '7-Day Limit', - limitWeekly: 'Weekly Limit', - limitMonthly: 'Monthly Limit', - // Detail rows - remainingQuota: 'Remaining Quota', - expiresAt: 'Expires At', - todayExpires: '(expires today)', - daysLeft: '({days} days)', - usedQuota: 'Used Quota', - resetNow: 'Resetting soon', - subscriptionType: 'Subscription Type', - subscriptionExpires: 'Subscription Expires', - // Usage stat cells - todayRequests: 'Today Requests', - todayInputTokens: 'Today Input', - todayOutputTokens: 'Today Output', - todayTokens: 'Today Tokens', - todayCacheCreation: 'Today Cache Creation', - todayCacheRead: 'Today Cache Read', - todayCost: 'Today Cost', - rpmTpm: 'RPM / TPM', - totalRequests: 'Total Requests', - totalInputTokens: 'Total Input', - totalOutputTokens: 'Total Output', - totalTokensLabel: 'Total Tokens', - totalCacheCreation: 'Total Cache Creation', - totalCacheRead: 'Total Cache Read', - totalCost: 'Total Cost', - avgDuration: 'Avg Duration', - // Messages - enterApiKey: 'Please enter an API Key', - querySuccess: 'Query successful', - queryFailed: 'Query failed', - queryFailedRetry: 'Query failed, please try again later', - noDailyUsage: 'No daily usage data', - }, - - // Setup Wizard - setup: { - title: 'Sub2API Setup', - description: 'Configure your Sub2API instance', - database: { - title: 'Database Configuration', - description: 'Connect to your PostgreSQL database', - host: 'Host', - port: 'Port', - username: 'Username', - password: 'Password', - databaseName: 'Database Name', - sslMode: 'SSL Mode', - passwordPlaceholder: 'Password', - ssl: { - disable: 'Disable', - require: 'Require', - verifyCa: 'Verify CA', - verifyFull: 'Verify Full' - } - }, - redis: { - title: 'Redis Configuration', - description: 'Connect to your Redis server', - host: 'Host', - port: 'Port', - password: 'Password (optional)', - database: 'Database', - passwordPlaceholder: 'Password', - enableTls: 'Enable TLS', - enableTlsHint: 'Use TLS when connecting to Redis (public CA certs)' - }, - admin: { - title: 'Admin Account', - description: 'Create your administrator account', - email: 'Email', - password: 'Password', - confirmPassword: 'Confirm Password', - passwordPlaceholder: 'Min 8 characters', - confirmPasswordPlaceholder: 'Confirm password', - passwordMismatch: 'Passwords do not match' - }, - ready: { - title: 'Ready to Install', - description: 'Review your configuration and complete setup', - database: 'Database', - redis: 'Redis', - adminEmail: 'Admin Email' - }, - status: { - testing: 'Testing...', - success: 'Connection Successful', - testConnection: 'Test Connection', - installing: 'Installing...', - completeInstallation: 'Complete Installation', - completed: 'Installation completed!', - redirecting: 'Redirecting to login page...', - restarting: 'Service is restarting, please wait...', - timeout: 'Service restart is taking longer than expected. Please refresh the page manually.' - } - }, - - // Common - common: { - loading: 'Loading...', - submitting: 'Submitting...', - justNow: 'just now', - peakRateTooltip: 'Peak rate: {window}', - peakRateImageNote: '; image tokens billed as tokens are also affected, per-image billing is unaffected', - save: 'Save', - saved: 'Saved successfully', - deleted: 'Deleted successfully', - cancel: 'Cancel', - delete: 'Delete', - edit: 'Edit', - create: 'Create', - update: 'Update', - confirm: 'Confirm', - reset: 'Reset', - search: 'Search', - filter: 'Filter', - export: 'Export', - import: 'Import', - actions: 'Actions', - status: 'Status', - name: 'Name', - email: 'Email', - password: 'Password', - submit: 'Submit', - back: 'Back', - next: 'Next', - yes: 'Yes', - no: 'No', - all: 'All', - none: 'None', - selectAll: 'Select all', - noData: 'No data', - expand: 'Expand', - collapse: 'Collapse', - success: 'Success', - error: 'Error', - critical: 'Critical', - warning: 'Warning', - info: 'Info', - active: 'Active', - inactive: 'Inactive', - more: 'More', - close: 'Close', - enabled: 'Enabled', - disabled: 'Disabled', - total: 'Total', - balance: 'Balance', - availableBalance: 'Available balance', - frozenBalance: 'Frozen balance', - totalBalance: 'Total balance', - available: 'Available', - copiedToClipboard: 'Copied to clipboard', - copied: 'Copied', - copyFailed: 'Failed to copy', - verifying: 'Verifying...', - processing: 'Processing...', - contactSupport: 'Contact Support', - add: 'Add', - invalidEmail: 'Please enter a valid email address', - optional: 'optional', - selectOption: 'Select an option', - searchPlaceholder: 'Search...', - noOptionsFound: 'No options found', - noGroupsAvailable: 'No groups available', - unknownError: 'Unknown error occurred', - saving: 'Saving...', - selectedCount: '({count} selected)', - refresh: 'Refresh', - autoRefresh: { - title: 'Auto Refresh', - enable: 'Enable auto refresh', - countdown: 'Auto refresh: {seconds}s', - seconds: '{n} seconds', - }, - view: 'View', - settings: 'Settings', - chooseFile: 'Choose File', - copy: 'Copy', - notAvailable: 'N/A', - now: 'Now', - today: 'Today', - tomorrow: 'Tomorrow', - unknown: 'Unknown', - minutes: 'min', - time: { - never: 'Never', - justNow: 'Just now', - minutesAgo: '{n}m ago', - hoursAgo: '{n}h ago', - daysAgo: '{n}d ago', - countdown: { - daysHours: '{d}d {h}h', - hoursMinutes: '{h}h {m}m', - minutes: '{m}m', - withSuffix: '{time} to lift' - } - } - }, - - adminCompliance: { - title: 'Deployment and Operation Compliance Acknowledgment', - blockingNotice: 'Deployment and operation compliance acknowledgment is required before continuing to use the console.', - riskNotice: 'This acknowledgment provides clear, conspicuous, and reproducible notice of compliance obligations and operation risks for self-hosted instances.', - version: 'Document Version', - openDocument: 'Open the GitHub document', - documentSource: 'The agreement text comes from Markdown files in this project repository. When the agreement content changes, the document version must be incremented; acknowledgments of older versions become invalid and console users must acknowledge again.', - inputLabel: 'Type the following confirmation phrase exactly', - inputPlaceholder: 'Type the confirmation phrase to continue', - inputMismatch: 'The confirmation phrase does not match. Type the displayed text exactly.', - legalNote: 'This acknowledgment defines the no-affiliation relationship and responsibility boundary between self-hosted instances and the open-source project, copyright holders, contributors, and maintainers. The party that deploys, operates, or controls the relevant instance remains independently responsible for its applicable obligations.', - logout: 'Log out', - accept: 'Acknowledge and Continue', - accepted: 'Compliance acknowledgment recorded', - acceptFailed: 'Failed to submit acknowledgment' - }, - - legal: { - loadFailed: 'Failed to load document', - retryLater: 'Refresh the page and try again later.', - notFound: 'Document not found', - notFoundDescription: 'This legal document does not exist or has been removed by an administrator.', - updatedAt: 'Updated: {date}', - empty: 'No content', - loginAgreement: 'Login Agreement', - adminCompliance: 'Deployment and Operation Compliance Commitment', - loginAgreementPrompt: { - checkboxPrefix: 'I have read and agree to ', - documentSeparator: ', ', - noticeTitle: 'Accept the latest terms before continuing.', - noticeDescription: 'Account/password login and quick sign-in stay disabled until you accept.', - viewTerms: 'View terms', - dialogTitle: 'Terms Update Notice', - dialogDescription: 'Our service terms were updated on {date}. Please read and accept the following terms before continuing.', - recently: 'recently', - relatedDocuments: 'Related documents', - reject: 'Reject', - accept: 'Accept and continue', - loginRejectedWarning: 'Account/password login and quick sign-in are disabled until you accept the latest terms.', - loginRequiredWarning: 'Please read and accept the latest terms before logging in.', - registerRejectedWarning: 'Registration and quick sign-in are disabled until you accept the latest terms.', - registerRequiredWarning: 'Please read and accept the latest terms before registering.' - } - }, - - // Navigation - nav: { - dashboard: 'Dashboard', - announcements: 'Announcements', - apiKeys: 'API Keys', - batchImage: 'Batch Images', - usage: 'Usage', - redeem: 'Redeem', - affiliate: 'Affiliate Rebates', - affiliateManagement: 'Affiliate Rebates', - affiliateInviteRecords: 'Invite Records', - affiliateRebateRecords: 'Rebate Records', - affiliateTransferRecords: 'Transfer Records', - profile: 'Profile', - users: 'Users', - groups: 'Groups', - channels: 'Channels', - availableChannels: 'Available Channels', - subscriptions: 'Subscriptions', - accounts: 'Accounts', - proxies: 'Proxies', - redeemCodes: 'Redeem Codes', - ops: 'Ops', - promoCodes: 'Promo Codes', - settings: 'Settings', - myAccount: 'My Account', - lightMode: 'Light Mode', - darkMode: 'Dark Mode', - collapse: 'Collapse', - expand: 'Expand', - logout: 'Logout', - github: 'GitHub', - mySubscriptions: 'My Subscriptions', - buySubscription: 'Recharge / Subscription', - docs: 'Docs', - myOrders: 'My Orders', - orderManagement: 'Orders', - paymentDashboard: 'Payment Dashboard', - paymentConfig: 'Payment Config', - paymentPlans: 'Plans', - channelManagement: 'Channels', - channelPricing: 'Channel Pricing', - channelMonitor: 'Channel Monitor', - channelStatus: 'Channel Status', - riskControl: 'Risk Control', - }, - - // Auth - auth: { - welcomeBack: 'Welcome Back', - signInToAccount: 'Sign in to your account to continue', - signIn: 'Sign In', - signingIn: 'Signing in...', - createAccount: 'Create Account', - signUpToStart: 'Sign up to start using {siteName}', - signUp: 'Sign up', - processing: 'Processing...', - continue: 'Continue', - rememberMe: 'Remember me', - dontHaveAccount: "Don't have an account?", - alreadyHaveAccount: 'Already have an account?', - registrationDisabled: 'Registration is currently disabled. Please contact the administrator.', - emailLabel: 'Email', - emailPlaceholder: 'Enter your email', - passwordLabel: 'Password', - passwordPlaceholder: 'Enter your password', - createPasswordPlaceholder: 'Create a strong password', - passwordHint: 'At least 6 characters', - emailRequired: 'Email is required', - invalidEmail: 'Please enter a valid email address', - passwordRequired: 'Password is required', - passwordMinLength: 'Password must be at least 6 characters', - loginFailed: 'Login failed. Please check your credentials and try again.', - errors: { - USER_NOT_ACTIVE: 'Account has been disabled.', - }, - registrationFailed: 'Registration failed. Please try again.', - emailSuffixNotAllowed: 'This email domain is not allowed for registration.', - emailSuffixNotAllowedWithAllowed: - 'This email domain is not allowed. Allowed domains: {suffixes}', - emailSuffixAllowedMore: 'and {count} more', - loginSuccess: 'Login successful! Welcome back.', - accountCreatedSuccess: 'Account created successfully! Welcome to {siteName}.', - reloginRequired: 'Session expired. Please log in again.', - turnstileExpired: 'Verification expired, please try again', - turnstileFailed: 'Verification failed, please try again', - completeVerification: 'Please complete the verification', - verifyYourEmail: 'Verify Your Email', - sessionExpired: 'Session expired', - sessionExpiredDesc: 'Please go back to the registration page and start again.', - verificationCode: 'Verification Code', - verificationCodeHint: 'Enter the 6-digit code sent to your email', - sendingCode: 'Sending...', - sendCode: 'Send code', - clickToResend: 'Click to resend code', - resendCode: 'Resend verification code', - sendCodeDesc: "We'll send a verification code to", - codeSentSuccess: 'Verification code sent! Please check your inbox.', - verifying: 'Verifying...', - verifyAndCreate: 'Verify & Create Account', - resendCountdown: 'Resend code in {countdown}s', - backToRegistration: 'Back to registration', - sendCodeFailed: 'Failed to send verification code. Please try again.', - verifyFailed: 'Verification failed. Please try again.', - codeRequired: 'Verification code is required', - invalidCode: 'Please enter a valid 6-digit code', - promoCodeLabel: 'Promo Code', - promoCodePlaceholder: 'Enter promo code (optional)', - promoCodeValid: 'Valid! You will receive ${amount} bonus balance', - promoCodeInvalid: 'Invalid promo code', - promoCodeNotFound: 'Promo code not found', - promoCodeExpired: 'This promo code has expired', - promoCodeDisabled: 'This promo code is disabled', - promoCodeMaxUsed: 'This promo code has reached its usage limit', - promoCodeAlreadyUsed: 'You have already used this promo code', - promoCodeValidating: 'Promo code is being validated, please wait', - promoCodeInvalidCannotRegister: 'Invalid promo code. Please check and try again or clear the promo code field', - invitationCodeLabel: 'Invitation Code', - invitationCodePlaceholder: 'Enter invitation code', - invitationCodeRequired: 'Invitation code is required', - invitationCodeValid: 'Invitation code is valid', - invitationCodeInvalid: 'Invalid or used invitation code', - invitationCodeValidating: 'Validating invitation code...', - invitationCodeInvalidCannotRegister: 'Invalid invitation code. Please check and try again', - oauthOrContinue: 'or continue with others', - linuxdo: { - signIn: 'Continue with Linux.do', - orContinue: 'or continue with email', - callbackTitle: 'Signing you in', - callbackProcessing: 'Completing login, please wait...', - callbackHint: 'If you are not redirected automatically, go back to the login page and try again.', - callbackMissingToken: 'Missing login token, please try again.', - backToLogin: 'Back to Login', - invitationRequired: 'This Linux.do account is not yet registered. The site requires an invitation code — please enter one to complete registration.', - invalidPendingToken: 'The registration token has expired. Please sign in with Linux.do again.', - completeRegistration: 'Complete Registration', - completing: 'Completing registration…', - completeRegistrationFailed: 'Registration failed. Please check your invitation code and try again.' - }, - dingtalk: { - signIn: 'Continue with DingTalk', - callbackTitle: 'Signing you in with DingTalk', - callbackProcessing: 'Completing DingTalk login, please wait...', - callbackHint: 'If you are not redirected automatically, go back to the login page and try again.', - callbackMissingToken: 'Missing login token, please try again.', - backToLogin: 'Back to Login', - invitationRequired: 'This DingTalk account is not yet registered. The site requires an invitation code — please enter one to complete registration.', - invalidPendingToken: 'The registration token has expired. Please sign in with DingTalk again.', - completeRegistration: 'Complete Registration', - completing: 'Completing registration…', - completeRegistrationFailed: 'Registration failed. Please check your invitation code and try again.', - createAccountTitle: 'Create DingTalk Account', - registrationDisabledRedirectToBind: 'New account registration is currently disabled. Please bind to your existing account with its email and password.', - error: { - title: 'DingTalk Sign-in Failed', - csrf: 'Login session expired, please scan again', - corp_rejected: 'Your DingTalk account is not part of this organization. Please contact administrator', - dingtalk_not_enabled: 'DingTalk login is not enabled', - upstream_error: 'DingTalk service is temporarily unavailable. Please try again later', - missing_browser_session: 'Browser session lost. Please login again', - missing_params: 'Request parameters are incomplete', - invalid_state: 'Invalid login state', - provider_error: 'DingTalk authorization failed', - session_error: 'Failed to create session. Please retry', - retry: 'Retry Login' - } - }, - emailOAuth: { - signIn: 'Continue with {providerName}' - }, - oidc: { - signIn: 'Continue with {providerName}', - callbackTitle: 'Signing you in with {providerName}', - callbackProcessing: 'Completing login with {providerName}, please wait...', - callbackHint: 'If you are not redirected automatically, go back to the login page and try again.', - callbackMissingToken: 'Missing login token, please try again.', - backToLogin: 'Back to Login', - invitationRequired: - 'This {providerName} account is not yet registered. The site requires an invitation code — please enter one to complete registration.', - invalidPendingToken: 'The registration token has expired. Please sign in again.', - completeRegistration: 'Complete Registration', - completing: 'Completing registration…', - completeRegistrationFailed: 'Registration failed. Please check your invitation code and try again.' - }, - oauthFlow: { - profileDetailsTitle: 'Use {providerName} profile details', - profileDetailsDescription: 'Choose whether to apply the nickname or avatar from {providerName} to this account.', - useDisplayName: 'Use display name', - useAvatar: 'Use avatar', - avatarAlt: '{providerName} avatar', - reviewProfileBeforeContinue: 'Review the {providerName} profile details before continuing.', - chooseHowToContinue: 'Choose how to continue', - chooseAccountActionHint: 'Choose whether to bind an existing account or create a new one.', - suggestedEmail: 'Suggested email: {email}', - bindExistingAccount: 'Bind existing account', - createNewAccount: 'Create new account', - createAccountHint: 'Enter an email address to create your account and continue.', - bindLoginHint: 'Log in to an existing account to bind this {providerName} sign-in.', - signInThenBindDescription: 'Sign in to an existing account, then bind this {providerName} sign-in to it.', - bindSignInToExistingAccount: 'Bind this {providerName} sign-in to an existing account.', - bindCurrentAccountTitle: 'Bind the current account', - bindCurrentAccountDescription: 'Bind this {providerName} sign-in to the account currently signed in on this browser.', - bindCurrentAccount: 'Bind current account', - logInAndBind: 'Log in and bind', - useDifferentEmail: 'Use a different email', - backToOptions: 'Back to options', - yourAccount: 'your account', - totpHint: 'Enter the 6-digit verification code for {account} to finish binding this {providerName} sign-in.', - verifyAndContinue: 'Verify and continue', - wechatAvailabilityUnknown: 'WeChat sign-in availability could not be confirmed. Refresh and retry.', - wechatSystemBrowserOnly: 'This WeChat sign-in flow is only available in your system browser.', - wechatBrowserOnly: 'This WeChat sign-in flow is only available inside the WeChat browser.', - wechatNotConfigured: 'WeChat sign-in is not configured yet.' - }, - linuxdoCallbackPageTitle: 'LinuxDo Sign-In Callback', - dingtalkCallbackPageTitle: 'DingTalk Sign-In Callback', - dingtalkProviderName: 'DingTalk', - oidcCallbackPageTitle: 'OIDC Sign-In Callback', - oauthCallbackPageTitle: 'OAuth Callback', - wechatProviderName: 'WeChat', - wechatCallbackPageTitle: 'WeChat Sign-In Callback', - wechatPaymentCallbackPageTitle: 'WeChat Payment Callback', - wechatPayment: { - callbackTitle: 'Resuming WeChat payment', - callbackProcessing: 'Resuming WeChat payment...', - backToPayment: 'Back to payment', - callbackMissingResumeToken: 'The WeChat payment callback is missing the resume token.' - }, - oauth: { - callbackTitle: 'OAuth Callback', - callbackHint: 'Copy the code and state back to the admin authorization flow when needed.', - invalidCallbackTitle: 'Invalid sign-in callback', - invalidCallbackHint: 'This page does not contain a valid authorization result. Return to the login page and start quick sign-in again.', - code: 'Code', - state: 'State', - fullUrl: 'Full URL' - }, - // Forgot password - forgotPassword: 'Forgot password?', - forgotPasswordTitle: 'Reset Your Password', - forgotPasswordHint: 'Enter your email address and we will send you a link to reset your password.', - sendResetLink: 'Send Reset Link', - sendingResetLink: 'Sending...', - sendResetLinkFailed: 'Failed to send reset link. Please try again.', - resetEmailSent: 'Reset Link Sent', - resetEmailSentHint: 'If an account exists with this email, you will receive a password reset link shortly. Please check your inbox and spam folder.', - backToLogin: 'Back to Login', - rememberedPassword: 'Remembered your password?', - // Reset password - resetPasswordTitle: 'Set New Password', - resetPasswordHint: 'Enter your new password below.', - newPassword: 'New Password', - newPasswordPlaceholder: 'Enter your new password', - confirmPassword: 'Confirm Password', - confirmPasswordPlaceholder: 'Confirm your new password', - confirmPasswordRequired: 'Please confirm your password', - passwordsDoNotMatch: 'Passwords do not match', - resetPassword: 'Reset Password', - resettingPassword: 'Resetting...', - resetPasswordFailed: 'Failed to reset password. Please try again.', - passwordResetSuccess: 'Password Reset Successful', - passwordResetSuccessHint: 'Your password has been reset. You can now sign in with your new password.', - invalidResetLink: 'Invalid Reset Link', - invalidResetLinkHint: 'This password reset link is invalid or has expired. Please request a new one.', - requestNewResetLink: 'Request New Reset Link', - invalidOrExpiredToken: 'The password reset link is invalid or has expired. Please request a new one.' - }, - - // Dashboard - dashboard: { - title: 'Dashboard', - welcomeMessage: "Welcome back! Here's an overview of your account.", - balance: 'Balance', - apiKeys: 'API Keys', - todayRequests: 'Today Requests', - todayCost: 'Today Cost', - todayTokens: 'Today Tokens', - totalTokens: 'Total Tokens', - cacheToday: 'Cache (Today)', - performance: 'Performance', - avgResponse: 'Avg Response', - averageTime: 'Average time', - timeRange: 'Time Range', - granularity: 'Granularity', - day: 'Day', - hour: 'Hour', - modelDistribution: 'Model Distribution', - groupDistribution: 'Group Usage Distribution', - platformBreakdown: 'Per-platform Breakdown', - platformBreakdownEmpty: 'No platform usage yet', - platformCount: '{count} platforms', - platformOther: 'Other', - platformQuota: { - title: 'Quota Usage', - daily: 'Daily', - weekly: 'Weekly', - monthly: 'Monthly (30-day rolling)', - resetsAt: 'Resets {time}', - noLimit: 'unlimited', - disabled: 'Disabled', - }, - tokenUsageTrend: 'Token Usage Trend', - noDataAvailable: 'No data available', - model: 'Model', - group: 'Group', - noGroup: 'No Group', - requests: 'Requests', - tokens: 'Tokens', - actual: 'Actual', - standard: 'Standard', - input: 'Input', - output: 'Output', - cache: 'Cache', - recentUsage: 'Recent Usage', - last7Days: 'Last 7 days', - noUsageRecords: 'No usage records', - startUsingApi: 'Start using the API to see your usage history here.', - viewAllUsage: 'View all usage', - quickActions: 'Quick Actions', - createApiKey: 'Create API Key', - generateNewKey: 'Generate a new API key', - batchImageAgent: 'Batch Image Assistant', - batchImageAgentDesc: 'Copy instructions for an agent', - viewUsage: 'View Usage', - checkDetailedLogs: 'Check detailed usage logs', - redeemCode: 'Redeem Code', - addBalanceWithCode: 'Add balance with a code' - }, - - // Groups (shared) - groups: { - subscription: 'Sub' - }, - - // API Keys - keys: { - title: 'API Keys', - description: 'Manage your API keys and access tokens', - searchPlaceholder: 'Search name or key...', - endpoints: { - title: 'API Endpoints', - default: 'Default', - copied: 'Copied', - copiedHint: 'Copied to clipboard', - clickToCopy: 'Click to copy this endpoint', - speedTest: 'Speed Test', - }, - allGroups: 'All Groups', - allStatus: 'All Status', - columnSettings: 'Column Settings', - columnAlwaysVisible: 'This column is always visible', - createKey: 'Create API Key', - editKey: 'Edit API Key', - deleteKey: 'Delete API Key', - deleteConfirmMessage: "Are you sure you want to delete '{name}'? This action cannot be undone.", - apiKey: 'API Key', - group: 'Group', - currentConcurrency: 'Current Concurrency', - noGroup: 'No group', - searchGroup: 'Search groups...', - noGroupFound: 'No groups found', - created: 'Created', - copyToClipboard: 'Copy to clipboard', - copied: 'Copied!', - importToCcSwitch: 'Import to CCS', - enable: 'Enable', - disable: 'Disable', - nameLabel: 'Name', - namePlaceholder: 'My API Key', - groupLabel: 'Group', - selectGroup: 'Select a group', - statusLabel: 'Status', - selectStatus: 'Select status', - saving: 'Saving...', - noKeysYet: 'No API keys yet', - createFirstKey: 'Create your first API key to get started with the API.', - keyCreatedSuccess: 'API key created successfully', - keyUpdatedSuccess: 'API key updated successfully', - keyDeletedSuccess: 'API key deleted successfully', - keyEnabledSuccess: 'API key enabled successfully', - keyDisabledSuccess: 'API key disabled successfully', - failedToLoad: 'Failed to load API keys', - failedToSave: 'Failed to save API key', - failedToDelete: 'Failed to delete API key', - failedToUpdateStatus: 'Failed to update API key status', - clickToChangeGroup: 'Click to change group', - groupChangedSuccess: 'Group changed successfully', - failedToChangeGroup: 'Failed to change group', - groupRequired: 'Please select a group', - usage: 'Usage', - today: 'Today', - total: 'Last 30d', - quota: 'Quota', - lastUsedAt: 'Last Used', - useKey: 'Use Key', - useKeyModal: { - title: 'Use API Key', - description: - 'Add the following environment variables to your terminal profile or run directly in terminal to configure API access.', - copy: 'Copy', - copied: 'Copied', - note: 'These environment variables will be active in the current terminal session. For permanent configuration, add them to ~/.bashrc, ~/.zshrc, or the appropriate configuration file.', - noGroupTitle: 'Please assign a group first', - noGroupDescription: 'This API key has not been assigned to a group. Please click the group column in the key list to assign one before viewing the configuration.', - openai: { - description: 'Add the following configuration files to your Codex CLI config directory.', - configTomlHint: 'Make sure the following content is at the beginning of the config.toml file', - note: 'Make sure the config directory exists. macOS/Linux users can run mkdir -p ~/.codex to create it.', - noteWindows: 'Press Win+R and enter %userprofile%\\.codex to open the config directory. Create it manually if it does not exist.', - }, - cliTabs: { - claudeCode: 'Claude Code', - geminiCli: 'Gemini CLI', - codexCli: 'Codex CLI', - codexCliWs: 'Codex CLI (WebSocket)', - opencode: 'OpenCode', - }, - antigravity: { - description: 'Configure API access for Antigravity group. Select the configuration method based on your client.', - claudeCode: 'Claude Code', - geminiCli: 'Gemini CLI', - claudeNote: 'These environment variables will be active in the current terminal session. For permanent configuration, add them to ~/.bashrc, ~/.zshrc, or the appropriate configuration file.', - geminiNote: 'These environment variables will be active in the current terminal session. For permanent configuration, add them to ~/.bashrc, ~/.zshrc, or the appropriate configuration file.', - }, - gemini: { - description: 'Add the following environment variables to your terminal profile or run directly in terminal to configure Gemini CLI access.', - modelComment: 'If you have Gemini 3 access, you can use: gemini-3-pro-preview', - note: 'These environment variables will be active in the current terminal session. For permanent configuration, add them to ~/.bashrc, ~/.zshrc, or the appropriate configuration file.', - }, - opencode: { - title: 'OpenCode Example', - subtitle: 'opencode.json', - hint: 'Config path: ~/.config/opencode/opencode.json (or opencode.jsonc), create if not exists. Use default providers (openai/anthropic/google) or custom provider_id. API Key can be configured directly or via /connect command. This is an example, adjust models and options as needed.', - }, - }, - customKeyLabel: 'Custom Key', - customKeyPlaceholder: 'Enter your custom key (min 16 chars)', - customKeyHint: 'Only letters, numbers, underscores and hyphens allowed. Minimum 16 characters.', - customKeyTooShort: 'Custom key must be at least 16 characters', - customKeyInvalidChars: 'Custom key can only contain letters, numbers, underscores, and hyphens', - customKeyRequired: 'Please enter a custom key', - ipRestriction: 'IP Restriction', - ipWhitelist: 'IP Whitelist', - ipWhitelistPlaceholder: '192.168.1.100\n10.0.0.0/8', - ipWhitelistHint: 'One IP or CIDR per line. Only these IPs can use this key when set.', - ipBlacklist: 'IP Blacklist', - ipBlacklistPlaceholder: '1.2.3.4\n5.6.0.0/16', - ipBlacklistHint: 'One IP or CIDR per line. These IPs will be blocked from using this key.', - ipRestrictionEnabled: 'IP restriction enabled', - ccSwitchNotInstalled: 'CC-Switch is not installed or the protocol handler is not registered. Please install CC-Switch first or manually copy the API key.', - ccsClientSelect: { - title: 'Select Client', - description: 'Please select the client type to import to CC-Switch:', - claudeCode: 'Claude Code', - claudeCodeDesc: 'Import as Claude Code configuration', - geminiCli: 'Gemini CLI', - geminiCliDesc: 'Import as Gemini CLI configuration', - }, - // Quota and expiration - quotaLimit: 'Quota Limit', - quotaAmount: 'Quota Amount (USD)', - quotaAmountPlaceholder: 'Enter quota limit in USD', - quotaAmountHint: 'Set the maximum amount this key can spend. 0 = unlimited.', - quotaUsed: 'Quota Used', - reset: 'Reset', - resetQuotaUsed: 'Reset used quota to 0', - resetQuotaTitle: 'Confirm Reset Quota', - resetQuotaConfirmMessage: 'Are you sure you want to reset the used quota (${used}) for key "{name}" to 0? This action cannot be undone.', - quotaResetSuccess: 'Quota reset successfully', - failedToResetQuota: 'Failed to reset quota', - rateLimitColumn: 'Rate Limit', - rateLimitSection: 'Rate Limit', - resetUsage: 'Reset', - rateLimit5h: '5-Hour Limit (USD)', - rateLimit1d: 'Daily Limit (USD)', - rateLimit7d: '7-Day Limit (USD)', - rateLimitHint: 'Set the maximum spending for this key within each time window. 0 = unlimited.', - rateLimitUsage: 'Rate Limit Usage', - resetRateLimitUsage: 'Reset Rate Limit Usage', - resetRateLimitTitle: 'Confirm Reset Rate Limit', - resetRateLimitConfirmMessage: 'Are you sure you want to reset the rate limit usage for key "{name}"? All time window usage will be reset to zero. This action cannot be undone.', - rateLimitResetSuccess: 'Rate limit usage reset successfully', - failedToResetRateLimit: 'Failed to reset rate limit usage', - resetNow: 'Resetting soon', - expiration: 'Expiration', - expiresInDays: '{days} days', - extendDays: '+{days} days', - customDate: 'Custom', - expirationDate: 'Expiration Date', - expirationDateHint: 'Select when this API key should expire.', - currentExpiration: 'Current expiration', - expiresAt: 'Expires', - noExpiration: 'Never', - status: { - active: 'Active', - inactive: 'Inactive', - quota_exhausted: 'Quota Exhausted', - expired: 'Expired', - }, - }, - - // Usage - usage: { - title: 'Usage Records', - description: 'View and analyze your API usage history', - costDetails: 'Cost Breakdown', - tokenDetails: 'Token Breakdown', - cacheTtlOverriddenHint: 'Cache TTL Override enabled', - cacheTtlOverriddenLabel: 'TTL Override', - cacheTtlOverridden5m: 'Billed as 5m', - cacheTtlOverridden1h: 'Billed as 1h', - totalRequests: 'Total Requests', - totalTokens: 'Total Tokens', - cacheTotal: 'Cache', - cacheBreakdown: 'Cache Token Breakdown', - cacheCreationTokensLabel: 'Cache Creation', - cacheReadTokensLabel: 'Cache Read', - totalCost: 'Total Cost', - standardCost: 'Standard', - actualCost: 'Actual', - accountCost: 'Cost', - userBilled: 'User billed', - accountBilled: 'Account billed', - resetNow: 'Now', - resetPending: 'Pending refresh', - accountMultiplier: 'Account rate', - avgDuration: 'Avg Duration', - inSelectedRange: 'in selected range', - perRequest: 'per request', - apiKeyFilter: 'API Key', - allApiKeys: 'All API Keys', - timeRange: 'Time Range', - exportCsv: 'Export CSV', - exportExcel: 'Export Excel', - exportingProgress: 'Exporting data...', - exportedCount: 'Exported {current}/{total} records', - estimatedTime: 'Estimated time remaining: {time}', - cancelExport: 'Cancel Export', - exportCancelled: 'Export cancelled', - exporting: 'Exporting...', - preparingExport: 'Preparing export...', - model: 'Model', - requestedModel: 'Requested', - upstreamModel: 'Upstream', - reasoningEffort: 'Reasoning Effort', - endpoint: 'Endpoint', - endpointDistribution: 'Endpoint Distribution', - inbound: 'Inbound', - upstream: 'Upstream', - mapping: 'Mapping', - path: 'Path', - inboundEndpoint: 'Inbound Endpoint', - upstreamEndpoint: 'Upstream Endpoint', - type: 'Type', - tokens: 'Tokens', - cost: 'Cost', - firstToken: 'First Token', - duration: 'Duration', - time: 'Time', - ws: 'WS', - stream: 'Stream', - sync: 'Sync', - cyber: 'Cyber', - unknown: 'Unknown', - in: 'In', - out: 'Out', - cacheHit: 'Cache hit', - cacheCreate: 'Cache create', - cacheHitRate: 'Cache hit rate', - inputTokenPrice: 'Input price', - outputTokenPrice: 'Output price', - perMillionTokens: '/ 1M tokens', - unitPrice: 'Per-request price', - imageUnitPrice: 'Per-image price', - imageTotalPrice: 'Image total price', - imageCount: 'Image count', - imageBillingSize: 'Billing size', - imageInputSize: 'Input size', - imageOutputSize: 'Output size', - imageOutputTokens: 'Image Output Tokens', - imageOutputTokenPrice: 'Image Output Price', - imageOutputCost: 'Image Output Cost', - imageSizeSource: 'Size source', - imageSizeBreakdown: 'Size breakdown', - imageSizeSourceOutput: 'Upstream output', - imageSizeSourceInput: 'Request input', - imageSizeSourceDefault: 'Default billing tier', - imageSizeSourceLegacy: 'Legacy record', - imageSizeSourceMissing: 'Not recorded', - imageSizeNotRecorded: 'not recorded', - imageSizeLegacyUnstandardized: 'legacy unstandardized', - imageSizeUnknown: 'unknown', - cacheRead: 'Read', - cacheWrite: 'Write', - serviceTier: 'Service tier', - serviceTierPriority: 'Fast', - serviceTierFlex: 'Flex', - serviceTierStandard: 'Standard', - rate: 'Rate', - original: 'Original', - billed: 'Billed', - noRecords: 'No usage records found. Try adjusting your filters.', - failedToLoad: 'Failed to load usage logs', - noDataToExport: 'No data to export', - exportSuccess: 'Usage data exported successfully', - exportFailed: 'Failed to export usage data', - exportExcelSuccess: 'Usage data exported successfully (Excel format)', - exportExcelFailed: 'Failed to export usage data', - imageUnit: ' images', - userAgent: 'User-Agent', - ipGeo: { - fetch: 'Fetch region', - fetching: 'Fetching...', - failed: 'Failed', - private: 'Private address', - refreshTitle: 'Refresh region info', - batchFetch: 'Batch fetch regions', - batchFetching: 'Fetching...', - pending: '{count} IPs pending', - batchFailed: 'Failed to batch fetch IP regions', - detailOrg: 'ISP', - detailTimezone: 'Timezone', - detailAccuracy: 'Accuracy', - detailCoordinates: 'Coordinates', - }, - tabs: { usage: 'Usage', errors: 'Error Requests' }, - errors: { - time: 'Time', model: 'Model', endpoint: 'Endpoint', status: 'Status', - category: 'Category', platform: 'Platform', message: 'Message', - keyName: 'Key Name', keyDeleted: 'Deleted', allKeys: 'All keys', - modelPlaceholder: 'Search model', allCategories: 'All categories', allStatuses: 'All status codes', - empty: 'No error requests', failedToLoad: 'Failed to load error requests', - categories: { - auth: 'Auth failed', rate_limit: 'Rate limited', quota: 'Balance/Subscription', - invalid_request: 'Invalid request', service_unavailable: 'Service unavailable', - upstream: 'Upstream error', internal: 'Platform error', other: 'Other', cyber: 'Cyber policy', - }, - detail: { - title: 'Error Request Detail', - responseBody: 'Response Body', - upstreamStatus: 'Upstream Status', - loadFailed: 'Failed to load detail, please try again', - }, - }, - }, - - // Shared keys for channel monitor (admin + user views) - monitorCommon: { - status: { - operational: 'Operational', - degraded: 'Degraded', - failed: 'Failed', - error: 'Error', - unknown: '-' - }, - providers: { - openai: 'OpenAI', - anthropic: 'Anthropic', - gemini: 'Gemini' - }, - extraModelsHeader: 'Extra Models', - extraModelsEmpty: 'No extra models', - latencyEmpty: '-', - availabilityPrefix: 'Availability', - dialogLatency: 'Dialog Latency', - endpointPing: 'Endpoint PING', - history60pts: 'HISTORY ({n} PTS)', - nextUpdateIn: 'NEXT UPDATE IN {n}s', - past: 'PAST', - now: 'NOW', - maintenancePaused: 'Maintenance · timeline paused', - extraModelsCount: '+ {n} models', - pollEvery: '{n}s polling', - updatedAt: 'Updated {time}', - relativeSecondsAgo: '{n}s ago', - relativeMinutesAgo: '{n}m ago', - relativeHoursAgo: '{n}h ago', - relativeDaysAgo: '{n}d ago' - }, - - // Channel Status (user-facing read-only view) - channelStatus: { - title: 'Channel Status', - description: 'Inspect channel availability, latency and recent status', - searchPlaceholder: 'Search channels...', - allProviders: 'All Providers', - loadError: 'Failed to load channel status', - detailLoadError: 'Failed to load channel detail', - detailTitle: 'Channel Detail', - closeDetail: 'Close', - windowTab: { - '7d': '7 days', - '15d': '15 days', - '30d': '30 days' - }, - overall: { - operational: 'OPERATIONAL', - degraded: 'DEGRADED', - unavailable: 'UNAVAILABLE' - }, - columns: { - name: 'Name', - provider: 'Provider', - groupName: 'Group', - primaryModel: 'Primary Model', - availability7d: '7d Availability', - latency: 'Latency (ms)' - }, - detailColumns: { - model: 'Model', - latestStatus: 'Latest Status', - latestLatency: 'Latest Latency (ms)', - availability7d: '7d Availability', - availability15d: '15d Availability', - availability30d: '30d Availability', - avgLatency7d: '7d Avg Latency (ms)' - }, - empty: { - title: 'No channels available', - description: 'No monitored channels have been configured yet.' - } - }, - - // Available Channels (user-facing) - availableChannels: { - title: 'Available Channels', - description: 'Channels you can access, along with their supported models and pricing', - searchPlaceholder: 'Search channels or models...', - empty: 'No available channels', - noModels: 'No models configured', - noPricing: 'Pricing not configured', - exclusive: 'Exclusive', - public: 'Public', - exclusiveTooltip: 'Exclusive groups granted to you by an admin', - publicTooltip: 'Groups open to all users', - columns: { - name: 'Channel', - description: 'Description', - platform: 'Platform', - groups: 'Your Accessible Groups', - supportedModels: 'Supported Models' - }, - pricing: { - billingMode: 'Billing Mode', - billingModeToken: 'Per Token', - billingModePerRequest: 'Per Request', - billingModeImage: 'Per Image', - inputPrice: 'Input', - outputPrice: 'Output', - cacheWritePrice: 'Cache Write', - cacheReadPrice: 'Cache Read', - imageOutputPrice: 'Image Output', - perRequestPrice: 'Per Request', - intervals: 'Tiered Pricing', - unitPerMillion: '/ 1M tokens', - unitPerRequest: '/ request' - } - }, - - affiliate: { - title: 'Affiliate Rebates', - description: 'Invite new users and convert your rebate quota into account balance', - yourCode: 'Your Affiliate Code', - inviteLink: 'Invite Link', - copyCode: 'Copy Code', - copyLink: 'Copy Link', - codeCopied: 'Affiliate code copied', - linkCopied: 'Invite link copied', - loadFailed: 'Failed to load affiliate data', - transferFailed: 'Failed to transfer affiliate quota', - stats: { - rebateRate: 'My Rebate Rate', - rebateRateHint: 'What you earn each time an invitee recharges', - invitedUsers: 'Invited Users', - availableQuota: 'Available Rebate Quota', - frozenQuota: 'Frozen', - frozenQuotaHint: 'Recently earned rebates pending release', - totalQuota: 'Historical Rebate Quota' - }, - transfer: { - title: 'Transfer Rebate Quota', - description: 'Move available rebate quota into your account balance', - button: 'Transfer to Balance', - transferring: 'Transferring...', - empty: 'No available rebate quota', - success: '{amount} has been transferred to your balance' - }, - invitees: { - title: 'Invited Users', - empty: 'No invited users yet', - columns: { - email: 'Email', - username: 'Username', - rebate: 'Rebate', - joinedAt: 'Joined At' - } - }, - tips: { - title: 'How It Works', - line1: 'Share your affiliate code or invite link with new users.', - line2: 'When invitees recharge, you receive {rate} of the recharge as rebate quota.', - line3: 'Transfer rebate quota to balance at any time.', - line4: 'Newly earned rebates may have a waiting period before they can be transferred.' - } - }, - - // Redeem - redeem: { - title: 'Redeem Code', - description: 'Enter your redeem code to add balance or increase concurrency', - currentBalance: 'Current Balance', - concurrency: 'Concurrency', - requests: 'requests', - redeemCodeLabel: 'Redeem Code', - redeemCodePlaceholder: 'Enter your redeem code', - redeemCodeHint: 'Redeem codes are case-sensitive', - redeeming: 'Redeeming...', - redeemButton: 'Redeem Code', - redeemSuccess: 'Code Redeemed Successfully!', - redeemFailed: 'Redemption Failed', - added: 'Added', - concurrentRequests: 'concurrent requests', - newBalance: 'New Balance', - newConcurrency: 'New Concurrency', - aboutCodes: 'About Redeem Codes', - codeRule1: 'Each code can only be used once', - codeRule2: 'Codes may add balance, increase concurrency, or grant trial access', - codeRule3: 'Contact support if you have issues redeeming a code', - codeRule4: 'Balance and concurrency updates are immediate', - recentActivity: 'Recent Activity', - historyWillAppear: 'Your redemption history will appear here', - balanceAddedRedeem: 'Balance Added (Redeem)', - balanceAddedAffiliate: 'Balance Added (Affiliate Transfer)', - balanceAddedAdmin: 'Balance Added (Admin)', - balanceDeductedAdmin: 'Balance Deducted (Admin)', - concurrencyAddedRedeem: 'Concurrency Added (Redeem)', - concurrencyAddedAdmin: 'Concurrency Added (Admin)', - concurrencyReducedAdmin: 'Concurrency Reduced (Admin)', - adminAdjustment: 'Admin Adjustment', - subscriptionAssigned: 'Subscription Assigned', - subscriptionAssignedDesc: 'You have been granted access to {groupName}', - subscriptionDays: '{days} days', - days: ' days', - codeRedeemSuccess: 'Code redeemed successfully!', - failedToRedeem: 'Failed to redeem code. Please check the code and try again.', - subscriptionRefreshFailed: 'Redeemed successfully, but failed to refresh subscription status.', - pleaseEnterCode: 'Please enter a redeem code' - }, - - // Profile - profile: { - title: 'Profile Settings', - description: 'Manage your account information and settings', - accountBalance: 'Account Balance', - concurrencyLimit: 'Concurrency Limit', - rpmLimit: 'RPM Limit', - rpmUnlimited: 'Unlimited', - memberSince: 'Member Since', - overviewTitle: 'Account Overview', - overviewDescription: 'Check account status, profile sources, and common actions at a glance.', - basicsTitle: 'Profile & Avatar', - basicsDescription: 'Keep your public profile details and avatar aligned.', - linkedProfileSources: 'Profile Sources', - linkedProfileSourcesDescription: 'Some profile details may stay synced from third-party sign-in methods.', - securityTitle: 'Security Settings', - securityDescription: 'Password, two-factor authentication, and alerts live in the right rail.', - administrator: 'Administrator', - user: 'User', - username: 'Username', - email: 'Email', - status: 'Status', - role: 'Role', - enterUsername: 'Enter username', - editProfile: 'Edit Profile', - updateProfile: 'Update Profile', - updating: 'Updating...', - updateSuccess: 'Profile updated successfully', - updateFailed: 'Failed to update profile', - usernameRequired: 'Username is required', - changePassword: 'Change Password', - currentPassword: 'Current Password', - newPassword: 'New Password', - confirmNewPassword: 'Confirm New Password', - passwordHint: 'Password must be at least 8 characters long', - changingPassword: 'Changing...', - changePasswordButton: 'Change Password', - passwordsNotMatch: 'New passwords do not match', - passwordTooShort: 'Password must be at least 8 characters long', - passwordChangeSuccess: 'Password changed successfully', - passwordChangeFailed: 'Failed to change password', - // TOTP 2FA - totp: { - title: 'Two-Factor Authentication (2FA)', - description: 'Enhance account security with Google Authenticator or similar apps', - enabled: 'Enabled', - enabledAt: 'Enabled at', - notEnabled: 'Not Enabled', - notEnabledHint: 'Enable two-factor authentication to enhance account security', - enable: 'Enable', - disable: 'Disable', - featureDisabled: 'Feature Unavailable', - featureDisabledHint: 'Two-factor authentication has not been enabled by the administrator', - setupTitle: 'Set Up Two-Factor Authentication', - setupStep1: 'Scan the QR code below with your authenticator app', - setupStep2: 'Enter the 6-digit code from your app', - manualEntry: "Can't scan? Enter the key manually:", - enterCode: 'Enter 6-digit code', - verify: 'Verify', - setupFailed: 'Failed to get setup information', - verifyFailed: 'Invalid code, please try again', - enableSuccess: 'Two-factor authentication enabled', - disableTitle: 'Disable Two-Factor Authentication', - disableWarning: 'After disabling, you will no longer need a verification code to log in. This may reduce your account security.', - enterPassword: 'Enter your current password to confirm', - confirmDisable: 'Confirm Disable', - disableSuccess: 'Two-factor authentication disabled', - disableFailed: 'Failed to disable, please check your password', - loginTitle: 'Two-Factor Authentication', - loginHint: 'Enter the 6-digit code from your authenticator app', - loginFailed: 'Verification failed, please try again', - // New translations for email verification - verifyEmailFirst: 'Please verify your email first', - verifyPasswordFirst: 'Please verify your identity first', - emailCode: 'Email Verification Code', - enterEmailCode: 'Enter 6-digit code', - sendCode: 'Send Code', - codeSent: 'Verification code sent to your email', - sendCodeFailed: 'Failed to send verification code' - }, - balanceNotify: { - title: 'Balance Low Notification', - description: 'Send email alert when account balance falls below threshold', - enabled: 'Enable Balance Low Notification', - threshold: 'Custom Threshold', - thresholdHint: 'Leave empty to use system default', - thresholdPlaceholder: 'Enter amount', - systemDefault: 'System Default', - extraEmails: 'Notification Emails', - extraEmailsHint: 'You must add and verify an email address to receive low balance alerts', - primaryEmail: 'Primary', - noExtraEmails: 'No extra notification emails', - enterEmail: 'Enter email address', - addEmail: 'Add Email', - emailPlaceholder: 'Enter email address', - sendCode: 'Send Code', - resend: 'Resend', - codeSent: 'Verification code sent', - codeSentTo: 'Code sent to {email}', - enterCode: 'Enter verification code', - codePlaceholder: '6-digit code', - verify: 'Verify', - emailAdded: 'Email added', - emailRemoved: 'Email removed', - verifySuccess: 'Email added successfully', - removeEmail: 'Remove', - removeSuccess: 'Email removed', - emailDuplicate: 'This email already exists', - maxEmailsReached: 'Maximum number of notification emails reached', - unverified: 'Unverified', - verified: 'Verified', - }, - avatar: { - title: 'Profile Avatar', - description: 'Upload an avatar image. Static uploads are compressed to 20KB before saving.', - uploadAction: 'Upload image', - uploadHint: 'Static uploads are compressed to 20KB when possible. GIF uploads must already be within 20KB.', - uploadRequired: 'Upload an avatar image first', - saveSuccess: 'Avatar updated', - deleteSuccess: 'Avatar removed', - invalidType: 'Please choose an image file', - gifTooLarge: 'GIF avatars must already be 20KB or smaller', - compressTooLarge: 'Unable to compress this image below 20KB. Try a smaller image.', - compressFailed: 'Failed to compress the selected image.', - readFailed: 'Failed to read the selected image.', - emptyDeleteHint: 'Avatar is already empty', - }, - authBindings: { - title: 'Connected Sign-In Methods', - description: 'View current bindings and connect another provider to this account.', - bindAction: 'Bind {providerName}', - bindSuccess: 'Account linked successfully', - emailPlaceholder: 'Enter email address', - codePlaceholder: 'Enter verification code', - passwordPlaceholder: 'Set a login password', - replaceEmailPasswordPlaceholder: 'Enter current password', - sendCodeAction: 'Send code', - manageEmailAction: 'Manage email', - hideEmailFormAction: 'Hide email form', - confirmEmailBindAction: 'Bind email', - confirmEmailReplaceAction: 'Replace primary email', - codeSentTo: 'Code sent to {email}', - replaceSuccess: 'Primary email updated', - unbindAction: 'Unbind', - unbindSuccess: '{providerName} unbound', - boundCount: '{count} linked records', - status: { - bound: 'Bound', - notBound: 'Not bound', - }, - providers: { - email: 'Email', - linuxdo: 'LinuxDo', - dingtalk: 'DingTalk', - oidc: '{providerName}', - wechat: 'WeChat', - }, - notes: { - emailManagedFromProfile: 'Primary email is managed in the profile form', - canUnbind: 'You can unbind this sign-in method', - bindAnotherBeforeUnbind: 'Bind another sign-in method before unbinding', - }, - source: { - avatar: 'Avatar is currently synced from {providerName}', - username: 'Nickname is currently synced from {providerName}', - }, - } - }, - - // Empty States - empty: { - noData: 'No data found' - }, - - // Table - table: { - expandActions: 'Expand More Actions', - collapseActions: 'Collapse Actions' - }, - - // Pagination - pagination: { - showing: 'Showing', - to: 'to', - of: 'of', - results: 'results', - page: 'Page', - pageOf: 'Page {page} of {total}', - previous: 'Previous', - next: 'Next', - perPage: 'Per page', - goToPage: 'Go to page {page}', - jumpTo: 'Jump to', - jumpPlaceholder: 'Page', - jumpAction: 'Go' - }, - - // Errors - errors: { - somethingWentWrong: 'Something went wrong', - pageNotFound: 'Page not found', - unauthorized: 'Unauthorized', - forbidden: 'Forbidden', - serverError: 'Server error', - networkError: 'Network error', - timeout: 'Request timeout', - tryAgain: 'Please try again' - }, - - // Dates - dates: { - today: 'Today', - yesterday: 'Yesterday', - thisWeek: 'This Week', - lastWeek: 'Last Week', - thisMonth: 'This Month', - lastMonth: 'Last Month', - last24Hours: 'Last 24 Hours', - last7Days: 'Last 7 Days', - last14Days: 'Last 14 Days', - last30Days: 'Last 30 Days', - custom: 'Custom', - startDate: 'Start Date', - endDate: 'End Date', - apply: 'Apply', - selectDateRange: 'Select date range' - }, - - // Admin - admin: { - // Dashboard - dashboard: { - title: 'Admin Dashboard', - description: 'System overview and real-time statistics', - apiKeys: 'API Keys', - accounts: 'Accounts', - users: 'Users', - todayRequests: 'Today Requests', - newUsersToday: 'New Users Today', - todayTokens: 'Today Tokens', - totalTokens: 'Total Tokens', - cacheToday: 'Cache (Today)', - performance: 'Performance', - avgResponse: 'Avg Response', - active: 'active', - ok: 'ok', - err: 'err', - activeUsers: 'active users', - create: 'Create', - timeRange: 'Time Range', - granularity: 'Granularity', - day: 'Day', - hour: 'Hour', - modelDistribution: 'Model Distribution', - groupDistribution: 'Group Usage Distribution', - metricTokens: 'By Tokens', - metricActualCost: 'By Actual Cost', - tokenUsageTrend: 'Token Usage Trend', - userUsageTrend: 'User Usage Trend (Top 12)', - model: 'Model', - group: 'Group', - noGroup: 'No Group', - requests: 'Requests', - tokens: 'Tokens', - actual: 'Actual', - standard: 'Standard', - accountCost: 'Cost', - noDataAvailable: 'No data available', - recentUsage: 'Recent Usage', - viewModelDistribution: 'Model Distribution', - viewSpendingRanking: 'User Spending Ranking', - spendingRankingTitle: 'User Spending Ranking', - spendingRankingUser: 'User', - spendingRankingRequests: 'Requests', - spendingRankingTokens: 'Tokens', - spendingRankingSpend: 'Spend', - spendingRankingOther: 'Others', - spendingRankingUsage: 'Usage', - spendShort: 'Spend', - requestsShort: 'Req', - tokensShort: 'Tok', - quickActions: 'Quick Actions', - batchImage: 'Batch Image', - batchImageDesc: 'Submit jobs and copy agent instructions', - groupPricing: 'Group Pricing', - groupPricingDesc: 'Configure batch discount and hold ratio', - failedToLoad: 'Failed to load dashboard statistics' - }, - - backup: { - title: 'Database Backup', - description: 'Full database backup to S3-compatible storage with scheduled backup and restore', - s3: { - title: 'S3 Storage Configuration', - description: 'Configure S3-compatible storage (supports Cloudflare R2)', - descriptionPrefix: 'Configure S3-compatible storage (supports', - descriptionSuffix: ')', - enabled: 'Enable S3 Storage', - endpoint: 'Endpoint', - region: 'Region', - bucket: 'Bucket', - prefix: 'Key Prefix', - accessKeyId: 'Access Key ID', - secretAccessKey: 'Secret Access Key', - secretConfigured: 'Already configured, leave empty to keep', - forcePathStyle: 'Force Path Style', - testConnection: 'Test Connection', - testSuccess: 'S3 connection test successful', - testFailed: 'S3 connection test failed', - saved: 'S3 configuration saved' - }, - schedule: { - title: 'Scheduled Backup', - description: 'Configure automatic scheduled backups', - enabled: 'Enable Scheduled Backup', - cronExpr: 'Cron Expression', - cronHint: 'e.g. "0 2 * * *" means every day at 2:00 AM', - retainDays: 'Backup Expire Days', - retainDaysHint: 'Backup files auto-delete after this many days, 0 = never expire', - retainCount: 'Max Retain Count', - retainCountHint: 'Maximum number of backups to keep, 0 = unlimited', - saved: 'Schedule configuration saved' - }, - operations: { - title: 'Backup Records', - description: 'Create manual backups and manage existing backup records', - createBackup: 'Create Backup', - backing: 'Backing up...', - backupCreated: 'Backup created successfully', - expireDays: 'Expire Days', - alreadyInProgress: 'A backup is already in progress', - backupRunning: 'Backup in progress...', - backupFailed: 'Backup failed', - restoreRunning: 'Restore in progress...', - restoreFailed: 'Restore failed', - }, - columns: { - status: 'Status', - fileName: 'File Name', - size: 'Size', - expiresAt: 'Expires At', - triggeredBy: 'Triggered By', - startedAt: 'Started At', - actions: 'Actions' - }, - status: { - pending: 'Pending', - running: 'Running', - completed: 'Completed', - failed: 'Failed' - }, - progress: { - pending: 'Preparing', - dumping: 'Dumping database', - uploading: 'Uploading', - }, - trigger: { - manual: 'Manual', - scheduled: 'Scheduled' - }, - neverExpire: 'Never', - empty: 'No backup records', - actions: { - download: 'Download', - restore: 'Restore', - restoreConfirm: 'Are you sure you want to restore from this backup? This will overwrite the current database!', - restorePasswordPrompt: 'Please enter your admin password to confirm the restore operation', - restoreSuccess: 'Database restored successfully', - deleteConfirm: 'Are you sure you want to delete this backup?', - deleted: 'Backup deleted' - }, - r2Guide: { - title: 'Cloudflare R2 Setup Guide', - intro: 'Cloudflare R2 provides S3-compatible object storage with a free tier of 10GB storage + 1M Class A requests/month, ideal for database backups.', - step1: { - title: 'Create an R2 Bucket', - line1: 'Log in to the Cloudflare Dashboard (dash.cloudflare.com), select "R2 Object Storage" from the sidebar', - line2: 'Click "Create bucket", enter a name (e.g. sub2api-backups), choose a region', - line3: 'Click create to finish' - }, - step2: { - title: 'Create an API Token', - line1: 'On the R2 page, click "Manage R2 API Tokens" in the top right', - line2: 'Click "Create API token", set permission to "Object Read & Write"', - line3: 'Recommended: restrict to specific bucket for better security', - line4: 'After creation, you will see the Access Key ID and Secret Access Key', - warning: 'The Secret Access Key is only shown once — copy and save it immediately!' - }, - step3: { - title: 'Get the S3 Endpoint', - desc: 'Find your Account ID on the R2 overview page (in the URL or the right panel). The endpoint format is:', - accountId: 'your_account_id' - }, - step4: { - title: 'Fill in the Configuration', - checkEnabled: 'Checked', - bucketValue: 'Your bucket name', - fromStep2: 'Value from Step 2', - unchecked: 'Unchecked' - }, - freeTier: 'R2 Free Tier: 10GB storage + 1M Class A requests + 10M Class B requests per month — more than enough for database backups.' - } - }, - - dataManagement: { - title: 'Data Management', - description: 'Manage data management agent status, object storage settings, and backup jobs in one place', - agent: { - title: 'Data Management Agent Status', - description: 'The system probes a fixed Unix socket and enables data management only when reachable.', - enabled: 'Data management agent is ready. Data management operations are available.', - disabled: 'Data management agent is unavailable. Only diagnostic information is available now.', - socketPath: 'Socket Path', - version: 'Version', - status: 'Status', - uptime: 'Uptime', - reasonLabel: 'Unavailable Reason', - reason: { - DATA_MANAGEMENT_AGENT_SOCKET_MISSING: 'Data management socket file is missing', - DATA_MANAGEMENT_AGENT_UNAVAILABLE: 'Data management agent is unreachable', - BACKUP_AGENT_SOCKET_MISSING: 'Backup socket file is missing', - BACKUP_AGENT_UNAVAILABLE: 'Backup agent is unreachable', - UNKNOWN: 'Unknown reason' - } - }, - sections: { - config: { - title: 'Backup Configuration', - description: 'Configure backup source, retention policy, and S3 settings.' - }, - s3: { - title: 'S3 Object Storage', - description: 'Configure and test uploads of backup artifacts to a standard S3-compatible storage.' - }, - backup: { - title: 'Backup Operations', - description: 'Trigger PostgreSQL, Redis, and full backup jobs.' - }, - history: { - title: 'Backup History', - description: 'Review backup job status, errors, and artifact metadata.' - } - }, - form: { - sourceMode: 'Source Mode', - backupRoot: 'Backup Root', - activePostgresProfile: 'Active PostgreSQL Profile', - activeRedisProfile: 'Active Redis Profile', - activeS3Profile: 'Active S3 Profile', - retentionDays: 'Retention Days', - keepLast: 'Keep Last Jobs', - uploadToS3: 'Upload to S3', - useActivePostgresProfile: 'Use Active PostgreSQL Profile', - useActiveRedisProfile: 'Use Active Redis Profile', - useActiveS3Profile: 'Use Active Profile', - idempotencyKey: 'Idempotency Key (Optional)', - secretConfigured: 'Configured already, leave empty to keep unchanged', - source: { - profileID: 'Profile ID (Unique)', - profileName: 'Profile Name', - setActive: 'Set as active after creation' - }, - postgres: { - title: 'PostgreSQL', - host: 'Host', - port: 'Port', - user: 'User', - password: 'Password', - database: 'Database', - sslMode: 'SSL Mode', - containerName: 'Container Name (docker_exec mode)' - }, - redis: { - title: 'Redis', - addr: 'Address (host:port)', - username: 'Username', - password: 'Password', - db: 'Database Index', - containerName: 'Container Name (docker_exec mode)' - }, - s3: { - enabled: 'Enable S3 Upload', - profileID: 'Profile ID (Unique)', - profileName: 'Profile Name', - endpoint: 'Endpoint (Optional)', - region: 'Region', - bucket: 'Bucket', - accessKeyID: 'Access Key ID', - secretAccessKey: 'Secret Access Key', - prefix: 'Object Prefix', - forcePathStyle: 'Force Path Style', - useSSL: 'Use SSL', - setActive: 'Set as active after creation' - } - }, - sourceProfiles: { - createTitle: 'Create Source Profile', - editTitle: 'Edit Source Profile', - empty: 'No source profiles yet, create one first', - deleteConfirm: 'Delete source profile {profileID}?', - columns: { - profile: 'Profile', - active: 'Active', - connection: 'Connection', - database: 'Database', - updatedAt: 'Updated At', - actions: 'Actions' - } - }, - s3Profiles: { - createTitle: 'Create S3 Profile', - editTitle: 'Edit S3 Profile', - empty: 'No S3 profiles yet, create one first', - editHint: 'Click "Edit" to modify profile details in the right drawer.', - deleteConfirm: 'Delete S3 profile {profileID}?', - columns: { - profile: 'Profile', - active: 'Active', - storage: 'Storage', - updatedAt: 'Updated At', - actions: 'Actions' - } - }, - history: { - total: '{count} jobs', - empty: 'No backup jobs yet', - columns: { - jobID: 'Job ID', - type: 'Type', - status: 'Status', - triggeredBy: 'Triggered By', - pgProfile: 'PostgreSQL Profile', - redisProfile: 'Redis Profile', - s3Profile: 'S3 Profile', - finishedAt: 'Finished At', - artifact: 'Artifact', - error: 'Error' - }, - status: { - queued: 'Queued', - running: 'Running', - succeeded: 'Succeeded', - failed: 'Failed', - partial_succeeded: 'Partial Succeeded' - } - }, - actions: { - refresh: 'Refresh Status', - disabledHint: 'Start datamanagementd first and ensure the socket is reachable.', - reloadConfig: 'Reload Config', - reloadSourceProfiles: 'Reload Source Profiles', - reloadProfiles: 'Reload Profiles', - newSourceProfile: 'New Source Profile', - saveConfig: 'Save Config', - configSaved: 'Configuration saved', - testS3: 'Test S3 Connection', - s3TestOK: 'S3 connection test succeeded', - s3TestFailed: 'S3 connection test failed', - newProfile: 'New Profile', - saveProfile: 'Save Profile', - activateProfile: 'Activate', - profileIDRequired: 'Profile ID is required', - profileNameRequired: 'Profile name is required', - profileSelectRequired: 'Select a profile to edit first', - profileCreated: 'S3 profile created', - profileSaved: 'S3 profile saved', - profileActivated: 'S3 profile activated', - profileDeleted: 'S3 profile deleted', - sourceProfileCreated: 'Source profile created', - sourceProfileSaved: 'Source profile saved', - sourceProfileActivated: 'Source profile activated', - sourceProfileDeleted: 'Source profile deleted', - createBackup: 'Create Backup Job', - jobCreated: 'Backup job created: {jobID} ({status})', - refreshJobs: 'Refresh Jobs', - loadMore: 'Load More' - } - }, - - affiliates: { - invitesDescription: 'View site-wide inviter and invitee relationships', - rebatesDescription: 'View recharge orders that generated affiliate rebates', - transfersDescription: 'View affiliate quota transfers into account balance', - errors: { - loadFailed: 'Failed to load affiliate records' - }, - records: { - search: 'Search', - searchPlaceholder: 'Email, username, user ID, or order number', - startAt: 'Start date', - endAt: 'End date', - inviter: 'Inviter', - invitee: 'Invitee', - user: 'User', - affCode: 'Invite Code', - order: 'Order', - totalRebate: 'Total Rebate', - orderAmount: 'Top-up Amount', - payAmount: 'Paid Amount', - rebateAmount: 'Rebate Amount', - paymentType: 'Payment Method', - orderStatus: 'Order Status', - transferAmount: 'Transfer Amount', - balanceAfter: 'Balance After', - availableQuotaAfter: 'Available After', - frozenQuotaAfter: 'Frozen After', - historyQuotaAfter: 'Historical Rebate After', - invitedAt: 'Invited At', - rebatedAt: 'Rebated At', - transferredAt: 'Transferred At' - }, - overview: { - title: 'Affiliate User Overview', - affCode: 'Invite Code', - rebateRate: 'Rebate Rate', - invitedCount: 'Invited Users', - rebatedInviteeCount: 'Rebated Invitees', - availableQuota: 'Available Quota', - historyQuota: 'Historical Rebate' - } - }, - - // Users - users: { - title: 'User Management', - description: 'Manage users and their permissions', - createUser: 'Create User', - editUser: 'Edit User', - deleteUser: 'Delete User', - searchUsers: 'Search by email, username, notes, or API key...', - allRoles: 'All Roles', - allStatus: 'All Status', - allGroups: 'All Groups', - searchGroups: 'Search groups...', - fuzzySearch: 'Fuzzy search', - apiKeyGroupFilter: 'API Key Group', - apiKeyGroupExclusive: 'Exclusive Groups', - apiKeyGroupPublic: 'Public Groups', - apiKeyGroupSubscription: 'Subscription Groups', - apiKeyGroupDisabled: 'Disabled Groups', - authorizedGroupFilter: 'Authorized Group', - allAuthorizedGroups: 'All Authorized Groups', - searchAuthorizedGroups: 'Search authorized groups...', - allApiKeyGroups: 'All API Key Groups', - searchApiKeyGroups: 'Search API Key groups...', - admin: 'Admin', - user: 'User', - disabled: 'Disabled', - email: 'Email', - password: 'Password', - username: 'Username', - notes: 'Notes', - enterEmail: 'Enter email', - enterPassword: 'Enter password', - enterUsername: 'Enter username (optional)', - enterNotes: 'Enter notes (admin only)', - notesHint: 'This note is only visible to administrators', - enterNewPassword: 'Enter new password (optional)', - leaveEmptyToKeep: 'Leave empty to keep current password', - generatePassword: 'Generate random password', - copyPassword: 'Copy password', - creating: 'Creating...', - updating: 'Updating...', - form: { - rpmLimit: 'Requests Per Minute (RPM)', - rpmLimitPlaceholder: '0 = unlimited', - rpmLimitHint: 'Max requests per minute for this user; 0 = unlimited. Acts as a fallback only when the group has no rpm_limit set.' - }, - columns: { - user: 'User', - id: 'ID', - email: 'Email', - username: 'Username', - notes: 'Notes', - role: 'Role', - groups: 'Groups', - subscriptions: 'Subscriptions', - balance: 'Balance', - balancePlatformQuota: 'Balance (Platform Quota)', - usage: 'Usage', - usageAnthropic: 'Usage (Claude)', - usageOpenAI: 'Usage (OpenAI)', - usageGemini: 'Usage (Gemini)', - usageAntigravity: 'Usage (Antigravity)', - concurrency: 'Concurrency', - status: 'Status', - lastActive: 'Last Active', - lastUsed: 'Last Used', - created: 'Created', - actions: 'Actions' - }, - today: 'Today', - total: 'Last 30d', - sortBy: 'Sort By', - sortCurrentPageOnly: 'Sorts current page only', - noSubscription: 'No subscription', - publicGroupCount: '+{count} public', - exclusiveLabel: 'exclusive', - publicLabel: 'public', - daysRemaining: '{days}d', - expired: 'Expired', - disable: 'Disable', - enable: 'Enable', - disableUser: 'Disable User', - enableUser: 'Enable User', - viewApiKeys: 'View API Keys', - groups: 'Groups', - apiKeys: 'API Keys', - userApiKeys: 'User API Keys', - noApiKeys: 'This user has no API keys', - group: 'Group', - none: 'None', - groupChangedSuccess: 'Group updated successfully', - groupChangedWithGrant: 'Group updated. User auto-granted access to "{group}"', - groupChangeFailed: 'Failed to update group', - noUsersYet: 'No users yet', - createFirstUser: 'Create your first user to get started.', - userCreated: 'User created successfully', - userUpdated: 'User updated successfully', - userDeleted: 'User deleted successfully', - userEnabled: 'User enabled successfully', - userDisabled: 'User disabled successfully', - failedToLoad: 'Failed to load users', - failedToCreate: 'Failed to create user', - failedToUpdate: 'Failed to update user', - failedToDelete: 'Failed to delete user', - failedToToggle: 'Failed to update user status', - failedToLoadApiKeys: 'Failed to load user API keys', - emailRequired: 'Please enter email', - concurrencyMin: 'Concurrency must be at least 1', - soraStorageQuota: 'Sora Storage Quota', - soraStorageQuotaHint: 'In GB, 0 means use group or system default quota', - amountRequired: 'Please enter a valid amount', - insufficientBalance: 'Insufficient balance', - deleteConfirm: "Are you sure you want to delete '{email}'? This action cannot be undone.", - setAllowedGroups: 'Set Allowed Groups', - allowedGroupsHint: - 'Select which standard groups this user can use. Subscription groups are managed separately.', - noStandardGroups: 'No standard groups available', - allowAllGroups: 'Allow All Groups', - allowAllGroupsHint: 'User can use any non-exclusive group', - allowedGroupsUpdated: 'Allowed groups updated successfully', - failedToLoadGroups: 'Failed to load groups', - failedToUpdateAllowedGroups: 'Failed to update allowed groups', - // User Group Configuration - groupConfig: 'User Group Configuration', - groupConfigHint: 'Configure custom rate multipliers for user {email} (overrides group defaults)', - exclusiveGroups: 'Exclusive Groups', - publicGroups: 'Public Groups (Default Available)', - defaultRate: 'Default Rate', - customRate: 'Custom Rate', - useDefaultRate: 'Use Default', - customRatePlaceholder: 'Leave empty for default', - groupConfigUpdated: 'Group configuration updated successfully', - replaceGroup: 'Replace Group', - clickToReplace: 'Click to replace', - replaceGroupTitle: 'Replace Exclusive Group', - replaceGroupHint: 'Select a new group to replace "{old}". Keys will be migrated and permissions updated automatically.', - replaceGroupConfirm: 'Confirm Replace', - replaceGroupSuccess: 'Group replaced successfully, {count} key(s) migrated', - selectNewGroup: 'Select target group', - noOtherGroups: 'No other exclusive groups available', - deposit: 'Deposit', - withdraw: 'Withdraw', - depositAmount: 'Deposit Amount', - withdrawAmount: 'Withdraw Amount', - withdrawAll: 'All', - currentBalance: 'Current Balance', - depositNotesPlaceholder: - 'e.g., New user registration bonus, promotional credit, compensation, etc.', - withdrawNotesPlaceholder: - 'e.g., Service issue refund, incorrect charge reversal, account closure refund, etc.', - notesOptional: 'Notes are optional but helpful for record keeping', - amountHint: 'Please enter a positive amount', - newBalance: 'New Balance', - depositing: 'Depositing...', - withdrawing: 'Withdrawing...', - confirmDeposit: 'Confirm Deposit', - confirmWithdraw: 'Confirm Withdraw', - depositSuccess: 'Deposit successful', - withdrawSuccess: 'Withdraw successful', - failedToDeposit: 'Failed to deposit', - failedToWithdraw: 'Failed to withdraw', - useDepositWithdrawButtons: 'Please use deposit/withdraw buttons to adjust balance', - // Balance History - balanceHistory: 'Recharge History', - balanceHistoryTip: 'Click to open recharge history', - columnAlwaysVisible: 'This column is always visible', - // Per-platform usage breakdown (hover tooltip) - platformBreakdown: 'Per-platform breakdown', - platformBreakdownEmpty: 'No platform usage yet', - platformBreakdownHint: 'Hover for per-platform usage', - platformOther: 'Other', - balanceHistoryTitle: 'User Recharge & Concurrency History', - noBalanceHistory: 'No records found for this user', - allTypes: 'All Types', - typeBalance: 'Balance (Redeem)', - typeAffiliateBalance: 'Balance (Affiliate Transfer)', - typeAdminBalance: 'Balance (Admin)', - typeConcurrency: 'Concurrency (Redeem)', - typeAdminConcurrency: 'Concurrency (Admin)', - typeSubscription: 'Subscription', - failedToLoadBalanceHistory: 'Failed to load balance history', - createdAt: 'Created', - totalRecharged: 'Total Recharged', - roles: { - admin: 'Admin', - user: 'User' - }, - // Settings Dropdowns - filterSettings: 'Filter Settings', - columnSettings: 'Column Settings', - filterValue: 'Enter value', - // User Attributes - attributes: { - title: 'User Attributes', - description: 'Configure custom user attribute fields', - configButton: 'Attributes', - addAttribute: 'Add Attribute', - editAttribute: 'Edit Attribute', - deleteAttribute: 'Delete Attribute', - deleteConfirm: "Are you sure you want to delete attribute '{name}'? All user values for this attribute will be deleted.", - noAttributes: 'No custom attributes', - noAttributesHint: 'Click the button above to add custom attributes', - key: 'Attribute Key', - keyHint: 'For programmatic reference, only letters, numbers and underscores', - name: 'Display Name', - nameHint: 'Name shown in forms', - type: 'Attribute Type', - fieldDescription: 'Description', - fieldDescriptionHint: 'Description text for the attribute', - placeholder: 'Placeholder', - placeholderHint: 'Placeholder text for input field', - required: 'Required', - enabled: 'Enabled', - options: 'Options', - optionsHint: 'For select/multi-select types', - addOption: 'Add Option', - optionValue: 'Option Value', - optionLabel: 'Display Text', - validation: 'Validation Rules', - minLength: 'Min Length', - maxLength: 'Max Length', - min: 'Min Value', - max: 'Max Value', - pattern: 'Regex Pattern', - patternMessage: 'Validation Error Message', - types: { - text: 'Text', - textarea: 'Textarea', - number: 'Number', - email: 'Email', - url: 'URL', - date: 'Date', - select: 'Select', - multi_select: 'Multi-Select' - }, - created: 'Attribute created successfully', - updated: 'Attribute updated successfully', - deleted: 'Attribute deleted successfully', - reordered: 'Attribute order updated successfully', - failedToLoad: 'Failed to load attributes', - failedToCreate: 'Failed to create attribute', - failedToUpdate: 'Failed to update attribute', - keyRequired: 'Please enter attribute key', - nameRequired: 'Please enter display name', - optionsRequired: 'Please add at least one option', - failedToDelete: 'Failed to delete attribute', - failedToReorder: 'Failed to update order', - keyExists: 'Attribute key already exists', - dragToReorder: 'Drag to reorder' - }, - platformQuota: { - menuItem: 'Platform Quotas', - title: 'Platform Quotas', - subtitle: 'Configure daily / weekly / monthly USD usage limits for each upstream platform for user {email}', - columns: { - platform: 'Platform', - daily: 'Daily (USD)', - weekly: 'Weekly (USD)', - monthly: 'Monthly (USD, 30-day rolling)', - usage: 'Current Usage', - }, - placeholder: 'unlimited', - save: 'Save', - saving: 'Saving...', - cancel: 'Cancel', - clearAll: 'Clear All (remove all limits)', - clearAllConfirm: 'Clear daily / weekly / monthly limits for ALL platforms? All platforms will become "unlimited" with no local undo — you must manually re-enter values before saving.', - reset: { - button: 'Reset window', - confirm: 'Reset the {window} usage for {platform} for this user? This is effective immediately.', - success: 'Reset {platform} {window} usage', - failed: 'Reset failed', - }, - updateSuccess: 'Platform quotas updated', - updateFailed: 'Save failed', - loadFailed: 'Load failed', - hint: 'Empty = no limit for that window.', - windowDaily: 'daily', - windowWeekly: 'weekly', - windowMonthly: 'monthly', - cellNotConfigured: 'Not configured', - cellColumnTooltip: 'Only platforms with a limit are shown', - subscriptionWarning: 'This user has an active subscription. Platform quotas only apply to balance (standard) mode requests; subscription mode requests are not subject to these limits.', - invalidNumber: 'The following fields contain invalid numbers. Please fix them before saving: {fields}', - } - }, - - // Groups - groups: { - title: 'Group Management', - description: 'Manage API key groups and rate multipliers', - searchGroups: 'Search groups...', - createGroup: 'Create Group', - editGroup: 'Edit Group', - deleteGroup: 'Delete Group', - sortOrder: 'Sort', - columnSettings: 'Column Settings', - sortOrderHint: 'Drag groups to adjust display order, groups at the top will be displayed first', - sortOrderUpdated: 'Sort order updated', - failedToUpdateSortOrder: 'Failed to update sort order', - allPlatforms: 'All Platforms', - allStatus: 'All Status', - allGroups: 'All Groups', - exclusive: 'Exclusive', - nonExclusive: 'Non-Exclusive', - public: 'Public', - columns: { - name: 'Name', - platform: 'Platform', - rateMultiplier: 'Rate Multiplier', - rpmOverride: 'RPM Override', - rpmOverrideHint: 'Per-user RPM cap in this group; empty = group default; 0 = unlimited', - rateDefault: 'default', - rpmDefault: 'default', - type: 'Type', - accounts: 'Accounts', - capacity: 'Capacity', - usage: 'Usage', - status: 'Status', - actions: 'Actions', - billingType: 'Billing Type', - userName: 'Username', - userEmail: 'Email', - userNotes: 'Notes', - userStatus: 'Status' - }, - usageToday: 'Today', - usageTotal: 'Total', - accountsAvailable: 'Avail:', - accountsRateLimited: 'Limited:', - accountsTotal: 'Total:', - accountsUnit: '', - rateAndAccounts: '{rate}x rate · {count} accounts', - accountsCount: '{count} accounts', - rateLabel: 'rate', - accountFilters: { - title: 'Account Filter Controls', - oauthOnly: 'Only allow OAuth accounts', - oauthOnlyEnabled: 'Enabled — API Key accounts will be excluded', - privacySetOnly: 'Only allow accounts with privacy protection set', - privacySetOnlyEnabled: 'Enabled — accounts with unset Privacy will be excluded', - disabled: 'Disabled' - }, - form: { - name: 'Name', - description: 'Description', - platform: 'Platform', - rateMultiplier: 'Rate Multiplier', - status: 'Status', - exclusive: 'Exclusive Group', - rpmLimit: 'Requests Per Minute (RPM)', - rpmLimitPlaceholder: '0 = unlimited', - rpmLimitHint: 'Max requests per minute for each user in this group; 0 = unlimited. Once set, it takes over per-user rate limiting in this group (overrides the user-level rpm_limit fallback).' - }, - enterGroupName: 'Enter group name', - optionalDescription: 'Optional description', - platformHint: 'Select the platform this group is associated with', - platformNotEditable: 'Platform cannot be changed after creation', - rateMultiplierHint: 'Cost multiplier for this group (e.g., 1.5 = 150% of base cost)', - exclusiveHint: 'Exclusive group, manually assign to specific users', - exclusiveTooltip: { - title: 'What is an exclusive group?', - description: 'When enabled, users cannot see this group when creating API Keys. Only after an admin manually assigns a user to this group can they use it.', - example: 'Use case:', - exampleContent: 'Public group rate is 0.8. Create an exclusive group with 0.7 rate, manually assign VIP users to give them better pricing.' - }, - noGroupsYet: 'No groups yet', - createFirstGroup: 'Create your first group to organize API keys.', - creating: 'Creating...', - updating: 'Updating...', - limitDay: 'd', - limitWeek: 'w', - limitMonth: 'mo', - groupCreated: 'Group created successfully', - groupUpdated: 'Group updated successfully', - groupDeleted: 'Group deleted successfully', - failedToLoad: 'Failed to load groups', - failedToCreate: 'Failed to create group', - failedToUpdate: 'Failed to update group', - failedToDelete: 'Failed to delete group', - nameRequired: 'Please enter group name', - rateMultipliers: 'Rate Multipliers', - rateMultipliersTitle: 'Group Rate Multipliers', - addUserRate: 'Add User Rate Multiplier', - rpmOverrides: 'RPM Overrides', - rpmOverridesTitle: 'Group RPM Overrides', - addUserRpm: 'Add User RPM Override', - noRpmOverrides: 'No users have an RPM override yet', - rpmSaved: 'RPM overrides saved', - groupRpmDefault: 'Group default RPM', - searchUserPlaceholder: 'Search user email...', - noRateMultipliers: 'No user rate multipliers configured', - rateUpdated: 'Rate multiplier updated', - rateDeleted: 'Rate multiplier removed', - rateAdded: 'Rate multiplier added', - clearAll: 'Clear All', - confirmClearAll: 'Are you sure you want to clear all rate multiplier settings for this group? This cannot be undone.', - rateCleared: 'All rate multipliers cleared', - batchAdjust: 'Batch Adjust Rates', - multiplierFactor: 'Factor', - applyMultiplier: 'Apply', - rateAdjusted: 'Rates adjusted successfully', - rateSaved: 'Rate multipliers saved', - finalRate: 'Final Rate', - unsavedChanges: 'Unsaved changes', - revertChanges: 'Revert', - userInfo: 'User Info', - platforms: { - all: 'All Platforms', - anthropic: 'Anthropic', - openai: 'OpenAI', - gemini: 'Gemini', - antigravity: 'Antigravity', - grok: 'Grok', - }, - deleteConfirm: - "Are you sure you want to delete '{name}'? All associated API keys will no longer belong to any group.", - deleteConfirmSubscription: - "Are you sure you want to delete subscription group '{name}'? This will invalidate all API keys bound to this subscription and delete all related subscription records. This action cannot be undone.", - subscription: { - title: 'Subscription Settings', - type: 'Billing Type', - typeHint: - 'Standard billing deducts from user balance. Subscription mode uses quota limits instead.', - typeNotEditable: 'Billing type cannot be changed after group creation.', - standard: 'Standard (Balance)', - subscription: 'Subscription (Quota)', - dailyLimit: 'Daily Limit (USD)', - weeklyLimit: 'Weekly Limit (USD)', - monthlyLimit: 'Monthly Limit (USD)', - defaultValidityDays: 'Default Validity (Days)', - validityHint: 'Number of days the subscription is valid when assigned to a user', - noLimit: 'No limit' - }, - imagePricing: { - title: 'Image Generation Pricing', - description: 'Configure image generation access and base image prices. Leave empty to use default prices.', - allowImageGeneration: 'Allow image generation for this group', - allowBatchImageGeneration: 'Allow batch image generation for this group', - independentMultiplier: 'Use independent image multiplier', - imageMultiplier: 'Image multiplier', - batchDiscountMultiplier: 'Batch image discount', - batchHoldMultiplier: 'Batch hold price ratio', - batchSectionHint: 'Batch image settings only apply to batch jobs: settlement applies the batch discount, and the upfront hold is normal image price × batch hold price ratio. Reference images also create upstream input-token usage, so a batch image discount above 0.5 is recommended.', - batchDisabledHint: 'Enable image generation for this group before enabling batch image generation.', - batchGeminiOnlyHint: 'Batch image generation is currently available only for Gemini groups.', - modeHint: 'By default, image billing uses image price × current effective group multiplier. Independent mode uses image price × image multiplier.', - finalPricePreview: 'Final per-image price preview', - notConfigured: 'Not configured' - }, - peakRate: { - enable: 'Enable peak rate multiplier', - peakStart: 'Peak start', - peakEnd: 'Peak end', - peakMultiplier: 'Peak multiplier', - multiplierHint: 'Applies to token billing multiplier; image tokens in token billing are also affected. 0 means peak token requests are billed at 0x.' - }, - modelsList: { - title: 'Custom /v1/models Model List', - hint: 'Only changes the /v1/models response. Whitelist model calls and account routing are unchanged.', - loading: 'Loading model list...', - empty: 'No displayable models', - selectedSummary: 'Selected {selected} / {total}', - selectAll: 'Select all', - invertSelection: 'Invert' - }, - claudeCode: { - title: 'Claude Code Client Restriction', - tooltip: 'When enabled, this group only allows official Claude Code clients. Non-Claude Code requests will be rejected or fallback to the specified group.', - enabled: 'Claude Code Only', - disabled: 'Allow All Clients', - fallbackGroup: 'Fallback Group', - fallbackHint: 'Non-Claude Code requests will use this group. Leave empty to reject directly.', - noFallback: 'No Fallback (Reject)' - }, - openaiMessages: { - title: 'OpenAI Messages Dispatch', - allowDispatch: 'Allow /v1/messages dispatch', - allowDispatchHint: 'When enabled, API keys in this OpenAI group can dispatch requests through /v1/messages endpoint', - familyMappingTitle: 'Family Default Mapping', - familyMappingHint: 'Requests that match the Opus, Sonnet, or Haiku families will prefer the target model configured here.', - opusModel: 'Opus Target Model', - opusModelPlaceholder: 'e.g., gpt-5.4', - sonnetModel: 'Sonnet Target Model', - sonnetModelPlaceholder: 'e.g., gpt-5.3-codex', - haikuModel: 'Haiku Target Model', - haikuModelPlaceholder: 'e.g., gpt-5.4-mini', - exactMappingTitle: 'Exact Model Overrides', - exactMappingHint: 'Exact Claude model overrides take priority over the family defaults and can route a specific Claude model to a different target model.', - noExactMappings: 'No exact model overrides yet', - addExactMapping: 'Add Exact Mapping', - claudeModel: 'Claude Model', - claudeModelPlaceholder: 'e.g., claude-sonnet-4-5-20250929', - targetModel: 'Target Model', - targetModelPlaceholder: 'e.g., gpt-5.4', - removeExactMapping: 'Remove Exact Mapping' - }, - invalidRequestFallback: { - title: 'Invalid Request Fallback Group', - hint: 'Triggered only when upstream explicitly returns prompt too long. Leave empty to disable fallback.', - noFallback: 'No Fallback' - }, - copyAccounts: { - title: 'Copy Accounts from Groups', - tooltip: 'Select one or more groups of the same platform. After creation, all accounts from these groups will be automatically bound to the new group (deduplicated).', - tooltipEdit: 'Select one or more groups of the same platform. After saving, current group accounts will be replaced with accounts from these groups (deduplicated).', - selectPlaceholder: 'Select groups to copy accounts from...', - hint: 'Multiple groups can be selected, accounts will be deduplicated', - hintEdit: '⚠️ Warning: This will replace all existing account bindings' - }, - modelRouting: { - title: 'Model Routing', - tooltip: 'Configure specific model requests to be routed to designated accounts. Supports wildcard matching, e.g., claude-opus-* matches all opus models.', - enabled: 'Enabled', - disabled: 'Disabled', - disabledHint: 'Routing rules will only take effect when enabled', - addRule: 'Add Routing Rule', - modelPattern: 'Model Pattern', - modelPatternPlaceholder: 'claude-opus-*', - modelPatternHint: 'Supports * wildcard, e.g., claude-opus-* matches all opus models', - accounts: 'Priority Accounts', - selectAccounts: 'Select accounts', - noAccounts: 'No accounts in this group', - loadingAccounts: 'Loading accounts...', - removeRule: 'Remove Rule', - noRules: 'No routing rules', - noRulesHint: 'Add routing rules to route specific model requests to designated accounts', - searchAccountPlaceholder: 'Search accounts...', - accountsHint: 'Select accounts to prioritize for this model pattern' - }, - mcpXml: { - title: 'MCP XML Protocol Injection', - tooltip: 'When enabled, if the request contains MCP tools, an XML format call protocol prompt will be injected into the system prompt. Disable this to avoid interference with certain clients.', - enabled: 'Enabled', - disabled: 'Disabled' - }, - claudeMaxSimulation: { - title: 'Claude Max Usage Simulation', - tooltip: - 'When enabled, for Claude models without upstream cache-write usage, the system deterministically maps tokens to a small input plus 1h cache creation while keeping total tokens unchanged.', - enabled: 'Enabled (simulate 1h cache)', - disabled: 'Disabled', - hint: 'Only token categories in usage billing logs are adjusted. No per-request mapping state is persisted.' - }, - supportedScopes: { - title: 'Supported Model Families', - tooltip: 'Select the model families this group supports. Unchecked families will not be routed to this group.', - claude: 'Claude', - geminiText: 'Gemini Text', - geminiImage: 'Gemini Image', - hint: 'Select at least one model family' - } - }, - - // Available Channels (aggregated read-only view) - availableChannels: { - title: 'Available Channels', - description: 'Aggregated view: each channel with its linked groups and supported models (wildcards expanded)', - searchPlaceholder: 'Search channels or models...', - columns: { - name: 'Channel', - status: 'Status', - billingSource: 'Billing Model Source', - groups: 'Linked Groups', - supportedModels: 'Supported Models' - }, - empty: 'No data', - noGroups: 'No linked groups', - noModels: 'No model mapping configured', - noPricing: 'Pricing not configured', - statusActive: 'Active', - statusDisabled: 'Disabled', - billingSource: { - requested: 'Requested model', - upstream: 'Upstream model', - channel_mapped: 'Channel-mapped model' - }, - pricing: { - billingMode: 'Billing Mode', - billingModeToken: 'Per Token', - billingModePerRequest: 'Per Request', - billingModeImage: 'Per Image', - inputPrice: 'Input', - outputPrice: 'Output', - cacheWritePrice: 'Cache Write', - cacheReadPrice: 'Cache Read', - imageOutputPrice: 'Image Output', - perRequestPrice: 'Per Request', - intervals: 'Tiered Pricing', - unitPerMillion: '/ 1M tokens', - unitPerRequest: '/ request' - } - }, - - // Channel Management - channels: { - title: 'Channel Management', - description: 'Manage channels and custom model pricing', - searchChannels: 'Search channels...', - createChannel: 'Create Channel', - editChannel: 'Edit Channel', - deleteChannel: 'Delete Channel', - statusActive: 'Active', - statusDisabled: 'Disabled', - allStatus: 'All Status', - groupsUnit: 'groups', - pricingUnit: 'pricing rules', - noChannelsYet: 'No Channels Yet', - createFirstChannel: 'Create your first channel to manage model pricing', - loadError: 'Failed to load channels', - createSuccess: 'Channel created', - updateSuccess: 'Channel updated', - deleteSuccess: 'Channel deleted', - createError: 'Failed to create channel', - updateError: 'Failed to update channel', - deleteError: 'Failed to delete channel', - nameRequired: 'Please enter a channel name', - duplicateModels: 'Model "{0}" appears in multiple pricing entries', - modelConflict: "Model patterns '{model1}' and '{model2}' conflict: overlapping match range. Model names are matched case-insensitively, so an existing entry already covers all case variants — no need to add the variant separately.", - mappingConflict: "Mapping source patterns '{model1}' and '{model2}' conflict: overlapping match range. Source patterns are matched case-insensitively, so an existing entry already covers all case variants.", - intervalValidation: { - negativeMin: 'Interval #{index}: minimum token count ({value}) cannot be negative', - maxPositive: 'Interval #{index}: maximum token count ({value}) must be greater than 0', - maxGreaterThanMin: 'Interval #{index}: maximum token count ({max}) must be greater than minimum token count ({min})', - negativePrice: 'Interval #{index}: {field} cannot be negative', - unboundedLast: 'Interval #{index}: an unbounded interval (empty maximum token count) must be last', - overlap: 'Intervals #{previousIndex} and #{currentIndex} overlap: previous upper bound ({previousMax}) is greater than current lower bound ({currentMin})', - price: { - inputPrice: 'input price', - outputPrice: 'output price', - cacheWritePrice: 'cache write price', - cacheReadPrice: 'cache read price', - perRequestPrice: 'per-request price' - } - }, - deleteConfirm: 'Are you sure you want to delete channel "{name}"? This cannot be undone.', - columns: { - name: 'Name', - description: 'Description', - status: 'Status', - groups: 'Groups', - pricing: 'Pricing', - createdAt: 'Created', - actions: 'Actions' - }, - billingMode: { - token: 'Token', - perRequest: 'Per Request', - image: 'Image (Per Request)' - }, - form: { - name: 'Name', - namePlaceholder: 'Enter channel name', - description: 'Description', - descriptionPlaceholder: 'Optional description', - status: 'Status', - groups: 'Associated Groups', - noGroupsAvailable: 'No groups available', - inOtherChannel: 'In "{name}"', - modelPricing: 'Model Pricing', - models: 'Models', - modelsPlaceholder: 'Type full model name and press Enter', - modelInputHint: 'Press Enter to add, supports paste for batch import.', - billingMode: 'Billing Mode', - defaultPrices: 'Default prices (fallback when no interval matches)', - inputPrice: 'Input', - outputPrice: 'Output', - cacheWritePrice: 'Cache Write', - cacheReadPrice: 'Cache Read', - cacheWritePriceShort: 'Cache W', - cacheReadPriceShort: 'Cache R', - imageTokenPrice: 'Image Output', - imageOutputPrice: 'Image Output Price', - pricePlaceholder: 'Default', - intervals: 'Context Intervals (optional)', - minTokens: 'Min', - maxTokens: 'Max', - inclusive: '(inclusive)', - addInterval: 'Add Interval', - requestTiers: 'Request Tiers', - imageTiers: 'Image Tiers (Per Request)', - addTier: 'Add Tier', - noTiersYet: 'No tiers yet. Click add to configure per-request pricing.', - noPricingRules: 'No pricing rules yet. Click "Add" to create one.', - perRequestPrice: 'Price per Request', - perRequestPriceRequired: 'Per-request price or billing tiers required for per-request/image billing mode', - tierLabel: 'Tier', - resolution: 'Resolution', - modelMapping: 'Model Mapping', - modelMappingHint: 'Map request model names to actual model names. Runs before account-level mapping.', - noMappingRules: 'No mapping rules. Click "Add" to create one.', - mappingSource: 'Source model', - mappingTarget: 'Target model', - billingModelSource: 'Billing Model', - billingModelSourceChannelMapped: 'Bill by channel-mapped model', - billingModelSourceRequested: 'Bill by requested model', - billingModelSourceUpstream: 'Bill by final upstream model', - billingModelSourceHint: 'Controls which model name is used for pricing lookup', - selectedCount: '{count} selected', - searchGroups: 'Search groups...', - noGroupsMatch: 'No groups match your search', - restrictModels: 'Restrict Models', - restrictModelsHint: 'When enabled, only models in the pricing list are allowed. Others will be rejected.', - defaultPerRequestPrice: 'Default per-request price (fallback when no tier matches)', - defaultImagePrice: 'Default image price (fallback when no tier matches)', - platformConfig: 'Platform Configuration', - webSearchEmulation: 'Web Search Emulation', - webSearchEmulationHint: '⚠️ When enabled, all accounts in this channel\'s Anthropic groups will intercept web_search requests. Use with caution.', - webSearchEmulationGlobalDisabled: 'Please enable the global switch first in Settings → Gateway → Web Search Emulation', - codexImageGenerationBridge: 'Codex Image Generation Bridge', - codexImageGenerationBridgeHint: 'When enabled, Codex /responses text requests in OpenAI groups may be automatically given the image_generation tool. Keep off unless the routed accounts support image generation.', - bedrockCCCompat: 'Bedrock CC Compatibility', - bedrockCCCompatHint: '⚠️ When enabled, requests to Bedrock accounts in this channel will be transformed for Claude Code compatibility (thinking type conversion, tool_use ID sanitization).', - basicSettings: 'Basic Settings', - addPlatform: 'Add Platform', - noPlatforms: 'Click "Add Platform" to start configuring the channel', - mappingCount: 'mappings', - pricingEntry: 'Pricing Entry', - noModels: 'No models added', - applyPricingToAccountStats: 'Apply Pricing to Account Stats', - applyPricingToAccountStatsDesc: 'When enabled, requests not matched by custom rules will use standard model pricing for account stats calculation', - accountStatsPricingRules: 'Custom Account Stats Pricing Rules', - addRule: 'Add Rule', - noRulesConfigured: 'No custom rules configured. Channel model pricing above will be used.', - ruleName: 'Rule name (optional)', - ruleGroups: 'Groups', - ruleAccounts: 'Accounts', - searchAccountPlaceholder: 'Search accounts...', - ruleAccountsHint: 'Leave empty to match all accounts', - ruleModelPricing: 'Model Pricing', - noGroupsInChannel: 'No groups selected in platform tabs above', - unnamed: 'Unnamed', - syncLatestModels: 'Sync Latest Models', - syncingModels: 'Syncing...', - syncModelsSuccess: 'Synced {count} new model(s)', - syncModelsAlreadyUpToDate: 'Models already up to date', - syncModelsError: 'Failed to sync models' - } - }, - - riskControl: { - title: 'Risk Control', - description: 'Configure content moderation and review audit records', - loadFailed: 'Failed to load risk control', - saveFailed: 'Failed to save content moderation config', - logsFailed: 'Failed to load audit records', - saved: 'Content moderation config saved', - refresh: 'Refresh', - config: 'Content Moderation Config', - configHint: 'Use OpenAI Moderations to score request content and handle threshold hits by mode.', - openSettings: 'Moderation Settings', - settingsTitle: 'Content Moderation Settings', - refreshStatus: 'Refresh Status', - records: 'Audit Records', - recordsHint: 'Shows hits, blocks, errors, and sampled records.', - saveConfig: 'Save Moderation Config', - statusFailed: 'Failed to load runtime status', - enabled: 'Enable Content Moderation', - enabledHint: 'When off, gateway requests are not moderated even if the menu is enabled.', - mode: 'Global Mode', - modePreBlock: 'Pre-Block', - modePreBlockDesc: 'Synchronously reviews the latest user input before every request and rejects hits immediately.', - modeObserve: 'Observe Only', - modeObserveDesc: 'Requests pass through while the latest user input is queued for async review; hits are recorded, notified, and counted.', - modeOff: 'Off', - modeOffDesc: 'Content moderation is disabled and no audit records are written.', - baseUrl: 'OpenAI Base URL', - model: 'Model', - apiKey: 'OpenAI API Key', - apiKeys: 'OpenAI API Keys', - apiKeyCount: '{count} keys', - apiKeyPlaceholder: 'Enter API Key', - apiKeysPlaceholder: 'Add API Keys, one per line. They will be appended on save.', - apiKeysPlaceholderReplace: 'Replace API Keys, one per line. Stored keys will be replaced on save.', - apiKeysPlaceholderKeep: 'Add API Keys, one per line. They will be appended on save.', - apiKeysHint: '{count} keys are currently stored. This input only adds keys; save appends and de-duplicates them.', - apiKeysWriteMode: 'Write mode', - apiKeysModeAppend: 'Add', - apiKeysModeReplace: 'Replace', - apiKeysModeAppendHint: 'Default: save appends input keys and keeps stored keys.', - apiKeysModeReplaceHint: 'Replace mode: save replaces all stored keys with input keys.', - apiKeysReplaceWarning: 'Replace mode', - apiKeysReplaceNoInput: 'Replace mode requires at least 1 API Key', - apiKeyPlaceholderKeep: 'Leave empty to keep current key', - apiKeyWillClear: 'Configured key will be cleared on save', - apiKeyConfigured: 'Configured', - apiKeyTemporary: 'Pending', - apiKeyPendingDelete: 'Pending delete', - apiKeyPendingDeleteCount: '{count} keys pending deletion', - deleteApiKey: 'Delete this key', - undoDeleteApiKey: 'Undo delete', - inputApiKeyCount: '{count} keys in input', - storedApiKeyCount: '{count} stored keys', - testInputApiKeys: 'Test input keys', - testStoredApiKeys: 'Test stored keys', - testContentWithStoredApiKey: 'Test content with stored key', - testingApiKeys: 'Testing', - apiKeyTestNoInput: 'Enter OpenAI API Keys to test first', - apiKeyTestDone: 'Key test completed for {count} keys', - apiKeyTestFailed: 'Failed to test OpenAI API Keys', - apiKeyHealth: 'Key Availability', - apiKeyFreezeRule: '400 does not freeze; 401/403 freeze for 10 minutes; 429/529 freeze for 1 minute; other HTTP errors freeze for 10 seconds.', - apiKeyRows: '{count} keys', - apiKeyRowsCollapsed: '{count} keys hidden', - apiKeyRowsExpanded: 'Showing all {count} keys', - expandApiKeyRows: 'Expand', - collapseApiKeyRows: 'Collapse', - apiKeyHealthEmpty: 'No key status yet', - apiKeyHealthEmptyHint: 'Save keys or test input keys to see availability.', - apiKeyStatusOk: 'Available', - apiKeyStatusError: 'Error', - apiKeyStatusFrozen: 'Frozen', - apiKeyStatusUnknown: 'Untested', - apiKeyFailureCount: '{count} failures', - apiKeyLatency: '{ms} ms', - apiKeyHTTPStatus: 'HTTP {status}', - apiKeyFrozenUntil: 'Frozen until {time}', - apiKeyLastChecked: 'Checked at {time}', - apiKeyNotTested: 'Not tested', - auditTestInput: 'Audit Test Input', - auditTestInputHint: 'Enter a prompt and upload or paste images; images are sent as base64 and are not stored.', - auditTestPromptPlaceholder: 'Enter a user prompt to test; leave empty to only test key availability.', - auditTestImages: 'Test Images', - auditTestImagesHint: 'Upload, drag, or paste images. Up to 1 image, 8MB each.', - addAuditTestImage: 'Add image', - clearAuditTest: 'Clear test', - auditTestImageLimit: 'You can add up to {count} test images', - auditTestImageTooLarge: 'Each test image must be 8MB or smaller', - auditTestImageReadFailed: 'Failed to read test image', - auditTestResult: 'Audit Test Result', - auditTestHighest: 'Top category {category}, score {score}', - auditTestComposite: 'Composite score', - auditTestFlagged: 'Threshold hit', - auditTestPassed: 'Pass', - notConfigured: 'Not configured', - clearApiKey: 'Clear stored key', - keepApiKey: 'Keep stored key', - timeoutMs: 'HTTP Timeout (ms)', - retryCount: 'Retry Count', - sampleRate: 'Sample Rate', - recordNonHits: 'Record Non-Hits', - recordNonHitsHint: 'When enabled, sampled non-hit request summaries are redacted before storage.', - preHashCheck: 'Enable Pre-Hash Check', - preHashCheckHint: 'Hashes from async hits are blocked before moderation; this does not send email or increment ban counters.', - flaggedHashCount: 'Current hash collection size: {count}', - flaggedHashHint: 'Hashes are stored permanently in Redis; paste a full 64-character hash to remove a false block, or clear all stored hashes.', - flaggedHashPlaceholder: 'Paste full 64-character input hash', - deleteFlaggedHash: 'Delete hash', - clearFlaggedHashes: 'Clear all', - clearFlaggedHashesConfirm: 'Clear all risk input hashes? This does not delete audit records, but removes all historical hash blocks.', - flaggedHashDeleted: 'Risk hash deleted', - flaggedHashNotFound: 'Risk hash not found', - flaggedHashDeleteFailed: 'Failed to delete risk hash', - flaggedHashesCleared: 'Cleared {count} risk hashes', - flaggedHashesClearFailed: 'Failed to clear risk hashes', - workerCount: 'Worker Count', - queueSize: 'Async Queue Size', - blockStatus: 'Block HTTP Status', - blockMessage: 'Custom Block Message', - defaultBlockMessage: 'Content audit matched a risk rule. Please adjust your input and try again.', - emailOnHit: 'Email on Hit', - emailOnHitHint: 'When enabled, send a risk-control email on every hit; auto-ban notices are always sent.', - autoBan: 'Auto Ban User', - autoBanHint: 'Disable the user, invalidate auth cache, and send a ban notice after the hit threshold is reached.', - cyberPolicyExcludeBan: 'Exclude Cyber Policy Hits from Ban Count', - cyberPolicyExcludeBanHint: 'When enabled, cyber_policy hits no longer count toward auto-ban violations: no ban judgment on the hit itself, and history rows are excluded from the rolling count. Logs and notice emails are unaffected.', - violationNotCounted: 'Not counted', - banThreshold: 'Ban Threshold', - violationWindowHours: 'Count Window (hours)', - hitRetentionDays: 'Hit Record Retention (days)', - nonHitRetentionDays: 'Non-Hit Record Retention (days, max 3)', - violationCount: '{count} hits', - emailSent: 'Email sent', - emailNotSent: 'No email', - autoBanned: 'Banned', - unbanUser: 'Unban', - unbanSuccess: 'User has been unbanned', - unbanFailed: 'Failed to unban user', - inputDetailTitle: 'Input Summary Detail', - inputDetailContent: 'Full Content', - matchedKeyword: 'Matched Keyword', - queueDelay: 'Queued {ms} ms', - allGroups: 'All Groups', - allGroupsHint: 'Auditing all groups', - selectedGroupsHint: 'Auditing selected groups', - groupScope: 'Audit Groups', - groupScopeHint: 'Switch on for all groups, or turn off to choose specific groups.', - selectedGroups: 'Selected Groups', - searchGroups: 'Search group name or platform', - noGroups: 'No groups available', - modelFilter: 'Model scope', - modelFilterHint: 'Moderate by the client-requested model name; channel model mappings do not change this match.', - modelFilterAll: 'All models', - modelFilterAllDesc: 'All model requests go through content moderation.', - modelFilterInclude: 'Only selected', - modelFilterIncludeDesc: 'Only listed models go through content moderation.', - modelFilterExclude: 'Exclude selected', - modelFilterExcludeDesc: 'Listed models skip content moderation; other models are moderated.', - modelFilterModels: 'Model list', - modelFilterModelCount: '{count} models configured', - modelFilterModelsRequired: 'This model scope requires at least 1 model', - modelFilterAllSummary: 'Applies to all models', - modelFilterIncludeSummary: 'Applies to {count} models', - modelFilterExcludeSummary: 'Excludes {count} models', - emptyLogs: 'No audit records', - preBlockSyncStatus: 'Pre-Block Sync Status', - preBlockSyncHint: 'Live counters for the synchronous moderation path, excluding async record tasks.', - preBlockActive: 'Sync Processing', - preBlockActiveHint: 'Currently checking', - preBlockChecked: 'Checked', - preBlockCheckedHint: 'Entered pre-block path', - preBlockAllowed: 'Allowed', - preBlockAllowedHint: 'No block triggered', - preBlockBlocked: 'Blocked', - preBlockBlockedHint: 'Rejected after hit', - preBlockErrors: 'Audit Errors', - preBlockErrorsHint: 'Failed or no usable key', - preBlockAvgLatency: 'Avg Latency', - preBlockAvgLatencyHint: 'Synchronous path average', - preBlockAPIKeyLoad: 'Audit Key Load', - preBlockAPIKeyLoadHint: 'Synchronous pre-block checks round-robin usable audit keys directly.', - preBlockAPIKeyLoadSummary: 'Sync active {active} / usable keys {available}, {total} total, worker: {workerActive} / {workerTotal}', - preBlockAPIKeyTotals: 'Total {total}, success {success}, errors {errors}', - preBlockAPIKeyLoadEmpty: 'No audit key load data yet', - preBlockKeyActiveShort: 'Active', - preBlockKeyTotalShort: 'Total', - preBlockKeyAvgShort: 'Avg', - preBlockKeyLastShort: 'Last', - workerStatus: 'Worker Runtime', - workerStatusHint: 'Queue and worker pool status for async audit tasks and pre-block record tasks, excluding synchronous pre-block checks.', - workerPool: 'Worker Pool', - workerPoolMeta: '{active} processing, {idle} idle and ready, {total} total', - queueUsage: 'Queue Usage', - activeWorkers: 'Processing', - idleWorkers: 'Idle Ready', - workerActive: 'Processing an async audit or record task', - workerIdle: 'Started, idle and ready', - workerDisabled: 'Risk control or content audit is disabled', - processed: 'Processed', - droppedErrors: 'Dropped / Errors', - autoRefresh: 'Auto refresh every 15s', - lastCleanup: 'Last cleanup: {time}', - cleanupStats: 'Last cleanup deleted {hit} hits and {nonHit} non-hits', - riskSwitchOff: 'System switch off', - riskThresholds: 'Risk Thresholds', - riskThresholdsHint: 'Adjust hit thresholds by OpenAI Moderations category. Scores greater than or equal to the threshold count as hits.', - riskThresholdDefault: 'Default {value}', - riskThresholdReset: 'Restore defaults', - riskThresholdPercent: 'Threshold percentage', - tabs: { - basic: 'Basic', - scope: 'Scope', - runtime: 'Runtime', - response: 'Hit Notice', - riskThresholds: 'Risk Thresholds', - keywords: 'Keyword Block', - retention: 'Retention', - }, - blockedKeywords: 'Blocked keywords', - blockedKeywordsPlaceholder: 'One keyword per line, e.g.:\nbadword1\nbadword2', - blockedKeywordsDescription: 'Matching is case-insensitive. Whether the upstream moderation API is invoked after a hit depends on the strategy below.', - blockedKeywordsPreBlockHint: 'Keyword blocking only takes effect in "Pre-block" mode.', - blockedKeywordsModeWarning: 'Current mode is "{mode}". Keyword blocking will not run until you switch to "Pre-block" mode.', - blockedKeywordCount: '{count} keywords configured', - blockedKeywordsLimit: 'Up to {max} keywords, each no longer than 200 characters. Duplicates are removed automatically.', - keywordBlockingMode: 'Moderation strategy', - keywordModeKeywordAndApi: 'Keyword + API', - keywordModeKeywordAndApiDesc: 'Block on keyword hit; otherwise fall through to the upstream moderation API.', - keywordModeKeywordOnly: 'Keyword only', - keywordModeKeywordOnlyDesc: 'Decide using keywords only; misses are allowed without calling the API, saving upstream cost.', - keywordModeKeywordOnlyNotice: 'Keyword-only strategy: requests that do not match any keyword are allowed without calling the upstream moderation API.', - keywordModeApiOnly: 'API only', - keywordModeApiOnlyDesc: 'Use the upstream moderation API only; the keyword list configured here is not consulted.', - keywordModeApiOnlyNotice: 'API-only strategy: the keyword list is not consulted; all requests go through the upstream moderation API.', - overview: { - status: 'Status', - enabled: 'Enabled', - disabled: 'Disabled', - apiKey: 'API Key', - groupScope: 'Scope', - logs: 'Audit Records', - currentFilter: 'Current filter', - }, - filters: { - search: 'Search user/key/summary', - from: 'From', - to: 'To', - allGroups: 'All Groups', - allEndpoints: 'All Endpoints', - }, - table: { - time: 'Time', - group: 'Group', - user: 'User', - apiKey: 'API Key', - endpoint: 'Endpoint', - result: 'Result', - highest: 'Highest', - actionMeta: 'Action', - latency: 'Latency', - input: 'Input Summary', - }, - result: { - all: 'All Results', - hit: 'Hit', - blocked: 'Blocked', - pass: 'Pass', - error: 'Error', - }, - action: { - block: 'Blocked', - keywordBlock: 'Keyword Blocked', - cyberPolicy: 'Cyber policy', - error: 'Error', - }, - }, - - // Channel Monitor - channelMonitor: { - title: 'Channel Monitor', - description: 'Monitor channel availability, latency and status', - searchPlaceholder: 'Search monitor name...', - allProviders: 'All Providers', - allStatus: 'All Status', - enabledFilter: 'Enabled', - onlyEnabled: 'Enabled only', - onlyDisabled: 'Disabled only', - createButton: 'Create Monitor', - createTitle: 'Create Channel Monitor', - editTitle: 'Edit Channel Monitor', - runNow: 'Run Now', - runSuccess: 'Check completed', - runFailed: 'Check failed', - apiKeyDecryptFailed: 'API Key decryption failed. Please re-edit this monitor with a fresh key.', - createSuccess: 'Monitor created', - updateSuccess: 'Monitor updated', - deleteSuccess: 'Monitor deleted', - loadError: 'Failed to load monitors', - deleteConfirm: 'Are you sure you want to delete monitor "{name}"? This action cannot be undone.', - nameRequired: 'Please enter a monitor name', - primaryModelRequired: 'Please enter a primary model', - columns: { - name: 'Name', - provider: 'Provider', - primaryModel: 'Primary Model', - availability7d: '7d Availability', - latency: 'Latency (ms)', - enabled: 'Enabled', - actions: 'Actions' - }, - form: { - name: 'Name', - namePlaceholder: 'Enter monitor name', - provider: 'Platform', - apiMode: 'OpenAI protocol', - apiModeChatCompletions: 'OpenAI Compatible', - apiModeChatCompletionsHint: 'Use /v1/chat/completions with messages; works for most compatible providers.', - apiModeResponses: 'Responses API', - apiModeResponsesHint: 'Use /v1/responses with default instructions + input; best for self-check/Codex paths.', - endpoint: 'Endpoint', - endpointPlaceholder: 'https://api.example.com', - useCurrentDomain: 'Use current service', - apiKey: 'API Key', - apiKeyPlaceholder: 'Enter API Key', - apiKeyEditPlaceholder: 'Leave blank to keep current key', - useMyKey: 'Use my key', - selectKeyTitle: 'Select my API Key', - selectKeyHint: 'Only your active, non-expired keys are listed.', - noActiveKey: 'No active API keys available', - primaryModel: 'Primary Model', - primaryModelPlaceholder: 'gpt-4o-mini', - extraModels: 'Extra Models', - extraModelsPlaceholder: 'Press Enter to add extra model', - groupName: 'Group Name', - groupNamePlaceholder: 'Optional, used to group rows in user view', - intervalSeconds: 'Interval (seconds)', - intervalSecondsHint: 'Range: 15 - 3600 seconds', - jitterSeconds: 'Random Jitter (± seconds)', - jitterSecondsHint: 'Each check fires at interval ± a random offset within this value; 0 means fixed interval. Interval minus jitter must be ≥ 15s', - enabled: 'Enable monitor', - kindRequired: 'Please select a provider' - }, - runResultTitle: 'Check Result', - noMonitorsYet: 'No monitors yet', - createFirstMonitor: 'Create your first monitor to track channel availability', - advanced: { - section: 'Advanced (optional)', - sectionHint: 'Customize request headers and body to bypass upstream client-detection (e.g. "only Claude Code clients allowed").', - headers: 'Custom request headers', - headersPlaceholder: 'User-Agent: claude-cli/1.0.83 (external, cli)\nx-app: cli\nanthropic-beta: claude-code-20250219', - headerNamePlaceholder: 'Header name', - headerValuePlaceholder: 'Value', - headerAddRow: 'Add header', - headerNameInvalid: 'Header name cannot contain whitespace or colon: {name}', - headersHint: 'Merged on top of adapter defaults (user wins). Hop-by-hop headers (Host / Content-Length / ...) are ignored.', - headersParseError: 'Cannot parse line: {line}', - bodyMode: 'Body handling', - bodyModeOff: 'Default', - bodyModeMerge: 'Merge', - bodyModeReplace: 'Replace', - bodyModeHintOff: 'Use the adapter default body (includes challenge validation).', - bodyModeHintMerge: 'Shallow-merge with the default body; user fields win but model / messages / contents are protected (use Replace to change those).', - bodyModeHintReplace: 'Use the JSON below as the complete body. Challenge validation is skipped; HTTP 2xx + non-empty response text is treated as operational.', - bodyJson: 'Body JSON', - bodyJsonFormat: 'Format', - bodyJsonHint: 'Parsed on blur. Empty means no override.', - bodyJsonError: 'JSON parse failed', - bodyJsonObjectError: 'Body must be a JSON object (no arrays or primitives)' - }, - templateField: { - label: 'Request template', - none: 'No template', - placeholder: 'Pick a template (filtered by current provider)', - applyHint: 'Picking a template copies its headers and body to this monitor (snapshot). Later template edits are not auto-synced.' - }, - template: { - manageButton: 'Templates', - managerTitle: 'Request template manager', - createButton: 'New template', - emptyState: 'No templates for this provider yet', - missingName: 'Template name is required', - createSuccess: 'Template created', - updateSuccess: 'Template updated', - deleteSuccess: 'Template deleted', - applyButton: 'Apply to monitors', - applyTooltip: 'Overwrite snapshot fields on associated monitors', - applyTitle: 'Apply template', - applyConfirm: 'Apply', - applyConfirmMessage: 'Overwrite {n} associated monitor(s) with the current configuration of "{name}"? Any local customizations on those monitors will be discarded.', - applySuccess: 'Applied to {n} monitor(s)', - applyPickerTitle: 'Apply template "{name}"', - applyPickerHint: 'Select which monitors to overwrite (all selected by default). Any local customizations will be discarded.', - applyPickerEmpty: 'No monitors are currently associated to this template', - applyPickerConfirm: 'Apply to {n} monitor(s)', - selectNone: 'Select none', - selectedCount: 'Selected {n} / {total}', - deleteConfirm: 'Delete template "{name}"? {n} associated monitor(s) will be disassociated but keep their current snapshot and continue running.', - associatedCount: '{n} associated monitor(s)', - headersSummary: '{n} custom header(s)', - form: { - name: 'Template name', - namePlaceholder: 'e.g. Claude Code mimicry', - description: 'Description', - descriptionPlaceholder: 'Optional: what this template is for, capture date, etc.' - } - } - }, - - // Subscriptions - subscriptions: { - title: 'Subscription Management', - description: 'Manage user subscriptions and quota limits', - assignSubscription: 'Assign Subscription', - adjustSubscription: 'Adjust Subscription', - revokeSubscription: 'Revoke Subscription', - restoreSubscription: 'Restore Subscription', - allStatus: 'All Status', - allGroups: 'All Groups', - allPlatforms: 'All Platforms', - daily: 'Daily', - weekly: 'Weekly', - monthly: 'Monthly', - noLimits: 'No limits configured', - unlimited: 'Unlimited', - resetNow: 'Resetting soon', - windowNotActive: 'Window not active', - resetInMinutes: 'Resets in {minutes}m', - resetInHoursMinutes: 'Resets in {hours}h {minutes}m', - resetInDaysHours: 'Resets in {days}d {hours}h', - quotaEndsInMinutes: 'Quota ends in {minutes}m', - quotaEndsInHoursMinutes: 'Quota ends in {hours}h {minutes}m', - quotaEndsInDaysHours: 'Quota ends in {days}d {hours}h', - daysRemaining: 'days remaining', - remainingDays: 'Remaining days', - noExpiration: 'No expiration', - status: { - active: 'Active', - expired: 'Expired', - revoked: 'Revoked', - suspended: 'Suspended' - }, - columns: { - user: 'User', - group: 'Group', - usage: 'Usage', - expires: 'Expires', - status: 'Status', - actions: 'Actions' - }, - form: { - user: 'User', - group: 'Subscription Group', - validityDays: 'Validity (Days)', - adjustDays: 'Adjust by (Days)' - }, - selectUser: 'Select a user', - selectGroup: 'Select a subscription group', - groupHint: 'Only groups with subscription billing type are shown', - validityHint: 'Number of days the subscription will be valid', - adjustingFor: 'Adjusting subscription for', - currentExpiration: 'Current expiration', - adjustDaysPlaceholder: 'Positive to extend, negative to shorten', - adjustHint: 'Enter positive number to extend, negative to shorten (remaining days must be > 0)', - assign: 'Assign', - assigning: 'Assigning...', - adjust: 'Adjust', - adjusting: 'Adjusting...', - revoke: 'Revoke', - restore: 'Restore', - resetQuota: 'Reset Quota', - resetQuotaTitle: 'Reset Usage Quota', - resetQuotaConfirm: "Reset the daily, weekly, and monthly usage quota for '{user}'? Usage will be zeroed and windows restarted from today.", - quotaResetSuccess: 'Quota reset successfully', - failedToResetQuota: 'Failed to reset quota', - noSubscriptionsYet: 'No subscriptions yet', - assignFirstSubscription: 'Assign a subscription to get started.', - subscriptionAssigned: 'Subscription assigned successfully', - subscriptionAdjusted: 'Subscription adjusted successfully', - subscriptionRevoked: 'Subscription revoked successfully', - subscriptionRestored: 'Subscription restored successfully', - failedToLoad: 'Failed to load subscriptions', - failedToAssign: 'Failed to assign subscription', - failedToAdjust: 'Failed to adjust subscription', - failedToRevoke: 'Failed to revoke subscription', - failedToRestore: 'Failed to restore subscription', - adjustWouldExpire: 'Remaining days after adjustment must be greater than 0', - adjustOutOfRange: 'Adjustment days must be between -36500 and 36500', - pleaseSelectUser: 'Please select a user', - pleaseSelectGroup: 'Please select a group', - validityDaysRequired: 'Please enter a valid number of days (at least 1)', - revokeConfirm: - "Are you sure you want to revoke the subscription for '{user}'? You can restore it later from the revoked list.", - restoreConfirm: - "Restore the subscription for '{user}'? If the original subscription has expired, it will be restored as expired.", - guide: { - title: 'Subscription Management Guide', - subtitle: 'Subscription mode lets you assign time-based usage quotas to users, with daily/weekly/monthly limits. Follow these steps to get started.', - showGuide: 'Usage Guide', - step1: { - title: 'Create a Subscription Group', - line1: 'Go to "Group Management" page, click "Create Group"', - line2: 'Set billing type to "Subscription", configure daily/weekly/monthly quota limits', - line3: 'Save the group and ensure its status is "Active"', - link: 'Go to Group Management' - }, - step2: { - title: 'Assign Subscription to User', - line1: 'Click the "Assign Subscription" button in the top right', - line2: 'Search for a user by email and select them', - line3: 'Choose a subscription group, set validity days, then click "Assign"' - }, - step3: { - title: 'Manage Existing Subscriptions' - }, - actions: { - adjust: 'Adjust', - adjustDesc: 'Extend or shorten the subscription validity period', - resetQuota: 'Reset Quota', - resetQuotaDesc: 'Reset daily/weekly/monthly usage to zero', - revoke: 'Revoke', - revokeDesc: 'Immediately terminate the subscription (restorable from the revoked list)' - }, - tip: 'Tip: Only groups with billing type "Subscription" and status "Active" appear in the group dropdown. If no options are available, create one in Group Management first.' - } - }, - - // Accounts - accounts: { - title: 'Account Management', - description: 'Manage AI platform accounts and credentials', - createAccount: 'Create Account', - autoRefresh: 'Auto Refresh', - enableAutoRefresh: 'Enable auto refresh', - refreshInterval5s: '5 seconds', - refreshInterval10s: '10 seconds', - refreshInterval15s: '15 seconds', - refreshInterval30s: '30 seconds', - autoRefreshCountdown: 'Auto refresh: {seconds}s', - listPendingSyncHint: 'List changes are pending sync. Click sync to load latest rows.', - listPendingSyncAction: 'Sync now', - syncFromCrs: 'Sync from CRS', - dataExport: 'Export', - dataExportSelected: 'Export Selected', - dataExportIncludeProxies: 'Include proxies linked to the exported accounts', - dataImport: 'Import', - moreActions: 'More Actions', - dataActions: 'Data', - toolActions: 'Tools', - viewColumns: 'Columns', - selectedCount: '{count} selected', - dataExportConfirmMessage: 'The exported data contains sensitive account and proxy information. Store it securely.', - dataExportConfirm: 'Confirm Export', - dataExported: 'Data exported successfully', - dataExportedSkippedShadows: 'Data exported. Skipped {count} spark shadow account(s): their scheduling config is not included in the backup; recreate and re-tune them after restore.', - dataExportFailed: 'Failed to export data', - dataImportTitle: 'Import Data', - dataImportHint: 'Upload the exported JSON file to import accounts and proxies.', - dataImportWarning: 'Import will create new accounts/proxies; groups must be bound manually. Ensure existing data does not conflict.', - dataImportFile: 'Data file', - dataImportButton: 'Start Import', - dataImporting: 'Importing...', - dataImportSelectFile: 'Please select a data file', - dataImportParseFailed: 'Failed to parse data file', - dataImportParseFailedFile: 'Failed to parse {name}', - dataImportInvalidFile: '{name} is not a supported data export file', - dataImportIgnoredFiles: 'Ignored {count} non-JSON file(s)', - dataImportFailed: 'Data import failed', - dataImportResult: 'Import Result', - dataImportResultSummary: 'Proxies created {proxy_created}, reused {proxy_reused}, failed {proxy_failed}; Accounts created {account_created}, failed {account_failed}', - dataImportErrors: 'Error Details', - dataImportSuccess: 'Import completed: accounts {account_created}, failed {account_failed}', - dataImportCompletedWithErrors: 'Import completed with errors: account failed {account_failed}, proxy failed {proxy_failed}', - syncFromCrsTitle: 'Sync Accounts from CRS', - syncFromCrsDesc: - 'Sync accounts from claude-relay-service (CRS) into this system (CRS is called server-to-server).', - crsVersionRequirement: '⚠️ Note: CRS version must be ≥ v1.1.240 to support this feature', - crsBaseUrl: 'CRS Base URL', - crsBaseUrlPlaceholder: 'e.g. http://127.0.0.1:3000', - crsUsername: 'Username', - crsPassword: 'Password', - syncProxies: 'Also sync proxies (match by host/port/auth or create)', - syncNow: 'Sync Now', - syncing: 'Syncing...', - syncMissingFields: 'Please fill base URL, username and password', - syncResult: 'Sync Result', - syncResultSummary: 'Created {created}, updated {updated}, skipped {skipped}, failed {failed}', - syncErrors: 'Errors / Skipped Details', - syncCompleted: 'Sync completed: created {created}, updated {updated}, skipped {skipped}', - syncCompletedWithErrors: - 'Sync completed with errors: failed {failed} (created {created}, updated {updated}, skipped {skipped})', - syncFailed: 'Sync failed', - crsPreview: 'Preview', - crsPreviewing: 'Previewing...', - crsPreviewFailed: 'Preview failed', - crsExistingAccounts: 'Existing accounts (will be updated)', - crsNewAccounts: 'New accounts (select to sync)', - crsSelectAll: 'Select all', - crsSelectNone: 'Select none', - crsNoNewAccounts: 'All CRS accounts are already synced.', - crsWillUpdate: 'Will update {count} existing accounts.', - crsSelectedCount: '{count} new accounts selected', - crsUpdateBehaviorNote: - 'Existing accounts only sync fields returned by CRS; missing fields keep their current values. Credentials are merged by key — keys not returned by CRS are preserved. Proxies are kept when "Sync proxies" is unchecked.', - crsBack: 'Back', - editAccount: 'Edit Account', - deleteAccount: 'Delete Account', - searchAccounts: 'Search accounts...', - notes: 'Notes', - notesPlaceholder: 'Enter notes', - notesHint: 'Notes are optional', - allPlatforms: 'All Platforms', - allTypes: 'All Types', - allStatus: 'All Status', - allGroups: 'All Groups', - ungroupedGroup: 'Ungrouped', - oauthType: 'OAuth', - setupToken: 'Setup Token', - apiKey: 'API Key', - // Schedulable toggle - schedulable: 'Schedulable', - schedulableHint: 'Enable to include this account in API request scheduling', - schedulableEnabled: 'Scheduling enabled', - schedulableDisabled: 'Scheduling disabled', - failedToToggleSchedulable: 'Failed to toggle scheduling status', - groupCountTotal: '{count} groups total', - platforms: { - anthropic: 'Anthropic', - claude: 'Claude', - openai: 'OpenAI', - gemini: 'Gemini', - antigravity: 'Antigravity', - grok: 'Grok', - }, - types: { - oauth: 'OAuth', - chatgptOauth: 'ChatGPT OAuth', - responsesApi: 'Responses API', - googleOauth: 'Google OAuth', - codeAssist: 'Code Assist', - antigravityOauth: 'Antigravity OAuth', - grokOauth: 'Grok OAuth', - antigravityApikey: 'Connect via Base URL + API Key', - upstream: 'Upstream', - upstreamDesc: 'Connect via Base URL + API Key' - }, - antigravityProjectIdLabel: 'GCP Project ID (optional)', - antigravityProjectIdPlaceholder: 'your-gcp-project-id', - antigravityProjectIdHint: - 'Antigravity standard-tier accounts that do not receive an automatic project_id need a user-owned GCP project.', - status: { - active: 'Active', - inactive: 'Inactive', - error: 'Error', - cooldown: 'Cooldown', - paused: 'Paused', - limited: 'Limited', - rateLimited: 'Rate Limited', - overloaded: 'Overloaded', - tempUnschedulable: 'Temp Unschedulable', - quotaExceeded: 'Quota Exceeded', - unschedulable: 'Unschedulable', - rateLimitedUntil: 'Rate limited and removed from scheduling. Auto resumes at {time}', - rateLimitedAutoResume: 'Auto resumes in {time}', - modelRateLimitedUntil: '{model} rate limited until {time}', - modelCreditOveragesUntil: '{model} using AI Credits until {time}', - creditsExhausted: 'Credits Exhausted', - creditsExhaustedUntil: 'AI Credits exhausted, expected recovery at {time}', - overloadedUntil: 'Overloaded until {time}', - viewTempUnschedDetails: 'View temp unschedulable details' - }, - columns: { - name: 'Name', - id: 'Account ID', - platformType: 'Platform/Type', - platform: 'Platform', - type: 'Type', - capacity: 'Capacity', - notes: 'Notes', - priority: 'Priority', - billingRateMultiplier: 'Billing Rate', - weight: 'Weight', - schedulerScore: 'Scheduler Score', - status: 'Status', - schedulable: 'Schedulable', - todayStats: 'Today Stats', - groups: 'Groups', - usageWindows: 'Usage Windows', - proxy: 'Proxy', - lastUsed: 'Last Used', - createdAt: 'Created', - expiresAt: 'Expires At', - actions: 'Actions' - }, - schedulerScore: { - baseShort: 'Base', - stickyShort: 'Sticky', - ungrouped: 'Ungrouped', - hint: 'Displayed as "group / base score / sticky bonus". The base score is computed within the current filtered candidate set and includes priority, load, queue depth, error rate, first-token latency, reset window, quota headroom, and related factors. The sticky bonus applies only when sticky weighting is enabled for previous_response_id or session_hash. Higher scores are preferred.' - }, - usageWindowsHint: '"5h / 7d" are the upstream account\'s official rolling usage windows (e.g. OpenAI ChatGPT, Claude). They are imposed by the upstream provider on the account itself — not configured by sub2api, and unrelated to the models you map. Usage resets automatically once each window rolls over, and the limit cannot be lifted from within sub2api.', - allPrivacyModes: 'All Privacy States', - privacyUnset: 'Unset', - privacyTrainingOff: 'Training data sharing disabled', - privacyCfBlocked: 'Blocked by Cloudflare, training may still be on', - privacyFailed: 'Failed to disable training', - privacyAntigravitySet: 'Telemetry and marketing emails disabled', - privacyAntigravityFailed: 'Privacy setting failed', - setPrivacy: 'Set Privacy', - subscriptionAbnormal: 'Abnormal', - subscriptionExpires: 'Expires', - // Capacity status tooltips - capacity: { - windowCost: { - blocked: '5h window cost exceeded, account scheduling paused', - stickyOnly: '5h window cost at threshold, only sticky sessions allowed', - normal: '5h window cost normal' - }, - sessions: { - full: 'Active sessions full, new sessions must wait (idle timeout: {idle} min)', - normal: 'Active sessions normal (idle timeout: {idle} min)' - }, - rpm: { - full: 'RPM limit reached', - warning: 'RPM approaching limit', - normal: 'RPM normal', - tieredNormal: 'RPM limit (Tiered) - Normal', - tieredWarning: 'RPM limit (Tiered) - Approaching limit', - tieredStickyOnly: 'RPM limit (Tiered) - Sticky only | Buffer: {buffer}', - tieredBlocked: 'RPM limit (Tiered) - Blocked | Buffer: {buffer}', - stickyExemptNormal: 'RPM limit (Sticky Exempt) - Normal', - stickyExemptWarning: 'RPM limit (Sticky Exempt) - Approaching limit', - stickyExemptOver: 'RPM limit (Sticky Exempt) - Over limit, sticky only' - }, - quota: { - exceeded: 'Quota exceeded, account paused', - normal: 'Quota normal' - }, - }, - tempUnschedulable: { - title: 'Temp Unschedulable', - statusTitle: 'Temp Unschedulable Status', - hint: 'Disable accounts temporarily when error code and keyword both match.', - notice: 'Rules are evaluated in order and require both error code and keyword match.', - addRule: 'Add Rule', - ruleOrder: 'Rule Order', - ruleIndex: 'Rule #{index}', - errorCode: 'Error Code', - errorCodePlaceholder: 'e.g. 429', - durationMinutes: 'Duration (minutes)', - durationPlaceholder: 'e.g. 30', - keywords: 'Keywords', - keywordsPlaceholder: 'e.g. overloaded, too many requests', - keywordsHint: 'Separate keywords with commas; any keyword match will trigger.', - description: 'Description', - descriptionPlaceholder: 'Optional note for this rule', - rulesInvalid: 'Add at least one rule with error code, keywords, and duration.', - viewDetails: 'View temp unschedulable details', - accountName: 'Account', - triggeredAt: 'Triggered At', - until: 'Until', - remaining: 'Remaining', - matchedKeyword: 'Matched Keyword', - errorMessage: 'Error Details', - reset: 'Recover State', - resetSuccess: 'Account state recovered successfully', - resetFailed: 'Failed to recover account state', - failedToLoad: 'Failed to load temp unschedulable status', - notActive: 'This account is not temporarily unschedulable.', - expired: 'Expired', - remainingMinutes: 'About {minutes} minutes', - remainingHours: 'About {hours} hours', - remainingHoursMinutes: 'About {hours} hours {minutes} minutes', - presets: { - overloadLabel: '529 Overloaded', - overloadDesc: 'Overloaded - pause 60 minutes', - rateLimitLabel: '429 Rate Limit', - rateLimitDesc: 'Rate limited - pause 10 minutes', - unavailableLabel: '503 Unavailable', - unavailableDesc: 'Unavailable - pause 30 minutes' - } - }, - clearRateLimit: 'Clear Rate Limit', - resetQuota: 'Reset Quota', - quotaLimit: 'Quota Limit', - quotaLimitPlaceholder: '0 means unlimited', - quotaLimitHint: 'Set daily/weekly/total spending limits (USD). Anthropic API key accounts can also configure client affinity. Changing limits won\'t reset usage.', - quotaLimitToggle: 'Enable Quota Limit', - quotaLimitToggleHint: 'When enabled, account will be paused when usage reaches the set limit', - quotaDailyLimit: 'Daily Limit', - quotaDailyLimitHint: 'Automatically resets every 24 hours from first usage.', - quotaWeeklyLimit: 'Weekly Limit', - quotaWeeklyLimitHint: 'Automatically resets every 7 days from first usage.', - quotaTotalLimit: 'Total Limit', - quotaTotalLimitHint: 'Cumulative spending limit. Does not auto-reset — use "Reset Quota" to clear.', - quotaResetMode: 'Reset Mode', - quotaResetModeRolling: 'Rolling Window', - quotaResetModeFixed: 'Fixed Time', - quotaResetHour: 'Reset Hour', - quotaWeeklyResetDay: 'Reset Day', - quotaResetTimezone: 'Reset Timezone', - quotaDailyLimitHintFixed: 'Resets daily at {hour}:00 ({timezone}).', - quotaWeeklyLimitHintFixed: 'Resets every {day} at {hour}:00 ({timezone}).', - dayOfWeek: { - monday: 'Monday', - tuesday: 'Tuesday', - wednesday: 'Wednesday', - thursday: 'Thursday', - friday: 'Friday', - saturday: 'Saturday', - sunday: 'Sunday', - }, - quotaLimitAmount: 'Total Limit', - quotaLimitAmountHint: 'Cumulative spending limit. Does not auto-reset.', - quotaNotify: { - alert: 'Alert', - enabled: 'Enable Alert', - threshold: 'Alert Amount', - thresholdPlaceholder: 'Enter percentage', - }, - testConnection: 'Test Connection', - reAuthorize: 'Re-Authorize', - refreshToken: 'Refresh Token', - noAccountsYet: 'No accounts yet', - createFirstAccount: 'Create your first account to start using AI services.', - tokenRefreshed: 'Token refreshed successfully', - accountDeleted: 'Account deleted successfully', - rateLimitCleared: 'Rate limit cleared successfully', - bulkSchedulableEnabled: 'Successfully enabled scheduling for {count} account(s)', - bulkSchedulableDisabled: 'Successfully disabled scheduling for {count} account(s)', - bulkSchedulablePartial: 'Scheduling updated partially: {success} succeeded, {failed} failed', - bulkSchedulableResultUnknown: 'Bulk scheduling result incomplete. Please retry or refresh.', - bulkActions: { - selected: '{count} account(s) selected', - selectCurrentPage: 'Select this page', - clear: 'Clear selection', - edit: 'Bulk Edit', - delete: 'Bulk Delete', - enableScheduling: 'Enable Scheduling', - disableScheduling: 'Disable Scheduling', - resetStatus: 'Reset Status', - refreshToken: 'Refresh Token', - resetStatusSuccess: 'Successfully reset {count} account(s) status', - refreshTokenSuccess: 'Successfully refreshed {count} account(s) token', - partialSuccess: 'Partially completed: {success} succeeded, {failed} failed' - }, - bulkEdit: { - title: 'Bulk Edit Accounts', - selectionInfo: - '{count} account(s) selected. Only checked or filled fields will be updated; others stay unchanged.', - baseUrlPlaceholder: 'https://api.anthropic.com or https://api.openai.com', - baseUrlNotice: 'Applies to API Key accounts only; leave empty to keep existing value', - submit: 'Update Accounts', - updating: 'Updating...', - success: 'Updated {count} account(s)', - partialSuccess: 'Partially updated: {success} succeeded, {failed} failed', - failed: 'Bulk update failed', - noSelection: 'Please select accounts to edit', - noFieldsSelected: 'Select at least one field to update', - mixedPlatformWarning: 'Selected accounts span multiple platforms ({platforms}). Model mapping presets shown are combined — ensure mappings are appropriate for each platform.' - }, - bulkDeleteTitle: 'Bulk Delete Accounts', - bulkDeleteConfirm: 'Delete the selected {count} account(s)? This action cannot be undone.', - bulkDeleteSuccess: 'Deleted {count} account(s)', - bulkDeletePartial: 'Partially deleted: {success} succeeded, {failed} failed', - bulkDeleteFailed: 'Bulk delete failed', - recoverState: 'Recover State', - recoverStateHint: 'Used to recover error, rate-limit, and temporary unschedulable runtime state.', - recoverStateSuccess: 'Account state recovered successfully', - recoverStateFailed: 'Failed to recover account state', - fallbackActive: 'Fallback', - fallbackActiveTip: 'Origin proxy {origin} expired', - revertProxy: 'Revert proxy', - revertProxySuccess: 'Successfully reverted to original proxy', - revertProxyFailed: 'Failed to revert proxy', - createSparkShadow: 'Create Spark Shadow', - createSparkShadowConfirm: 'Create a spark shadow account linked to "{name}"? It shares the parent\'s credentials and serves only spark models.', - createSparkShadowSuccess: 'Spark shadow account created', - createSparkShadowFailed: 'Failed to create spark shadow account', - resetStatus: 'Reset Status', - statusReset: 'Account status reset successfully', - failedToResetStatus: 'Failed to reset account status', - failedToLoad: 'Failed to load accounts', - failedToRefresh: 'Failed to refresh token', - failedToDelete: 'Failed to delete account', - failedToClearRateLimit: 'Failed to clear rate limit', - deleteConfirm: "Are you sure you want to delete '{name}'? This action cannot be undone.", - // Create/Edit Account Modal - platform: 'Platform', - accountName: 'Account Name', - enterAccountName: 'Enter account name', - accountType: 'Account Type', - claudeCode: 'Claude Code', - claudeConsole: 'Claude Console', - bedrockLabel: 'AWS Bedrock', - bedrockDesc: 'SigV4 / API Key', - vertexLabel: 'Vertex', - vertexDesc: 'Service Account', - vertexAnthropicHint: 'Use a Google Cloud Service Account JSON to call Anthropic Claude via Vertex AI. It is recommended to configure model mapping to map client Claude model names to Vertex model IDs.', - vertexGeminiHint: 'Use a Google Cloud Service Account JSON to access Vertex AI Gemini. It is recommended to place Vertex accounts in a separate group to avoid mixing with AI Studio/Gemini OAuth on the same models.', - vertexSaJsonLabel: 'Service Account JSON', - vertexSaJsonLoaded: 'Service Account JSON loaded', - vertexSaJsonDrop: 'Drop Service Account JSON here', - vertexSaJsonKeyHidden: 'Key content is not displayed in the form.', - vertexSaJsonDropHint: 'Drag a .json file here, or click the button to select one.', - vertexSaJsonSelectBtn: 'Select JSON', - vertexSaJsonUploadHint: 'After uploading or dropping a JSON file, the project_id will be auto-extracted. Key content is only used for account creation.', - vertexSaJsonEditHint: 'Service Account JSON is not shown on the edit page; to change the JSON, delete the account and recreate it.', - vertexProjectIdPlaceholder: 'Auto-extracted from JSON', - vertexLocationHint: 'Available locations vary by Vertex model. Select the default endpoint location for this account.', - vertexLocationRequired: 'Please enter a Vertex location', - vertexSaJsonMissingFields: 'Service Account JSON is missing project_id, client_email, or private_key', - vertexSaJsonMissingProjectId: 'Service Account JSON is missing project_id', - vertexSaJsonMissingClientEmail: 'Service Account JSON is missing client_email', - vertexSaJsonInvalid: 'Service Account JSON format is invalid', - vertexSaJsonRequired: 'Please upload a Service Account JSON', - oauthSetupToken: 'OAuth / Setup Token', - addMethod: 'Add Method', - setupTokenLongLived: 'Setup Token (Long-lived)', - baseUrl: 'Base URL', - baseUrlHint: 'Leave default for official Anthropic API', - apiKeyRequired: 'API Key *', - apiKeyPlaceholder: 'sk-ant-api03-...', - apiKeyHint: 'Your Claude Console API Key', - // OpenAI specific hints - openai: { - baseUrlHint: 'Leave default for official OpenAI API', - apiKeyHint: 'Your OpenAI API Key', - oauthPassthrough: 'Auto passthrough (auth only)', - oauthPassthroughDesc: - 'When enabled, this OpenAI account uses automatic passthrough: the gateway forwards request/response as-is and only swaps auth, while keeping billing/concurrency/audit and necessary safety filtering.', - responsesWebsocketsV2: 'Responses WebSocket v2', - responsesWebsocketsV2Desc: - 'Disabled by default. Enable to allow responses_websockets_v2 capability (still gated by global and account-type switches).', - wsMode: 'WS mode', - wsModeDesc: 'Only applies to the current OpenAI account type.', - wsModeOff: 'Off (off)', - wsModeCtxPool: 'Context Pool (ctx_pool)', - wsModePassthrough: 'Passthrough (passthrough)', - wsModeHttpBridge: 'HTTP Bridge (http_bridge)', - wsModeShared: 'Shared (shared)', - wsModeDedicated: 'Dedicated (dedicated)', - wsModeConcurrencyHint: - 'When WS mode is enabled, account concurrency becomes the WS connection pool limit for this account.', - wsModePassthroughHint: 'Passthrough mode does not use the WS connection pool.', - oauthResponsesWebsocketsV2: 'OAuth WebSocket Mode', - oauthResponsesWebsocketsV2Desc: - 'Only applies to OpenAI OAuth. This account can use OpenAI WebSocket Mode only when enabled.', - apiKeyResponsesWebsocketsV2: 'API Key WebSocket Mode', - apiKeyResponsesWebsocketsV2Desc: - 'Only applies to OpenAI API Key. This account can use OpenAI WebSocket Mode only when enabled.', - responsesWebsocketsV2PassthroughHint: - 'Automatic passthrough is currently enabled: it only affects HTTP passthrough and does not disable WS mode.', - responsesMode: 'Responses API support', - responsesModeDesc: - 'Only applies to the OpenAI API Key text forwarding path. Auto follows probe results; force modes override probing.', - responsesModeAuto: 'Auto', - responsesModeForceResponses: 'Force Responses', - responsesModeForceChatCompletions: 'Force Chat Completions', - responsesModeTextDisabledHint: - 'Not applicable when the Responses / Chat Completions endpoint is not enabled.', - endpointCapabilities: 'Endpoint capabilities', - endpointCapabilitiesDesc: - 'Used by account routing. The text endpoint follows the Responses API support setting above and is shown as Responses, Chat Completions, or auto mode; Embeddings independently controls /v1/embeddings.', - capabilityResponses: 'Responses', - capabilityTextAuto: 'Responses / Chat Completions (Auto)', - capabilityResponsesAuto: 'Responses (auto probe)', - capabilityChatCompletions: 'Chat Completions', - capabilityChatCompletionsAuto: 'Chat Completions (auto probe)', - capabilityEmbeddings: 'Embeddings', - responsesStatusAutoSupported: 'Auto probe: Responses', - responsesStatusAutoUnsupported: 'Auto probe: Chat Completions', - responsesStatusAutoUnknown: 'Auto probe: unknown', - responsesStatusForcedResponses: 'Forced Responses', - responsesStatusForcedChatCompletions: 'Forced Chat Completions', - codexCLIOnly: 'Codex official clients only', - codexCLIOnlyDesc: - 'Only applies to OpenAI OAuth. When enabled, only Codex official client families are allowed; when disabled, the gateway bypasses this restriction and keeps existing behavior.', - codexCLIOnlyAppServer: 'Allow Codex app-server clients', - codexCLIOnlyAppServerDesc: - "Effective only when the switch above is on. When enabled, this account also allows third-party clients that embed the Codex engine over the app-server protocol (e.g. Claude Code's codex plugin); they still pass the global engine-fingerprint gate. OR-combined with the global app-server toggle.", - codexImageTool: 'Codex image tool', - codexImageToolDesc: - 'One policy for the image_generation tool on Codex /responses text requests: whether it is auto-injected, and whether client-provided tools pass through. Account policy takes precedence over channel and global settings; standalone image-generation endpoints are unaffected.', - codexImageToolInherit: 'Follow channel', - codexImageToolInheritDesc: 'No account override; injection follows the channel or global policy, and client-provided image tools pass through.', - codexImageToolEnabled: 'Force inject', - codexImageToolEnabledDesc: 'Always inject the image tool for Codex /responses requests.', - codexImageToolDisabled: 'No injection', - codexImageToolDisabledDesc: 'Never auto-inject; client-provided image tools still pass through.', - codexImageToolBlock: 'Block all', - codexImageToolBlockDesc: 'No injection, and client-provided image tools plus matching tool_choice are removed.', - codexImageToolBadgeInherit: 'Channel policy', - codexImageToolBadgeEnabled: 'Force inject', - codexImageToolBadgeDisabled: 'No injection', - codexImageToolBadgeBlock: 'Blocked', - compactMode: 'Compact mode', - compactModeDesc: - 'Controls how this account participates in /responses/compact routing. Auto follows probe results, Force On always allows, Force Off always excludes.', - compactModeAuto: 'Auto', - compactModeForceOn: 'Force On', - compactModeForceOff: 'Force Off', - compactModelMapping: 'Compact-only model mapping', - compactModelMappingDesc: - 'Only applies to /responses/compact. Use this when the upstream compact endpoint requires a special compact model.', - compactSupported: 'Compact supported', - compactUnsupported: 'Compact unsupported', - compactAuto: 'Compact Auto', - compactUnknown: 'Compact Auto', - compactLastChecked: 'Last compact probe', - testMode: 'Test mode', - testModeDefault: 'Default request', - testModeCompact: 'Compact probe', - modelRestrictionDisabledByPassthrough: 'Automatic passthrough is enabled: model whitelist/mapping will not take effect.', - }, - grok: { - baseUrlHint: 'Grok OAuth accounts forward to the official xAI API base URL.', - apiKeyHint: 'Grok subscription support uses OAuth refresh tokens; API keys are out of scope for this account type.' - }, - anthropic: { - apiKeyPassthrough: 'Auto passthrough (auth only)', - apiKeyPassthroughDesc: - 'Only applies to Anthropic API Key accounts. When enabled, messages/count_tokens are forwarded in passthrough mode with auth replacement only, while billing/concurrency/audit and safety filtering are preserved. Disable to roll back immediately.', - apiKeyAuthScheme: 'Upstream auth scheme', - apiKeyAuthSchemeDesc: 'Choose the API key auth header used when forwarding to an Anthropic-compatible upstream. Ollama Cloud uses Authorization: Bearer.', - apiKeyAuthSchemeXApiKey: 'x-api-key', - apiKeyAuthSchemeBearer: 'Authorization: Bearer', - webSearchEmulation: 'Web Search Emulation', - webSearchEmulationDesc: - 'Enable web search emulation for this API Key account. When a pure web_search request is detected, the gateway calls a third-party search API and constructs the response locally. Default follows channel config.', - webSearchDefault: 'Default', - webSearchEnabled: 'Enabled', - webSearchDisabled: 'Disabled', - }, - modelRestriction: 'Model Restriction (Optional)', - modelWhitelist: 'Model Whitelist', - modelMapping: 'Model Mapping', - selectAllowedModels: 'Select allowed models. Leave empty to support all models.', - mapRequestModels: - 'Map request models to actual models. Left is the requested model, right is the actual model sent to API.', - selectedModels: 'Selected {count} model(s)', - supportsAllModels: '(supports all models)', - requestModel: 'Request model', - actualModel: 'Actual model', - addMapping: 'Add Mapping', - mappingExists: 'Mapping for {model} already exists', - wildcardOnlyAtEnd: 'Wildcard * can only be at the end', - targetNoWildcard: 'Target model cannot contain wildcard *', - searchModels: 'Search models...', - noMatchingModels: 'No matching models', - fillRelatedModels: 'Sync latest supported models', - syncUpstreamModels: 'Sync upstream supported models', - syncUpstreamModelsLoading: 'Syncing upstream...', - syncUpstreamModelsSuccess: 'Synced {count} new model(s) from upstream ({total} upstream total)', - syncUpstreamModelsNoChanges: 'All {count} upstream model(s) are already in the whitelist', - syncUpstreamModelsEmpty: 'Upstream returned no models to sync', - syncUpstreamModelsFailed: 'Failed to sync upstream models', - syncUpstreamModelsError: 'Failed to sync upstream models: {message}', - clearAllModels: 'Clear all models', - customModelName: 'Custom model name', - enterCustomModelName: 'Enter custom model name', - addModel: 'Add', - modelExists: 'Model already exists', - modelCount: '{count} models', - poolMode: 'Pool Mode', - poolModeHint: 'Enable when upstream is an account pool; errors won\'t mark local account status', - poolModeInfo: - 'When enabled, upstream 429/403/401 errors will auto-retry without marking the account as rate-limited or errored. Suitable for upstream pointing to another sub2api instance.', - poolModeRetryCount: 'Same-Account Retries', - poolModeRetryCountHint: - 'Only applies in pool mode. Use 0 to disable in-place retry. Default {default}, maximum {max}.', - poolModeRetryStatusCodes: 'Retry Status Codes', - poolModeRetryStatusCodesHint: - 'Comma-separated HTTP status codes (100-599) that trigger same-account retry in pool mode. Leave blank to use defaults ({default}).', - customErrorCodes: 'Custom Error Codes', - customErrorCodesHint: 'Only stop scheduling for selected error codes', - customErrorCodesWarning: - 'Only selected error codes will stop scheduling. Other errors will return 500.', - customErrorCodes429Warning: - '429 already has built-in rate limit handling. Adding it to custom error codes will disable the account instead of temporary rate limiting. Are you sure?', - customErrorCodes529Warning: - '529 already has built-in overload handling. Adding it to custom error codes will disable the account instead of temporary overload marking. Are you sure?', - selectedErrorCodes: 'Selected', - noneSelectedUsesDefault: 'None selected (uses default policy)', - enterErrorCode: 'Enter error code (100-599)', - invalidErrorCode: 'Please enter a valid HTTP error code (100-599)', - errorCodeExists: 'This error code is already selected', - interceptWarmupRequests: 'Intercept Warmup Requests', - interceptWarmupRequestsDesc: - 'When enabled, warmup requests like title generation will return mock responses without consuming upstream tokens', - headerOverride: { - title: 'Header Override', - hint: 'Override same-named request headers on forwarding (case-insensitive)', - info: 'Applies to outbound requests of this account only: configured headers override client/gateway-generated headers of the same name before forwarding. Auth headers (authorization, x-api-key) and connection-control headers cannot be overridden.', - namePlaceholder: 'Header name (e.g. user-agent)', - valuePlaceholder: 'Override value (leave empty to skip)', - addRow: 'Add Header', - fillTemplate: 'Fill Template', - emptyValueHint: 'Rows with an empty value are placeholders and do not override anything.', - bulkDisableHint: 'Saving will disable header override and clear existing configuration on the selected accounts.', - bulkReplaceHint: 'Saving will replace the existing header override configuration on all selected accounts with the rows below.', - bulkEmptyRows: 'Add at least one header row before saving, or turn the toggle off to clear existing configuration.', - invalidName: 'Invalid header name (only letters, digits and !#$%&\'*+-.^_`|~ are allowed)', - blockedName: 'This header cannot be overridden (auth and connection-control headers are managed by the system)', - duplicateName: 'Duplicate header name (matching is case-insensitive)', - invalidValue: 'Invalid header value (control characters are not allowed; max length 8192)', - tooManyEntries: 'Too many header override entries (max 64)' - }, - autoPauseOnExpired: 'Auto Pause On Expired', - autoPauseOnExpiredDesc: 'When enabled, the account will auto pause scheduling after it expires', - autoPause5hThreshold: '5h Usage Threshold (%)', - autoPause7dThreshold: '7d Usage Threshold (%)', - autoPauseThresholdHint: 'Leave empty or set 0 to use the global default threshold (configured in Ops settings); set a value to override the global default. Reaching the threshold only skips the account during scheduling and does not modify schedulable.', - autoPause5hDisabled: 'Disable 5h auto-pause', - autoPause7dDisabled: 'Disable 7d auto-pause', - autoPauseDisabledHint: 'When enabled, this account is never auto-paused (even if a global default threshold is configured).', - // Quota control (Anthropic OAuth/SetupToken only) - quotaControl: { - title: 'Quota Control', - hint: 'Configure cost window, session limits, client affinity and other scheduling controls.', - windowCost: { - label: '5h Window Cost Limit', - hint: 'Limit account cost usage within the 5-hour window', - limit: 'Cost Threshold', - limitPlaceholder: '50', - limitHint: 'Account will not participate in new scheduling after reaching threshold', - stickyReserve: 'Sticky Reserve', - stickyReservePlaceholder: '10', - stickyReserveHint: 'Additional reserve for sticky sessions' - }, - sessionLimit: { - label: 'Session Count Limit', - hint: 'Limit the number of active concurrent sessions', - maxSessions: 'Max Sessions', - maxSessionsPlaceholder: '3', - maxSessionsHint: 'Maximum number of active concurrent sessions', - idleTimeout: 'Idle Timeout', - idleTimeoutPlaceholder: '5', - idleTimeoutHint: 'Sessions will be released after idle timeout' - }, - rpmLimit: { - label: 'RPM Limit', - hint: 'Limit requests per minute to protect upstream accounts', - baseRpm: 'Base RPM', - baseRpmPlaceholder: '15', - baseRpmHint: 'Max requests per minute, 0 or empty means no limit', - strategy: 'RPM Strategy', - strategyTiered: 'Tiered Model', - strategyStickyExempt: 'Sticky Exempt', - strategyTieredHint: 'Green → Yellow → Sticky only → Blocked, progressive throttling', - strategyStickyExemptHint: 'Only sticky sessions allowed when over limit', - strategyHint: 'Tiered: gradually restrict when exceeded; Sticky Exempt: existing sessions unrestricted', - stickyBuffer: 'Sticky Buffer', - stickyBufferPlaceholder: 'Default: 20% of base RPM', - stickyBufferHint: 'Extra requests allowed for sticky sessions after exceeding base RPM. Leave empty to use default (20% of base RPM, min 1)', - userMsgQueue: 'User Message Rate Control', - userMsgQueueHint: 'Rate-limit user messages to avoid triggering upstream RPM limits', - umqModeOff: 'Off', - umqModeThrottle: 'Throttle', - umqModeSerialize: 'Serialize', - }, - tlsFingerprint: { - label: 'TLS Fingerprint Simulation', - hint: 'Simulate Node.js/Claude Code client TLS fingerprint', - defaultProfile: 'Built-in Default', - randomProfile: 'Random' - }, - sessionIdMasking: { - label: 'Session ID Masking', - hint: 'When enabled, fixes the session ID in metadata.user_id for 15 minutes, making upstream think requests come from the same session' - }, - cacheTTLOverride: { - label: 'Cache TTL Override', - hint: 'Force all cache creation tokens to be billed as the selected TTL tier (5m or 1h)', - target: 'Target TTL', - targetHint: 'Select the TTL tier for billing' - }, - customBaseUrl: { - label: 'Custom Relay URL', - hint: 'Forward requests to a custom relay service. Proxy URL will be passed as a query parameter.', - urlHint: 'Relay service URL (e.g., https://relay.example.com)', - }, - clientAffinity: { - label: 'Client Affinity Scheduling', - hint: 'When enabled, new sessions prefer accounts previously used by this client to reduce account switching' - } - }, - affinityNoClients: 'No affinity clients', - affinityClients: '{count} affinity clients:', - affinitySection: 'Client Affinity', - affinitySectionHint: 'Control how clients are distributed across accounts. Configure zone thresholds to balance load.', - affinityToggle: 'Enable Client Affinity', - affinityToggleHint: 'New sessions prefer accounts previously used by this client', - affinityBase: 'Base Limit (Green Zone)', - affinityBasePlaceholder: 'Empty = no limit', - affinityBaseHint: 'Max clients in green zone (full priority scheduling)', - affinityBaseOffHint: 'No green zone limit. All clients receive full priority scheduling.', - affinityBuffer: 'Buffer (Yellow Zone)', - affinityBufferPlaceholder: 'e.g. 3', - affinityBufferHint: 'Additional clients allowed in the yellow zone (degraded priority)', - affinityBufferInfinite: 'Unlimited', - expired: 'Expired', - proxy: 'Proxy', - noProxy: 'No Proxy', - concurrency: 'Concurrency', - loadFactor: 'Load Factor', - loadFactorHint: 'Higher load factor increases scheduling frequency', - priority: 'Priority', - priorityHint: 'Lower value accounts are used first', - billingRateMultiplier: 'Billing Rate Multiplier', - billingRateMultiplierHint: '0 = free, affects account billing only', - expiresAt: 'Expires At', - expiresAtHint: 'Leave empty for no expiration', - higherPriorityFirst: 'Lower value means higher priority', - mixedScheduling: 'Use in /v1/messages', - mixedSchedulingHint: 'Enable to participate in Anthropic/Gemini group scheduling', - mixedSchedulingTooltip: - '!! WARNING !! Antigravity Claude and Anthropic Claude cannot be used in the same context. If you have both Anthropic and Antigravity accounts, enabling this option will cause frequent 400 errors. When enabled, please use the group feature to isolate Antigravity accounts from Anthropic accounts. Make sure you understand this before enabling!!', - aiCreditsBalance: 'AI Credits', - allowOverages: 'Allow Overages (AI Credits)', - allowOveragesTooltip: - 'Only use AI Credits after free quota is explicitly exhausted. Ordinary concurrent 429 rate limits will not switch to overages.', - creating: 'Creating...', - updating: 'Updating...', - accountCreated: 'Account created successfully', - accountUpdated: 'Account updated successfully', - failedToCreate: 'Failed to create account', - failedToUpdate: 'Failed to update account', - pleaseSelectStatus: 'Please select a valid account status', - mixedChannelWarningTitle: 'Mixed Channel Warning', - mixedChannelWarning: 'Warning: Group "{groupName}" contains both {currentPlatform} and {otherPlatform} accounts. Mixing different channels may cause thinking block signature validation issues, which will fallback to non-thinking mode. Are you sure you want to continue?', - pleaseEnterAccountName: 'Please enter account name', - pleaseEnterApiKey: 'Please enter API Key', - bedrockAccessKeyId: 'AWS Access Key ID', - bedrockSecretAccessKey: 'AWS Secret Access Key', - bedrockSessionToken: 'AWS Session Token', - bedrockRegion: 'AWS Region', - bedrockRegionHint: 'e.g. us-east-1, us-west-2, eu-west-1', - bedrockForceGlobal: 'Force Global cross-region inference', - bedrockForceGlobalHint: 'When enabled, model IDs use the global. prefix (e.g. global.anthropic.claude-...), routing requests to any supported region worldwide for higher availability', - bedrockAccessKeyIdRequired: 'Please enter AWS Access Key ID', - bedrockSecretAccessKeyRequired: 'Please enter AWS Secret Access Key', - bedrockRegionRequired: 'Please select AWS Region', - bedrockSessionTokenHint: 'Optional, for temporary credentials', - bedrockSecretKeyLeaveEmpty: 'Leave empty to keep current key', - bedrockAuthMode: 'Authentication Mode', - bedrockAuthModeSigv4: 'SigV4 Signing', - bedrockAuthModeApikey: 'Bedrock API Key', - bedrockApiKeyLabel: 'Bedrock API Key', - bedrockApiKeyDesc: 'Bearer Token', - bedrockApiKeyInput: 'API Key', - bedrockApiKeyRequired: 'Please enter Bedrock API Key', - bedrockApiKeyLeaveEmpty: 'Leave empty to keep current key', - apiKeyIsRequired: 'API Key is required', - leaveEmptyToKeep: 'Leave empty to keep current key', - // Upstream type - upstream: { - baseUrl: 'Upstream Base URL', - baseUrlHint: 'The address of the upstream Antigravity service, e.g., https://cloudcode-pa.googleapis.com', - apiKey: 'Upstream API Key', - apiKeyHint: 'API Key for the upstream service', - pleaseEnterBaseUrl: 'Please enter upstream Base URL', - pleaseEnterApiKey: 'Please enter upstream API Key' - }, - // OAuth flow - oauth: { - title: 'Claude Account Authorization', - authMethod: 'Authorization Method', - manualAuth: 'Manual Authorization', - cookieAutoAuth: 'Cookie Auto-Auth', - cookieAutoAuthDesc: - 'Use claude.ai sessionKey to automatically complete OAuth authorization without manually opening browser.', - sessionKey: 'sessionKey', - keysCount: '{count} keys', - batchCreateAccounts: 'Will batch create {count} accounts', - sessionKeyPlaceholder: - 'One sessionKey per line, e.g.:\nsk-ant-sid01-xxxxx...\nsk-ant-sid01-yyyyy...', - sessionKeyPlaceholderSingle: 'sk-ant-sid01-xxxxx...', - howToGetSessionKey: 'How to get sessionKey', - step1: 'Login to claude.ai in your browser', - step2: 'Press F12 to open Developer Tools', - step3: 'Go to Application tab', - step4: 'Find Cookies → https://claude.ai', - step5: 'Find the row with key sessionKey', - step6: 'Copy the Value', - sessionKeyFormat: 'sessionKey usually starts with sk-ant-sid01-', - startAutoAuth: 'Start Auto-Auth', - authorizing: 'Authorizing...', - followSteps: 'Follow these steps to authorize your Claude account:', - step1GenerateUrl: 'Click the button below to generate the authorization URL', - generateAuthUrl: 'Generate Auth URL', - generating: 'Generating...', - regenerate: 'Regenerate', - step2OpenUrl: 'Open the URL in your browser and complete authorization', - openUrlDesc: - 'Open the authorization URL in a new tab, log in to your Claude account and authorize.', - proxyWarning: - 'Note: If you configured a proxy, make sure your browser uses the same proxy to access the authorization page.', - step3EnterCode: 'Enter the Authorization Code', - authCodeDesc: - 'After authorization is complete, the page will display an Authorization Code. Copy and paste it below:', - authCode: 'Authorization Code', - authCodePlaceholder: 'Paste the Authorization Code from Claude page...', - authCodeHint: 'Paste the Authorization Code copied from the Claude page', - completeAuth: 'Complete Authorization', - verifying: 'Verifying...', - pleaseEnterSessionKey: 'Please enter at least one valid sessionKey', - authFailed: 'Authorization failed', - cookieAuthFailed: 'Cookie authorization failed', - keyAuthFailed: 'Key {index}: {error}', - successCreated: 'Successfully created {count} account(s)', - batchSuccess: 'Successfully created {count} account(s)', - batchPartialSuccess: 'Partial success: {success} succeeded, {failed} failed', - batchFailed: 'Batch creation failed', - // OpenAI specific - openai: { - title: 'OpenAI Account Authorization', - followSteps: 'Follow these steps to complete OpenAI account authorization:', - step1GenerateUrl: 'Click the button below to generate the authorization URL', - generateAuthUrl: 'Generate Auth URL', - step2OpenUrl: 'Open the URL in your browser and complete authorization', - openUrlDesc: - 'Open the authorization URL in a new tab, log in to your OpenAI account and authorize.', - importantNotice: - 'Important: The page may take a while to load after authorization. Please wait patiently. When the browser address bar changes to http://localhost..., the authorization is complete.', - step3EnterCode: 'Enter Authorization URL or Code', - authCodeDesc: - 'After authorization is complete, when the page URL becomes http://localhost:xxx/auth/callback?code=...:', - authCode: 'Authorization URL or Code', - authCodePlaceholder: - 'Option 1: Copy the complete URL\n(http://localhost:xxx/auth/callback?code=...)\nOption 2: Copy only the code parameter value', - authCodeHint: - 'You can copy the entire URL or just the code parameter value, the system will auto-detect', - failedToGenerateUrl: 'Failed to generate OpenAI auth URL', - failedToExchangeCode: 'Failed to exchange OpenAI auth code', - failedToValidateRT: 'Failed to validate refresh token', - errors: { - OPENAI_OAUTH_PROXY_REQUIRED: - 'No proxy is configured and this server could not reach OpenAI directly, so the OpenAI OAuth request failed. Select a proxy that can access OpenAI and retry; if the authorization code has expired, regenerate the authorization URL.' - }, - // Refresh Token auth - refreshTokenAuth: 'Manual RT Input', - refreshTokenDesc: 'Enter your existing OpenAI Refresh Token(s). Supports batch input (one per line). The system will automatically validate and create accounts.', - refreshTokenPlaceholder: 'Paste your OpenAI Refresh Token...\nSupports multiple, one per line', - codexSessionAuth: 'Codex JSON / AT Batch Input', - codexSessionDesc: 'Paste Codex JSON or an accessToken. Accounts use the step 1 settings.', - codexSessionInputLabel: 'Codex JSON or accessToken', - codexSessionPlaceholder: 'Multiple lines supported, one token or JSON per line', - codexSessionHint: 'sessionToken will not be saved as refresh_token. Without refresh_token, the account expires with the accessToken expiry; import is rejected if the expiry cannot be parsed and step 1 has no expiration.', - codexSessionImportAndCreate: 'Import & Create Account', - codexSessionEmpty: 'Please enter Codex JSON or accessToken', - codexSessionImportFailed: 'Failed to import Codex account', - codexSessionImportSuccess: 'Import completed: created {created}, updated {updated}, skipped {skipped}', - codexSessionImportPartial: 'Partial success: created {created}, updated {updated}, skipped {skipped}, failed {failed}', - codexPatAuth: 'Codex Personal Access Token', - codexPatDesc: 'Enter a Codex at- personal access token. The system validates it with OpenAI whoami before creating the account.', - codexPatInputLabel: 'Codex PAT', - codexPatPlaceholder: 'at-...', - codexPatHint: 'This is a separate auth mode. It does not save refresh_token or write an OAuth access_token expiration.', - codexPatImportAndCreate: 'Validate & Create Codex PAT Account', - codexPatEmpty: 'Please enter a Codex personal access token', - codexPatImportFailed: 'Failed to create Codex PAT account', - sessionTokenAuth: 'Manual ST Input', - sessionTokenDesc: 'Enter your existing Session Token(s). Supports batch input (one per line). The system will automatically validate and create accounts.', - sessionTokenPlaceholder: 'Paste your Session Token...\nSupports multiple, one per line', - sessionTokenRawLabel: 'Raw Input', - sessionTokenRawPlaceholder: 'Paste /api/auth/session raw payload or Session Token...', - sessionTokenRawHint: 'You can paste full JSON. The system will auto-parse ST and AT.', - openSessionUrl: 'Open Fetch URL', - copySessionUrl: 'Copy URL', - sessionUrlHint: 'This URL usually returns AT. If sessionToken is absent, copy __Secure-next-auth.session-token from browser cookies as ST.', - parsedSessionTokensLabel: 'Parsed ST', - parsedSessionTokensEmpty: 'No ST parsed. Please check your input.', - parsedAccessTokensLabel: 'Parsed AT', - validating: 'Validating...', - validateAndCreate: 'Validate & Create Account', - pleaseEnterRefreshToken: 'Please enter Refresh Token', - pleaseEnterSessionToken: 'Please enter Session Token' - }, - grok: { - title: 'Grok Account Authorization', - followSteps: 'Follow these steps to authorize your xAI/Grok account:', - step1GenerateUrl: 'Generate the xAI authorization URL', - generateAuthUrl: 'Generate Auth URL', - step2OpenUrl: 'Open the URL in your browser and complete authorization', - openUrlDesc: 'Open the authorization URL in a new tab, sign in to xAI, and authorize API access.', - importantNotice: 'When the browser reaches the local callback URL, copy the full URL or the code query parameter back here.', - step3EnterCode: 'Enter Authorization URL or Code', - authCodeDesc: 'After authorization, paste the callback URL, query string, or authorization code:', - authCode: 'Authorization URL or Code', - authCodePlaceholder: 'Paste the full callback URL, ?code=... query string, or code value', - authCodeHint: 'Full callback URLs, query strings, and bare codes are accepted.', - refreshTokenAuth: 'Manual RT Input', - refreshTokenDesc: 'Enter existing xAI refresh token(s). Supports batch input, one per line.', - refreshTokenPlaceholder: 'Paste your xAI refresh token...\nSupports multiple, one per line', - validating: 'Validating...', - validateAndCreate: 'Validate & Create Account', - pleaseEnterRefreshToken: 'Please enter Refresh Token', - failedToGenerateUrl: 'Failed to generate Grok auth URL', - missingExchangeParams: 'Missing authorization code, state, or OAuth session', - failedToExchangeCode: 'Failed to exchange Grok authorization code', - failedToValidateRT: 'Failed to validate Grok refresh token', - oauthOnlyHint: 'Initial Grok support is OAuth subscription-backed Responses API text and reasoning traffic only.' - }, - // Gemini specific - gemini: { - title: 'Gemini Account Authorization', - followSteps: 'Follow these steps to authorize your Gemini account:', - step1GenerateUrl: 'Generate the authorization URL', - generateAuthUrl: 'Generate Auth URL', - projectIdLabel: 'Project ID (optional)', - projectIdPlaceholder: 'e.g. my-gcp-project or cloud-ai-companion-xxxxx', - projectIdHint: - 'Leave empty to auto-detect after code exchange. If auto-detection fails, fill it in and re-generate the auth URL to try again.', - howToGetProjectId: 'How to get', - step2OpenUrl: 'Open the URL in your browser and complete authorization', - openUrlDesc: - 'Open the authorization URL in a new tab, log in to your Google account and authorize.', - step3EnterCode: 'Enter Authorization URL or Code', - authCodeDesc: - 'After authorization, copy the callback URL (recommended) or just the code and paste it below.', - authCode: 'Callback URL or Code', - authCodePlaceholder: - 'Option 1 (recommended): Paste the callback URL\nOption 2: Paste only the code value', - authCodeHint: 'The system will auto-extract code/state from the URL.', - redirectUri: 'Redirect URI', - redirectUriHint: - 'This must be configured in your Google OAuth client and must match exactly.', - confirmRedirectUri: - 'I have configured this Redirect URI in the Google OAuth client (must match exactly)', - invalidRedirectUri: 'Redirect URI must be a valid http(s) URL', - redirectUriNotConfirmed: 'Please confirm the Redirect URI is configured correctly', - missingRedirectUri: 'Missing redirect URI', - failedToGenerateUrl: 'Failed to generate Gemini auth URL', - missingExchangeParams: 'Missing auth code, session ID, or state', - failedToExchangeCode: 'Failed to exchange Gemini auth code', - missingProjectId: 'GCP Project ID retrieval failed: Your Google account is not linked to an active GCP project. Please activate GCP and bind a credit card in Google Cloud Console, or manually enter the Project ID during authorization.', - modelPassthrough: 'Gemini Model Passthrough', - modelPassthroughDesc: - 'All model requests are forwarded directly to the Gemini API without model restrictions or mappings.', - stateWarningTitle: 'Note', - stateWarningDesc: 'Recommended: paste the full callback URL (includes code & state).', - oauthTypeLabel: 'OAuth Type', - needsProjectId: 'Built-in OAuth (Code Assist)', - needsProjectIdDesc: 'Requires GCP project and Project ID', - noProjectIdNeeded: 'Custom OAuth (AI Studio)', - noProjectIdNeededDesc: 'Requires admin-configured OAuth client', - aiStudioNotConfiguredShort: 'Not configured', - aiStudioNotConfiguredTip: - 'AI Studio OAuth is not configured: set GEMINI_OAUTH_CLIENT_ID / GEMINI_OAUTH_CLIENT_SECRET and add Redirect URI: http://localhost:1455/auth/callback (Consent screen scopes must include https://www.googleapis.com/auth/generative-language.retriever)', - aiStudioNotConfigured: - 'AI Studio OAuth is not configured: set GEMINI_OAUTH_CLIENT_ID / GEMINI_OAUTH_CLIENT_SECRET and add Redirect URI: http://localhost:1455/auth/callback' - }, - // Antigravity specific - antigravity: { - title: 'Antigravity Account Authorization', - followSteps: 'Follow these steps to authorize your Antigravity account:', - step1GenerateUrl: 'Generate the authorization URL', - generateAuthUrl: 'Generate Auth URL', - step2OpenUrl: 'Open the URL in your browser and complete authorization', - openUrlDesc: 'Open the authorization URL in a new tab, log in to your Google account and authorize.', - importantNotice: - 'Important: The page may take a while to load after authorization. Please wait patiently. When the browser address bar shows http://localhost..., authorization is complete.', - step3EnterCode: 'Enter Authorization URL or Code', - authCodeDesc: - 'After authorization, when the page URL becomes http://localhost:xxx/auth/callback?code=...:', - authCode: 'Authorization URL or Code', - authCodePlaceholder: - 'Option 1: Copy the complete URL\n(http://localhost:xxx/auth/callback?code=...)\nOption 2: Copy only the code parameter value', - authCodeHint: 'You can copy the entire URL or just the code parameter value, the system will auto-detect', - failedToGenerateUrl: 'Failed to generate Antigravity auth URL', - missingExchangeParams: 'Missing code, session ID, or state', - failedToExchangeCode: 'Failed to exchange Antigravity auth code', - // Refresh Token auth - refreshTokenAuth: 'Manual RT', - refreshTokenDesc: 'Enter your existing Antigravity Refresh Token. Supports batch input (one per line). The system will automatically validate and create accounts.', - refreshTokenPlaceholder: 'Paste your Antigravity Refresh Token...\nSupports multiple tokens, one per line', - validating: 'Validating...', - validateAndCreate: 'Validate & Create', - pleaseEnterRefreshToken: 'Please enter Refresh Token', - failedToValidateRT: 'Failed to validate Refresh Token' - } - }, // Gemini specific (platform-wide) - gemini: { - helpButton: 'Help', - helpDialog: { - title: 'Gemini Usage Guide', - apiKeySection: 'API Key Links' - }, - modelPassthrough: 'Gemini Model Passthrough', - modelPassthroughDesc: - 'All model requests are forwarded directly to the Gemini API without model restrictions or mappings.', - baseUrlHint: 'Leave default for official Gemini API', - apiKeyHint: 'Your Gemini API Key (starts with AIza)', - tier: { - label: 'Account Tier', - hint: 'Tip: The system will try to auto-detect the tier first; if auto-detection is unavailable or fails, your selected tier is used as a fallback (simulated quota).', - aiStudioHint: - 'AI Studio quotas are per-model (Pro/Flash are limited independently). If billing is enabled, choose Pay-as-you-go.', - googleOne: { - free: 'Google One Free', - pro: 'Google One Pro', - ultra: 'Google One Ultra' - }, - gcp: { - standard: 'GCP Standard', - enterprise: 'GCP Enterprise' - }, - aiStudio: { - free: 'Google AI Free', - paid: 'Google AI Pay-as-you-go' - } - }, - accountType: { - oauthTitle: 'OAuth (Gemini)', - oauthDesc: 'Authorize with your Google account and choose an OAuth type.', - apiKeyTitle: 'API Key (AI Studio)', - apiKeyDesc: 'Fastest setup. Use an AIza API key.', - apiKeyNote: - 'Best for light testing. Free tier has strict rate limits and data may be used for training.', - apiKeyLink: 'Get API Key', - quotaLink: 'Quota guide' - }, - oauthType: { - builtInTitle: 'Built-in OAuth (Gemini CLI / Code Assist)', - builtInDesc: 'Uses Google built-in client ID. No admin configuration required.', - builtInRequirement: 'Requires a GCP project and Project ID.', - googleOneDesc: 'Personal account with Google One subscription quota', - codeAssistDesc: 'Enterprise-grade, requires a GCP project', - codeAssistRequirement: 'Requires an active GCP project with billing enabled', - showAdvanced: 'Show advanced options (custom OAuth Client)', - hideAdvanced: 'Hide advanced options (custom OAuth Client)', - gcpProjectLink: 'Create project', - customTitle: 'Custom OAuth (AI Studio OAuth)', - customDesc: 'Uses admin-configured OAuth client for org management.', - customRequirement: 'Admin must configure Client ID and add you as a test user.', - badges: { - recommended: 'Recommended', - highConcurrency: 'High concurrency', - individuals: 'Recommended for individuals', - noGcp: 'No GCP required', - enterprise: 'Enterprise users', - noAdmin: 'No admin setup', - orgManaged: 'Org managed', - adminRequired: 'Admin required' - } - }, - setupGuide: { - title: 'Gemini Setup Checklist', - checklistTitle: 'Checklist', - checklistItems: { - usIp: 'Use a US IP and ensure your account country is set to US.', - age: 'Account must be 18+.' - }, - activationTitle: 'One-click Activation', - activationItems: { - geminiWeb: 'Activate Gemini Web to avoid User not initialized.', - gcpProject: 'Activate a GCP project and get the Project ID for Code Assist.' - }, - links: { - countryCheck: 'Check country association', - countryChange: 'Change country association', - geminiWebActivation: 'Activate Gemini Web', - gcpProject: 'Open GCP Console' - } - }, - quotaPolicy: { - title: 'Gemini Quota & Limit Policy (Reference)', - note: 'Note: Gemini does not provide an official quota inquiry API. The "Daily Quota" shown here is an estimate simulated by the system based on account tiers for scheduling reference only. Please refer to official Google errors for actual limits.', - columns: { - channel: 'Auth Channel', - account: 'Account Status', - limits: 'Limit Policy', - docs: 'Official Docs' - }, - docs: { - codeAssist: 'Code Assist Quotas', - aiStudio: 'AI Studio Pricing', - vertex: 'Vertex AI Quotas' - }, - simulatedNote: 'Simulated quota, for reference only', - rows: { - googleOne: { - channel: 'Google One OAuth (Individuals / Code Assist for Individuals)', - limitsFree: 'Shared pool: 1000 RPD / 60 RPM', - limitsPro: 'Shared pool: 1500 RPD / 120 RPM', - limitsUltra: 'Shared pool: 2000 RPD / 120 RPM' - }, - gcp: { - channel: 'GCP Code Assist OAuth (Enterprise)', - limitsStandard: 'Shared pool: 1500 RPD / 120 RPM', - limitsEnterprise: 'Shared pool: 2000 RPD / 120 RPM' - }, - cli: { - channel: 'Gemini CLI (Official Google Login / Code Assist)', - free: 'Free Google Account', - premium: 'Google One AI Premium', - limitsFree: 'RPD ~1000; RPM ~60 (soft)', - limitsPremium: 'RPD ~1500+; RPM ~60+ (priority queue)' - }, - gcloud: { - channel: 'GCP Code Assist (gcloud auth)', - account: 'No Code Assist subscription', - limits: 'RPD ~1000; RPM ~60 (preview)' - }, - aiStudio: { - channel: 'AI Studio API Key / OAuth', - free: 'No billing (free tier)', - paid: 'Billing enabled (pay-as-you-go)', - limitsFree: 'RPD 50; RPM 2 (Pro) / 15 (Flash)', - limitsPaid: 'RPD unlimited; RPM 1000 (Pro) / 2000 (Flash) (per model)' - }, - customOAuth: { - channel: 'Custom OAuth Client (GCP)', - free: 'Project not billed', - paid: 'Project billed', - limitsFree: 'RPD 50; RPM 2 (project quota)', - limitsPaid: 'RPD unlimited; RPM 1000+ (project quota)' - } - } - }, - rateLimit: { - ok: 'Not rate limited', - unlimited: 'Unlimited', - limited: 'Rate limited {time}', - now: 'now' - } - }, - // Re-Auth Modal - reAuthorizeAccount: 'Re-Authorize Account', - claudeCodeAccount: 'Claude Code Account', - openaiAccount: 'OpenAI Account', - geminiAccount: 'Gemini Account', - antigravityAccount: 'Antigravity Account', - grokAccount: 'Grok Account', - inputMethod: 'Input Method', - reAuthorizedSuccess: 'Account re-authorized successfully', - // Test Modal - testAccountConnection: 'Test Account Connection', - account: 'Account', - readyToTest: 'Ready to test. Click "Start Test" to begin...', - connectingToApi: 'Connecting to API...', - testCompleted: 'Test completed successfully!', - testFailed: 'Test failed', - connectedToApi: 'Connected to API', - usingModel: 'Using model: {model}', - sendingTestMessage: 'Sending test message: "hi"', - sendingImageRequest: 'Sending image generation test request...', - response: 'Response:', - startTest: 'Start Test', - testing: 'Testing...', - retry: 'Retry', - copyOutput: 'Copy output', - outputCopied: 'Output copied', - startingTestForAccount: 'Starting test for account: {name}', - testAccountTypeLabel: 'Account type: {type}', - selectTestModel: 'Select Test Model', - testModel: 'Test model', - testPrompt: 'Prompt: "hi"', - imagePromptLabel: 'Image prompt', - imagePromptPlaceholder: 'Example: Generate an orange cat astronaut sticker in pixel-art style on a solid background.', - imagePromptDefault: 'Generate a cute orange cat astronaut sticker on a clean pastel background.', - imageTestHint: 'When an image model is selected, this test sends a real image-generation request and previews the returned image below.', - imageTestMode: 'Mode: Image generation test', - imagePreview: 'Generated images:', - imageReceived: 'Received test image #{count}', - // Stats Modal - viewStats: 'View Stats', - usageStatistics: 'Usage Statistics', - last30DaysUsage: 'Last 30 days usage statistics (based on actual usage days)', - stats: { - totalCost: '30-Day Total Cost', - accumulatedCost: 'Accumulated cost', - standardCost: 'Standard', - totalRequests: '30-Day Total Requests', - totalCalls: 'Total API calls', - avgDailyCost: 'Daily Avg Cost', - basedOnActualDays: 'Based on {days} actual usage days', - avgDailyRequests: 'Daily Avg Requests', - avgDailyUsage: 'Average daily usage', - todayOverview: 'Today Overview', - cost: 'Cost', - requests: 'Requests', - tokens: 'Tokens', - highestCostDay: 'Highest Cost Day', - highestRequestDay: 'Highest Request Day', - date: 'Date', - accumulatedTokens: 'Accumulated Tokens', - totalTokens: '30-Day Total', - dailyAvgTokens: 'Daily Average', - performance: 'Performance', - avgResponseTime: 'Avg Response', - daysActive: 'Days Active', - recentActivity: 'Recent Activity', - todayRequests: 'Today Requests', - todayTokens: 'Today Tokens', - todayCost: 'Today Cost', - usageTrend: '30-Day Cost & Request Trend', - noData: 'No usage data available for this account' - }, - usageWindow: { - statsTitle: '5-Hour Window Usage Statistics', - statsTitleDaily: 'Daily Usage Statistics', - geminiProDaily: 'Pro', - geminiFlashDaily: 'Flash', - gemini3Pro: 'G3P', - gemini3Flash: 'G3F', - gemini3Image: 'G31FI', - claude: 'Claude', - grokRequests: 'Req', - grokTokens: 'Tok', - grokUnknown: 'Grok quota is unknown until the first upstream response includes xAI rate-limit headers.', - grokRetryAfter: 'Retry after {time}', - grokProbe: 'Probe', - grokProbeTooltip: 'Send a minimal xAI Responses probe and read quota headers', - grokResetUnsupported: 'Reset unsupported', - grokResetUnsupportedTooltip: 'xAI does not expose reset credits for Grok OAuth accounts', - grokNoHeaders: 'No quota headers observed', - grokLastStatus: 'Status {status}', - grokLastProbe: 'Probe {time}', - grokLastHeadersSeen: 'Headers {time}', - passiveSampled: 'Passive', - activeQuery: 'Query' - }, - openaiQuotaReset: { - count: 'Credits', - reset: 'Reset', - countTooltipLoad: 'Click to load the available reset-credit count', - countTooltipRefresh: 'Click to refresh the available reset-credit count', - resetTooltipReady: 'Consume 1 reset credit to immediately restore the window', - resetTooltipNeedQuery: 'Click Credits first to load the available count', - resetTooltipNoCredits: 'No reset credits available', - resetTooltipShadow: 'Spark shadow accounts cannot reset credits; reset on the parent account', - expiresAt: 'Expires {time}', - expiresAtFull: 'Reset credit expires at {time}', - expandExpirations: 'Expand the other {count} reset credit expiration(s)', - collapseExpirations: 'Collapse reset credit expirations', - expirationDetails: 'Reset credit expiration details', - noCreditsAvailable: 'No reset credits available', - resetSuccess: 'Reset {windows} window(s)', - confirmTitle: 'Confirm Weekly Limit Reset', - confirmMessage: 'This will consume 1 reset credit to immediately restore the current window ({count} remaining). This action cannot be undone. Continue?' - }, - tier: { - free: 'Free', - pro: 'Pro', - ultra: 'Ultra', - aiPremium: 'AI Premium', - standard: 'Standard', - basic: 'Basic', - personal: 'Personal', - unlimited: 'Unlimited' - }, - ineligibleWarning: - 'This account is not eligible for Antigravity, but API forwarding still works. Use at your own risk.', - forbidden: 'Forbidden', - forbiddenValidation: 'Verification Required', - forbiddenViolation: 'Violation Ban', - openVerification: 'Open Verification Link', - copyLink: 'Copy Link', - linkCopied: 'Link Copied', - needsReauth: 'Re-auth Required', - rateLimited: 'Rate Limited', - usageError: 'Fetch Error' - }, - - // Scheduled Tests - scheduledTests: { - title: 'Scheduled Tests', - addPlan: 'Add Plan', - editPlan: 'Edit Plan', - deletePlan: 'Delete Plan', - model: 'Model', - cronExpression: 'Cron Expression', - enabled: 'Enabled', - lastRun: 'Last Run', - nextRun: 'Next Run', - maxResults: 'Max Results', - noPlans: 'No scheduled test plans', - confirmDelete: 'Are you sure you want to delete this plan?', - createSuccess: 'Plan created successfully', - updateSuccess: 'Plan updated successfully', - deleteSuccess: 'Plan deleted successfully', - results: 'Test Results', - noResults: 'No test results yet', - responseText: 'Response', - errorMessage: 'Error', - success: 'Success', - failed: 'Failed', - running: 'Running', - schedule: 'Schedule', - cronHelp: 'Standard 5-field cron expression (e.g., */30 * * * *)', - cronTooltipTitle: 'Cron expression examples:', - cronTooltipMeaning: 'Defines when the test runs automatically. The 5 fields are: minute, hour, day, month, and weekday.', - cronTooltipExampleEvery30Min: '*/30 * * * *: run every 30 minutes', - cronTooltipExampleHourly: '0 * * * *: run at the start of every hour', - cronTooltipExampleDaily: '0 9 * * *: run every day at 09:00', - cronTooltipExampleWeekly: '0 9 * * 1: run every Monday at 09:00', - cronTooltipRange: 'Recommended range: use standard 5-field cron. For health checks, start with a moderate frequency such as every 30 minutes, every hour, or once a day instead of running too often.', - maxResultsTooltipTitle: 'What Max Results means:', - maxResultsTooltipMeaning: 'Sets how many historical test results are kept for a single plan so the result list does not grow without limit.', - maxResultsTooltipBody: 'Only the newest test results are kept. Once the number of saved results exceeds this value, older records are pruned automatically so the history list and storage stay under control.', - maxResultsTooltipExample: 'For example, 100 means keeping at most the latest 100 test results. When the 101st result is saved, the oldest one is removed.', - maxResultsTooltipRange: 'Recommended range: usually 20 to 200. Use 20-50 when you only care about recent health status, or 100-200 if you want a longer trend history.', - autoRecover: 'Auto Recover', - autoRecoverHelp: 'Automatically recover account from error/rate-limited state on successful test' - }, - - // Proxies - proxies: { - title: 'Proxy Management', - description: 'Manage proxy servers for accounts', - createProxy: 'Create Proxy', - editProxy: 'Edit Proxy', - deleteProxy: 'Delete Proxy', - ad: { - inline: 'Need proxy IP?' - }, - dataImport: 'Import', - dataExportSelected: 'Export Selected', - dataImportTitle: 'Import Proxies', - dataImportHint: 'Upload the exported proxy JSON file to import proxies in bulk.', - dataImportWarning: 'Import will create or reuse proxies, keep their status, and trigger latency checks after completion.', - dataImportFile: 'Data File', - dataImportButton: 'Start Import', - dataImporting: 'Importing...', - dataImportSelectFile: 'Please select a data file', - dataImportParseFailed: 'Failed to parse data', - dataImportFailed: 'Failed to import data', - dataImportResult: 'Import Result', - dataImportResultSummary: 'Created {proxy_created}, reused {proxy_reused}, failed {proxy_failed}', - dataImportErrors: 'Failure Details', - dataImportSuccess: 'Import completed: created {proxy_created}, reused {proxy_reused}', - dataImportCompletedWithErrors: 'Import completed with errors: failed {proxy_failed}', - dataExport: 'Export', - dataExportConfirmMessage: 'The exported data contains sensitive proxy information. Store it securely.', - dataExportConfirm: 'Confirm Export', - dataExported: 'Data exported successfully', - dataExportFailed: 'Failed to export data', - copyProxyUrl: 'Copy Proxy URL', - urlCopied: 'Proxy URL copied', - searchProxies: 'Search proxies...', - allProtocols: 'All Protocols', - allStatus: 'All Status', - protocols: { - http: 'HTTP', - https: 'HTTPS', - socks5: 'SOCKS5', - socks5h: 'SOCKS5H (Remote DNS)' - }, - columns: { - name: 'Name', - protocol: 'Protocol', - address: 'Address', - auth: 'Auth', - location: 'Location', - status: 'Status', - accounts: 'Accounts', - latency: 'Latency', - expiry: 'Validity', - createdAt: 'Created', - actions: 'Actions' - }, - testConnection: 'Test Connection', - qualityCheck: 'Quality Check', - batchQualityCheck: 'Batch Quality Check', - batchTest: 'Test All Proxies', - testFailed: 'Failed', - latencyFailed: 'Connection failed', - batchTestEmpty: 'No proxies available for testing', - batchTestDone: 'Batch test completed for {count} proxies', - batchTestFailed: 'Batch test failed', - batchDeleteAction: 'Delete', - batchDelete: 'Batch delete', - batchDeleteConfirm: 'Delete {count} selected proxies? In-use ones will be skipped.', - batchDeleteDone: 'Deleted {deleted} proxies, skipped {skipped}', - batchDeleteSkipped: 'Skipped {skipped} proxies', - batchDeleteFailed: 'Batch delete failed', - deleteBlockedInUse: 'This proxy is in use and cannot be deleted', - accountsTitle: 'Accounts using this IP', - accountsEmpty: 'No accounts are using this proxy', - accountsFailed: 'Failed to load accounts list', - accountName: 'Account', - accountPlatform: 'Platform', - accountNotes: 'Notes', - name: 'Name', - protocol: 'Protocol', - host: 'Host', - port: 'Port', - username: 'Username (Optional)', - password: 'Password (Optional)', - status: 'Status', - enterProxyName: 'Enter proxy name', - leaveEmptyToKeep: 'Leave empty to keep current', - optionalAuth: 'Optional authentication', - form: { - hostPlaceholder: 'proxy.example.com', - portPlaceholder: '8080' - }, - noProxiesYet: 'No proxies yet', - createFirstProxy: 'Create your first proxy to route traffic through it.', - // Batch import - standardAdd: 'Standard Add', - batchAdd: 'Quick Add', - batchInput: 'Proxy List', - batchInputPlaceholder: - "Enter one proxy per line in the following formats:\nsocks5://user:pass{'@'}192.168.1.1:1080\nhttp://192.168.1.1:8080\nhttps://user:pass{'@'}proxy.example.com:443", - batchInputHint: - "Supports http, https, socks5 protocols. Format: protocol://[user:pass{'@'}]host:port", - parsedCount: '{count} valid', - invalidCount: '{count} invalid', - duplicateCount: '{count} duplicate', - importing: 'Importing...', - importProxies: 'Import {count} proxies', - batchImportSuccess: 'Successfully imported {created} proxies, skipped {skipped} duplicates', - batchImportAllSkipped: 'All {skipped} proxies already exist, skipped import', - failedToImport: 'Failed to batch import', - // Other messages - creating: 'Creating...', - updating: 'Updating...', - proxyCreated: 'Proxy created successfully', - proxyUpdated: 'Proxy updated successfully', - proxyDeleted: 'Proxy deleted successfully', - proxyWorking: 'Proxy is working!', - proxyWorkingWithLatency: 'Proxy is working! Latency: {latency}ms', - proxyTestFailed: 'Proxy test failed', - qualityCheckDone: 'Quality check completed: score {score} ({grade})', - qualityCheckFailed: 'Failed to run proxy quality check', - batchQualityDone: - 'Batch quality check completed for {count} proxies: healthy {healthy}, warn {warn}, challenge {challenge}, abnormal {failed}', - batchQualityFailed: 'Batch quality check failed', - batchQualityEmpty: 'No proxies available for quality check', - qualityReportTitle: 'Proxy Quality Report', - qualityGrade: 'Grade {grade}', - qualityExitIP: 'Exit IP', - qualityCountry: 'Exit Region', - qualityBaseLatency: 'Base Latency', - qualityCheckedAt: 'Checked At', - qualityTableTarget: 'Target', - qualityTableStatus: 'Status', - qualityTableLatency: 'Latency', - qualityTableMessage: 'Message', - qualityInline: 'Quality {grade}/{score}', - qualityStatusHealthy: 'Healthy', - qualityStatusPass: 'Pass', - qualityStatusWarn: 'Warn', - qualityStatusFail: 'Fail', - qualityStatusChallenge: 'Challenge', - qualityTargetBase: 'Base Connectivity', - failedToLoad: 'Failed to load proxies', - failedToCreate: 'Failed to create proxy', - failedToUpdate: 'Failed to update proxy', - failedToDelete: 'Failed to delete proxy', - failedToTest: 'Failed to test proxy', - nameRequired: 'Please enter proxy name', - hostRequired: 'Please enter host address', - portInvalid: 'Port must be between 1-65535', - deleteConfirm: - "Are you sure you want to delete '{name}'? Accounts using this proxy will have their proxy removed.", - neverExpires: 'Never', - expired: 'Expired', - overdueDays: 'Overdue {days}d', - expiringInDays: 'Expires in {days}d', - remainingDays: '{days}d left', - expiresAt: 'Validity', - nDays: '{days}d', - expiryDaysPlaceholder: 'Custom days, empty = never', - expiryWarnDays: 'Expiry warning (days)', - fallbackMode: 'Failure fallback', - fallbackNone: 'No fallback', - fallbackProxy: 'Backup proxy', - fallbackDirect: 'Direct connection', - backupProxy: 'Backup proxy', - }, - - // Redeem Codes - redeem: { - title: 'Redeem Code Management', - description: 'Generate and manage redeem codes', - generateCodes: 'Generate Codes', - searchCodes: 'Search codes or email...', - allTypes: 'All Types', - allStatus: 'All Status', - balance: 'Balance', - concurrency: 'Concurrency', - subscription: 'Subscription', - invitation: 'Invitation', - invitationHint: 'Invitation codes are used to restrict user registration. They are automatically marked as used after use.', - unused: 'Unused', - used: 'Used', - columns: { - code: 'Code', - type: 'Type', - value: 'Value', - status: 'Status', - usedBy: 'Used By', - usedAt: 'Used At', - expiresAt: 'Expires At', - actions: 'Actions' - }, - userPrefix: 'User #{id}', - exportCsv: 'Export CSV', - batchUpdate: 'Batch Update', - batchUpdateTitle: 'Batch Update Redeem Codes', - selectedCount: '{count} redeem code(s) selected', - clearSelection: 'Clear selection', - selectCodesFirst: 'Select redeem codes first', - noBatchFieldsSelected: 'Select at least one field to update', - batchUpdateSuccess: 'Updated {count} redeem code(s)', - failedToBatchUpdate: 'Failed to batch update redeem codes', - batchFields: { - status: 'Status', - expiresAt: 'Expires At', - notes: 'Notes', - group: 'Group' - }, - batchNotesPlaceholder: 'Enter the new note, or leave blank to clear it', - clearGroup: 'Clear group', - deleteAllUnused: 'Delete All Unused Codes', - deleteCode: 'Delete Redeem Code', - deleteCodeConfirm: - 'Are you sure you want to delete this redeem code? This action cannot be undone.', - deleteAllUnusedConfirm: - 'Are you sure you want to delete all unused (active) redeem codes? This action cannot be undone.', - deleteAll: 'Delete All', - generateCodesTitle: 'Generate Redeem Codes', - generatedSuccessfully: 'Generated Successfully', - codesCreated: '{count} redeem code(s) created', - codeType: 'Code Type', - amount: 'Amount ($)', - value: 'Value', - count: 'Count', - generating: 'Generating...', - generate: 'Generate', - copyAll: 'Copy All', - copied: 'Copied!', - download: 'Download', - codesExported: 'Codes exported successfully', - codeDeleted: 'Redeem code deleted successfully', - codesDeleted: 'Successfully deleted {count} unused code(s)', - noUnusedCodes: 'No unused codes to delete', - failedToLoad: 'Failed to load redeem codes', - failedToGenerate: 'Failed to generate codes', - failedToExport: 'Failed to export codes', - failedToDelete: 'Failed to delete code', - failedToDeleteUnused: 'Failed to delete unused codes', - failedToCopy: 'Failed to copy codes', - types: { - balance: 'Balance', - concurrency: 'Concurrency', - subscription: 'Subscription', - invitation: 'Invitation', - // Admin adjustment types (created when admin modifies user balance/concurrency) - admin_balance: 'Balance (Admin)', - admin_concurrency: 'Concurrency (Admin)' - }, - selectGroup: 'Select Group', - selectGroupPlaceholder: 'Choose a subscription group', - validityDays: 'Validity Days', - codeExpiry: 'Code Expiry', - neverExpires: 'Never expires', - expiryPresetDays: '{days} days', - customExpiry: 'Custom', - customExpiryDays: 'Custom days', - expiryDaysRequired: 'Please enter a valid expiry day count', - groupRequired: 'Please select a subscription group', - days: ' days', - status: { - unused: 'Unused', - used: 'Used', - expired: 'Expired', - disabled: 'Disabled' - } - }, - - // Announcements - announcements: { - title: 'Announcements', - description: 'Create announcements and target by conditions', - createAnnouncement: 'Create Announcement', - editAnnouncement: 'Edit Announcement', - deleteAnnouncement: 'Delete Announcement', - searchAnnouncements: 'Search announcements...', - status: 'Status', - allStatus: 'All Status', - columns: { - title: 'Title', - status: 'Status', - notifyMode: 'Notify Mode', - targeting: 'Targeting', - timeRange: 'Schedule', - createdAt: 'Created At', - actions: 'Actions' - }, - statusLabels: { - draft: 'Draft', - active: 'Active', - archived: 'Archived' - }, - notifyModeLabels: { - silent: 'Silent', - popup: 'Popup' - }, - form: { - title: 'Title', - content: 'Content (Markdown supported)', - status: 'Status', - notifyMode: 'Notify Mode', - notifyModeHint: 'Popup mode will show a popup notification to users', - startsAt: 'Starts At', - endsAt: 'Ends At', - startsAtHint: 'Leave empty to start immediately', - endsAtHint: 'Leave empty to never expire', - targetingMode: 'Targeting', - targetingAll: 'All users', - targetingCustom: 'Custom rules', - addOrGroup: 'Add OR group', - addAndCondition: 'Add AND condition', - conditionType: 'Condition type', - conditionSubscription: 'Subscription', - conditionBalance: 'Balance', - operator: 'Operator', - balanceValue: 'Balance threshold', - selectPackages: 'Select packages' - }, - operators: { - gt: '>', - gte: '≥', - lt: '<', - lte: '≤', - eq: '=' - }, - targetingSummaryAll: 'All users', - targetingSummaryCustom: 'Custom ({groups} groups)', - timeImmediate: 'Immediate', - timeNever: 'Never', - readStatus: 'Read Status', - eligible: 'Eligible', - readAt: 'Read at', - unread: 'Unread', - searchUsers: 'Search users...', - failedToLoad: 'Failed to load announcements', - failedToCreate: 'Failed to create announcement', - failedToUpdate: 'Failed to update announcement', - failedToDelete: 'Failed to delete announcement', - failedToLoadReadStatus: 'Failed to load read status', - deleteConfirm: 'Are you sure you want to delete this announcement? This action cannot be undone.' - }, - - // Promo Codes - promo: { - title: 'Promo Code Management', - description: 'Create and manage registration promo codes', - createCode: 'Create Promo Code', - editCode: 'Edit Promo Code', - deleteCode: 'Delete Promo Code', - searchCodes: 'Search codes...', - allStatus: 'All Status', - columns: { - code: 'Code', - bonusAmount: 'Bonus Amount', - maxUses: 'Max Uses', - usedCount: 'Used', - usage: 'Usage', - status: 'Status', - expiresAt: 'Expires At', - createdAt: 'Created At', - actions: 'Actions' - }, - // Form labels (flat structure for template usage) - code: 'Promo Code', - autoGenerate: 'auto-generate if empty', - codePlaceholder: 'Enter promo code or leave empty', - bonusAmount: 'Bonus Amount ($)', - maxUses: 'Max Uses', - zeroUnlimited: '0 = unlimited', - expiresAt: 'Expires At', - notes: 'Notes', - notesPlaceholder: 'Optional notes for this code', - status: 'Status', - neverExpires: 'Never expires', - // Status labels - statusActive: 'Active', - statusDisabled: 'Disabled', - statusExpired: 'Expired', - statusMaxUsed: 'Used Up', - // Usage records - usageRecords: 'Usage Records', - viewUsages: 'View Usages', - noUsages: 'No usage records yet', - userPrefix: 'User #{id}', - copied: 'Copied!', - // Messages - noCodesYet: 'No promo codes yet', - createFirstCode: 'Create your first promo code to offer registration bonuses.', - codeCreated: 'Promo code created successfully', - codeUpdated: 'Promo code updated successfully', - codeDeleted: 'Promo code deleted successfully', - deleteCodeConfirm: 'Are you sure you want to delete this promo code? This action cannot be undone.', - copyRegisterLink: 'Copy register link', - registerLinkCopied: 'Register link copied to clipboard', - failedToLoad: 'Failed to load promo codes', - failedToCreate: 'Failed to create promo code', - failedToUpdate: 'Failed to update promo code', - failedToDelete: 'Failed to delete promo code', - failedToLoadUsages: 'Failed to load usage records' - }, - - // Usage Records - usage: { - title: 'Usage Records', - description: 'View and manage all user usage records', - userFilter: 'User', - searchUserPlaceholder: 'Search user by email...', - searchApiKeyPlaceholder: 'Search API key by name...', - searchAccountPlaceholder: 'Search account by name...', - selectedUser: 'Selected', - user: 'User', - account: 'Account', - group: 'Group', - requestId: 'Request ID', - requestIdCopied: 'Request ID copied', - allModels: 'All Models', - allAccounts: 'All Accounts', - allGroups: 'All Groups', - allTypes: 'All Types', - inputCost: 'Input Cost', - outputCost: 'Output Cost', - cacheCreationCost: 'Cache Creation Cost', - cacheReadCost: 'Cache Read Cost', - inputTokens: 'Input Tokens', - outputTokens: 'Output Tokens', - cacheCreationTokens: 'Cache Creation Tokens', - cacheCreation5mTokens: 'Cache Write', - cacheCreation1hTokens: 'Cache Write', - cacheReadTokens: 'Cache Read Tokens', - failedToLoad: 'Failed to load usage records', - billingType: 'Billing Type', - allBillingTypes: 'All Billing Types', - billingTypeBalance: 'Balance', - billingTypeSubscription: 'Subscription', - billingMode: 'Billing Mode', - billingModeToken: 'Token', - billingModePerRequest: 'Per Request', - billingModeImage: 'Image', - allBillingModes: 'All Billing Modes', - ipAddress: 'IP', - clickToViewBalance: 'Click to view balance history', - failedToLoadUser: 'Failed to load user info', - userDeletedBadge: 'Deleted', - cleanup: { - button: 'Cleanup', - title: 'Cleanup Usage Records', - warning: 'Cleanup is irreversible and will affect historical stats.', - submit: 'Submit Cleanup', - submitting: 'Submitting...', - confirmTitle: 'Confirm Cleanup', - confirmMessage: 'Are you sure you want to submit this cleanup task? This action cannot be undone.', - confirmSubmit: 'Confirm Cleanup', - cancel: 'Cancel', - cancelConfirmTitle: 'Confirm Cancel', - cancelConfirmMessage: 'Are you sure you want to cancel this cleanup task?', - cancelConfirm: 'Confirm Cancel', - cancelSuccess: 'Cleanup task canceled', - cancelFailed: 'Failed to cancel cleanup task', - recentTasks: 'Recent Cleanup Tasks', - loadingTasks: 'Loading tasks...', - noTasks: 'No cleanup tasks yet', - range: 'Range', - deletedRows: 'Deleted', - missingRange: 'Please select a date range', - submitSuccess: 'Cleanup task created', - submitFailed: 'Failed to create cleanup task', - loadFailed: 'Failed to load cleanup tasks', - status: { - pending: 'Pending', - running: 'Running', - succeeded: 'Succeeded', - failed: 'Failed', - canceled: 'Canceled' - } - } - }, - - // Ops Monitoring - ops: { - title: 'Ops Monitoring', - description: 'Operational monitoring and troubleshooting', - // Dashboard - systemHealth: 'System Health', - overview: 'Overview', - noSystemMetrics: 'No system metrics collected yet.', - collectedAt: 'Collected at:', - window: 'window', - memory: 'Memory', - db: 'DB', - goroutines: 'Goroutines', - jobs: 'Jobs', - jobsHelp: 'Click “Details” to view job heartbeats and recent errors', - active: 'active', - idle: 'idle', - waiting: 'waiting', - conns: 'conns', - queue: 'queue', - accountSwitches: 'Account switches', - ok: 'ok', - lastRun: 'last_run:', - lastSuccess: 'last_success:', - lastError: 'last_error:', - noData: 'No data.', - loadingText: 'loading', - ready: 'ready', - autoRefreshRemaining: 'Remaining {seconds}s', - systemLogs: { - title: 'System Logs', - description: 'Newest logs are shown first. Filter, search, and clean up by condition.', - queue: 'Queue', - written: 'Written', - dropped: 'Dropped', - failed: 'Failed', - runtimeConfig: 'Runtime Log Configuration (applies immediately)', - all: 'All', - level: 'Level', - stacktraceThreshold: 'Stacktrace threshold', - samplingInitial: 'Sampling initial', - samplingThereafter: 'Sampling thereafter', - retentionDays: 'Retention days', - caller: 'caller', - sampling: 'sampling', - saveAndApply: 'Save and apply', - resetDefaults: 'Reset defaults', - latestWriteError: 'Latest write error:', - timeRange: 'Time range', - startTime: 'Start time (optional)', - endTime: 'End time (optional)', - component: 'Component', - componentPlaceholder: 'e.g. http.access', - keyId: 'KEY ID', - platform: 'Platform', - model: 'Model', - keyword: 'Keyword', - keywordPlaceholder: 'message/request_id', - search: 'Search', - cleanCurrentFilters: 'Clean current filters', - refreshHealth: 'Refresh health', - empty: 'No system logs', - time: 'Time', - logDetails: 'Log Details', - loadFailed: 'Failed to load system logs', - runtimeConfigActive: 'Runtime log configuration is active', - runtimeConfigSaveFailed: 'Failed to save log configuration', - resetRuntimeConfigConfirm: 'Reset to startup configuration (env/yaml) and apply immediately?', - runtimeConfigReset: 'Reset to startup log configuration', - runtimeConfigResetFailed: 'Failed to reset log configuration', - cleanupConfirm: 'Clean up system logs matching the current filters? This cannot be undone.', - cleanupSuccess: 'Cleanup complete. Deleted {count} log entries.', - cleanupFailed: 'Failed to clean up system logs' - }, - requestsTotal: 'Requests (total)', - slaScope: 'SLA scope:', - tokens: 'Tokens', - tps: 'TPS:', - current: 'current', - peak: 'peak', - average: 'average', - totalRequests: 'Total Requests', - avgQps: 'Avg QPS', - avgTps: 'Avg TPS', - avgLatency: 'Avg Request Duration', - avgTtft: 'Avg TTFT', - exceptions: 'Exceptions', - requestErrors: 'Request Errors', - errorCount: 'Error Count', - upstreamErrors: 'Upstream Errors', - errorCountExcl429529: 'Error Count (excl 429/529)', - sla: 'SLA (excl business limits)', - businessLimited: 'business_limited:', - errors: 'Errors', - errorRate: 'error_rate:', - upstreamRate: 'upstream_rate:', - latencyDuration: 'Request Duration', - ttftLabel: 'TTFT (first_token_ms)', - p50: 'p50:', - p90: 'p90:', - p95: 'p95:', - p99: 'p99:', - avg: 'avg:', - max: 'max:', - requests: 'Requests', - requestsTitle: 'Requests', - upstream: 'Upstream', - client: 'Client', - system: 'System', - other: 'Other', - errorsSla: 'Errors (SLA scope)', - upstreamExcl429529: 'Upstream (excl 429/529)', - failedToLoadData: 'Failed to load ops data.', - failedToLoadOverview: 'Failed to load overview', - failedToLoadThroughputTrend: 'Failed to load throughput trend', - failedToLoadSwitchTrend: 'Failed to load avg account switches trend', - failedToLoadLatencyHistogram: 'Failed to load request duration histogram', - failedToLoadErrorTrend: 'Failed to load error trend', - failedToLoadErrorDistribution: 'Failed to load error distribution', - failedToLoadErrorDetail: 'Failed to load error detail', - retryFailed: 'Retry failed', - tpsK: 'TPS (K)', - top: 'Top:', - throughputTrend: 'Throughput Trend', - switchRateTrend: 'Avg Account Switches', - latencyHistogram: 'Request Duration Histogram', - errorTrend: 'Error Trend', - errorDistribution: 'Error Distribution', - switchRate: 'Avg switches', - // Health Score & Diagnosis - health: 'Health', - healthCondition: 'Health Condition', - healthHelp: 'Overall system health score based on SLA, error rate, and resource usage', - healthyStatus: 'Healthy', - riskyStatus: 'At Risk', - idleStatus: 'Idle', - timeRange: { - '5m': 'Last 5 minutes', - '30m': 'Last 30 minutes', - '1h': 'Last 1 hour', - '1d': 'Last 1 day', - '15d': 'Last 15 days', - '6h': 'Last 6 hours', - '24h': 'Last 24 hours', - '7d': 'Last 7 days', - '30d': 'Last 30 days' - }, - openaiTokenStats: { - title: 'OpenAI Token Request Stats', - viewModeTopN: 'TopN', - viewModePagination: 'Pagination', - prevPage: 'Previous', - nextPage: 'Next', - pageInfo: 'Page {page}/{total}', - totalModels: 'Total models: {total}', - failedToLoad: 'Failed to load OpenAI token stats', - empty: 'No OpenAI token stats for the current filters', - table: { - model: 'Model', - requestCount: 'Requests', - avgTokensPerSec: 'Avg Tokens/sec', - avgFirstTokenMs: 'Avg First Token Latency (ms)', - totalOutputTokens: 'Total Output Tokens', - avgDurationMs: 'Avg Duration (ms)', - requestsWithFirstToken: 'Requests With First Token' - } - }, - fullscreen: { - enter: 'Enter Fullscreen' - }, - diagnosis: { - title: 'Smart Diagnosis', - footer: 'Automated diagnostic suggestions based on current metrics', - idle: 'System is currently idle', - idleImpact: 'No active traffic', - // Resource diagnostics - dbDown: 'Database connection failed', - dbDownImpact: 'All database operations will fail', - dbDownAction: 'Check database service status, network connectivity, and connection configuration', - redisDown: 'Redis connection failed', - redisDownImpact: 'Cache functionality degraded, performance may decline', - redisDownAction: 'Check Redis service status and network connectivity', - cpuCritical: 'CPU usage critically high ({usage}%)', - cpuCriticalImpact: 'System response slowing, may affect all requests', - cpuCriticalAction: 'Check CPU-intensive tasks, consider scaling or code optimization', - cpuHigh: 'CPU usage elevated ({usage}%)', - cpuHighImpact: 'System load is high, needs attention', - cpuHighAction: 'Monitor CPU trends, prepare scaling plan', - memoryCritical: 'Memory usage critically high ({usage}%)', - memoryCriticalImpact: 'May trigger OOM, system stability threatened', - memoryCriticalAction: 'Check for memory leaks, consider increasing memory or optimizing usage', - memoryHigh: 'Memory usage elevated ({usage}%)', - memoryHighImpact: 'Memory pressure is high, needs attention', - memoryHighAction: 'Monitor memory trends, check for memory leaks', - ttftHigh: 'Time to first token elevated ({ttft}ms)', - ttftHighImpact: 'User perceived latency increased', - ttftHighAction: 'Optimize request processing flow, reduce pre-processing time', - // Error rate diagnostics - upstreamCritical: 'Upstream error rate critically high ({rate}%)', - upstreamCriticalImpact: 'May affect many user requests', - upstreamCriticalAction: 'Check upstream service health, enable fallback strategies', - upstreamHigh: 'Upstream error rate elevated ({rate}%)', - upstreamHighImpact: 'Recommend checking upstream service status', - upstreamHighAction: 'Contact upstream service team, prepare fallback plan', - errorHigh: 'Error rate too high ({rate}%)', - errorHighImpact: 'Many requests failing', - errorHighAction: 'Check error logs, identify root cause, urgent fix required', - errorElevated: 'Error rate elevated ({rate}%)', - errorElevatedImpact: 'Recommend checking error logs', - errorElevatedAction: 'Analyze error types and distribution, create fix plan', - // SLA diagnostics - slaCritical: 'SLA critically below target ({sla}%)', - slaCriticalImpact: 'User experience severely degraded', - slaCriticalAction: 'Urgently investigate errors and latency, consider rate limiting', - slaLow: 'SLA below target ({sla}%)', - slaLowImpact: 'Service quality needs attention', - slaLowAction: 'Analyze SLA decline causes, optimize system performance', - // Health score diagnostics - healthCritical: 'Overall health score critically low ({score})', - healthCriticalImpact: 'Multiple metrics may be degraded; prioritize error rate and latency investigation', - healthCriticalAction: 'Comprehensive system check, prioritize critical-level issues', - healthLow: 'Overall health score low ({score})', - healthLowImpact: 'May indicate minor instability; monitor SLA and error rates', - healthLowAction: 'Monitor metric trends, prevent issue escalation', - healthy: 'All system metrics normal', - healthyImpact: 'Service running stable' - }, - // Error Log - errorLog: { - timeId: 'Time / ID', - commonErrors: { - contextDeadlineExceeded: 'context deadline exceeded', - connectionRefused: 'connection refused', - rateLimit: 'rate limit' - }, - time: 'Time', - type: 'Type', - context: 'Context', - platform: 'Platform', - model: 'Model', - group: 'Group', - user: 'User', - userId: 'User ID', - apiKey: 'API Key', - keyDeletedBadge: 'Key Deleted', - account: 'Account', - accountId: 'Account ID', - status: 'Status', - message: 'Message', - ip: 'IP', - latency: 'Request Duration', - action: 'Action', - noErrors: 'No errors in this window.', - grp: 'GRP:', - acc: 'ACC:', - details: 'Details', - phase: 'Phase', - id: 'ID:', - typeUpstream: 'Upstream', - typeRequest: 'Request', - typeAuth: 'Auth', - typeRouting: 'Routing', - typeInternal: 'Internal', - endpoint: 'Endpoint', - requestType: 'Type', - requestTypeSync: 'Sync', - requestTypeStream: 'Stream', - requestTypeWs: 'WS' - }, - // Error Details Modal - errorDetails: { - upstreamErrors: 'Upstream Errors', - requestErrors: 'Request Errors', - unresolved: 'Unresolved', - resolved: 'Resolved', - viewErrors: 'Errors', - viewExcluded: 'Excluded', - statusCodeOther: 'Other', - owner: { - provider: 'Provider', - client: 'Client', - platform: 'Platform' - }, - phase: { - request: 'Request', - auth: 'Auth', - routing: 'Routing', - upstream: 'Upstream', - network: 'Network', - internal: 'Internal' - }, - total: 'Total:', - searchPlaceholder: 'Search request_id / client_request_id / message', - }, - // Error Detail Modal - errorDetail: { - title: 'Error Detail', - titleWithId: 'Error #{id}', - noErrorSelected: 'No error selected.', - resolution: 'Resolved:', - failedToUpdateResolvedStatus: 'Failed to update resolved status', - classificationKeys: { - phase: 'Phase', - owner: 'Owner', - source: 'Source', - resolvedAt: 'Resolved At', - resolvedBy: 'Resolved By' - }, - source: { - upstream_http: 'Upstream HTTP' - }, - upstreamKeys: { - status: 'Status', - message: 'Message', - detail: 'Detail', - upstreamErrors: 'Upstream Errors' - }, - upstreamEvent: { - account: 'Account', - status: 'Status', - requestId: 'Request ID' - }, - responsePreview: { - expand: 'Response (click to expand)', - collapse: 'Response (click to collapse)' - }, - loading: 'Loading…', - requestId: 'Request ID', - time: 'Time', - phase: 'Phase', - status: 'Status', - message: 'Message', - basicInfo: 'Basic Info', - platform: 'Platform', - model: 'Model', - group: 'Group', - user: 'User', - account: 'Account', - latency: 'Request Duration', - businessLimited: 'Business Limited', - requestPath: 'Request Path', - inboundEndpoint: 'Inbound Endpoint', - upstreamEndpoint: 'Upstream Endpoint', - requestedModel: 'Requested Model', - upstreamModel: 'Upstream Model', - requestType: 'Request Type', - requestTypeUnknown: 'Unknown', - requestTypeSync: 'Sync', - requestTypeStream: 'Stream', - requestTypeWs: 'WebSocket', - modelMapping: 'Model Mapping', - timings: 'Timings', - auth: 'Auth', - routing: 'Routing', - upstream: 'Upstream', - response: 'Response', - classification: 'Classification', - errorBody: 'Error Body', - trimmed: 'trimmed', - markResolved: 'Mark resolved', - markUnresolved: 'Mark unresolved', - tabOverview: 'Overview', - tabRequest: 'Request', - tabResponse: 'Response', - responseBody: 'Response', - compareA: 'Compare A', - compareB: 'Compare B', - suggestion: 'Suggestion', - suggestUpstream: 'Upstream instability: check account status or consider switching accounts', - suggestRequest: 'Client request error: ask customer to fix request parameters', - suggestAuth: 'Auth failed: verify API key/credentials', - suggestPlatform: 'Platform error: prioritize investigation and fix', - suggestGeneric: 'See details for more context', - apiKeyPrefix: 'Key Prefix', - attemptedKeyPrefix: 'Attempted Key Prefix', - deletedKeyOwner: 'Deleted Key Owner', - keyDeletedBadge: 'Key Deleted' - }, - requestDetails: { - title: 'Request Details', - details: 'Details', - rangeLabel: 'Window: {range}', - rangeMinutes: '{n} minutes', - rangeHours: '{n} hours', - empty: 'No requests in this window.', - emptyHint: 'Try a different time range or remove filters.', - failedToLoad: 'Failed to load request details', - requestIdCopied: 'Request ID copied', - copyFailed: 'Copy failed', - copy: 'Copy', - viewError: 'View Error', - kind: { - success: 'SUCCESS', - error: 'ERROR' - }, - table: { - time: 'Time', - kind: 'Kind', - platform: 'Platform', - model: 'Model', - duration: 'Duration', - status: 'Status', - requestId: 'Request ID', - actions: 'Actions' - } - }, - alertEvents: { - title: 'Alert Events', - description: 'Recent alert firing/resolution records (email-only)', - loading: 'Loading...', - empty: 'No alert events', - loadFailed: 'Failed to load alert events', - status: { - firing: 'FIRING', - resolved: 'RESOLVED', - manualResolved: 'MANUAL RESOLVED' - }, - detail: { - title: 'Alert Detail', - loading: 'Loading detail...', - empty: 'No detail', - loadFailed: 'Failed to load alert detail', - manualResolve: 'Mark as Resolved', - manualResolvedSuccess: 'Marked as manually resolved', - manualResolvedFailed: 'Failed to mark as manually resolved', - silence: 'Ignore Alert', - silenceSuccess: 'Alert silenced', - silenceFailed: 'Failed to silence alert', - viewRule: 'View Rule', - viewLogs: 'View Logs', - firedAt: 'Fired At', - resolvedAt: 'Resolved At', - ruleId: 'Rule ID', - dimensions: 'Dimensions', - historyTitle: 'History', - historyHint: 'Recent events with same rule + dimensions', - historyLoading: 'Loading history...', - historyEmpty: 'No history' - }, - table: { - time: 'Time', - status: 'Status', - severity: 'Severity', - platform: 'Platform', - ruleId: 'Rule ID', - title: 'Title', - duration: 'Duration', - metric: 'Metric / Threshold', - dimensions: 'Dimensions', - email: 'Email Sent', - emailSent: 'Sent', - emailIgnored: 'Ignored' - } - }, - alertRules: { - title: 'Alert Rules', - description: 'Create and manage threshold-based system alerts (email-only)', - loading: 'Loading...', - empty: 'No alert rules', - loadFailed: 'Failed to load alert rules', - saveFailed: 'Failed to save alert rule', - saveSuccess: 'Alert rule saved successfully', - deleteFailed: 'Failed to delete alert rule', - deleteSuccess: 'Alert rule deleted successfully', - manage: 'Manage Alert Rules', - create: 'Create Rule', - createTitle: 'Create Alert Rule', - editTitle: 'Edit Alert Rule', - deleteConfirmTitle: 'Delete this rule?', - deleteConfirmMessage: 'This will remove the rule and its related events. Continue?', - metricGroups: { - system: 'System Metrics', - group: 'Group-level Metrics (requires group_id)', - account: 'Account-level Metrics' - }, - metrics: { - successRate: 'Success Rate (%)', - errorRate: 'Error Rate (%)', - upstreamErrorRate: 'Upstream Error Rate (%)', - p95: 'P95 Latency (ms)', - p99: 'P99 Latency (ms)', - cpu: 'CPU Usage (%)', - memory: 'Memory Usage (%)', - queueDepth: 'Concurrency Queue Depth', - groupAvailableAccounts: 'Group Available Accounts', - groupAvailableRatio: 'Group Available Ratio (%)', - groupRateLimitRatio: 'Group Rate Limit Ratio (%)', - accountRateLimitedCount: 'Rate-limited Accounts', - accountErrorCount: 'Error Accounts (excluding temporarily unschedulable)', - accountErrorRatio: 'Error Account Ratio (%)', - accountTempUnscheduledCount: 'Temporarily Unschedulable Accounts', - overloadAccountCount: 'Overloaded Accounts' - }, - metricDescriptions: { - successRate: 'Percentage of successful requests in the window (0-100).', - errorRate: 'Percentage of failed requests in the window (0-100).', - upstreamErrorRate: 'Percentage of upstream failures in the window (0-100).', - p95: 'P95 request latency within the window (ms).', - p99: 'P99 request latency within the window (ms).', - cpu: 'Current instance CPU usage (0-100).', - memory: 'Current instance memory usage (0-100).', - queueDepth: 'Concurrency queue depth within the window (queued requests).', - groupAvailableAccounts: 'Number of available accounts in the selected group (requires group_id).', - groupAvailableRatio: 'Available account ratio in the selected group (0-100, requires group_id).', - groupRateLimitRatio: 'Rate-limited account ratio in the selected group (0-100, requires group_id).', - accountRateLimitedCount: 'Number of rate-limited accounts within the window.', - accountErrorCount: 'Number of error accounts within the window (excluding temporarily unschedulable).', - accountErrorRatio: 'Error account ratio within the window (0-100).', - accountTempUnscheduledCount: 'Number of accounts currently temporarily unschedulable (e.g. proxy/credential failure auto-eviction).', - overloadAccountCount: 'Number of overloaded accounts within the window.' - }, - hints: { - recommended: 'Recommended: operator {operator}, threshold {threshold}{unit}', - groupRequired: 'This is a group-level metric; selecting a group (group_id) is required.', - groupOptional: 'Optional: limit the rule to a specific group via group_id.' - }, - table: { - name: 'Name', - metric: 'Metric', - severity: 'Severity', - enabled: 'Enabled', - actions: 'Actions' - }, - form: { - name: 'Name', - description: 'Description', - metric: 'Metric', - operator: 'Operator', - groupId: 'Group (group_id)', - groupPlaceholder: 'Select a group', - allGroups: 'All groups', - threshold: 'Threshold', - severity: 'Severity', - window: 'Window (minutes)', - sustained: 'Sustained (samples)', - cooldown: 'Cooldown (minutes)', - enabled: 'Enabled', - notifyEmail: 'Send email notifications' - }, - validation: { - title: 'Please fix the following issues', - invalid: 'Invalid rule', - nameRequired: 'Name is required', - metricRequired: 'Metric is required', - groupIdRequired: 'group_id is required for group-level metrics', - operatorRequired: 'Operator is required', - thresholdRequired: 'Threshold must be a number', - windowRange: 'Window must be one of: 1, 5, 60 minutes', - sustainedRange: 'Sustained must be between 1 and 1440 samples', - cooldownRange: 'Cooldown must be between 0 and 1440 minutes' - } - }, - runtime: { - title: 'Ops Runtime Settings', - description: 'Stored in database; changes take effect without editing config files.', - loading: 'Loading...', - noData: 'No runtime settings available', - loadFailed: 'Failed to load runtime settings', - saveSuccess: 'Runtime settings saved', - saveFailed: 'Failed to save runtime settings', - alertTitle: 'Alert Evaluator', - groupAvailabilityTitle: 'Group Availability Monitor', - evalIntervalSeconds: 'Evaluation Interval (seconds)', - silencing: { - title: 'Alert Silencing (Maintenance Mode)', - enabled: 'Enable silencing', - globalUntil: 'Silence until (RFC3339)', - untilHint: 'Leave empty to only toggle silencing without an expiry (not recommended).', - reason: 'Reason', - reasonPlaceholder: 'e.g., planned maintenance', - entries: { - title: 'Advanced: targeted silencing', - hint: 'Optional: silence only certain rules or severities. Leave fields empty to match all.', - add: 'Add Entry', - empty: 'No targeted entries', - entryTitle: 'Entry #{n}', - ruleId: 'Rule ID (optional)', - ruleIdPlaceholder: 'e.g., 1', - severities: 'Severities (optional)', - severitiesPlaceholder: 'e.g., P0,P1 (empty = all)', - until: 'Until (RFC3339)', - reason: 'Reason', - validation: { - untilRequired: 'Entry until time is required', - untilFormat: 'Entry until time must be a valid RFC3339 timestamp', - ruleIdPositive: 'Entry rule_id must be a positive integer', - severitiesFormat: 'Entry severities must be a comma-separated list of P0..P3' - } - }, - validation: { - timeFormat: 'Silence time must be a valid RFC3339 timestamp' - } - }, - lockEnabled: 'Distributed Lock Enabled', - lockKey: 'Distributed Lock Key', - lockTTLSeconds: 'Distributed Lock TTL (seconds)', - showAdvancedDeveloperSettings: 'Show advanced developer settings (Distributed Lock)', - advancedSettingsSummary: 'Advanced settings (Distributed Lock)', - evalIntervalHint: 'How often the evaluator runs. Keeping the default is recommended.', - validation: { - title: 'Please fix the following issues', - invalid: 'Invalid settings', - evalIntervalRange: 'Evaluation interval must be between 1 and 86400 seconds', - lockKeyRequired: 'Distributed lock key is required when lock is enabled', - lockKeyPrefix: 'Distributed lock key must start with "{prefix}"', - lockKeyHint: 'Recommended: start with "{prefix}" to avoid conflicts', - lockTtlRange: 'Distributed lock TTL must be between 1 and 86400 seconds', - slaMinPercentRange: 'SLA minimum percentage must be between 0 and 100', - ttftP99MaxRange: 'TTFT P99 maximum must be a number ≥ 0', - requestErrorRateMaxRange: 'Request error rate maximum must be between 0 and 100', - upstreamErrorRateMaxRange: 'Upstream error rate maximum must be between 0 and 100' - } - }, - email: { - title: 'Email Notification', - description: 'Configure alert/report email notifications (stored in database).', - loading: 'Loading...', - noData: 'No email notification config', - loadFailed: 'Failed to load email notification config', - saveSuccess: 'Email notification config saved', - saveFailed: 'Failed to save email notification config', - alertTitle: 'Alert Emails', - reportTitle: 'Report Emails', - recipients: 'Recipients', - recipientsHint: 'If empty, the system may fallback to the first admin email.', - minSeverity: 'Min Severity', - minSeverityAll: 'All severities', - rateLimitPerHour: 'Rate limit per hour', - batchWindowSeconds: 'Batch window (seconds)', - includeResolved: 'Include resolved alerts', - dailySummary: 'Daily summary', - weeklySummary: 'Weekly summary', - errorDigest: 'Error digest', - errorDigestMinCount: 'Min errors for digest', - accountHealth: 'Account health', - accountHealthThreshold: 'Error rate threshold (%)', - cronPlaceholder: 'Cron expression', - reportHint: 'Schedules use cron syntax; leave empty to use defaults.', - validation: { - title: 'Please fix the following issues', - invalid: 'Invalid email notification config', - alertRecipientsRequired: 'Alert emails are enabled but no recipients are configured', - reportRecipientsRequired: 'Report emails are enabled but no recipients are configured', - invalidRecipients: 'One or more recipient emails are invalid', - rateLimitRange: 'Rate limit per hour must be a number ≥ 0', - batchWindowRange: 'Batch window must be between 0 and 86400 seconds', - cronRequired: 'A cron expression is required when schedule is enabled', - cronFormat: 'Cron expression format looks invalid (expected at least 5 parts)', - digestMinCountRange: 'Min errors for digest must be a number ≥ 0', - accountHealthThresholdRange: 'Account health threshold must be between 0 and 100' - } - }, - settings: { - title: 'Ops Monitoring Settings', - loadFailed: 'Failed to load settings', - saveSuccess: 'Ops monitoring settings saved successfully', - saveFailed: 'Failed to save settings', - dataCollection: 'Data Collection', - evaluationInterval: 'Evaluation Interval (seconds)', - evaluationIntervalHint: 'Frequency of detection tasks, recommended to keep default', - alertConfig: 'Alert Configuration', - enableAlert: 'Enable Alerts', - alertRecipients: 'Alert Recipient Emails', - emailPlaceholder: 'Enter email address', - recipientsHint: 'If empty, the system will use the first admin email as default recipient', - minSeverity: 'Minimum Severity', - reportConfig: 'Report Configuration', - enableReport: 'Enable Reports', - reportRecipients: 'Report Recipient Emails', - dailySummary: 'Daily Summary', - weeklySummary: 'Weekly Summary', - metricThresholds: 'Metric Thresholds', - metricThresholdsHint: 'Configure alert thresholds for metrics, values exceeding thresholds will be displayed in red', - slaMinPercent: 'SLA Minimum Percentage', - slaMinPercentHint: 'SLA below this value will be displayed in red (default: 99.5%)', - ttftP99MaxMs: 'TTFT P99 Maximum (ms)', - ttftP99MaxMsHint: 'TTFT P99 above this value will be displayed in red (default: 500ms)', - requestErrorRateMaxPercent: 'Request Error Rate Maximum (%)', - requestErrorRateMaxPercentHint: 'Request error rate above this value will be displayed in red (default: 5%)', - upstreamErrorRateMaxPercent: 'Upstream Error Rate Maximum (%)', - upstreamErrorRateMaxPercentHint: 'Upstream error rate above this value will be displayed in red (default: 5%)', - advancedSettings: 'Advanced Settings', - dataRetention: 'Data Retention Policy', - enableCleanup: 'Enable Data Cleanup', - cleanupSchedule: 'Cleanup Schedule (Cron)', - cleanupScheduleHint: 'Example: 0 2 * * * means 2 AM daily', - errorLogRetentionDays: 'Error Log Retention Days', - minuteMetricsRetentionDays: 'Minute Metrics Retention Days', - hourlyMetricsRetentionDays: 'Hourly Metrics Retention Days', - retentionDaysHint: 'Recommended 7-90 days; longer periods consume more storage. Set to 0 to wipe all history on every scheduled cleanup', - aggregation: 'Pre-aggregation Tasks', - enableAggregation: 'Enable Pre-aggregation', - aggregationHint: 'Pre-aggregation improves query performance for long time windows', - openaiQuotaAutoPause: 'OpenAI Account Quota Auto-pause', - openaiQuotaAutoPauseHint: 'When an OpenAI account reaches its 5h / 7d usage threshold, the scheduler skips it automatically and resumes once the window rolls over. Per-account thresholds take precedence over this global default.', - openaiQuotaAutoPauseDefault5h: 'Default 5h usage threshold (%)', - openaiQuotaAutoPauseDefault7d: 'Default 7d usage threshold (%)', - openaiQuotaAutoPauseThresholdHint: 'Value 0-100; leave blank or 0 to disable the global default threshold.', - errorFiltering: 'Error Filtering', - ignoreCountTokensErrors: 'Ignore count_tokens errors', - ignoreCountTokensErrorsHint: 'When enabled, errors from count_tokens requests will not be written to the error log.', - ignoreContextCanceled: 'Ignore client disconnect errors', - ignoreContextCanceledHint: 'When enabled, client disconnect (context canceled) errors will not be written to the error log.', - ignoreNoAvailableAccounts: 'Ignore no available accounts errors', - ignoreNoAvailableAccountsHint: 'When enabled, "No available accounts" errors will not be written to the error log (not recommended; usually a config issue).', - ignoreInvalidApiKeyErrors: 'Ignore invalid API key errors', - ignoreInvalidApiKeyErrorsHint: 'When enabled, invalid or missing API key errors (INVALID_API_KEY, API_KEY_REQUIRED) will not be written to the error log.', - ignoreInsufficientBalanceErrors: 'Ignore Insufficient Balance Errors', - ignoreInsufficientBalanceErrorsHint: 'When enabled, insufficient account balance errors will not be written to the error log.', - autoRefresh: 'Auto Refresh', - enableAutoRefresh: 'Enable auto refresh', - enableAutoRefreshHint: 'Automatically refresh dashboard data at a fixed interval.', - refreshInterval: 'Refresh Interval', - refreshInterval15s: '15 seconds', - refreshInterval30s: '30 seconds', - refreshInterval60s: '60 seconds', - dashboardCards: 'Dashboard Cards', - displayAlertEvents: 'Display alert events', - displayAlertEventsHint: 'Show or hide the recent alert events card on the ops dashboard. Enabled by default.', - displayOpenAITokenStats: 'Display OpenAI token request stats', - displayOpenAITokenStatsHint: 'Show or hide the OpenAI token request stats card on the ops dashboard. Hidden by default.', - autoRefreshCountdown: 'Auto refresh: {seconds}s', - validation: { - title: 'Please fix the following issues', - retentionDaysRange: 'Retention days must be between 0 and 365 (0 = wipe all on every cleanup)', - slaMinPercentRange: 'SLA minimum percentage must be between 0 and 100', - ttftP99MaxRange: 'TTFT P99 maximum must be a number ≥ 0', - requestErrorRateMaxRange: 'Request error rate maximum must be between 0 and 100', - upstreamErrorRateMaxRange: 'Upstream error rate maximum must be between 0 and 100', - openaiQuotaAutoPauseRange: 'OpenAI quota auto-pause threshold must be between 0 and 100' - } - }, - concurrency: { - title: 'Concurrency / Queue', - byPlatform: 'By Platform', - byGroup: 'By Group', - byAccount: 'By Account', - byUser: 'By User', - showByUserTooltip: 'Switch to user view to see concurrency usage per user', - switchToUser: 'Switch to user view', - switchToPlatform: 'Switch to platform view', - totalRows: '{count} rows', - disabledHint: 'Realtime monitoring is disabled in settings.', - empty: 'No data', - queued: 'Queue {count}', - rateLimited: 'Rate-limited {count}', - errorAccounts: 'Errors {count}', - loadFailed: 'Failed to load concurrency data' - }, - realtime: { - title: 'Realtime', - connected: 'Realtime connected', - connecting: 'Realtime connecting', - reconnecting: 'Realtime reconnecting', - offline: 'Realtime offline', - closed: 'Realtime closed', - reconnectIn: 'retry in {seconds}s' - }, - queryMode: { - auto: 'Auto', - raw: 'Raw', - preagg: 'Preagg' - }, - accountAvailability: { - available: 'Available', - unavailable: 'Unavailable', - accountError: 'Error' - }, - tooltips: { - totalRequests: 'Total number of requests (including both successful and failed requests) in the selected time window.', - throughputTrend: 'Requests/QPS + Tokens/TPS in the selected window.', - switchRateTrend: 'Trend of account switches / total requests over the last 5 hours (avg switches).', - latencyHistogram: 'Request duration distribution (ms) for successful requests.', - errorTrend: 'Error counts over time (SLA scope excludes business limits; upstream excludes 429/529).', - errorDistribution: 'Error distribution by status code (SLA scope, excluding business limits).', - goroutines: - 'Number of Go runtime goroutines (lightweight threads). There is no absolute "safe" number—use your historical baseline. Heuristic: <2k is common; 2k–8k watch; >8k plus rising queue/latency often suggests blocking/leaks.', - cpu: 'CPU usage percentage, showing system processor load.', - memory: 'Memory usage, including used and total available memory.', - db: 'Database connection pool status, including active, idle, and waiting connections.', - redis: 'Redis connection pool status, showing active and idle connections.', - jobs: 'Background job execution status, including last run time, success time, and error information.', - qps: 'Queries Per Second (QPS) and Tokens Per Second (TPS), real-time system throughput.', - tokens: 'Total number of tokens processed in the current time window.', - sla: 'Service Level Agreement success rate, excluding business limits (e.g., insufficient balance, quota exceeded).', - errors: 'Error statistics, including total errors, error rate, and upstream error rate.', - upstreamErrors: 'Upstream error statistics, excluding rate limit errors (429/529).', - latency: 'Request duration statistics, including p50, p90, p95, p99 percentiles.', - ttft: 'Time To First Token, measuring the speed of first token return in streaming responses.', - health: 'System health score (0-100), considering SLA, error rate, and resource usage.' - }, - charts: { - emptyRequest: 'No requests in this window.', - emptyError: 'No errors in this window.', - resetZoom: 'Reset', - resetZoomHint: 'Reset zoom (if enabled)', - downloadChart: 'Download', - downloadChartHint: 'Download chart as image' - } - }, - - // Settings - settings: { - title: 'System Settings', - description: 'Manage registration, email verification, default values, and SMTP settings', - tabs: { - general: 'General', - agreement: 'Agreement', - features: 'Feature Switches', - security: 'Security', - users: 'Users', - gateway: 'Gateway', - email: 'Email', - backup: 'Backup', - payment: 'Payment', - }, - features: { - channelMonitor: { - title: 'Channel Monitor', - description: 'Periodically probe configured channels and surface availability / latency to users. Turning it off stops the scheduler and returns an empty list on the user page.', - configureLink: 'Configure monitors in Channel Management > Channel Monitor', - enabled: 'Enable Channel Monitor', - enabledHint: 'Disabling stops background checks; existing history is preserved.', - defaultInterval: 'Default check interval (seconds)', - defaultIntervalHint: 'Pre-fills the interval when creating a new monitor; each monitor can override it. Range 15 – 3600.', - }, - availableChannels: { - title: 'Available Channels', - description: 'Show logged-in users an aggregate view of the channels, models and pricing they can access. Disabled by default.', - configureLink: 'Configure model pricing in Channel Management > Channel Pricing', - enabled: 'Enable Available Channels', - enabledHint: 'When off, the sidebar entry is hidden and the endpoint returns an empty list.', - }, - riskControl: { - title: 'Risk Control', - description: 'Enable the content moderation menu and gateway audit entry point. Disabled by default.', - configureLink: 'Configure content moderation in Risk Control', - enabled: 'Enable Risk Control', - enabledHint: 'When off, the admin sidebar entry is hidden and gateway moderation is skipped.', - cyberSessionBlock: 'Cyber session auto-block', - cyberSessionBlockHint: 'When enabled, sessions hit by upstream cyber_policy are blocked locally for the TTL and no longer forwarded. Only the offending session is blocked; other sessions on the same key are unaffected.', - cyberSessionBlockTTL: 'Block TTL (seconds)', - }, - affiliate: { - title: 'Affiliate (Invite Rebate)', - description: 'Existing users invite new ones; the inviter earns a percentage rebate on the invitee’s recharges. Disabled by default.', - enabled: 'Enable Affiliate', - enabledHint: 'When off, the affiliate menu is hidden, the aff parameter is ignored at signup, and new recharges generate no rebate. Existing rebate balances can still be transferred.', - rebateRate: 'Global Rebate Rate', - rebateRateHint: 'Default percentage given back to the inviter on recharges (0-100, e.g. 10 = 10%).', - freezeHours: 'Rebate Freeze Period (hours)', - freezeHoursDesc: 'New rebates will be frozen for this period before becoming available for withdrawal. 0 = no freeze.', - durationDays: 'Rebate Duration (days)', - durationDaysDesc: 'Rebate relationship expires after this many days since invitee registration. 0 = permanent.', - perInviteeCap: 'Per-Invitee Rebate Cap', - perInviteeCapDesc: 'Maximum total rebate from a single invitee. 0 = no limit.', - customUsers: { - title: 'Per-User Overrides', - description: 'Set a custom invite code or exclusive rebate rate for specific users. Lists only users that have an override applied.', - addButton: 'Add Custom User', - searchPlaceholder: 'Search by email or username', - batchButton: 'Batch Set Rate ({count} selected)', - empty: 'No users with custom affiliate settings yet', - customBadge: 'custom', - useGlobal: 'use global', - resetTitle: 'Reset Custom Settings', - resetMessage: 'Reset all custom settings for {email}?\n• The exclusive rebate rate will be cleared (fall back to the global rate)\n• The invite code will be regenerated as a new system code (previously shared links will stop working)', - totalLabel: '{total} total', - col: { - email: 'Email', - username: 'Username', - code: 'Invite Code', - rate: 'Custom Rate', - actions: 'Actions', - }, - }, - modal: { - addTitle: 'Add Custom User', - editTitle: 'Edit Custom Settings', - userLabel: 'User', - userPlaceholder: 'Search by email or username', - changeUser: 'Change user', - codeLabel: 'Custom Invite Code (optional)', - codePlaceholder: 'e.g. VIP2026', - codeHint: '4-32 characters; A-Z, 0-9, underscore, dash. Leave empty to keep current. Input is upper-cased.', - rateLabel: 'Exclusive Rebate Rate (optional)', - ratePlaceholder: 'e.g. 30', - rateHint: '0-100. Leave empty (in edit mode) to clear and fall back to the global rate.', - errorBadRate: 'Please enter a number between 0 and 100', - errorEmpty: 'Fill at least one: custom invite code or exclusive rebate rate', - }, - batchModal: { - title: 'Batch Set Rate ({count} users selected)', - hint: 'Apply the same exclusive rebate rate to all selected users.', - placeholder: 'e.g. 30', - clearHint: 'Submitting empty will clear the exclusive rate for selected users.', - }, - }, - }, - emailTabDisabledTitle: 'Email Verification Not Enabled', - emailTabDisabledHint: 'Enable email verification in the Security tab to configure SMTP settings.', - registration: { - title: 'Registration Settings', - description: 'Control user registration and verification', - enableRegistration: 'Enable Registration', - enableRegistrationHint: 'Allow new users to register', - emailVerification: 'Email Verification', - emailVerificationHint: 'Require email verification for new registrations', - emailSuffixWhitelist: 'Email Domain Whitelist', - emailSuffixWhitelistHint: - "Only email addresses from the specified domains can register (for example, {'@'}qq.com, {'@'}gmail.com, *.edu.cn)", - emailSuffixWhitelistPlaceholder: "{'@'}example.com, *.edu.cn", - emailSuffixWhitelistInputHint: 'Leave empty for no restriction. Use *.edu.cn to match edu.cn and its subdomains.', - promoCode: 'Promo Code', - promoCodeHint: 'Allow users to use promo codes during registration', - invitationCode: 'Invitation Code Registration', - invitationCodeHint: 'When enabled, users must enter a valid invitation code to register', - passwordReset: 'Password Reset', - passwordResetHint: 'Allow users to reset their password via email', - frontendUrl: 'Frontend URL', - frontendUrlPlaceholder: 'https://example.com', - frontendUrlHint: 'Used to generate password reset links in emails. Example: https://example.com', - totp: 'Two-Factor Authentication (2FA)', - totpHint: 'Allow users to use authenticator apps like Google Authenticator', - totpKeyNotConfigured: - 'Please configure TOTP_ENCRYPTION_KEY in environment variables first. Generate a key with: openssl rand -hex 32' - }, - turnstile: { - title: 'Cloudflare Turnstile', - description: 'Bot protection for login and registration', - enableTurnstile: 'Enable Turnstile', - enableTurnstileHint: 'Require Cloudflare Turnstile verification', - siteKey: 'Site Key', - secretKey: 'Secret Key', - siteKeyHint: 'Get this from your Cloudflare Dashboard', - cloudflareDashboard: 'Cloudflare Dashboard', - secretKeyHint: 'Server-side verification key (keep this secret)', - secretKeyConfiguredHint: 'Secret key configured. Leave empty to keep the current value.' - }, - apiKeyAcl: { - title: 'API Key IP Access Control', - description: 'Choose which client IP is used by API Key allowlists and denylists', - trustForwardedIp: 'Trust forwarded client IP', - trustForwardedIpHint: - 'Disabled by default. Enable only when the origin is reachable only through Cloudflare or Nginx reverse proxy. When enabled, API Key IP allowlists and denylists use CF-Connecting-IP, X-Real-IP, or X-Forwarded-For, matching the request IP shown in usage records.' - }, - linuxdo: { - title: 'LinuxDo Connect Login', - description: 'Configure LinuxDo Connect OAuth for Sub2API end-user login', - enable: 'Enable LinuxDo Login', - enableHint: 'Show LinuxDo login on the login/register pages', - clientId: 'Client ID', - clientIdPlaceholder: 'e.g., hprJ5pC3...', - clientIdHint: 'Get this from Connect.Linux.Do', - clientSecret: 'Client Secret', - clientSecretPlaceholder: '********', - clientSecretHint: 'Used by backend to exchange tokens (keep it secret)', - clientSecretConfiguredPlaceholder: '********', - clientSecretConfiguredHint: 'Secret configured. Leave empty to keep the current value.', - redirectUrl: 'Redirect URL', - redirectUrlPlaceholder: 'https://your-domain.com/api/v1/auth/oauth/linuxdo/callback', - redirectUrlHint: - 'Must match the redirect URL configured in Connect.Linux.Do (must be an absolute http(s) URL)', - quickSetCopy: 'Generate & Copy (current site)', - redirectUrlSetAndCopied: 'Redirect URL generated and copied to clipboard' - }, - dingtalk: { - title: 'DingTalk Login', - description: 'Configure DingTalk OAuth for Sub2API end-user login', - enable: 'Enable DingTalk Login (Internal Corporate App)', - enableHint: 'Show DingTalk login on the login/register pages', - clientId: 'Client ID (AppKey)', - clientIdPlaceholder: 'e.g., dingxxxxxxxxxxxxxxxx', - clientIdHint: 'Get this from the DingTalk Open Platform app details', - clientSecret: 'Client Secret (AppSecret)', - clientSecretPlaceholder: '********', - clientSecretHint: 'Used by backend to exchange tokens (keep it secret)', - clientSecretConfiguredPlaceholder: '********', - clientSecretConfiguredHint: 'Secret configured. Leave empty to keep the current value.', - redirectUrl: 'Redirect URL', - redirectUrlPlaceholder: 'https://your-domain.com/api/v1/auth/oauth/dingtalk/callback', - redirectUrlHint: - 'Must match the redirect URL configured in DingTalk Open Platform (must be an absolute http(s) URL)', - corpPolicy: { - label: 'Corp Restriction Policy', - hint: 'Control which DingTalk accounts (orgs) are allowed to sign in', - none: 'No restriction (all DingTalk accounts allowed)', - internalOnly: 'Internal only (single corp)' - }, - bypassRegistration: 'Enable DingTalk signup', - bypassRegistrationHint: 'Allow new users to register via DingTalk even when public registration is disabled.', - syncDisplayName: 'Sync DingTalk display name', - syncDisplayNameHint: 'Overwrite username with the DingTalk staff name on each login (also stored in the dingtalk_name attribute).', - syncCorpEmail: 'Sync corporate email', - syncCorpEmailHint: 'Write the DingTalk corporate email to the dingtalk_email attribute on each login (does not change the login email).', - syncCorpEmailPermissionHint: 'Requires the OAPI permission "Personal info incl. email (fieldEmail)" to be granted to the app on the DingTalk open platform, otherwise OAPI will not return the email field.', - syncDept: 'Sync department', - syncDeptHint: 'Write the full DingTalk department path to the dingtalk_department attribute on each login (fetched live each time).', - syncDeptPermissionHint: 'Requires the OAPI "Department info read (qyapi_get_department_list)" permission to be granted to the app on the DingTalk open platform, otherwise the department path cannot be resolved.', - syncDisplayNameTarget: 'Attribute key', - syncDisplayNameTargetHint: 'Defaults to dingtalk_name / DingTalk Name. Saving settings auto-creates the user attribute by the key and display name above (existing definition only has its display name synced).', - syncCorpEmailTarget: 'Attribute key', - syncCorpEmailTargetHint: 'Defaults to dingtalk_email / DingTalk Corporate Email. Saving settings auto-creates the user attribute by the key and display name above (existing definition only has its display name synced).', - syncDeptTarget: 'Attribute key', - syncDeptTargetHint: 'Defaults to dingtalk_department / DingTalk Department. Saving settings auto-creates the user attribute by the key and display name above (existing definition only has its display name synced).', - syncAttrDisplayName: 'Display name' - }, - oidc: { - title: 'OIDC Login', - description: 'Configure a standard OIDC provider (for example Keycloak)', - enable: 'Enable OIDC Login', - enableHint: 'Show OIDC login on the login/register pages', - providerName: 'Provider Name', - providerNamePlaceholder: 'for example Keycloak', - clientId: 'Client ID', - clientIdPlaceholder: 'OIDC client id', - clientSecret: 'Client Secret', - clientSecretPlaceholder: '********', - clientSecretHint: 'Used by backend to exchange tokens (keep it secret)', - clientSecretConfiguredPlaceholder: '********', - clientSecretConfiguredHint: 'Secret configured. Leave empty to keep the current value.', - issuerUrl: 'Issuer URL', - issuerUrlPlaceholder: 'https://id.example.com/realms/main', - discoveryUrl: 'Discovery URL', - discoveryUrlPlaceholder: 'Optional, leave empty to auto-derive from issuer', - authorizeUrl: 'Authorize URL', - authorizeUrlPlaceholder: 'Optional, can be discovered automatically', - tokenUrl: 'Token URL', - tokenUrlPlaceholder: 'Optional, can be discovered automatically', - userinfoUrl: 'UserInfo URL', - userinfoUrlPlaceholder: 'Optional, can be discovered automatically', - jwksUrl: 'JWKS URL', - jwksUrlPlaceholder: 'Optional, required when strict ID token validation is enabled', - scopes: 'Scopes', - scopesPlaceholder: 'openid email profile', - scopesHint: 'Must include openid', - redirectUrl: 'Backend Redirect URL', - redirectUrlPlaceholder: 'https://your-domain.com/api/v1/auth/oauth/oidc/callback', - redirectUrlHint: 'Must match the callback URL configured in the OIDC provider', - quickSetCopy: 'Generate & Copy (current site)', - redirectUrlSetAndCopied: 'Redirect URL generated and copied to clipboard', - frontendRedirectUrl: 'Frontend Callback Path', - frontendRedirectUrlPlaceholder: '/auth/oidc/callback', - frontendRedirectUrlHint: 'Frontend route used after backend callback', - tokenAuthMethod: 'Token Auth Method', - clockSkewSeconds: 'Clock Skew (seconds)', - allowedSigningAlgs: 'Allowed Signing Algs', - allowedSigningAlgsPlaceholder: 'RS256,ES256,PS256', - usePkce: 'Use PKCE', - validateIdToken: 'Validate ID Token', - requireEmailVerified: 'Require Email Verified', - userinfoEmailPath: 'UserInfo Email Path', - userinfoEmailPathPlaceholder: 'for example data.email', - userinfoIdPath: 'UserInfo ID Path', - userinfoIdPathPlaceholder: 'for example data.id', - userinfoUsernamePath: 'UserInfo Username Path', - userinfoUsernamePathPlaceholder: 'for example data.username' - }, - defaults: { - title: 'Default User Settings', - description: 'Default values for new users', - defaultBalance: 'Default Balance', - defaultBalanceHint: 'Initial balance for new users', - affiliateRebateRate: 'Affiliate Rebate Rate', - affiliateRebateRateHint: - 'Rebate percentage credited to inviter after recharge (0-100%, e.g. 10 means 10%)', - defaultConcurrency: 'Default Concurrency', - defaultConcurrencyHint: 'Maximum concurrent requests for new users', - defaultUserRpmLimit: 'Default User RPM Limit', - defaultUserRpmLimitHint: 'Default max requests per minute for new users; 0 = unlimited. Only applied at new user creation.', - defaultSubscriptions: 'Default Subscriptions', - defaultSubscriptionsHint: 'Auto-assign these subscriptions when a new user is created or registered', - addDefaultSubscription: 'Add Default Subscription', - defaultSubscriptionsEmpty: 'No default subscriptions configured.', - defaultSubscriptionsDuplicate: - 'Duplicate subscription group: {groupId}. Each group can only appear once.', - subscriptionGroup: 'Subscription Group', - subscriptionValidityDays: 'Validity (days)', - defaultPlatformQuotas: 'Default Platform Quotas (on signup)', - defaultPlatformQuotasHint: 'Automatically assigned to new users on signup; existing users are not affected. Leave blank = unlimited.', - platformQuotaNotice: 'Monthly quota uses a 30-day rolling window, not a calendar month.', - }, - platformQuota: { - platform: 'Platform', - daily: 'Daily (USD)', - weekly: 'Weekly (USD)', - monthly: 'Monthly (USD, 30d rolling)', - placeholder: 'Unlimited', - }, - claudeCode: { - title: 'Claude Code Settings', - description: 'Control Claude Code client access requirements', - minVersion: 'Minimum Version', - minVersionPlaceholder: 'e.g. 2.1.63', - minVersionHint: - 'Reject Claude Code clients below this version (semver format). Leave empty to disable version check.', - maxVersion: 'Maximum Version', - maxVersionPlaceholder: 'e.g. 2.5.0', - maxVersionHint: - 'Reject Claude Code clients above this version (semver format). Leave empty to allow any version.' - }, - scheduling: { - title: 'Gateway Scheduling Settings', - description: 'Control API Key scheduling behavior', - allowUngroupedKey: 'Allow Ungrouped Key Scheduling', - allowUngroupedKeyHint: 'When disabled, API Keys not assigned to any group cannot make requests (403 Forbidden). Keep disabled to ensure all Keys belong to a specific group.' - }, - gatewayForwarding: { - title: 'Request Forwarding', - description: 'Control how requests are forwarded to upstream OAuth accounts', - fingerprintUnification: 'Fingerprint Unification', - fingerprintUnificationHint: 'Unify X-Stainless-* headers across users sharing the same OAuth account. Disabling passes through each client\'s original headers.', - metadataPassthrough: 'Metadata Passthrough', - metadataPassthroughHint: 'Pass through client\'s original metadata.user_id without rewriting. May improve upstream cache hit rates.', - cchSigning: 'CCH Signing', - cchSigningHint: 'Sign the billing header in forwarded requests with CCH hash. When disabled, the placeholder is preserved.', - claudeOAuthSystemPromptInjection: 'Claude OAuth System Blocks', - claudeOAuthSystemPromptInjectionHint: 'Inject Claude Code-like system blocks for Claude OAuth requests from non-Claude-Code clients. Enabled by default.', - claudeOAuthSystemPrompt: 'Claude OAuth Expansion Prompt', - claudeOAuthSystemPromptPlaceholder: 'Leave empty to use the built-in Claude Code expansion prompt.', - claudeOAuthSystemPromptHint: 'Legacy compatibility: controls only the third injected system block.', - claudeOAuthSystemPromptBlocks: 'Claude OAuth System Blocks', - claudeOAuthSystemPromptBlocksPlaceholder: 'Leave empty to use the built-in 3 blocks. Supports an array or {"blocks": [...]}.', - claudeOAuthSystemPromptBlocksHint: 'Each block is saved as JSON with enabled, type, text, and optional cache_control. {billing_header} stays dynamic per request; the Claude Code identity and expansion prompts can be edited directly or restored from presets.', - systemBlockTitle: 'System Block {index}', - systemBlockPreset: 'Preset', - systemBlockPresetBilling: 'Billing header', - systemBlockPresetIdentity: 'Claude Code identity', - systemBlockPresetExpansion: 'Claude Code expansion', - systemBlockPresetCustom: 'Custom', - systemBlockType: 'Type', - systemBlockTypeText: 'Text', - systemBlockText: 'Content', - systemBlockCacheControl: 'Cache control', - systemBlockHide: 'Hide block details', - systemBlockShow: 'Show block details', - addSystemBlock: 'Add block', - resetSystemBlocks: 'Reset defaults', - cacheTTL5m: '5 minutes', - cacheTTL1h: '1 hour', - anthropicCacheTTL1hInjection: 'Anthropic Cache TTL Injection', - anthropicCacheTTL1hInjectionHint: 'When enabled, existing ephemeral cache_control blocks in Anthropic OAuth/Setup Token request bodies are forced to 1h; response usage is billed back as 5m by default, with account-level TTL billing override taking priority.', - rewriteMessageCacheControl: 'Rewrite Message Cache Breakpoints', - rewriteMessageCacheControlHint: 'Default off: preserve client cache_control on message content blocks. When enabled, client breakpoints are stripped and proxy breakpoints are injected for clients that do not manage caching themselves.', - clientDatelineNormalization: 'Client Dateline Normalization', - clientDatelineNormalizationHint: 'Default on. Rewrites the "Today\'s date is …" sentence in Anthropic OAuth/Setup Token requests back to a canonical ASCII apostrophe and hyphen date format, erasing steganographic fingerprint bits some clients inject when they detect a non-official base URL. Applies to system prompts and blocks only; API-Key accounts are unaffected.', - antigravityUserAgentVersion: 'Antigravity UA Version', - antigravityUserAgentVersionPlaceholder: '1.23.2', - antigravityUserAgentVersionHint: 'Leave empty to use ANTIGRAVITY_USER_AGENT_VERSION or the built-in default 1.23.2; when set, the admin setting takes precedence.', - openaiCodexUserAgent: 'OpenAI Codex UA', - openaiCodexUserAgentPlaceholder: 'codex-tui/0.125.0 (Ubuntu 22.4.0; x86_64) xterm-256color (codex-tui; 0.125.0)', - openaiCodexUserAgentHint: 'Used to bypass Cloudflare browser-UA challenges on the OpenAI upstream. Only applies when the client User-Agent is detected as a browser (Mozilla/...). Leave empty to use the built-in default.', - codexHardeningTitle: "Codex Settings", - codexClientRestrictionTitle: "Codex client restriction", - codexHardeningDesc: - "Only affects OpenAI OAuth accounts with 'Codex official clients only' enabled (global). Beyond User-Agent/Originator, harden the decision with a version range, an engine-fingerprint gate, and black/whitelists.", - minCodexVersion: "Min Codex Version", - minCodexVersionPlaceholder: "e.g. 0.142.0", - maxCodexVersion: "Max Codex Version", - maxCodexVersionPlaceholder: "e.g. 0.200.0", - codexVersionHint: - "Official clients only: checks their version against the [min, max] range. Leave a side empty to not limit it.", - codexFingerprintSignals: "Codex engine fingerprint signals", - codexFingerprintSignalsDesc: - "Define engine-fingerprint signals: every Required signal must match (AND); within a row, '/'-separated variants are OR'd. None checked = not enforced. Default checks only the x-codex- prefix. Types: header exact / header prefix / body path.", - codexFpTypeHeaderExact: "Header exact", - codexFpTypeHeaderPrefix: "Header prefix", - codexFpTypeBodyPath: "Body path", - codexFpMatchPlaceholder: "match; '/'-separate variants (e.g. session-id / session_id or x-codex-)", - codexFpRequired: "Required", - codexFingerprintNoRequiredWarn: "No signal is marked Required — the engine-fingerprint gate is inactive, allowing every candidate that passes identity/version. Check at least one signal to enable it.", - codexAllowAppServer: "Codex app-server", - codexAllowAppServerDesc: - "Allow third-party clients that embed the Codex engine and connect over the app-server protocol (e.g. Claude Code's codex plugin). Off by default; when on, such clients are allowed once they pass the engine-fingerprint gate (the signal list below); off = only official clients and the whitelist are allowed.", - codexBlacklist: "User-Agent/Originator Blacklist", - codexBlacklistDesc: - "Deny if any field matches; takes precedence over any allow. originator is exact; User-Agent is a 'contains' match (comma-separated).", - codexWhitelist: "User-Agent/Originator Whitelist", - codexWhitelistDesc: - "Allow clients outside the official set: requires exact originator and every User-Agent marker present. Still subject to the fingerprint gate unless 'Skip engine fingerprint' is checked.", - codexWhitelistSkipFingerprint: "Skip engine fingerprint", - codexWhitelistSkipFingerprintTooltip: - "Risk: when checked this entry is allowed on originator + User-Agent alone (both forgeable), with no engine-fingerprint backstop. Use only for trusted third-party clients that genuinely do not send a codex engine fingerprint.", - codexOriginatorPlaceholder: "originator (exact, e.g. opencode)", - codexUaContainsPlaceholder: "User-Agent contains markers, comma-separated (e.g. opencode/)", - codexAddRow: "Add entry", - codexRemoveRow: "Remove", - }, - webSearchEmulation: { - title: 'Web Search Emulation', - description: 'Inject web search capability for Anthropic API Key accounts that don\'t natively support it', - enabled: 'Enable Web Search Emulation', - enabledHint: 'Global switch. When disabled, web search emulation is inactive for all channels and accounts.', - providers: 'Search Providers', - addProvider: 'Add Provider', - providerType: 'Provider Type', - apiKey: 'API Key', - apiKeyPlaceholder: 'Enter API Key', - apiKeyConfigured: 'Configured', - showApiKey: 'Show', - hideApiKey: 'Hide', - copyApiKey: 'Copy', - copied: 'Copied', - quotaLimit: 'Quota Limit', - quotaLimitHint: 'Leave empty for unlimited; must be > 0 if set', - quotaLimitMustBePositive: 'Quota limit must be greater than 0', - subscribedAt: 'Subscribed At', - subscribedAtHint: 'Quota resets monthly from this date; leave empty to disable auto-reset', - quotaUsage: 'Usage', - resetUsage: 'Reset', - resetUsageConfirm: 'Reset usage counter for this provider?', - resetUsageSuccess: 'Usage counter reset', - proxy: 'Proxy', - removeProvider: 'Remove', - noProviders: 'No search providers configured', - test: 'Test', - testDefaultQuery: 'Major world events this year', - testing: 'Searching...', - testResultTitle: 'Search Results', - testResultProvider: 'Provider', - testNoResults: 'No results found', - }, - site: { - title: 'Site Settings', - description: 'Customize site branding', - backendMode: 'Backend Mode', - backendModeDescription: - 'Disables user registration, public site, and self-service features. Only admin can log in and manage the platform.', - siteName: 'Site Name', - siteNamePlaceholder: 'Sub2API', - siteNameHint: 'Displayed in emails and page titles', - siteSubtitle: 'Site Subtitle', - siteSubtitlePlaceholder: 'Subscription to API Conversion Platform', - siteSubtitleHint: 'Displayed on login and register pages', - apiBaseUrl: 'API Base URL', - apiBaseUrlPlaceholder: 'https://api.example.com', - apiBaseUrlHint: - 'Used for "Use Key", "Import to CC Switch", and callback URL suggestions. Leave empty to use current site URL.', - tablePreferencesTitle: 'Global Table Preferences', - tablePreferencesDescription: 'Configure default pagination behavior for shared table components', - tableDefaultPageSize: 'Default Rows Per Page', - tableDefaultPageSizeHint: 'Must be an integer between 5 and 1000', - tablePageSizeOptions: 'Rows Per Page Options', - tablePageSizeOptionsPlaceholder: '10, 20, 50, 100', - tablePageSizeOptionsHint: 'Use commas to separate integers between 5 and 1000; values are deduplicated and sorted on save', - tableDefaultPageSizeRangeError: 'Default rows per page must be between {min} and {max}', - tablePageSizeOptionsFormatError: 'Invalid options format. Enter comma-separated integers between {min} and {max}', - customEndpoints: { - title: 'Custom Endpoints', - description: 'Add additional API endpoint URLs for users to quickly copy on the API Keys page', - itemLabel: 'Endpoint #{n}', - name: 'Name', - namePlaceholder: 'e.g., OpenAI Compatible', - endpointUrl: 'Endpoint URL', - endpointUrlPlaceholder: 'https://api2.example.com', - descriptionLabel: 'Description', - descriptionPlaceholder: 'e.g., Supports OpenAI format requests', - add: 'Add Endpoint', - }, - contactInfo: 'Contact Info', - contactInfoPlaceholder: 'e.g., QQ: 123456789', - contactInfoHint: 'Customer support contact info, displayed on redeem page, profile, etc.', - docUrl: 'Documentation URL', - docUrlPlaceholder: 'https://docs.example.com', - docUrlHint: 'Link to your documentation site. Leave empty to hide the documentation link.', - siteLogo: 'Site Logo', - uploadImage: 'Upload Image', - remove: 'Remove', - logoHint: 'PNG, JPG, or SVG. Max 300KB. Recommended: 80x80px square image.', - logoSizeError: 'Image size exceeds 300KB limit ({size}KB)', - logoTypeError: 'Please select an image file', - logoReadError: 'Failed to read the image file', - homeContent: 'Home Page Content', - homeContentPlaceholder: 'Enter custom content for the home page. Supports Markdown & HTML. If a URL is entered, it will be displayed as an iframe.', - homeContentHint: 'Customize the home page content. Supports Markdown/HTML. If you enter a URL (starting with http:// or https://), it will be used as an iframe src to embed an external page. When set, the default status information will no longer be displayed.', - homeContentIframeWarning: '⚠️ iframe mode note: Some websites have X-Frame-Options or CSP security policies that prevent embedding in iframes. If the page appears blank or shows an error, please verify the target website allows embedding, or consider using HTML mode to build your own content.', - hideCcsImportButton: 'Hide CCS Import Button', - hideCcsImportButtonHint: 'When enabled, the "Import to CCS" button will be hidden on the API Keys page' - }, - purchase: { - title: 'Recharge / Subscription Page', - description: 'Show a "Recharge / Subscription" entry in the sidebar and open the configured URL in an iframe', - enabled: 'Show Recharge / Subscription Entry', - enabledHint: 'Only shown in standard mode (not simple mode)', - url: 'Recharge / Subscription URL', - urlPlaceholder: 'https://example.com/purchase', - urlHint: 'Must be an absolute http(s) URL', - iframeWarning: - '⚠️ iframe note: Some websites block embedding via X-Frame-Options or CSP (frame-ancestors). If the page is blank, provide an "Open in new tab" alternative.', - integrationDoc: 'Payment Integration Docs', - integrationDocHint: 'Covers endpoint specs, idempotency semantics, and code samples' - }, - soraClient: { - title: 'Sora Client', - description: 'Control whether to show the Sora client entry in the sidebar', - enabled: 'Enable Sora Client', - enabledHint: 'When enabled, the Sora entry will be shown in the sidebar for users to access Sora features' - }, - customMenu: { - title: 'Custom Menu Pages', - description: 'Add custom iframe pages to the sidebar navigation. Each page can be visible to regular users or administrators.', - itemLabel: 'Menu Item #{n}', - name: 'Menu Name', - namePlaceholder: 'e.g. Help Center', - url: 'Page URL', - urlPlaceholder: 'https://example.com/page', - iconSvg: 'SVG Icon', - iconSvgPlaceholder: '...', - iconPreview: 'Icon Preview', - uploadSvg: 'Upload SVG', - removeSvg: 'Remove', - visibility: 'Visible To', - visibilityUser: 'Regular Users', - visibilityAdmin: 'Administrators', - add: 'Add Menu Item', - remove: 'Remove', - moveUp: 'Move Up', - moveDown: 'Move Down', - }, - payment: { - title: 'Payment Settings', - description: 'Configure payment system options', - configGuide: 'Configuration Guide', - enabled: 'Enable Payment', - enabledHint: 'Enable or disable the payment system', - enabledPaymentTypes: 'Enabled Providers', - enabledPaymentTypesHint: 'Disabling a provider will also disable its instances.', - findProvider: 'Looking for a suitable EasyPay provider?', - minAmount: 'Minimum Amount', - maxAmount: 'Maximum Amount', - dailyLimit: 'Daily Limit', - balanceRechargeMultiplier: 'Balance Recharge Multiplier', - balanceRechargeMultiplierHint: 'How many USD balance the user receives for each 1 CNY paid', - balanceRechargePreview: 'Preview: 1 CNY = {usd} USD', - subscriptionUsdToCnyRate: 'Subscription USD to CNY Rate', - subscriptionUsdToCnyRateHint: - 'CNY charged per 1 USD of plan price on CNY channels (e.g. 7.15). 0 or empty = disabled, plan price is charged as-is. When enabled, all plan prices must be set in USD', - subscriptionUsdToCnyRateDisabled: 'Disabled (price charged as-is)', - rechargeFeeRate: 'Recharge Fee Rate', - rechargeFeeRateHint: 'Percentage of service fee charged on top of recharge amount, 0 means no fee', - rechargeFeePreview: 'Preview: Recharge 100, fee {fee}', - orderTimeout: 'Order Timeout', - orderTimeoutHint: 'In minutes, minimum 1', - maxPendingOrders: 'Max Pending Orders', - cancelRateLimit: 'Limit Cancel Rate', - cancelRateLimitHint: 'When enabled, users who exceed the cancel limit within the time window cannot create new orders', - cancelRateLimitEvery: 'Every', - cancelRateLimitAllowMax: 'allow max', - cancelRateLimitTimes: 'cancels', - cancelRateLimitWindow: 'Window', - cancelRateLimitUnit: 'Unit', - cancelRateLimitMax: 'Max Cancels', - cancelRateLimitUnitMinute: 'Minutes', - cancelRateLimitUnitHour: 'Hours', - cancelRateLimitUnitDay: 'Days', - cancelRateLimitWindowMode: 'Window Mode', - cancelRateLimitWindowModeRolling: 'Rolling', - cancelRateLimitWindowModeFixed: 'Fixed', - alipayForceQRCode: 'Force Alipay QR Code', - alipayForceQRCodeHint: 'When enabled, mobile Alipay users always see a QR code instead of being redirected to the mobile payment page', - helpText: 'Help Text', - helpImageUrl: 'Help Image URL', - manageProviders: 'Manage Providers', - balancePaymentDisabled: 'Disable Balance Recharge', - noLimit: 'Empty = no limit', - helpImage: 'Help Image', - helpImagePlaceholder: 'Upload or enter image URL', - helpTextPlaceholder: 'Enter help text...', - providerEasypay: 'EasyPay', - providerAlipay: 'Alipay (Direct)', - providerWxpay: 'WeChat Pay (Direct)', - providerStripe: 'Stripe', - providerAirwallex: 'Airwallex', - typeDisabled: 'type disabled', - enableTypesFirst: 'Enable at least one payment type above first', - easypayRedirect: 'Redirect', - paymentMode: 'Payment Mode', - modeRedirect: 'Redirect', - modeQRCode: 'QR Code', - modePopup: 'Popup', - validationNameRequired: 'Provider name is required', - validationTypesRequired: 'Please select at least one supported payment type', - validationFieldRequired: '{field} is required', - validationEasyPayCustomMethodRequired: 'Each custom EasyPay method requires both a payment type and an upstream type', - validationEasyPayCustomMethodTypeInvalid: 'Custom EasyPay payment types may only contain lowercase letters, digits, underscores, and hyphens', - validationEasyPayCustomMethodUpstreamTypeInvalid: 'EasyPay upstream types may only contain lowercase letters, digits, underscores, and hyphens', - validationEasyPayCustomMethodReserved: 'Custom EasyPay payment types cannot use built-in alipay or wxpay', - validationEasyPayCustomMethodPrefixReserved: 'Custom EasyPay payment types cannot start with alipay or wxpay', - validationEasyPayCustomMethodDuplicate: 'Custom EasyPay payment types must be unique', - field_apiBase: 'API Base URL', - field_notifyUrl: 'Notify URL', - field_returnUrl: 'Return URL', - callbackBaseUrl: 'Callback Base URL', - field_privateKey: 'Private Key', - field_publicKey: 'Public Key', - field_mpAppId: 'MP App ID', - field_mchId: 'Merchant ID', - field_apiV3Key: 'API v3 Key', - field_publicKeyId: 'Public Key ID', - field_certSerial: 'Certificate Serial', - field_h5AppName: 'H5 App Name', - field_h5AppUrl: 'H5 App URL', - wxpayConfigHint: 'WeChat Pay usually only needs App ID. Fill MP App ID, H5 App Name, and H5 App URL only when your Official Account or H5 flow specifically requires them.', - wxpayAdvancedOptions: 'WeChat Pay Advanced Options', - field_secretKey: 'Secret Key', - field_clientId: 'Client ID', - field_apiKey: 'API Key', - field_publishableKey: 'Publishable Key', - field_webhookSecret: 'Webhook Secret', - field_countryCode: 'Country/region code', - field_currency: 'Payment currency', - field_accountId: 'Airwallex Account ID', - field_airwallexApiBaseHint: 'Must match the API key environment: use https://api-demo.airwallex.com/api/v1 for sandbox/demo keys, and https://api.airwallex.com/api/v1 for production keys. Mixed environments return credentials_invalid / Access Denied.', - field_paymentCurrencyHint: 'Default is CNY. Stripe and Airwallex can choose HKD, USD, or another listed currency supported by the account; WeChat Pay, Alipay, and EasyPay remain CNY.', - field_accountIdHint: 'Leave this empty unless you use multiple accounts, an organization-level key, or connected-account payments. A single-account scoped API key uses the selected account by default.', - field_cid: 'Channel ID', - field_cidAlipay: 'Alipay Channel ID', - field_cidWxpay: 'WeChat Channel ID', - easypayCustomMethods: 'Custom EasyPay methods', - easypayCustomMethodsHint: 'Add provider-specific methods supported by this EasyPay endpoint. The payment type is stored on Sub2API orders; the upstream type is sent as EasyPay type.', - addCustomMethod: 'Add method', - customMethodType: 'Payment type', - customMethodUpstreamType: 'Upstream type', - customMethodDisplayName: 'Display name', - stripeWebhookHint: 'Configure the following URL as a Webhook endpoint in Stripe Dashboard:', - stripeWebhookApiVersionHint: 'Set this Webhook endpoint API version to match the integrated Stripe SDK. Recommended: {version}. A mismatch can cause webhook parsing errors.', - airwallexWebhookHint: 'Configure the following URL as a Webhook endpoint in Airwallex. Select at least Payment Intent -> Succeeded (payment_intent.succeeded), preferably also Payment Intent -> Cancelled (payment_intent.cancelled). Use the account default or latest stable API version.', - airwallexGuideSummary: 'When creating an Airwallex scoped API key, select Read and Write for Payment Acceptance under account-level permissions.', - airwallexGuideNote: 'Do not grant unrelated permissions such as Spend, Payouts, Transfers, Funds Splits, or POS Terminals unless you explicitly need them. For webhooks, select at least payment_intent.succeeded, preferably also payment_intent.cancelled, and use the account default or latest stable API version.', - limitsTitle: 'Limits', - limitSingleMin: 'Min per order', - limitSingleMax: 'Max per order', - limitDaily: 'Daily limit', - limitsHint: 'All empty = use global config; partially filled = empty means no limit', - limitsUseGlobal: 'Use global', - limitsNoLimit: 'No limit', - productNamePrefix: 'Product Name Prefix', - productNameSuffix: 'Product Name Suffix', - preview: 'Preview', - loadBalanceStrategy: 'Load Balance Strategy', - strategyRoundRobin: 'Round Robin', - strategyLeastAmount: 'Least Daily Amount', - providerManagement: 'Provider Management', - providerManagementDesc: 'Manage payment provider instances', - createProvider: 'Add Provider', - editProvider: 'Edit Provider', - deleteProvider: 'Delete Provider', - deleteProviderConfirm: 'Are you sure you want to delete this provider?', - providerName: 'Provider Name', - providerKey: 'Provider Type', - selectProviderKey: 'Select Provider Type', - providerConfig: 'Credentials', - paymentGuideTrigger: 'View payment guide', - guideOpenLabel: 'Enable: ', - guideCallLabel: 'Call: ', - guideFallbackLabel: 'Fallback: ', - alipayGuideSummary: 'Desktop prefers QR precreate and falls back to cashier; mobile prefers WAP checkout.', - alipayGuideFaceToFaceTitle: 'Face-to-face / QR Payment', - alipayGuideFaceToFaceOpen: 'Enable face-to-face or QR payment capability.', - alipayGuideFaceToFaceCall: 'Desktop orders call alipay.trade.precreate first and render the QR code directly.', - alipayGuideFaceToFaceFallback: 'If unavailable or failed, the flow falls back to website checkout automatically.', - alipayGuidePagePayTitle: 'Website Payment', - alipayGuidePagePayOpen: 'Enable website payment.', - alipayGuidePagePayCall: 'When face-to-face is unavailable on desktop, the flow calls alipay.trade.page.pay and still renders the returned link as a QR code.', - alipayGuidePagePayFallback: 'The cashier link stays available so users can reopen the checkout page manually.', - alipayGuideWapTitle: 'WAP Payment', - alipayGuideWapOpen: 'Enable mobile website payment.', - alipayGuideWapCall: 'Mobile orders call alipay.trade.wap.pay first and jump to Alipay checkout.', - alipayGuideWapFallback: 'If mobile payment is unavailable or fails, the frontend switches to QR payment and shows a notice.', - wxpayGuideSummary: 'Desktop prefers Native QR; mobile routes to JSAPI or H5 based on browser context.', - wxpayGuideNote: 'The current form defaults to one shared App ID, which fits the common single-subject web, mobile, and Official Account setup.', - wxpayGuideNativeTitle: 'Native / QR Payment', - wxpayGuideNativeOpen: 'Enable Native or QR payment capability.', - wxpayGuideNativeCall: 'Desktop orders use Native by default and the frontend renders the QR payload.', - wxpayGuideNativeFallback: 'Mobile flows also fall back here when JSAPI or H5 cannot be used.', - wxpayGuideJsapiTitle: 'JSAPI / Official Account', - wxpayGuideJsapiOpen: 'Enable Official Account payment and ensure the browser is inside WeChat with an available OpenID.', - wxpayGuideJsapiCall: 'Inside WeChat, the app calls JSAPI after authorization and launches WeChat Pay directly.', - wxpayGuideJsapiFallback: 'If configuration is missing, the bridge is unavailable, or launch fails, the flow falls back to QR payment.', - wxpayGuideH5Title: 'H5 Payment', - wxpayGuideH5Open: 'Enable H5 payment.', - wxpayGuideH5Call: 'On mobile browsers outside WeChat, the app calls H5 payment when a client IP is available.', - wxpayGuideH5Fallback: 'If H5 is unavailable or order creation fails, the flow falls back to QR payment.', - noProviders: 'No provider instances configured', - supportedTypes: 'Supported Payment Types', - supportedTypesHint: 'Comma-separated, e.g. alipay,wxpay', - refundEnabled: 'Allow Refund', - allowUserRefund: 'Allow User Refund', - enableConflict: '{method} already has an enabled provider instance: {provider}. Disable the existing instance before switching.', - }, - balanceNotify: { - title: 'Balance Low Notification', - description: 'Send email notification when user balance falls below threshold', - enabled: 'Enable Balance Low Notification', - threshold: 'Default Threshold', - thresholdHint: 'Used when user has not set a custom value', - thresholdPlaceholder: 'Enter amount', - rechargeUrl: 'Recharge Page URL', - rechargeUrlPlaceholder: 'https://example.com/payment', - rechargeUrlHint: 'A top-up button will appear in the email when set', - }, - quotaNotify: { - title: 'Account Quota Notification', - description: 'Notify admins when account quota usage reaches alert threshold', - enabled: 'Enable Account Quota Notification', - emails: 'Notification Emails', - emailsHint: 'Leave empty to disable notifications', - addEmail: 'Add Email', - emailPlaceholder: 'Enter email address', - }, - subscriptionExpiryNotify: { - title: 'Subscription Expiry Reminder', - description: 'Control whether users receive subscription expiry reminder emails.', - enabled: 'Enable Subscription Expiry Reminder', - enabledHint: 'When enabled, the system sends reminders 7, 3, and 1 day before expiry.' - }, - smtp: { - title: 'SMTP Settings', - description: 'Configure email sending for verification codes', - testConnection: 'Test Connection', - testing: 'Testing...', - host: 'SMTP Host', - hostPlaceholder: 'smtp.gmail.com', - port: 'SMTP Port', - portPlaceholder: '587', - username: 'SMTP Username', - usernamePlaceholder: "your-email{'@'}gmail.com", - password: 'SMTP Password', - passwordPlaceholder: '********', - passwordHint: 'Leave empty to keep existing password', - passwordConfiguredPlaceholder: '********', - passwordConfiguredHint: 'Password configured. Leave empty to keep the current value.', - fromEmail: 'From Email', - fromEmailPlaceholder: "noreply{'@'}example.com", - fromName: 'From Name', - fromNamePlaceholder: 'Sub2API', - useTls: 'Use TLS', - useTlsHint: 'Enable TLS encryption for SMTP connection' - }, - testEmail: { - title: 'Send Test Email', - description: 'Send a test email to verify your SMTP configuration', - recipientEmail: 'Recipient Email', - recipientEmailPlaceholder: "test{'@'}example.com", - sendTestEmail: 'Send Test Email', - sending: 'Sending...', - enterRecipientHint: 'Please enter a recipient email address' - }, - emailTemplates: { - title: 'Email Templates', - description: 'Customize notification email subjects and HTML content for each event and locale.', - event: 'Event', - locale: 'Locale', - localeEn: 'English', - localeZh: 'Chinese', - subject: 'Subject', - subjectPlaceholder: 'Enter the email subject', - html: 'HTML Template', - htmlPlaceholder: 'Edit the email HTML template', - placeholders: 'Available Placeholders', - placeholdersHelp: 'Click a placeholder to copy it. The backend replaces these values when sending emails.', - livePreview: 'Live Preview', - previewSecurityHint: 'Preview HTML is generated by the backend preview endpoint and displayed in a sandboxed iframe with scripts disabled.', - preview: 'Preview / Refresh', - previewing: 'Previewing...', - save: 'Save Template', - saving: 'Saving...', - restoreOfficial: 'Restore Official', - restoring: 'Restoring...', - restoreConfirm: 'Restore the official template for this event and locale? Your custom version will be replaced.', - restoreSuccess: 'Official template restored', - saveSuccess: 'Email template saved', - placeholderCopied: 'Placeholder copied', - validationRequired: 'Subject and HTML template are required', - empty: 'No email template events or locales are available yet.', - noPreview: 'Refresh the preview to see the rendered email subject.', - customized: 'Customized' - }, - opsMonitoring: { - title: 'Ops Monitoring', - description: 'Enable ops monitoring for troubleshooting and health visibility', - disabled: 'Ops monitoring is disabled', - enabled: 'Enable Ops Monitoring', - enabledHint: 'Enable the ops monitoring module (admin only)', - realtimeEnabled: 'Enable Realtime Monitoring', - realtimeEnabledHint: 'Enable realtime QPS/metrics push (WebSocket)', - queryMode: 'Default Query Mode', - queryModeHint: 'Default query mode for Ops Dashboard (auto/raw/preagg)', - queryModeAuto: 'Auto (recommended)', - queryModeRaw: 'Raw (most accurate, slower)', - queryModePreagg: 'Preagg (fastest, requires aggregation)', - metricsInterval: 'Metrics Collection Interval (seconds)', - metricsIntervalHint: 'How often to collect system/request metrics (60-3600 seconds)' - }, - adminApiKey: { - title: 'Admin API Key', - description: 'Global API key for external system integration with full admin access', - notConfigured: 'Admin API key not configured', - configured: 'Admin API key is active', - currentKey: 'Current Key', - regenerate: 'Regenerate', - regenerating: 'Regenerating...', - delete: 'Delete', - deleting: 'Deleting...', - create: 'Create Key', - creating: 'Creating...', - regenerateConfirm: 'Are you sure? The current key will be immediately invalidated.', - deleteConfirm: - 'Are you sure you want to delete the admin API key? External integrations will stop working.', - keyGenerated: 'New admin API key generated', - keyDeleted: 'Admin API key deleted', - copyKey: 'Copy Key', - keyCopied: 'Key copied to clipboard', - keyWarning: 'This key will only be shown once. Please copy it now.', - securityWarning: 'Warning: This key provides full admin access. Keep it secure.', - usage: 'Usage: Add to request header - x-api-key: ' - }, - soraS3: { - title: 'Sora Storage', - description: 'Manage Sora media storage profiles with S3 and Google Drive support', - newProfile: 'New Profile', - reloadProfiles: 'Reload Profiles', - empty: 'No storage profiles yet, create one first', - createTitle: 'Create Storage Profile', - editTitle: 'Edit Storage Profile', - selectProvider: 'Select Storage Type', - providerS3Desc: 'S3-compatible object storage', - providerGDriveDesc: 'Google Drive cloud storage', - profileID: 'Profile ID', - profileName: 'Profile Name', - setActive: 'Set as active after creation', - saveProfile: 'Save Profile', - activateProfile: 'Activate', - profileCreated: 'Storage profile created', - profileSaved: 'Storage profile saved', - profileDeleted: 'Storage profile deleted', - profileActivated: 'Active storage profile switched', - profileIDRequired: 'Profile ID is required', - profileNameRequired: 'Profile name is required', - profileSelectRequired: 'Please select a profile first', - endpointRequired: 'S3 endpoint is required when enabled', - bucketRequired: 'Bucket is required when enabled', - accessKeyRequired: 'Access Key ID is required when enabled', - deleteConfirm: 'Delete storage profile {profileID}?', - columns: { - profile: 'Profile', - profileId: 'Profile ID', - name: 'Name', - provider: 'Type', - active: 'Active', - endpoint: 'Endpoint', - bucket: 'Bucket', - storagePath: 'Storage Path', - capacityUsage: 'Capacity / Used', - capacityUnlimited: 'Unlimited', - videoCount: 'Videos', - videoCompleted: 'completed', - videoInProgress: 'in progress', - quota: 'Default Quota', - updatedAt: 'Updated At', - actions: 'Actions', - rootFolder: 'Root folder', - testInTable: 'Test', - testingInTable: 'Testing...', - testTimeout: 'Test timed out (15s)' - }, - enabled: 'Enable Storage', - enabledHint: 'When enabled, Sora generated media files will be automatically uploaded', - endpoint: 'S3 Endpoint', - region: 'Region', - bucket: 'Bucket', - prefix: 'Object Prefix', - accessKeyId: 'Access Key ID', - secretAccessKey: 'Secret Access Key', - secretConfigured: '(Configured, leave blank to keep)', - cdnUrl: 'CDN URL', - cdnUrlHint: 'Optional. When configured, files are accessed via CDN URL', - forcePathStyle: 'Force Path Style', - defaultQuota: 'Default Storage Quota', - defaultQuotaHint: 'Default quota when not specified at user or group level. 0 means unlimited', - testConnection: 'Test Connection', - testing: 'Testing...', - testSuccess: 'Connection test successful', - testFailed: 'Connection test failed', - saved: 'Storage settings saved successfully', - saveFailed: 'Failed to save storage settings', - gdrive: { - authType: 'Authentication Method', - serviceAccount: 'Service Account', - clientId: 'Client ID', - clientSecret: 'Client Secret', - clientSecretConfigured: '(Configured, leave blank to keep)', - refreshToken: 'Refresh Token', - refreshTokenConfigured: '(Configured, leave blank to keep)', - serviceAccountJson: 'Service Account JSON', - serviceAccountConfigured: '(Configured, leave blank to keep)', - folderId: 'Folder ID (optional)', - authorize: 'Authorize Google Drive', - authorizeHint: 'Get Refresh Token via OAuth2', - oauthFieldsRequired: 'Please fill in Client ID and Client Secret first', - oauthSuccess: 'Google Drive authorization successful', - oauthFailed: 'Google Drive authorization failed', - closeWindow: 'This window will close automatically', - processing: 'Processing authorization...', - testStorage: 'Test Storage', - testSuccess: 'Google Drive storage test passed (upload, access, delete all OK)', - testFailed: 'Google Drive storage test failed' - } - }, - overloadCooldown: { - title: '529 Overload Cooldown', - description: 'Configure account scheduling pause strategy when upstream returns 529 (overloaded)', - enabled: 'Enable Overload Cooldown', - enabledHint: 'Pause account scheduling on 529 errors, auto-recover after cooldown', - cooldownMinutes: 'Cooldown Duration (minutes)', - cooldownMinutesHint: 'Duration to pause account scheduling (1-120 minutes)', - saved: 'Overload cooldown settings saved', - saveFailed: 'Failed to save overload cooldown settings' - }, - rateLimit429Cooldown: { - title: '429 Default Cooldown', - description: 'Configure the default account cooldown when upstream returns 429 without an explicit reset time', - enabled: 'Enable 429 Default Cooldown', - enabledHint: 'Pause account scheduling when a 429 has no reset time, then auto-recover after cooldown', - cooldownSeconds: 'Cooldown Duration (seconds)', - cooldownSecondsHint: 'Default cooldown duration (1-7200 seconds); explicit upstream reset times still take precedence', - saved: '429 default cooldown settings saved', - saveFailed: 'Failed to save 429 default cooldown settings' - }, - streamTimeout: { - title: 'Stream Timeout Handling', - description: 'Configure account handling strategy when upstream response times out', - enabled: 'Enable Stream Timeout Handling', - enabledHint: 'Automatically handle problematic accounts when upstream times out', - timeoutSeconds: 'Timeout Threshold (seconds)', - timeoutSecondsHint: 'Stream data interval exceeding this time is considered timeout (30-300s)', - action: 'Action', - actionTempUnsched: 'Temporarily Unschedulable', - actionError: 'Mark as Error', - actionNone: 'No Action', - actionHint: 'Action to take on the account after timeout', - tempUnschedMinutes: 'Pause Duration (minutes)', - tempUnschedMinutesHint: 'Duration of temporary unschedulable state (1-60 minutes)', - thresholdCount: 'Trigger Threshold (count)', - thresholdCountHint: 'Number of timeouts before triggering action (1-10)', - thresholdWindowMinutes: 'Threshold Window (minutes)', - thresholdWindowMinutesHint: 'Time window for counting timeouts (1-60 minutes)', - saved: 'Stream timeout settings saved', - saveFailed: 'Failed to save stream timeout settings' - }, - rectifier: { - title: 'Request Rectifier', - description: 'Automatically fix request parameters and retry when upstream returns specific errors', - enabled: 'Enable Request Rectifier', - enabledHint: 'Master switch - disabling turns off all rectification features', - thinkingSignature: 'Thinking Signature Rectifier', - thinkingSignatureHint: 'Automatically strip signatures and retry when upstream returns thinking block signature validation errors', - thinkingBudget: 'Thinking Budget Rectifier', - thinkingBudgetHint: 'Automatically set budget to 32000 and retry when upstream returns budget_tokens constraint error (≥1024)', - apikeySignature: 'API Key Signature Rectifier', - apikeySignatureHint: - 'Automatically strip signatures and retry when API Key accounts receive signature-related errors (built-in patterns always apply)', - apikeyPatterns: 'Custom Match Patterns', - apikeyPatternsHint: - 'Additional keywords matched against the response body (case-insensitive). Built-in patterns always apply; use these for supplementary matching.', - apikeyPatternPlaceholder: 'e.g., thinking_error', - addPattern: 'Add Pattern', - saved: 'Rectifier settings saved', - saveFailed: 'Failed to save rectifier settings' - }, - betaPolicy: { - title: 'Beta Policy', - description: 'How to handle Beta features when configuring the forwarding of Anthropic API requests. Applicable only to the /v1/messages endpoint.', - action: 'Action', - actionPass: 'Pass (transparent)', - actionFilter: 'Filter (remove)', - actionBlock: 'Block (reject)', - scope: 'Scope', - scopeAll: 'All accounts', - scopeOAuth: 'OAuth only', - scopeAPIKey: 'API Key only', - scopeBedrock: 'Bedrock only', - errorMessage: 'Error message', - errorMessagePlaceholder: 'Custom error message when blocked', - errorMessageHint: 'Leave empty for default message', - saved: 'Beta policy settings saved', - saveFailed: 'Failed to save beta policy settings', - modelWhitelist: 'Model Whitelist', - modelWhitelistHint: 'Leave empty to apply to all models. Supports exact match and wildcard prefix (e.g., claude-opus-*)', - modelPatternPlaceholder: 'e.g., claude-opus-* or claude-opus-4-6', - addModelPattern: 'Add model pattern', - removePattern: 'Remove', - fallbackAction: 'Fallback Action', - fallbackActionHint: 'Action for models not matching the whitelist', - fallbackErrorMessagePlaceholder: 'Custom error message when non-whitelisted models are blocked', - quickPresets: 'Quick Presets', - presetOpusOnly: 'Opus only for 1M', - presetOpusOnlyDesc: 'Pass for Opus, filter others', - commonPatterns: 'Common patterns' - }, - openaiFastPolicy: { - title: 'OpenAI Fast/Flex Policy', - description: 'Intercept, filter, or pass OpenAI fast(priority) / flex requests based on the request body service_tier field. Applies to the OpenAI gateway only.', - empty: 'No rules configured. Click the button below to add one.', - ruleHeader: 'Rule #{index}', - removeRule: 'Remove rule', - addRule: 'Add rule', - saveHint: 'Saved together with system settings (click the global Save button at the bottom of the page).', - serviceTier: 'service_tier match', - tierAll: 'All tiers', - tierPriority: 'priority (fast)', - tierFlex: 'flex', - action: 'Action', - actionPass: 'Pass (keep service_tier)', - actionFilter: 'Filter (remove service_tier)', - actionForcePriority: 'Force priority (fast)', - actionBlock: 'Block (reject request)', - scope: 'Scope', - scopeAll: 'All accounts', - scopeOAuth: 'OAuth only', - scopeAPIKey: 'API Key only', - scopeBedrock: 'Bedrock only', - errorMessage: 'Error message', - errorMessagePlaceholder: 'Custom error message when blocked', - errorMessageHint: 'Leave empty for the default message.', - modelWhitelist: 'Model whitelist', - modelWhitelistHint: 'Leave empty to apply to all models. Supports exact match and wildcard prefix (e.g., gpt-5.5*).', - modelPatternPlaceholder: 'e.g., gpt-5.5 or gpt-5.5*', - addModelPattern: 'Add model pattern', - fallbackAction: 'Fallback action', - fallbackActionHint: 'Action for models not matching the whitelist.', - fallbackErrorMessagePlaceholder: 'Custom error message when non-whitelisted models are blocked' - }, - wechatConnect: { - title: 'WeChat Connect', - description: 'Third-party login configuration for WeChat Open Platform or Official Account / Mini Program.', - enabledLabel: 'Enable WeChat Connect', - enabledHint: 'Enable this to configure WeChat OAuth callbacks and authorization.', - appIdLabel: 'App ID', - appIdPlaceholder: 'WeChat App ID', - appSecretLabel: 'App Secret', - appSecretConfiguredPlaceholder: 'Secret configured. Leave empty to keep the current value.', - appSecretPlaceholder: 'WeChat App Secret', - appSecretConfiguredHint: 'Secret configured. Leave empty to keep the current value.', - appSecretHint: 'Enter a new secret to replace the current WeChat credential.', - modeLabel: 'Mode', - openModeLabel: 'Use Open outside WeChat', - openModeHint: 'Use Open Platform QR authorization outside the WeChat browser.', - mpModeLabel: 'Use MP inside WeChat', - mpModeHint: 'Use Official Account authorization inside the WeChat browser.', - redirectUrlLabel: 'Redirect URL', - redirectUrlPlaceholder: 'https://your-site.com/api/v1/auth/oauth/wechat/callback', - generateAndCopy: 'Generate & Copy (current site)', - redirectUrlSetAndCopied: 'Redirect URL generated and copied to clipboard', - frontendRedirectUrlLabel: 'Frontend redirect URL', - frontendRedirectUrlPlaceholder: '/auth/wechat/callback', - frontendRedirectUrlHint: 'Usually the frontend route callback path; keep it aligned with the backend.' - }, - authSourceDefaults: { - title: 'Auth Source Defaults', - description: 'Configure per-source default balance, concurrency, subscriptions, and grant rules.', - requireEmailLabel: 'Require email on third-party signup', - requireEmailHint: 'When enabled, Linux DO, OIDC, and WeChat signups must provide an email before account creation.', - enabledHint: 'These defaults apply when a new user registers through this source. Grant on first bind only applies when an existing user binds this source.', - sources: { - email: { - title: 'Email signup', - description: 'Default quota grants for email-password signups.' - }, - linuxdo: { - title: 'Linux DO signup', - description: 'Default quota grants for Linux DO signups.' - }, - oidc: { - title: 'OIDC signup', - description: 'Default quota grants for OIDC signups.' - }, - wechat: { - title: 'WeChat signup', - description: 'Default quota grants for WeChat signups.' - } - }, - grantOnFirstBindLabel: 'Grant on first bind', - grantOnFirstBindHint: 'Grant default entitlements when an existing user first binds this source.', - defaultSubscriptionsLabel: 'Default subscriptions', - defaultSubscriptionsHint: 'Applies only to this auth source. Leave empty to skip source-specific subscriptions.', - noSourceSubscriptions: 'No source-specific default subscriptions configured.', - platformQuotasOverride: 'Platform Quota Overrides', - platformQuotasOverrideHint: 'Blank fields inherit the system default. Set to 0 to fully block that window for this auth source.', - }, - paymentVisibleMethods: { - methodLabel: '{title} visible method', - methodHint: 'Controls whether checkout shows this method and which source key it exposes.', - sourceLabel: 'Payment source', - sourceHint: 'Choose an explicit source before enabling the method. Not configured methods are not exposed.', - sourceRequiredError: 'Select a payment source before enabling {title}.' - }, - openaiExperimentalScheduler: { - title: 'OpenAI experimental scheduler policy', - description: "Disabled by default. When enabled, this only changes the gateway's experimental account-selection policy for OpenAI traffic; it does not indicate an upstream OpenAI capability.", - stickyWeightedTitle: 'Sticky weighting', - stickyWeightedDescription: 'When enabled, previous_response_id and session_hash affinity are scored by the advanced scheduler. When disabled, sticky accounts keep the legacy hard-hit behavior.', - subscriptionPriorityTitle: 'Subscription priority', - subscriptionPriorityDescription: 'When enabled, the scheduler scores ChatGPT subscription accounts first and falls back to non-subscription accounts only if no subscription slot can be acquired.', - weightsTitle: 'Scheduler weight overrides', - weightsDescription: 'Blank values use config/environment values; when config is not set, built-in defaults apply. Non-blank page settings take priority.', - defaultPlaceholder: 'config/default: {value}', - topKLabel: 'TopK', - priorityWeight: 'Priority', - loadWeight: 'Load', - queueWeight: 'Queue', - errorRateWeight: 'Error rate', - ttftWeight: 'TTFT', - resetWeight: 'Reset window', - quotaHeadroomWeight: 'Quota headroom', - previousResponseWeight: 'previous_response sticky', - sessionStickyWeight: 'session_hash sticky' - }, - usageRecords: { - title: 'Usage Records', - description: 'Settings for usage and failed-request records visible to end users.', - }, - user_error_view: { - label: 'Allow users to view their own error requests', - description: 'When enabled, users can see a redacted view of their failed requests on the usage page (no internal/upstream details). Requires ops monitoring enabled to have data.', - }, - saveSettings: 'Save Settings', - saving: 'Saving...', - settingsSaved: 'Settings saved successfully', - smtpConnectionSuccess: 'SMTP connection successful', - testEmailSent: 'Test email sent successfully', - failedToLoad: 'Failed to load settings', - failedToSave: 'Failed to save settings', - failedToTestSmtp: 'SMTP connection test failed', - failedToSendTestEmail: 'Failed to send test email' - }, - - // Error Passthrough Rules - errorPassthrough: { - title: 'Error Passthrough Rules', - description: 'Configure how upstream errors are returned to clients', - createRule: 'Create Rule', - editRule: 'Edit Rule', - deleteRule: 'Delete Rule', - noRules: 'No rules configured', - createFirstRule: 'Create your first error passthrough rule', - allPlatforms: 'All Platforms', - passthrough: 'Passthrough', - custom: 'Custom', - code: 'Code', - body: 'Body', - skipMonitoring: 'Skip Monitoring', - - // Columns - columns: { - priority: 'Priority', - name: 'Name', - conditions: 'Conditions', - platforms: 'Platforms', - behavior: 'Behavior', - status: 'Status', - actions: 'Actions' - }, - - // Match Mode - matchMode: { - any: 'Code OR Keyword', - all: 'Code AND Keyword', - anyHint: 'Status code matches any error code, OR message contains any keyword', - allHint: 'Status code matches any error code, AND message contains any keyword' - }, - - // Form - form: { - name: 'Rule Name', - namePlaceholder: 'e.g., Context Limit Passthrough', - priority: 'Priority', - priorityHint: 'Lower values have higher priority', - description: 'Description', - descriptionPlaceholder: 'Describe the purpose of this rule...', - matchConditions: 'Match Conditions', - errorCodes: 'Error Codes', - errorCodesPlaceholder: '422, 400, 429', - errorCodesHint: 'Separate multiple codes with commas', - keywords: 'Keywords', - keywordsPlaceholder: 'One keyword per line\ncontext limit\nmodel not supported', - keywordsHint: 'One keyword per line, case-insensitive', - matchMode: 'Match Mode', - platforms: 'Platforms', - platformsHint: 'Leave empty to apply to all platforms', - responseBehavior: 'Response Behavior', - passthroughCode: 'Passthrough upstream status code', - responseCode: 'Custom status code', - passthroughBody: 'Passthrough upstream error message', - customMessage: 'Custom error message', - customMessagePlaceholder: 'Error message to return to client...', - skipMonitoring: 'Skip monitoring', - skipMonitoringHint: 'When enabled, errors matching this rule will not be recorded in ops monitoring', - enabled: 'Enable this rule' - }, - - // Messages - nameRequired: 'Please enter rule name', - conditionsRequired: 'Please configure at least one error code or keyword', - ruleCreated: 'Rule created successfully', - ruleUpdated: 'Rule updated successfully', - ruleDeleted: 'Rule deleted successfully', - deleteConfirm: 'Are you sure you want to delete rule "{name}"?', - failedToLoad: 'Failed to load rules', - failedToSave: 'Failed to save rule', - failedToDelete: 'Failed to delete rule', - failedToToggle: 'Failed to toggle status' - }, - - // TLS Fingerprint Profiles - tlsFingerprintProfiles: { - title: 'TLS Fingerprint Profiles', - description: 'Manage TLS fingerprint profiles for simulating specific client TLS handshake characteristics', - createProfile: 'Create Profile', - editProfile: 'Edit Profile', - deleteProfile: 'Delete Profile', - noProfiles: 'No profiles configured', - createFirstProfile: 'Create your first TLS fingerprint profile', - - columns: { - name: 'Name', - description: 'Description', - grease: 'GREASE', - alpn: 'ALPN', - actions: 'Actions' - }, - - form: { - pasteYaml: 'Paste YAML Configuration', - pasteYamlPlaceholder: 'Paste YAML output from TLS Fingerprint Collector here...', - pasteYamlHint: 'Paste the YAML copied from TLS Fingerprint Collector to auto-fill all fields.', - openCollector: 'Open Collector', - parseYaml: 'Parse YAML', - yamlParsed: 'YAML parsed successfully, fields auto-filled', - yamlParseFailed: 'Failed to parse YAML: name field not found', - name: 'Profile Name', - namePlaceholder: 'e.g. macOS Node.js v24', - description: 'Description', - descriptionPlaceholder: 'Optional description for this profile', - enableGrease: 'Enable GREASE', - enableGreaseHint: 'Insert GREASE values in TLS ClientHello extensions', - cipherSuites: 'Cipher Suites', - cipherSuitesHint: 'Comma-separated hex values, e.g. 0x1301, 0x1302, 0xc02c', - curves: 'Elliptic Curves', - curvesHint: 'Comma-separated curve IDs', - pointFormats: 'Point Formats', - signatureAlgorithms: 'Signature Algorithms', - alpnProtocols: 'ALPN Protocols', - alpnProtocolsHint: 'Comma-separated, e.g. h2, http/1.1', - supportedVersions: 'Supported TLS Versions', - keyShareGroups: 'Key Share Groups', - pskModes: 'PSK Modes', - extensions: 'Extensions' - }, - - deleteConfirm: 'Delete Profile', - deleteConfirmMessage: 'Are you sure you want to delete profile "{name}"? Accounts using this profile will fall back to the built-in default.', - createSuccess: 'Profile created successfully', - updateSuccess: 'Profile updated successfully', - deleteSuccess: 'Profile deleted successfully', - loadFailed: 'Failed to load profiles', - saveFailed: 'Failed to save profile', - deleteFailed: 'Failed to delete profile' - } - }, - - // Subscription Progress (Header component) - subscriptionProgress: { - title: 'My Subscriptions', - viewDetails: 'View subscription details', - activeCount: '{count} active subscription(s)', - daily: 'Daily', - weekly: 'Weekly', - monthly: 'Monthly', - daysRemaining: '{days} days left', - expired: 'Expired', - expiresToday: 'Expires today', - expiresTomorrow: 'Expires tomorrow', - viewAll: 'View all subscriptions', - noSubscriptions: 'No active subscriptions', - unlimited: 'Unlimited' - }, - - // Version Badge - version: { - currentVersion: 'Current Version', - latestVersion: 'Latest Version', - upToDate: "You're running the latest version.", - updateAvailable: 'A new version is available!', - releaseNotes: 'Release Notes', - noReleaseNotes: 'No release notes', - viewUpdate: 'View Update', - viewRelease: 'View Release', - viewChangelog: 'View Changelog', - refresh: 'Refresh', - sourceMode: 'Source Build', - sourceModeHint: 'Source build, use git pull to update', - updateNow: 'Update Now', - updating: 'Updating...', - updateComplete: 'Update Complete', - updateFailed: 'Update Failed', - restartRequired: 'Please restart the service to apply the update', - restartNow: 'Restart Now', - restarting: 'Restarting...', - retry: 'Retry' - }, - - // Recharge / Subscription Page - purchase: { - title: 'Recharge / Subscription', - description: 'Recharge balance or purchase subscription via the embedded page', - openInNewTab: 'Open in new tab', - notEnabledTitle: 'Feature not enabled', - notEnabledDesc: 'The administrator has not enabled the recharge/subscription entry. Please contact admin.', - notConfiguredTitle: 'Recharge / Subscription URL not configured', - notConfiguredDesc: - 'The administrator enabled the entry but has not configured a recharge/subscription URL. Please contact admin.' - }, - - // Custom Page (iframe embed) - customPage: { - title: 'Custom Page', - openInNewTab: 'Open in new tab', - notFoundTitle: 'Page not found', - notFoundDesc: 'This custom page does not exist or has been removed.', - notConfiguredTitle: 'Page URL not configured', - notConfiguredDesc: 'The URL for this custom page has not been properly configured.', - tableOfContents: 'Contents', - copyCode: 'Copy', - copiedCode: 'Copied', - copyCodeFailed: 'Failed' - }, - - // Announcements Page - announcements: { - title: 'Announcements', - description: 'View system announcements', - unreadOnly: 'Show unread only', - markRead: 'Mark as read', - markAllRead: 'Mark all as read', - viewAll: 'View all announcements', - markedAsRead: 'Marked as read', - allMarkedAsRead: 'All announcements marked as read', - newCount: '{count} new announcement | {count} new announcements', - readAt: 'Read at', - read: 'Read', - unread: 'Unread', - startsAt: 'Starts at', - endsAt: 'Ends at', - empty: 'No announcements', - emptyUnread: 'No unread announcements', - total: 'announcements', - emptyDescription: 'There are no system announcements at this time', - readStatus: 'You have read this announcement', - markReadHint: 'Click "Mark as read" to mark this announcement' - }, - - // User Subscriptions Page - userSubscriptions: { - title: 'My Subscriptions', - description: 'View your subscription plans and usage', - noActiveSubscriptions: 'No Active Subscriptions', - noActiveSubscriptionsDesc: - "You don't have any active subscriptions. Contact administrator to get one.", - failedToLoad: 'Failed to load subscriptions', - status: { - active: 'Active', - expired: 'Expired', - revoked: 'Revoked' - }, - usage: 'Usage', - expires: 'Expires', - noExpiration: 'No expiration', - unlimited: 'Unlimited', - unlimitedDesc: 'No usage limits on this subscription', - daily: 'Daily', - weekly: 'Weekly', - monthly: 'Monthly', - daysRemaining: '{days} days remaining', - expiresOn: 'Expires on {date}', - resetIn: 'Resets in {time}', - quotaEndsIn: 'Quota ends in {time}', - windowNotActive: 'Awaiting first use', - usageOf: '{used} of {limit}' - }, - - // Onboarding Tour - onboarding: { - restartTour: 'Restart Onboarding Tour', - dontShowAgain: "Don't show again", - dontShowAgainTitle: 'Permanently close onboarding guide', - confirmDontShow: "Are you sure you don't want to see the onboarding guide again?\n\nYou can restart it anytime from the user menu in the top right corner.", - confirmExit: 'Are you sure you want to exit the onboarding guide? You can restart it anytime from the top right menu.', - interactiveHint: 'Press Enter or Click to continue', - navigation: { - flipPage: 'Flip Page', - exit: 'Exit' - }, - // Admin tour steps - admin: { - welcome: { - title: '👋 Welcome to Sub2API', - description: '

Sub2API is a powerful AI service gateway platform that helps you easily manage and distribute AI services.

🎯 Core Features:

  • 📦 Group Management - Create service tiers (VIP, Free Trial, etc.)
  • 🔗 Account Pool - Connect multiple upstream AI service accounts
  • 🔑 Key Distribution - Generate independent API Keys for users
  • 💰 Billing Control - Flexible rate and quota management

Let\'s complete the initial setup in 3 minutes →

', - nextBtn: 'Start Setup 🚀', - prevBtn: 'Skip' - }, - groupManage: { - title: '📦 Step 1: Group Management', - description: '

What is a Group?

Groups are the core concept of Sub2API, like a "service package":

  • 🎯 Each group can contain multiple upstream accounts
  • 💰 Each group has independent billing multiplier
  • 👥 Can be set as public or exclusive

💡 Example: You can create "VIP Premium" (high rate) and "Free Trial" (low rate) groups

👉 Click "Group Management" on the left sidebar

' - }, - createGroup: { - title: '➕ Create New Group', - description: '

Let\'s create your first group.

📝 Tip: Recommend creating a test group first to familiarize yourself with the process

👉 Click the "Create Group" button

' - }, - groupName: { - title: '✏️ 1. Group Name', - description: '

Give your group an easy-to-identify name.

💡 Naming Suggestions:
  • "Test Group" - For testing
  • "VIP Premium" - High-quality service
  • "Free Trial" - Trial version

Click "Next" when done

', - nextBtn: 'Next' - }, - groupPlatform: { - title: '🤖 2. Select Platform', - description: '

Choose the AI platform this group supports.

📌 Platform Guide:
  • Anthropic - Claude models
  • OpenAI - GPT models
  • Google - Gemini models

One group can only have one platform

', - nextBtn: 'Next' - }, - groupMultiplier: { - title: '💰 3. Rate Multiplier', - description: '

Set the billing multiplier to control user charges.

⚙️ Billing Rules:
  • 1.0 - Original price (cost price)
  • 1.5 - User consumes $1, charged $1.5
  • 2.0 - User consumes $1, charged $2
  • 0.8 - Subsidy mode (loss-making)

Recommend setting test group to 1.0

', - nextBtn: 'Next' - }, - groupExclusive: { - title: '🔒 4. Exclusive Group (Optional)', - description: '

Control group visibility and access permissions.

🔐 Permission Guide:
  • Off - Public group, visible to all users
  • On - Exclusive group, only for specified users

💡 Use Cases: VIP exclusive, internal testing, special customers

', - nextBtn: 'Next' - }, - groupSubmit: { - title: '✅ Save Group', - description: '

Confirm the information and click create to save the group.

⚠️ Note: Platform type cannot be changed after creation, but other settings can be edited anytime

📌 Next Step: After creation, we\'ll add upstream accounts to this group

👉 Click "Create" button

' - }, - accountManage: { - title: '🔗 Step 2: Add Account', - description: '

Great! Group created successfully 🎉

Now add upstream AI service accounts to enable actual service delivery.

🔑 Account Purpose:
  • Connect to upstream AI services (Claude, GPT, etc.)
  • One group can contain multiple accounts (load balancing)
  • Supports OAuth and Session Key methods

👉 Click "Account Management" on the left sidebar

' - }, - createAccount: { - title: '➕ Add New Account', - description: '

Click the button to start adding your first upstream account.

💡 Tip: Recommend using OAuth method - more secure and no manual key extraction needed

👉 Click "Add Account" button

' - }, - accountName: { - title: '✏️ 1. Account Name', - description: '

Set an easy-to-identify name for the account.

💡 Naming Suggestions: "Claude Main", "GPT Backup 1", "Test Account", etc.

', - nextBtn: 'Next' - }, - accountPlatform: { - title: '🤖 2. Select Platform', - description: '

Choose the service provider platform for this account.

⚠️ Important: Platform must match the group you just created

', - nextBtn: 'Next' - }, - accountType: { - title: '🔐 3. Authorization Method', - description: '

Choose the account authorization method.

✅ Recommended: OAuth Method
  • No manual key extraction needed
  • More secure with auto-refresh support
  • Works with Claude Code, ChatGPT OAuth
📌 Session Key Method
  • Requires manual extraction from browser
  • May need periodic updates
  • For platforms without OAuth support
', - nextBtn: 'Next' - }, - accountPriority: { - title: '⚖️ 4. Priority (Optional)', - description: '

Set the account call priority.

📊 Priority Rules:
  • Lower number = higher priority
  • System uses low-value accounts first
  • Same priority = random selection

💡 Use Case: Set main account to lower value, backup accounts to higher value

', - nextBtn: 'Next' - }, - accountGroups: { - title: '🎯 5. Assign Groups', - description: '

Key Step! Assign the account to the group you just created.

⚠️ Important Reminder:
  • Must select at least one group
  • Unassigned accounts cannot be used
  • One account can be assigned to multiple groups

💡 Tip: Select the test group you just created

', - nextBtn: 'Next' - }, - accountSubmit: { - title: '✅ Save Account', - description: '

Confirm the information and click save.

📌 OAuth Flow:
  • Will redirect to service provider page after clicking save
  • Complete login and authorization on provider page
  • Auto-return after successful authorization

📌 Next Step: After adding account, we\'ll create an API key

👉 Click "Save" button

' - }, - keyManage: { - title: '🔑 Step 3: Generate Key', - description: '

Congratulations! Account setup complete 🎉

Final step: generate an API Key to test if the service works properly.

🔑 API Key Purpose:
  • Credential for calling AI services
  • Each key is bound to one group
  • Can set quota and expiration
  • Supports independent usage statistics

👉 Click "API Keys" on the left sidebar

' - }, - createKey: { - title: '➕ Create Key', - description: '

Click the button to create your first API Key.

💡 Tip: Copy and save immediately after creation - key is only shown once

👉 Click "Create Key" button

' - }, - keyName: { - title: '✏️ 1. Key Name', - description: '

Set an easy-to-manage name for the key.

💡 Naming Suggestions: "Test Key", "Production", "Mobile", etc.

', - nextBtn: 'Next' - }, - keyGroup: { - title: '🎯 2. Select Group', - description: '

Select the group you just configured.

📌 Group Determines:
  • Which accounts this key can use
  • What billing multiplier applies
  • Whether it\'s an exclusive key

💡 Tip: Select the test group you just created

', - nextBtn: 'Next' - }, - keySubmit: { - title: '🎉 Generate and Copy', - description: '

System will generate a complete API Key after clicking create.

⚠️ Important Reminder:
  • Key is only shown once, copy immediately
  • Need to regenerate if lost
  • Keep it safe, don\'t share with others
🚀 Next Steps:
  • Copy the generated sk-xxx key
  • Use in any OpenAI-compatible client
  • Start experiencing AI services!

👉 Click "Create" button

' - } - }, - // User tour steps - user: { - welcome: { - title: '👋 Welcome to Sub2API', - description: '

Hello! Welcome to the Sub2API AI service platform.

🎯 Quick Start:

  • 🔑 Create API Key
  • 📋 Copy key to your application
  • 🚀 Start using AI services

Just 1 minute, let\'s get started →

', - nextBtn: 'Start 🚀', - prevBtn: 'Skip' - }, - keyManage: { - title: '🔑 API Key Management', - description: '

Manage all your API access keys here.

📌 What is an API Key?
An API key is your credential for accessing AI services, like a key that allows your application to call AI capabilities.

👉 Click to enter key page

' - }, - createKey: { - title: '➕ Create New Key', - description: '

Click the button to create your first API key.

💡 Tip: Key is only shown once after creation, make sure to copy and save

👉 Click "Create Key"

' - }, - keyName: { - title: '✏️ Key Name', - description: '

Give your key an easy-to-identify name.

💡 Examples: "My First Key", "For Testing", etc.

', - nextBtn: 'Next' - }, - keyGroup: { - title: '🎯 Select Group', - description: '

Select the service group assigned by the administrator.

📌 Group Info:
Different groups may have different service quality and billing rates, choose according to your needs.

', - nextBtn: 'Next' - }, - keySubmit: { - title: '🎉 Complete Creation', - description: '

Click to confirm and create your API key.

⚠️ Important:
  • Copy the key (sk-xxx) immediately after creation
  • Key is only shown once, need to regenerate if lost

🚀 How to Use:
Configure the key in any OpenAI-compatible client (like ChatBox, OpenCat, etc.) and start using!

👉 Click "Create" button

' - } - } - }, - - // Payment System - payment: { - title: 'Recharge / Subscription', - amountLabel: 'Amount', - paymentAmount: 'Payment Amount', - creditedBalance: 'Credited Balance', - quickAmounts: 'Quick Amounts', - customAmount: 'Custom Amount', - enterAmount: 'Enter amount', - paymentMethod: 'Payment Method', - fee: 'Fee', - actualPay: 'Actual Payment', - createOrder: 'Confirm Payment', - methods: { - easypay: 'EasyPay', - alipay: 'Alipay', - wxpay: 'WeChat Pay', - stripe: 'Stripe', - airwallex: 'Airwallex', - card: 'Card', - link: 'Link', - alipay_direct: 'Alipay (Direct)', - wxpay_direct: 'WeChat Pay (Direct)', - }, - status: { - pending: 'Pending', - paid: 'Paid', - recharging: 'Recharging', - completed: 'Completed', - expired: 'Expired', - cancelled: 'Cancelled', - failed: 'Failed', - refund_requested: 'Refund Requested', - refunding: 'Refunding', - refund_pending: 'Refund Pending', - refunded: 'Refunded', - partially_refunded: 'Partially Refunded', - refund_failed: 'Refund Failed', - }, - qr: { - scanToPay: 'Scan to Pay', - scanAlipay: 'Alipay QR Payment', - scanWxpay: 'WeChat QR Payment', - scanAlipayHint: 'Open Alipay on your phone and scan the QR code to pay', - scanWxpayHint: 'Open WeChat on your phone and scan the QR code to pay', - payInNewWindow: 'Complete Payment in New Window', - payInNewWindowHint: 'The payment page has opened in a new window. Please complete the payment there and return to this page.', - openPayWindow: 'Reopen Payment Page', - expiresIn: 'Expires in', - expired: 'Order Expired', - expiredDesc: 'This order has expired. Please create a new one.', - cancelled: 'Order Cancelled', - cancelledDesc: 'You have cancelled this payment.', - waitingPayment: 'Waiting for payment...', - cancelOrder: 'Cancel Order', - }, - orders: { - title: 'My Orders', - empty: 'No orders yet', - orderId: 'Order ID', - orderNo: 'Order No.', - amount: 'Amount', - payAmount: 'Paid', - creditedAmount: 'Credited Amount', - fee: 'Fee', - baseAmount: 'Base Amount', - includedInPayAmount: 'included in paid amount', - status: 'Status', - paymentMethod: 'Payment Method', - createdAt: 'Created', - cancel: 'Cancel Order', - userId: 'User ID', - orderType: 'Order Type', - actions: 'Actions', - requestRefund: 'Request Refund', - }, - result: { - success: 'Payment Successful', - subscriptionSuccess: 'Subscription Successful', - processing: 'Payment Processing', - processingHint: 'Payment confirmation is still pending. This page will refresh automatically.', - failed: 'Payment Failed', - backToRecharge: 'Back to Recharge', - viewOrders: 'View Orders', - }, - currentBalance: 'Current Balance', - groupFallback: 'Group #{id}', - rechargeAccount: 'Recharge Account', - activeSubscription: 'Active Subscription', - noActiveSubscription: 'No active subscription', - tabTopUp: 'Top Up', - tabSubscribe: 'Subscribe', - noPlans: 'No subscription plans available', - notAvailable: 'Top-up is currently unavailable', - confirmSubscription: 'Confirm Subscription', - confirmCancel: 'Are you sure you want to cancel this order?', - amountTooLow: 'Minimum amount is {min}', - amountTooHigh: 'Maximum amount is {max}', - amountNoMethod: 'No payment method available for this amount', - rechargeRatePreview: 'Current rate: 1 CNY = {usd} USD', - refundReason: 'Refund Reason', - refundReasonPlaceholder: 'Please describe your refund reason', - stripeLoadFailed: 'Failed to load payment component. Please refresh and try again.', - stripeMissingParams: 'Missing order ID or client secret', - stripeNotConfigured: 'Stripe is not configured', - airwallexLoadFailed: 'Failed to load Airwallex payment component. Please refresh and try again.', - airwallexMissingParams: 'Missing Airwallex payment parameters', - errors: { - tooManyPending: 'Too many pending orders (max {max}). Please complete or cancel existing orders first.', - cancelRateLimited: 'Too many cancellations. Please try again later.', - wechatH5NotAuthorized: 'This merchant has not enabled WeChat H5 payment. Open this page in WeChat to continue.', - wechatPaymentMpNotConfigured: 'This site has not completed WeChat MP/JSAPI payment setup, so in-app WeChat payment is unavailable right now.', - wechatJsapiUnavailable: 'WeChat payment could not be invoked in the current environment. Reopen this page inside WeChat and try again.', - wechatJsapiFailed: 'WeChat payment did not complete. Try invoking it again or switch to QR payment.', - wechatUnavailable: 'WeChat payment is temporarily unavailable. Please try again later.', - wechatOpenInWeChatHint: 'Open the current page inside WeChat, or switch to desktop WeChat QR payment.', - wechatScanOnDesktopHint: 'On desktop, use WeChat Scan to pay; on mobile, reopen the current page inside WeChat.', - wechatSwitchBrowserHint: 'Switch to desktop WeChat QR payment, or reopen this page in an external browser and retry.', - mobilePaymentFallbackToQr: 'This merchant has not enabled mobile payment. The flow has been switched to QR payment automatically.', - alipayDesktopUnavailable: 'The desktop Alipay flow could not generate a QR code.', - alipayDesktopQrHint: 'Desktop Alipay should render a QR code. Refresh and retry, or make sure the payment page was not blocked.', - alipayMobileUnavailable: 'This page could not hand off to Alipay.', - alipayMobileOpenHint: 'Allow the current page to open the Alipay app, or retry from the system browser.', - // Structured error codes (reason strings from backend ApplicationError) - PAYMENT_DISABLED: 'Payment system is disabled.', - USER_INACTIVE: 'Your account is disabled.', - BALANCE_PAYMENT_DISABLED: 'Balance recharge has been disabled.', - INVALID_AMOUNT: 'Invalid amount.', - INVALID_INPUT: 'Invalid request.', - PLAN_NOT_AVAILABLE: 'Plan not found or no longer available.', - GROUP_NOT_FOUND: 'Subscription group is no longer available.', - GROUP_TYPE_MISMATCH: 'Group is not a subscription type.', - TOO_MANY_PENDING: 'Too many pending orders (max {max}). Please complete or cancel existing orders first.', - DAILY_LIMIT_EXCEEDED: 'Daily recharge limit reached. Remaining: {remaining}.', - PAYMENT_GATEWAY_ERROR: 'Payment method is unavailable.', - NO_AVAILABLE_INSTANCE: 'No payment channel available right now.', - PAYMENT_PROVIDER_MISCONFIGURED: 'Payment provider misconfigured. Please contact an administrator.', - WXPAY_CONFIG_MISSING_KEY: 'WeChat Pay config missing required key: {key}.', - WXPAY_CONFIG_INVALID_KEY_LENGTH: 'WeChat Pay {key} length is invalid (expected {expected} bytes, got {actual}).', - WXPAY_CONFIG_INVALID_KEY: 'WeChat Pay {key} is malformed. Make sure you copied the full PEM content.', - PENDING_ORDERS: 'This provider has pending orders. Please wait for them to complete before making changes.', - PAYMENT_PROVIDER_CONFLICT: 'Another enabled provider instance is already serving this payment method. Disable it before continuing.', - CANCEL_RATE_LIMITED: 'Too many cancellations. Please try again later.', - NOT_FOUND: 'Order not found.', - FORBIDDEN: 'No permission for this order.', - CONFLICT: 'Order status has changed. Please refresh.', - INVALID_ORDER_TYPE: 'Only balance orders can request a refund.', - INVALID_STATUS: 'The current order status does not allow this operation.', - BALANCE_NOT_ENOUGH: 'Refund amount exceeds balance.', - REFUND_AMOUNT_EXCEEDED: 'Refund amount exceeds the recharge amount.', - REFUND_FAILED: 'Refund failed.', - }, - airwallexPay: 'Airwallex Payment', - stripePay: 'Pay Now', - stripeSuccessProcessing: 'Payment successful, processing your order...', - stripePopup: { - redirecting: 'Redirecting to payment page...', - loadingQr: 'Loading WeChat Pay QR code...', - timeout: 'Timed out waiting for payment credentials, please retry', - qrFailed: 'Failed to get WeChat Pay QR code', - }, - subscribeNow: 'Subscribe Now', - renewNow: 'Renew', - selectPlan: 'Select Plan', - planFeatures: 'Features', - planCard: { - rate: 'Rate', - peakRate: 'Peak Rate', - dailyLimit: 'Daily', - weeklyLimit: 'Weekly', - monthlyLimit: 'Monthly', - quota: 'Quota', - unlimited: 'Unlimited', - models: 'Models', - }, - days: 'days', - months: 'months', - years: 'years', - oneMonth: '1 Month', - oneYear: '1 Year', - perMonth: 'month', - perYear: 'year', - admin: { - tabs: { - overview: 'Overview', - orders: 'Orders', - channels: 'Channels', - plans: 'Plans', - }, - todayRevenue: 'Today Revenue', - totalRevenue: 'Total Revenue', - todayOrders: 'Today Orders', - orderCount: 'Order Count', - avgAmount: 'Average Amount', - revenue: 'Revenue', - dailyRevenue: 'Daily Revenue', - paymentDistribution: 'Payment Distribution', - colUser: 'User', - topUsers: 'Top Users', - noData: 'No data', - days: 'days', - weeks: 'weeks', - months: 'months', - searchOrders: 'Search orders...', - allStatuses: 'All Statuses', - allPaymentTypes: 'All Payment Types', - allOrderTypes: 'All Order Types', - orderDetail: 'Order Detail', - orderType: 'Order Type', - orders: 'Orders', - balanceOrder: 'Balance Top-Up', - subscriptionOrder: 'Subscription', - paidAt: 'Paid At', - completedAt: 'Completed At', - expiresAt: 'Expires At', - feeRate: 'Fee Rate', - refund: 'Refund', - refundOrder: 'Refund Order', - refundAmount: 'Refund Amount', - maxRefundable: 'Max Refundable', - refundReason: 'Refund Reason', - refundReasonPlaceholder: 'Please enter refund reason', - confirmRefund: 'Confirm Refund', - refundSuccess: 'Refund successful', - refundPending: 'Refund pending gateway confirmation', - queryRefundStatus: 'Query refund status', - refundInfo: 'Refund Info', - refundEnabled: 'Refund Enabled', - allowUserRefund: 'Allow User Refund', - alreadyRefunded: 'Already Refunded', - deductBalance: 'Deduct Balance', - deductBalanceHint: 'Subtract recharged amount from user balance', - userBalance: 'User Balance', - orderAmount: 'Order Amount', - insufficientBalance: 'Insufficient balance — will deduct to $0', - noDeduction: 'Will NOT deduct user balance', - forceRefund: 'Force refund (ignore balance check)', - orderCancelled: 'Order Cancelled', - retry: 'Retry', - retrySuccess: 'Retry successful', - approveRefund: 'Approve Refund', - retryRefund: 'Retry Refund', - refundRequestInfo: 'Refund Request Info', - refundRequestedAt: 'Requested At', - refundRequestedBy: 'Requested By', - refundRequestReason: 'Request Reason', - auditLogs: 'Audit Logs', - operator: 'Operator', - channelName: 'Channel Name', - channelDescription: 'Channel Description', - createChannel: 'Create Channel', - editChannel: 'Edit Channel', - deleteChannel: 'Delete Channel', - deleteChannelConfirm: 'Are you sure you want to delete this channel?', - planName: 'Plan Name', - planDescription: 'Plan Description', - createPlan: 'Create Plan', - editPlan: 'Edit Plan', - deletePlan: 'Delete Plan', - deletePlanConfirm: 'Are you sure you want to delete this plan?', - originalPrice: 'Original Price', - price: 'Price', - subscriptionCnyPayPreview: 'CNY channel charge preview: {amount}', - subscriptionCnyPayPreviewWithFee: '({feeRate}% fee included: {total})', - validityDays: 'Validity (days)', - validityUnit: 'Validity Unit', - sortOrder: 'Sort Order', - forSale: 'For Sale', - onSale: 'On Sale', - offSale: 'Off Sale', - group: 'Group', - groupId: 'Group ID', - features: 'Features', - featuresHint: 'One feature per line', - featuresPlaceholder: 'Enter plan features...', - providerManagement: 'Provider Management', - providerManagementDesc: 'Manage payment provider instances', - createProvider: 'Create Provider', - editProvider: 'Edit Provider', - deleteProvider: 'Delete Provider', - deleteProviderConfirm: 'Are you sure you want to delete this provider?', - providerName: 'Provider Name', - providerKey: 'Provider Key', - selectProviderKey: 'Select Provider Key', - providerConfig: 'Provider Config', - noProviders: 'No providers configured', - noProvidersHint: 'Create a provider instance to start accepting payments', - supportedTypes: 'Supported Payment Types', - supportedTypesHint: 'Select the payment types this provider supports', - rateMultiplier: 'Rate Multiplier', - dashboardTitle: 'Payment Dashboard', - dashboardDesc: 'Recharge order analytics and insights', - daySuffix: 'd', - paymentConfigTitle: 'Payment Config', - paymentConfigDesc: 'Configure payment providers and settings', - plansPageTitle: 'Subscription Plans', - plansPageDesc: 'Manage subscription plan configuration', - tabPlanConfig: 'Plan Configuration', - tabUserSubs: 'User Subscriptions', - selectGroup: 'Select a group', - groupRequired: 'Please select a subscription group', - priceRequired: 'Price must be greater than 0', - validityDaysRequired: 'Validity days must be greater than 0', - groupMissing: 'Missing', - groupInfo: 'Group Info', - platform: 'Platform', - rateMultiplierLabel: 'Rate', - dailyLimit: 'Daily Limit', - weeklyLimit: 'Weekly Limit', - monthlyLimit: 'Monthly Limit', - unlimited: 'Unlimited', - searchUserSubs: 'Search user subscriptions...', - daily: 'D', - weekly: 'W', - monthly: 'M', - subsStatus: { - active: 'Active', - expired: 'Expired', - revoked: 'Revoked', - }, - }, - }, - -} diff --git a/frontend/src/i18n/locales/en/admin/accounts.ts b/frontend/src/i18n/locales/en/admin/accounts.ts new file mode 100644 index 0000000000..8664da1d5a --- /dev/null +++ b/frontend/src/i18n/locales/en/admin/accounts.ts @@ -0,0 +1,1241 @@ +export default { + accounts: { + title: 'Account Management', + description: 'Manage AI platform accounts and credentials', + createAccount: 'Create Account', + autoRefresh: 'Auto Refresh', + enableAutoRefresh: 'Enable auto refresh', + refreshInterval5s: '5 seconds', + refreshInterval10s: '10 seconds', + refreshInterval15s: '15 seconds', + refreshInterval30s: '30 seconds', + autoRefreshCountdown: 'Auto refresh: {seconds}s', + listPendingSyncHint: 'List changes are pending sync. Click sync to load latest rows.', + listPendingSyncAction: 'Sync now', + syncFromCrs: 'Sync from CRS', + dataExport: 'Export', + dataExportSelected: 'Export Selected', + dataExportIncludeProxies: 'Include proxies linked to the exported accounts', + dataImport: 'Import', + moreActions: 'More Actions', + dataActions: 'Data', + toolActions: 'Tools', + viewColumns: 'Columns', + selectedCount: '{count} selected', + dataExportConfirmMessage: 'The exported data contains sensitive account and proxy information. Store it securely.', + dataExportConfirm: 'Confirm Export', + dataExported: 'Data exported successfully', + dataExportedSkippedShadows: 'Data exported. Skipped {count} spark shadow account(s): their scheduling config is not included in the backup; recreate and re-tune them after restore.', + dataExportFailed: 'Failed to export data', + dataImportTitle: 'Import Data', + dataImportHint: 'Upload the exported JSON file to import accounts and proxies.', + dataImportWarning: 'Import will create new accounts/proxies; groups must be bound manually. Ensure existing data does not conflict.', + dataImportFile: 'Data file', + dataImportButton: 'Start Import', + dataImporting: 'Importing...', + dataImportSelectFile: 'Please select a data file', + dataImportParseFailed: 'Failed to parse data file', + dataImportParseFailedFile: 'Failed to parse {name}', + dataImportInvalidFile: '{name} is not a supported data export file', + dataImportIgnoredFiles: 'Ignored {count} non-JSON file(s)', + dataImportFailed: 'Data import failed', + dataImportResult: 'Import Result', + dataImportResultSummary: 'Proxies created {proxy_created}, reused {proxy_reused}, failed {proxy_failed}; Accounts created {account_created}, failed {account_failed}', + dataImportErrors: 'Error Details', + dataImportSuccess: 'Import completed: accounts {account_created}, failed {account_failed}', + dataImportCompletedWithErrors: 'Import completed with errors: account failed {account_failed}, proxy failed {proxy_failed}', + syncFromCrsTitle: 'Sync Accounts from CRS', + syncFromCrsDesc: + 'Sync accounts from claude-relay-service (CRS) into this system (CRS is called server-to-server).', + crsVersionRequirement: '⚠️ Note: CRS version must be ≥ v1.1.240 to support this feature', + crsBaseUrl: 'CRS Base URL', + crsBaseUrlPlaceholder: 'e.g. http://127.0.0.1:3000', + crsUsername: 'Username', + crsPassword: 'Password', + syncProxies: 'Also sync proxies (match by host/port/auth or create)', + syncNow: 'Sync Now', + syncing: 'Syncing...', + syncMissingFields: 'Please fill base URL, username and password', + syncResult: 'Sync Result', + syncResultSummary: 'Created {created}, updated {updated}, skipped {skipped}, failed {failed}', + syncErrors: 'Errors / Skipped Details', + syncCompleted: 'Sync completed: created {created}, updated {updated}, skipped {skipped}', + syncCompletedWithErrors: + 'Sync completed with errors: failed {failed} (created {created}, updated {updated}, skipped {skipped})', + syncFailed: 'Sync failed', + crsPreview: 'Preview', + crsPreviewing: 'Previewing...', + crsPreviewFailed: 'Preview failed', + crsExistingAccounts: 'Existing accounts (will be updated)', + crsNewAccounts: 'New accounts (select to sync)', + crsSelectAll: 'Select all', + crsSelectNone: 'Select none', + crsNoNewAccounts: 'All CRS accounts are already synced.', + crsWillUpdate: 'Will update {count} existing accounts.', + crsSelectedCount: '{count} new accounts selected', + crsUpdateBehaviorNote: + 'Existing accounts only sync fields returned by CRS; missing fields keep their current values. Credentials are merged by key — keys not returned by CRS are preserved. Proxies are kept when "Sync proxies" is unchecked.', + crsBack: 'Back', + editAccount: 'Edit Account', + deleteAccount: 'Delete Account', + searchAccounts: 'Search accounts...', + notes: 'Notes', + notesPlaceholder: 'Enter notes', + notesHint: 'Notes are optional', + allPlatforms: 'All Platforms', + allTypes: 'All Types', + allStatus: 'All Status', + allGroups: 'All Groups', + ungroupedGroup: 'Ungrouped', + oauthType: 'OAuth', + setupToken: 'Setup Token', + apiKey: 'API Key', + // Schedulable toggle + schedulable: 'Schedulable', + schedulableHint: 'Enable to include this account in API request scheduling', + schedulableEnabled: 'Scheduling enabled', + schedulableDisabled: 'Scheduling disabled', + failedToToggleSchedulable: 'Failed to toggle scheduling status', + groupCountTotal: '{count} groups total', + platforms: { + anthropic: 'Anthropic', + claude: 'Claude', + openai: 'OpenAI', + gemini: 'Gemini', + antigravity: 'Antigravity', + grok: 'Grok', + }, + types: { + oauth: 'OAuth', + chatgptOauth: 'ChatGPT OAuth', + responsesApi: 'Responses API', + googleOauth: 'Google OAuth', + codeAssist: 'Code Assist', + antigravityOauth: 'Antigravity OAuth', + grokOauth: 'Grok OAuth', + antigravityApikey: 'Connect via Base URL + API Key', + upstream: 'Upstream', + upstreamDesc: 'Connect via Base URL + API Key' + }, + antigravityProjectIdLabel: 'GCP Project ID (optional)', + antigravityProjectIdPlaceholder: 'your-gcp-project-id', + antigravityProjectIdHint: + 'Antigravity standard-tier accounts that do not receive an automatic project_id need a user-owned GCP project.', + status: { + active: 'Active', + inactive: 'Inactive', + error: 'Error', + cooldown: 'Cooldown', + paused: 'Paused', + limited: 'Limited', + rateLimited: 'Rate Limited', + overloaded: 'Overloaded', + tempUnschedulable: 'Temp Unschedulable', + quotaExceeded: 'Quota Exceeded', + unschedulable: 'Unschedulable', + rateLimitedUntil: 'Rate limited and removed from scheduling. Auto resumes at {time}', + rateLimitedAutoResume: 'Auto resumes in {time}', + modelRateLimitedUntil: '{model} rate limited until {time}', + modelCreditOveragesUntil: '{model} using AI Credits until {time}', + creditsExhausted: 'Credits Exhausted', + creditsExhaustedUntil: 'AI Credits exhausted, expected recovery at {time}', + overloadedUntil: 'Overloaded until {time}', + viewTempUnschedDetails: 'View temp unschedulable details' + }, + columns: { + name: 'Name', + id: 'Account ID', + platformType: 'Platform/Type', + platform: 'Platform', + type: 'Type', + capacity: 'Capacity', + notes: 'Notes', + priority: 'Priority', + billingRateMultiplier: 'Billing Rate', + weight: 'Weight', + schedulerScore: 'Scheduler Score', + status: 'Status', + schedulable: 'Schedulable', + todayStats: 'Today Stats', + groups: 'Groups', + usageWindows: 'Usage Windows', + proxy: 'Proxy', + lastUsed: 'Last Used', + createdAt: 'Created', + expiresAt: 'Expires At', + actions: 'Actions' + }, + schedulerScore: { + baseShort: 'Base', + stickyShort: 'Sticky', + ungrouped: 'Ungrouped', + hint: 'Displayed as "group / base score / sticky bonus". The base score is computed within the current filtered candidate set and includes priority, load, queue depth, error rate, first-token latency, reset window, quota headroom, and related factors. The sticky bonus applies only when sticky weighting is enabled for previous_response_id or session_hash. Higher scores are preferred.' + }, + usageWindowsHint: '"5h / 7d" are the upstream account\'s official rolling usage windows (e.g. OpenAI ChatGPT, Claude). They are imposed by the upstream provider on the account itself — not configured by sub2api, and unrelated to the models you map. Usage resets automatically once each window rolls over, and the limit cannot be lifted from within sub2api.', + allPrivacyModes: 'All Privacy States', + privacyUnset: 'Unset', + privacyTrainingOff: 'Training data sharing disabled', + privacyCfBlocked: 'Blocked by Cloudflare, training may still be on', + privacyFailed: 'Failed to disable training', + privacyAntigravitySet: 'Telemetry and marketing emails disabled', + privacyAntigravityFailed: 'Privacy setting failed', + setPrivacy: 'Set Privacy', + subscriptionAbnormal: 'Abnormal', + subscriptionExpires: 'Expires', + // Capacity status tooltips + capacity: { + windowCost: { + blocked: '5h window cost exceeded, account scheduling paused', + stickyOnly: '5h window cost at threshold, only sticky sessions allowed', + normal: '5h window cost normal' + }, + sessions: { + full: 'Active sessions full, new sessions must wait (idle timeout: {idle} min)', + normal: 'Active sessions normal (idle timeout: {idle} min)' + }, + rpm: { + full: 'RPM limit reached', + warning: 'RPM approaching limit', + normal: 'RPM normal', + tieredNormal: 'RPM limit (Tiered) - Normal', + tieredWarning: 'RPM limit (Tiered) - Approaching limit', + tieredStickyOnly: 'RPM limit (Tiered) - Sticky only | Buffer: {buffer}', + tieredBlocked: 'RPM limit (Tiered) - Blocked | Buffer: {buffer}', + stickyExemptNormal: 'RPM limit (Sticky Exempt) - Normal', + stickyExemptWarning: 'RPM limit (Sticky Exempt) - Approaching limit', + stickyExemptOver: 'RPM limit (Sticky Exempt) - Over limit, sticky only' + }, + quota: { + exceeded: 'Quota exceeded, account paused', + normal: 'Quota normal' + }, + }, + tempUnschedulable: { + title: 'Temp Unschedulable', + statusTitle: 'Temp Unschedulable Status', + hint: 'Disable accounts temporarily when error code and keyword both match.', + notice: 'Rules are evaluated in order and require both error code and keyword match.', + addRule: 'Add Rule', + ruleOrder: 'Rule Order', + ruleIndex: 'Rule #{index}', + errorCode: 'Error Code', + errorCodePlaceholder: 'e.g. 429', + durationMinutes: 'Duration (minutes)', + durationPlaceholder: 'e.g. 30', + keywords: 'Keywords', + keywordsPlaceholder: 'e.g. overloaded, too many requests', + keywordsHint: 'Separate keywords with commas; any keyword match will trigger.', + description: 'Description', + descriptionPlaceholder: 'Optional note for this rule', + rulesInvalid: 'Add at least one rule with error code, keywords, and duration.', + viewDetails: 'View temp unschedulable details', + accountName: 'Account', + triggeredAt: 'Triggered At', + until: 'Until', + remaining: 'Remaining', + matchedKeyword: 'Matched Keyword', + errorMessage: 'Error Details', + reset: 'Recover State', + resetSuccess: 'Account state recovered successfully', + resetFailed: 'Failed to recover account state', + failedToLoad: 'Failed to load temp unschedulable status', + notActive: 'This account is not temporarily unschedulable.', + expired: 'Expired', + remainingMinutes: 'About {minutes} minutes', + remainingHours: 'About {hours} hours', + remainingHoursMinutes: 'About {hours} hours {minutes} minutes', + presets: { + overloadLabel: '529 Overloaded', + overloadDesc: 'Overloaded - pause 60 minutes', + rateLimitLabel: '429 Rate Limit', + rateLimitDesc: 'Rate limited - pause 10 minutes', + unavailableLabel: '503 Unavailable', + unavailableDesc: 'Unavailable - pause 30 minutes' + } + }, + clearRateLimit: 'Clear Rate Limit', + resetQuota: 'Reset Quota', + quotaLimit: 'Quota Limit', + quotaLimitPlaceholder: '0 means unlimited', + quotaLimitHint: 'Set daily/weekly/total spending limits (USD). Anthropic API key accounts can also configure client affinity. Changing limits won\'t reset usage.', + quotaLimitToggle: 'Enable Quota Limit', + quotaLimitToggleHint: 'When enabled, account will be paused when usage reaches the set limit', + quotaDailyLimit: 'Daily Limit', + quotaDailyLimitHint: 'Automatically resets every 24 hours from first usage.', + quotaWeeklyLimit: 'Weekly Limit', + quotaWeeklyLimitHint: 'Automatically resets every 7 days from first usage.', + quotaTotalLimit: 'Total Limit', + quotaTotalLimitHint: 'Cumulative spending limit. Does not auto-reset — use "Reset Quota" to clear.', + quotaResetMode: 'Reset Mode', + quotaResetModeRolling: 'Rolling Window', + quotaResetModeFixed: 'Fixed Time', + quotaResetHour: 'Reset Hour', + quotaWeeklyResetDay: 'Reset Day', + quotaResetTimezone: 'Reset Timezone', + quotaDailyLimitHintFixed: 'Resets daily at {hour}:00 ({timezone}).', + quotaWeeklyLimitHintFixed: 'Resets every {day} at {hour}:00 ({timezone}).', + dayOfWeek: { + monday: 'Monday', + tuesday: 'Tuesday', + wednesday: 'Wednesday', + thursday: 'Thursday', + friday: 'Friday', + saturday: 'Saturday', + sunday: 'Sunday', + }, + quotaLimitAmount: 'Total Limit', + quotaLimitAmountHint: 'Cumulative spending limit. Does not auto-reset.', + quotaNotify: { + alert: 'Alert', + enabled: 'Enable Alert', + threshold: 'Alert Amount', + thresholdPlaceholder: 'Enter percentage', + }, + testConnection: 'Test Connection', + reAuthorize: 'Re-Authorize', + refreshToken: 'Refresh Token', + noAccountsYet: 'No accounts yet', + createFirstAccount: 'Create your first account to start using AI services.', + tokenRefreshed: 'Token refreshed successfully', + accountDeleted: 'Account deleted successfully', + rateLimitCleared: 'Rate limit cleared successfully', + bulkSchedulableEnabled: 'Successfully enabled scheduling for {count} account(s)', + bulkSchedulableDisabled: 'Successfully disabled scheduling for {count} account(s)', + bulkSchedulablePartial: 'Scheduling updated partially: {success} succeeded, {failed} failed', + bulkSchedulableResultUnknown: 'Bulk scheduling result incomplete. Please retry or refresh.', + bulkActions: { + selected: '{count} account(s) selected', + selectCurrentPage: 'Select this page', + clear: 'Clear selection', + edit: 'Bulk Edit', + delete: 'Bulk Delete', + enableScheduling: 'Enable Scheduling', + disableScheduling: 'Disable Scheduling', + resetStatus: 'Reset Status', + refreshToken: 'Refresh Token', + resetStatusSuccess: 'Successfully reset {count} account(s) status', + refreshTokenSuccess: 'Successfully refreshed {count} account(s) token', + partialSuccess: 'Partially completed: {success} succeeded, {failed} failed' + }, + bulkEdit: { + title: 'Bulk Edit Accounts', + selectionInfo: + '{count} account(s) selected. Only checked or filled fields will be updated; others stay unchanged.', + baseUrlPlaceholder: 'https://api.anthropic.com or https://api.openai.com', + baseUrlNotice: 'Applies to API Key accounts only; leave empty to keep existing value', + submit: 'Update Accounts', + updating: 'Updating...', + success: 'Updated {count} account(s)', + partialSuccess: 'Partially updated: {success} succeeded, {failed} failed', + failed: 'Bulk update failed', + noSelection: 'Please select accounts to edit', + noFieldsSelected: 'Select at least one field to update', + mixedPlatformWarning: 'Selected accounts span multiple platforms ({platforms}). Model mapping presets shown are combined — ensure mappings are appropriate for each platform.' + }, + bulkDeleteTitle: 'Bulk Delete Accounts', + bulkDeleteConfirm: 'Delete the selected {count} account(s)? This action cannot be undone.', + bulkDeleteSuccess: 'Deleted {count} account(s)', + bulkDeletePartial: 'Partially deleted: {success} succeeded, {failed} failed', + bulkDeleteFailed: 'Bulk delete failed', + recoverState: 'Recover State', + recoverStateHint: 'Used to recover error, rate-limit, and temporary unschedulable runtime state.', + recoverStateSuccess: 'Account state recovered successfully', + recoverStateFailed: 'Failed to recover account state', + fallbackActive: 'Fallback', + fallbackActiveTip: 'Origin proxy {origin} expired', + revertProxy: 'Revert proxy', + revertProxySuccess: 'Successfully reverted to original proxy', + revertProxyFailed: 'Failed to revert proxy', + createSparkShadow: 'Create Spark Shadow', + createSparkShadowConfirm: 'Create a spark shadow account linked to "{name}"? It shares the parent\'s credentials and serves only spark models.', + createSparkShadowSuccess: 'Spark shadow account created', + createSparkShadowFailed: 'Failed to create spark shadow account', + resetStatus: 'Reset Status', + statusReset: 'Account status reset successfully', + failedToResetStatus: 'Failed to reset account status', + failedToLoad: 'Failed to load accounts', + failedToRefresh: 'Failed to refresh token', + failedToDelete: 'Failed to delete account', + failedToClearRateLimit: 'Failed to clear rate limit', + deleteConfirm: "Are you sure you want to delete '{name}'? This action cannot be undone.", + // Create/Edit Account Modal + platform: 'Platform', + accountName: 'Account Name', + enterAccountName: 'Enter account name', + accountType: 'Account Type', + claudeCode: 'Claude Code', + claudeConsole: 'Claude Console', + bedrockLabel: 'AWS Bedrock', + bedrockDesc: 'SigV4 / API Key', + vertexLabel: 'Vertex', + vertexDesc: 'Service Account', + vertexAnthropicHint: 'Use a Google Cloud Service Account JSON to call Anthropic Claude via Vertex AI. It is recommended to configure model mapping to map client Claude model names to Vertex model IDs.', + vertexGeminiHint: 'Use a Google Cloud Service Account JSON to access Vertex AI Gemini. It is recommended to place Vertex accounts in a separate group to avoid mixing with AI Studio/Gemini OAuth on the same models.', + vertexSaJsonLabel: 'Service Account JSON', + vertexSaJsonLoaded: 'Service Account JSON loaded', + vertexSaJsonDrop: 'Drop Service Account JSON here', + vertexSaJsonKeyHidden: 'Key content is not displayed in the form.', + vertexSaJsonDropHint: 'Drag a .json file here, or click the button to select one.', + vertexSaJsonSelectBtn: 'Select JSON', + vertexSaJsonUploadHint: 'After uploading or dropping a JSON file, the project_id will be auto-extracted. Key content is only used for account creation.', + vertexSaJsonEditHint: 'Service Account JSON is not shown on the edit page; to change the JSON, delete the account and recreate it.', + vertexProjectIdPlaceholder: 'Auto-extracted from JSON', + vertexLocationHint: 'Available locations vary by Vertex model. Select the default endpoint location for this account.', + vertexLocationRequired: 'Please enter a Vertex location', + vertexSaJsonMissingFields: 'Service Account JSON is missing project_id, client_email, or private_key', + vertexSaJsonMissingProjectId: 'Service Account JSON is missing project_id', + vertexSaJsonMissingClientEmail: 'Service Account JSON is missing client_email', + vertexSaJsonInvalid: 'Service Account JSON format is invalid', + vertexSaJsonRequired: 'Please upload a Service Account JSON', + oauthSetupToken: 'OAuth / Setup Token', + addMethod: 'Add Method', + setupTokenLongLived: 'Setup Token (Long-lived)', + baseUrl: 'Base URL', + baseUrlHint: 'Leave default for official Anthropic API', + apiKeyRequired: 'API Key *', + apiKeyPlaceholder: 'sk-ant-api03-...', + apiKeyHint: 'Your Claude Console API Key', + // OpenAI specific hints + openai: { + baseUrlHint: 'Leave default for official OpenAI API', + apiKeyHint: 'Your OpenAI API Key', + oauthPassthrough: 'Auto passthrough (auth only)', + oauthPassthroughDesc: + 'When enabled, this OpenAI account uses automatic passthrough: the gateway forwards request/response as-is and only swaps auth, while keeping billing/concurrency/audit and necessary safety filtering.', + responsesWebsocketsV2: 'Responses WebSocket v2', + responsesWebsocketsV2Desc: + 'Disabled by default. Enable to allow responses_websockets_v2 capability (still gated by global and account-type switches).', + wsMode: 'WS mode', + wsModeDesc: 'Only applies to the current OpenAI account type.', + wsModeOff: 'Off (off)', + wsModeCtxPool: 'Context Pool (ctx_pool)', + wsModePassthrough: 'Passthrough (passthrough)', + wsModeHttpBridge: 'HTTP Bridge (http_bridge)', + wsModeShared: 'Shared (shared)', + wsModeDedicated: 'Dedicated (dedicated)', + wsModeConcurrencyHint: + 'When WS mode is enabled, account concurrency becomes the WS connection pool limit for this account.', + wsModePassthroughHint: 'Passthrough mode does not use the WS connection pool.', + oauthResponsesWebsocketsV2: 'OAuth WebSocket Mode', + oauthResponsesWebsocketsV2Desc: + 'Only applies to OpenAI OAuth. This account can use OpenAI WebSocket Mode only when enabled.', + apiKeyResponsesWebsocketsV2: 'API Key WebSocket Mode', + apiKeyResponsesWebsocketsV2Desc: + 'Only applies to OpenAI API Key. This account can use OpenAI WebSocket Mode only when enabled.', + responsesWebsocketsV2PassthroughHint: + 'Automatic passthrough is currently enabled: it only affects HTTP passthrough and does not disable WS mode.', + responsesMode: 'Responses API support', + responsesModeDesc: + 'Only applies to the OpenAI API Key text forwarding path. Auto follows probe results; force modes override probing.', + responsesModeAuto: 'Auto', + responsesModeForceResponses: 'Force Responses', + responsesModeForceChatCompletions: 'Force Chat Completions', + responsesModeTextDisabledHint: + 'Not applicable when the Responses / Chat Completions endpoint is not enabled.', + endpointCapabilities: 'Endpoint capabilities', + endpointCapabilitiesDesc: + 'Used by account routing. The text endpoint follows the Responses API support setting above and is shown as Responses, Chat Completions, or auto mode; Embeddings independently controls /v1/embeddings.', + capabilityResponses: 'Responses', + capabilityTextAuto: 'Responses / Chat Completions (Auto)', + capabilityResponsesAuto: 'Responses (auto probe)', + capabilityChatCompletions: 'Chat Completions', + capabilityChatCompletionsAuto: 'Chat Completions (auto probe)', + capabilityEmbeddings: 'Embeddings', + responsesStatusAutoSupported: 'Auto probe: Responses', + responsesStatusAutoUnsupported: 'Auto probe: Chat Completions', + responsesStatusAutoUnknown: 'Auto probe: unknown', + responsesStatusForcedResponses: 'Forced Responses', + responsesStatusForcedChatCompletions: 'Forced Chat Completions', + codexCLIOnly: 'Codex official clients only', + codexCLIOnlyDesc: + 'Only applies to OpenAI OAuth. When enabled, only Codex official client families are allowed; when disabled, the gateway bypasses this restriction and keeps existing behavior.', + codexCLIOnlyAppServer: 'Allow Codex app-server clients', + codexCLIOnlyAppServerDesc: + "Effective only when the switch above is on. When enabled, this account also allows third-party clients that embed the Codex engine over the app-server protocol (e.g. Claude Code's codex plugin); they still pass the global engine-fingerprint gate. OR-combined with the global app-server toggle.", + codexImageTool: 'Codex image tool', + codexImageToolDesc: + 'One policy for the image_generation tool on Codex /responses text requests: whether it is auto-injected, and whether client-provided tools pass through. Account policy takes precedence over channel and global settings; standalone image-generation endpoints are unaffected.', + codexImageToolInherit: 'Follow channel', + codexImageToolInheritDesc: 'No account override; injection follows the channel or global policy, and client-provided image tools pass through.', + codexImageToolEnabled: 'Force inject', + codexImageToolEnabledDesc: 'Always inject the image tool for Codex /responses requests.', + codexImageToolDisabled: 'No injection', + codexImageToolDisabledDesc: 'Never auto-inject; client-provided image tools still pass through.', + codexImageToolBlock: 'Block all', + codexImageToolBlockDesc: 'No injection, and client-provided image tools plus matching tool_choice are removed.', + codexImageToolBadgeInherit: 'Channel policy', + codexImageToolBadgeEnabled: 'Force inject', + codexImageToolBadgeDisabled: 'No injection', + codexImageToolBadgeBlock: 'Blocked', + compactMode: 'Compact mode', + compactModeDesc: + 'Controls how this account participates in /responses/compact routing. Auto follows probe results, Force On always allows, Force Off always excludes.', + compactModeAuto: 'Auto', + compactModeForceOn: 'Force On', + compactModeForceOff: 'Force Off', + compactModelMapping: 'Compact-only model mapping', + compactModelMappingDesc: + 'Only applies to /responses/compact. Use this when the upstream compact endpoint requires a special compact model.', + compactSupported: 'Compact supported', + compactUnsupported: 'Compact unsupported', + compactAuto: 'Compact Auto', + compactUnknown: 'Compact Auto', + compactLastChecked: 'Last compact probe', + testMode: 'Test mode', + testModeDefault: 'Default request', + testModeCompact: 'Compact probe', + modelRestrictionDisabledByPassthrough: 'Automatic passthrough is enabled: model whitelist/mapping will not take effect.', + }, + grok: { + baseUrlHint: 'Grok OAuth accounts forward to the official xAI API base URL.', + apiKeyHint: 'Grok subscription support uses OAuth refresh tokens; API keys are out of scope for this account type.' + }, + anthropic: { + apiKeyPassthrough: 'Auto passthrough (auth only)', + apiKeyPassthroughDesc: + 'Only applies to Anthropic API Key accounts. When enabled, messages/count_tokens are forwarded in passthrough mode with auth replacement only, while billing/concurrency/audit and safety filtering are preserved. Disable to roll back immediately.', + apiKeyAuthScheme: 'Upstream auth scheme', + apiKeyAuthSchemeDesc: 'Choose the API key auth header used when forwarding to an Anthropic-compatible upstream. Ollama Cloud uses Authorization: Bearer.', + apiKeyAuthSchemeXApiKey: 'x-api-key', + apiKeyAuthSchemeBearer: 'Authorization: Bearer', + webSearchEmulation: 'Web Search Emulation', + webSearchEmulationDesc: + 'Enable web search emulation for this API Key account. When a pure web_search request is detected, the gateway calls a third-party search API and constructs the response locally. Default follows channel config.', + webSearchDefault: 'Default', + webSearchEnabled: 'Enabled', + webSearchDisabled: 'Disabled', + }, + modelRestriction: 'Model Restriction (Optional)', + modelWhitelist: 'Model Whitelist', + modelMapping: 'Model Mapping', + selectAllowedModels: 'Select allowed models. Leave empty to support all models.', + mapRequestModels: + 'Map request models to actual models. Left is the requested model, right is the actual model sent to API.', + selectedModels: 'Selected {count} model(s)', + supportsAllModels: '(supports all models)', + requestModel: 'Request model', + actualModel: 'Actual model', + addMapping: 'Add Mapping', + mappingExists: 'Mapping for {model} already exists', + wildcardOnlyAtEnd: 'Wildcard * can only be at the end', + targetNoWildcard: 'Target model cannot contain wildcard *', + searchModels: 'Search models...', + noMatchingModels: 'No matching models', + fillRelatedModels: 'Sync latest supported models', + syncUpstreamModels: 'Sync upstream supported models', + syncUpstreamModelsLoading: 'Syncing upstream...', + syncUpstreamModelsSuccess: 'Synced {count} new model(s) from upstream ({total} upstream total)', + syncUpstreamModelsNoChanges: 'All {count} upstream model(s) are already in the whitelist', + syncUpstreamModelsEmpty: 'Upstream returned no models to sync', + syncUpstreamModelsFailed: 'Failed to sync upstream models', + syncUpstreamModelsError: 'Failed to sync upstream models: {message}', + clearAllModels: 'Clear all models', + customModelName: 'Custom model name', + enterCustomModelName: 'Enter custom model name', + addModel: 'Add', + modelExists: 'Model already exists', + modelCount: '{count} models', + poolMode: 'Pool Mode', + poolModeHint: 'Enable when upstream is an account pool; errors won\'t mark local account status', + poolModeInfo: + 'When enabled, upstream 429/403/401 errors will auto-retry without marking the account as rate-limited or errored. Suitable for upstream pointing to another sub2api instance.', + poolModeRetryCount: 'Same-Account Retries', + poolModeRetryCountHint: + 'Only applies in pool mode. Use 0 to disable in-place retry. Default {default}, maximum {max}.', + poolModeRetryStatusCodes: 'Retry Status Codes', + poolModeRetryStatusCodesHint: + 'Comma-separated HTTP status codes (100-599) that trigger same-account retry in pool mode. Leave blank to use defaults ({default}).', + customErrorCodes: 'Custom Error Codes', + customErrorCodesHint: 'Only stop scheduling for selected error codes', + customErrorCodesWarning: + 'Only selected error codes will stop scheduling. Other errors will return 500.', + customErrorCodes429Warning: + '429 already has built-in rate limit handling. Adding it to custom error codes will disable the account instead of temporary rate limiting. Are you sure?', + customErrorCodes529Warning: + '529 already has built-in overload handling. Adding it to custom error codes will disable the account instead of temporary overload marking. Are you sure?', + selectedErrorCodes: 'Selected', + noneSelectedUsesDefault: 'None selected (uses default policy)', + enterErrorCode: 'Enter error code (100-599)', + invalidErrorCode: 'Please enter a valid HTTP error code (100-599)', + errorCodeExists: 'This error code is already selected', + interceptWarmupRequests: 'Intercept Warmup Requests', + interceptWarmupRequestsDesc: + 'When enabled, warmup requests like title generation will return mock responses without consuming upstream tokens', + headerOverride: { + title: 'Header Override', + hint: 'Override same-named request headers on forwarding (case-insensitive)', + info: 'Applies to outbound requests of this account only: configured headers override client/gateway-generated headers of the same name before forwarding. Auth headers (authorization, x-api-key) and connection-control headers cannot be overridden.', + namePlaceholder: 'Header name (e.g. user-agent)', + valuePlaceholder: 'Override value (leave empty to skip)', + addRow: 'Add Header', + fillTemplate: 'Fill Template', + emptyValueHint: 'Rows with an empty value are placeholders and do not override anything.', + bulkDisableHint: 'Saving will disable header override and clear existing configuration on the selected accounts.', + bulkReplaceHint: 'Saving will replace the existing header override configuration on all selected accounts with the rows below.', + bulkEmptyRows: 'Add at least one header row before saving, or turn the toggle off to clear existing configuration.', + invalidName: 'Invalid header name (only letters, digits and !#$%&\'*+-.^_`|~ are allowed)', + blockedName: 'This header cannot be overridden (auth and connection-control headers are managed by the system)', + duplicateName: 'Duplicate header name (matching is case-insensitive)', + invalidValue: 'Invalid header value (control characters are not allowed; max length 8192)', + tooManyEntries: 'Too many header override entries (max 64)' + }, + autoPauseOnExpired: 'Auto Pause On Expired', + autoPauseOnExpiredDesc: 'When enabled, the account will auto pause scheduling after it expires', + autoPause5hThreshold: '5h Usage Threshold (%)', + autoPause7dThreshold: '7d Usage Threshold (%)', + autoPauseThresholdHint: 'Leave empty or set 0 to use the global default threshold (configured in Ops settings); set a value to override the global default. Reaching the threshold only skips the account during scheduling and does not modify schedulable.', + autoPause5hDisabled: 'Disable 5h auto-pause', + autoPause7dDisabled: 'Disable 7d auto-pause', + autoPauseDisabledHint: 'When enabled, this account is never auto-paused (even if a global default threshold is configured).', + // Quota control (Anthropic OAuth/SetupToken only) + quotaControl: { + title: 'Quota Control', + hint: 'Configure cost window, session limits, client affinity and other scheduling controls.', + windowCost: { + label: '5h Window Cost Limit', + hint: 'Limit account cost usage within the 5-hour window', + limit: 'Cost Threshold', + limitPlaceholder: '50', + limitHint: 'Account will not participate in new scheduling after reaching threshold', + stickyReserve: 'Sticky Reserve', + stickyReservePlaceholder: '10', + stickyReserveHint: 'Additional reserve for sticky sessions' + }, + sessionLimit: { + label: 'Session Count Limit', + hint: 'Limit the number of active concurrent sessions', + maxSessions: 'Max Sessions', + maxSessionsPlaceholder: '3', + maxSessionsHint: 'Maximum number of active concurrent sessions', + idleTimeout: 'Idle Timeout', + idleTimeoutPlaceholder: '5', + idleTimeoutHint: 'Sessions will be released after idle timeout' + }, + rpmLimit: { + label: 'RPM Limit', + hint: 'Limit requests per minute to protect upstream accounts', + baseRpm: 'Base RPM', + baseRpmPlaceholder: '15', + baseRpmHint: 'Max requests per minute, 0 or empty means no limit', + strategy: 'RPM Strategy', + strategyTiered: 'Tiered Model', + strategyStickyExempt: 'Sticky Exempt', + strategyTieredHint: 'Green → Yellow → Sticky only → Blocked, progressive throttling', + strategyStickyExemptHint: 'Only sticky sessions allowed when over limit', + strategyHint: 'Tiered: gradually restrict when exceeded; Sticky Exempt: existing sessions unrestricted', + stickyBuffer: 'Sticky Buffer', + stickyBufferPlaceholder: 'Default: 20% of base RPM', + stickyBufferHint: 'Extra requests allowed for sticky sessions after exceeding base RPM. Leave empty to use default (20% of base RPM, min 1)', + userMsgQueue: 'User Message Rate Control', + userMsgQueueHint: 'Rate-limit user messages to avoid triggering upstream RPM limits', + umqModeOff: 'Off', + umqModeThrottle: 'Throttle', + umqModeSerialize: 'Serialize', + }, + tlsFingerprint: { + label: 'TLS Fingerprint Simulation', + hint: 'Simulate Node.js/Claude Code client TLS fingerprint', + defaultProfile: 'Built-in Default', + randomProfile: 'Random' + }, + sessionIdMasking: { + label: 'Session ID Masking', + hint: 'When enabled, fixes the session ID in metadata.user_id for 15 minutes, making upstream think requests come from the same session' + }, + cacheTTLOverride: { + label: 'Cache TTL Override', + hint: 'Force all cache creation tokens to be billed as the selected TTL tier (5m or 1h)', + target: 'Target TTL', + targetHint: 'Select the TTL tier for billing' + }, + customBaseUrl: { + label: 'Custom Relay URL', + hint: 'Forward requests to a custom relay service. Proxy URL will be passed as a query parameter.', + urlHint: 'Relay service URL (e.g., https://relay.example.com)', + }, + clientAffinity: { + label: 'Client Affinity Scheduling', + hint: 'When enabled, new sessions prefer accounts previously used by this client to reduce account switching' + } + }, + affinityNoClients: 'No affinity clients', + affinityClients: '{count} affinity clients:', + affinitySection: 'Client Affinity', + affinitySectionHint: 'Control how clients are distributed across accounts. Configure zone thresholds to balance load.', + affinityToggle: 'Enable Client Affinity', + affinityToggleHint: 'New sessions prefer accounts previously used by this client', + affinityBase: 'Base Limit (Green Zone)', + affinityBasePlaceholder: 'Empty = no limit', + affinityBaseHint: 'Max clients in green zone (full priority scheduling)', + affinityBaseOffHint: 'No green zone limit. All clients receive full priority scheduling.', + affinityBuffer: 'Buffer (Yellow Zone)', + affinityBufferPlaceholder: 'e.g. 3', + affinityBufferHint: 'Additional clients allowed in the yellow zone (degraded priority)', + affinityBufferInfinite: 'Unlimited', + expired: 'Expired', + proxy: 'Proxy', + noProxy: 'No Proxy', + concurrency: 'Concurrency', + loadFactor: 'Load Factor', + loadFactorHint: 'Higher load factor increases scheduling frequency', + priority: 'Priority', + priorityHint: 'Lower value accounts are used first', + billingRateMultiplier: 'Billing Rate Multiplier', + billingRateMultiplierHint: '0 = free, affects account billing only', + expiresAt: 'Expires At', + expiresAtHint: 'Leave empty for no expiration', + higherPriorityFirst: 'Lower value means higher priority', + mixedScheduling: 'Use in /v1/messages', + mixedSchedulingHint: 'Enable to participate in Anthropic/Gemini group scheduling', + mixedSchedulingTooltip: + '!! WARNING !! Antigravity Claude and Anthropic Claude cannot be used in the same context. If you have both Anthropic and Antigravity accounts, enabling this option will cause frequent 400 errors. When enabled, please use the group feature to isolate Antigravity accounts from Anthropic accounts. Make sure you understand this before enabling!!', + aiCreditsBalance: 'AI Credits', + allowOverages: 'Allow Overages (AI Credits)', + allowOveragesTooltip: + 'Only use AI Credits after free quota is explicitly exhausted. Ordinary concurrent 429 rate limits will not switch to overages.', + creating: 'Creating...', + updating: 'Updating...', + accountCreated: 'Account created successfully', + accountUpdated: 'Account updated successfully', + failedToCreate: 'Failed to create account', + failedToUpdate: 'Failed to update account', + pleaseSelectStatus: 'Please select a valid account status', + mixedChannelWarningTitle: 'Mixed Channel Warning', + mixedChannelWarning: 'Warning: Group "{groupName}" contains both {currentPlatform} and {otherPlatform} accounts. Mixing different channels may cause thinking block signature validation issues, which will fallback to non-thinking mode. Are you sure you want to continue?', + pleaseEnterAccountName: 'Please enter account name', + pleaseEnterApiKey: 'Please enter API Key', + bedrockAccessKeyId: 'AWS Access Key ID', + bedrockSecretAccessKey: 'AWS Secret Access Key', + bedrockSessionToken: 'AWS Session Token', + bedrockRegion: 'AWS Region', + bedrockRegionHint: 'e.g. us-east-1, us-west-2, eu-west-1', + bedrockForceGlobal: 'Force Global cross-region inference', + bedrockForceGlobalHint: 'When enabled, model IDs use the global. prefix (e.g. global.anthropic.claude-...), routing requests to any supported region worldwide for higher availability', + bedrockAccessKeyIdRequired: 'Please enter AWS Access Key ID', + bedrockSecretAccessKeyRequired: 'Please enter AWS Secret Access Key', + bedrockRegionRequired: 'Please select AWS Region', + bedrockSessionTokenHint: 'Optional, for temporary credentials', + bedrockSecretKeyLeaveEmpty: 'Leave empty to keep current key', + bedrockAuthMode: 'Authentication Mode', + bedrockAuthModeSigv4: 'SigV4 Signing', + bedrockAuthModeApikey: 'Bedrock API Key', + bedrockApiKeyLabel: 'Bedrock API Key', + bedrockApiKeyDesc: 'Bearer Token', + bedrockApiKeyInput: 'API Key', + bedrockApiKeyRequired: 'Please enter Bedrock API Key', + bedrockApiKeyLeaveEmpty: 'Leave empty to keep current key', + apiKeyIsRequired: 'API Key is required', + leaveEmptyToKeep: 'Leave empty to keep current key', + // Upstream type + upstream: { + baseUrl: 'Upstream Base URL', + baseUrlHint: 'The address of the upstream Antigravity service, e.g., https://cloudcode-pa.googleapis.com', + apiKey: 'Upstream API Key', + apiKeyHint: 'API Key for the upstream service', + pleaseEnterBaseUrl: 'Please enter upstream Base URL', + pleaseEnterApiKey: 'Please enter upstream API Key' + }, + // OAuth flow + oauth: { + title: 'Claude Account Authorization', + authMethod: 'Authorization Method', + manualAuth: 'Manual Authorization', + cookieAutoAuth: 'Cookie Auto-Auth', + cookieAutoAuthDesc: + 'Use claude.ai sessionKey to automatically complete OAuth authorization without manually opening browser.', + sessionKey: 'sessionKey', + keysCount: '{count} keys', + batchCreateAccounts: 'Will batch create {count} accounts', + sessionKeyPlaceholder: + 'One sessionKey per line, e.g.:\nsk-ant-sid01-xxxxx...\nsk-ant-sid01-yyyyy...', + sessionKeyPlaceholderSingle: 'sk-ant-sid01-xxxxx...', + howToGetSessionKey: 'How to get sessionKey', + step1: 'Login to claude.ai in your browser', + step2: 'Press F12 to open Developer Tools', + step3: 'Go to Application tab', + step4: 'Find Cookies → https://claude.ai', + step5: 'Find the row with key sessionKey', + step6: 'Copy the Value', + sessionKeyFormat: 'sessionKey usually starts with sk-ant-sid01-', + startAutoAuth: 'Start Auto-Auth', + authorizing: 'Authorizing...', + followSteps: 'Follow these steps to authorize your Claude account:', + step1GenerateUrl: 'Click the button below to generate the authorization URL', + generateAuthUrl: 'Generate Auth URL', + generating: 'Generating...', + regenerate: 'Regenerate', + step2OpenUrl: 'Open the URL in your browser and complete authorization', + openUrlDesc: + 'Open the authorization URL in a new tab, log in to your Claude account and authorize.', + proxyWarning: + 'Note: If you configured a proxy, make sure your browser uses the same proxy to access the authorization page.', + step3EnterCode: 'Enter the Authorization Code', + authCodeDesc: + 'After authorization is complete, the page will display an Authorization Code. Copy and paste it below:', + authCode: 'Authorization Code', + authCodePlaceholder: 'Paste the Authorization Code from Claude page...', + authCodeHint: 'Paste the Authorization Code copied from the Claude page', + completeAuth: 'Complete Authorization', + verifying: 'Verifying...', + pleaseEnterSessionKey: 'Please enter at least one valid sessionKey', + authFailed: 'Authorization failed', + cookieAuthFailed: 'Cookie authorization failed', + keyAuthFailed: 'Key {index}: {error}', + successCreated: 'Successfully created {count} account(s)', + batchSuccess: 'Successfully created {count} account(s)', + batchPartialSuccess: 'Partial success: {success} succeeded, {failed} failed', + batchFailed: 'Batch creation failed', + // OpenAI specific + openai: { + title: 'OpenAI Account Authorization', + followSteps: 'Follow these steps to complete OpenAI account authorization:', + step1GenerateUrl: 'Click the button below to generate the authorization URL', + generateAuthUrl: 'Generate Auth URL', + step2OpenUrl: 'Open the URL in your browser and complete authorization', + openUrlDesc: + 'Open the authorization URL in a new tab, log in to your OpenAI account and authorize.', + importantNotice: + 'Important: The page may take a while to load after authorization. Please wait patiently. When the browser address bar changes to http://localhost..., the authorization is complete.', + step3EnterCode: 'Enter Authorization URL or Code', + authCodeDesc: + 'After authorization is complete, when the page URL becomes http://localhost:xxx/auth/callback?code=...:', + authCode: 'Authorization URL or Code', + authCodePlaceholder: + 'Option 1: Copy the complete URL\n(http://localhost:xxx/auth/callback?code=...)\nOption 2: Copy only the code parameter value', + authCodeHint: + 'You can copy the entire URL or just the code parameter value, the system will auto-detect', + failedToGenerateUrl: 'Failed to generate OpenAI auth URL', + failedToExchangeCode: 'Failed to exchange OpenAI auth code', + failedToValidateRT: 'Failed to validate refresh token', + errors: { + OPENAI_OAUTH_PROXY_REQUIRED: + 'No proxy is configured and this server could not reach OpenAI directly, so the OpenAI OAuth request failed. Select a proxy that can access OpenAI and retry; if the authorization code has expired, regenerate the authorization URL.' + }, + // Refresh Token auth + refreshTokenAuth: 'Manual RT Input', + refreshTokenDesc: 'Enter your existing OpenAI Refresh Token(s). Supports batch input (one per line). The system will automatically validate and create accounts.', + refreshTokenPlaceholder: 'Paste your OpenAI Refresh Token...\nSupports multiple, one per line', + codexSessionAuth: 'Codex JSON / AT Batch Input', + codexSessionDesc: 'Paste Codex JSON or an accessToken. Accounts use the step 1 settings.', + codexSessionInputLabel: 'Codex JSON or accessToken', + codexSessionPlaceholder: 'Multiple lines supported, one token or JSON per line', + codexSessionHint: 'sessionToken will not be saved as refresh_token. Without refresh_token, the account expires with the accessToken expiry; import is rejected if the expiry cannot be parsed and step 1 has no expiration.', + codexSessionImportAndCreate: 'Import & Create Account', + codexSessionEmpty: 'Please enter Codex JSON or accessToken', + codexSessionImportFailed: 'Failed to import Codex account', + codexSessionImportSuccess: 'Import completed: created {created}, updated {updated}, skipped {skipped}', + codexSessionImportPartial: 'Partial success: created {created}, updated {updated}, skipped {skipped}, failed {failed}', + codexPatAuth: 'Codex Personal Access Token', + codexPatDesc: 'Enter a Codex at- personal access token. The system validates it with OpenAI whoami before creating the account.', + codexPatInputLabel: 'Codex PAT', + codexPatPlaceholder: 'at-...', + codexPatHint: 'This is a separate auth mode. It does not save refresh_token or write an OAuth access_token expiration.', + codexPatImportAndCreate: 'Validate & Create Codex PAT Account', + codexPatEmpty: 'Please enter a Codex personal access token', + codexPatImportFailed: 'Failed to create Codex PAT account', + sessionTokenAuth: 'Manual ST Input', + sessionTokenDesc: 'Enter your existing Session Token(s). Supports batch input (one per line). The system will automatically validate and create accounts.', + sessionTokenPlaceholder: 'Paste your Session Token...\nSupports multiple, one per line', + sessionTokenRawLabel: 'Raw Input', + sessionTokenRawPlaceholder: 'Paste /api/auth/session raw payload or Session Token...', + sessionTokenRawHint: 'You can paste full JSON. The system will auto-parse ST and AT.', + openSessionUrl: 'Open Fetch URL', + copySessionUrl: 'Copy URL', + sessionUrlHint: 'This URL usually returns AT. If sessionToken is absent, copy __Secure-next-auth.session-token from browser cookies as ST.', + parsedSessionTokensLabel: 'Parsed ST', + parsedSessionTokensEmpty: 'No ST parsed. Please check your input.', + parsedAccessTokensLabel: 'Parsed AT', + validating: 'Validating...', + validateAndCreate: 'Validate & Create Account', + pleaseEnterRefreshToken: 'Please enter Refresh Token', + pleaseEnterSessionToken: 'Please enter Session Token' + }, + grok: { + title: 'Grok Account Authorization', + followSteps: 'Follow these steps to authorize your xAI/Grok account:', + step1GenerateUrl: 'Generate the xAI authorization URL', + generateAuthUrl: 'Generate Auth URL', + step2OpenUrl: 'Open the URL in your browser and complete authorization', + openUrlDesc: 'Open the authorization URL in a new tab, sign in to xAI, and authorize API access.', + importantNotice: 'When the browser reaches the local callback URL, copy the full URL or the code query parameter back here.', + step3EnterCode: 'Enter Authorization URL or Code', + authCodeDesc: 'After authorization, paste the callback URL, query string, or authorization code:', + authCode: 'Authorization URL or Code', + authCodePlaceholder: 'Paste the full callback URL, ?code=... query string, or code value', + authCodeHint: 'Full callback URLs, query strings, and bare codes are accepted.', + refreshTokenAuth: 'Manual RT Input', + refreshTokenDesc: 'Enter existing xAI refresh token(s). Supports batch input, one per line.', + refreshTokenPlaceholder: 'Paste your xAI refresh token...\nSupports multiple, one per line', + validating: 'Validating...', + validateAndCreate: 'Validate & Create Account', + pleaseEnterRefreshToken: 'Please enter Refresh Token', + failedToGenerateUrl: 'Failed to generate Grok auth URL', + missingExchangeParams: 'Missing authorization code, state, or OAuth session', + failedToExchangeCode: 'Failed to exchange Grok authorization code', + failedToValidateRT: 'Failed to validate Grok refresh token', + oauthOnlyHint: 'Initial Grok support is OAuth subscription-backed Responses API text and reasoning traffic only.' + }, + // Gemini specific + gemini: { + title: 'Gemini Account Authorization', + followSteps: 'Follow these steps to authorize your Gemini account:', + step1GenerateUrl: 'Generate the authorization URL', + generateAuthUrl: 'Generate Auth URL', + projectIdLabel: 'Project ID (optional)', + projectIdPlaceholder: 'e.g. my-gcp-project or cloud-ai-companion-xxxxx', + projectIdHint: + 'Leave empty to auto-detect after code exchange. If auto-detection fails, fill it in and re-generate the auth URL to try again.', + howToGetProjectId: 'How to get', + step2OpenUrl: 'Open the URL in your browser and complete authorization', + openUrlDesc: + 'Open the authorization URL in a new tab, log in to your Google account and authorize.', + step3EnterCode: 'Enter Authorization URL or Code', + authCodeDesc: + 'After authorization, copy the callback URL (recommended) or just the code and paste it below.', + authCode: 'Callback URL or Code', + authCodePlaceholder: + 'Option 1 (recommended): Paste the callback URL\nOption 2: Paste only the code value', + authCodeHint: 'The system will auto-extract code/state from the URL.', + redirectUri: 'Redirect URI', + redirectUriHint: + 'This must be configured in your Google OAuth client and must match exactly.', + confirmRedirectUri: + 'I have configured this Redirect URI in the Google OAuth client (must match exactly)', + invalidRedirectUri: 'Redirect URI must be a valid http(s) URL', + redirectUriNotConfirmed: 'Please confirm the Redirect URI is configured correctly', + missingRedirectUri: 'Missing redirect URI', + failedToGenerateUrl: 'Failed to generate Gemini auth URL', + missingExchangeParams: 'Missing auth code, session ID, or state', + failedToExchangeCode: 'Failed to exchange Gemini auth code', + missingProjectId: 'GCP Project ID retrieval failed: Your Google account is not linked to an active GCP project. Please activate GCP and bind a credit card in Google Cloud Console, or manually enter the Project ID during authorization.', + modelPassthrough: 'Gemini Model Passthrough', + modelPassthroughDesc: + 'All model requests are forwarded directly to the Gemini API without model restrictions or mappings.', + stateWarningTitle: 'Note', + stateWarningDesc: 'Recommended: paste the full callback URL (includes code & state).', + oauthTypeLabel: 'OAuth Type', + needsProjectId: 'Built-in OAuth (Code Assist)', + needsProjectIdDesc: 'Requires GCP project and Project ID', + noProjectIdNeeded: 'Custom OAuth (AI Studio)', + noProjectIdNeededDesc: 'Requires admin-configured OAuth client', + aiStudioNotConfiguredShort: 'Not configured', + aiStudioNotConfiguredTip: + 'AI Studio OAuth is not configured: set GEMINI_OAUTH_CLIENT_ID / GEMINI_OAUTH_CLIENT_SECRET and add Redirect URI: http://localhost:1455/auth/callback (Consent screen scopes must include https://www.googleapis.com/auth/generative-language.retriever)', + aiStudioNotConfigured: + 'AI Studio OAuth is not configured: set GEMINI_OAUTH_CLIENT_ID / GEMINI_OAUTH_CLIENT_SECRET and add Redirect URI: http://localhost:1455/auth/callback' + }, + // Antigravity specific + antigravity: { + title: 'Antigravity Account Authorization', + followSteps: 'Follow these steps to authorize your Antigravity account:', + step1GenerateUrl: 'Generate the authorization URL', + generateAuthUrl: 'Generate Auth URL', + step2OpenUrl: 'Open the URL in your browser and complete authorization', + openUrlDesc: 'Open the authorization URL in a new tab, log in to your Google account and authorize.', + importantNotice: + 'Important: The page may take a while to load after authorization. Please wait patiently. When the browser address bar shows http://localhost..., authorization is complete.', + step3EnterCode: 'Enter Authorization URL or Code', + authCodeDesc: + 'After authorization, when the page URL becomes http://localhost:xxx/auth/callback?code=...:', + authCode: 'Authorization URL or Code', + authCodePlaceholder: + 'Option 1: Copy the complete URL\n(http://localhost:xxx/auth/callback?code=...)\nOption 2: Copy only the code parameter value', + authCodeHint: 'You can copy the entire URL or just the code parameter value, the system will auto-detect', + failedToGenerateUrl: 'Failed to generate Antigravity auth URL', + missingExchangeParams: 'Missing code, session ID, or state', + failedToExchangeCode: 'Failed to exchange Antigravity auth code', + // Refresh Token auth + refreshTokenAuth: 'Manual RT', + refreshTokenDesc: 'Enter your existing Antigravity Refresh Token. Supports batch input (one per line). The system will automatically validate and create accounts.', + refreshTokenPlaceholder: 'Paste your Antigravity Refresh Token...\nSupports multiple tokens, one per line', + validating: 'Validating...', + validateAndCreate: 'Validate & Create', + pleaseEnterRefreshToken: 'Please enter Refresh Token', + failedToValidateRT: 'Failed to validate Refresh Token' + } + }, // Gemini specific (platform-wide) + gemini: { + helpButton: 'Help', + helpDialog: { + title: 'Gemini Usage Guide', + apiKeySection: 'API Key Links' + }, + modelPassthrough: 'Gemini Model Passthrough', + modelPassthroughDesc: + 'All model requests are forwarded directly to the Gemini API without model restrictions or mappings.', + baseUrlHint: 'Leave default for official Gemini API', + apiKeyHint: 'Your Gemini API Key (starts with AIza)', + tier: { + label: 'Account Tier', + hint: 'Tip: The system will try to auto-detect the tier first; if auto-detection is unavailable or fails, your selected tier is used as a fallback (simulated quota).', + aiStudioHint: + 'AI Studio quotas are per-model (Pro/Flash are limited independently). If billing is enabled, choose Pay-as-you-go.', + googleOne: { + free: 'Google One Free', + pro: 'Google One Pro', + ultra: 'Google One Ultra' + }, + gcp: { + standard: 'GCP Standard', + enterprise: 'GCP Enterprise' + }, + aiStudio: { + free: 'Google AI Free', + paid: 'Google AI Pay-as-you-go' + } + }, + accountType: { + oauthTitle: 'OAuth (Gemini)', + oauthDesc: 'Authorize with your Google account and choose an OAuth type.', + apiKeyTitle: 'API Key (AI Studio)', + apiKeyDesc: 'Fastest setup. Use an AIza API key.', + apiKeyNote: + 'Best for light testing. Free tier has strict rate limits and data may be used for training.', + apiKeyLink: 'Get API Key', + quotaLink: 'Quota guide' + }, + oauthType: { + builtInTitle: 'Built-in OAuth (Gemini CLI / Code Assist)', + builtInDesc: 'Uses Google built-in client ID. No admin configuration required.', + builtInRequirement: 'Requires a GCP project and Project ID.', + googleOneDesc: 'Personal account with Google One subscription quota', + codeAssistDesc: 'Enterprise-grade, requires a GCP project', + codeAssistRequirement: 'Requires an active GCP project with billing enabled', + showAdvanced: 'Show advanced options (custom OAuth Client)', + hideAdvanced: 'Hide advanced options (custom OAuth Client)', + gcpProjectLink: 'Create project', + customTitle: 'Custom OAuth (AI Studio OAuth)', + customDesc: 'Uses admin-configured OAuth client for org management.', + customRequirement: 'Admin must configure Client ID and add you as a test user.', + badges: { + recommended: 'Recommended', + highConcurrency: 'High concurrency', + individuals: 'Recommended for individuals', + noGcp: 'No GCP required', + enterprise: 'Enterprise users', + noAdmin: 'No admin setup', + orgManaged: 'Org managed', + adminRequired: 'Admin required' + } + }, + setupGuide: { + title: 'Gemini Setup Checklist', + checklistTitle: 'Checklist', + checklistItems: { + usIp: 'Use a US IP and ensure your account country is set to US.', + age: 'Account must be 18+.' + }, + activationTitle: 'One-click Activation', + activationItems: { + geminiWeb: 'Activate Gemini Web to avoid User not initialized.', + gcpProject: 'Activate a GCP project and get the Project ID for Code Assist.' + }, + links: { + countryCheck: 'Check country association', + countryChange: 'Change country association', + geminiWebActivation: 'Activate Gemini Web', + gcpProject: 'Open GCP Console' + } + }, + quotaPolicy: { + title: 'Gemini Quota & Limit Policy (Reference)', + note: 'Note: Gemini does not provide an official quota inquiry API. The "Daily Quota" shown here is an estimate simulated by the system based on account tiers for scheduling reference only. Please refer to official Google errors for actual limits.', + columns: { + channel: 'Auth Channel', + account: 'Account Status', + limits: 'Limit Policy', + docs: 'Official Docs' + }, + docs: { + codeAssist: 'Code Assist Quotas', + aiStudio: 'AI Studio Pricing', + vertex: 'Vertex AI Quotas' + }, + simulatedNote: 'Simulated quota, for reference only', + rows: { + googleOne: { + channel: 'Google One OAuth (Individuals / Code Assist for Individuals)', + limitsFree: 'Shared pool: 1000 RPD / 60 RPM', + limitsPro: 'Shared pool: 1500 RPD / 120 RPM', + limitsUltra: 'Shared pool: 2000 RPD / 120 RPM' + }, + gcp: { + channel: 'GCP Code Assist OAuth (Enterprise)', + limitsStandard: 'Shared pool: 1500 RPD / 120 RPM', + limitsEnterprise: 'Shared pool: 2000 RPD / 120 RPM' + }, + cli: { + channel: 'Gemini CLI (Official Google Login / Code Assist)', + free: 'Free Google Account', + premium: 'Google One AI Premium', + limitsFree: 'RPD ~1000; RPM ~60 (soft)', + limitsPremium: 'RPD ~1500+; RPM ~60+ (priority queue)' + }, + gcloud: { + channel: 'GCP Code Assist (gcloud auth)', + account: 'No Code Assist subscription', + limits: 'RPD ~1000; RPM ~60 (preview)' + }, + aiStudio: { + channel: 'AI Studio API Key / OAuth', + free: 'No billing (free tier)', + paid: 'Billing enabled (pay-as-you-go)', + limitsFree: 'RPD 50; RPM 2 (Pro) / 15 (Flash)', + limitsPaid: 'RPD unlimited; RPM 1000 (Pro) / 2000 (Flash) (per model)' + }, + customOAuth: { + channel: 'Custom OAuth Client (GCP)', + free: 'Project not billed', + paid: 'Project billed', + limitsFree: 'RPD 50; RPM 2 (project quota)', + limitsPaid: 'RPD unlimited; RPM 1000+ (project quota)' + } + } + }, + rateLimit: { + ok: 'Not rate limited', + unlimited: 'Unlimited', + limited: 'Rate limited {time}', + now: 'now' + } + }, + // Re-Auth Modal + reAuthorizeAccount: 'Re-Authorize Account', + claudeCodeAccount: 'Claude Code Account', + openaiAccount: 'OpenAI Account', + geminiAccount: 'Gemini Account', + antigravityAccount: 'Antigravity Account', + grokAccount: 'Grok Account', + inputMethod: 'Input Method', + reAuthorizedSuccess: 'Account re-authorized successfully', + // Test Modal + testAccountConnection: 'Test Account Connection', + account: 'Account', + readyToTest: 'Ready to test. Click "Start Test" to begin...', + connectingToApi: 'Connecting to API...', + testCompleted: 'Test completed successfully!', + testFailed: 'Test failed', + connectedToApi: 'Connected to API', + usingModel: 'Using model: {model}', + sendingTestMessage: 'Sending test message: "hi"', + sendingImageRequest: 'Sending image generation test request...', + response: 'Response:', + startTest: 'Start Test', + testing: 'Testing...', + retry: 'Retry', + copyOutput: 'Copy output', + outputCopied: 'Output copied', + startingTestForAccount: 'Starting test for account: {name}', + testAccountTypeLabel: 'Account type: {type}', + selectTestModel: 'Select Test Model', + testModel: 'Test model', + testPrompt: 'Prompt: "hi"', + imagePromptLabel: 'Image prompt', + imagePromptPlaceholder: 'Example: Generate an orange cat astronaut sticker in pixel-art style on a solid background.', + imagePromptDefault: 'Generate a cute orange cat astronaut sticker on a clean pastel background.', + imageTestHint: 'When an image model is selected, this test sends a real image-generation request and previews the returned image below.', + imageTestMode: 'Mode: Image generation test', + imagePreview: 'Generated images:', + imageReceived: 'Received test image #{count}', + // Stats Modal + viewStats: 'View Stats', + usageStatistics: 'Usage Statistics', + last30DaysUsage: 'Last 30 days usage statistics (based on actual usage days)', + stats: { + totalCost: '30-Day Total Cost', + accumulatedCost: 'Accumulated cost', + standardCost: 'Standard', + totalRequests: '30-Day Total Requests', + totalCalls: 'Total API calls', + avgDailyCost: 'Daily Avg Cost', + basedOnActualDays: 'Based on {days} actual usage days', + avgDailyRequests: 'Daily Avg Requests', + avgDailyUsage: 'Average daily usage', + todayOverview: 'Today Overview', + cost: 'Cost', + requests: 'Requests', + tokens: 'Tokens', + highestCostDay: 'Highest Cost Day', + highestRequestDay: 'Highest Request Day', + date: 'Date', + accumulatedTokens: 'Accumulated Tokens', + totalTokens: '30-Day Total', + dailyAvgTokens: 'Daily Average', + performance: 'Performance', + avgResponseTime: 'Avg Response', + daysActive: 'Days Active', + recentActivity: 'Recent Activity', + todayRequests: 'Today Requests', + todayTokens: 'Today Tokens', + todayCost: 'Today Cost', + usageTrend: '30-Day Cost & Request Trend', + noData: 'No usage data available for this account' + }, + usageWindow: { + statsTitle: '5-Hour Window Usage Statistics', + statsTitleDaily: 'Daily Usage Statistics', + geminiProDaily: 'Pro', + geminiFlashDaily: 'Flash', + gemini3Pro: 'G3P', + gemini3Flash: 'G3F', + gemini3Image: 'G31FI', + claude: 'Claude', + grokRequests: 'Req', + grokTokens: 'Tok', + grokUnknown: 'Grok quota is unknown until the first upstream response includes xAI rate-limit headers.', + grokRetryAfter: 'Retry after {time}', + grokProbe: 'Probe', + grokProbeTooltip: 'Send a minimal xAI Responses probe and read quota headers', + grokResetUnsupported: 'Reset unsupported', + grokResetUnsupportedTooltip: 'xAI does not expose reset credits for Grok OAuth accounts', + grokNoHeaders: 'No quota headers observed', + grokLastStatus: 'Status {status}', + grokLastProbe: 'Probe {time}', + grokLastHeadersSeen: 'Headers {time}', + passiveSampled: 'Passive', + activeQuery: 'Query' + }, + openaiQuotaReset: { + count: 'Credits', + reset: 'Reset', + countTooltipLoad: 'Click to load the available reset-credit count', + countTooltipRefresh: 'Click to refresh the available reset-credit count', + resetTooltipReady: 'Consume 1 reset credit to immediately restore the window', + resetTooltipNeedQuery: 'Click Credits first to load the available count', + resetTooltipNoCredits: 'No reset credits available', + resetTooltipShadow: 'Spark shadow accounts cannot reset credits; reset on the parent account', + expiresAt: 'Expires {time}', + expiresAtFull: 'Reset credit expires at {time}', + expandExpirations: 'Expand the other {count} reset credit expiration(s)', + collapseExpirations: 'Collapse reset credit expirations', + expirationDetails: 'Reset credit expiration details', + noCreditsAvailable: 'No reset credits available', + resetSuccess: 'Reset {windows} window(s)', + confirmTitle: 'Confirm Weekly Limit Reset', + confirmMessage: 'This will consume 1 reset credit to immediately restore the current window ({count} remaining). This action cannot be undone. Continue?' + }, + tier: { + free: 'Free', + pro: 'Pro', + ultra: 'Ultra', + aiPremium: 'AI Premium', + standard: 'Standard', + basic: 'Basic', + personal: 'Personal', + unlimited: 'Unlimited' + }, + ineligibleWarning: + 'This account is not eligible for Antigravity, but API forwarding still works. Use at your own risk.', + forbidden: 'Forbidden', + forbiddenValidation: 'Verification Required', + forbiddenViolation: 'Violation Ban', + openVerification: 'Open Verification Link', + copyLink: 'Copy Link', + linkCopied: 'Link Copied', + needsReauth: 'Re-auth Required', + rateLimited: 'Rate Limited', + usageError: 'Fetch Error' + }, + + // Scheduled Tests +} diff --git a/frontend/src/i18n/locales/en/admin/channels.ts b/frontend/src/i18n/locales/en/admin/channels.ts new file mode 100644 index 0000000000..399b9f3208 --- /dev/null +++ b/frontend/src/i18n/locales/en/admin/channels.ts @@ -0,0 +1,714 @@ +export default { + availableChannels: { + title: 'Available Channels', + description: 'Aggregated view: each channel with its linked groups and supported models (wildcards expanded)', + searchPlaceholder: 'Search channels or models...', + columns: { + name: 'Channel', + status: 'Status', + billingSource: 'Billing Model Source', + groups: 'Linked Groups', + supportedModels: 'Supported Models' + }, + empty: 'No data', + noGroups: 'No linked groups', + noModels: 'No model mapping configured', + noPricing: 'Pricing not configured', + statusActive: 'Active', + statusDisabled: 'Disabled', + billingSource: { + requested: 'Requested model', + upstream: 'Upstream model', + channel_mapped: 'Channel-mapped model' + }, + pricing: { + billingMode: 'Billing Mode', + billingModeToken: 'Per Token', + billingModePerRequest: 'Per Request', + billingModeImage: 'Per Image', + inputPrice: 'Input', + outputPrice: 'Output', + cacheWritePrice: 'Cache Write', + cacheReadPrice: 'Cache Read', + imageOutputPrice: 'Image Output', + perRequestPrice: 'Per Request', + intervals: 'Tiered Pricing', + unitPerMillion: '/ 1M tokens', + unitPerRequest: '/ request' + } + }, + + // Channel Management + channels: { + title: 'Channel Management', + description: 'Manage channels and custom model pricing', + searchChannels: 'Search channels...', + createChannel: 'Create Channel', + editChannel: 'Edit Channel', + deleteChannel: 'Delete Channel', + statusActive: 'Active', + statusDisabled: 'Disabled', + allStatus: 'All Status', + groupsUnit: 'groups', + pricingUnit: 'pricing rules', + noChannelsYet: 'No Channels Yet', + createFirstChannel: 'Create your first channel to manage model pricing', + loadError: 'Failed to load channels', + createSuccess: 'Channel created', + updateSuccess: 'Channel updated', + deleteSuccess: 'Channel deleted', + createError: 'Failed to create channel', + updateError: 'Failed to update channel', + deleteError: 'Failed to delete channel', + nameRequired: 'Please enter a channel name', + duplicateModels: 'Model "{0}" appears in multiple pricing entries', + modelConflict: "Model patterns '{model1}' and '{model2}' conflict: overlapping match range. Model names are matched case-insensitively, so an existing entry already covers all case variants — no need to add the variant separately.", + mappingConflict: "Mapping source patterns '{model1}' and '{model2}' conflict: overlapping match range. Source patterns are matched case-insensitively, so an existing entry already covers all case variants.", + intervalValidation: { + negativeMin: 'Interval #{index}: minimum token count ({value}) cannot be negative', + maxPositive: 'Interval #{index}: maximum token count ({value}) must be greater than 0', + maxGreaterThanMin: 'Interval #{index}: maximum token count ({max}) must be greater than minimum token count ({min})', + negativePrice: 'Interval #{index}: {field} cannot be negative', + unboundedLast: 'Interval #{index}: an unbounded interval (empty maximum token count) must be last', + overlap: 'Intervals #{previousIndex} and #{currentIndex} overlap: previous upper bound ({previousMax}) is greater than current lower bound ({currentMin})', + price: { + inputPrice: 'input price', + outputPrice: 'output price', + cacheWritePrice: 'cache write price', + cacheReadPrice: 'cache read price', + perRequestPrice: 'per-request price' + } + }, + deleteConfirm: 'Are you sure you want to delete channel "{name}"? This cannot be undone.', + columns: { + name: 'Name', + description: 'Description', + status: 'Status', + groups: 'Groups', + pricing: 'Pricing', + createdAt: 'Created', + actions: 'Actions' + }, + billingMode: { + token: 'Token', + perRequest: 'Per Request', + image: 'Image (Per Request)' + }, + form: { + name: 'Name', + namePlaceholder: 'Enter channel name', + description: 'Description', + descriptionPlaceholder: 'Optional description', + status: 'Status', + groups: 'Associated Groups', + noGroupsAvailable: 'No groups available', + inOtherChannel: 'In "{name}"', + modelPricing: 'Model Pricing', + models: 'Models', + modelsPlaceholder: 'Type full model name and press Enter', + modelInputHint: 'Press Enter to add, supports paste for batch import.', + billingMode: 'Billing Mode', + defaultPrices: 'Default prices (fallback when no interval matches)', + inputPrice: 'Input', + outputPrice: 'Output', + cacheWritePrice: 'Cache Write', + cacheReadPrice: 'Cache Read', + cacheWritePriceShort: 'Cache W', + cacheReadPriceShort: 'Cache R', + imageTokenPrice: 'Image Output', + imageOutputPrice: 'Image Output Price', + pricePlaceholder: 'Default', + intervals: 'Context Intervals (optional)', + minTokens: 'Min', + maxTokens: 'Max', + inclusive: '(inclusive)', + addInterval: 'Add Interval', + requestTiers: 'Request Tiers', + imageTiers: 'Image Tiers (Per Request)', + addTier: 'Add Tier', + noTiersYet: 'No tiers yet. Click add to configure per-request pricing.', + noPricingRules: 'No pricing rules yet. Click "Add" to create one.', + perRequestPrice: 'Price per Request', + perRequestPriceRequired: 'Per-request price or billing tiers required for per-request/image billing mode', + tierLabel: 'Tier', + resolution: 'Resolution', + modelMapping: 'Model Mapping', + modelMappingHint: 'Map request model names to actual model names. Runs before account-level mapping.', + noMappingRules: 'No mapping rules. Click "Add" to create one.', + mappingSource: 'Source model', + mappingTarget: 'Target model', + billingModelSource: 'Billing Model', + billingModelSourceChannelMapped: 'Bill by channel-mapped model', + billingModelSourceRequested: 'Bill by requested model', + billingModelSourceUpstream: 'Bill by final upstream model', + billingModelSourceHint: 'Controls which model name is used for pricing lookup', + selectedCount: '{count} selected', + searchGroups: 'Search groups...', + noGroupsMatch: 'No groups match your search', + restrictModels: 'Restrict Models', + restrictModelsHint: 'When enabled, only models in the pricing list are allowed. Others will be rejected.', + defaultPerRequestPrice: 'Default per-request price (fallback when no tier matches)', + defaultImagePrice: 'Default image price (fallback when no tier matches)', + platformConfig: 'Platform Configuration', + webSearchEmulation: 'Web Search Emulation', + webSearchEmulationHint: '⚠️ When enabled, all accounts in this channel\'s Anthropic groups will intercept web_search requests. Use with caution.', + webSearchEmulationGlobalDisabled: 'Please enable the global switch first in Settings → Gateway → Web Search Emulation', + codexImageGenerationBridge: 'Codex Image Generation Bridge', + codexImageGenerationBridgeHint: 'When enabled, Codex /responses text requests in OpenAI groups may be automatically given the image_generation tool. Keep off unless the routed accounts support image generation.', + bedrockCCCompat: 'Bedrock CC Compatibility', + bedrockCCCompatHint: '⚠️ When enabled, requests to Bedrock accounts in this channel will be transformed for Claude Code compatibility (thinking type conversion, tool_use ID sanitization).', + basicSettings: 'Basic Settings', + addPlatform: 'Add Platform', + noPlatforms: 'Click "Add Platform" to start configuring the channel', + mappingCount: 'mappings', + pricingEntry: 'Pricing Entry', + noModels: 'No models added', + applyPricingToAccountStats: 'Apply Pricing to Account Stats', + applyPricingToAccountStatsDesc: 'When enabled, requests not matched by custom rules will use standard model pricing for account stats calculation', + accountStatsPricingRules: 'Custom Account Stats Pricing Rules', + addRule: 'Add Rule', + noRulesConfigured: 'No custom rules configured. Channel model pricing above will be used.', + ruleName: 'Rule name (optional)', + ruleGroups: 'Groups', + ruleAccounts: 'Accounts', + searchAccountPlaceholder: 'Search accounts...', + ruleAccountsHint: 'Leave empty to match all accounts', + ruleModelPricing: 'Model Pricing', + noGroupsInChannel: 'No groups selected in platform tabs above', + unnamed: 'Unnamed', + syncLatestModels: 'Sync Latest Models', + syncingModels: 'Syncing...', + syncModelsSuccess: 'Synced {count} new model(s)', + syncModelsAlreadyUpToDate: 'Models already up to date', + syncModelsError: 'Failed to sync models' + } + }, + + riskControl: { + title: 'Risk Control', + description: 'Configure content moderation and review audit records', + loadFailed: 'Failed to load risk control', + saveFailed: 'Failed to save content moderation config', + logsFailed: 'Failed to load audit records', + saved: 'Content moderation config saved', + refresh: 'Refresh', + config: 'Content Moderation Config', + configHint: 'Use OpenAI Moderations to score request content and handle threshold hits by mode.', + openSettings: 'Moderation Settings', + settingsTitle: 'Content Moderation Settings', + refreshStatus: 'Refresh Status', + records: 'Audit Records', + recordsHint: 'Shows hits, blocks, errors, and sampled records.', + saveConfig: 'Save Moderation Config', + statusFailed: 'Failed to load runtime status', + enabled: 'Enable Content Moderation', + enabledHint: 'When off, gateway requests are not moderated even if the menu is enabled.', + mode: 'Global Mode', + modePreBlock: 'Pre-Block', + modePreBlockDesc: 'Synchronously reviews the latest user input before every request and rejects hits immediately.', + modeObserve: 'Observe Only', + modeObserveDesc: 'Requests pass through while the latest user input is queued for async review; hits are recorded, notified, and counted.', + modeOff: 'Off', + modeOffDesc: 'Content moderation is disabled and no audit records are written.', + baseUrl: 'OpenAI Base URL', + model: 'Model', + apiKey: 'OpenAI API Key', + apiKeys: 'OpenAI API Keys', + apiKeyCount: '{count} keys', + apiKeyPlaceholder: 'Enter API Key', + apiKeysPlaceholder: 'Add API Keys, one per line. They will be appended on save.', + apiKeysPlaceholderReplace: 'Replace API Keys, one per line. Stored keys will be replaced on save.', + apiKeysPlaceholderKeep: 'Add API Keys, one per line. They will be appended on save.', + apiKeysHint: '{count} keys are currently stored. This input only adds keys; save appends and de-duplicates them.', + apiKeysWriteMode: 'Write mode', + apiKeysModeAppend: 'Add', + apiKeysModeReplace: 'Replace', + apiKeysModeAppendHint: 'Default: save appends input keys and keeps stored keys.', + apiKeysModeReplaceHint: 'Replace mode: save replaces all stored keys with input keys.', + apiKeysReplaceWarning: 'Replace mode', + apiKeysReplaceNoInput: 'Replace mode requires at least 1 API Key', + apiKeyPlaceholderKeep: 'Leave empty to keep current key', + apiKeyWillClear: 'Configured key will be cleared on save', + apiKeyConfigured: 'Configured', + apiKeyTemporary: 'Pending', + apiKeyPendingDelete: 'Pending delete', + apiKeyPendingDeleteCount: '{count} keys pending deletion', + deleteApiKey: 'Delete this key', + undoDeleteApiKey: 'Undo delete', + inputApiKeyCount: '{count} keys in input', + storedApiKeyCount: '{count} stored keys', + testInputApiKeys: 'Test input keys', + testStoredApiKeys: 'Test stored keys', + testContentWithStoredApiKey: 'Test content with stored key', + testingApiKeys: 'Testing', + apiKeyTestNoInput: 'Enter OpenAI API Keys to test first', + apiKeyTestDone: 'Key test completed for {count} keys', + apiKeyTestFailed: 'Failed to test OpenAI API Keys', + apiKeyHealth: 'Key Availability', + apiKeyFreezeRule: '400 does not freeze; 401/403 freeze for 10 minutes; 429/529 freeze for 1 minute; other HTTP errors freeze for 10 seconds.', + apiKeyRows: '{count} keys', + apiKeyRowsCollapsed: '{count} keys hidden', + apiKeyRowsExpanded: 'Showing all {count} keys', + expandApiKeyRows: 'Expand', + collapseApiKeyRows: 'Collapse', + apiKeyHealthEmpty: 'No key status yet', + apiKeyHealthEmptyHint: 'Save keys or test input keys to see availability.', + apiKeyStatusOk: 'Available', + apiKeyStatusError: 'Error', + apiKeyStatusFrozen: 'Frozen', + apiKeyStatusUnknown: 'Untested', + apiKeyFailureCount: '{count} failures', + apiKeyLatency: '{ms} ms', + apiKeyHTTPStatus: 'HTTP {status}', + apiKeyFrozenUntil: 'Frozen until {time}', + apiKeyLastChecked: 'Checked at {time}', + apiKeyNotTested: 'Not tested', + auditTestInput: 'Audit Test Input', + auditTestInputHint: 'Enter a prompt and upload or paste images; images are sent as base64 and are not stored.', + auditTestPromptPlaceholder: 'Enter a user prompt to test; leave empty to only test key availability.', + auditTestImages: 'Test Images', + auditTestImagesHint: 'Upload, drag, or paste images. Up to 1 image, 8MB each.', + addAuditTestImage: 'Add image', + clearAuditTest: 'Clear test', + auditTestImageLimit: 'You can add up to {count} test images', + auditTestImageTooLarge: 'Each test image must be 8MB or smaller', + auditTestImageReadFailed: 'Failed to read test image', + auditTestResult: 'Audit Test Result', + auditTestHighest: 'Top category {category}, score {score}', + auditTestComposite: 'Composite score', + auditTestFlagged: 'Threshold hit', + auditTestPassed: 'Pass', + notConfigured: 'Not configured', + clearApiKey: 'Clear stored key', + keepApiKey: 'Keep stored key', + timeoutMs: 'HTTP Timeout (ms)', + retryCount: 'Retry Count', + sampleRate: 'Sample Rate', + recordNonHits: 'Record Non-Hits', + recordNonHitsHint: 'When enabled, sampled non-hit request summaries are redacted before storage.', + preHashCheck: 'Enable Pre-Hash Check', + preHashCheckHint: 'Hashes from async hits are blocked before moderation; this does not send email or increment ban counters.', + flaggedHashCount: 'Current hash collection size: {count}', + flaggedHashHint: 'Hashes are stored permanently in Redis; paste a full 64-character hash to remove a false block, or clear all stored hashes.', + flaggedHashPlaceholder: 'Paste full 64-character input hash', + deleteFlaggedHash: 'Delete hash', + clearFlaggedHashes: 'Clear all', + clearFlaggedHashesConfirm: 'Clear all risk input hashes? This does not delete audit records, but removes all historical hash blocks.', + flaggedHashDeleted: 'Risk hash deleted', + flaggedHashNotFound: 'Risk hash not found', + flaggedHashDeleteFailed: 'Failed to delete risk hash', + flaggedHashesCleared: 'Cleared {count} risk hashes', + flaggedHashesClearFailed: 'Failed to clear risk hashes', + workerCount: 'Worker Count', + queueSize: 'Async Queue Size', + blockStatus: 'Block HTTP Status', + blockMessage: 'Custom Block Message', + defaultBlockMessage: 'Content audit matched a risk rule. Please adjust your input and try again.', + emailOnHit: 'Email on Hit', + emailOnHitHint: 'When enabled, send a risk-control email on every hit; auto-ban notices are always sent.', + autoBan: 'Auto Ban User', + autoBanHint: 'Disable the user, invalidate auth cache, and send a ban notice after the hit threshold is reached.', + cyberPolicyExcludeBan: 'Exclude Cyber Policy Hits from Ban Count', + cyberPolicyExcludeBanHint: 'When enabled, cyber_policy hits no longer count toward auto-ban violations: no ban judgment on the hit itself, and history rows are excluded from the rolling count. Logs and notice emails are unaffected.', + violationNotCounted: 'Not counted', + banThreshold: 'Ban Threshold', + violationWindowHours: 'Count Window (hours)', + hitRetentionDays: 'Hit Record Retention (days)', + nonHitRetentionDays: 'Non-Hit Record Retention (days, max 3)', + violationCount: '{count} hits', + emailSent: 'Email sent', + emailNotSent: 'No email', + autoBanned: 'Banned', + unbanUser: 'Unban', + unbanSuccess: 'User has been unbanned', + unbanFailed: 'Failed to unban user', + inputDetailTitle: 'Input Summary Detail', + inputDetailContent: 'Full Content', + matchedKeyword: 'Matched Keyword', + queueDelay: 'Queued {ms} ms', + allGroups: 'All Groups', + allGroupsHint: 'Auditing all groups', + selectedGroupsHint: 'Auditing selected groups', + groupScope: 'Audit Groups', + groupScopeHint: 'Switch on for all groups, or turn off to choose specific groups.', + selectedGroups: 'Selected Groups', + searchGroups: 'Search group name or platform', + noGroups: 'No groups available', + modelFilter: 'Model scope', + modelFilterHint: 'Moderate by the client-requested model name; channel model mappings do not change this match.', + modelFilterAll: 'All models', + modelFilterAllDesc: 'All model requests go through content moderation.', + modelFilterInclude: 'Only selected', + modelFilterIncludeDesc: 'Only listed models go through content moderation.', + modelFilterExclude: 'Exclude selected', + modelFilterExcludeDesc: 'Listed models skip content moderation; other models are moderated.', + modelFilterModels: 'Model list', + modelFilterModelCount: '{count} models configured', + modelFilterModelsRequired: 'This model scope requires at least 1 model', + modelFilterAllSummary: 'Applies to all models', + modelFilterIncludeSummary: 'Applies to {count} models', + modelFilterExcludeSummary: 'Excludes {count} models', + emptyLogs: 'No audit records', + preBlockSyncStatus: 'Pre-Block Sync Status', + preBlockSyncHint: 'Live counters for the synchronous moderation path, excluding async record tasks.', + preBlockActive: 'Sync Processing', + preBlockActiveHint: 'Currently checking', + preBlockChecked: 'Checked', + preBlockCheckedHint: 'Entered pre-block path', + preBlockAllowed: 'Allowed', + preBlockAllowedHint: 'No block triggered', + preBlockBlocked: 'Blocked', + preBlockBlockedHint: 'Rejected after hit', + preBlockErrors: 'Audit Errors', + preBlockErrorsHint: 'Failed or no usable key', + preBlockAvgLatency: 'Avg Latency', + preBlockAvgLatencyHint: 'Synchronous path average', + preBlockAPIKeyLoad: 'Audit Key Load', + preBlockAPIKeyLoadHint: 'Synchronous pre-block checks round-robin usable audit keys directly.', + preBlockAPIKeyLoadSummary: 'Sync active {active} / usable keys {available}, {total} total, worker: {workerActive} / {workerTotal}', + preBlockAPIKeyTotals: 'Total {total}, success {success}, errors {errors}', + preBlockAPIKeyLoadEmpty: 'No audit key load data yet', + preBlockKeyActiveShort: 'Active', + preBlockKeyTotalShort: 'Total', + preBlockKeyAvgShort: 'Avg', + preBlockKeyLastShort: 'Last', + workerStatus: 'Worker Runtime', + workerStatusHint: 'Queue and worker pool status for async audit tasks and pre-block record tasks, excluding synchronous pre-block checks.', + workerPool: 'Worker Pool', + workerPoolMeta: '{active} processing, {idle} idle and ready, {total} total', + queueUsage: 'Queue Usage', + activeWorkers: 'Processing', + idleWorkers: 'Idle Ready', + workerActive: 'Processing an async audit or record task', + workerIdle: 'Started, idle and ready', + workerDisabled: 'Risk control or content audit is disabled', + processed: 'Processed', + droppedErrors: 'Dropped / Errors', + autoRefresh: 'Auto refresh every 15s', + lastCleanup: 'Last cleanup: {time}', + cleanupStats: 'Last cleanup deleted {hit} hits and {nonHit} non-hits', + riskSwitchOff: 'System switch off', + riskThresholds: 'Risk Thresholds', + riskThresholdsHint: 'Adjust hit thresholds by OpenAI Moderations category. Scores greater than or equal to the threshold count as hits.', + riskThresholdDefault: 'Default {value}', + riskThresholdReset: 'Restore defaults', + riskThresholdPercent: 'Threshold percentage', + tabs: { + basic: 'Basic', + scope: 'Scope', + runtime: 'Runtime', + response: 'Hit Notice', + riskThresholds: 'Risk Thresholds', + keywords: 'Keyword Block', + retention: 'Retention', + }, + blockedKeywords: 'Blocked keywords', + blockedKeywordsPlaceholder: 'One keyword per line, e.g.:\nbadword1\nbadword2', + blockedKeywordsDescription: 'Matching is case-insensitive. Whether the upstream moderation API is invoked after a hit depends on the strategy below.', + blockedKeywordsPreBlockHint: 'Keyword blocking only takes effect in "Pre-block" mode.', + blockedKeywordsModeWarning: 'Current mode is "{mode}". Keyword blocking will not run until you switch to "Pre-block" mode.', + blockedKeywordCount: '{count} keywords configured', + blockedKeywordsLimit: 'Up to {max} keywords, each no longer than 200 characters. Duplicates are removed automatically.', + keywordBlockingMode: 'Moderation strategy', + keywordModeKeywordAndApi: 'Keyword + API', + keywordModeKeywordAndApiDesc: 'Block on keyword hit; otherwise fall through to the upstream moderation API.', + keywordModeKeywordOnly: 'Keyword only', + keywordModeKeywordOnlyDesc: 'Decide using keywords only; misses are allowed without calling the API, saving upstream cost.', + keywordModeKeywordOnlyNotice: 'Keyword-only strategy: requests that do not match any keyword are allowed without calling the upstream moderation API.', + keywordModeApiOnly: 'API only', + keywordModeApiOnlyDesc: 'Use the upstream moderation API only; the keyword list configured here is not consulted.', + keywordModeApiOnlyNotice: 'API-only strategy: the keyword list is not consulted; all requests go through the upstream moderation API.', + overview: { + status: 'Status', + enabled: 'Enabled', + disabled: 'Disabled', + apiKey: 'API Key', + groupScope: 'Scope', + logs: 'Audit Records', + currentFilter: 'Current filter', + }, + filters: { + search: 'Search user/key/summary', + from: 'From', + to: 'To', + allGroups: 'All Groups', + allEndpoints: 'All Endpoints', + }, + table: { + time: 'Time', + group: 'Group', + user: 'User', + apiKey: 'API Key', + endpoint: 'Endpoint', + result: 'Result', + highest: 'Highest', + actionMeta: 'Action', + latency: 'Latency', + input: 'Input Summary', + }, + result: { + all: 'All Results', + hit: 'Hit', + blocked: 'Blocked', + pass: 'Pass', + error: 'Error', + }, + action: { + block: 'Blocked', + keywordBlock: 'Keyword Blocked', + cyberPolicy: 'Cyber policy', + error: 'Error', + }, + }, + + // Channel Monitor + channelMonitor: { + title: 'Channel Monitor', + description: 'Monitor channel availability, latency and status', + searchPlaceholder: 'Search monitor name...', + allProviders: 'All Providers', + allStatus: 'All Status', + enabledFilter: 'Enabled', + onlyEnabled: 'Enabled only', + onlyDisabled: 'Disabled only', + createButton: 'Create Monitor', + createTitle: 'Create Channel Monitor', + editTitle: 'Edit Channel Monitor', + runNow: 'Run Now', + runSuccess: 'Check completed', + runFailed: 'Check failed', + apiKeyDecryptFailed: 'API Key decryption failed. Please re-edit this monitor with a fresh key.', + createSuccess: 'Monitor created', + updateSuccess: 'Monitor updated', + deleteSuccess: 'Monitor deleted', + loadError: 'Failed to load monitors', + deleteConfirm: 'Are you sure you want to delete monitor "{name}"? This action cannot be undone.', + nameRequired: 'Please enter a monitor name', + primaryModelRequired: 'Please enter a primary model', + columns: { + name: 'Name', + provider: 'Provider', + primaryModel: 'Primary Model', + availability7d: '7d Availability', + latency: 'Latency (ms)', + enabled: 'Enabled', + actions: 'Actions' + }, + form: { + name: 'Name', + namePlaceholder: 'Enter monitor name', + provider: 'Platform', + apiMode: 'OpenAI protocol', + apiModeChatCompletions: 'OpenAI Compatible', + apiModeChatCompletionsHint: 'Use /v1/chat/completions with messages; works for most compatible providers.', + apiModeResponses: 'Responses API', + apiModeResponsesHint: 'Use /v1/responses with default instructions + input; best for self-check/Codex paths.', + endpoint: 'Endpoint', + endpointPlaceholder: 'https://api.example.com', + useCurrentDomain: 'Use current service', + apiKey: 'API Key', + apiKeyPlaceholder: 'Enter API Key', + apiKeyEditPlaceholder: 'Leave blank to keep current key', + useMyKey: 'Use my key', + selectKeyTitle: 'Select my API Key', + selectKeyHint: 'Only your active, non-expired keys are listed.', + noActiveKey: 'No active API keys available', + primaryModel: 'Primary Model', + primaryModelPlaceholder: 'gpt-4o-mini', + extraModels: 'Extra Models', + extraModelsPlaceholder: 'Press Enter to add extra model', + groupName: 'Group Name', + groupNamePlaceholder: 'Optional, used to group rows in user view', + intervalSeconds: 'Interval (seconds)', + intervalSecondsHint: 'Range: 15 - 3600 seconds', + jitterSeconds: 'Random Jitter (± seconds)', + jitterSecondsHint: 'Each check fires at interval ± a random offset within this value; 0 means fixed interval. Interval minus jitter must be ≥ 15s', + enabled: 'Enable monitor', + kindRequired: 'Please select a provider' + }, + runResultTitle: 'Check Result', + noMonitorsYet: 'No monitors yet', + createFirstMonitor: 'Create your first monitor to track channel availability', + advanced: { + section: 'Advanced (optional)', + sectionHint: 'Customize request headers and body to bypass upstream client-detection (e.g. "only Claude Code clients allowed").', + headers: 'Custom request headers', + headersPlaceholder: 'User-Agent: claude-cli/1.0.83 (external, cli)\nx-app: cli\nanthropic-beta: claude-code-20250219', + headerNamePlaceholder: 'Header name', + headerValuePlaceholder: 'Value', + headerAddRow: 'Add header', + headerNameInvalid: 'Header name cannot contain whitespace or colon: {name}', + headersHint: 'Merged on top of adapter defaults (user wins). Hop-by-hop headers (Host / Content-Length / ...) are ignored.', + headersParseError: 'Cannot parse line: {line}', + bodyMode: 'Body handling', + bodyModeOff: 'Default', + bodyModeMerge: 'Merge', + bodyModeReplace: 'Replace', + bodyModeHintOff: 'Use the adapter default body (includes challenge validation).', + bodyModeHintMerge: 'Shallow-merge with the default body; user fields win but model / messages / contents are protected (use Replace to change those).', + bodyModeHintReplace: 'Use the JSON below as the complete body. Challenge validation is skipped; HTTP 2xx + non-empty response text is treated as operational.', + bodyJson: 'Body JSON', + bodyJsonFormat: 'Format', + bodyJsonHint: 'Parsed on blur. Empty means no override.', + bodyJsonError: 'JSON parse failed', + bodyJsonObjectError: 'Body must be a JSON object (no arrays or primitives)' + }, + templateField: { + label: 'Request template', + none: 'No template', + placeholder: 'Pick a template (filtered by current provider)', + applyHint: 'Picking a template copies its headers and body to this monitor (snapshot). Later template edits are not auto-synced.' + }, + template: { + manageButton: 'Templates', + managerTitle: 'Request template manager', + createButton: 'New template', + emptyState: 'No templates for this provider yet', + missingName: 'Template name is required', + createSuccess: 'Template created', + updateSuccess: 'Template updated', + deleteSuccess: 'Template deleted', + applyButton: 'Apply to monitors', + applyTooltip: 'Overwrite snapshot fields on associated monitors', + applyTitle: 'Apply template', + applyConfirm: 'Apply', + applyConfirmMessage: 'Overwrite {n} associated monitor(s) with the current configuration of "{name}"? Any local customizations on those monitors will be discarded.', + applySuccess: 'Applied to {n} monitor(s)', + applyPickerTitle: 'Apply template "{name}"', + applyPickerHint: 'Select which monitors to overwrite (all selected by default). Any local customizations will be discarded.', + applyPickerEmpty: 'No monitors are currently associated to this template', + applyPickerConfirm: 'Apply to {n} monitor(s)', + selectNone: 'Select none', + selectedCount: 'Selected {n} / {total}', + deleteConfirm: 'Delete template "{name}"? {n} associated monitor(s) will be disassociated but keep their current snapshot and continue running.', + associatedCount: '{n} associated monitor(s)', + headersSummary: '{n} custom header(s)', + form: { + name: 'Template name', + namePlaceholder: 'e.g. Claude Code mimicry', + description: 'Description', + descriptionPlaceholder: 'Optional: what this template is for, capture date, etc.' + } + } + }, + + // Subscriptions + subscriptions: { + title: 'Subscription Management', + description: 'Manage user subscriptions and quota limits', + assignSubscription: 'Assign Subscription', + adjustSubscription: 'Adjust Subscription', + revokeSubscription: 'Revoke Subscription', + restoreSubscription: 'Restore Subscription', + allStatus: 'All Status', + allGroups: 'All Groups', + allPlatforms: 'All Platforms', + daily: 'Daily', + weekly: 'Weekly', + monthly: 'Monthly', + noLimits: 'No limits configured', + unlimited: 'Unlimited', + resetNow: 'Resetting soon', + windowNotActive: 'Window not active', + resetInMinutes: 'Resets in {minutes}m', + resetInHoursMinutes: 'Resets in {hours}h {minutes}m', + resetInDaysHours: 'Resets in {days}d {hours}h', + quotaEndsInMinutes: 'Quota ends in {minutes}m', + quotaEndsInHoursMinutes: 'Quota ends in {hours}h {minutes}m', + quotaEndsInDaysHours: 'Quota ends in {days}d {hours}h', + daysRemaining: 'days remaining', + remainingDays: 'Remaining days', + noExpiration: 'No expiration', + status: { + active: 'Active', + expired: 'Expired', + revoked: 'Revoked', + suspended: 'Suspended' + }, + columns: { + user: 'User', + group: 'Group', + usage: 'Usage', + expires: 'Expires', + status: 'Status', + actions: 'Actions' + }, + form: { + user: 'User', + group: 'Subscription Group', + validityDays: 'Validity (Days)', + adjustDays: 'Adjust by (Days)' + }, + selectUser: 'Select a user', + selectGroup: 'Select a subscription group', + groupHint: 'Only groups with subscription billing type are shown', + validityHint: 'Number of days the subscription will be valid', + adjustingFor: 'Adjusting subscription for', + currentExpiration: 'Current expiration', + adjustDaysPlaceholder: 'Positive to extend, negative to shorten', + adjustHint: 'Enter positive number to extend, negative to shorten (remaining days must be > 0)', + assign: 'Assign', + assigning: 'Assigning...', + adjust: 'Adjust', + adjusting: 'Adjusting...', + revoke: 'Revoke', + restore: 'Restore', + resetQuota: 'Reset Quota', + resetQuotaTitle: 'Reset Usage Quota', + resetQuotaConfirm: "Reset the daily, weekly, and monthly usage quota for '{user}'? Usage will be zeroed and windows restarted from today.", + quotaResetSuccess: 'Quota reset successfully', + failedToResetQuota: 'Failed to reset quota', + noSubscriptionsYet: 'No subscriptions yet', + assignFirstSubscription: 'Assign a subscription to get started.', + subscriptionAssigned: 'Subscription assigned successfully', + subscriptionAdjusted: 'Subscription adjusted successfully', + subscriptionRevoked: 'Subscription revoked successfully', + subscriptionRestored: 'Subscription restored successfully', + failedToLoad: 'Failed to load subscriptions', + failedToAssign: 'Failed to assign subscription', + failedToAdjust: 'Failed to adjust subscription', + failedToRevoke: 'Failed to revoke subscription', + failedToRestore: 'Failed to restore subscription', + adjustWouldExpire: 'Remaining days after adjustment must be greater than 0', + adjustOutOfRange: 'Adjustment days must be between -36500 and 36500', + pleaseSelectUser: 'Please select a user', + pleaseSelectGroup: 'Please select a group', + validityDaysRequired: 'Please enter a valid number of days (at least 1)', + revokeConfirm: + "Are you sure you want to revoke the subscription for '{user}'? You can restore it later from the revoked list.", + restoreConfirm: + "Restore the subscription for '{user}'? If the original subscription has expired, it will be restored as expired.", + guide: { + title: 'Subscription Management Guide', + subtitle: 'Subscription mode lets you assign time-based usage quotas to users, with daily/weekly/monthly limits. Follow these steps to get started.', + showGuide: 'Usage Guide', + step1: { + title: 'Create a Subscription Group', + line1: 'Go to "Group Management" page, click "Create Group"', + line2: 'Set billing type to "Subscription", configure daily/weekly/monthly quota limits', + line3: 'Save the group and ensure its status is "Active"', + link: 'Go to Group Management' + }, + step2: { + title: 'Assign Subscription to User', + line1: 'Click the "Assign Subscription" button in the top right', + line2: 'Search for a user by email and select them', + line3: 'Choose a subscription group, set validity days, then click "Assign"' + }, + step3: { + title: 'Manage Existing Subscriptions' + }, + actions: { + adjust: 'Adjust', + adjustDesc: 'Extend or shorten the subscription validity period', + resetQuota: 'Reset Quota', + resetQuotaDesc: 'Reset daily/weekly/monthly usage to zero', + revoke: 'Revoke', + revokeDesc: 'Immediately terminate the subscription (restorable from the revoked list)' + }, + tip: 'Tip: Only groups with billing type "Subscription" and status "Active" appear in the group dropdown. If no options are available, create one in Group Management first.' + } + }, + + // Accounts +} diff --git a/frontend/src/i18n/locales/en/admin/index.ts b/frontend/src/i18n/locales/en/admin/index.ts new file mode 100644 index 0000000000..e4e9bba438 --- /dev/null +++ b/frontend/src/i18n/locales/en/admin/index.ts @@ -0,0 +1,15 @@ +import overview from './overview' +import channels from './channels' +import accounts from './accounts' +import resources from './resources' +import ops from './ops' +import settings from './settings' + +export default { + ...overview, + ...channels, + ...accounts, + ...resources, + ...ops, + ...settings, +} diff --git a/frontend/src/i18n/locales/en/admin/ops.ts b/frontend/src/i18n/locales/en/admin/ops.ts new file mode 100644 index 0000000000..1bcbc42775 --- /dev/null +++ b/frontend/src/i18n/locales/en/admin/ops.ts @@ -0,0 +1,803 @@ +export default { + ops: { + title: 'Ops Monitoring', + description: 'Operational monitoring and troubleshooting', + // Dashboard + systemHealth: 'System Health', + overview: 'Overview', + noSystemMetrics: 'No system metrics collected yet.', + collectedAt: 'Collected at:', + window: 'window', + memory: 'Memory', + db: 'DB', + goroutines: 'Goroutines', + jobs: 'Jobs', + jobsHelp: 'Click “Details” to view job heartbeats and recent errors', + active: 'active', + idle: 'idle', + waiting: 'waiting', + conns: 'conns', + queue: 'queue', + accountSwitches: 'Account switches', + ok: 'ok', + lastRun: 'last_run:', + lastSuccess: 'last_success:', + lastError: 'last_error:', + noData: 'No data.', + loadingText: 'loading', + ready: 'ready', + autoRefreshRemaining: 'Remaining {seconds}s', + systemLogs: { + title: 'System Logs', + description: 'Newest logs are shown first. Filter, search, and clean up by condition.', + queue: 'Queue', + written: 'Written', + dropped: 'Dropped', + failed: 'Failed', + runtimeConfig: 'Runtime Log Configuration (applies immediately)', + all: 'All', + level: 'Level', + stacktraceThreshold: 'Stacktrace threshold', + samplingInitial: 'Sampling initial', + samplingThereafter: 'Sampling thereafter', + retentionDays: 'Retention days', + caller: 'caller', + sampling: 'sampling', + saveAndApply: 'Save and apply', + resetDefaults: 'Reset defaults', + latestWriteError: 'Latest write error:', + timeRange: 'Time range', + startTime: 'Start time (optional)', + endTime: 'End time (optional)', + component: 'Component', + componentPlaceholder: 'e.g. http.access', + keyId: 'KEY ID', + platform: 'Platform', + model: 'Model', + keyword: 'Keyword', + keywordPlaceholder: 'message/request_id', + search: 'Search', + cleanCurrentFilters: 'Clean current filters', + refreshHealth: 'Refresh health', + empty: 'No system logs', + time: 'Time', + logDetails: 'Log Details', + loadFailed: 'Failed to load system logs', + runtimeConfigActive: 'Runtime log configuration is active', + runtimeConfigSaveFailed: 'Failed to save log configuration', + resetRuntimeConfigConfirm: 'Reset to startup configuration (env/yaml) and apply immediately?', + runtimeConfigReset: 'Reset to startup log configuration', + runtimeConfigResetFailed: 'Failed to reset log configuration', + cleanupConfirm: 'Clean up system logs matching the current filters? This cannot be undone.', + cleanupSuccess: 'Cleanup complete. Deleted {count} log entries.', + cleanupFailed: 'Failed to clean up system logs' + }, + requestsTotal: 'Requests (total)', + slaScope: 'SLA scope:', + tokens: 'Tokens', + tps: 'TPS:', + current: 'current', + peak: 'peak', + average: 'average', + totalRequests: 'Total Requests', + avgQps: 'Avg QPS', + avgTps: 'Avg TPS', + avgLatency: 'Avg Request Duration', + avgTtft: 'Avg TTFT', + exceptions: 'Exceptions', + requestErrors: 'Request Errors', + errorCount: 'Error Count', + upstreamErrors: 'Upstream Errors', + errorCountExcl429529: 'Error Count (excl 429/529)', + sla: 'SLA (excl business limits)', + businessLimited: 'business_limited:', + errors: 'Errors', + errorRate: 'error_rate:', + upstreamRate: 'upstream_rate:', + latencyDuration: 'Request Duration', + ttftLabel: 'TTFT (first_token_ms)', + p50: 'p50:', + p90: 'p90:', + p95: 'p95:', + p99: 'p99:', + avg: 'avg:', + max: 'max:', + requests: 'Requests', + requestsTitle: 'Requests', + upstream: 'Upstream', + client: 'Client', + system: 'System', + other: 'Other', + errorsSla: 'Errors (SLA scope)', + upstreamExcl429529: 'Upstream (excl 429/529)', + failedToLoadData: 'Failed to load ops data.', + failedToLoadOverview: 'Failed to load overview', + failedToLoadThroughputTrend: 'Failed to load throughput trend', + failedToLoadSwitchTrend: 'Failed to load avg account switches trend', + failedToLoadLatencyHistogram: 'Failed to load request duration histogram', + failedToLoadErrorTrend: 'Failed to load error trend', + failedToLoadErrorDistribution: 'Failed to load error distribution', + failedToLoadErrorDetail: 'Failed to load error detail', + retryFailed: 'Retry failed', + tpsK: 'TPS (K)', + top: 'Top:', + throughputTrend: 'Throughput Trend', + switchRateTrend: 'Avg Account Switches', + latencyHistogram: 'Request Duration Histogram', + errorTrend: 'Error Trend', + errorDistribution: 'Error Distribution', + switchRate: 'Avg switches', + // Health Score & Diagnosis + health: 'Health', + healthCondition: 'Health Condition', + healthHelp: 'Overall system health score based on SLA, error rate, and resource usage', + healthyStatus: 'Healthy', + riskyStatus: 'At Risk', + idleStatus: 'Idle', + timeRange: { + '5m': 'Last 5 minutes', + '30m': 'Last 30 minutes', + '1h': 'Last 1 hour', + '1d': 'Last 1 day', + '15d': 'Last 15 days', + '6h': 'Last 6 hours', + '24h': 'Last 24 hours', + '7d': 'Last 7 days', + '30d': 'Last 30 days' + }, + openaiTokenStats: { + title: 'OpenAI Token Request Stats', + viewModeTopN: 'TopN', + viewModePagination: 'Pagination', + prevPage: 'Previous', + nextPage: 'Next', + pageInfo: 'Page {page}/{total}', + totalModels: 'Total models: {total}', + failedToLoad: 'Failed to load OpenAI token stats', + empty: 'No OpenAI token stats for the current filters', + table: { + model: 'Model', + requestCount: 'Requests', + avgTokensPerSec: 'Avg Tokens/sec', + avgFirstTokenMs: 'Avg First Token Latency (ms)', + totalOutputTokens: 'Total Output Tokens', + avgDurationMs: 'Avg Duration (ms)', + requestsWithFirstToken: 'Requests With First Token' + } + }, + fullscreen: { + enter: 'Enter Fullscreen' + }, + diagnosis: { + title: 'Smart Diagnosis', + footer: 'Automated diagnostic suggestions based on current metrics', + idle: 'System is currently idle', + idleImpact: 'No active traffic', + // Resource diagnostics + dbDown: 'Database connection failed', + dbDownImpact: 'All database operations will fail', + dbDownAction: 'Check database service status, network connectivity, and connection configuration', + redisDown: 'Redis connection failed', + redisDownImpact: 'Cache functionality degraded, performance may decline', + redisDownAction: 'Check Redis service status and network connectivity', + cpuCritical: 'CPU usage critically high ({usage}%)', + cpuCriticalImpact: 'System response slowing, may affect all requests', + cpuCriticalAction: 'Check CPU-intensive tasks, consider scaling or code optimization', + cpuHigh: 'CPU usage elevated ({usage}%)', + cpuHighImpact: 'System load is high, needs attention', + cpuHighAction: 'Monitor CPU trends, prepare scaling plan', + memoryCritical: 'Memory usage critically high ({usage}%)', + memoryCriticalImpact: 'May trigger OOM, system stability threatened', + memoryCriticalAction: 'Check for memory leaks, consider increasing memory or optimizing usage', + memoryHigh: 'Memory usage elevated ({usage}%)', + memoryHighImpact: 'Memory pressure is high, needs attention', + memoryHighAction: 'Monitor memory trends, check for memory leaks', + ttftHigh: 'Time to first token elevated ({ttft}ms)', + ttftHighImpact: 'User perceived latency increased', + ttftHighAction: 'Optimize request processing flow, reduce pre-processing time', + // Error rate diagnostics + upstreamCritical: 'Upstream error rate critically high ({rate}%)', + upstreamCriticalImpact: 'May affect many user requests', + upstreamCriticalAction: 'Check upstream service health, enable fallback strategies', + upstreamHigh: 'Upstream error rate elevated ({rate}%)', + upstreamHighImpact: 'Recommend checking upstream service status', + upstreamHighAction: 'Contact upstream service team, prepare fallback plan', + errorHigh: 'Error rate too high ({rate}%)', + errorHighImpact: 'Many requests failing', + errorHighAction: 'Check error logs, identify root cause, urgent fix required', + errorElevated: 'Error rate elevated ({rate}%)', + errorElevatedImpact: 'Recommend checking error logs', + errorElevatedAction: 'Analyze error types and distribution, create fix plan', + // SLA diagnostics + slaCritical: 'SLA critically below target ({sla}%)', + slaCriticalImpact: 'User experience severely degraded', + slaCriticalAction: 'Urgently investigate errors and latency, consider rate limiting', + slaLow: 'SLA below target ({sla}%)', + slaLowImpact: 'Service quality needs attention', + slaLowAction: 'Analyze SLA decline causes, optimize system performance', + // Health score diagnostics + healthCritical: 'Overall health score critically low ({score})', + healthCriticalImpact: 'Multiple metrics may be degraded; prioritize error rate and latency investigation', + healthCriticalAction: 'Comprehensive system check, prioritize critical-level issues', + healthLow: 'Overall health score low ({score})', + healthLowImpact: 'May indicate minor instability; monitor SLA and error rates', + healthLowAction: 'Monitor metric trends, prevent issue escalation', + healthy: 'All system metrics normal', + healthyImpact: 'Service running stable' + }, + // Error Log + errorLog: { + timeId: 'Time / ID', + commonErrors: { + contextDeadlineExceeded: 'context deadline exceeded', + connectionRefused: 'connection refused', + rateLimit: 'rate limit' + }, + time: 'Time', + type: 'Type', + context: 'Context', + platform: 'Platform', + model: 'Model', + group: 'Group', + user: 'User', + userId: 'User ID', + apiKey: 'API Key', + keyDeletedBadge: 'Key Deleted', + account: 'Account', + accountId: 'Account ID', + status: 'Status', + message: 'Message', + ip: 'IP', + latency: 'Request Duration', + action: 'Action', + noErrors: 'No errors in this window.', + grp: 'GRP:', + acc: 'ACC:', + details: 'Details', + phase: 'Phase', + id: 'ID:', + typeUpstream: 'Upstream', + typeRequest: 'Request', + typeAuth: 'Auth', + typeRouting: 'Routing', + typeInternal: 'Internal', + endpoint: 'Endpoint', + requestType: 'Type', + requestTypeSync: 'Sync', + requestTypeStream: 'Stream', + requestTypeWs: 'WS' + }, + // Error Details Modal + errorDetails: { + upstreamErrors: 'Upstream Errors', + requestErrors: 'Request Errors', + unresolved: 'Unresolved', + resolved: 'Resolved', + viewErrors: 'Errors', + viewExcluded: 'Excluded', + statusCodeOther: 'Other', + owner: { + provider: 'Provider', + client: 'Client', + platform: 'Platform' + }, + phase: { + request: 'Request', + auth: 'Auth', + routing: 'Routing', + upstream: 'Upstream', + network: 'Network', + internal: 'Internal' + }, + total: 'Total:', + searchPlaceholder: 'Search request_id / client_request_id / message', + }, + // Error Detail Modal + errorDetail: { + title: 'Error Detail', + titleWithId: 'Error #{id}', + noErrorSelected: 'No error selected.', + resolution: 'Resolved:', + failedToUpdateResolvedStatus: 'Failed to update resolved status', + classificationKeys: { + phase: 'Phase', + owner: 'Owner', + source: 'Source', + resolvedAt: 'Resolved At', + resolvedBy: 'Resolved By' + }, + source: { + upstream_http: 'Upstream HTTP' + }, + upstreamKeys: { + status: 'Status', + message: 'Message', + detail: 'Detail', + upstreamErrors: 'Upstream Errors' + }, + upstreamEvent: { + account: 'Account', + status: 'Status', + requestId: 'Request ID' + }, + responsePreview: { + expand: 'Response (click to expand)', + collapse: 'Response (click to collapse)' + }, + loading: 'Loading…', + requestId: 'Request ID', + time: 'Time', + phase: 'Phase', + status: 'Status', + message: 'Message', + basicInfo: 'Basic Info', + platform: 'Platform', + model: 'Model', + group: 'Group', + user: 'User', + account: 'Account', + latency: 'Request Duration', + businessLimited: 'Business Limited', + requestPath: 'Request Path', + inboundEndpoint: 'Inbound Endpoint', + upstreamEndpoint: 'Upstream Endpoint', + requestedModel: 'Requested Model', + upstreamModel: 'Upstream Model', + requestType: 'Request Type', + requestTypeUnknown: 'Unknown', + requestTypeSync: 'Sync', + requestTypeStream: 'Stream', + requestTypeWs: 'WebSocket', + modelMapping: 'Model Mapping', + timings: 'Timings', + auth: 'Auth', + routing: 'Routing', + upstream: 'Upstream', + response: 'Response', + classification: 'Classification', + errorBody: 'Error Body', + trimmed: 'trimmed', + markResolved: 'Mark resolved', + markUnresolved: 'Mark unresolved', + tabOverview: 'Overview', + tabRequest: 'Request', + tabResponse: 'Response', + responseBody: 'Response', + compareA: 'Compare A', + compareB: 'Compare B', + suggestion: 'Suggestion', + suggestUpstream: 'Upstream instability: check account status or consider switching accounts', + suggestRequest: 'Client request error: ask customer to fix request parameters', + suggestAuth: 'Auth failed: verify API key/credentials', + suggestPlatform: 'Platform error: prioritize investigation and fix', + suggestGeneric: 'See details for more context', + apiKeyPrefix: 'Key Prefix', + attemptedKeyPrefix: 'Attempted Key Prefix', + deletedKeyOwner: 'Deleted Key Owner', + keyDeletedBadge: 'Key Deleted' + }, + requestDetails: { + title: 'Request Details', + details: 'Details', + rangeLabel: 'Window: {range}', + rangeMinutes: '{n} minutes', + rangeHours: '{n} hours', + empty: 'No requests in this window.', + emptyHint: 'Try a different time range or remove filters.', + failedToLoad: 'Failed to load request details', + requestIdCopied: 'Request ID copied', + copyFailed: 'Copy failed', + copy: 'Copy', + viewError: 'View Error', + kind: { + success: 'SUCCESS', + error: 'ERROR' + }, + table: { + time: 'Time', + kind: 'Kind', + platform: 'Platform', + model: 'Model', + duration: 'Duration', + status: 'Status', + requestId: 'Request ID', + actions: 'Actions' + } + }, + alertEvents: { + title: 'Alert Events', + description: 'Recent alert firing/resolution records (email-only)', + loading: 'Loading...', + empty: 'No alert events', + loadFailed: 'Failed to load alert events', + status: { + firing: 'FIRING', + resolved: 'RESOLVED', + manualResolved: 'MANUAL RESOLVED' + }, + detail: { + title: 'Alert Detail', + loading: 'Loading detail...', + empty: 'No detail', + loadFailed: 'Failed to load alert detail', + manualResolve: 'Mark as Resolved', + manualResolvedSuccess: 'Marked as manually resolved', + manualResolvedFailed: 'Failed to mark as manually resolved', + silence: 'Ignore Alert', + silenceSuccess: 'Alert silenced', + silenceFailed: 'Failed to silence alert', + viewRule: 'View Rule', + viewLogs: 'View Logs', + firedAt: 'Fired At', + resolvedAt: 'Resolved At', + ruleId: 'Rule ID', + dimensions: 'Dimensions', + historyTitle: 'History', + historyHint: 'Recent events with same rule + dimensions', + historyLoading: 'Loading history...', + historyEmpty: 'No history' + }, + table: { + time: 'Time', + status: 'Status', + severity: 'Severity', + platform: 'Platform', + ruleId: 'Rule ID', + title: 'Title', + duration: 'Duration', + metric: 'Metric / Threshold', + dimensions: 'Dimensions', + email: 'Email Sent', + emailSent: 'Sent', + emailIgnored: 'Ignored' + } + }, + alertRules: { + title: 'Alert Rules', + description: 'Create and manage threshold-based system alerts (email-only)', + loading: 'Loading...', + empty: 'No alert rules', + loadFailed: 'Failed to load alert rules', + saveFailed: 'Failed to save alert rule', + saveSuccess: 'Alert rule saved successfully', + deleteFailed: 'Failed to delete alert rule', + deleteSuccess: 'Alert rule deleted successfully', + manage: 'Manage Alert Rules', + create: 'Create Rule', + createTitle: 'Create Alert Rule', + editTitle: 'Edit Alert Rule', + deleteConfirmTitle: 'Delete this rule?', + deleteConfirmMessage: 'This will remove the rule and its related events. Continue?', + metricGroups: { + system: 'System Metrics', + group: 'Group-level Metrics (requires group_id)', + account: 'Account-level Metrics' + }, + metrics: { + successRate: 'Success Rate (%)', + errorRate: 'Error Rate (%)', + upstreamErrorRate: 'Upstream Error Rate (%)', + p95: 'P95 Latency (ms)', + p99: 'P99 Latency (ms)', + cpu: 'CPU Usage (%)', + memory: 'Memory Usage (%)', + queueDepth: 'Concurrency Queue Depth', + groupAvailableAccounts: 'Group Available Accounts', + groupAvailableRatio: 'Group Available Ratio (%)', + groupRateLimitRatio: 'Group Rate Limit Ratio (%)', + accountRateLimitedCount: 'Rate-limited Accounts', + accountErrorCount: 'Error Accounts (excluding temporarily unschedulable)', + accountErrorRatio: 'Error Account Ratio (%)', + accountTempUnscheduledCount: 'Temporarily Unschedulable Accounts', + overloadAccountCount: 'Overloaded Accounts' + }, + metricDescriptions: { + successRate: 'Percentage of successful requests in the window (0-100).', + errorRate: 'Percentage of failed requests in the window (0-100).', + upstreamErrorRate: 'Percentage of upstream failures in the window (0-100).', + p95: 'P95 request latency within the window (ms).', + p99: 'P99 request latency within the window (ms).', + cpu: 'Current instance CPU usage (0-100).', + memory: 'Current instance memory usage (0-100).', + queueDepth: 'Concurrency queue depth within the window (queued requests).', + groupAvailableAccounts: 'Number of available accounts in the selected group (requires group_id).', + groupAvailableRatio: 'Available account ratio in the selected group (0-100, requires group_id).', + groupRateLimitRatio: 'Rate-limited account ratio in the selected group (0-100, requires group_id).', + accountRateLimitedCount: 'Number of rate-limited accounts within the window.', + accountErrorCount: 'Number of error accounts within the window (excluding temporarily unschedulable).', + accountErrorRatio: 'Error account ratio within the window (0-100).', + accountTempUnscheduledCount: 'Number of accounts currently temporarily unschedulable (e.g. proxy/credential failure auto-eviction).', + overloadAccountCount: 'Number of overloaded accounts within the window.' + }, + hints: { + recommended: 'Recommended: operator {operator}, threshold {threshold}{unit}', + groupRequired: 'This is a group-level metric; selecting a group (group_id) is required.', + groupOptional: 'Optional: limit the rule to a specific group via group_id.' + }, + table: { + name: 'Name', + metric: 'Metric', + severity: 'Severity', + enabled: 'Enabled', + actions: 'Actions' + }, + form: { + name: 'Name', + description: 'Description', + metric: 'Metric', + operator: 'Operator', + groupId: 'Group (group_id)', + groupPlaceholder: 'Select a group', + allGroups: 'All groups', + threshold: 'Threshold', + severity: 'Severity', + window: 'Window (minutes)', + sustained: 'Sustained (samples)', + cooldown: 'Cooldown (minutes)', + enabled: 'Enabled', + notifyEmail: 'Send email notifications' + }, + validation: { + title: 'Please fix the following issues', + invalid: 'Invalid rule', + nameRequired: 'Name is required', + metricRequired: 'Metric is required', + groupIdRequired: 'group_id is required for group-level metrics', + operatorRequired: 'Operator is required', + thresholdRequired: 'Threshold must be a number', + windowRange: 'Window must be one of: 1, 5, 60 minutes', + sustainedRange: 'Sustained must be between 1 and 1440 samples', + cooldownRange: 'Cooldown must be between 0 and 1440 minutes' + } + }, + runtime: { + title: 'Ops Runtime Settings', + description: 'Stored in database; changes take effect without editing config files.', + loading: 'Loading...', + noData: 'No runtime settings available', + loadFailed: 'Failed to load runtime settings', + saveSuccess: 'Runtime settings saved', + saveFailed: 'Failed to save runtime settings', + alertTitle: 'Alert Evaluator', + groupAvailabilityTitle: 'Group Availability Monitor', + evalIntervalSeconds: 'Evaluation Interval (seconds)', + silencing: { + title: 'Alert Silencing (Maintenance Mode)', + enabled: 'Enable silencing', + globalUntil: 'Silence until (RFC3339)', + untilHint: 'Leave empty to only toggle silencing without an expiry (not recommended).', + reason: 'Reason', + reasonPlaceholder: 'e.g., planned maintenance', + entries: { + title: 'Advanced: targeted silencing', + hint: 'Optional: silence only certain rules or severities. Leave fields empty to match all.', + add: 'Add Entry', + empty: 'No targeted entries', + entryTitle: 'Entry #{n}', + ruleId: 'Rule ID (optional)', + ruleIdPlaceholder: 'e.g., 1', + severities: 'Severities (optional)', + severitiesPlaceholder: 'e.g., P0,P1 (empty = all)', + until: 'Until (RFC3339)', + reason: 'Reason', + validation: { + untilRequired: 'Entry until time is required', + untilFormat: 'Entry until time must be a valid RFC3339 timestamp', + ruleIdPositive: 'Entry rule_id must be a positive integer', + severitiesFormat: 'Entry severities must be a comma-separated list of P0..P3' + } + }, + validation: { + timeFormat: 'Silence time must be a valid RFC3339 timestamp' + } + }, + lockEnabled: 'Distributed Lock Enabled', + lockKey: 'Distributed Lock Key', + lockTTLSeconds: 'Distributed Lock TTL (seconds)', + showAdvancedDeveloperSettings: 'Show advanced developer settings (Distributed Lock)', + advancedSettingsSummary: 'Advanced settings (Distributed Lock)', + evalIntervalHint: 'How often the evaluator runs. Keeping the default is recommended.', + validation: { + title: 'Please fix the following issues', + invalid: 'Invalid settings', + evalIntervalRange: 'Evaluation interval must be between 1 and 86400 seconds', + lockKeyRequired: 'Distributed lock key is required when lock is enabled', + lockKeyPrefix: 'Distributed lock key must start with "{prefix}"', + lockKeyHint: 'Recommended: start with "{prefix}" to avoid conflicts', + lockTtlRange: 'Distributed lock TTL must be between 1 and 86400 seconds', + slaMinPercentRange: 'SLA minimum percentage must be between 0 and 100', + ttftP99MaxRange: 'TTFT P99 maximum must be a number ≥ 0', + requestErrorRateMaxRange: 'Request error rate maximum must be between 0 and 100', + upstreamErrorRateMaxRange: 'Upstream error rate maximum must be between 0 and 100' + } + }, + email: { + title: 'Email Notification', + description: 'Configure alert/report email notifications (stored in database).', + loading: 'Loading...', + noData: 'No email notification config', + loadFailed: 'Failed to load email notification config', + saveSuccess: 'Email notification config saved', + saveFailed: 'Failed to save email notification config', + alertTitle: 'Alert Emails', + reportTitle: 'Report Emails', + recipients: 'Recipients', + recipientsHint: 'If empty, the system may fallback to the first admin email.', + minSeverity: 'Min Severity', + minSeverityAll: 'All severities', + rateLimitPerHour: 'Rate limit per hour', + batchWindowSeconds: 'Batch window (seconds)', + includeResolved: 'Include resolved alerts', + dailySummary: 'Daily summary', + weeklySummary: 'Weekly summary', + errorDigest: 'Error digest', + errorDigestMinCount: 'Min errors for digest', + accountHealth: 'Account health', + accountHealthThreshold: 'Error rate threshold (%)', + cronPlaceholder: 'Cron expression', + reportHint: 'Schedules use cron syntax; leave empty to use defaults.', + validation: { + title: 'Please fix the following issues', + invalid: 'Invalid email notification config', + alertRecipientsRequired: 'Alert emails are enabled but no recipients are configured', + reportRecipientsRequired: 'Report emails are enabled but no recipients are configured', + invalidRecipients: 'One or more recipient emails are invalid', + rateLimitRange: 'Rate limit per hour must be a number ≥ 0', + batchWindowRange: 'Batch window must be between 0 and 86400 seconds', + cronRequired: 'A cron expression is required when schedule is enabled', + cronFormat: 'Cron expression format looks invalid (expected at least 5 parts)', + digestMinCountRange: 'Min errors for digest must be a number ≥ 0', + accountHealthThresholdRange: 'Account health threshold must be between 0 and 100' + } + }, + settings: { + title: 'Ops Monitoring Settings', + loadFailed: 'Failed to load settings', + saveSuccess: 'Ops monitoring settings saved successfully', + saveFailed: 'Failed to save settings', + dataCollection: 'Data Collection', + evaluationInterval: 'Evaluation Interval (seconds)', + evaluationIntervalHint: 'Frequency of detection tasks, recommended to keep default', + alertConfig: 'Alert Configuration', + enableAlert: 'Enable Alerts', + alertRecipients: 'Alert Recipient Emails', + emailPlaceholder: 'Enter email address', + recipientsHint: 'If empty, the system will use the first admin email as default recipient', + minSeverity: 'Minimum Severity', + reportConfig: 'Report Configuration', + enableReport: 'Enable Reports', + reportRecipients: 'Report Recipient Emails', + dailySummary: 'Daily Summary', + weeklySummary: 'Weekly Summary', + metricThresholds: 'Metric Thresholds', + metricThresholdsHint: 'Configure alert thresholds for metrics, values exceeding thresholds will be displayed in red', + slaMinPercent: 'SLA Minimum Percentage', + slaMinPercentHint: 'SLA below this value will be displayed in red (default: 99.5%)', + ttftP99MaxMs: 'TTFT P99 Maximum (ms)', + ttftP99MaxMsHint: 'TTFT P99 above this value will be displayed in red (default: 500ms)', + requestErrorRateMaxPercent: 'Request Error Rate Maximum (%)', + requestErrorRateMaxPercentHint: 'Request error rate above this value will be displayed in red (default: 5%)', + upstreamErrorRateMaxPercent: 'Upstream Error Rate Maximum (%)', + upstreamErrorRateMaxPercentHint: 'Upstream error rate above this value will be displayed in red (default: 5%)', + advancedSettings: 'Advanced Settings', + dataRetention: 'Data Retention Policy', + enableCleanup: 'Enable Data Cleanup', + cleanupSchedule: 'Cleanup Schedule (Cron)', + cleanupScheduleHint: 'Example: 0 2 * * * means 2 AM daily', + errorLogRetentionDays: 'Error Log Retention Days', + minuteMetricsRetentionDays: 'Minute Metrics Retention Days', + hourlyMetricsRetentionDays: 'Hourly Metrics Retention Days', + retentionDaysHint: 'Recommended 7-90 days; longer periods consume more storage. Set to 0 to wipe all history on every scheduled cleanup', + aggregation: 'Pre-aggregation Tasks', + enableAggregation: 'Enable Pre-aggregation', + aggregationHint: 'Pre-aggregation improves query performance for long time windows', + openaiQuotaAutoPause: 'OpenAI Account Quota Auto-pause', + openaiQuotaAutoPauseHint: 'When an OpenAI account reaches its 5h / 7d usage threshold, the scheduler skips it automatically and resumes once the window rolls over. Per-account thresholds take precedence over this global default.', + openaiQuotaAutoPauseDefault5h: 'Default 5h usage threshold (%)', + openaiQuotaAutoPauseDefault7d: 'Default 7d usage threshold (%)', + openaiQuotaAutoPauseThresholdHint: 'Value 0-100; leave blank or 0 to disable the global default threshold.', + errorFiltering: 'Error Filtering', + ignoreCountTokensErrors: 'Ignore count_tokens errors', + ignoreCountTokensErrorsHint: 'When enabled, errors from count_tokens requests will not be written to the error log.', + ignoreContextCanceled: 'Ignore client disconnect errors', + ignoreContextCanceledHint: 'When enabled, client disconnect (context canceled) errors will not be written to the error log.', + ignoreNoAvailableAccounts: 'Ignore no available accounts errors', + ignoreNoAvailableAccountsHint: 'When enabled, "No available accounts" errors will not be written to the error log (not recommended; usually a config issue).', + ignoreInvalidApiKeyErrors: 'Ignore invalid API key errors', + ignoreInvalidApiKeyErrorsHint: 'When enabled, invalid or missing API key errors (INVALID_API_KEY, API_KEY_REQUIRED) will not be written to the error log.', + ignoreInsufficientBalanceErrors: 'Ignore Insufficient Balance Errors', + ignoreInsufficientBalanceErrorsHint: 'When enabled, insufficient account balance errors will not be written to the error log.', + autoRefresh: 'Auto Refresh', + enableAutoRefresh: 'Enable auto refresh', + enableAutoRefreshHint: 'Automatically refresh dashboard data at a fixed interval.', + refreshInterval: 'Refresh Interval', + refreshInterval15s: '15 seconds', + refreshInterval30s: '30 seconds', + refreshInterval60s: '60 seconds', + dashboardCards: 'Dashboard Cards', + displayAlertEvents: 'Display alert events', + displayAlertEventsHint: 'Show or hide the recent alert events card on the ops dashboard. Enabled by default.', + displayOpenAITokenStats: 'Display OpenAI token request stats', + displayOpenAITokenStatsHint: 'Show or hide the OpenAI token request stats card on the ops dashboard. Hidden by default.', + autoRefreshCountdown: 'Auto refresh: {seconds}s', + validation: { + title: 'Please fix the following issues', + retentionDaysRange: 'Retention days must be between 0 and 365 (0 = wipe all on every cleanup)', + slaMinPercentRange: 'SLA minimum percentage must be between 0 and 100', + ttftP99MaxRange: 'TTFT P99 maximum must be a number ≥ 0', + requestErrorRateMaxRange: 'Request error rate maximum must be between 0 and 100', + upstreamErrorRateMaxRange: 'Upstream error rate maximum must be between 0 and 100', + openaiQuotaAutoPauseRange: 'OpenAI quota auto-pause threshold must be between 0 and 100' + } + }, + concurrency: { + title: 'Concurrency / Queue', + byPlatform: 'By Platform', + byGroup: 'By Group', + byAccount: 'By Account', + byUser: 'By User', + showByUserTooltip: 'Switch to user view to see concurrency usage per user', + switchToUser: 'Switch to user view', + switchToPlatform: 'Switch to platform view', + totalRows: '{count} rows', + disabledHint: 'Realtime monitoring is disabled in settings.', + empty: 'No data', + queued: 'Queue {count}', + rateLimited: 'Rate-limited {count}', + errorAccounts: 'Errors {count}', + loadFailed: 'Failed to load concurrency data' + }, + realtime: { + title: 'Realtime', + connected: 'Realtime connected', + connecting: 'Realtime connecting', + reconnecting: 'Realtime reconnecting', + offline: 'Realtime offline', + closed: 'Realtime closed', + reconnectIn: 'retry in {seconds}s' + }, + queryMode: { + auto: 'Auto', + raw: 'Raw', + preagg: 'Preagg' + }, + accountAvailability: { + available: 'Available', + unavailable: 'Unavailable', + accountError: 'Error' + }, + tooltips: { + totalRequests: 'Total number of requests (including both successful and failed requests) in the selected time window.', + throughputTrend: 'Requests/QPS + Tokens/TPS in the selected window.', + switchRateTrend: 'Trend of account switches / total requests over the last 5 hours (avg switches).', + latencyHistogram: 'Request duration distribution (ms) for successful requests.', + errorTrend: 'Error counts over time (SLA scope excludes business limits; upstream excludes 429/529).', + errorDistribution: 'Error distribution by status code (SLA scope, excluding business limits).', + goroutines: + 'Number of Go runtime goroutines (lightweight threads). There is no absolute "safe" number—use your historical baseline. Heuristic: <2k is common; 2k–8k watch; >8k plus rising queue/latency often suggests blocking/leaks.', + cpu: 'CPU usage percentage, showing system processor load.', + memory: 'Memory usage, including used and total available memory.', + db: 'Database connection pool status, including active, idle, and waiting connections.', + redis: 'Redis connection pool status, showing active and idle connections.', + jobs: 'Background job execution status, including last run time, success time, and error information.', + qps: 'Queries Per Second (QPS) and Tokens Per Second (TPS), real-time system throughput.', + tokens: 'Total number of tokens processed in the current time window.', + sla: 'Service Level Agreement success rate, excluding business limits (e.g., insufficient balance, quota exceeded).', + errors: 'Error statistics, including total errors, error rate, and upstream error rate.', + upstreamErrors: 'Upstream error statistics, excluding rate limit errors (429/529).', + latency: 'Request duration statistics, including p50, p90, p95, p99 percentiles.', + ttft: 'Time To First Token, measuring the speed of first token return in streaming responses.', + health: 'System health score (0-100), considering SLA, error rate, and resource usage.' + }, + charts: { + emptyRequest: 'No requests in this window.', + emptyError: 'No errors in this window.', + resetZoom: 'Reset', + resetZoomHint: 'Reset zoom (if enabled)', + downloadChart: 'Download', + downloadChartHint: 'Download chart as image' + } + }, + + // Settings +} diff --git a/frontend/src/i18n/locales/en/admin/overview.ts b/frontend/src/i18n/locales/en/admin/overview.ts new file mode 100644 index 0000000000..57e7f21137 --- /dev/null +++ b/frontend/src/i18n/locales/en/admin/overview.ts @@ -0,0 +1,949 @@ +export default { + // Dashboard + dashboard: { + title: 'Admin Dashboard', + description: 'System overview and real-time statistics', + apiKeys: 'API Keys', + accounts: 'Accounts', + users: 'Users', + todayRequests: 'Today Requests', + newUsersToday: 'New Users Today', + todayTokens: 'Today Tokens', + totalTokens: 'Total Tokens', + cacheToday: 'Cache (Today)', + performance: 'Performance', + avgResponse: 'Avg Response', + active: 'active', + ok: 'ok', + err: 'err', + activeUsers: 'active users', + create: 'Create', + timeRange: 'Time Range', + granularity: 'Granularity', + day: 'Day', + hour: 'Hour', + modelDistribution: 'Model Distribution', + groupDistribution: 'Group Usage Distribution', + metricTokens: 'By Tokens', + metricActualCost: 'By Actual Cost', + tokenUsageTrend: 'Token Usage Trend', + userUsageTrend: 'User Usage Trend (Top 12)', + model: 'Model', + group: 'Group', + noGroup: 'No Group', + requests: 'Requests', + tokens: 'Tokens', + actual: 'Actual', + standard: 'Standard', + accountCost: 'Cost', + noDataAvailable: 'No data available', + recentUsage: 'Recent Usage', + viewModelDistribution: 'Model Distribution', + viewSpendingRanking: 'User Spending Ranking', + spendingRankingTitle: 'User Spending Ranking', + spendingRankingUser: 'User', + spendingRankingRequests: 'Requests', + spendingRankingTokens: 'Tokens', + spendingRankingSpend: 'Spend', + spendingRankingOther: 'Others', + spendingRankingUsage: 'Usage', + spendShort: 'Spend', + requestsShort: 'Req', + tokensShort: 'Tok', + quickActions: 'Quick Actions', + batchImage: 'Batch Image', + batchImageDesc: 'Submit jobs and copy agent instructions', + groupPricing: 'Group Pricing', + groupPricingDesc: 'Configure batch discount and hold ratio', + failedToLoad: 'Failed to load dashboard statistics' + }, + + backup: { + title: 'Database Backup', + description: 'Full database backup to S3-compatible storage with scheduled backup and restore', + s3: { + title: 'S3 Storage Configuration', + description: 'Configure S3-compatible storage (supports Cloudflare R2)', + descriptionPrefix: 'Configure S3-compatible storage (supports', + descriptionSuffix: ')', + enabled: 'Enable S3 Storage', + endpoint: 'Endpoint', + region: 'Region', + bucket: 'Bucket', + prefix: 'Key Prefix', + accessKeyId: 'Access Key ID', + secretAccessKey: 'Secret Access Key', + secretConfigured: 'Already configured, leave empty to keep', + forcePathStyle: 'Force Path Style', + testConnection: 'Test Connection', + testSuccess: 'S3 connection test successful', + testFailed: 'S3 connection test failed', + saved: 'S3 configuration saved' + }, + schedule: { + title: 'Scheduled Backup', + description: 'Configure automatic scheduled backups', + enabled: 'Enable Scheduled Backup', + cronExpr: 'Cron Expression', + cronHint: 'e.g. "0 2 * * *" means every day at 2:00 AM', + retainDays: 'Backup Expire Days', + retainDaysHint: 'Backup files auto-delete after this many days, 0 = never expire', + retainCount: 'Max Retain Count', + retainCountHint: 'Maximum number of backups to keep, 0 = unlimited', + saved: 'Schedule configuration saved' + }, + operations: { + title: 'Backup Records', + description: 'Create manual backups and manage existing backup records', + createBackup: 'Create Backup', + backing: 'Backing up...', + backupCreated: 'Backup created successfully', + expireDays: 'Expire Days', + alreadyInProgress: 'A backup is already in progress', + backupRunning: 'Backup in progress...', + backupFailed: 'Backup failed', + restoreRunning: 'Restore in progress...', + restoreFailed: 'Restore failed', + }, + columns: { + status: 'Status', + fileName: 'File Name', + size: 'Size', + expiresAt: 'Expires At', + triggeredBy: 'Triggered By', + startedAt: 'Started At', + actions: 'Actions' + }, + status: { + pending: 'Pending', + running: 'Running', + completed: 'Completed', + failed: 'Failed' + }, + progress: { + pending: 'Preparing', + dumping: 'Dumping database', + uploading: 'Uploading', + }, + trigger: { + manual: 'Manual', + scheduled: 'Scheduled' + }, + neverExpire: 'Never', + empty: 'No backup records', + actions: { + download: 'Download', + restore: 'Restore', + restoreConfirm: 'Are you sure you want to restore from this backup? This will overwrite the current database!', + restorePasswordPrompt: 'Please enter your admin password to confirm the restore operation', + restoreSuccess: 'Database restored successfully', + deleteConfirm: 'Are you sure you want to delete this backup?', + deleted: 'Backup deleted' + }, + r2Guide: { + title: 'Cloudflare R2 Setup Guide', + intro: 'Cloudflare R2 provides S3-compatible object storage with a free tier of 10GB storage + 1M Class A requests/month, ideal for database backups.', + step1: { + title: 'Create an R2 Bucket', + line1: 'Log in to the Cloudflare Dashboard (dash.cloudflare.com), select "R2 Object Storage" from the sidebar', + line2: 'Click "Create bucket", enter a name (e.g. sub2api-backups), choose a region', + line3: 'Click create to finish' + }, + step2: { + title: 'Create an API Token', + line1: 'On the R2 page, click "Manage R2 API Tokens" in the top right', + line2: 'Click "Create API token", set permission to "Object Read & Write"', + line3: 'Recommended: restrict to specific bucket for better security', + line4: 'After creation, you will see the Access Key ID and Secret Access Key', + warning: 'The Secret Access Key is only shown once — copy and save it immediately!' + }, + step3: { + title: 'Get the S3 Endpoint', + desc: 'Find your Account ID on the R2 overview page (in the URL or the right panel). The endpoint format is:', + accountId: 'your_account_id' + }, + step4: { + title: 'Fill in the Configuration', + checkEnabled: 'Checked', + bucketValue: 'Your bucket name', + fromStep2: 'Value from Step 2', + unchecked: 'Unchecked' + }, + freeTier: 'R2 Free Tier: 10GB storage + 1M Class A requests + 10M Class B requests per month — more than enough for database backups.' + } + }, + + dataManagement: { + title: 'Data Management', + description: 'Manage data management agent status, object storage settings, and backup jobs in one place', + agent: { + title: 'Data Management Agent Status', + description: 'The system probes a fixed Unix socket and enables data management only when reachable.', + enabled: 'Data management agent is ready. Data management operations are available.', + disabled: 'Data management agent is unavailable. Only diagnostic information is available now.', + socketPath: 'Socket Path', + version: 'Version', + status: 'Status', + uptime: 'Uptime', + reasonLabel: 'Unavailable Reason', + reason: { + DATA_MANAGEMENT_AGENT_SOCKET_MISSING: 'Data management socket file is missing', + DATA_MANAGEMENT_AGENT_UNAVAILABLE: 'Data management agent is unreachable', + BACKUP_AGENT_SOCKET_MISSING: 'Backup socket file is missing', + BACKUP_AGENT_UNAVAILABLE: 'Backup agent is unreachable', + UNKNOWN: 'Unknown reason' + } + }, + sections: { + config: { + title: 'Backup Configuration', + description: 'Configure backup source, retention policy, and S3 settings.' + }, + s3: { + title: 'S3 Object Storage', + description: 'Configure and test uploads of backup artifacts to a standard S3-compatible storage.' + }, + backup: { + title: 'Backup Operations', + description: 'Trigger PostgreSQL, Redis, and full backup jobs.' + }, + history: { + title: 'Backup History', + description: 'Review backup job status, errors, and artifact metadata.' + } + }, + form: { + sourceMode: 'Source Mode', + backupRoot: 'Backup Root', + activePostgresProfile: 'Active PostgreSQL Profile', + activeRedisProfile: 'Active Redis Profile', + activeS3Profile: 'Active S3 Profile', + retentionDays: 'Retention Days', + keepLast: 'Keep Last Jobs', + uploadToS3: 'Upload to S3', + useActivePostgresProfile: 'Use Active PostgreSQL Profile', + useActiveRedisProfile: 'Use Active Redis Profile', + useActiveS3Profile: 'Use Active Profile', + idempotencyKey: 'Idempotency Key (Optional)', + secretConfigured: 'Configured already, leave empty to keep unchanged', + source: { + profileID: 'Profile ID (Unique)', + profileName: 'Profile Name', + setActive: 'Set as active after creation' + }, + postgres: { + title: 'PostgreSQL', + host: 'Host', + port: 'Port', + user: 'User', + password: 'Password', + database: 'Database', + sslMode: 'SSL Mode', + containerName: 'Container Name (docker_exec mode)' + }, + redis: { + title: 'Redis', + addr: 'Address (host:port)', + username: 'Username', + password: 'Password', + db: 'Database Index', + containerName: 'Container Name (docker_exec mode)' + }, + s3: { + enabled: 'Enable S3 Upload', + profileID: 'Profile ID (Unique)', + profileName: 'Profile Name', + endpoint: 'Endpoint (Optional)', + region: 'Region', + bucket: 'Bucket', + accessKeyID: 'Access Key ID', + secretAccessKey: 'Secret Access Key', + prefix: 'Object Prefix', + forcePathStyle: 'Force Path Style', + useSSL: 'Use SSL', + setActive: 'Set as active after creation' + } + }, + sourceProfiles: { + createTitle: 'Create Source Profile', + editTitle: 'Edit Source Profile', + empty: 'No source profiles yet, create one first', + deleteConfirm: 'Delete source profile {profileID}?', + columns: { + profile: 'Profile', + active: 'Active', + connection: 'Connection', + database: 'Database', + updatedAt: 'Updated At', + actions: 'Actions' + } + }, + s3Profiles: { + createTitle: 'Create S3 Profile', + editTitle: 'Edit S3 Profile', + empty: 'No S3 profiles yet, create one first', + editHint: 'Click "Edit" to modify profile details in the right drawer.', + deleteConfirm: 'Delete S3 profile {profileID}?', + columns: { + profile: 'Profile', + active: 'Active', + storage: 'Storage', + updatedAt: 'Updated At', + actions: 'Actions' + } + }, + history: { + total: '{count} jobs', + empty: 'No backup jobs yet', + columns: { + jobID: 'Job ID', + type: 'Type', + status: 'Status', + triggeredBy: 'Triggered By', + pgProfile: 'PostgreSQL Profile', + redisProfile: 'Redis Profile', + s3Profile: 'S3 Profile', + finishedAt: 'Finished At', + artifact: 'Artifact', + error: 'Error' + }, + status: { + queued: 'Queued', + running: 'Running', + succeeded: 'Succeeded', + failed: 'Failed', + partial_succeeded: 'Partial Succeeded' + } + }, + actions: { + refresh: 'Refresh Status', + disabledHint: 'Start datamanagementd first and ensure the socket is reachable.', + reloadConfig: 'Reload Config', + reloadSourceProfiles: 'Reload Source Profiles', + reloadProfiles: 'Reload Profiles', + newSourceProfile: 'New Source Profile', + saveConfig: 'Save Config', + configSaved: 'Configuration saved', + testS3: 'Test S3 Connection', + s3TestOK: 'S3 connection test succeeded', + s3TestFailed: 'S3 connection test failed', + newProfile: 'New Profile', + saveProfile: 'Save Profile', + activateProfile: 'Activate', + profileIDRequired: 'Profile ID is required', + profileNameRequired: 'Profile name is required', + profileSelectRequired: 'Select a profile to edit first', + profileCreated: 'S3 profile created', + profileSaved: 'S3 profile saved', + profileActivated: 'S3 profile activated', + profileDeleted: 'S3 profile deleted', + sourceProfileCreated: 'Source profile created', + sourceProfileSaved: 'Source profile saved', + sourceProfileActivated: 'Source profile activated', + sourceProfileDeleted: 'Source profile deleted', + createBackup: 'Create Backup Job', + jobCreated: 'Backup job created: {jobID} ({status})', + refreshJobs: 'Refresh Jobs', + loadMore: 'Load More' + } + }, + + affiliates: { + invitesDescription: 'View site-wide inviter and invitee relationships', + rebatesDescription: 'View recharge orders that generated affiliate rebates', + transfersDescription: 'View affiliate quota transfers into account balance', + errors: { + loadFailed: 'Failed to load affiliate records' + }, + records: { + search: 'Search', + searchPlaceholder: 'Email, username, user ID, or order number', + startAt: 'Start date', + endAt: 'End date', + inviter: 'Inviter', + invitee: 'Invitee', + user: 'User', + affCode: 'Invite Code', + order: 'Order', + totalRebate: 'Total Rebate', + orderAmount: 'Top-up Amount', + payAmount: 'Paid Amount', + rebateAmount: 'Rebate Amount', + paymentType: 'Payment Method', + orderStatus: 'Order Status', + transferAmount: 'Transfer Amount', + balanceAfter: 'Balance After', + availableQuotaAfter: 'Available After', + frozenQuotaAfter: 'Frozen After', + historyQuotaAfter: 'Historical Rebate After', + invitedAt: 'Invited At', + rebatedAt: 'Rebated At', + transferredAt: 'Transferred At' + }, + overview: { + title: 'Affiliate User Overview', + affCode: 'Invite Code', + rebateRate: 'Rebate Rate', + invitedCount: 'Invited Users', + rebatedInviteeCount: 'Rebated Invitees', + availableQuota: 'Available Quota', + historyQuota: 'Historical Rebate' + } + }, + + // Users + users: { + title: 'User Management', + description: 'Manage users and their permissions', + createUser: 'Create User', + editUser: 'Edit User', + deleteUser: 'Delete User', + searchUsers: 'Search by email, username, notes, or API key...', + allRoles: 'All Roles', + allStatus: 'All Status', + allGroups: 'All Groups', + searchGroups: 'Search groups...', + fuzzySearch: 'Fuzzy search', + apiKeyGroupFilter: 'API Key Group', + apiKeyGroupExclusive: 'Exclusive Groups', + apiKeyGroupPublic: 'Public Groups', + apiKeyGroupSubscription: 'Subscription Groups', + apiKeyGroupDisabled: 'Disabled Groups', + authorizedGroupFilter: 'Authorized Group', + allAuthorizedGroups: 'All Authorized Groups', + searchAuthorizedGroups: 'Search authorized groups...', + allApiKeyGroups: 'All API Key Groups', + searchApiKeyGroups: 'Search API Key groups...', + admin: 'Admin', + user: 'User', + disabled: 'Disabled', + email: 'Email', + password: 'Password', + username: 'Username', + notes: 'Notes', + enterEmail: 'Enter email', + enterPassword: 'Enter password', + enterUsername: 'Enter username (optional)', + enterNotes: 'Enter notes (admin only)', + notesHint: 'This note is only visible to administrators', + enterNewPassword: 'Enter new password (optional)', + leaveEmptyToKeep: 'Leave empty to keep current password', + generatePassword: 'Generate random password', + copyPassword: 'Copy password', + creating: 'Creating...', + updating: 'Updating...', + form: { + rpmLimit: 'Requests Per Minute (RPM)', + rpmLimitPlaceholder: '0 = unlimited', + rpmLimitHint: 'Max requests per minute for this user; 0 = unlimited. Acts as a fallback only when the group has no rpm_limit set.' + }, + columns: { + user: 'User', + id: 'ID', + email: 'Email', + username: 'Username', + notes: 'Notes', + role: 'Role', + groups: 'Groups', + subscriptions: 'Subscriptions', + balance: 'Balance', + balancePlatformQuota: 'Balance (Platform Quota)', + usage: 'Usage', + usageAnthropic: 'Usage (Claude)', + usageOpenAI: 'Usage (OpenAI)', + usageGemini: 'Usage (Gemini)', + usageAntigravity: 'Usage (Antigravity)', + concurrency: 'Concurrency', + status: 'Status', + lastActive: 'Last Active', + lastUsed: 'Last Used', + created: 'Created', + actions: 'Actions' + }, + today: 'Today', + total: 'Last 30d', + sortBy: 'Sort By', + sortCurrentPageOnly: 'Sorts current page only', + noSubscription: 'No subscription', + publicGroupCount: '+{count} public', + exclusiveLabel: 'exclusive', + publicLabel: 'public', + daysRemaining: '{days}d', + expired: 'Expired', + disable: 'Disable', + enable: 'Enable', + disableUser: 'Disable User', + enableUser: 'Enable User', + viewApiKeys: 'View API Keys', + groups: 'Groups', + apiKeys: 'API Keys', + userApiKeys: 'User API Keys', + noApiKeys: 'This user has no API keys', + group: 'Group', + none: 'None', + groupChangedSuccess: 'Group updated successfully', + groupChangedWithGrant: 'Group updated. User auto-granted access to "{group}"', + groupChangeFailed: 'Failed to update group', + noUsersYet: 'No users yet', + createFirstUser: 'Create your first user to get started.', + userCreated: 'User created successfully', + userUpdated: 'User updated successfully', + userDeleted: 'User deleted successfully', + userEnabled: 'User enabled successfully', + userDisabled: 'User disabled successfully', + failedToLoad: 'Failed to load users', + failedToCreate: 'Failed to create user', + failedToUpdate: 'Failed to update user', + failedToDelete: 'Failed to delete user', + failedToToggle: 'Failed to update user status', + failedToLoadApiKeys: 'Failed to load user API keys', + emailRequired: 'Please enter email', + concurrencyMin: 'Concurrency must be at least 1', + soraStorageQuota: 'Sora Storage Quota', + soraStorageQuotaHint: 'In GB, 0 means use group or system default quota', + amountRequired: 'Please enter a valid amount', + insufficientBalance: 'Insufficient balance', + deleteConfirm: "Are you sure you want to delete '{email}'? This action cannot be undone.", + setAllowedGroups: 'Set Allowed Groups', + allowedGroupsHint: + 'Select which standard groups this user can use. Subscription groups are managed separately.', + noStandardGroups: 'No standard groups available', + allowAllGroups: 'Allow All Groups', + allowAllGroupsHint: 'User can use any non-exclusive group', + allowedGroupsUpdated: 'Allowed groups updated successfully', + failedToLoadGroups: 'Failed to load groups', + failedToUpdateAllowedGroups: 'Failed to update allowed groups', + // User Group Configuration + groupConfig: 'User Group Configuration', + groupConfigHint: 'Configure custom rate multipliers for user {email} (overrides group defaults)', + exclusiveGroups: 'Exclusive Groups', + publicGroups: 'Public Groups (Default Available)', + defaultRate: 'Default Rate', + customRate: 'Custom Rate', + useDefaultRate: 'Use Default', + customRatePlaceholder: 'Leave empty for default', + groupConfigUpdated: 'Group configuration updated successfully', + replaceGroup: 'Replace Group', + clickToReplace: 'Click to replace', + replaceGroupTitle: 'Replace Exclusive Group', + replaceGroupHint: 'Select a new group to replace "{old}". Keys will be migrated and permissions updated automatically.', + replaceGroupConfirm: 'Confirm Replace', + replaceGroupSuccess: 'Group replaced successfully, {count} key(s) migrated', + selectNewGroup: 'Select target group', + noOtherGroups: 'No other exclusive groups available', + deposit: 'Deposit', + withdraw: 'Withdraw', + depositAmount: 'Deposit Amount', + withdrawAmount: 'Withdraw Amount', + withdrawAll: 'All', + currentBalance: 'Current Balance', + depositNotesPlaceholder: + 'e.g., New user registration bonus, promotional credit, compensation, etc.', + withdrawNotesPlaceholder: + 'e.g., Service issue refund, incorrect charge reversal, account closure refund, etc.', + notesOptional: 'Notes are optional but helpful for record keeping', + amountHint: 'Please enter a positive amount', + newBalance: 'New Balance', + depositing: 'Depositing...', + withdrawing: 'Withdrawing...', + confirmDeposit: 'Confirm Deposit', + confirmWithdraw: 'Confirm Withdraw', + depositSuccess: 'Deposit successful', + withdrawSuccess: 'Withdraw successful', + failedToDeposit: 'Failed to deposit', + failedToWithdraw: 'Failed to withdraw', + useDepositWithdrawButtons: 'Please use deposit/withdraw buttons to adjust balance', + // Balance History + balanceHistory: 'Recharge History', + balanceHistoryTip: 'Click to open recharge history', + columnAlwaysVisible: 'This column is always visible', + // Per-platform usage breakdown (hover tooltip) + platformBreakdown: 'Per-platform breakdown', + platformBreakdownEmpty: 'No platform usage yet', + platformBreakdownHint: 'Hover for per-platform usage', + platformOther: 'Other', + balanceHistoryTitle: 'User Recharge & Concurrency History', + noBalanceHistory: 'No records found for this user', + allTypes: 'All Types', + typeBalance: 'Balance (Redeem)', + typeAffiliateBalance: 'Balance (Affiliate Transfer)', + typeAdminBalance: 'Balance (Admin)', + typeConcurrency: 'Concurrency (Redeem)', + typeAdminConcurrency: 'Concurrency (Admin)', + typeSubscription: 'Subscription', + failedToLoadBalanceHistory: 'Failed to load balance history', + createdAt: 'Created', + totalRecharged: 'Total Recharged', + roles: { + admin: 'Admin', + user: 'User' + }, + // Settings Dropdowns + filterSettings: 'Filter Settings', + columnSettings: 'Column Settings', + filterValue: 'Enter value', + // User Attributes + attributes: { + title: 'User Attributes', + description: 'Configure custom user attribute fields', + configButton: 'Attributes', + addAttribute: 'Add Attribute', + editAttribute: 'Edit Attribute', + deleteAttribute: 'Delete Attribute', + deleteConfirm: "Are you sure you want to delete attribute '{name}'? All user values for this attribute will be deleted.", + noAttributes: 'No custom attributes', + noAttributesHint: 'Click the button above to add custom attributes', + key: 'Attribute Key', + keyHint: 'For programmatic reference, only letters, numbers and underscores', + name: 'Display Name', + nameHint: 'Name shown in forms', + type: 'Attribute Type', + fieldDescription: 'Description', + fieldDescriptionHint: 'Description text for the attribute', + placeholder: 'Placeholder', + placeholderHint: 'Placeholder text for input field', + required: 'Required', + enabled: 'Enabled', + options: 'Options', + optionsHint: 'For select/multi-select types', + addOption: 'Add Option', + optionValue: 'Option Value', + optionLabel: 'Display Text', + validation: 'Validation Rules', + minLength: 'Min Length', + maxLength: 'Max Length', + min: 'Min Value', + max: 'Max Value', + pattern: 'Regex Pattern', + patternMessage: 'Validation Error Message', + types: { + text: 'Text', + textarea: 'Textarea', + number: 'Number', + email: 'Email', + url: 'URL', + date: 'Date', + select: 'Select', + multi_select: 'Multi-Select' + }, + created: 'Attribute created successfully', + updated: 'Attribute updated successfully', + deleted: 'Attribute deleted successfully', + reordered: 'Attribute order updated successfully', + failedToLoad: 'Failed to load attributes', + failedToCreate: 'Failed to create attribute', + failedToUpdate: 'Failed to update attribute', + keyRequired: 'Please enter attribute key', + nameRequired: 'Please enter display name', + optionsRequired: 'Please add at least one option', + failedToDelete: 'Failed to delete attribute', + failedToReorder: 'Failed to update order', + keyExists: 'Attribute key already exists', + dragToReorder: 'Drag to reorder' + }, + platformQuota: { + menuItem: 'Platform Quotas', + title: 'Platform Quotas', + subtitle: 'Configure daily / weekly / monthly USD usage limits for each upstream platform for user {email}', + columns: { + platform: 'Platform', + daily: 'Daily (USD)', + weekly: 'Weekly (USD)', + monthly: 'Monthly (USD, 30-day rolling)', + usage: 'Current Usage', + }, + placeholder: 'unlimited', + save: 'Save', + saving: 'Saving...', + cancel: 'Cancel', + clearAll: 'Clear All (remove all limits)', + clearAllConfirm: 'Clear daily / weekly / monthly limits for ALL platforms? All platforms will become "unlimited" with no local undo — you must manually re-enter values before saving.', + reset: { + button: 'Reset window', + confirm: 'Reset the {window} usage for {platform} for this user? This is effective immediately.', + success: 'Reset {platform} {window} usage', + failed: 'Reset failed', + }, + updateSuccess: 'Platform quotas updated', + updateFailed: 'Save failed', + loadFailed: 'Load failed', + hint: 'Empty = no limit for that window.', + windowDaily: 'daily', + windowWeekly: 'weekly', + windowMonthly: 'monthly', + cellNotConfigured: 'Not configured', + cellColumnTooltip: 'Only platforms with a limit are shown', + subscriptionWarning: 'This user has an active subscription. Platform quotas only apply to balance (standard) mode requests; subscription mode requests are not subject to these limits.', + invalidNumber: 'The following fields contain invalid numbers. Please fix them before saving: {fields}', + } + }, + + // Groups + groups: { + title: 'Group Management', + description: 'Manage API key groups and rate multipliers', + searchGroups: 'Search groups...', + createGroup: 'Create Group', + editGroup: 'Edit Group', + deleteGroup: 'Delete Group', + sortOrder: 'Sort', + columnSettings: 'Column Settings', + sortOrderHint: 'Drag groups to adjust display order, groups at the top will be displayed first', + sortOrderUpdated: 'Sort order updated', + failedToUpdateSortOrder: 'Failed to update sort order', + allPlatforms: 'All Platforms', + allStatus: 'All Status', + allGroups: 'All Groups', + exclusive: 'Exclusive', + nonExclusive: 'Non-Exclusive', + public: 'Public', + columns: { + name: 'Name', + platform: 'Platform', + rateMultiplier: 'Rate Multiplier', + rpmOverride: 'RPM Override', + rpmOverrideHint: 'Per-user RPM cap in this group; empty = group default; 0 = unlimited', + rateDefault: 'default', + rpmDefault: 'default', + type: 'Type', + accounts: 'Accounts', + capacity: 'Capacity', + usage: 'Usage', + status: 'Status', + actions: 'Actions', + billingType: 'Billing Type', + userName: 'Username', + userEmail: 'Email', + userNotes: 'Notes', + userStatus: 'Status' + }, + usageToday: 'Today', + usageTotal: 'Total', + accountsAvailable: 'Avail:', + accountsRateLimited: 'Limited:', + accountsTotal: 'Total:', + accountsUnit: '', + rateAndAccounts: '{rate}x rate · {count} accounts', + accountsCount: '{count} accounts', + rateLabel: 'rate', + accountFilters: { + title: 'Account Filter Controls', + oauthOnly: 'Only allow OAuth accounts', + oauthOnlyEnabled: 'Enabled — API Key accounts will be excluded', + privacySetOnly: 'Only allow accounts with privacy protection set', + privacySetOnlyEnabled: 'Enabled — accounts with unset Privacy will be excluded', + disabled: 'Disabled' + }, + form: { + name: 'Name', + description: 'Description', + platform: 'Platform', + rateMultiplier: 'Rate Multiplier', + status: 'Status', + exclusive: 'Exclusive Group', + rpmLimit: 'Requests Per Minute (RPM)', + rpmLimitPlaceholder: '0 = unlimited', + rpmLimitHint: 'Max requests per minute for each user in this group; 0 = unlimited. Once set, it takes over per-user rate limiting in this group (overrides the user-level rpm_limit fallback).' + }, + enterGroupName: 'Enter group name', + optionalDescription: 'Optional description', + platformHint: 'Select the platform this group is associated with', + platformNotEditable: 'Platform cannot be changed after creation', + rateMultiplierHint: 'Cost multiplier for this group (e.g., 1.5 = 150% of base cost)', + exclusiveHint: 'Exclusive group, manually assign to specific users', + exclusiveTooltip: { + title: 'What is an exclusive group?', + description: 'When enabled, users cannot see this group when creating API Keys. Only after an admin manually assigns a user to this group can they use it.', + example: 'Use case:', + exampleContent: 'Public group rate is 0.8. Create an exclusive group with 0.7 rate, manually assign VIP users to give them better pricing.' + }, + noGroupsYet: 'No groups yet', + createFirstGroup: 'Create your first group to organize API keys.', + creating: 'Creating...', + updating: 'Updating...', + limitDay: 'd', + limitWeek: 'w', + limitMonth: 'mo', + groupCreated: 'Group created successfully', + groupUpdated: 'Group updated successfully', + groupDeleted: 'Group deleted successfully', + failedToLoad: 'Failed to load groups', + failedToCreate: 'Failed to create group', + failedToUpdate: 'Failed to update group', + failedToDelete: 'Failed to delete group', + nameRequired: 'Please enter group name', + rateMultipliers: 'Rate Multipliers', + rateMultipliersTitle: 'Group Rate Multipliers', + addUserRate: 'Add User Rate Multiplier', + rpmOverrides: 'RPM Overrides', + rpmOverridesTitle: 'Group RPM Overrides', + addUserRpm: 'Add User RPM Override', + noRpmOverrides: 'No users have an RPM override yet', + rpmSaved: 'RPM overrides saved', + groupRpmDefault: 'Group default RPM', + searchUserPlaceholder: 'Search user email...', + noRateMultipliers: 'No user rate multipliers configured', + rateUpdated: 'Rate multiplier updated', + rateDeleted: 'Rate multiplier removed', + rateAdded: 'Rate multiplier added', + clearAll: 'Clear All', + confirmClearAll: 'Are you sure you want to clear all rate multiplier settings for this group? This cannot be undone.', + rateCleared: 'All rate multipliers cleared', + batchAdjust: 'Batch Adjust Rates', + multiplierFactor: 'Factor', + applyMultiplier: 'Apply', + rateAdjusted: 'Rates adjusted successfully', + rateSaved: 'Rate multipliers saved', + finalRate: 'Final Rate', + unsavedChanges: 'Unsaved changes', + revertChanges: 'Revert', + userInfo: 'User Info', + platforms: { + all: 'All Platforms', + anthropic: 'Anthropic', + openai: 'OpenAI', + gemini: 'Gemini', + antigravity: 'Antigravity', + grok: 'Grok', + }, + deleteConfirm: + "Are you sure you want to delete '{name}'? All associated API keys will no longer belong to any group.", + deleteConfirmSubscription: + "Are you sure you want to delete subscription group '{name}'? This will invalidate all API keys bound to this subscription and delete all related subscription records. This action cannot be undone.", + subscription: { + title: 'Subscription Settings', + type: 'Billing Type', + typeHint: + 'Standard billing deducts from user balance. Subscription mode uses quota limits instead.', + typeNotEditable: 'Billing type cannot be changed after group creation.', + standard: 'Standard (Balance)', + subscription: 'Subscription (Quota)', + dailyLimit: 'Daily Limit (USD)', + weeklyLimit: 'Weekly Limit (USD)', + monthlyLimit: 'Monthly Limit (USD)', + defaultValidityDays: 'Default Validity (Days)', + validityHint: 'Number of days the subscription is valid when assigned to a user', + noLimit: 'No limit' + }, + imagePricing: { + title: 'Image Generation Pricing', + description: 'Configure image generation access and base image prices. Leave empty to use default prices.', + allowImageGeneration: 'Allow image generation for this group', + allowBatchImageGeneration: 'Allow batch image generation for this group', + independentMultiplier: 'Use independent image multiplier', + imageMultiplier: 'Image multiplier', + batchDiscountMultiplier: 'Batch image discount', + batchHoldMultiplier: 'Batch hold price ratio', + batchSectionHint: 'Batch image settings only apply to batch jobs: settlement applies the batch discount, and the upfront hold is normal image price × batch hold price ratio. Reference images also create upstream input-token usage, so a batch image discount above 0.5 is recommended.', + batchDisabledHint: 'Enable image generation for this group before enabling batch image generation.', + batchGeminiOnlyHint: 'Batch image generation is currently available only for Gemini groups.', + modeHint: 'By default, image billing uses image price × current effective group multiplier. Independent mode uses image price × image multiplier.', + finalPricePreview: 'Final per-image price preview', + notConfigured: 'Not configured' + }, + peakRate: { + enable: 'Enable peak rate multiplier', + peakStart: 'Peak start', + peakEnd: 'Peak end', + peakMultiplier: 'Peak multiplier', + multiplierHint: 'Applies to token billing multiplier; image tokens in token billing are also affected. 0 means peak token requests are billed at 0x.' + }, + modelsList: { + title: 'Custom /v1/models Model List', + hint: 'Only changes the /v1/models response. Whitelist model calls and account routing are unchanged.', + loading: 'Loading model list...', + empty: 'No displayable models', + selectedSummary: 'Selected {selected} / {total}', + selectAll: 'Select all', + invertSelection: 'Invert' + }, + claudeCode: { + title: 'Claude Code Client Restriction', + tooltip: 'When enabled, this group only allows official Claude Code clients. Non-Claude Code requests will be rejected or fallback to the specified group.', + enabled: 'Claude Code Only', + disabled: 'Allow All Clients', + fallbackGroup: 'Fallback Group', + fallbackHint: 'Non-Claude Code requests will use this group. Leave empty to reject directly.', + noFallback: 'No Fallback (Reject)' + }, + openaiMessages: { + title: 'OpenAI Messages Dispatch', + allowDispatch: 'Allow /v1/messages dispatch', + allowDispatchHint: 'When enabled, API keys in this OpenAI group can dispatch requests through /v1/messages endpoint', + familyMappingTitle: 'Family Default Mapping', + familyMappingHint: 'Requests that match the Opus, Sonnet, or Haiku families will prefer the target model configured here.', + opusModel: 'Opus Target Model', + opusModelPlaceholder: 'e.g., gpt-5.4', + sonnetModel: 'Sonnet Target Model', + sonnetModelPlaceholder: 'e.g., gpt-5.3-codex', + haikuModel: 'Haiku Target Model', + haikuModelPlaceholder: 'e.g., gpt-5.4-mini', + exactMappingTitle: 'Exact Model Overrides', + exactMappingHint: 'Exact Claude model overrides take priority over the family defaults and can route a specific Claude model to a different target model.', + noExactMappings: 'No exact model overrides yet', + addExactMapping: 'Add Exact Mapping', + claudeModel: 'Claude Model', + claudeModelPlaceholder: 'e.g., claude-sonnet-4-5-20250929', + targetModel: 'Target Model', + targetModelPlaceholder: 'e.g., gpt-5.4', + removeExactMapping: 'Remove Exact Mapping' + }, + invalidRequestFallback: { + title: 'Invalid Request Fallback Group', + hint: 'Triggered only when upstream explicitly returns prompt too long. Leave empty to disable fallback.', + noFallback: 'No Fallback' + }, + copyAccounts: { + title: 'Copy Accounts from Groups', + tooltip: 'Select one or more groups of the same platform. After creation, all accounts from these groups will be automatically bound to the new group (deduplicated).', + tooltipEdit: 'Select one or more groups of the same platform. After saving, current group accounts will be replaced with accounts from these groups (deduplicated).', + selectPlaceholder: 'Select groups to copy accounts from...', + hint: 'Multiple groups can be selected, accounts will be deduplicated', + hintEdit: '⚠️ Warning: This will replace all existing account bindings' + }, + modelRouting: { + title: 'Model Routing', + tooltip: 'Configure specific model requests to be routed to designated accounts. Supports wildcard matching, e.g., claude-opus-* matches all opus models.', + enabled: 'Enabled', + disabled: 'Disabled', + disabledHint: 'Routing rules will only take effect when enabled', + addRule: 'Add Routing Rule', + modelPattern: 'Model Pattern', + modelPatternPlaceholder: 'claude-opus-*', + modelPatternHint: 'Supports * wildcard, e.g., claude-opus-* matches all opus models', + accounts: 'Priority Accounts', + selectAccounts: 'Select accounts', + noAccounts: 'No accounts in this group', + loadingAccounts: 'Loading accounts...', + removeRule: 'Remove Rule', + noRules: 'No routing rules', + noRulesHint: 'Add routing rules to route specific model requests to designated accounts', + searchAccountPlaceholder: 'Search accounts...', + accountsHint: 'Select accounts to prioritize for this model pattern' + }, + mcpXml: { + title: 'MCP XML Protocol Injection', + tooltip: 'When enabled, if the request contains MCP tools, an XML format call protocol prompt will be injected into the system prompt. Disable this to avoid interference with certain clients.', + enabled: 'Enabled', + disabled: 'Disabled' + }, + claudeMaxSimulation: { + title: 'Claude Max Usage Simulation', + tooltip: + 'When enabled, for Claude models without upstream cache-write usage, the system deterministically maps tokens to a small input plus 1h cache creation while keeping total tokens unchanged.', + enabled: 'Enabled (simulate 1h cache)', + disabled: 'Disabled', + hint: 'Only token categories in usage billing logs are adjusted. No per-request mapping state is persisted.' + }, + supportedScopes: { + title: 'Supported Model Families', + tooltip: 'Select the model families this group supports. Unchecked families will not be routed to this group.', + claude: 'Claude', + geminiText: 'Gemini Text', + geminiImage: 'Gemini Image', + hint: 'Select at least one model family' + } + }, + + // Available Channels (aggregated read-only view) +} diff --git a/frontend/src/i18n/locales/en/admin/resources.ts b/frontend/src/i18n/locales/en/admin/resources.ts new file mode 100644 index 0000000000..30b05e5e6b --- /dev/null +++ b/frontend/src/i18n/locales/en/admin/resources.ts @@ -0,0 +1,520 @@ +export default { + scheduledTests: { + title: 'Scheduled Tests', + addPlan: 'Add Plan', + editPlan: 'Edit Plan', + deletePlan: 'Delete Plan', + model: 'Model', + cronExpression: 'Cron Expression', + enabled: 'Enabled', + lastRun: 'Last Run', + nextRun: 'Next Run', + maxResults: 'Max Results', + noPlans: 'No scheduled test plans', + confirmDelete: 'Are you sure you want to delete this plan?', + createSuccess: 'Plan created successfully', + updateSuccess: 'Plan updated successfully', + deleteSuccess: 'Plan deleted successfully', + results: 'Test Results', + noResults: 'No test results yet', + responseText: 'Response', + errorMessage: 'Error', + success: 'Success', + failed: 'Failed', + running: 'Running', + schedule: 'Schedule', + cronHelp: 'Standard 5-field cron expression (e.g., */30 * * * *)', + cronTooltipTitle: 'Cron expression examples:', + cronTooltipMeaning: 'Defines when the test runs automatically. The 5 fields are: minute, hour, day, month, and weekday.', + cronTooltipExampleEvery30Min: '*/30 * * * *: run every 30 minutes', + cronTooltipExampleHourly: '0 * * * *: run at the start of every hour', + cronTooltipExampleDaily: '0 9 * * *: run every day at 09:00', + cronTooltipExampleWeekly: '0 9 * * 1: run every Monday at 09:00', + cronTooltipRange: 'Recommended range: use standard 5-field cron. For health checks, start with a moderate frequency such as every 30 minutes, every hour, or once a day instead of running too often.', + maxResultsTooltipTitle: 'What Max Results means:', + maxResultsTooltipMeaning: 'Sets how many historical test results are kept for a single plan so the result list does not grow without limit.', + maxResultsTooltipBody: 'Only the newest test results are kept. Once the number of saved results exceeds this value, older records are pruned automatically so the history list and storage stay under control.', + maxResultsTooltipExample: 'For example, 100 means keeping at most the latest 100 test results. When the 101st result is saved, the oldest one is removed.', + maxResultsTooltipRange: 'Recommended range: usually 20 to 200. Use 20-50 when you only care about recent health status, or 100-200 if you want a longer trend history.', + autoRecover: 'Auto Recover', + autoRecoverHelp: 'Automatically recover account from error/rate-limited state on successful test' + }, + + // Proxies + proxies: { + title: 'Proxy Management', + description: 'Manage proxy servers for accounts', + createProxy: 'Create Proxy', + editProxy: 'Edit Proxy', + deleteProxy: 'Delete Proxy', + ad: { + inline: 'Need proxy IP?' + }, + dataImport: 'Import', + dataExportSelected: 'Export Selected', + dataImportTitle: 'Import Proxies', + dataImportHint: 'Upload the exported proxy JSON file to import proxies in bulk.', + dataImportWarning: 'Import will create or reuse proxies, keep their status, and trigger latency checks after completion.', + dataImportFile: 'Data File', + dataImportButton: 'Start Import', + dataImporting: 'Importing...', + dataImportSelectFile: 'Please select a data file', + dataImportParseFailed: 'Failed to parse data', + dataImportFailed: 'Failed to import data', + dataImportResult: 'Import Result', + dataImportResultSummary: 'Created {proxy_created}, reused {proxy_reused}, failed {proxy_failed}', + dataImportErrors: 'Failure Details', + dataImportSuccess: 'Import completed: created {proxy_created}, reused {proxy_reused}', + dataImportCompletedWithErrors: 'Import completed with errors: failed {proxy_failed}', + dataExport: 'Export', + dataExportConfirmMessage: 'The exported data contains sensitive proxy information. Store it securely.', + dataExportConfirm: 'Confirm Export', + dataExported: 'Data exported successfully', + dataExportFailed: 'Failed to export data', + copyProxyUrl: 'Copy Proxy URL', + urlCopied: 'Proxy URL copied', + searchProxies: 'Search proxies...', + allProtocols: 'All Protocols', + allStatus: 'All Status', + protocols: { + http: 'HTTP', + https: 'HTTPS', + socks5: 'SOCKS5', + socks5h: 'SOCKS5H (Remote DNS)' + }, + columns: { + name: 'Name', + protocol: 'Protocol', + address: 'Address', + auth: 'Auth', + location: 'Location', + status: 'Status', + accounts: 'Accounts', + latency: 'Latency', + expiry: 'Validity', + createdAt: 'Created', + actions: 'Actions' + }, + testConnection: 'Test Connection', + qualityCheck: 'Quality Check', + batchQualityCheck: 'Batch Quality Check', + batchTest: 'Test All Proxies', + testFailed: 'Failed', + latencyFailed: 'Connection failed', + batchTestEmpty: 'No proxies available for testing', + batchTestDone: 'Batch test completed for {count} proxies', + batchTestFailed: 'Batch test failed', + batchDeleteAction: 'Delete', + batchDelete: 'Batch delete', + batchDeleteConfirm: 'Delete {count} selected proxies? In-use ones will be skipped.', + batchDeleteDone: 'Deleted {deleted} proxies, skipped {skipped}', + batchDeleteSkipped: 'Skipped {skipped} proxies', + batchDeleteFailed: 'Batch delete failed', + deleteBlockedInUse: 'This proxy is in use and cannot be deleted', + accountsTitle: 'Accounts using this IP', + accountsEmpty: 'No accounts are using this proxy', + accountsFailed: 'Failed to load accounts list', + accountName: 'Account', + accountPlatform: 'Platform', + accountNotes: 'Notes', + name: 'Name', + protocol: 'Protocol', + host: 'Host', + port: 'Port', + username: 'Username (Optional)', + password: 'Password (Optional)', + status: 'Status', + enterProxyName: 'Enter proxy name', + leaveEmptyToKeep: 'Leave empty to keep current', + optionalAuth: 'Optional authentication', + form: { + hostPlaceholder: 'proxy.example.com', + portPlaceholder: '8080' + }, + noProxiesYet: 'No proxies yet', + createFirstProxy: 'Create your first proxy to route traffic through it.', + // Batch import + standardAdd: 'Standard Add', + batchAdd: 'Quick Add', + batchInput: 'Proxy List', + batchInputPlaceholder: + "Enter one proxy per line in the following formats:\nsocks5://user:pass{'@'}192.168.1.1:1080\nhttp://192.168.1.1:8080\nhttps://user:pass{'@'}proxy.example.com:443", + batchInputHint: + "Supports http, https, socks5 protocols. Format: protocol://[user:pass{'@'}]host:port", + parsedCount: '{count} valid', + invalidCount: '{count} invalid', + duplicateCount: '{count} duplicate', + importing: 'Importing...', + importProxies: 'Import {count} proxies', + batchImportSuccess: 'Successfully imported {created} proxies, skipped {skipped} duplicates', + batchImportAllSkipped: 'All {skipped} proxies already exist, skipped import', + failedToImport: 'Failed to batch import', + // Other messages + creating: 'Creating...', + updating: 'Updating...', + proxyCreated: 'Proxy created successfully', + proxyUpdated: 'Proxy updated successfully', + proxyDeleted: 'Proxy deleted successfully', + proxyWorking: 'Proxy is working!', + proxyWorkingWithLatency: 'Proxy is working! Latency: {latency}ms', + proxyTestFailed: 'Proxy test failed', + qualityCheckDone: 'Quality check completed: score {score} ({grade})', + qualityCheckFailed: 'Failed to run proxy quality check', + batchQualityDone: + 'Batch quality check completed for {count} proxies: healthy {healthy}, warn {warn}, challenge {challenge}, abnormal {failed}', + batchQualityFailed: 'Batch quality check failed', + batchQualityEmpty: 'No proxies available for quality check', + qualityReportTitle: 'Proxy Quality Report', + qualityGrade: 'Grade {grade}', + qualityExitIP: 'Exit IP', + qualityCountry: 'Exit Region', + qualityBaseLatency: 'Base Latency', + qualityCheckedAt: 'Checked At', + qualityTableTarget: 'Target', + qualityTableStatus: 'Status', + qualityTableLatency: 'Latency', + qualityTableMessage: 'Message', + qualityInline: 'Quality {grade}/{score}', + qualityStatusHealthy: 'Healthy', + qualityStatusPass: 'Pass', + qualityStatusWarn: 'Warn', + qualityStatusFail: 'Fail', + qualityStatusChallenge: 'Challenge', + qualityTargetBase: 'Base Connectivity', + failedToLoad: 'Failed to load proxies', + failedToCreate: 'Failed to create proxy', + failedToUpdate: 'Failed to update proxy', + failedToDelete: 'Failed to delete proxy', + failedToTest: 'Failed to test proxy', + nameRequired: 'Please enter proxy name', + hostRequired: 'Please enter host address', + portInvalid: 'Port must be between 1-65535', + deleteConfirm: + "Are you sure you want to delete '{name}'? Accounts using this proxy will have their proxy removed.", + neverExpires: 'Never', + expired: 'Expired', + overdueDays: 'Overdue {days}d', + expiringInDays: 'Expires in {days}d', + remainingDays: '{days}d left', + expiresAt: 'Validity', + nDays: '{days}d', + expiryDaysPlaceholder: 'Custom days, empty = never', + expiryWarnDays: 'Expiry warning (days)', + fallbackMode: 'Failure fallback', + fallbackNone: 'No fallback', + fallbackProxy: 'Backup proxy', + fallbackDirect: 'Direct connection', + backupProxy: 'Backup proxy', + }, + + // Redeem Codes + redeem: { + title: 'Redeem Code Management', + description: 'Generate and manage redeem codes', + generateCodes: 'Generate Codes', + searchCodes: 'Search codes or email...', + allTypes: 'All Types', + allStatus: 'All Status', + balance: 'Balance', + concurrency: 'Concurrency', + subscription: 'Subscription', + invitation: 'Invitation', + invitationHint: 'Invitation codes are used to restrict user registration. They are automatically marked as used after use.', + unused: 'Unused', + used: 'Used', + columns: { + code: 'Code', + type: 'Type', + value: 'Value', + status: 'Status', + usedBy: 'Used By', + usedAt: 'Used At', + expiresAt: 'Expires At', + actions: 'Actions' + }, + userPrefix: 'User #{id}', + exportCsv: 'Export CSV', + batchUpdate: 'Batch Update', + batchUpdateTitle: 'Batch Update Redeem Codes', + selectedCount: '{count} redeem code(s) selected', + clearSelection: 'Clear selection', + selectCodesFirst: 'Select redeem codes first', + noBatchFieldsSelected: 'Select at least one field to update', + batchUpdateSuccess: 'Updated {count} redeem code(s)', + failedToBatchUpdate: 'Failed to batch update redeem codes', + batchFields: { + status: 'Status', + expiresAt: 'Expires At', + notes: 'Notes', + group: 'Group' + }, + batchNotesPlaceholder: 'Enter the new note, or leave blank to clear it', + clearGroup: 'Clear group', + deleteAllUnused: 'Delete All Unused Codes', + deleteCode: 'Delete Redeem Code', + deleteCodeConfirm: + 'Are you sure you want to delete this redeem code? This action cannot be undone.', + deleteAllUnusedConfirm: + 'Are you sure you want to delete all unused (active) redeem codes? This action cannot be undone.', + deleteAll: 'Delete All', + generateCodesTitle: 'Generate Redeem Codes', + generatedSuccessfully: 'Generated Successfully', + codesCreated: '{count} redeem code(s) created', + codeType: 'Code Type', + amount: 'Amount ($)', + value: 'Value', + count: 'Count', + generating: 'Generating...', + generate: 'Generate', + copyAll: 'Copy All', + copied: 'Copied!', + download: 'Download', + codesExported: 'Codes exported successfully', + codeDeleted: 'Redeem code deleted successfully', + codesDeleted: 'Successfully deleted {count} unused code(s)', + noUnusedCodes: 'No unused codes to delete', + failedToLoad: 'Failed to load redeem codes', + failedToGenerate: 'Failed to generate codes', + failedToExport: 'Failed to export codes', + failedToDelete: 'Failed to delete code', + failedToDeleteUnused: 'Failed to delete unused codes', + failedToCopy: 'Failed to copy codes', + types: { + balance: 'Balance', + concurrency: 'Concurrency', + subscription: 'Subscription', + invitation: 'Invitation', + // Admin adjustment types (created when admin modifies user balance/concurrency) + admin_balance: 'Balance (Admin)', + admin_concurrency: 'Concurrency (Admin)' + }, + selectGroup: 'Select Group', + selectGroupPlaceholder: 'Choose a subscription group', + validityDays: 'Validity Days', + codeExpiry: 'Code Expiry', + neverExpires: 'Never expires', + expiryPresetDays: '{days} days', + customExpiry: 'Custom', + customExpiryDays: 'Custom days', + expiryDaysRequired: 'Please enter a valid expiry day count', + groupRequired: 'Please select a subscription group', + days: ' days', + status: { + unused: 'Unused', + used: 'Used', + expired: 'Expired', + disabled: 'Disabled' + } + }, + + // Announcements + announcements: { + title: 'Announcements', + description: 'Create announcements and target by conditions', + createAnnouncement: 'Create Announcement', + editAnnouncement: 'Edit Announcement', + deleteAnnouncement: 'Delete Announcement', + searchAnnouncements: 'Search announcements...', + status: 'Status', + allStatus: 'All Status', + columns: { + title: 'Title', + status: 'Status', + notifyMode: 'Notify Mode', + targeting: 'Targeting', + timeRange: 'Schedule', + createdAt: 'Created At', + actions: 'Actions' + }, + statusLabels: { + draft: 'Draft', + active: 'Active', + archived: 'Archived' + }, + notifyModeLabels: { + silent: 'Silent', + popup: 'Popup' + }, + form: { + title: 'Title', + content: 'Content (Markdown supported)', + status: 'Status', + notifyMode: 'Notify Mode', + notifyModeHint: 'Popup mode will show a popup notification to users', + startsAt: 'Starts At', + endsAt: 'Ends At', + startsAtHint: 'Leave empty to start immediately', + endsAtHint: 'Leave empty to never expire', + targetingMode: 'Targeting', + targetingAll: 'All users', + targetingCustom: 'Custom rules', + addOrGroup: 'Add OR group', + addAndCondition: 'Add AND condition', + conditionType: 'Condition type', + conditionSubscription: 'Subscription', + conditionBalance: 'Balance', + operator: 'Operator', + balanceValue: 'Balance threshold', + selectPackages: 'Select packages' + }, + operators: { + gt: '>', + gte: '≥', + lt: '<', + lte: '≤', + eq: '=' + }, + targetingSummaryAll: 'All users', + targetingSummaryCustom: 'Custom ({groups} groups)', + timeImmediate: 'Immediate', + timeNever: 'Never', + readStatus: 'Read Status', + eligible: 'Eligible', + readAt: 'Read at', + unread: 'Unread', + searchUsers: 'Search users...', + failedToLoad: 'Failed to load announcements', + failedToCreate: 'Failed to create announcement', + failedToUpdate: 'Failed to update announcement', + failedToDelete: 'Failed to delete announcement', + failedToLoadReadStatus: 'Failed to load read status', + deleteConfirm: 'Are you sure you want to delete this announcement? This action cannot be undone.' + }, + + // Promo Codes + promo: { + title: 'Promo Code Management', + description: 'Create and manage registration promo codes', + createCode: 'Create Promo Code', + editCode: 'Edit Promo Code', + deleteCode: 'Delete Promo Code', + searchCodes: 'Search codes...', + allStatus: 'All Status', + columns: { + code: 'Code', + bonusAmount: 'Bonus Amount', + maxUses: 'Max Uses', + usedCount: 'Used', + usage: 'Usage', + status: 'Status', + expiresAt: 'Expires At', + createdAt: 'Created At', + actions: 'Actions' + }, + // Form labels (flat structure for template usage) + code: 'Promo Code', + autoGenerate: 'auto-generate if empty', + codePlaceholder: 'Enter promo code or leave empty', + bonusAmount: 'Bonus Amount ($)', + maxUses: 'Max Uses', + zeroUnlimited: '0 = unlimited', + expiresAt: 'Expires At', + notes: 'Notes', + notesPlaceholder: 'Optional notes for this code', + status: 'Status', + neverExpires: 'Never expires', + // Status labels + statusActive: 'Active', + statusDisabled: 'Disabled', + statusExpired: 'Expired', + statusMaxUsed: 'Used Up', + // Usage records + usageRecords: 'Usage Records', + viewUsages: 'View Usages', + noUsages: 'No usage records yet', + userPrefix: 'User #{id}', + copied: 'Copied!', + // Messages + noCodesYet: 'No promo codes yet', + createFirstCode: 'Create your first promo code to offer registration bonuses.', + codeCreated: 'Promo code created successfully', + codeUpdated: 'Promo code updated successfully', + codeDeleted: 'Promo code deleted successfully', + deleteCodeConfirm: 'Are you sure you want to delete this promo code? This action cannot be undone.', + copyRegisterLink: 'Copy register link', + registerLinkCopied: 'Register link copied to clipboard', + failedToLoad: 'Failed to load promo codes', + failedToCreate: 'Failed to create promo code', + failedToUpdate: 'Failed to update promo code', + failedToDelete: 'Failed to delete promo code', + failedToLoadUsages: 'Failed to load usage records' + }, + + // Usage Records + usage: { + title: 'Usage Records', + description: 'View and manage all user usage records', + userFilter: 'User', + searchUserPlaceholder: 'Search user by email...', + searchApiKeyPlaceholder: 'Search API key by name...', + searchAccountPlaceholder: 'Search account by name...', + selectedUser: 'Selected', + user: 'User', + account: 'Account', + group: 'Group', + requestId: 'Request ID', + requestIdCopied: 'Request ID copied', + allModels: 'All Models', + allAccounts: 'All Accounts', + allGroups: 'All Groups', + allTypes: 'All Types', + inputCost: 'Input Cost', + outputCost: 'Output Cost', + cacheCreationCost: 'Cache Creation Cost', + cacheReadCost: 'Cache Read Cost', + inputTokens: 'Input Tokens', + outputTokens: 'Output Tokens', + cacheCreationTokens: 'Cache Creation Tokens', + cacheCreation5mTokens: 'Cache Write', + cacheCreation1hTokens: 'Cache Write', + cacheReadTokens: 'Cache Read Tokens', + failedToLoad: 'Failed to load usage records', + billingType: 'Billing Type', + allBillingTypes: 'All Billing Types', + billingTypeBalance: 'Balance', + billingTypeSubscription: 'Subscription', + billingMode: 'Billing Mode', + billingModeToken: 'Token', + billingModePerRequest: 'Per Request', + billingModeImage: 'Image', + allBillingModes: 'All Billing Modes', + ipAddress: 'IP', + clickToViewBalance: 'Click to view balance history', + failedToLoadUser: 'Failed to load user info', + userDeletedBadge: 'Deleted', + cleanup: { + button: 'Cleanup', + title: 'Cleanup Usage Records', + warning: 'Cleanup is irreversible and will affect historical stats.', + submit: 'Submit Cleanup', + submitting: 'Submitting...', + confirmTitle: 'Confirm Cleanup', + confirmMessage: 'Are you sure you want to submit this cleanup task? This action cannot be undone.', + confirmSubmit: 'Confirm Cleanup', + cancel: 'Cancel', + cancelConfirmTitle: 'Confirm Cancel', + cancelConfirmMessage: 'Are you sure you want to cancel this cleanup task?', + cancelConfirm: 'Confirm Cancel', + cancelSuccess: 'Cleanup task canceled', + cancelFailed: 'Failed to cancel cleanup task', + recentTasks: 'Recent Cleanup Tasks', + loadingTasks: 'Loading tasks...', + noTasks: 'No cleanup tasks yet', + range: 'Range', + deletedRows: 'Deleted', + missingRange: 'Please select a date range', + submitSuccess: 'Cleanup task created', + submitFailed: 'Failed to create cleanup task', + loadFailed: 'Failed to load cleanup tasks', + status: { + pending: 'Pending', + running: 'Running', + succeeded: 'Succeeded', + failed: 'Failed', + canceled: 'Canceled' + } + } + }, + + // Ops Monitoring +} diff --git a/frontend/src/i18n/locales/en/admin/settings.ts b/frontend/src/i18n/locales/en/admin/settings.ts new file mode 100644 index 0000000000..d3d3f41ac9 --- /dev/null +++ b/frontend/src/i18n/locales/en/admin/settings.ts @@ -0,0 +1,1269 @@ +export default { + settings: { + title: 'System Settings', + description: 'Manage registration, email verification, default values, and SMTP settings', + tabs: { + general: 'General', + agreement: 'Agreement', + features: 'Feature Switches', + security: 'Security', + users: 'Users', + gateway: 'Gateway', + email: 'Email', + backup: 'Backup', + payment: 'Payment', + }, + features: { + channelMonitor: { + title: 'Channel Monitor', + description: 'Periodically probe configured channels and surface availability / latency to users. Turning it off stops the scheduler and returns an empty list on the user page.', + configureLink: 'Configure monitors in Channel Management > Channel Monitor', + enabled: 'Enable Channel Monitor', + enabledHint: 'Disabling stops background checks; existing history is preserved.', + defaultInterval: 'Default check interval (seconds)', + defaultIntervalHint: 'Pre-fills the interval when creating a new monitor; each monitor can override it. Range 15 – 3600.', + }, + availableChannels: { + title: 'Available Channels', + description: 'Show logged-in users an aggregate view of the channels, models and pricing they can access. Disabled by default.', + configureLink: 'Configure model pricing in Channel Management > Channel Pricing', + enabled: 'Enable Available Channels', + enabledHint: 'When off, the sidebar entry is hidden and the endpoint returns an empty list.', + }, + riskControl: { + title: 'Risk Control', + description: 'Enable the content moderation menu and gateway audit entry point. Disabled by default.', + configureLink: 'Configure content moderation in Risk Control', + enabled: 'Enable Risk Control', + enabledHint: 'When off, the admin sidebar entry is hidden and gateway moderation is skipped.', + cyberSessionBlock: 'Cyber session auto-block', + cyberSessionBlockHint: 'When enabled, sessions hit by upstream cyber_policy are blocked locally for the TTL and no longer forwarded. Only the offending session is blocked; other sessions on the same key are unaffected.', + cyberSessionBlockTTL: 'Block TTL (seconds)', + }, + affiliate: { + title: 'Affiliate (Invite Rebate)', + description: 'Existing users invite new ones; the inviter earns a percentage rebate on the invitee’s recharges. Disabled by default.', + enabled: 'Enable Affiliate', + enabledHint: 'When off, the affiliate menu is hidden, the aff parameter is ignored at signup, and new recharges generate no rebate. Existing rebate balances can still be transferred.', + rebateRate: 'Global Rebate Rate', + rebateRateHint: 'Default percentage given back to the inviter on recharges (0-100, e.g. 10 = 10%).', + freezeHours: 'Rebate Freeze Period (hours)', + freezeHoursDesc: 'New rebates will be frozen for this period before becoming available for withdrawal. 0 = no freeze.', + durationDays: 'Rebate Duration (days)', + durationDaysDesc: 'Rebate relationship expires after this many days since invitee registration. 0 = permanent.', + perInviteeCap: 'Per-Invitee Rebate Cap', + perInviteeCapDesc: 'Maximum total rebate from a single invitee. 0 = no limit.', + customUsers: { + title: 'Per-User Overrides', + description: 'Set a custom invite code or exclusive rebate rate for specific users. Lists only users that have an override applied.', + addButton: 'Add Custom User', + searchPlaceholder: 'Search by email or username', + batchButton: 'Batch Set Rate ({count} selected)', + empty: 'No users with custom affiliate settings yet', + customBadge: 'custom', + useGlobal: 'use global', + resetTitle: 'Reset Custom Settings', + resetMessage: 'Reset all custom settings for {email}?\n• The exclusive rebate rate will be cleared (fall back to the global rate)\n• The invite code will be regenerated as a new system code (previously shared links will stop working)', + totalLabel: '{total} total', + col: { + email: 'Email', + username: 'Username', + code: 'Invite Code', + rate: 'Custom Rate', + actions: 'Actions', + }, + }, + modal: { + addTitle: 'Add Custom User', + editTitle: 'Edit Custom Settings', + userLabel: 'User', + userPlaceholder: 'Search by email or username', + changeUser: 'Change user', + codeLabel: 'Custom Invite Code (optional)', + codePlaceholder: 'e.g. VIP2026', + codeHint: '4-32 characters; A-Z, 0-9, underscore, dash. Leave empty to keep current. Input is upper-cased.', + rateLabel: 'Exclusive Rebate Rate (optional)', + ratePlaceholder: 'e.g. 30', + rateHint: '0-100. Leave empty (in edit mode) to clear and fall back to the global rate.', + errorBadRate: 'Please enter a number between 0 and 100', + errorEmpty: 'Fill at least one: custom invite code or exclusive rebate rate', + }, + batchModal: { + title: 'Batch Set Rate ({count} users selected)', + hint: 'Apply the same exclusive rebate rate to all selected users.', + placeholder: 'e.g. 30', + clearHint: 'Submitting empty will clear the exclusive rate for selected users.', + }, + }, + }, + emailTabDisabledTitle: 'Email Verification Not Enabled', + emailTabDisabledHint: 'Enable email verification in the Security tab to configure SMTP settings.', + registration: { + title: 'Registration Settings', + description: 'Control user registration and verification', + enableRegistration: 'Enable Registration', + enableRegistrationHint: 'Allow new users to register', + emailVerification: 'Email Verification', + emailVerificationHint: 'Require email verification for new registrations', + emailSuffixWhitelist: 'Email Domain Whitelist', + emailSuffixWhitelistHint: + "Only email addresses from the specified domains can register (for example, {'@'}qq.com, {'@'}gmail.com, *.edu.cn)", + emailSuffixWhitelistPlaceholder: "{'@'}example.com, *.edu.cn", + emailSuffixWhitelistInputHint: 'Leave empty for no restriction. Use *.edu.cn to match edu.cn and its subdomains.', + promoCode: 'Promo Code', + promoCodeHint: 'Allow users to use promo codes during registration', + invitationCode: 'Invitation Code Registration', + invitationCodeHint: 'When enabled, users must enter a valid invitation code to register', + passwordReset: 'Password Reset', + passwordResetHint: 'Allow users to reset their password via email', + frontendUrl: 'Frontend URL', + frontendUrlPlaceholder: 'https://example.com', + frontendUrlHint: 'Used to generate password reset links in emails. Example: https://example.com', + totp: 'Two-Factor Authentication (2FA)', + totpHint: 'Allow users to use authenticator apps like Google Authenticator', + totpKeyNotConfigured: + 'Please configure TOTP_ENCRYPTION_KEY in environment variables first. Generate a key with: openssl rand -hex 32' + }, + turnstile: { + title: 'Cloudflare Turnstile', + description: 'Bot protection for login and registration', + enableTurnstile: 'Enable Turnstile', + enableTurnstileHint: 'Require Cloudflare Turnstile verification', + siteKey: 'Site Key', + secretKey: 'Secret Key', + siteKeyHint: 'Get this from your Cloudflare Dashboard', + cloudflareDashboard: 'Cloudflare Dashboard', + secretKeyHint: 'Server-side verification key (keep this secret)', + secretKeyConfiguredHint: 'Secret key configured. Leave empty to keep the current value.' + }, + apiKeyAcl: { + title: 'API Key IP Access Control', + description: 'Choose which client IP is used by API Key allowlists and denylists', + trustForwardedIp: 'Trust forwarded client IP', + trustForwardedIpHint: + 'Disabled by default. Enable only when the origin is reachable only through Cloudflare or Nginx reverse proxy. When enabled, API Key IP allowlists and denylists use CF-Connecting-IP, X-Real-IP, or X-Forwarded-For, matching the request IP shown in usage records.' + }, + linuxdo: { + title: 'LinuxDo Connect Login', + description: 'Configure LinuxDo Connect OAuth for Sub2API end-user login', + enable: 'Enable LinuxDo Login', + enableHint: 'Show LinuxDo login on the login/register pages', + clientId: 'Client ID', + clientIdPlaceholder: 'e.g., hprJ5pC3...', + clientIdHint: 'Get this from Connect.Linux.Do', + clientSecret: 'Client Secret', + clientSecretPlaceholder: '********', + clientSecretHint: 'Used by backend to exchange tokens (keep it secret)', + clientSecretConfiguredPlaceholder: '********', + clientSecretConfiguredHint: 'Secret configured. Leave empty to keep the current value.', + redirectUrl: 'Redirect URL', + redirectUrlPlaceholder: 'https://your-domain.com/api/v1/auth/oauth/linuxdo/callback', + redirectUrlHint: + 'Must match the redirect URL configured in Connect.Linux.Do (must be an absolute http(s) URL)', + quickSetCopy: 'Generate & Copy (current site)', + redirectUrlSetAndCopied: 'Redirect URL generated and copied to clipboard' + }, + dingtalk: { + title: 'DingTalk Login', + description: 'Configure DingTalk OAuth for Sub2API end-user login', + enable: 'Enable DingTalk Login (Internal Corporate App)', + enableHint: 'Show DingTalk login on the login/register pages', + clientId: 'Client ID (AppKey)', + clientIdPlaceholder: 'e.g., dingxxxxxxxxxxxxxxxx', + clientIdHint: 'Get this from the DingTalk Open Platform app details', + clientSecret: 'Client Secret (AppSecret)', + clientSecretPlaceholder: '********', + clientSecretHint: 'Used by backend to exchange tokens (keep it secret)', + clientSecretConfiguredPlaceholder: '********', + clientSecretConfiguredHint: 'Secret configured. Leave empty to keep the current value.', + redirectUrl: 'Redirect URL', + redirectUrlPlaceholder: 'https://your-domain.com/api/v1/auth/oauth/dingtalk/callback', + redirectUrlHint: + 'Must match the redirect URL configured in DingTalk Open Platform (must be an absolute http(s) URL)', + corpPolicy: { + label: 'Corp Restriction Policy', + hint: 'Control which DingTalk accounts (orgs) are allowed to sign in', + none: 'No restriction (all DingTalk accounts allowed)', + internalOnly: 'Internal only (single corp)' + }, + bypassRegistration: 'Enable DingTalk signup', + bypassRegistrationHint: 'Allow new users to register via DingTalk even when public registration is disabled.', + syncDisplayName: 'Sync DingTalk display name', + syncDisplayNameHint: 'Overwrite username with the DingTalk staff name on each login (also stored in the dingtalk_name attribute).', + syncCorpEmail: 'Sync corporate email', + syncCorpEmailHint: 'Write the DingTalk corporate email to the dingtalk_email attribute on each login (does not change the login email).', + syncCorpEmailPermissionHint: 'Requires the OAPI permission "Personal info incl. email (fieldEmail)" to be granted to the app on the DingTalk open platform, otherwise OAPI will not return the email field.', + syncDept: 'Sync department', + syncDeptHint: 'Write the full DingTalk department path to the dingtalk_department attribute on each login (fetched live each time).', + syncDeptPermissionHint: 'Requires the OAPI "Department info read (qyapi_get_department_list)" permission to be granted to the app on the DingTalk open platform, otherwise the department path cannot be resolved.', + syncDisplayNameTarget: 'Attribute key', + syncDisplayNameTargetHint: 'Defaults to dingtalk_name / DingTalk Name. Saving settings auto-creates the user attribute by the key and display name above (existing definition only has its display name synced).', + syncCorpEmailTarget: 'Attribute key', + syncCorpEmailTargetHint: 'Defaults to dingtalk_email / DingTalk Corporate Email. Saving settings auto-creates the user attribute by the key and display name above (existing definition only has its display name synced).', + syncDeptTarget: 'Attribute key', + syncDeptTargetHint: 'Defaults to dingtalk_department / DingTalk Department. Saving settings auto-creates the user attribute by the key and display name above (existing definition only has its display name synced).', + syncAttrDisplayName: 'Display name' + }, + oidc: { + title: 'OIDC Login', + description: 'Configure a standard OIDC provider (for example Keycloak)', + enable: 'Enable OIDC Login', + enableHint: 'Show OIDC login on the login/register pages', + providerName: 'Provider Name', + providerNamePlaceholder: 'for example Keycloak', + clientId: 'Client ID', + clientIdPlaceholder: 'OIDC client id', + clientSecret: 'Client Secret', + clientSecretPlaceholder: '********', + clientSecretHint: 'Used by backend to exchange tokens (keep it secret)', + clientSecretConfiguredPlaceholder: '********', + clientSecretConfiguredHint: 'Secret configured. Leave empty to keep the current value.', + issuerUrl: 'Issuer URL', + issuerUrlPlaceholder: 'https://id.example.com/realms/main', + discoveryUrl: 'Discovery URL', + discoveryUrlPlaceholder: 'Optional, leave empty to auto-derive from issuer', + authorizeUrl: 'Authorize URL', + authorizeUrlPlaceholder: 'Optional, can be discovered automatically', + tokenUrl: 'Token URL', + tokenUrlPlaceholder: 'Optional, can be discovered automatically', + userinfoUrl: 'UserInfo URL', + userinfoUrlPlaceholder: 'Optional, can be discovered automatically', + jwksUrl: 'JWKS URL', + jwksUrlPlaceholder: 'Optional, required when strict ID token validation is enabled', + scopes: 'Scopes', + scopesPlaceholder: 'openid email profile', + scopesHint: 'Must include openid', + redirectUrl: 'Backend Redirect URL', + redirectUrlPlaceholder: 'https://your-domain.com/api/v1/auth/oauth/oidc/callback', + redirectUrlHint: 'Must match the callback URL configured in the OIDC provider', + quickSetCopy: 'Generate & Copy (current site)', + redirectUrlSetAndCopied: 'Redirect URL generated and copied to clipboard', + frontendRedirectUrl: 'Frontend Callback Path', + frontendRedirectUrlPlaceholder: '/auth/oidc/callback', + frontendRedirectUrlHint: 'Frontend route used after backend callback', + tokenAuthMethod: 'Token Auth Method', + clockSkewSeconds: 'Clock Skew (seconds)', + allowedSigningAlgs: 'Allowed Signing Algs', + allowedSigningAlgsPlaceholder: 'RS256,ES256,PS256', + usePkce: 'Use PKCE', + validateIdToken: 'Validate ID Token', + requireEmailVerified: 'Require Email Verified', + userinfoEmailPath: 'UserInfo Email Path', + userinfoEmailPathPlaceholder: 'for example data.email', + userinfoIdPath: 'UserInfo ID Path', + userinfoIdPathPlaceholder: 'for example data.id', + userinfoUsernamePath: 'UserInfo Username Path', + userinfoUsernamePathPlaceholder: 'for example data.username' + }, + defaults: { + title: 'Default User Settings', + description: 'Default values for new users', + defaultBalance: 'Default Balance', + defaultBalanceHint: 'Initial balance for new users', + affiliateRebateRate: 'Affiliate Rebate Rate', + affiliateRebateRateHint: + 'Rebate percentage credited to inviter after recharge (0-100%, e.g. 10 means 10%)', + defaultConcurrency: 'Default Concurrency', + defaultConcurrencyHint: 'Maximum concurrent requests for new users', + defaultUserRpmLimit: 'Default User RPM Limit', + defaultUserRpmLimitHint: 'Default max requests per minute for new users; 0 = unlimited. Only applied at new user creation.', + defaultSubscriptions: 'Default Subscriptions', + defaultSubscriptionsHint: 'Auto-assign these subscriptions when a new user is created or registered', + addDefaultSubscription: 'Add Default Subscription', + defaultSubscriptionsEmpty: 'No default subscriptions configured.', + defaultSubscriptionsDuplicate: + 'Duplicate subscription group: {groupId}. Each group can only appear once.', + subscriptionGroup: 'Subscription Group', + subscriptionValidityDays: 'Validity (days)', + defaultPlatformQuotas: 'Default Platform Quotas (on signup)', + defaultPlatformQuotasHint: 'Automatically assigned to new users on signup; existing users are not affected. Leave blank = unlimited.', + platformQuotaNotice: 'Monthly quota uses a 30-day rolling window, not a calendar month.', + }, + platformQuota: { + platform: 'Platform', + daily: 'Daily (USD)', + weekly: 'Weekly (USD)', + monthly: 'Monthly (USD, 30d rolling)', + placeholder: 'Unlimited', + }, + claudeCode: { + title: 'Claude Code Settings', + description: 'Control Claude Code client access requirements', + minVersion: 'Minimum Version', + minVersionPlaceholder: 'e.g. 2.1.63', + minVersionHint: + 'Reject Claude Code clients below this version (semver format). Leave empty to disable version check.', + maxVersion: 'Maximum Version', + maxVersionPlaceholder: 'e.g. 2.5.0', + maxVersionHint: + 'Reject Claude Code clients above this version (semver format). Leave empty to allow any version.' + }, + scheduling: { + title: 'Gateway Scheduling Settings', + description: 'Control API Key scheduling behavior', + allowUngroupedKey: 'Allow Ungrouped Key Scheduling', + allowUngroupedKeyHint: 'When disabled, API Keys not assigned to any group cannot make requests (403 Forbidden). Keep disabled to ensure all Keys belong to a specific group.' + }, + gatewayForwarding: { + title: 'Request Forwarding', + description: 'Control how requests are forwarded to upstream OAuth accounts', + fingerprintUnification: 'Fingerprint Unification', + fingerprintUnificationHint: 'Unify X-Stainless-* headers across users sharing the same OAuth account. Disabling passes through each client\'s original headers.', + metadataPassthrough: 'Metadata Passthrough', + metadataPassthroughHint: 'Pass through client\'s original metadata.user_id without rewriting. May improve upstream cache hit rates.', + cchSigning: 'CCH Signing', + cchSigningHint: 'Sign the billing header in forwarded requests with CCH hash. When disabled, the placeholder is preserved.', + claudeOAuthSystemPromptInjection: 'Claude OAuth System Blocks', + claudeOAuthSystemPromptInjectionHint: 'Inject Claude Code-like system blocks for Claude OAuth requests from non-Claude-Code clients. Enabled by default.', + claudeOAuthSystemPrompt: 'Claude OAuth Expansion Prompt', + claudeOAuthSystemPromptPlaceholder: 'Leave empty to use the built-in Claude Code expansion prompt.', + claudeOAuthSystemPromptHint: 'Legacy compatibility: controls only the third injected system block.', + claudeOAuthSystemPromptBlocks: 'Claude OAuth System Blocks', + claudeOAuthSystemPromptBlocksPlaceholder: 'Leave empty to use the built-in 3 blocks. Supports an array or {"blocks": [...]}.', + claudeOAuthSystemPromptBlocksHint: 'Each block is saved as JSON with enabled, type, text, and optional cache_control. {billing_header} stays dynamic per request; the Claude Code identity and expansion prompts can be edited directly or restored from presets.', + systemBlockTitle: 'System Block {index}', + systemBlockPreset: 'Preset', + systemBlockPresetBilling: 'Billing header', + systemBlockPresetIdentity: 'Claude Code identity', + systemBlockPresetExpansion: 'Claude Code expansion', + systemBlockPresetCustom: 'Custom', + systemBlockType: 'Type', + systemBlockTypeText: 'Text', + systemBlockText: 'Content', + systemBlockCacheControl: 'Cache control', + systemBlockHide: 'Hide block details', + systemBlockShow: 'Show block details', + addSystemBlock: 'Add block', + resetSystemBlocks: 'Reset defaults', + cacheTTL5m: '5 minutes', + cacheTTL1h: '1 hour', + anthropicCacheTTL1hInjection: 'Anthropic Cache TTL Injection', + anthropicCacheTTL1hInjectionHint: 'When enabled, existing ephemeral cache_control blocks in Anthropic OAuth/Setup Token request bodies are forced to 1h; response usage is billed back as 5m by default, with account-level TTL billing override taking priority.', + rewriteMessageCacheControl: 'Rewrite Message Cache Breakpoints', + rewriteMessageCacheControlHint: 'Default off: preserve client cache_control on message content blocks. When enabled, client breakpoints are stripped and proxy breakpoints are injected for clients that do not manage caching themselves.', + clientDatelineNormalization: 'Client Dateline Normalization', + clientDatelineNormalizationHint: 'Default on. Rewrites the "Today\'s date is …" sentence in Anthropic OAuth/Setup Token requests back to a canonical ASCII apostrophe and hyphen date format, erasing steganographic fingerprint bits some clients inject when they detect a non-official base URL. Applies to system prompts and blocks only; API-Key accounts are unaffected.', + antigravityUserAgentVersion: 'Antigravity UA Version', + antigravityUserAgentVersionPlaceholder: '1.23.2', + antigravityUserAgentVersionHint: 'Leave empty to use ANTIGRAVITY_USER_AGENT_VERSION or the built-in default 1.23.2; when set, the admin setting takes precedence.', + openaiCodexUserAgent: 'OpenAI Codex UA', + openaiCodexUserAgentPlaceholder: 'codex-tui/0.125.0 (Ubuntu 22.4.0; x86_64) xterm-256color (codex-tui; 0.125.0)', + openaiCodexUserAgentHint: 'Used to bypass Cloudflare browser-UA challenges on the OpenAI upstream. Only applies when the client User-Agent is detected as a browser (Mozilla/...). Leave empty to use the built-in default.', + codexHardeningTitle: "Codex Settings", + codexClientRestrictionTitle: "Codex client restriction", + codexHardeningDesc: + "Only affects OpenAI OAuth accounts with 'Codex official clients only' enabled (global). Beyond User-Agent/Originator, harden the decision with a version range, an engine-fingerprint gate, and black/whitelists.", + minCodexVersion: "Min Codex Version", + minCodexVersionPlaceholder: "e.g. 0.142.0", + maxCodexVersion: "Max Codex Version", + maxCodexVersionPlaceholder: "e.g. 0.200.0", + codexVersionHint: + "Official clients only: checks their version against the [min, max] range. Leave a side empty to not limit it.", + codexFingerprintSignals: "Codex engine fingerprint signals", + codexFingerprintSignalsDesc: + "Define engine-fingerprint signals: every Required signal must match (AND); within a row, '/'-separated variants are OR'd. None checked = not enforced. Default checks only the x-codex- prefix. Types: header exact / header prefix / body path.", + codexFpTypeHeaderExact: "Header exact", + codexFpTypeHeaderPrefix: "Header prefix", + codexFpTypeBodyPath: "Body path", + codexFpMatchPlaceholder: "match; '/'-separate variants (e.g. session-id / session_id or x-codex-)", + codexFpRequired: "Required", + codexFingerprintNoRequiredWarn: "No signal is marked Required — the engine-fingerprint gate is inactive, allowing every candidate that passes identity/version. Check at least one signal to enable it.", + codexAllowAppServer: "Codex app-server", + codexAllowAppServerDesc: + "Allow third-party clients that embed the Codex engine and connect over the app-server protocol (e.g. Claude Code's codex plugin). Off by default; when on, such clients are allowed once they pass the engine-fingerprint gate (the signal list below); off = only official clients and the whitelist are allowed.", + codexBlacklist: "User-Agent/Originator Blacklist", + codexBlacklistDesc: + "Deny if any field matches; takes precedence over any allow. originator is exact; User-Agent is a 'contains' match (comma-separated).", + codexWhitelist: "User-Agent/Originator Whitelist", + codexWhitelistDesc: + "Allow clients outside the official set: requires exact originator and every User-Agent marker present. Still subject to the fingerprint gate unless 'Skip engine fingerprint' is checked.", + codexWhitelistSkipFingerprint: "Skip engine fingerprint", + codexWhitelistSkipFingerprintTooltip: + "Risk: when checked this entry is allowed on originator + User-Agent alone (both forgeable), with no engine-fingerprint backstop. Use only for trusted third-party clients that genuinely do not send a codex engine fingerprint.", + codexOriginatorPlaceholder: "originator (exact, e.g. opencode)", + codexUaContainsPlaceholder: "User-Agent contains markers, comma-separated (e.g. opencode/)", + codexAddRow: "Add entry", + codexRemoveRow: "Remove", + }, + webSearchEmulation: { + title: 'Web Search Emulation', + description: 'Inject web search capability for Anthropic API Key accounts that don\'t natively support it', + enabled: 'Enable Web Search Emulation', + enabledHint: 'Global switch. When disabled, web search emulation is inactive for all channels and accounts.', + providers: 'Search Providers', + addProvider: 'Add Provider', + providerType: 'Provider Type', + apiKey: 'API Key', + apiKeyPlaceholder: 'Enter API Key', + apiKeyConfigured: 'Configured', + showApiKey: 'Show', + hideApiKey: 'Hide', + copyApiKey: 'Copy', + copied: 'Copied', + quotaLimit: 'Quota Limit', + quotaLimitHint: 'Leave empty for unlimited; must be > 0 if set', + quotaLimitMustBePositive: 'Quota limit must be greater than 0', + subscribedAt: 'Subscribed At', + subscribedAtHint: 'Quota resets monthly from this date; leave empty to disable auto-reset', + quotaUsage: 'Usage', + resetUsage: 'Reset', + resetUsageConfirm: 'Reset usage counter for this provider?', + resetUsageSuccess: 'Usage counter reset', + proxy: 'Proxy', + removeProvider: 'Remove', + noProviders: 'No search providers configured', + test: 'Test', + testDefaultQuery: 'Major world events this year', + testing: 'Searching...', + testResultTitle: 'Search Results', + testResultProvider: 'Provider', + testNoResults: 'No results found', + }, + site: { + title: 'Site Settings', + description: 'Customize site branding', + backendMode: 'Backend Mode', + backendModeDescription: + 'Disables user registration, public site, and self-service features. Only admin can log in and manage the platform.', + siteName: 'Site Name', + siteNamePlaceholder: 'Sub2API', + siteNameHint: 'Displayed in emails and page titles', + siteSubtitle: 'Site Subtitle', + siteSubtitlePlaceholder: 'Subscription to API Conversion Platform', + siteSubtitleHint: 'Displayed on login and register pages', + apiBaseUrl: 'API Base URL', + apiBaseUrlPlaceholder: 'https://api.example.com', + apiBaseUrlHint: + 'Used for "Use Key", "Import to CC Switch", and callback URL suggestions. Leave empty to use current site URL.', + tablePreferencesTitle: 'Global Table Preferences', + tablePreferencesDescription: 'Configure default pagination behavior for shared table components', + tableDefaultPageSize: 'Default Rows Per Page', + tableDefaultPageSizeHint: 'Must be an integer between 5 and 1000', + tablePageSizeOptions: 'Rows Per Page Options', + tablePageSizeOptionsPlaceholder: '10, 20, 50, 100', + tablePageSizeOptionsHint: 'Use commas to separate integers between 5 and 1000; values are deduplicated and sorted on save', + tableDefaultPageSizeRangeError: 'Default rows per page must be between {min} and {max}', + tablePageSizeOptionsFormatError: 'Invalid options format. Enter comma-separated integers between {min} and {max}', + customEndpoints: { + title: 'Custom Endpoints', + description: 'Add additional API endpoint URLs for users to quickly copy on the API Keys page', + itemLabel: 'Endpoint #{n}', + name: 'Name', + namePlaceholder: 'e.g., OpenAI Compatible', + endpointUrl: 'Endpoint URL', + endpointUrlPlaceholder: 'https://api2.example.com', + descriptionLabel: 'Description', + descriptionPlaceholder: 'e.g., Supports OpenAI format requests', + add: 'Add Endpoint', + }, + contactInfo: 'Contact Info', + contactInfoPlaceholder: 'e.g., QQ: 123456789', + contactInfoHint: 'Customer support contact info, displayed on redeem page, profile, etc.', + docUrl: 'Documentation URL', + docUrlPlaceholder: 'https://docs.example.com', + docUrlHint: 'Link to your documentation site. Leave empty to hide the documentation link.', + siteLogo: 'Site Logo', + uploadImage: 'Upload Image', + remove: 'Remove', + logoHint: 'PNG, JPG, or SVG. Max 300KB. Recommended: 80x80px square image.', + logoSizeError: 'Image size exceeds 300KB limit ({size}KB)', + logoTypeError: 'Please select an image file', + logoReadError: 'Failed to read the image file', + homeContent: 'Home Page Content', + homeContentPlaceholder: 'Enter custom content for the home page. Supports Markdown & HTML. If a URL is entered, it will be displayed as an iframe.', + homeContentHint: 'Customize the home page content. Supports Markdown/HTML. If you enter a URL (starting with http:// or https://), it will be used as an iframe src to embed an external page. When set, the default status information will no longer be displayed.', + homeContentIframeWarning: '⚠️ iframe mode note: Some websites have X-Frame-Options or CSP security policies that prevent embedding in iframes. If the page appears blank or shows an error, please verify the target website allows embedding, or consider using HTML mode to build your own content.', + hideCcsImportButton: 'Hide CCS Import Button', + hideCcsImportButtonHint: 'When enabled, the "Import to CCS" button will be hidden on the API Keys page' + }, + purchase: { + title: 'Recharge / Subscription Page', + description: 'Show a "Recharge / Subscription" entry in the sidebar and open the configured URL in an iframe', + enabled: 'Show Recharge / Subscription Entry', + enabledHint: 'Only shown in standard mode (not simple mode)', + url: 'Recharge / Subscription URL', + urlPlaceholder: 'https://example.com/purchase', + urlHint: 'Must be an absolute http(s) URL', + iframeWarning: + '⚠️ iframe note: Some websites block embedding via X-Frame-Options or CSP (frame-ancestors). If the page is blank, provide an "Open in new tab" alternative.', + integrationDoc: 'Payment Integration Docs', + integrationDocHint: 'Covers endpoint specs, idempotency semantics, and code samples' + }, + soraClient: { + title: 'Sora Client', + description: 'Control whether to show the Sora client entry in the sidebar', + enabled: 'Enable Sora Client', + enabledHint: 'When enabled, the Sora entry will be shown in the sidebar for users to access Sora features' + }, + customMenu: { + title: 'Custom Menu Pages', + description: 'Add custom iframe pages to the sidebar navigation. Each page can be visible to regular users or administrators.', + itemLabel: 'Menu Item #{n}', + name: 'Menu Name', + namePlaceholder: 'e.g. Help Center', + url: 'Page URL', + urlPlaceholder: 'https://example.com/page', + iconSvg: 'SVG Icon', + iconSvgPlaceholder: '...', + iconPreview: 'Icon Preview', + uploadSvg: 'Upload SVG', + removeSvg: 'Remove', + visibility: 'Visible To', + visibilityUser: 'Regular Users', + visibilityAdmin: 'Administrators', + add: 'Add Menu Item', + remove: 'Remove', + moveUp: 'Move Up', + moveDown: 'Move Down', + }, + payment: { + title: 'Payment Settings', + description: 'Configure payment system options', + configGuide: 'Configuration Guide', + enabled: 'Enable Payment', + enabledHint: 'Enable or disable the payment system', + enabledPaymentTypes: 'Enabled Providers', + enabledPaymentTypesHint: 'Disabling a provider will also disable its instances.', + findProvider: 'Looking for a suitable EasyPay provider?', + minAmount: 'Minimum Amount', + maxAmount: 'Maximum Amount', + dailyLimit: 'Daily Limit', + balanceRechargeMultiplier: 'Balance Recharge Multiplier', + balanceRechargeMultiplierHint: 'How many USD balance the user receives for each 1 CNY paid', + balanceRechargePreview: 'Preview: 1 CNY = {usd} USD', + subscriptionUsdToCnyRate: 'Subscription USD to CNY Rate', + subscriptionUsdToCnyRateHint: + 'CNY charged per 1 USD of plan price on CNY channels (e.g. 7.15). 0 or empty = disabled, plan price is charged as-is. When enabled, all plan prices must be set in USD', + subscriptionUsdToCnyRateDisabled: 'Disabled (price charged as-is)', + rechargeFeeRate: 'Recharge Fee Rate', + rechargeFeeRateHint: 'Percentage of service fee charged on top of recharge amount, 0 means no fee', + rechargeFeePreview: 'Preview: Recharge 100, fee {fee}', + orderTimeout: 'Order Timeout', + orderTimeoutHint: 'In minutes, minimum 1', + maxPendingOrders: 'Max Pending Orders', + cancelRateLimit: 'Limit Cancel Rate', + cancelRateLimitHint: 'When enabled, users who exceed the cancel limit within the time window cannot create new orders', + cancelRateLimitEvery: 'Every', + cancelRateLimitAllowMax: 'allow max', + cancelRateLimitTimes: 'cancels', + cancelRateLimitWindow: 'Window', + cancelRateLimitUnit: 'Unit', + cancelRateLimitMax: 'Max Cancels', + cancelRateLimitUnitMinute: 'Minutes', + cancelRateLimitUnitHour: 'Hours', + cancelRateLimitUnitDay: 'Days', + cancelRateLimitWindowMode: 'Window Mode', + cancelRateLimitWindowModeRolling: 'Rolling', + cancelRateLimitWindowModeFixed: 'Fixed', + alipayForceQRCode: 'Force Alipay QR Code', + alipayForceQRCodeHint: 'When enabled, mobile Alipay users always see a QR code instead of being redirected to the mobile payment page', + helpText: 'Help Text', + helpImageUrl: 'Help Image URL', + manageProviders: 'Manage Providers', + balancePaymentDisabled: 'Disable Balance Recharge', + noLimit: 'Empty = no limit', + helpImage: 'Help Image', + helpImagePlaceholder: 'Upload or enter image URL', + helpTextPlaceholder: 'Enter help text...', + providerEasypay: 'EasyPay', + providerAlipay: 'Alipay (Direct)', + providerWxpay: 'WeChat Pay (Direct)', + providerStripe: 'Stripe', + providerAirwallex: 'Airwallex', + typeDisabled: 'type disabled', + enableTypesFirst: 'Enable at least one payment type above first', + easypayRedirect: 'Redirect', + paymentMode: 'Payment Mode', + modeRedirect: 'Redirect', + modeQRCode: 'QR Code', + modePopup: 'Popup', + validationNameRequired: 'Provider name is required', + validationTypesRequired: 'Please select at least one supported payment type', + validationFieldRequired: '{field} is required', + validationEasyPayCustomMethodRequired: 'Each custom EasyPay method requires both a payment type and an upstream type', + validationEasyPayCustomMethodTypeInvalid: 'Custom EasyPay payment types may only contain lowercase letters, digits, underscores, and hyphens', + validationEasyPayCustomMethodUpstreamTypeInvalid: 'EasyPay upstream types may only contain lowercase letters, digits, underscores, and hyphens', + validationEasyPayCustomMethodReserved: 'Custom EasyPay payment types cannot use built-in alipay or wxpay', + validationEasyPayCustomMethodPrefixReserved: 'Custom EasyPay payment types cannot start with alipay or wxpay', + validationEasyPayCustomMethodDuplicate: 'Custom EasyPay payment types must be unique', + field_apiBase: 'API Base URL', + field_notifyUrl: 'Notify URL', + field_returnUrl: 'Return URL', + callbackBaseUrl: 'Callback Base URL', + field_privateKey: 'Private Key', + field_publicKey: 'Public Key', + field_mpAppId: 'MP App ID', + field_mchId: 'Merchant ID', + field_apiV3Key: 'API v3 Key', + field_publicKeyId: 'Public Key ID', + field_certSerial: 'Certificate Serial', + field_h5AppName: 'H5 App Name', + field_h5AppUrl: 'H5 App URL', + wxpayConfigHint: 'WeChat Pay usually only needs App ID. Fill MP App ID, H5 App Name, and H5 App URL only when your Official Account or H5 flow specifically requires them.', + wxpayAdvancedOptions: 'WeChat Pay Advanced Options', + field_secretKey: 'Secret Key', + field_clientId: 'Client ID', + field_apiKey: 'API Key', + field_publishableKey: 'Publishable Key', + field_webhookSecret: 'Webhook Secret', + field_countryCode: 'Country/region code', + field_currency: 'Payment currency', + field_accountId: 'Airwallex Account ID', + field_airwallexApiBaseHint: 'Must match the API key environment: use https://api-demo.airwallex.com/api/v1 for sandbox/demo keys, and https://api.airwallex.com/api/v1 for production keys. Mixed environments return credentials_invalid / Access Denied.', + field_paymentCurrencyHint: 'Default is CNY. Stripe and Airwallex can choose HKD, USD, or another listed currency supported by the account; WeChat Pay, Alipay, and EasyPay remain CNY.', + field_accountIdHint: 'Leave this empty unless you use multiple accounts, an organization-level key, or connected-account payments. A single-account scoped API key uses the selected account by default.', + field_cid: 'Channel ID', + field_cidAlipay: 'Alipay Channel ID', + field_cidWxpay: 'WeChat Channel ID', + easypayCustomMethods: 'Custom EasyPay methods', + easypayCustomMethodsHint: 'Add provider-specific methods supported by this EasyPay endpoint. The payment type is stored on Sub2API orders; the upstream type is sent as EasyPay type.', + addCustomMethod: 'Add method', + customMethodType: 'Payment type', + customMethodUpstreamType: 'Upstream type', + customMethodDisplayName: 'Display name', + stripeWebhookHint: 'Configure the following URL as a Webhook endpoint in Stripe Dashboard:', + stripeWebhookApiVersionHint: 'Set this Webhook endpoint API version to match the integrated Stripe SDK. Recommended: {version}. A mismatch can cause webhook parsing errors.', + airwallexWebhookHint: 'Configure the following URL as a Webhook endpoint in Airwallex. Select at least Payment Intent -> Succeeded (payment_intent.succeeded), preferably also Payment Intent -> Cancelled (payment_intent.cancelled). Use the account default or latest stable API version.', + airwallexGuideSummary: 'When creating an Airwallex scoped API key, select Read and Write for Payment Acceptance under account-level permissions.', + airwallexGuideNote: 'Do not grant unrelated permissions such as Spend, Payouts, Transfers, Funds Splits, or POS Terminals unless you explicitly need them. For webhooks, select at least payment_intent.succeeded, preferably also payment_intent.cancelled, and use the account default or latest stable API version.', + limitsTitle: 'Limits', + limitSingleMin: 'Min per order', + limitSingleMax: 'Max per order', + limitDaily: 'Daily limit', + limitsHint: 'All empty = use global config; partially filled = empty means no limit', + limitsUseGlobal: 'Use global', + limitsNoLimit: 'No limit', + productNamePrefix: 'Product Name Prefix', + productNameSuffix: 'Product Name Suffix', + preview: 'Preview', + loadBalanceStrategy: 'Load Balance Strategy', + strategyRoundRobin: 'Round Robin', + strategyLeastAmount: 'Least Daily Amount', + providerManagement: 'Provider Management', + providerManagementDesc: 'Manage payment provider instances', + createProvider: 'Add Provider', + editProvider: 'Edit Provider', + deleteProvider: 'Delete Provider', + deleteProviderConfirm: 'Are you sure you want to delete this provider?', + providerName: 'Provider Name', + providerKey: 'Provider Type', + selectProviderKey: 'Select Provider Type', + providerConfig: 'Credentials', + paymentGuideTrigger: 'View payment guide', + guideOpenLabel: 'Enable: ', + guideCallLabel: 'Call: ', + guideFallbackLabel: 'Fallback: ', + alipayGuideSummary: 'Desktop prefers QR precreate and falls back to cashier; mobile prefers WAP checkout.', + alipayGuideFaceToFaceTitle: 'Face-to-face / QR Payment', + alipayGuideFaceToFaceOpen: 'Enable face-to-face or QR payment capability.', + alipayGuideFaceToFaceCall: 'Desktop orders call alipay.trade.precreate first and render the QR code directly.', + alipayGuideFaceToFaceFallback: 'If unavailable or failed, the flow falls back to website checkout automatically.', + alipayGuidePagePayTitle: 'Website Payment', + alipayGuidePagePayOpen: 'Enable website payment.', + alipayGuidePagePayCall: 'When face-to-face is unavailable on desktop, the flow calls alipay.trade.page.pay and still renders the returned link as a QR code.', + alipayGuidePagePayFallback: 'The cashier link stays available so users can reopen the checkout page manually.', + alipayGuideWapTitle: 'WAP Payment', + alipayGuideWapOpen: 'Enable mobile website payment.', + alipayGuideWapCall: 'Mobile orders call alipay.trade.wap.pay first and jump to Alipay checkout.', + alipayGuideWapFallback: 'If mobile payment is unavailable or fails, the frontend switches to QR payment and shows a notice.', + wxpayGuideSummary: 'Desktop prefers Native QR; mobile routes to JSAPI or H5 based on browser context.', + wxpayGuideNote: 'The current form defaults to one shared App ID, which fits the common single-subject web, mobile, and Official Account setup.', + wxpayGuideNativeTitle: 'Native / QR Payment', + wxpayGuideNativeOpen: 'Enable Native or QR payment capability.', + wxpayGuideNativeCall: 'Desktop orders use Native by default and the frontend renders the QR payload.', + wxpayGuideNativeFallback: 'Mobile flows also fall back here when JSAPI or H5 cannot be used.', + wxpayGuideJsapiTitle: 'JSAPI / Official Account', + wxpayGuideJsapiOpen: 'Enable Official Account payment and ensure the browser is inside WeChat with an available OpenID.', + wxpayGuideJsapiCall: 'Inside WeChat, the app calls JSAPI after authorization and launches WeChat Pay directly.', + wxpayGuideJsapiFallback: 'If configuration is missing, the bridge is unavailable, or launch fails, the flow falls back to QR payment.', + wxpayGuideH5Title: 'H5 Payment', + wxpayGuideH5Open: 'Enable H5 payment.', + wxpayGuideH5Call: 'On mobile browsers outside WeChat, the app calls H5 payment when a client IP is available.', + wxpayGuideH5Fallback: 'If H5 is unavailable or order creation fails, the flow falls back to QR payment.', + noProviders: 'No provider instances configured', + supportedTypes: 'Supported Payment Types', + supportedTypesHint: 'Comma-separated, e.g. alipay,wxpay', + refundEnabled: 'Allow Refund', + allowUserRefund: 'Allow User Refund', + enableConflict: '{method} already has an enabled provider instance: {provider}. Disable the existing instance before switching.', + }, + balanceNotify: { + title: 'Balance Low Notification', + description: 'Send email notification when user balance falls below threshold', + enabled: 'Enable Balance Low Notification', + threshold: 'Default Threshold', + thresholdHint: 'Used when user has not set a custom value', + thresholdPlaceholder: 'Enter amount', + rechargeUrl: 'Recharge Page URL', + rechargeUrlPlaceholder: 'https://example.com/payment', + rechargeUrlHint: 'A top-up button will appear in the email when set', + }, + quotaNotify: { + title: 'Account Quota Notification', + description: 'Notify admins when account quota usage reaches alert threshold', + enabled: 'Enable Account Quota Notification', + emails: 'Notification Emails', + emailsHint: 'Leave empty to disable notifications', + addEmail: 'Add Email', + emailPlaceholder: 'Enter email address', + }, + subscriptionExpiryNotify: { + title: 'Subscription Expiry Reminder', + description: 'Control whether users receive subscription expiry reminder emails.', + enabled: 'Enable Subscription Expiry Reminder', + enabledHint: 'When enabled, the system sends reminders 7, 3, and 1 day before expiry.' + }, + smtp: { + title: 'SMTP Settings', + description: 'Configure email sending for verification codes', + testConnection: 'Test Connection', + testing: 'Testing...', + host: 'SMTP Host', + hostPlaceholder: 'smtp.gmail.com', + port: 'SMTP Port', + portPlaceholder: '587', + username: 'SMTP Username', + usernamePlaceholder: "your-email{'@'}gmail.com", + password: 'SMTP Password', + passwordPlaceholder: '********', + passwordHint: 'Leave empty to keep existing password', + passwordConfiguredPlaceholder: '********', + passwordConfiguredHint: 'Password configured. Leave empty to keep the current value.', + fromEmail: 'From Email', + fromEmailPlaceholder: "noreply{'@'}example.com", + fromName: 'From Name', + fromNamePlaceholder: 'Sub2API', + useTls: 'Use TLS', + useTlsHint: 'Enable TLS encryption for SMTP connection' + }, + testEmail: { + title: 'Send Test Email', + description: 'Send a test email to verify your SMTP configuration', + recipientEmail: 'Recipient Email', + recipientEmailPlaceholder: "test{'@'}example.com", + sendTestEmail: 'Send Test Email', + sending: 'Sending...', + enterRecipientHint: 'Please enter a recipient email address' + }, + emailTemplates: { + title: 'Email Templates', + description: 'Customize notification email subjects and HTML content for each event and locale.', + event: 'Event', + locale: 'Locale', + localeEn: 'English', + localeZh: 'Chinese', + subject: 'Subject', + subjectPlaceholder: 'Enter the email subject', + html: 'HTML Template', + htmlPlaceholder: 'Edit the email HTML template', + placeholders: 'Available Placeholders', + placeholdersHelp: 'Click a placeholder to copy it. The backend replaces these values when sending emails.', + livePreview: 'Live Preview', + previewSecurityHint: 'Preview HTML is generated by the backend preview endpoint and displayed in a sandboxed iframe with scripts disabled.', + preview: 'Preview / Refresh', + previewing: 'Previewing...', + save: 'Save Template', + saving: 'Saving...', + restoreOfficial: 'Restore Official', + restoring: 'Restoring...', + restoreConfirm: 'Restore the official template for this event and locale? Your custom version will be replaced.', + restoreSuccess: 'Official template restored', + saveSuccess: 'Email template saved', + placeholderCopied: 'Placeholder copied', + validationRequired: 'Subject and HTML template are required', + empty: 'No email template events or locales are available yet.', + noPreview: 'Refresh the preview to see the rendered email subject.', + customized: 'Customized' + }, + opsMonitoring: { + title: 'Ops Monitoring', + description: 'Enable ops monitoring for troubleshooting and health visibility', + disabled: 'Ops monitoring is disabled', + enabled: 'Enable Ops Monitoring', + enabledHint: 'Enable the ops monitoring module (admin only)', + realtimeEnabled: 'Enable Realtime Monitoring', + realtimeEnabledHint: 'Enable realtime QPS/metrics push (WebSocket)', + queryMode: 'Default Query Mode', + queryModeHint: 'Default query mode for Ops Dashboard (auto/raw/preagg)', + queryModeAuto: 'Auto (recommended)', + queryModeRaw: 'Raw (most accurate, slower)', + queryModePreagg: 'Preagg (fastest, requires aggregation)', + metricsInterval: 'Metrics Collection Interval (seconds)', + metricsIntervalHint: 'How often to collect system/request metrics (60-3600 seconds)' + }, + adminApiKey: { + title: 'Admin API Key', + description: 'Global API key for external system integration with full admin access', + notConfigured: 'Admin API key not configured', + configured: 'Admin API key is active', + currentKey: 'Current Key', + regenerate: 'Regenerate', + regenerating: 'Regenerating...', + delete: 'Delete', + deleting: 'Deleting...', + create: 'Create Key', + creating: 'Creating...', + regenerateConfirm: 'Are you sure? The current key will be immediately invalidated.', + deleteConfirm: + 'Are you sure you want to delete the admin API key? External integrations will stop working.', + keyGenerated: 'New admin API key generated', + keyDeleted: 'Admin API key deleted', + copyKey: 'Copy Key', + keyCopied: 'Key copied to clipboard', + keyWarning: 'This key will only be shown once. Please copy it now.', + securityWarning: 'Warning: This key provides full admin access. Keep it secure.', + usage: 'Usage: Add to request header - x-api-key: ' + }, + soraS3: { + title: 'Sora Storage', + description: 'Manage Sora media storage profiles with S3 and Google Drive support', + newProfile: 'New Profile', + reloadProfiles: 'Reload Profiles', + empty: 'No storage profiles yet, create one first', + createTitle: 'Create Storage Profile', + editTitle: 'Edit Storage Profile', + selectProvider: 'Select Storage Type', + providerS3Desc: 'S3-compatible object storage', + providerGDriveDesc: 'Google Drive cloud storage', + profileID: 'Profile ID', + profileName: 'Profile Name', + setActive: 'Set as active after creation', + saveProfile: 'Save Profile', + activateProfile: 'Activate', + profileCreated: 'Storage profile created', + profileSaved: 'Storage profile saved', + profileDeleted: 'Storage profile deleted', + profileActivated: 'Active storage profile switched', + profileIDRequired: 'Profile ID is required', + profileNameRequired: 'Profile name is required', + profileSelectRequired: 'Please select a profile first', + endpointRequired: 'S3 endpoint is required when enabled', + bucketRequired: 'Bucket is required when enabled', + accessKeyRequired: 'Access Key ID is required when enabled', + deleteConfirm: 'Delete storage profile {profileID}?', + columns: { + profile: 'Profile', + profileId: 'Profile ID', + name: 'Name', + provider: 'Type', + active: 'Active', + endpoint: 'Endpoint', + bucket: 'Bucket', + storagePath: 'Storage Path', + capacityUsage: 'Capacity / Used', + capacityUnlimited: 'Unlimited', + videoCount: 'Videos', + videoCompleted: 'completed', + videoInProgress: 'in progress', + quota: 'Default Quota', + updatedAt: 'Updated At', + actions: 'Actions', + rootFolder: 'Root folder', + testInTable: 'Test', + testingInTable: 'Testing...', + testTimeout: 'Test timed out (15s)' + }, + enabled: 'Enable Storage', + enabledHint: 'When enabled, Sora generated media files will be automatically uploaded', + endpoint: 'S3 Endpoint', + region: 'Region', + bucket: 'Bucket', + prefix: 'Object Prefix', + accessKeyId: 'Access Key ID', + secretAccessKey: 'Secret Access Key', + secretConfigured: '(Configured, leave blank to keep)', + cdnUrl: 'CDN URL', + cdnUrlHint: 'Optional. When configured, files are accessed via CDN URL', + forcePathStyle: 'Force Path Style', + defaultQuota: 'Default Storage Quota', + defaultQuotaHint: 'Default quota when not specified at user or group level. 0 means unlimited', + testConnection: 'Test Connection', + testing: 'Testing...', + testSuccess: 'Connection test successful', + testFailed: 'Connection test failed', + saved: 'Storage settings saved successfully', + saveFailed: 'Failed to save storage settings', + gdrive: { + authType: 'Authentication Method', + serviceAccount: 'Service Account', + clientId: 'Client ID', + clientSecret: 'Client Secret', + clientSecretConfigured: '(Configured, leave blank to keep)', + refreshToken: 'Refresh Token', + refreshTokenConfigured: '(Configured, leave blank to keep)', + serviceAccountJson: 'Service Account JSON', + serviceAccountConfigured: '(Configured, leave blank to keep)', + folderId: 'Folder ID (optional)', + authorize: 'Authorize Google Drive', + authorizeHint: 'Get Refresh Token via OAuth2', + oauthFieldsRequired: 'Please fill in Client ID and Client Secret first', + oauthSuccess: 'Google Drive authorization successful', + oauthFailed: 'Google Drive authorization failed', + closeWindow: 'This window will close automatically', + processing: 'Processing authorization...', + testStorage: 'Test Storage', + testSuccess: 'Google Drive storage test passed (upload, access, delete all OK)', + testFailed: 'Google Drive storage test failed' + } + }, + overloadCooldown: { + title: '529 Overload Cooldown', + description: 'Configure account scheduling pause strategy when upstream returns 529 (overloaded)', + enabled: 'Enable Overload Cooldown', + enabledHint: 'Pause account scheduling on 529 errors, auto-recover after cooldown', + cooldownMinutes: 'Cooldown Duration (minutes)', + cooldownMinutesHint: 'Duration to pause account scheduling (1-120 minutes)', + saved: 'Overload cooldown settings saved', + saveFailed: 'Failed to save overload cooldown settings' + }, + rateLimit429Cooldown: { + title: '429 Default Cooldown', + description: 'Configure the default account cooldown when upstream returns 429 without an explicit reset time', + enabled: 'Enable 429 Default Cooldown', + enabledHint: 'Pause account scheduling when a 429 has no reset time, then auto-recover after cooldown', + cooldownSeconds: 'Cooldown Duration (seconds)', + cooldownSecondsHint: 'Default cooldown duration (1-7200 seconds); explicit upstream reset times still take precedence', + saved: '429 default cooldown settings saved', + saveFailed: 'Failed to save 429 default cooldown settings' + }, + streamTimeout: { + title: 'Stream Timeout Handling', + description: 'Configure account handling strategy when upstream response times out', + enabled: 'Enable Stream Timeout Handling', + enabledHint: 'Automatically handle problematic accounts when upstream times out', + timeoutSeconds: 'Timeout Threshold (seconds)', + timeoutSecondsHint: 'Stream data interval exceeding this time is considered timeout (30-300s)', + action: 'Action', + actionTempUnsched: 'Temporarily Unschedulable', + actionError: 'Mark as Error', + actionNone: 'No Action', + actionHint: 'Action to take on the account after timeout', + tempUnschedMinutes: 'Pause Duration (minutes)', + tempUnschedMinutesHint: 'Duration of temporary unschedulable state (1-60 minutes)', + thresholdCount: 'Trigger Threshold (count)', + thresholdCountHint: 'Number of timeouts before triggering action (1-10)', + thresholdWindowMinutes: 'Threshold Window (minutes)', + thresholdWindowMinutesHint: 'Time window for counting timeouts (1-60 minutes)', + saved: 'Stream timeout settings saved', + saveFailed: 'Failed to save stream timeout settings' + }, + rectifier: { + title: 'Request Rectifier', + description: 'Automatically fix request parameters and retry when upstream returns specific errors', + enabled: 'Enable Request Rectifier', + enabledHint: 'Master switch - disabling turns off all rectification features', + thinkingSignature: 'Thinking Signature Rectifier', + thinkingSignatureHint: 'Automatically strip signatures and retry when upstream returns thinking block signature validation errors', + thinkingBudget: 'Thinking Budget Rectifier', + thinkingBudgetHint: 'Automatically set budget to 32000 and retry when upstream returns budget_tokens constraint error (≥1024)', + apikeySignature: 'API Key Signature Rectifier', + apikeySignatureHint: + 'Automatically strip signatures and retry when API Key accounts receive signature-related errors (built-in patterns always apply)', + apikeyPatterns: 'Custom Match Patterns', + apikeyPatternsHint: + 'Additional keywords matched against the response body (case-insensitive). Built-in patterns always apply; use these for supplementary matching.', + apikeyPatternPlaceholder: 'e.g., thinking_error', + addPattern: 'Add Pattern', + saved: 'Rectifier settings saved', + saveFailed: 'Failed to save rectifier settings' + }, + betaPolicy: { + title: 'Beta Policy', + description: 'How to handle Beta features when configuring the forwarding of Anthropic API requests. Applicable only to the /v1/messages endpoint.', + action: 'Action', + actionPass: 'Pass (transparent)', + actionFilter: 'Filter (remove)', + actionBlock: 'Block (reject)', + scope: 'Scope', + scopeAll: 'All accounts', + scopeOAuth: 'OAuth only', + scopeAPIKey: 'API Key only', + scopeBedrock: 'Bedrock only', + errorMessage: 'Error message', + errorMessagePlaceholder: 'Custom error message when blocked', + errorMessageHint: 'Leave empty for default message', + saved: 'Beta policy settings saved', + saveFailed: 'Failed to save beta policy settings', + modelWhitelist: 'Model Whitelist', + modelWhitelistHint: 'Leave empty to apply to all models. Supports exact match and wildcard prefix (e.g., claude-opus-*)', + modelPatternPlaceholder: 'e.g., claude-opus-* or claude-opus-4-6', + addModelPattern: 'Add model pattern', + removePattern: 'Remove', + fallbackAction: 'Fallback Action', + fallbackActionHint: 'Action for models not matching the whitelist', + fallbackErrorMessagePlaceholder: 'Custom error message when non-whitelisted models are blocked', + quickPresets: 'Quick Presets', + presetOpusOnly: 'Opus only for 1M', + presetOpusOnlyDesc: 'Pass for Opus, filter others', + commonPatterns: 'Common patterns' + }, + openaiFastPolicy: { + title: 'OpenAI Fast/Flex Policy', + description: 'Intercept, filter, or pass OpenAI fast(priority) / flex requests based on the request body service_tier field. Applies to the OpenAI gateway only.', + empty: 'No rules configured. Click the button below to add one.', + ruleHeader: 'Rule #{index}', + removeRule: 'Remove rule', + addRule: 'Add rule', + saveHint: 'Saved together with system settings (click the global Save button at the bottom of the page).', + serviceTier: 'service_tier match', + tierAll: 'All tiers', + tierPriority: 'priority (fast)', + tierFlex: 'flex', + action: 'Action', + actionPass: 'Pass (keep service_tier)', + actionFilter: 'Filter (remove service_tier)', + actionForcePriority: 'Force priority (fast)', + actionBlock: 'Block (reject request)', + scope: 'Scope', + scopeAll: 'All accounts', + scopeOAuth: 'OAuth only', + scopeAPIKey: 'API Key only', + scopeBedrock: 'Bedrock only', + errorMessage: 'Error message', + errorMessagePlaceholder: 'Custom error message when blocked', + errorMessageHint: 'Leave empty for the default message.', + modelWhitelist: 'Model whitelist', + modelWhitelistHint: 'Leave empty to apply to all models. Supports exact match and wildcard prefix (e.g., gpt-5.5*).', + modelPatternPlaceholder: 'e.g., gpt-5.5 or gpt-5.5*', + addModelPattern: 'Add model pattern', + fallbackAction: 'Fallback action', + fallbackActionHint: 'Action for models not matching the whitelist.', + fallbackErrorMessagePlaceholder: 'Custom error message when non-whitelisted models are blocked' + }, + wechatConnect: { + title: 'WeChat Connect', + description: 'Third-party login configuration for WeChat Open Platform or Official Account / Mini Program.', + enabledLabel: 'Enable WeChat Connect', + enabledHint: 'Enable this to configure WeChat OAuth callbacks and authorization.', + appIdLabel: 'App ID', + appIdPlaceholder: 'WeChat App ID', + appSecretLabel: 'App Secret', + appSecretConfiguredPlaceholder: 'Secret configured. Leave empty to keep the current value.', + appSecretPlaceholder: 'WeChat App Secret', + appSecretConfiguredHint: 'Secret configured. Leave empty to keep the current value.', + appSecretHint: 'Enter a new secret to replace the current WeChat credential.', + modeLabel: 'Mode', + openModeLabel: 'Use Open outside WeChat', + openModeHint: 'Use Open Platform QR authorization outside the WeChat browser.', + mpModeLabel: 'Use MP inside WeChat', + mpModeHint: 'Use Official Account authorization inside the WeChat browser.', + redirectUrlLabel: 'Redirect URL', + redirectUrlPlaceholder: 'https://your-site.com/api/v1/auth/oauth/wechat/callback', + generateAndCopy: 'Generate & Copy (current site)', + redirectUrlSetAndCopied: 'Redirect URL generated and copied to clipboard', + frontendRedirectUrlLabel: 'Frontend redirect URL', + frontendRedirectUrlPlaceholder: '/auth/wechat/callback', + frontendRedirectUrlHint: 'Usually the frontend route callback path; keep it aligned with the backend.' + }, + authSourceDefaults: { + title: 'Auth Source Defaults', + description: 'Configure per-source default balance, concurrency, subscriptions, and grant rules.', + requireEmailLabel: 'Require email on third-party signup', + requireEmailHint: 'When enabled, Linux DO, OIDC, and WeChat signups must provide an email before account creation.', + enabledHint: 'These defaults apply when a new user registers through this source. Grant on first bind only applies when an existing user binds this source.', + sources: { + email: { + title: 'Email signup', + description: 'Default quota grants for email-password signups.' + }, + linuxdo: { + title: 'Linux DO signup', + description: 'Default quota grants for Linux DO signups.' + }, + oidc: { + title: 'OIDC signup', + description: 'Default quota grants for OIDC signups.' + }, + wechat: { + title: 'WeChat signup', + description: 'Default quota grants for WeChat signups.' + } + }, + grantOnFirstBindLabel: 'Grant on first bind', + grantOnFirstBindHint: 'Grant default entitlements when an existing user first binds this source.', + defaultSubscriptionsLabel: 'Default subscriptions', + defaultSubscriptionsHint: 'Applies only to this auth source. Leave empty to skip source-specific subscriptions.', + noSourceSubscriptions: 'No source-specific default subscriptions configured.', + platformQuotasOverride: 'Platform Quota Overrides', + platformQuotasOverrideHint: 'Blank fields inherit the system default. Set to 0 to fully block that window for this auth source.', + }, + paymentVisibleMethods: { + methodLabel: '{title} visible method', + methodHint: 'Controls whether checkout shows this method and which source key it exposes.', + sourceLabel: 'Payment source', + sourceHint: 'Choose an explicit source before enabling the method. Not configured methods are not exposed.', + sourceRequiredError: 'Select a payment source before enabling {title}.' + }, + openaiExperimentalScheduler: { + title: 'OpenAI experimental scheduler policy', + description: "Disabled by default. When enabled, this only changes the gateway's experimental account-selection policy for OpenAI traffic; it does not indicate an upstream OpenAI capability.", + stickyWeightedTitle: 'Sticky weighting', + stickyWeightedDescription: 'When enabled, previous_response_id and session_hash affinity are scored by the advanced scheduler. When disabled, sticky accounts keep the legacy hard-hit behavior.', + subscriptionPriorityTitle: 'Subscription priority', + subscriptionPriorityDescription: 'When enabled, the scheduler scores ChatGPT subscription accounts first and falls back to non-subscription accounts only if no subscription slot can be acquired.', + weightsTitle: 'Scheduler weight overrides', + weightsDescription: 'Blank values use config/environment values; when config is not set, built-in defaults apply. Non-blank page settings take priority.', + defaultPlaceholder: 'config/default: {value}', + topKLabel: 'TopK', + priorityWeight: 'Priority', + loadWeight: 'Load', + queueWeight: 'Queue', + errorRateWeight: 'Error rate', + ttftWeight: 'TTFT', + resetWeight: 'Reset window', + quotaHeadroomWeight: 'Quota headroom', + previousResponseWeight: 'previous_response sticky', + sessionStickyWeight: 'session_hash sticky' + }, + usageRecords: { + title: 'Usage Records', + description: 'Settings for usage and failed-request records visible to end users.', + }, + user_error_view: { + label: 'Allow users to view their own error requests', + description: 'When enabled, users can see a redacted view of their failed requests on the usage page (no internal/upstream details). Requires ops monitoring enabled to have data.', + }, + saveSettings: 'Save Settings', + saving: 'Saving...', + settingsSaved: 'Settings saved successfully', + smtpConnectionSuccess: 'SMTP connection successful', + testEmailSent: 'Test email sent successfully', + failedToLoad: 'Failed to load settings', + failedToSave: 'Failed to save settings', + failedToTestSmtp: 'SMTP connection test failed', + failedToSendTestEmail: 'Failed to send test email' + }, + + // Error Passthrough Rules + errorPassthrough: { + title: 'Error Passthrough Rules', + description: 'Configure how upstream errors are returned to clients', + createRule: 'Create Rule', + editRule: 'Edit Rule', + deleteRule: 'Delete Rule', + noRules: 'No rules configured', + createFirstRule: 'Create your first error passthrough rule', + allPlatforms: 'All Platforms', + passthrough: 'Passthrough', + custom: 'Custom', + code: 'Code', + body: 'Body', + skipMonitoring: 'Skip Monitoring', + + // Columns + columns: { + priority: 'Priority', + name: 'Name', + conditions: 'Conditions', + platforms: 'Platforms', + behavior: 'Behavior', + status: 'Status', + actions: 'Actions' + }, + + // Match Mode + matchMode: { + any: 'Code OR Keyword', + all: 'Code AND Keyword', + anyHint: 'Status code matches any error code, OR message contains any keyword', + allHint: 'Status code matches any error code, AND message contains any keyword' + }, + + // Form + form: { + name: 'Rule Name', + namePlaceholder: 'e.g., Context Limit Passthrough', + priority: 'Priority', + priorityHint: 'Lower values have higher priority', + description: 'Description', + descriptionPlaceholder: 'Describe the purpose of this rule...', + matchConditions: 'Match Conditions', + errorCodes: 'Error Codes', + errorCodesPlaceholder: '422, 400, 429', + errorCodesHint: 'Separate multiple codes with commas', + keywords: 'Keywords', + keywordsPlaceholder: 'One keyword per line\ncontext limit\nmodel not supported', + keywordsHint: 'One keyword per line, case-insensitive', + matchMode: 'Match Mode', + platforms: 'Platforms', + platformsHint: 'Leave empty to apply to all platforms', + responseBehavior: 'Response Behavior', + passthroughCode: 'Passthrough upstream status code', + responseCode: 'Custom status code', + passthroughBody: 'Passthrough upstream error message', + customMessage: 'Custom error message', + customMessagePlaceholder: 'Error message to return to client...', + skipMonitoring: 'Skip monitoring', + skipMonitoringHint: 'When enabled, errors matching this rule will not be recorded in ops monitoring', + enabled: 'Enable this rule' + }, + + // Messages + nameRequired: 'Please enter rule name', + conditionsRequired: 'Please configure at least one error code or keyword', + ruleCreated: 'Rule created successfully', + ruleUpdated: 'Rule updated successfully', + ruleDeleted: 'Rule deleted successfully', + deleteConfirm: 'Are you sure you want to delete rule "{name}"?', + failedToLoad: 'Failed to load rules', + failedToSave: 'Failed to save rule', + failedToDelete: 'Failed to delete rule', + failedToToggle: 'Failed to toggle status' + }, + + // TLS Fingerprint Profiles + tlsFingerprintProfiles: { + title: 'TLS Fingerprint Profiles', + description: 'Manage TLS fingerprint profiles for simulating specific client TLS handshake characteristics', + createProfile: 'Create Profile', + editProfile: 'Edit Profile', + deleteProfile: 'Delete Profile', + noProfiles: 'No profiles configured', + createFirstProfile: 'Create your first TLS fingerprint profile', + + columns: { + name: 'Name', + description: 'Description', + grease: 'GREASE', + alpn: 'ALPN', + actions: 'Actions' + }, + + form: { + pasteYaml: 'Paste YAML Configuration', + pasteYamlPlaceholder: 'Paste YAML output from TLS Fingerprint Collector here...', + pasteYamlHint: 'Paste the YAML copied from TLS Fingerprint Collector to auto-fill all fields.', + openCollector: 'Open Collector', + parseYaml: 'Parse YAML', + yamlParsed: 'YAML parsed successfully, fields auto-filled', + yamlParseFailed: 'Failed to parse YAML: name field not found', + name: 'Profile Name', + namePlaceholder: 'e.g. macOS Node.js v24', + description: 'Description', + descriptionPlaceholder: 'Optional description for this profile', + enableGrease: 'Enable GREASE', + enableGreaseHint: 'Insert GREASE values in TLS ClientHello extensions', + cipherSuites: 'Cipher Suites', + cipherSuitesHint: 'Comma-separated hex values, e.g. 0x1301, 0x1302, 0xc02c', + curves: 'Elliptic Curves', + curvesHint: 'Comma-separated curve IDs', + pointFormats: 'Point Formats', + signatureAlgorithms: 'Signature Algorithms', + alpnProtocols: 'ALPN Protocols', + alpnProtocolsHint: 'Comma-separated, e.g. h2, http/1.1', + supportedVersions: 'Supported TLS Versions', + keyShareGroups: 'Key Share Groups', + pskModes: 'PSK Modes', + extensions: 'Extensions' + }, + + deleteConfirm: 'Delete Profile', + deleteConfirmMessage: 'Are you sure you want to delete profile "{name}"? Accounts using this profile will fall back to the built-in default.', + createSuccess: 'Profile created successfully', + updateSuccess: 'Profile updated successfully', + deleteSuccess: 'Profile deleted successfully', + loadFailed: 'Failed to load profiles', + saveFailed: 'Failed to save profile', + deleteFailed: 'Failed to delete profile' + } +} diff --git a/frontend/src/i18n/locales/en/common.ts b/frontend/src/i18n/locales/en/common.ts new file mode 100644 index 0000000000..ac0b8dfcae --- /dev/null +++ b/frontend/src/i18n/locales/en/common.ts @@ -0,0 +1,417 @@ +export default { + common: { + loading: 'Loading...', + submitting: 'Submitting...', + justNow: 'just now', + peakRateTooltip: 'Peak rate: {window}', + peakRateImageNote: '; image tokens billed as tokens are also affected, per-image billing is unaffected', + save: 'Save', + saved: 'Saved successfully', + deleted: 'Deleted successfully', + cancel: 'Cancel', + delete: 'Delete', + edit: 'Edit', + create: 'Create', + update: 'Update', + confirm: 'Confirm', + reset: 'Reset', + search: 'Search', + filter: 'Filter', + export: 'Export', + import: 'Import', + actions: 'Actions', + status: 'Status', + name: 'Name', + email: 'Email', + password: 'Password', + submit: 'Submit', + back: 'Back', + next: 'Next', + yes: 'Yes', + no: 'No', + all: 'All', + none: 'None', + selectAll: 'Select all', + noData: 'No data', + expand: 'Expand', + collapse: 'Collapse', + success: 'Success', + error: 'Error', + critical: 'Critical', + warning: 'Warning', + info: 'Info', + active: 'Active', + inactive: 'Inactive', + more: 'More', + close: 'Close', + enabled: 'Enabled', + disabled: 'Disabled', + total: 'Total', + balance: 'Balance', + availableBalance: 'Available balance', + frozenBalance: 'Frozen balance', + totalBalance: 'Total balance', + available: 'Available', + copiedToClipboard: 'Copied to clipboard', + copied: 'Copied', + copyFailed: 'Failed to copy', + verifying: 'Verifying...', + processing: 'Processing...', + contactSupport: 'Contact Support', + add: 'Add', + invalidEmail: 'Please enter a valid email address', + optional: 'optional', + selectOption: 'Select an option', + searchPlaceholder: 'Search...', + noOptionsFound: 'No options found', + noGroupsAvailable: 'No groups available', + unknownError: 'Unknown error occurred', + saving: 'Saving...', + selectedCount: '({count} selected)', + refresh: 'Refresh', + autoRefresh: { + title: 'Auto Refresh', + enable: 'Enable auto refresh', + countdown: 'Auto refresh: {seconds}s', + seconds: '{n} seconds', + }, + view: 'View', + settings: 'Settings', + chooseFile: 'Choose File', + copy: 'Copy', + notAvailable: 'N/A', + now: 'Now', + today: 'Today', + tomorrow: 'Tomorrow', + unknown: 'Unknown', + minutes: 'min', + time: { + never: 'Never', + justNow: 'Just now', + minutesAgo: '{n}m ago', + hoursAgo: '{n}h ago', + daysAgo: '{n}d ago', + countdown: { + daysHours: '{d}d {h}h', + hoursMinutes: '{h}h {m}m', + minutes: '{m}m', + withSuffix: '{time} to lift' + } + } + }, + + adminCompliance: { + title: 'Deployment and Operation Compliance Acknowledgment', + blockingNotice: 'Deployment and operation compliance acknowledgment is required before continuing to use the console.', + riskNotice: 'This acknowledgment provides clear, conspicuous, and reproducible notice of compliance obligations and operation risks for self-hosted instances.', + version: 'Document Version', + openDocument: 'Open the GitHub document', + documentSource: 'The agreement text comes from Markdown files in this project repository. When the agreement content changes, the document version must be incremented; acknowledgments of older versions become invalid and console users must acknowledge again.', + inputLabel: 'Type the following confirmation phrase exactly', + inputPlaceholder: 'Type the confirmation phrase to continue', + inputMismatch: 'The confirmation phrase does not match. Type the displayed text exactly.', + legalNote: 'This acknowledgment defines the no-affiliation relationship and responsibility boundary between self-hosted instances and the open-source project, copyright holders, contributors, and maintainers. The party that deploys, operates, or controls the relevant instance remains independently responsible for its applicable obligations.', + logout: 'Log out', + accept: 'Acknowledge and Continue', + accepted: 'Compliance acknowledgment recorded', + acceptFailed: 'Failed to submit acknowledgment' + }, + + legal: { + loadFailed: 'Failed to load document', + retryLater: 'Refresh the page and try again later.', + notFound: 'Document not found', + notFoundDescription: 'This legal document does not exist or has been removed by an administrator.', + updatedAt: 'Updated: {date}', + empty: 'No content', + loginAgreement: 'Login Agreement', + adminCompliance: 'Deployment and Operation Compliance Commitment', + loginAgreementPrompt: { + checkboxPrefix: 'I have read and agree to ', + documentSeparator: ', ', + noticeTitle: 'Accept the latest terms before continuing.', + noticeDescription: 'Account/password login and quick sign-in stay disabled until you accept.', + viewTerms: 'View terms', + dialogTitle: 'Terms Update Notice', + dialogDescription: 'Our service terms were updated on {date}. Please read and accept the following terms before continuing.', + recently: 'recently', + relatedDocuments: 'Related documents', + reject: 'Reject', + accept: 'Accept and continue', + loginRejectedWarning: 'Account/password login and quick sign-in are disabled until you accept the latest terms.', + loginRequiredWarning: 'Please read and accept the latest terms before logging in.', + registerRejectedWarning: 'Registration and quick sign-in are disabled until you accept the latest terms.', + registerRequiredWarning: 'Please read and accept the latest terms before registering.' + } + }, + + // Navigation + nav: { + dashboard: 'Dashboard', + announcements: 'Announcements', + apiKeys: 'API Keys', + batchImage: 'Batch Images', + usage: 'Usage', + redeem: 'Redeem', + affiliate: 'Affiliate Rebates', + affiliateManagement: 'Affiliate Rebates', + affiliateInviteRecords: 'Invite Records', + affiliateRebateRecords: 'Rebate Records', + affiliateTransferRecords: 'Transfer Records', + profile: 'Profile', + users: 'Users', + groups: 'Groups', + channels: 'Channels', + availableChannels: 'Available Channels', + subscriptions: 'Subscriptions', + accounts: 'Accounts', + proxies: 'Proxies', + redeemCodes: 'Redeem Codes', + ops: 'Ops', + promoCodes: 'Promo Codes', + settings: 'Settings', + myAccount: 'My Account', + lightMode: 'Light Mode', + darkMode: 'Dark Mode', + collapse: 'Collapse', + expand: 'Expand', + logout: 'Logout', + github: 'GitHub', + mySubscriptions: 'My Subscriptions', + buySubscription: 'Recharge / Subscription', + docs: 'Docs', + myOrders: 'My Orders', + orderManagement: 'Orders', + paymentDashboard: 'Payment Dashboard', + paymentConfig: 'Payment Config', + paymentPlans: 'Plans', + channelManagement: 'Channels', + channelPricing: 'Channel Pricing', + channelMonitor: 'Channel Monitor', + channelStatus: 'Channel Status', + riskControl: 'Risk Control', + }, + + // Auth + auth: { + welcomeBack: 'Welcome Back', + signInToAccount: 'Sign in to your account to continue', + signIn: 'Sign In', + signingIn: 'Signing in...', + createAccount: 'Create Account', + signUpToStart: 'Sign up to start using {siteName}', + signUp: 'Sign up', + processing: 'Processing...', + continue: 'Continue', + rememberMe: 'Remember me', + dontHaveAccount: "Don't have an account?", + alreadyHaveAccount: 'Already have an account?', + registrationDisabled: 'Registration is currently disabled. Please contact the administrator.', + emailLabel: 'Email', + emailPlaceholder: 'Enter your email', + passwordLabel: 'Password', + passwordPlaceholder: 'Enter your password', + createPasswordPlaceholder: 'Create a strong password', + passwordHint: 'At least 6 characters', + emailRequired: 'Email is required', + invalidEmail: 'Please enter a valid email address', + passwordRequired: 'Password is required', + passwordMinLength: 'Password must be at least 6 characters', + loginFailed: 'Login failed. Please check your credentials and try again.', + errors: { + USER_NOT_ACTIVE: 'Account has been disabled.', + }, + registrationFailed: 'Registration failed. Please try again.', + emailSuffixNotAllowed: 'This email domain is not allowed for registration.', + emailSuffixNotAllowedWithAllowed: + 'This email domain is not allowed. Allowed domains: {suffixes}', + emailSuffixAllowedMore: 'and {count} more', + loginSuccess: 'Login successful! Welcome back.', + accountCreatedSuccess: 'Account created successfully! Welcome to {siteName}.', + reloginRequired: 'Session expired. Please log in again.', + turnstileExpired: 'Verification expired, please try again', + turnstileFailed: 'Verification failed, please try again', + completeVerification: 'Please complete the verification', + verifyYourEmail: 'Verify Your Email', + sessionExpired: 'Session expired', + sessionExpiredDesc: 'Please go back to the registration page and start again.', + verificationCode: 'Verification Code', + verificationCodeHint: 'Enter the 6-digit code sent to your email', + sendingCode: 'Sending...', + sendCode: 'Send code', + clickToResend: 'Click to resend code', + resendCode: 'Resend verification code', + sendCodeDesc: "We'll send a verification code to", + codeSentSuccess: 'Verification code sent! Please check your inbox.', + verifying: 'Verifying...', + verifyAndCreate: 'Verify & Create Account', + resendCountdown: 'Resend code in {countdown}s', + backToRegistration: 'Back to registration', + sendCodeFailed: 'Failed to send verification code. Please try again.', + verifyFailed: 'Verification failed. Please try again.', + codeRequired: 'Verification code is required', + invalidCode: 'Please enter a valid 6-digit code', + promoCodeLabel: 'Promo Code', + promoCodePlaceholder: 'Enter promo code (optional)', + promoCodeValid: 'Valid! You will receive ${amount} bonus balance', + promoCodeInvalid: 'Invalid promo code', + promoCodeNotFound: 'Promo code not found', + promoCodeExpired: 'This promo code has expired', + promoCodeDisabled: 'This promo code is disabled', + promoCodeMaxUsed: 'This promo code has reached its usage limit', + promoCodeAlreadyUsed: 'You have already used this promo code', + promoCodeValidating: 'Promo code is being validated, please wait', + promoCodeInvalidCannotRegister: 'Invalid promo code. Please check and try again or clear the promo code field', + invitationCodeLabel: 'Invitation Code', + invitationCodePlaceholder: 'Enter invitation code', + invitationCodeRequired: 'Invitation code is required', + invitationCodeValid: 'Invitation code is valid', + invitationCodeInvalid: 'Invalid or used invitation code', + invitationCodeValidating: 'Validating invitation code...', + invitationCodeInvalidCannotRegister: 'Invalid invitation code. Please check and try again', + oauthOrContinue: 'or continue with others', + linuxdo: { + signIn: 'Continue with Linux.do', + orContinue: 'or continue with email', + callbackTitle: 'Signing you in', + callbackProcessing: 'Completing login, please wait...', + callbackHint: 'If you are not redirected automatically, go back to the login page and try again.', + callbackMissingToken: 'Missing login token, please try again.', + backToLogin: 'Back to Login', + invitationRequired: 'This Linux.do account is not yet registered. The site requires an invitation code — please enter one to complete registration.', + invalidPendingToken: 'The registration token has expired. Please sign in with Linux.do again.', + completeRegistration: 'Complete Registration', + completing: 'Completing registration…', + completeRegistrationFailed: 'Registration failed. Please check your invitation code and try again.' + }, + dingtalk: { + signIn: 'Continue with DingTalk', + callbackTitle: 'Signing you in with DingTalk', + callbackProcessing: 'Completing DingTalk login, please wait...', + callbackHint: 'If you are not redirected automatically, go back to the login page and try again.', + callbackMissingToken: 'Missing login token, please try again.', + backToLogin: 'Back to Login', + invitationRequired: 'This DingTalk account is not yet registered. The site requires an invitation code — please enter one to complete registration.', + invalidPendingToken: 'The registration token has expired. Please sign in with DingTalk again.', + completeRegistration: 'Complete Registration', + completing: 'Completing registration…', + completeRegistrationFailed: 'Registration failed. Please check your invitation code and try again.', + createAccountTitle: 'Create DingTalk Account', + registrationDisabledRedirectToBind: 'New account registration is currently disabled. Please bind to your existing account with its email and password.', + error: { + title: 'DingTalk Sign-in Failed', + csrf: 'Login session expired, please scan again', + corp_rejected: 'Your DingTalk account is not part of this organization. Please contact administrator', + dingtalk_not_enabled: 'DingTalk login is not enabled', + upstream_error: 'DingTalk service is temporarily unavailable. Please try again later', + missing_browser_session: 'Browser session lost. Please login again', + missing_params: 'Request parameters are incomplete', + invalid_state: 'Invalid login state', + provider_error: 'DingTalk authorization failed', + session_error: 'Failed to create session. Please retry', + retry: 'Retry Login' + } + }, + emailOAuth: { + signIn: 'Continue with {providerName}' + }, + oidc: { + signIn: 'Continue with {providerName}', + callbackTitle: 'Signing you in with {providerName}', + callbackProcessing: 'Completing login with {providerName}, please wait...', + callbackHint: 'If you are not redirected automatically, go back to the login page and try again.', + callbackMissingToken: 'Missing login token, please try again.', + backToLogin: 'Back to Login', + invitationRequired: + 'This {providerName} account is not yet registered. The site requires an invitation code — please enter one to complete registration.', + invalidPendingToken: 'The registration token has expired. Please sign in again.', + completeRegistration: 'Complete Registration', + completing: 'Completing registration…', + completeRegistrationFailed: 'Registration failed. Please check your invitation code and try again.' + }, + oauthFlow: { + profileDetailsTitle: 'Use {providerName} profile details', + profileDetailsDescription: 'Choose whether to apply the nickname or avatar from {providerName} to this account.', + useDisplayName: 'Use display name', + useAvatar: 'Use avatar', + avatarAlt: '{providerName} avatar', + reviewProfileBeforeContinue: 'Review the {providerName} profile details before continuing.', + chooseHowToContinue: 'Choose how to continue', + chooseAccountActionHint: 'Choose whether to bind an existing account or create a new one.', + suggestedEmail: 'Suggested email: {email}', + bindExistingAccount: 'Bind existing account', + createNewAccount: 'Create new account', + createAccountHint: 'Enter an email address to create your account and continue.', + bindLoginHint: 'Log in to an existing account to bind this {providerName} sign-in.', + signInThenBindDescription: 'Sign in to an existing account, then bind this {providerName} sign-in to it.', + bindSignInToExistingAccount: 'Bind this {providerName} sign-in to an existing account.', + bindCurrentAccountTitle: 'Bind the current account', + bindCurrentAccountDescription: 'Bind this {providerName} sign-in to the account currently signed in on this browser.', + bindCurrentAccount: 'Bind current account', + logInAndBind: 'Log in and bind', + useDifferentEmail: 'Use a different email', + backToOptions: 'Back to options', + yourAccount: 'your account', + totpHint: 'Enter the 6-digit verification code for {account} to finish binding this {providerName} sign-in.', + verifyAndContinue: 'Verify and continue', + wechatAvailabilityUnknown: 'WeChat sign-in availability could not be confirmed. Refresh and retry.', + wechatSystemBrowserOnly: 'This WeChat sign-in flow is only available in your system browser.', + wechatBrowserOnly: 'This WeChat sign-in flow is only available inside the WeChat browser.', + wechatNotConfigured: 'WeChat sign-in is not configured yet.' + }, + linuxdoCallbackPageTitle: 'LinuxDo Sign-In Callback', + dingtalkCallbackPageTitle: 'DingTalk Sign-In Callback', + dingtalkProviderName: 'DingTalk', + oidcCallbackPageTitle: 'OIDC Sign-In Callback', + oauthCallbackPageTitle: 'OAuth Callback', + wechatProviderName: 'WeChat', + wechatCallbackPageTitle: 'WeChat Sign-In Callback', + wechatPaymentCallbackPageTitle: 'WeChat Payment Callback', + wechatPayment: { + callbackTitle: 'Resuming WeChat payment', + callbackProcessing: 'Resuming WeChat payment...', + backToPayment: 'Back to payment', + callbackMissingResumeToken: 'The WeChat payment callback is missing the resume token.' + }, + oauth: { + callbackTitle: 'OAuth Callback', + callbackHint: 'Copy the code and state back to the admin authorization flow when needed.', + invalidCallbackTitle: 'Invalid sign-in callback', + invalidCallbackHint: 'This page does not contain a valid authorization result. Return to the login page and start quick sign-in again.', + code: 'Code', + state: 'State', + fullUrl: 'Full URL' + }, + // Forgot password + forgotPassword: 'Forgot password?', + forgotPasswordTitle: 'Reset Your Password', + forgotPasswordHint: 'Enter your email address and we will send you a link to reset your password.', + sendResetLink: 'Send Reset Link', + sendingResetLink: 'Sending...', + sendResetLinkFailed: 'Failed to send reset link. Please try again.', + resetEmailSent: 'Reset Link Sent', + resetEmailSentHint: 'If an account exists with this email, you will receive a password reset link shortly. Please check your inbox and spam folder.', + backToLogin: 'Back to Login', + rememberedPassword: 'Remembered your password?', + // Reset password + resetPasswordTitle: 'Set New Password', + resetPasswordHint: 'Enter your new password below.', + newPassword: 'New Password', + newPasswordPlaceholder: 'Enter your new password', + confirmPassword: 'Confirm Password', + confirmPasswordPlaceholder: 'Confirm your new password', + confirmPasswordRequired: 'Please confirm your password', + passwordsDoNotMatch: 'Passwords do not match', + resetPassword: 'Reset Password', + resettingPassword: 'Resetting...', + resetPasswordFailed: 'Failed to reset password. Please try again.', + passwordResetSuccess: 'Password Reset Successful', + passwordResetSuccessHint: 'Your password has been reset. You can now sign in with your new password.', + invalidResetLink: 'Invalid Reset Link', + invalidResetLinkHint: 'This password reset link is invalid or has expired. Please request a new one.', + requestNewResetLink: 'Request New Reset Link', + invalidOrExpiredToken: 'The password reset link is invalid or has expired. Please request a new one.' + }, + + // Dashboard +} diff --git a/frontend/src/i18n/locales/en/dashboard.ts b/frontend/src/i18n/locales/en/dashboard.ts new file mode 100644 index 0000000000..a9c7c750c8 --- /dev/null +++ b/frontend/src/i18n/locales/en/dashboard.ts @@ -0,0 +1,811 @@ +export default { + dashboard: { + title: 'Dashboard', + welcomeMessage: "Welcome back! Here's an overview of your account.", + balance: 'Balance', + apiKeys: 'API Keys', + todayRequests: 'Today Requests', + todayCost: 'Today Cost', + todayTokens: 'Today Tokens', + totalTokens: 'Total Tokens', + cacheToday: 'Cache (Today)', + performance: 'Performance', + avgResponse: 'Avg Response', + averageTime: 'Average time', + timeRange: 'Time Range', + granularity: 'Granularity', + day: 'Day', + hour: 'Hour', + modelDistribution: 'Model Distribution', + groupDistribution: 'Group Usage Distribution', + platformBreakdown: 'Per-platform Breakdown', + platformBreakdownEmpty: 'No platform usage yet', + platformCount: '{count} platforms', + platformOther: 'Other', + platformQuota: { + title: 'Quota Usage', + daily: 'Daily', + weekly: 'Weekly', + monthly: 'Monthly (30-day rolling)', + resetsAt: 'Resets {time}', + noLimit: 'unlimited', + disabled: 'Disabled', + }, + tokenUsageTrend: 'Token Usage Trend', + noDataAvailable: 'No data available', + model: 'Model', + group: 'Group', + noGroup: 'No Group', + requests: 'Requests', + tokens: 'Tokens', + actual: 'Actual', + standard: 'Standard', + input: 'Input', + output: 'Output', + cache: 'Cache', + recentUsage: 'Recent Usage', + last7Days: 'Last 7 days', + noUsageRecords: 'No usage records', + startUsingApi: 'Start using the API to see your usage history here.', + viewAllUsage: 'View all usage', + quickActions: 'Quick Actions', + createApiKey: 'Create API Key', + generateNewKey: 'Generate a new API key', + batchImageAgent: 'Batch Image Assistant', + batchImageAgentDesc: 'Copy instructions for an agent', + viewUsage: 'View Usage', + checkDetailedLogs: 'Check detailed usage logs', + redeemCode: 'Redeem Code', + addBalanceWithCode: 'Add balance with a code' + }, + + // Groups (shared) + groups: { + subscription: 'Sub' + }, + + // API Keys + keys: { + title: 'API Keys', + description: 'Manage your API keys and access tokens', + searchPlaceholder: 'Search name or key...', + endpoints: { + title: 'API Endpoints', + default: 'Default', + copied: 'Copied', + copiedHint: 'Copied to clipboard', + clickToCopy: 'Click to copy this endpoint', + speedTest: 'Speed Test', + }, + allGroups: 'All Groups', + allStatus: 'All Status', + columnSettings: 'Column Settings', + columnAlwaysVisible: 'This column is always visible', + createKey: 'Create API Key', + editKey: 'Edit API Key', + deleteKey: 'Delete API Key', + deleteConfirmMessage: "Are you sure you want to delete '{name}'? This action cannot be undone.", + apiKey: 'API Key', + group: 'Group', + currentConcurrency: 'Current Concurrency', + noGroup: 'No group', + searchGroup: 'Search groups...', + noGroupFound: 'No groups found', + created: 'Created', + copyToClipboard: 'Copy to clipboard', + copied: 'Copied!', + importToCcSwitch: 'Import to CCS', + enable: 'Enable', + disable: 'Disable', + nameLabel: 'Name', + namePlaceholder: 'My API Key', + groupLabel: 'Group', + selectGroup: 'Select a group', + statusLabel: 'Status', + selectStatus: 'Select status', + saving: 'Saving...', + noKeysYet: 'No API keys yet', + createFirstKey: 'Create your first API key to get started with the API.', + keyCreatedSuccess: 'API key created successfully', + keyUpdatedSuccess: 'API key updated successfully', + keyDeletedSuccess: 'API key deleted successfully', + keyEnabledSuccess: 'API key enabled successfully', + keyDisabledSuccess: 'API key disabled successfully', + failedToLoad: 'Failed to load API keys', + failedToSave: 'Failed to save API key', + failedToDelete: 'Failed to delete API key', + failedToUpdateStatus: 'Failed to update API key status', + clickToChangeGroup: 'Click to change group', + groupChangedSuccess: 'Group changed successfully', + failedToChangeGroup: 'Failed to change group', + groupRequired: 'Please select a group', + usage: 'Usage', + today: 'Today', + total: 'Last 30d', + quota: 'Quota', + lastUsedAt: 'Last Used', + useKey: 'Use Key', + useKeyModal: { + title: 'Use API Key', + description: + 'Add the following environment variables to your terminal profile or run directly in terminal to configure API access.', + copy: 'Copy', + copied: 'Copied', + note: 'These environment variables will be active in the current terminal session. For permanent configuration, add them to ~/.bashrc, ~/.zshrc, or the appropriate configuration file.', + noGroupTitle: 'Please assign a group first', + noGroupDescription: 'This API key has not been assigned to a group. Please click the group column in the key list to assign one before viewing the configuration.', + openai: { + description: 'Add the following configuration files to your Codex CLI config directory.', + configTomlHint: 'Make sure the following content is at the beginning of the config.toml file', + note: 'Make sure the config directory exists. macOS/Linux users can run mkdir -p ~/.codex to create it.', + noteWindows: 'Press Win+R and enter %userprofile%\\.codex to open the config directory. Create it manually if it does not exist.', + }, + cliTabs: { + claudeCode: 'Claude Code', + geminiCli: 'Gemini CLI', + codexCli: 'Codex CLI', + codexCliWs: 'Codex CLI (WebSocket)', + opencode: 'OpenCode', + }, + antigravity: { + description: 'Configure API access for Antigravity group. Select the configuration method based on your client.', + claudeCode: 'Claude Code', + geminiCli: 'Gemini CLI', + claudeNote: 'These environment variables will be active in the current terminal session. For permanent configuration, add them to ~/.bashrc, ~/.zshrc, or the appropriate configuration file.', + geminiNote: 'These environment variables will be active in the current terminal session. For permanent configuration, add them to ~/.bashrc, ~/.zshrc, or the appropriate configuration file.', + }, + gemini: { + description: 'Add the following environment variables to your terminal profile or run directly in terminal to configure Gemini CLI access.', + modelComment: 'If you have Gemini 3 access, you can use: gemini-3-pro-preview', + note: 'These environment variables will be active in the current terminal session. For permanent configuration, add them to ~/.bashrc, ~/.zshrc, or the appropriate configuration file.', + }, + opencode: { + title: 'OpenCode Example', + subtitle: 'opencode.json', + hint: 'Config path: ~/.config/opencode/opencode.json (or opencode.jsonc), create if not exists. Use default providers (openai/anthropic/google) or custom provider_id. API Key can be configured directly or via /connect command. This is an example, adjust models and options as needed.', + }, + }, + customKeyLabel: 'Custom Key', + customKeyPlaceholder: 'Enter your custom key (min 16 chars)', + customKeyHint: 'Only letters, numbers, underscores and hyphens allowed. Minimum 16 characters.', + customKeyTooShort: 'Custom key must be at least 16 characters', + customKeyInvalidChars: 'Custom key can only contain letters, numbers, underscores, and hyphens', + customKeyRequired: 'Please enter a custom key', + ipRestriction: 'IP Restriction', + ipWhitelist: 'IP Whitelist', + ipWhitelistPlaceholder: '192.168.1.100\n10.0.0.0/8', + ipWhitelistHint: 'One IP or CIDR per line. Only these IPs can use this key when set.', + ipBlacklist: 'IP Blacklist', + ipBlacklistPlaceholder: '1.2.3.4\n5.6.0.0/16', + ipBlacklistHint: 'One IP or CIDR per line. These IPs will be blocked from using this key.', + ipRestrictionEnabled: 'IP restriction enabled', + ccSwitchNotInstalled: 'CC-Switch is not installed or the protocol handler is not registered. Please install CC-Switch first or manually copy the API key.', + ccsClientSelect: { + title: 'Select Client', + description: 'Please select the client type to import to CC-Switch:', + claudeCode: 'Claude Code', + claudeCodeDesc: 'Import as Claude Code configuration', + geminiCli: 'Gemini CLI', + geminiCliDesc: 'Import as Gemini CLI configuration', + }, + // Quota and expiration + quotaLimit: 'Quota Limit', + quotaAmount: 'Quota Amount (USD)', + quotaAmountPlaceholder: 'Enter quota limit in USD', + quotaAmountHint: 'Set the maximum amount this key can spend. 0 = unlimited.', + quotaUsed: 'Quota Used', + reset: 'Reset', + resetQuotaUsed: 'Reset used quota to 0', + resetQuotaTitle: 'Confirm Reset Quota', + resetQuotaConfirmMessage: 'Are you sure you want to reset the used quota (${used}) for key "{name}" to 0? This action cannot be undone.', + quotaResetSuccess: 'Quota reset successfully', + failedToResetQuota: 'Failed to reset quota', + rateLimitColumn: 'Rate Limit', + rateLimitSection: 'Rate Limit', + resetUsage: 'Reset', + rateLimit5h: '5-Hour Limit (USD)', + rateLimit1d: 'Daily Limit (USD)', + rateLimit7d: '7-Day Limit (USD)', + rateLimitHint: 'Set the maximum spending for this key within each time window. 0 = unlimited.', + rateLimitUsage: 'Rate Limit Usage', + resetRateLimitUsage: 'Reset Rate Limit Usage', + resetRateLimitTitle: 'Confirm Reset Rate Limit', + resetRateLimitConfirmMessage: 'Are you sure you want to reset the rate limit usage for key "{name}"? All time window usage will be reset to zero. This action cannot be undone.', + rateLimitResetSuccess: 'Rate limit usage reset successfully', + failedToResetRateLimit: 'Failed to reset rate limit usage', + resetNow: 'Resetting soon', + expiration: 'Expiration', + expiresInDays: '{days} days', + extendDays: '+{days} days', + customDate: 'Custom', + expirationDate: 'Expiration Date', + expirationDateHint: 'Select when this API key should expire.', + currentExpiration: 'Current expiration', + expiresAt: 'Expires', + noExpiration: 'Never', + status: { + active: 'Active', + inactive: 'Inactive', + quota_exhausted: 'Quota Exhausted', + expired: 'Expired', + }, + }, + + // Usage + usage: { + title: 'Usage Records', + description: 'View and analyze your API usage history', + costDetails: 'Cost Breakdown', + tokenDetails: 'Token Breakdown', + cacheTtlOverriddenHint: 'Cache TTL Override enabled', + cacheTtlOverriddenLabel: 'TTL Override', + cacheTtlOverridden5m: 'Billed as 5m', + cacheTtlOverridden1h: 'Billed as 1h', + totalRequests: 'Total Requests', + totalTokens: 'Total Tokens', + cacheTotal: 'Cache', + cacheBreakdown: 'Cache Token Breakdown', + cacheCreationTokensLabel: 'Cache Creation', + cacheReadTokensLabel: 'Cache Read', + totalCost: 'Total Cost', + standardCost: 'Standard', + actualCost: 'Actual', + accountCost: 'Cost', + userBilled: 'User billed', + accountBilled: 'Account billed', + resetNow: 'Now', + resetPending: 'Pending refresh', + accountMultiplier: 'Account rate', + avgDuration: 'Avg Duration', + inSelectedRange: 'in selected range', + perRequest: 'per request', + apiKeyFilter: 'API Key', + allApiKeys: 'All API Keys', + timeRange: 'Time Range', + exportCsv: 'Export CSV', + exportExcel: 'Export Excel', + exportingProgress: 'Exporting data...', + exportedCount: 'Exported {current}/{total} records', + estimatedTime: 'Estimated time remaining: {time}', + cancelExport: 'Cancel Export', + exportCancelled: 'Export cancelled', + exporting: 'Exporting...', + preparingExport: 'Preparing export...', + model: 'Model', + requestedModel: 'Requested', + upstreamModel: 'Upstream', + reasoningEffort: 'Reasoning Effort', + endpoint: 'Endpoint', + endpointDistribution: 'Endpoint Distribution', + inbound: 'Inbound', + upstream: 'Upstream', + mapping: 'Mapping', + path: 'Path', + inboundEndpoint: 'Inbound Endpoint', + upstreamEndpoint: 'Upstream Endpoint', + type: 'Type', + tokens: 'Tokens', + cost: 'Cost', + firstToken: 'First Token', + duration: 'Duration', + time: 'Time', + ws: 'WS', + stream: 'Stream', + sync: 'Sync', + cyber: 'Cyber', + unknown: 'Unknown', + in: 'In', + out: 'Out', + cacheHit: 'Cache hit', + cacheCreate: 'Cache create', + cacheHitRate: 'Cache hit rate', + inputTokenPrice: 'Input price', + outputTokenPrice: 'Output price', + perMillionTokens: '/ 1M tokens', + unitPrice: 'Per-request price', + imageUnitPrice: 'Per-image price', + imageTotalPrice: 'Image total price', + imageCount: 'Image count', + imageBillingSize: 'Billing size', + imageInputSize: 'Input size', + imageOutputSize: 'Output size', + imageOutputTokens: 'Image Output Tokens', + imageOutputTokenPrice: 'Image Output Price', + imageOutputCost: 'Image Output Cost', + imageSizeSource: 'Size source', + imageSizeBreakdown: 'Size breakdown', + imageSizeSourceOutput: 'Upstream output', + imageSizeSourceInput: 'Request input', + imageSizeSourceDefault: 'Default billing tier', + imageSizeSourceLegacy: 'Legacy record', + imageSizeSourceMissing: 'Not recorded', + imageSizeNotRecorded: 'not recorded', + imageSizeLegacyUnstandardized: 'legacy unstandardized', + imageSizeUnknown: 'unknown', + cacheRead: 'Read', + cacheWrite: 'Write', + serviceTier: 'Service tier', + serviceTierPriority: 'Fast', + serviceTierFlex: 'Flex', + serviceTierStandard: 'Standard', + rate: 'Rate', + original: 'Original', + billed: 'Billed', + noRecords: 'No usage records found. Try adjusting your filters.', + failedToLoad: 'Failed to load usage logs', + noDataToExport: 'No data to export', + exportSuccess: 'Usage data exported successfully', + exportFailed: 'Failed to export usage data', + exportExcelSuccess: 'Usage data exported successfully (Excel format)', + exportExcelFailed: 'Failed to export usage data', + imageUnit: ' images', + userAgent: 'User-Agent', + ipGeo: { + fetch: 'Fetch region', + fetching: 'Fetching...', + failed: 'Failed', + private: 'Private address', + refreshTitle: 'Refresh region info', + batchFetch: 'Batch fetch regions', + batchFetching: 'Fetching...', + pending: '{count} IPs pending', + batchFailed: 'Failed to batch fetch IP regions', + detailOrg: 'ISP', + detailTimezone: 'Timezone', + detailAccuracy: 'Accuracy', + detailCoordinates: 'Coordinates', + }, + tabs: { usage: 'Usage', errors: 'Error Requests' }, + errors: { + time: 'Time', model: 'Model', endpoint: 'Endpoint', status: 'Status', + category: 'Category', platform: 'Platform', message: 'Message', + keyName: 'Key Name', keyDeleted: 'Deleted', allKeys: 'All keys', + modelPlaceholder: 'Search model', allCategories: 'All categories', allStatuses: 'All status codes', + empty: 'No error requests', failedToLoad: 'Failed to load error requests', + categories: { + auth: 'Auth failed', rate_limit: 'Rate limited', quota: 'Balance/Subscription', + invalid_request: 'Invalid request', service_unavailable: 'Service unavailable', + upstream: 'Upstream error', internal: 'Platform error', other: 'Other', cyber: 'Cyber policy', + }, + detail: { + title: 'Error Request Detail', + responseBody: 'Response Body', + upstreamStatus: 'Upstream Status', + loadFailed: 'Failed to load detail, please try again', + }, + }, + }, + + // Shared keys for channel monitor (admin + user views) + monitorCommon: { + status: { + operational: 'Operational', + degraded: 'Degraded', + failed: 'Failed', + error: 'Error', + unknown: '-' + }, + providers: { + openai: 'OpenAI', + anthropic: 'Anthropic', + gemini: 'Gemini' + }, + extraModelsHeader: 'Extra Models', + extraModelsEmpty: 'No extra models', + latencyEmpty: '-', + availabilityPrefix: 'Availability', + dialogLatency: 'Dialog Latency', + endpointPing: 'Endpoint PING', + history60pts: 'HISTORY ({n} PTS)', + nextUpdateIn: 'NEXT UPDATE IN {n}s', + past: 'PAST', + now: 'NOW', + maintenancePaused: 'Maintenance · timeline paused', + extraModelsCount: '+ {n} models', + pollEvery: '{n}s polling', + updatedAt: 'Updated {time}', + relativeSecondsAgo: '{n}s ago', + relativeMinutesAgo: '{n}m ago', + relativeHoursAgo: '{n}h ago', + relativeDaysAgo: '{n}d ago' + }, + + // Channel Status (user-facing read-only view) + channelStatus: { + title: 'Channel Status', + description: 'Inspect channel availability, latency and recent status', + searchPlaceholder: 'Search channels...', + allProviders: 'All Providers', + loadError: 'Failed to load channel status', + detailLoadError: 'Failed to load channel detail', + detailTitle: 'Channel Detail', + closeDetail: 'Close', + windowTab: { + '7d': '7 days', + '15d': '15 days', + '30d': '30 days' + }, + overall: { + operational: 'OPERATIONAL', + degraded: 'DEGRADED', + unavailable: 'UNAVAILABLE' + }, + columns: { + name: 'Name', + provider: 'Provider', + groupName: 'Group', + primaryModel: 'Primary Model', + availability7d: '7d Availability', + latency: 'Latency (ms)' + }, + detailColumns: { + model: 'Model', + latestStatus: 'Latest Status', + latestLatency: 'Latest Latency (ms)', + availability7d: '7d Availability', + availability15d: '15d Availability', + availability30d: '30d Availability', + avgLatency7d: '7d Avg Latency (ms)' + }, + empty: { + title: 'No channels available', + description: 'No monitored channels have been configured yet.' + } + }, + + // Available Channels (user-facing) + availableChannels: { + title: 'Available Channels', + description: 'Channels you can access, along with their supported models and pricing', + searchPlaceholder: 'Search channels or models...', + empty: 'No available channels', + noModels: 'No models configured', + noPricing: 'Pricing not configured', + exclusive: 'Exclusive', + public: 'Public', + exclusiveTooltip: 'Exclusive groups granted to you by an admin', + publicTooltip: 'Groups open to all users', + columns: { + name: 'Channel', + description: 'Description', + platform: 'Platform', + groups: 'Your Accessible Groups', + supportedModels: 'Supported Models' + }, + pricing: { + billingMode: 'Billing Mode', + billingModeToken: 'Per Token', + billingModePerRequest: 'Per Request', + billingModeImage: 'Per Image', + inputPrice: 'Input', + outputPrice: 'Output', + cacheWritePrice: 'Cache Write', + cacheReadPrice: 'Cache Read', + imageOutputPrice: 'Image Output', + perRequestPrice: 'Per Request', + intervals: 'Tiered Pricing', + unitPerMillion: '/ 1M tokens', + unitPerRequest: '/ request' + } + }, + + affiliate: { + title: 'Affiliate Rebates', + description: 'Invite new users and convert your rebate quota into account balance', + yourCode: 'Your Affiliate Code', + inviteLink: 'Invite Link', + copyCode: 'Copy Code', + copyLink: 'Copy Link', + codeCopied: 'Affiliate code copied', + linkCopied: 'Invite link copied', + loadFailed: 'Failed to load affiliate data', + transferFailed: 'Failed to transfer affiliate quota', + stats: { + rebateRate: 'My Rebate Rate', + rebateRateHint: 'What you earn each time an invitee recharges', + invitedUsers: 'Invited Users', + availableQuota: 'Available Rebate Quota', + frozenQuota: 'Frozen', + frozenQuotaHint: 'Recently earned rebates pending release', + totalQuota: 'Historical Rebate Quota' + }, + transfer: { + title: 'Transfer Rebate Quota', + description: 'Move available rebate quota into your account balance', + button: 'Transfer to Balance', + transferring: 'Transferring...', + empty: 'No available rebate quota', + success: '{amount} has been transferred to your balance' + }, + invitees: { + title: 'Invited Users', + empty: 'No invited users yet', + columns: { + email: 'Email', + username: 'Username', + rebate: 'Rebate', + joinedAt: 'Joined At' + } + }, + tips: { + title: 'How It Works', + line1: 'Share your affiliate code or invite link with new users.', + line2: 'When invitees recharge, you receive {rate} of the recharge as rebate quota.', + line3: 'Transfer rebate quota to balance at any time.', + line4: 'Newly earned rebates may have a waiting period before they can be transferred.' + } + }, + + // Redeem + redeem: { + title: 'Redeem Code', + description: 'Enter your redeem code to add balance or increase concurrency', + currentBalance: 'Current Balance', + concurrency: 'Concurrency', + requests: 'requests', + redeemCodeLabel: 'Redeem Code', + redeemCodePlaceholder: 'Enter your redeem code', + redeemCodeHint: 'Redeem codes are case-sensitive', + redeeming: 'Redeeming...', + redeemButton: 'Redeem Code', + redeemSuccess: 'Code Redeemed Successfully!', + redeemFailed: 'Redemption Failed', + added: 'Added', + concurrentRequests: 'concurrent requests', + newBalance: 'New Balance', + newConcurrency: 'New Concurrency', + aboutCodes: 'About Redeem Codes', + codeRule1: 'Each code can only be used once', + codeRule2: 'Codes may add balance, increase concurrency, or grant trial access', + codeRule3: 'Contact support if you have issues redeeming a code', + codeRule4: 'Balance and concurrency updates are immediate', + recentActivity: 'Recent Activity', + historyWillAppear: 'Your redemption history will appear here', + balanceAddedRedeem: 'Balance Added (Redeem)', + balanceAddedAffiliate: 'Balance Added (Affiliate Transfer)', + balanceAddedAdmin: 'Balance Added (Admin)', + balanceDeductedAdmin: 'Balance Deducted (Admin)', + concurrencyAddedRedeem: 'Concurrency Added (Redeem)', + concurrencyAddedAdmin: 'Concurrency Added (Admin)', + concurrencyReducedAdmin: 'Concurrency Reduced (Admin)', + adminAdjustment: 'Admin Adjustment', + subscriptionAssigned: 'Subscription Assigned', + subscriptionAssignedDesc: 'You have been granted access to {groupName}', + subscriptionDays: '{days} days', + days: ' days', + codeRedeemSuccess: 'Code redeemed successfully!', + failedToRedeem: 'Failed to redeem code. Please check the code and try again.', + subscriptionRefreshFailed: 'Redeemed successfully, but failed to refresh subscription status.', + pleaseEnterCode: 'Please enter a redeem code' + }, + + // Profile + profile: { + title: 'Profile Settings', + description: 'Manage your account information and settings', + accountBalance: 'Account Balance', + concurrencyLimit: 'Concurrency Limit', + rpmLimit: 'RPM Limit', + rpmUnlimited: 'Unlimited', + memberSince: 'Member Since', + overviewTitle: 'Account Overview', + overviewDescription: 'Check account status, profile sources, and common actions at a glance.', + basicsTitle: 'Profile & Avatar', + basicsDescription: 'Keep your public profile details and avatar aligned.', + linkedProfileSources: 'Profile Sources', + linkedProfileSourcesDescription: 'Some profile details may stay synced from third-party sign-in methods.', + securityTitle: 'Security Settings', + securityDescription: 'Password, two-factor authentication, and alerts live in the right rail.', + administrator: 'Administrator', + user: 'User', + username: 'Username', + email: 'Email', + status: 'Status', + role: 'Role', + enterUsername: 'Enter username', + editProfile: 'Edit Profile', + updateProfile: 'Update Profile', + updating: 'Updating...', + updateSuccess: 'Profile updated successfully', + updateFailed: 'Failed to update profile', + usernameRequired: 'Username is required', + changePassword: 'Change Password', + currentPassword: 'Current Password', + newPassword: 'New Password', + confirmNewPassword: 'Confirm New Password', + passwordHint: 'Password must be at least 8 characters long', + changingPassword: 'Changing...', + changePasswordButton: 'Change Password', + passwordsNotMatch: 'New passwords do not match', + passwordTooShort: 'Password must be at least 8 characters long', + passwordChangeSuccess: 'Password changed successfully', + passwordChangeFailed: 'Failed to change password', + // TOTP 2FA + totp: { + title: 'Two-Factor Authentication (2FA)', + description: 'Enhance account security with Google Authenticator or similar apps', + enabled: 'Enabled', + enabledAt: 'Enabled at', + notEnabled: 'Not Enabled', + notEnabledHint: 'Enable two-factor authentication to enhance account security', + enable: 'Enable', + disable: 'Disable', + featureDisabled: 'Feature Unavailable', + featureDisabledHint: 'Two-factor authentication has not been enabled by the administrator', + setupTitle: 'Set Up Two-Factor Authentication', + setupStep1: 'Scan the QR code below with your authenticator app', + setupStep2: 'Enter the 6-digit code from your app', + manualEntry: "Can't scan? Enter the key manually:", + enterCode: 'Enter 6-digit code', + verify: 'Verify', + setupFailed: 'Failed to get setup information', + verifyFailed: 'Invalid code, please try again', + enableSuccess: 'Two-factor authentication enabled', + disableTitle: 'Disable Two-Factor Authentication', + disableWarning: 'After disabling, you will no longer need a verification code to log in. This may reduce your account security.', + enterPassword: 'Enter your current password to confirm', + confirmDisable: 'Confirm Disable', + disableSuccess: 'Two-factor authentication disabled', + disableFailed: 'Failed to disable, please check your password', + loginTitle: 'Two-Factor Authentication', + loginHint: 'Enter the 6-digit code from your authenticator app', + loginFailed: 'Verification failed, please try again', + // New translations for email verification + verifyEmailFirst: 'Please verify your email first', + verifyPasswordFirst: 'Please verify your identity first', + emailCode: 'Email Verification Code', + enterEmailCode: 'Enter 6-digit code', + sendCode: 'Send Code', + codeSent: 'Verification code sent to your email', + sendCodeFailed: 'Failed to send verification code' + }, + balanceNotify: { + title: 'Balance Low Notification', + description: 'Send email alert when account balance falls below threshold', + enabled: 'Enable Balance Low Notification', + threshold: 'Custom Threshold', + thresholdHint: 'Leave empty to use system default', + thresholdPlaceholder: 'Enter amount', + systemDefault: 'System Default', + extraEmails: 'Notification Emails', + extraEmailsHint: 'You must add and verify an email address to receive low balance alerts', + primaryEmail: 'Primary', + noExtraEmails: 'No extra notification emails', + enterEmail: 'Enter email address', + addEmail: 'Add Email', + emailPlaceholder: 'Enter email address', + sendCode: 'Send Code', + resend: 'Resend', + codeSent: 'Verification code sent', + codeSentTo: 'Code sent to {email}', + enterCode: 'Enter verification code', + codePlaceholder: '6-digit code', + verify: 'Verify', + emailAdded: 'Email added', + emailRemoved: 'Email removed', + verifySuccess: 'Email added successfully', + removeEmail: 'Remove', + removeSuccess: 'Email removed', + emailDuplicate: 'This email already exists', + maxEmailsReached: 'Maximum number of notification emails reached', + unverified: 'Unverified', + verified: 'Verified', + }, + avatar: { + title: 'Profile Avatar', + description: 'Upload an avatar image. Static uploads are compressed to 20KB before saving.', + uploadAction: 'Upload image', + uploadHint: 'Static uploads are compressed to 20KB when possible. GIF uploads must already be within 20KB.', + uploadRequired: 'Upload an avatar image first', + saveSuccess: 'Avatar updated', + deleteSuccess: 'Avatar removed', + invalidType: 'Please choose an image file', + gifTooLarge: 'GIF avatars must already be 20KB or smaller', + compressTooLarge: 'Unable to compress this image below 20KB. Try a smaller image.', + compressFailed: 'Failed to compress the selected image.', + readFailed: 'Failed to read the selected image.', + emptyDeleteHint: 'Avatar is already empty', + }, + authBindings: { + title: 'Connected Sign-In Methods', + description: 'View current bindings and connect another provider to this account.', + bindAction: 'Bind {providerName}', + bindSuccess: 'Account linked successfully', + emailPlaceholder: 'Enter email address', + codePlaceholder: 'Enter verification code', + passwordPlaceholder: 'Set a login password', + replaceEmailPasswordPlaceholder: 'Enter current password', + sendCodeAction: 'Send code', + manageEmailAction: 'Manage email', + hideEmailFormAction: 'Hide email form', + confirmEmailBindAction: 'Bind email', + confirmEmailReplaceAction: 'Replace primary email', + codeSentTo: 'Code sent to {email}', + replaceSuccess: 'Primary email updated', + unbindAction: 'Unbind', + unbindSuccess: '{providerName} unbound', + boundCount: '{count} linked records', + status: { + bound: 'Bound', + notBound: 'Not bound', + }, + providers: { + email: 'Email', + linuxdo: 'LinuxDo', + dingtalk: 'DingTalk', + oidc: '{providerName}', + wechat: 'WeChat', + }, + notes: { + emailManagedFromProfile: 'Primary email is managed in the profile form', + canUnbind: 'You can unbind this sign-in method', + bindAnotherBeforeUnbind: 'Bind another sign-in method before unbinding', + }, + source: { + avatar: 'Avatar is currently synced from {providerName}', + username: 'Nickname is currently synced from {providerName}', + }, + } + }, + + // Empty States + empty: { + noData: 'No data found' + }, + + // Table + table: { + expandActions: 'Expand More Actions', + collapseActions: 'Collapse Actions' + }, + + // Pagination + pagination: { + showing: 'Showing', + to: 'to', + of: 'of', + results: 'results', + page: 'Page', + pageOf: 'Page {page} of {total}', + previous: 'Previous', + next: 'Next', + perPage: 'Per page', + goToPage: 'Go to page {page}', + jumpTo: 'Jump to', + jumpPlaceholder: 'Page', + jumpAction: 'Go' + }, + + // Errors + errors: { + somethingWentWrong: 'Something went wrong', + pageNotFound: 'Page not found', + unauthorized: 'Unauthorized', + forbidden: 'Forbidden', + serverError: 'Server error', + networkError: 'Network error', + timeout: 'Request timeout', + tryAgain: 'Please try again' + }, + + // Dates + dates: { + today: 'Today', + yesterday: 'Yesterday', + thisWeek: 'This Week', + lastWeek: 'Last Week', + thisMonth: 'This Month', + lastMonth: 'Last Month', + last24Hours: 'Last 24 Hours', + last7Days: 'Last 7 Days', + last14Days: 'Last 14 Days', + last30Days: 'Last 30 Days', + custom: 'Custom', + startDate: 'Start Date', + endDate: 'End Date', + apply: 'Apply', + selectDateRange: 'Select date range' + }, + + // Admin +} diff --git a/frontend/src/i18n/locales/en/index.ts b/frontend/src/i18n/locales/en/index.ts new file mode 100644 index 0000000000..377c67aec7 --- /dev/null +++ b/frontend/src/i18n/locales/en/index.ts @@ -0,0 +1,13 @@ +import landing from './landing' +import common from './common' +import dashboard from './dashboard' +import admin from './admin' +import misc from './misc' + +export default { + ...landing, + ...common, + ...dashboard, + admin, + ...misc, +} diff --git a/frontend/src/i18n/locales/en/landing.ts b/frontend/src/i18n/locales/en/landing.ts new file mode 100644 index 0000000000..afbecf4861 --- /dev/null +++ b/frontend/src/i18n/locales/en/landing.ts @@ -0,0 +1,255 @@ +export default { + batchImageGuide: { + title: 'Batch Image Generation', + description: 'Submit multiple prompts in one job and download the generated images when complete' + }, + // Home Page + home: { + viewOnGithub: 'View on GitHub', + viewDocs: 'View Documentation', + docs: 'Docs', + switchToLight: 'Switch to Light Mode', + switchToDark: 'Switch to Dark Mode', + dashboard: 'Dashboard', + login: 'Login', + getStarted: 'Get Started', + goToDashboard: 'Go to Dashboard', + // User-focused value proposition + heroSubtitle: 'One Key, All AI Models', + heroDescription: 'No need to manage multiple subscriptions. Access Claude, GPT, Gemini and more with a single API key', + tags: { + subscriptionToApi: 'Subscription to API', + stickySession: 'Session Persistence', + realtimeBilling: 'Pay As You Go' + }, + // Pain points section + painPoints: { + title: 'Sound Familiar?', + items: { + expensive: { + title: 'High Subscription Costs', + desc: 'Paying for multiple AI subscriptions that add up every month' + }, + complex: { + title: 'Account Chaos', + desc: 'Managing scattered accounts and API keys across different platforms' + }, + unstable: { + title: 'Service Interruptions', + desc: 'Single accounts hitting rate limits and disrupting your workflow' + }, + noControl: { + title: 'No Usage Control', + desc: "Can't track where your money goes or limit team member usage" + } + } + }, + // Solutions section + solutions: { + title: 'We Solve These Problems', + subtitle: 'Three simple steps to stress-free AI access' + }, + features: { + unifiedGateway: 'One-Click Access', + unifiedGatewayDesc: 'Get a single API key to call all connected AI models. No separate applications needed.', + multiAccount: 'Always Reliable', + multiAccountDesc: 'Smart routing across multiple upstream accounts with automatic failover. Say goodbye to errors.', + balanceQuota: 'Pay What You Use', + balanceQuotaDesc: 'Usage-based billing with quota limits. Full visibility into team consumption.' + }, + // Comparison section + comparison: { + title: 'Why Choose Us?', + headers: { + feature: 'Comparison', + official: 'Official Subscriptions', + us: 'Our Platform' + }, + items: { + pricing: { + feature: 'Pricing', + official: 'Fixed monthly fee, pay even if unused', + us: 'Pay only for what you use' + }, + models: { + feature: 'Model Selection', + official: 'Single provider only', + us: 'Switch between models freely' + }, + management: { + feature: 'Account Management', + official: 'Manage each service separately', + us: 'Unified key, one dashboard' + }, + stability: { + feature: 'Stability', + official: 'Single account rate limits', + us: 'Multi-account pool, auto-failover' + }, + control: { + feature: 'Usage Control', + official: 'Not available', + us: 'Quotas & detailed analytics' + } + } + }, + providers: { + title: 'Supported AI Models', + description: 'One API, Multiple Choices', + supported: 'Supported', + soon: 'Soon', + claude: 'Claude', + gemini: 'Gemini', + antigravity: 'Antigravity', + more: 'More' + }, + // CTA section + cta: { + title: 'Ready to Get Started?', + description: 'Sign up now and get free trial credits to experience seamless AI access', + button: 'Sign Up Free' + }, + footer: { + allRightsReserved: 'All rights reserved.' + } + }, + + // Key Usage Query Page + keyUsage: { + title: 'API Key Usage', + subtitle: 'Enter your API Key to view real-time spending and usage status', + placeholder: 'sk-ant-mirror-xxxxxxxxxxxx', + query: 'Query', + querying: 'Querying...', + privacyNote: 'Your Key is processed locally in the browser and will not be stored', + dateRange: 'Date Range:', + dateRangeToday: 'Today', + dateRange7d: '7 Days', + dateRange30d: '30 Days', + dateRange90d: '90 Days', + dateRangeCustom: 'Custom', + apply: 'Apply', + used: 'Used', + detailInfo: 'Detail Information', + tokenStats: 'Token Statistics', + dailyDetail: 'Daily Detail', + modelStats: 'Model Usage Statistics', + // Table headers + date: 'Date', + model: 'Model', + requests: 'Requests', + inputTokens: 'Input Tokens', + outputTokens: 'Output Tokens', + cacheCreationTokens: 'Cache Creation', + cacheReadTokens: 'Cache Read', + cacheWriteTokens: 'Cache Write', + totalTokens: 'Total Tokens', + cost: 'Cost', + // Status + quotaMode: 'Key Quota Mode', + walletBalance: 'Wallet Balance', + // Ring card titles + totalQuota: 'Total Quota', + limit5h: '5-Hour Limit', + limitDaily: 'Daily Limit', + limit7d: '7-Day Limit', + limitWeekly: 'Weekly Limit', + limitMonthly: 'Monthly Limit', + // Detail rows + remainingQuota: 'Remaining Quota', + expiresAt: 'Expires At', + todayExpires: '(expires today)', + daysLeft: '({days} days)', + usedQuota: 'Used Quota', + resetNow: 'Resetting soon', + subscriptionType: 'Subscription Type', + subscriptionExpires: 'Subscription Expires', + // Usage stat cells + todayRequests: 'Today Requests', + todayInputTokens: 'Today Input', + todayOutputTokens: 'Today Output', + todayTokens: 'Today Tokens', + todayCacheCreation: 'Today Cache Creation', + todayCacheRead: 'Today Cache Read', + todayCost: 'Today Cost', + rpmTpm: 'RPM / TPM', + totalRequests: 'Total Requests', + totalInputTokens: 'Total Input', + totalOutputTokens: 'Total Output', + totalTokensLabel: 'Total Tokens', + totalCacheCreation: 'Total Cache Creation', + totalCacheRead: 'Total Cache Read', + totalCost: 'Total Cost', + avgDuration: 'Avg Duration', + // Messages + enterApiKey: 'Please enter an API Key', + querySuccess: 'Query successful', + queryFailed: 'Query failed', + queryFailedRetry: 'Query failed, please try again later', + noDailyUsage: 'No daily usage data', + }, + + // Setup Wizard + setup: { + title: 'Sub2API Setup', + description: 'Configure your Sub2API instance', + database: { + title: 'Database Configuration', + description: 'Connect to your PostgreSQL database', + host: 'Host', + port: 'Port', + username: 'Username', + password: 'Password', + databaseName: 'Database Name', + sslMode: 'SSL Mode', + passwordPlaceholder: 'Password', + ssl: { + disable: 'Disable', + require: 'Require', + verifyCa: 'Verify CA', + verifyFull: 'Verify Full' + } + }, + redis: { + title: 'Redis Configuration', + description: 'Connect to your Redis server', + host: 'Host', + port: 'Port', + password: 'Password (optional)', + database: 'Database', + passwordPlaceholder: 'Password', + enableTls: 'Enable TLS', + enableTlsHint: 'Use TLS when connecting to Redis (public CA certs)' + }, + admin: { + title: 'Admin Account', + description: 'Create your administrator account', + email: 'Email', + password: 'Password', + confirmPassword: 'Confirm Password', + passwordPlaceholder: 'Min 8 characters', + confirmPasswordPlaceholder: 'Confirm password', + passwordMismatch: 'Passwords do not match' + }, + ready: { + title: 'Ready to Install', + description: 'Review your configuration and complete setup', + database: 'Database', + redis: 'Redis', + adminEmail: 'Admin Email' + }, + status: { + testing: 'Testing...', + success: 'Connection Successful', + testConnection: 'Test Connection', + installing: 'Installing...', + completeInstallation: 'Complete Installation', + completed: 'Installation completed!', + redirecting: 'Redirecting to login page...', + restarting: 'Service is restarting, please wait...', + timeout: 'Service restart is taking longer than expected. Please refresh the page manually.' + } + }, + + // Common +} diff --git a/frontend/src/i18n/locales/en/misc.ts b/frontend/src/i18n/locales/en/misc.ts new file mode 100644 index 0000000000..e378a46cf3 --- /dev/null +++ b/frontend/src/i18n/locales/en/misc.ts @@ -0,0 +1,591 @@ +export default { + + // Subscription Progress (Header component) + subscriptionProgress: { + title: 'My Subscriptions', + viewDetails: 'View subscription details', + activeCount: '{count} active subscription(s)', + daily: 'Daily', + weekly: 'Weekly', + monthly: 'Monthly', + daysRemaining: '{days} days left', + expired: 'Expired', + expiresToday: 'Expires today', + expiresTomorrow: 'Expires tomorrow', + viewAll: 'View all subscriptions', + noSubscriptions: 'No active subscriptions', + unlimited: 'Unlimited' + }, + + // Version Badge + version: { + currentVersion: 'Current Version', + latestVersion: 'Latest Version', + upToDate: "You're running the latest version.", + updateAvailable: 'A new version is available!', + releaseNotes: 'Release Notes', + noReleaseNotes: 'No release notes', + viewUpdate: 'View Update', + viewRelease: 'View Release', + viewChangelog: 'View Changelog', + refresh: 'Refresh', + sourceMode: 'Source Build', + sourceModeHint: 'Source build, use git pull to update', + updateNow: 'Update Now', + updating: 'Updating...', + updateComplete: 'Update Complete', + updateFailed: 'Update Failed', + restartRequired: 'Please restart the service to apply the update', + restartNow: 'Restart Now', + restarting: 'Restarting...', + retry: 'Retry' + }, + + // Recharge / Subscription Page + purchase: { + title: 'Recharge / Subscription', + description: 'Recharge balance or purchase subscription via the embedded page', + openInNewTab: 'Open in new tab', + notEnabledTitle: 'Feature not enabled', + notEnabledDesc: 'The administrator has not enabled the recharge/subscription entry. Please contact admin.', + notConfiguredTitle: 'Recharge / Subscription URL not configured', + notConfiguredDesc: + 'The administrator enabled the entry but has not configured a recharge/subscription URL. Please contact admin.' + }, + + // Custom Page (iframe embed) + customPage: { + title: 'Custom Page', + openInNewTab: 'Open in new tab', + notFoundTitle: 'Page not found', + notFoundDesc: 'This custom page does not exist or has been removed.', + notConfiguredTitle: 'Page URL not configured', + notConfiguredDesc: 'The URL for this custom page has not been properly configured.', + tableOfContents: 'Contents', + copyCode: 'Copy', + copiedCode: 'Copied', + copyCodeFailed: 'Failed' + }, + + // Announcements Page + announcements: { + title: 'Announcements', + description: 'View system announcements', + unreadOnly: 'Show unread only', + markRead: 'Mark as read', + markAllRead: 'Mark all as read', + viewAll: 'View all announcements', + markedAsRead: 'Marked as read', + allMarkedAsRead: 'All announcements marked as read', + newCount: '{count} new announcement | {count} new announcements', + readAt: 'Read at', + read: 'Read', + unread: 'Unread', + startsAt: 'Starts at', + endsAt: 'Ends at', + empty: 'No announcements', + emptyUnread: 'No unread announcements', + total: 'announcements', + emptyDescription: 'There are no system announcements at this time', + readStatus: 'You have read this announcement', + markReadHint: 'Click "Mark as read" to mark this announcement' + }, + + // User Subscriptions Page + userSubscriptions: { + title: 'My Subscriptions', + description: 'View your subscription plans and usage', + noActiveSubscriptions: 'No Active Subscriptions', + noActiveSubscriptionsDesc: + "You don't have any active subscriptions. Contact administrator to get one.", + failedToLoad: 'Failed to load subscriptions', + status: { + active: 'Active', + expired: 'Expired', + revoked: 'Revoked' + }, + usage: 'Usage', + expires: 'Expires', + noExpiration: 'No expiration', + unlimited: 'Unlimited', + unlimitedDesc: 'No usage limits on this subscription', + daily: 'Daily', + weekly: 'Weekly', + monthly: 'Monthly', + daysRemaining: '{days} days remaining', + expiresOn: 'Expires on {date}', + resetIn: 'Resets in {time}', + quotaEndsIn: 'Quota ends in {time}', + windowNotActive: 'Awaiting first use', + usageOf: '{used} of {limit}' + }, + + // Onboarding Tour + onboarding: { + restartTour: 'Restart Onboarding Tour', + dontShowAgain: "Don't show again", + dontShowAgainTitle: 'Permanently close onboarding guide', + confirmDontShow: "Are you sure you don't want to see the onboarding guide again?\n\nYou can restart it anytime from the user menu in the top right corner.", + confirmExit: 'Are you sure you want to exit the onboarding guide? You can restart it anytime from the top right menu.', + interactiveHint: 'Press Enter or Click to continue', + navigation: { + flipPage: 'Flip Page', + exit: 'Exit' + }, + // Admin tour steps + admin: { + welcome: { + title: '👋 Welcome to Sub2API', + description: '

Sub2API is a powerful AI service gateway platform that helps you easily manage and distribute AI services.

🎯 Core Features:

  • 📦 Group Management - Create service tiers (VIP, Free Trial, etc.)
  • 🔗 Account Pool - Connect multiple upstream AI service accounts
  • 🔑 Key Distribution - Generate independent API Keys for users
  • 💰 Billing Control - Flexible rate and quota management

Let\'s complete the initial setup in 3 minutes →

', + nextBtn: 'Start Setup 🚀', + prevBtn: 'Skip' + }, + groupManage: { + title: '📦 Step 1: Group Management', + description: '

What is a Group?

Groups are the core concept of Sub2API, like a "service package":

  • 🎯 Each group can contain multiple upstream accounts
  • 💰 Each group has independent billing multiplier
  • 👥 Can be set as public or exclusive

💡 Example: You can create "VIP Premium" (high rate) and "Free Trial" (low rate) groups

👉 Click "Group Management" on the left sidebar

' + }, + createGroup: { + title: '➕ Create New Group', + description: '

Let\'s create your first group.

📝 Tip: Recommend creating a test group first to familiarize yourself with the process

👉 Click the "Create Group" button

' + }, + groupName: { + title: '✏️ 1. Group Name', + description: '

Give your group an easy-to-identify name.

💡 Naming Suggestions:
  • "Test Group" - For testing
  • "VIP Premium" - High-quality service
  • "Free Trial" - Trial version

Click "Next" when done

', + nextBtn: 'Next' + }, + groupPlatform: { + title: '🤖 2. Select Platform', + description: '

Choose the AI platform this group supports.

📌 Platform Guide:
  • Anthropic - Claude models
  • OpenAI - GPT models
  • Google - Gemini models

One group can only have one platform

', + nextBtn: 'Next' + }, + groupMultiplier: { + title: '💰 3. Rate Multiplier', + description: '

Set the billing multiplier to control user charges.

⚙️ Billing Rules:
  • 1.0 - Original price (cost price)
  • 1.5 - User consumes $1, charged $1.5
  • 2.0 - User consumes $1, charged $2
  • 0.8 - Subsidy mode (loss-making)

Recommend setting test group to 1.0

', + nextBtn: 'Next' + }, + groupExclusive: { + title: '🔒 4. Exclusive Group (Optional)', + description: '

Control group visibility and access permissions.

🔐 Permission Guide:
  • Off - Public group, visible to all users
  • On - Exclusive group, only for specified users

💡 Use Cases: VIP exclusive, internal testing, special customers

', + nextBtn: 'Next' + }, + groupSubmit: { + title: '✅ Save Group', + description: '

Confirm the information and click create to save the group.

⚠️ Note: Platform type cannot be changed after creation, but other settings can be edited anytime

📌 Next Step: After creation, we\'ll add upstream accounts to this group

👉 Click "Create" button

' + }, + accountManage: { + title: '🔗 Step 2: Add Account', + description: '

Great! Group created successfully 🎉

Now add upstream AI service accounts to enable actual service delivery.

🔑 Account Purpose:
  • Connect to upstream AI services (Claude, GPT, etc.)
  • One group can contain multiple accounts (load balancing)
  • Supports OAuth and Session Key methods

👉 Click "Account Management" on the left sidebar

' + }, + createAccount: { + title: '➕ Add New Account', + description: '

Click the button to start adding your first upstream account.

💡 Tip: Recommend using OAuth method - more secure and no manual key extraction needed

👉 Click "Add Account" button

' + }, + accountName: { + title: '✏️ 1. Account Name', + description: '

Set an easy-to-identify name for the account.

💡 Naming Suggestions: "Claude Main", "GPT Backup 1", "Test Account", etc.

', + nextBtn: 'Next' + }, + accountPlatform: { + title: '🤖 2. Select Platform', + description: '

Choose the service provider platform for this account.

⚠️ Important: Platform must match the group you just created

', + nextBtn: 'Next' + }, + accountType: { + title: '🔐 3. Authorization Method', + description: '

Choose the account authorization method.

✅ Recommended: OAuth Method
  • No manual key extraction needed
  • More secure with auto-refresh support
  • Works with Claude Code, ChatGPT OAuth
📌 Session Key Method
  • Requires manual extraction from browser
  • May need periodic updates
  • For platforms without OAuth support
', + nextBtn: 'Next' + }, + accountPriority: { + title: '⚖️ 4. Priority (Optional)', + description: '

Set the account call priority.

📊 Priority Rules:
  • Lower number = higher priority
  • System uses low-value accounts first
  • Same priority = random selection

💡 Use Case: Set main account to lower value, backup accounts to higher value

', + nextBtn: 'Next' + }, + accountGroups: { + title: '🎯 5. Assign Groups', + description: '

Key Step! Assign the account to the group you just created.

⚠️ Important Reminder:
  • Must select at least one group
  • Unassigned accounts cannot be used
  • One account can be assigned to multiple groups

💡 Tip: Select the test group you just created

', + nextBtn: 'Next' + }, + accountSubmit: { + title: '✅ Save Account', + description: '

Confirm the information and click save.

📌 OAuth Flow:
  • Will redirect to service provider page after clicking save
  • Complete login and authorization on provider page
  • Auto-return after successful authorization

📌 Next Step: After adding account, we\'ll create an API key

👉 Click "Save" button

' + }, + keyManage: { + title: '🔑 Step 3: Generate Key', + description: '

Congratulations! Account setup complete 🎉

Final step: generate an API Key to test if the service works properly.

🔑 API Key Purpose:
  • Credential for calling AI services
  • Each key is bound to one group
  • Can set quota and expiration
  • Supports independent usage statistics

👉 Click "API Keys" on the left sidebar

' + }, + createKey: { + title: '➕ Create Key', + description: '

Click the button to create your first API Key.

💡 Tip: Copy and save immediately after creation - key is only shown once

👉 Click "Create Key" button

' + }, + keyName: { + title: '✏️ 1. Key Name', + description: '

Set an easy-to-manage name for the key.

💡 Naming Suggestions: "Test Key", "Production", "Mobile", etc.

', + nextBtn: 'Next' + }, + keyGroup: { + title: '🎯 2. Select Group', + description: '

Select the group you just configured.

📌 Group Determines:
  • Which accounts this key can use
  • What billing multiplier applies
  • Whether it\'s an exclusive key

💡 Tip: Select the test group you just created

', + nextBtn: 'Next' + }, + keySubmit: { + title: '🎉 Generate and Copy', + description: '

System will generate a complete API Key after clicking create.

⚠️ Important Reminder:
  • Key is only shown once, copy immediately
  • Need to regenerate if lost
  • Keep it safe, don\'t share with others
🚀 Next Steps:
  • Copy the generated sk-xxx key
  • Use in any OpenAI-compatible client
  • Start experiencing AI services!

👉 Click "Create" button

' + } + }, + // User tour steps + user: { + welcome: { + title: '👋 Welcome to Sub2API', + description: '

Hello! Welcome to the Sub2API AI service platform.

🎯 Quick Start:

  • 🔑 Create API Key
  • 📋 Copy key to your application
  • 🚀 Start using AI services

Just 1 minute, let\'s get started →

', + nextBtn: 'Start 🚀', + prevBtn: 'Skip' + }, + keyManage: { + title: '🔑 API Key Management', + description: '

Manage all your API access keys here.

📌 What is an API Key?
An API key is your credential for accessing AI services, like a key that allows your application to call AI capabilities.

👉 Click to enter key page

' + }, + createKey: { + title: '➕ Create New Key', + description: '

Click the button to create your first API key.

💡 Tip: Key is only shown once after creation, make sure to copy and save

👉 Click "Create Key"

' + }, + keyName: { + title: '✏️ Key Name', + description: '

Give your key an easy-to-identify name.

💡 Examples: "My First Key", "For Testing", etc.

', + nextBtn: 'Next' + }, + keyGroup: { + title: '🎯 Select Group', + description: '

Select the service group assigned by the administrator.

📌 Group Info:
Different groups may have different service quality and billing rates, choose according to your needs.

', + nextBtn: 'Next' + }, + keySubmit: { + title: '🎉 Complete Creation', + description: '

Click to confirm and create your API key.

⚠️ Important:
  • Copy the key (sk-xxx) immediately after creation
  • Key is only shown once, need to regenerate if lost

🚀 How to Use:
Configure the key in any OpenAI-compatible client (like ChatBox, OpenCat, etc.) and start using!

👉 Click "Create" button

' + } + } + }, + + // Payment System + payment: { + title: 'Recharge / Subscription', + amountLabel: 'Amount', + paymentAmount: 'Payment Amount', + creditedBalance: 'Credited Balance', + quickAmounts: 'Quick Amounts', + customAmount: 'Custom Amount', + enterAmount: 'Enter amount', + paymentMethod: 'Payment Method', + fee: 'Fee', + actualPay: 'Actual Payment', + createOrder: 'Confirm Payment', + methods: { + easypay: 'EasyPay', + alipay: 'Alipay', + wxpay: 'WeChat Pay', + stripe: 'Stripe', + airwallex: 'Airwallex', + card: 'Card', + link: 'Link', + alipay_direct: 'Alipay (Direct)', + wxpay_direct: 'WeChat Pay (Direct)', + }, + status: { + pending: 'Pending', + paid: 'Paid', + recharging: 'Recharging', + completed: 'Completed', + expired: 'Expired', + cancelled: 'Cancelled', + failed: 'Failed', + refund_requested: 'Refund Requested', + refunding: 'Refunding', + refund_pending: 'Refund Pending', + refunded: 'Refunded', + partially_refunded: 'Partially Refunded', + refund_failed: 'Refund Failed', + }, + qr: { + scanToPay: 'Scan to Pay', + scanAlipay: 'Alipay QR Payment', + scanWxpay: 'WeChat QR Payment', + scanAlipayHint: 'Open Alipay on your phone and scan the QR code to pay', + scanWxpayHint: 'Open WeChat on your phone and scan the QR code to pay', + payInNewWindow: 'Complete Payment in New Window', + payInNewWindowHint: 'The payment page has opened in a new window. Please complete the payment there and return to this page.', + openPayWindow: 'Reopen Payment Page', + expiresIn: 'Expires in', + expired: 'Order Expired', + expiredDesc: 'This order has expired. Please create a new one.', + cancelled: 'Order Cancelled', + cancelledDesc: 'You have cancelled this payment.', + waitingPayment: 'Waiting for payment...', + cancelOrder: 'Cancel Order', + }, + orders: { + title: 'My Orders', + empty: 'No orders yet', + orderId: 'Order ID', + orderNo: 'Order No.', + amount: 'Amount', + payAmount: 'Paid', + creditedAmount: 'Credited Amount', + fee: 'Fee', + baseAmount: 'Base Amount', + includedInPayAmount: 'included in paid amount', + status: 'Status', + paymentMethod: 'Payment Method', + createdAt: 'Created', + cancel: 'Cancel Order', + userId: 'User ID', + orderType: 'Order Type', + actions: 'Actions', + requestRefund: 'Request Refund', + }, + result: { + success: 'Payment Successful', + subscriptionSuccess: 'Subscription Successful', + processing: 'Payment Processing', + processingHint: 'Payment confirmation is still pending. This page will refresh automatically.', + failed: 'Payment Failed', + backToRecharge: 'Back to Recharge', + viewOrders: 'View Orders', + }, + currentBalance: 'Current Balance', + groupFallback: 'Group #{id}', + rechargeAccount: 'Recharge Account', + activeSubscription: 'Active Subscription', + noActiveSubscription: 'No active subscription', + tabTopUp: 'Top Up', + tabSubscribe: 'Subscribe', + noPlans: 'No subscription plans available', + notAvailable: 'Top-up is currently unavailable', + confirmSubscription: 'Confirm Subscription', + confirmCancel: 'Are you sure you want to cancel this order?', + amountTooLow: 'Minimum amount is {min}', + amountTooHigh: 'Maximum amount is {max}', + amountNoMethod: 'No payment method available for this amount', + rechargeRatePreview: 'Current rate: 1 CNY = {usd} USD', + refundReason: 'Refund Reason', + refundReasonPlaceholder: 'Please describe your refund reason', + stripeLoadFailed: 'Failed to load payment component. Please refresh and try again.', + stripeMissingParams: 'Missing order ID or client secret', + stripeNotConfigured: 'Stripe is not configured', + airwallexLoadFailed: 'Failed to load Airwallex payment component. Please refresh and try again.', + airwallexMissingParams: 'Missing Airwallex payment parameters', + errors: { + tooManyPending: 'Too many pending orders (max {max}). Please complete or cancel existing orders first.', + cancelRateLimited: 'Too many cancellations. Please try again later.', + wechatH5NotAuthorized: 'This merchant has not enabled WeChat H5 payment. Open this page in WeChat to continue.', + wechatPaymentMpNotConfigured: 'This site has not completed WeChat MP/JSAPI payment setup, so in-app WeChat payment is unavailable right now.', + wechatJsapiUnavailable: 'WeChat payment could not be invoked in the current environment. Reopen this page inside WeChat and try again.', + wechatJsapiFailed: 'WeChat payment did not complete. Try invoking it again or switch to QR payment.', + wechatUnavailable: 'WeChat payment is temporarily unavailable. Please try again later.', + wechatOpenInWeChatHint: 'Open the current page inside WeChat, or switch to desktop WeChat QR payment.', + wechatScanOnDesktopHint: 'On desktop, use WeChat Scan to pay; on mobile, reopen the current page inside WeChat.', + wechatSwitchBrowserHint: 'Switch to desktop WeChat QR payment, or reopen this page in an external browser and retry.', + mobilePaymentFallbackToQr: 'This merchant has not enabled mobile payment. The flow has been switched to QR payment automatically.', + alipayDesktopUnavailable: 'The desktop Alipay flow could not generate a QR code.', + alipayDesktopQrHint: 'Desktop Alipay should render a QR code. Refresh and retry, or make sure the payment page was not blocked.', + alipayMobileUnavailable: 'This page could not hand off to Alipay.', + alipayMobileOpenHint: 'Allow the current page to open the Alipay app, or retry from the system browser.', + // Structured error codes (reason strings from backend ApplicationError) + PAYMENT_DISABLED: 'Payment system is disabled.', + USER_INACTIVE: 'Your account is disabled.', + BALANCE_PAYMENT_DISABLED: 'Balance recharge has been disabled.', + INVALID_AMOUNT: 'Invalid amount.', + INVALID_INPUT: 'Invalid request.', + PLAN_NOT_AVAILABLE: 'Plan not found or no longer available.', + GROUP_NOT_FOUND: 'Subscription group is no longer available.', + GROUP_TYPE_MISMATCH: 'Group is not a subscription type.', + TOO_MANY_PENDING: 'Too many pending orders (max {max}). Please complete or cancel existing orders first.', + DAILY_LIMIT_EXCEEDED: 'Daily recharge limit reached. Remaining: {remaining}.', + PAYMENT_GATEWAY_ERROR: 'Payment method is unavailable.', + NO_AVAILABLE_INSTANCE: 'No payment channel available right now.', + PAYMENT_PROVIDER_MISCONFIGURED: 'Payment provider misconfigured. Please contact an administrator.', + WXPAY_CONFIG_MISSING_KEY: 'WeChat Pay config missing required key: {key}.', + WXPAY_CONFIG_INVALID_KEY_LENGTH: 'WeChat Pay {key} length is invalid (expected {expected} bytes, got {actual}).', + WXPAY_CONFIG_INVALID_KEY: 'WeChat Pay {key} is malformed. Make sure you copied the full PEM content.', + PENDING_ORDERS: 'This provider has pending orders. Please wait for them to complete before making changes.', + PAYMENT_PROVIDER_CONFLICT: 'Another enabled provider instance is already serving this payment method. Disable it before continuing.', + CANCEL_RATE_LIMITED: 'Too many cancellations. Please try again later.', + NOT_FOUND: 'Order not found.', + FORBIDDEN: 'No permission for this order.', + CONFLICT: 'Order status has changed. Please refresh.', + INVALID_ORDER_TYPE: 'Only balance orders can request a refund.', + INVALID_STATUS: 'The current order status does not allow this operation.', + BALANCE_NOT_ENOUGH: 'Refund amount exceeds balance.', + REFUND_AMOUNT_EXCEEDED: 'Refund amount exceeds the recharge amount.', + REFUND_FAILED: 'Refund failed.', + }, + airwallexPay: 'Airwallex Payment', + stripePay: 'Pay Now', + stripeSuccessProcessing: 'Payment successful, processing your order...', + stripePopup: { + redirecting: 'Redirecting to payment page...', + loadingQr: 'Loading WeChat Pay QR code...', + timeout: 'Timed out waiting for payment credentials, please retry', + qrFailed: 'Failed to get WeChat Pay QR code', + }, + subscribeNow: 'Subscribe Now', + renewNow: 'Renew', + selectPlan: 'Select Plan', + planFeatures: 'Features', + planCard: { + rate: 'Rate', + peakRate: 'Peak Rate', + dailyLimit: 'Daily', + weeklyLimit: 'Weekly', + monthlyLimit: 'Monthly', + quota: 'Quota', + unlimited: 'Unlimited', + models: 'Models', + }, + days: 'days', + months: 'months', + years: 'years', + oneMonth: '1 Month', + oneYear: '1 Year', + perMonth: 'month', + perYear: 'year', + admin: { + tabs: { + overview: 'Overview', + orders: 'Orders', + channels: 'Channels', + plans: 'Plans', + }, + todayRevenue: 'Today Revenue', + totalRevenue: 'Total Revenue', + todayOrders: 'Today Orders', + orderCount: 'Order Count', + avgAmount: 'Average Amount', + revenue: 'Revenue', + dailyRevenue: 'Daily Revenue', + paymentDistribution: 'Payment Distribution', + colUser: 'User', + topUsers: 'Top Users', + noData: 'No data', + days: 'days', + weeks: 'weeks', + months: 'months', + searchOrders: 'Search orders...', + allStatuses: 'All Statuses', + allPaymentTypes: 'All Payment Types', + allOrderTypes: 'All Order Types', + orderDetail: 'Order Detail', + orderType: 'Order Type', + orders: 'Orders', + balanceOrder: 'Balance Top-Up', + subscriptionOrder: 'Subscription', + paidAt: 'Paid At', + completedAt: 'Completed At', + expiresAt: 'Expires At', + feeRate: 'Fee Rate', + refund: 'Refund', + refundOrder: 'Refund Order', + refundAmount: 'Refund Amount', + maxRefundable: 'Max Refundable', + refundReason: 'Refund Reason', + refundReasonPlaceholder: 'Please enter refund reason', + confirmRefund: 'Confirm Refund', + refundSuccess: 'Refund successful', + refundPending: 'Refund pending gateway confirmation', + queryRefundStatus: 'Query refund status', + refundInfo: 'Refund Info', + refundEnabled: 'Refund Enabled', + allowUserRefund: 'Allow User Refund', + alreadyRefunded: 'Already Refunded', + deductBalance: 'Deduct Balance', + deductBalanceHint: 'Subtract recharged amount from user balance', + userBalance: 'User Balance', + orderAmount: 'Order Amount', + insufficientBalance: 'Insufficient balance — will deduct to $0', + noDeduction: 'Will NOT deduct user balance', + forceRefund: 'Force refund (ignore balance check)', + orderCancelled: 'Order Cancelled', + retry: 'Retry', + retrySuccess: 'Retry successful', + approveRefund: 'Approve Refund', + retryRefund: 'Retry Refund', + refundRequestInfo: 'Refund Request Info', + refundRequestedAt: 'Requested At', + refundRequestedBy: 'Requested By', + refundRequestReason: 'Request Reason', + auditLogs: 'Audit Logs', + operator: 'Operator', + channelName: 'Channel Name', + channelDescription: 'Channel Description', + createChannel: 'Create Channel', + editChannel: 'Edit Channel', + deleteChannel: 'Delete Channel', + deleteChannelConfirm: 'Are you sure you want to delete this channel?', + planName: 'Plan Name', + planDescription: 'Plan Description', + createPlan: 'Create Plan', + editPlan: 'Edit Plan', + deletePlan: 'Delete Plan', + deletePlanConfirm: 'Are you sure you want to delete this plan?', + originalPrice: 'Original Price', + price: 'Price', + subscriptionCnyPayPreview: 'CNY channel charge preview: {amount}', + subscriptionCnyPayPreviewWithFee: '({feeRate}% fee included: {total})', + validityDays: 'Validity (days)', + validityUnit: 'Validity Unit', + sortOrder: 'Sort Order', + forSale: 'For Sale', + onSale: 'On Sale', + offSale: 'Off Sale', + group: 'Group', + groupId: 'Group ID', + features: 'Features', + featuresHint: 'One feature per line', + featuresPlaceholder: 'Enter plan features...', + providerManagement: 'Provider Management', + providerManagementDesc: 'Manage payment provider instances', + createProvider: 'Create Provider', + editProvider: 'Edit Provider', + deleteProvider: 'Delete Provider', + deleteProviderConfirm: 'Are you sure you want to delete this provider?', + providerName: 'Provider Name', + providerKey: 'Provider Key', + selectProviderKey: 'Select Provider Key', + providerConfig: 'Provider Config', + noProviders: 'No providers configured', + noProvidersHint: 'Create a provider instance to start accepting payments', + supportedTypes: 'Supported Payment Types', + supportedTypesHint: 'Select the payment types this provider supports', + rateMultiplier: 'Rate Multiplier', + dashboardTitle: 'Payment Dashboard', + dashboardDesc: 'Recharge order analytics and insights', + daySuffix: 'd', + paymentConfigTitle: 'Payment Config', + paymentConfigDesc: 'Configure payment providers and settings', + plansPageTitle: 'Subscription Plans', + plansPageDesc: 'Manage subscription plan configuration', + tabPlanConfig: 'Plan Configuration', + tabUserSubs: 'User Subscriptions', + selectGroup: 'Select a group', + groupRequired: 'Please select a subscription group', + priceRequired: 'Price must be greater than 0', + validityDaysRequired: 'Validity days must be greater than 0', + groupMissing: 'Missing', + groupInfo: 'Group Info', + platform: 'Platform', + rateMultiplierLabel: 'Rate', + dailyLimit: 'Daily Limit', + weeklyLimit: 'Weekly Limit', + monthlyLimit: 'Monthly Limit', + unlimited: 'Unlimited', + searchUserSubs: 'Search user subscriptions...', + daily: 'D', + weekly: 'W', + monthly: 'M', + subsStatus: { + active: 'Active', + expired: 'Expired', + revoked: 'Revoked', + }, + }, + }, + +} diff --git a/frontend/src/i18n/locales/zh.ts b/frontend/src/i18n/locales/zh.ts deleted file mode 100644 index d57df8c01f..0000000000 --- a/frontend/src/i18n/locales/zh.ts +++ /dev/null @@ -1,7731 +0,0 @@ -export default { - batchImageGuide: { - title: '图片批量生成', - description: '一次提交多条提示词,任务完成后可统一下载图片结果' - }, - // Home Page - home: { - viewOnGithub: '在 GitHub 上查看', - viewDocs: '查看文档', - docs: '文档', - switchToLight: '切换到浅色模式', - switchToDark: '切换到深色模式', - dashboard: '控制台', - login: '登录', - getStarted: '立即开始', - goToDashboard: '进入控制台', - // 新增:面向用户的价值主张 - heroSubtitle: '一个密钥,畅用多个 AI 模型', - heroDescription: '无需管理多个订阅账号,一站式接入 Claude、GPT、Gemini 等主流 AI 服务', - tags: { - subscriptionToApi: '订阅转 API', - stickySession: '会话保持', - realtimeBilling: '按量计费' - }, - // 用户痛点区块 - painPoints: { - title: '你是否也遇到这些问题?', - items: { - expensive: { - title: '订阅费用高', - desc: '每个 AI 服务都要单独订阅,每月支出越来越多' - }, - complex: { - title: '多账号难管理', - desc: '不同平台的账号、密钥分散各处,管理起来很麻烦' - }, - unstable: { - title: '服务不稳定', - desc: '单一账号容易触发限制,影响正常使用' - }, - noControl: { - title: '用量无法控制', - desc: '不知道钱花在哪了,也无法限制团队成员的使用' - } - } - }, - // 解决方案区块 - solutions: { - title: '我们帮你解决', - subtitle: '简单三步,开始省心使用 AI' - }, - features: { - unifiedGateway: '一键接入', - unifiedGatewayDesc: '获取一个 API 密钥,即可调用所有已接入的 AI 模型,无需分别申请。', - multiAccount: '稳定可靠', - multiAccountDesc: '智能调度多个上游账号,自动切换和负载均衡,告别频繁报错。', - balanceQuota: '用多少付多少', - balanceQuotaDesc: '按实际使用量计费,支持设置配额上限,团队用量一目了然。' - }, - // 优势对比 - comparison: { - title: '为什么选择我们?', - headers: { - feature: '对比项', - official: '官方订阅', - us: '本平台' - }, - items: { - pricing: { - feature: '付费方式', - official: '固定月费,用不完也付', - us: '按量付费,用多少付多少' - }, - models: { - feature: '模型选择', - official: '单一服务商', - us: '多模型随意切换' - }, - management: { - feature: '账号管理', - official: '每个服务单独管理', - us: '统一密钥,一站管理' - }, - stability: { - feature: '服务稳定性', - official: '单账号易触发限制', - us: '多账号池,自动切换' - }, - control: { - feature: '用量控制', - official: '无法限制', - us: '可设配额、查明细' - } - } - }, - providers: { - title: '已支持的 AI 模型', - description: '一个 API,多种选择', - supported: '已支持', - soon: '即将推出', - claude: 'Claude', - gemini: 'Gemini', - antigravity: 'Antigravity', - more: '更多' - }, - // CTA 区块 - cta: { - title: '准备好开始了吗?', - description: '注册即可获得免费试用额度,体验一站式 AI 服务', - button: '免费注册' - }, - footer: { - allRightsReserved: '保留所有权利。' - } - }, - - // Key Usage Query Page - keyUsage: { - title: 'API Key 用量查询', - subtitle: '输入您的 API Key 以查看实时消费金额与使用状态', - placeholder: 'sk-ant-mirror-xxxxxxxxxxxx', - query: '查询', - querying: '查询中...', - privacyNote: '您的 Key 仅在浏览器本地处理,不会被存储', - dateRange: '统计范围:', - dateRangeToday: '今日', - dateRange7d: '7 天', - dateRange30d: '30 天', - dateRange90d: '90 天', - dateRangeCustom: '自定义', - apply: '应用', - used: '已使用', - detailInfo: '详细信息', - tokenStats: 'Token 统计', - dailyDetail: '按日明细', - modelStats: '模型用量统计', - // Table headers - date: '日期', - model: '模型', - requests: '请求数', - inputTokens: '输入 Tokens', - outputTokens: '输出 Tokens', - cacheCreationTokens: '缓存创建', - cacheReadTokens: '缓存读取', - cacheWriteTokens: '缓存写入', - totalTokens: '总 Tokens', - cost: '费用', - // Status - quotaMode: 'Key 限额模式', - walletBalance: '钱包余额', - // Ring card titles - totalQuota: '总额度', - limit5h: '5 小时限额', - limitDaily: '日限额', - limit7d: '7 天限额', - limitWeekly: '周限额', - limitMonthly: '月限额', - // Detail rows - remainingQuota: '剩余额度', - expiresAt: '过期时间', - todayExpires: '(今日到期)', - daysLeft: '({days} 天)', - usedQuota: '已用额度', - resetNow: '即将重置', - subscriptionType: '订阅类型', - subscriptionExpires: '订阅到期', - // Usage stat cells - todayRequests: '今日请求', - todayInputTokens: '今日输入', - todayOutputTokens: '今日输出', - todayTokens: '今日 Tokens', - todayCacheCreation: '今日缓存创建', - todayCacheRead: '今日缓存读取', - todayCost: '今日费用', - rpmTpm: 'RPM / TPM', - totalRequests: '累计请求', - totalInputTokens: '累计输入', - totalOutputTokens: '累计输出', - totalTokensLabel: '累计 Tokens', - totalCacheCreation: '累计缓存创建', - totalCacheRead: '累计缓存读取', - totalCost: '累计费用', - avgDuration: '平均耗时', - // Messages - enterApiKey: '请输入 API Key', - querySuccess: '查询成功', - queryFailed: '查询失败', - queryFailedRetry: '查询失败,请稍后重试', - noDailyUsage: '暂无按日用量数据', - }, - - // Setup Wizard - setup: { - title: 'Sub2API 安装向导', - description: '配置您的 Sub2API 实例', - database: { - title: '数据库配置', - description: '连接到您的 PostgreSQL 数据库', - host: '主机', - port: '端口', - username: '用户名', - password: '密码', - databaseName: '数据库名称', - sslMode: 'SSL 模式', - passwordPlaceholder: '密码', - ssl: { - disable: '禁用', - require: '要求', - verifyCa: '验证 CA', - verifyFull: '完全验证' - } - }, - redis: { - title: 'Redis 配置', - description: '连接到您的 Redis 服务器', - host: '主机', - port: '端口', - password: '密码(可选)', - database: '数据库', - passwordPlaceholder: '密码', - enableTls: '启用 TLS', - enableTlsHint: '连接 Redis 时使用 TLS(公共 CA 证书)' - }, - admin: { - title: '管理员账户', - description: '创建您的管理员账户', - email: '邮箱', - password: '密码', - confirmPassword: '确认密码', - passwordPlaceholder: '至少 8 个字符', - confirmPasswordPlaceholder: '确认密码', - passwordMismatch: '密码不匹配' - }, - ready: { - title: '准备安装', - description: '检查您的配置并完成安装', - database: '数据库', - redis: 'Redis', - adminEmail: '管理员邮箱' - }, - status: { - testing: '测试中...', - success: '连接成功', - testConnection: '测试连接', - installing: '安装中...', - completeInstallation: '完成安装', - completed: '安装完成!', - redirecting: '正在跳转到登录页面...', - restarting: '服务正在重启,请稍候...', - timeout: '服务重启时间超出预期,请手动刷新页面。' - } - }, - - // Common - common: { - loading: '加载中...', - submitting: '提交中...', - justNow: '刚刚', - peakRateTooltip: '高峰倍率:{window}', - peakRateImageNote: ';token 计费的图片 token 同样适用,图片按次计费不受高峰影响', - save: '保存', - saved: '保存成功', - deleted: '删除成功', - cancel: '取消', - delete: '删除', - edit: '编辑', - create: '创建', - update: '更新', - confirm: '确认', - reset: '重置', - search: '搜索', - filter: '筛选', - export: '导出', - import: '导入', - actions: '操作', - status: '状态', - name: '名称', - email: '邮箱', - password: '密码', - submit: '提交', - back: '返回', - next: '下一步', - yes: '是', - no: '否', - all: '全部', - none: '无', - selectAll: '全选', - noData: '暂无数据', - expand: '展开', - collapse: '收起', - success: '成功', - error: '错误', - critical: '严重', - warning: '警告', - info: '提示', - active: '启用', - inactive: '禁用', - more: '更多', - close: '关闭', - enabled: '已启用', - disabled: '已禁用', - total: '总计', - balance: '余额', - availableBalance: '可用余额', - frozenBalance: '冻结金额', - totalBalance: '总余额', - available: '可用', - copiedToClipboard: '已复制到剪贴板', - copied: '已复制', - copyFailed: '复制失败', - verifying: '验证中...', - processing: '处理中...', - contactSupport: '联系客服', - add: '添加', - invalidEmail: '请输入有效的邮箱地址', - optional: '可选', - selectOption: '请选择', - searchPlaceholder: '搜索...', - noOptionsFound: '无匹配选项', - noGroupsAvailable: '无可用分组', - unknownError: '发生未知错误', - saving: '保存中...', - selectedCount: '(已选 {count} 个)', - refresh: '刷新', - autoRefresh: { - title: '自动刷新', - enable: '启用自动刷新', - countdown: '自动刷新: {seconds}s', - seconds: '{n} 秒', - }, - view: '查看', - settings: '设置', - chooseFile: '选择文件', - copy: '复制', - notAvailable: '不可用', - now: '现在', - today: '今天', - tomorrow: '明天', - unknown: '未知', - minutes: '分钟', - time: { - never: '从未', - justNow: '刚刚', - minutesAgo: '{n}分钟前', - hoursAgo: '{n}小时前', - daysAgo: '{n}天前', - countdown: { - daysHours: '{d}d {h}h', - hoursMinutes: '{h}h {m}m', - minutes: '{m}m', - withSuffix: '{time} 后解除' - } - } - }, - - adminCompliance: { - title: '部署与运营合规确认', - blockingNotice: '继续使用控制台前,须完成部署与运营合规确认。', - riskNotice: '本确认用于以清晰、显著、可留痕的方式提示自部署实例的合规义务与运营风险。', - version: '协议版本', - openDocument: '在 GitHub 查看协议文件', - documentSource: '协议正文来自本项目仓库中的 Markdown 文件。修改协议内容时必须同步递增协议版本;已确认的旧版本将失效,控制台使用者须重新确认。', - inputLabel: '请逐字输入以下确认短语', - inputPlaceholder: '输入确认短语以继续', - inputMismatch: '确认短语不匹配,请逐字输入提示内容。', - legalNote: '本确认用于明确自部署实例与开源项目、著作权人、贡献者及维护者之间的非关联关系和责任边界;部署、运营或控制相关实例的主体应独立承担其适用义务。', - logout: '退出登录', - accept: '确认并继续', - accepted: '合规确认已记录', - acceptFailed: '提交确认失败' - }, - - legal: { - loadFailed: '文档加载失败', - retryLater: '请稍后刷新页面重试。', - notFound: '文档不存在', - notFoundDescription: '当前条款文档不存在或已被管理员移除。', - updatedAt: '更新日期:{date}', - empty: '暂无正文内容', - loginAgreement: '登录条款', - adminCompliance: '部署与运营合规承诺', - loginAgreementPrompt: { - checkboxPrefix: '我已阅读并同意', - documentSeparator: '、', - noticeTitle: '继续登录前需要先同意最新条款。', - noticeDescription: '未同意前,账号密码输入和快捷登录会保持禁用。', - viewTerms: '查看条款', - dialogTitle: '条款更新通知', - dialogDescription: '我们的服务条款已于 {date} 更新。在继续使用服务之前,请仔细阅读并同意以下条款。', - recently: '近期', - relatedDocuments: '相关文档', - reject: '拒绝', - accept: '同意并继续', - loginRejectedWarning: '未同意最新条款前,无法输入账号密码或使用快捷登录。', - loginRequiredWarning: '请先阅读并同意最新条款后再登录。', - registerRejectedWarning: '未同意最新条款前,无法注册或使用快捷登录。', - registerRequiredWarning: '请先阅读并同意最新条款后再注册。' - } - }, - - // Navigation - nav: { - dashboard: '仪表盘', - announcements: '公告', - apiKeys: 'API 密钥', - batchImage: '批量生图', - usage: '使用记录', - redeem: '兑换', - affiliate: '邀请返利', - affiliateManagement: '邀请返利', - affiliateInviteRecords: '邀请记录', - affiliateRebateRecords: '返利记录', - affiliateTransferRecords: '提取记录', - profile: '个人资料', - users: '用户管理', - groups: '分组管理', - channels: '渠道管理', - availableChannels: '可用渠道', - subscriptions: '订阅管理', - accounts: '账号管理', - proxies: 'IP管理', - redeemCodes: '兑换码', - ops: '运维监控', - promoCodes: '优惠码', - settings: '系统设置', - myAccount: '我的账户', - lightMode: '浅色模式', - darkMode: '深色模式', - collapse: '收起', - expand: '展开', - logout: '退出登录', - github: 'GitHub', - mySubscriptions: '我的订阅', - buySubscription: '充值/订阅', - docs: '文档', - myOrders: '我的订单', - orderManagement: '订单管理', - paymentDashboard: '支付概览', - paymentConfig: '支付配置', - paymentPlans: '订阅套餐', - channelManagement: '渠道管理', - channelPricing: '渠道定价', - channelMonitor: '渠道监控', - channelStatus: '渠道状态', - riskControl: '风控中心', - }, - - // Auth - auth: { - welcomeBack: '欢迎回来', - signInToAccount: '登录您的账户以继续', - signIn: '登录', - signingIn: '登录中...', - createAccount: '创建账户', - signUpToStart: '注册以开始使用 {siteName}', - signUp: '注册', - processing: '处理中...', - continue: '继续', - rememberMe: '记住我', - dontHaveAccount: '还没有账户?', - alreadyHaveAccount: '已有账户?', - registrationDisabled: '注册功能暂时关闭,请联系管理员。', - emailLabel: '邮箱', - emailPlaceholder: '请输入邮箱', - passwordLabel: '密码', - passwordPlaceholder: '请输入密码', - createPasswordPlaceholder: '创建一个安全的密码', - passwordHint: '至少 6 个字符', - emailRequired: '请输入邮箱', - invalidEmail: '请输入有效的邮箱地址', - passwordRequired: '请输入密码', - passwordMinLength: '密码至少需要 6 个字符', - loginFailed: '登录失败,请检查您的凭据后重试。', - errors: { - USER_NOT_ACTIVE: '账号已被禁用', - }, - registrationFailed: '注册失败,请重试。', - emailSuffixNotAllowed: '该邮箱域名不在允许注册范围内。', - emailSuffixNotAllowedWithAllowed: '该邮箱域名不被允许。可用域名:{suffixes}', - emailSuffixAllowedMore: '等 {count} 项', - loginSuccess: '登录成功!欢迎回来。', - accountCreatedSuccess: '账户创建成功!欢迎使用 {siteName}。', - reloginRequired: '会话已过期,请重新登录。', - turnstileExpired: '验证已过期,请重试', - turnstileFailed: '验证失败,请重试', - completeVerification: '请完成验证', - verifyYourEmail: '验证您的邮箱', - sessionExpired: '会话已过期', - sessionExpiredDesc: '请返回注册页面重新开始。', - verificationCode: '验证码', - verificationCodeHint: '请输入发送到您邮箱的6位验证码', - sendingCode: '发送中...', - sendCode: '发送验证码', - clickToResend: '点击重新发送验证码', - resendCode: '重新发送验证码', - sendCodeDesc: '我们将发送验证码到', - codeSentSuccess: '验证码已发送!请查收您的邮箱。', - verifying: '验证中...', - verifyAndCreate: '验证并创建账户', - resendCountdown: '{countdown}秒后可重新发送', - backToRegistration: '返回注册', - sendCodeFailed: '发送验证码失败,请重试。', - verifyFailed: '验证失败,请重试。', - codeRequired: '请输入验证码', - invalidCode: '请输入有效的6位验证码', - promoCodeLabel: '优惠码', - promoCodePlaceholder: '输入优惠码(可选)', - promoCodeValid: '有效!注册后将获得 ${amount} 赠送余额', - promoCodeInvalid: '无效的优惠码', - promoCodeNotFound: '优惠码不存在', - promoCodeExpired: '此优惠码已过期', - promoCodeDisabled: '此优惠码已被禁用', - promoCodeMaxUsed: '此优惠码已达到使用上限', - promoCodeAlreadyUsed: '您已使用过此优惠码', - promoCodeValidating: '优惠码正在验证中,请稍候', - promoCodeInvalidCannotRegister: '优惠码无效,请检查后重试或清空优惠码', - invitationCodeLabel: '邀请码', - invitationCodePlaceholder: '请输入邀请码', - invitationCodeRequired: '请输入邀请码', - invitationCodeValid: '邀请码有效', - invitationCodeInvalid: '邀请码无效或已被使用', - invitationCodeValidating: '正在验证邀请码...', - invitationCodeInvalidCannotRegister: '邀请码无效,请检查后重试', - oauthOrContinue: '或使用其他继续', - linuxdo: { - signIn: '使用 Linux.do 登录', - orContinue: '或使用邮箱密码继续', - callbackTitle: '正在完成登录', - callbackProcessing: '正在验证登录信息,请稍候...', - callbackHint: '如果页面未自动跳转,请返回登录页重试。', - callbackMissingToken: '登录信息缺失,请返回重试。', - backToLogin: '返回登录', - invitationRequired: '该 Linux.do 账号尚未注册,站点已开启邀请码注册,请输入邀请码以完成注册。', - invalidPendingToken: '注册凭证已失效,请重新使用 Linux.do 登录。', - completeRegistration: '完成注册', - completing: '正在完成注册...', - completeRegistrationFailed: '注册失败,请检查邀请码后重试。' - }, - dingtalk: { - signIn: '钉钉登录', - callbackTitle: '正在完成钉钉登录', - callbackProcessing: '正在验证钉钉登录信息,请稍候...', - callbackHint: '如果页面未自动跳转,请返回登录页重试。', - callbackMissingToken: '登录信息缺失,请返回重试。', - backToLogin: '返回登录', - invitationRequired: '该钉钉账号尚未注册,站点已开启邀请码注册,请输入邀请码以完成注册。', - invalidPendingToken: '注册凭证已失效,请重新使用钉钉登录。', - completeRegistration: '完成注册', - completing: '正在完成注册...', - completeRegistrationFailed: '注册失败,请检查邀请码后重试。', - createAccountTitle: '创建钉钉账户', - registrationDisabledRedirectToBind: '当前已禁止注册新账户,请使用已有账户邮箱和密码绑定钉钉登录', - error: { - title: '钉钉登录失败', - csrf: '登录会话已过期,请重新扫码登录', - corp_rejected: '您的钉钉账号不属于本企业,请联系管理员', - dingtalk_not_enabled: '钉钉登录暂未启用', - upstream_error: '钉钉服务暂时不可用,请稍后重试', - missing_browser_session: '浏览器会话丢失,请重新登录', - missing_params: '请求参数不完整', - invalid_state: '登录状态异常', - provider_error: '钉钉授权失败', - session_error: '会话创建失败,请重试', - retry: '重新登录' - } - }, - emailOAuth: { - signIn: '使用 {providerName} 登录' - }, - oidc: { - signIn: '使用 {providerName} 登录', - callbackTitle: '正在完成 {providerName} 登录', - callbackProcessing: '正在验证 {providerName} 登录信息,请稍候...', - callbackHint: '如果页面未自动跳转,请返回登录页重试。', - callbackMissingToken: '登录信息缺失,请返回重试。', - backToLogin: '返回登录', - invitationRequired: '该 {providerName} 账号尚未注册,站点已开启邀请码注册,请输入邀请码以完成注册。', - invalidPendingToken: '注册凭证已失效,请重新登录。', - completeRegistration: '完成注册', - completing: '正在完成注册...', - completeRegistrationFailed: '注册失败,请检查邀请码后重试。' - }, - oauthFlow: { - profileDetailsTitle: '使用 {providerName} 资料', - profileDetailsDescription: '选择是否将 {providerName} 的昵称或头像应用到当前账户。', - useDisplayName: '使用昵称', - useAvatar: '使用头像', - avatarAlt: '{providerName} 头像', - reviewProfileBeforeContinue: '请先确认 {providerName} 资料后再继续。', - chooseHowToContinue: '选择后续操作', - chooseAccountActionHint: '请选择绑定已有账户,或创建一个新账户。', - suggestedEmail: '建议邮箱:{email}', - bindExistingAccount: '绑定已有账户', - createNewAccount: '创建新账户', - createAccountHint: '请输入邮箱地址以创建账户并继续。', - bindLoginHint: '登录一个已有账户以绑定此次 {providerName} 登录。', - signInThenBindDescription: '请先登录已有账户,再将此次 {providerName} 登录绑定到该账户。', - bindSignInToExistingAccount: '将此次 {providerName} 登录绑定到已有账户。', - bindCurrentAccountTitle: '绑定当前账户', - bindCurrentAccountDescription: '将此次 {providerName} 登录绑定到当前浏览器已登录的账户。', - bindCurrentAccount: '绑定当前账户', - logInAndBind: '登录并绑定', - useDifferentEmail: '使用其他邮箱', - backToOptions: '返回选项', - yourAccount: '当前账户', - totpHint: '请输入 {account} 的 6 位验证码,以完成此次 {providerName} 登录绑定。', - verifyAndContinue: '验证并继续', - wechatAvailabilityUnknown: '暂时无法确认微信登录可用性,请刷新后重试。', - wechatSystemBrowserOnly: '当前微信登录流程仅支持在系统浏览器中继续。', - wechatBrowserOnly: '当前微信登录流程仅支持在微信内置浏览器中继续。', - wechatNotConfigured: '微信登录尚未配置。' - }, - linuxdoCallbackPageTitle: 'LinuxDo 登录回调', - dingtalkCallbackPageTitle: '钉钉登录回调', - dingtalkProviderName: '钉钉', - oidcCallbackPageTitle: 'OIDC 登录回调', - oauthCallbackPageTitle: 'OAuth 回调', - wechatProviderName: '微信', - wechatCallbackPageTitle: '微信登录回调', - wechatPaymentCallbackPageTitle: '微信支付回调', - wechatPayment: { - callbackTitle: '正在恢复微信支付', - callbackProcessing: '正在恢复微信支付...', - backToPayment: '返回支付页', - callbackMissingResumeToken: '微信支付回调缺少恢复令牌。' - }, - oauth: { - callbackTitle: 'OAuth 回调', - callbackHint: '按需将授权码和状态值复制回后台授权流程。', - invalidCallbackTitle: '无效的登录回调', - invalidCallbackHint: '当前页面缺少有效的授权结果,请返回登录页重新发起快捷登录。', - code: '授权码', - state: '状态', - fullUrl: '完整URL' - }, - // 忘记密码 - forgotPassword: '忘记密码?', - forgotPasswordTitle: '重置密码', - forgotPasswordHint: '输入您的邮箱地址,我们将向您发送密码重置链接。', - sendResetLink: '发送重置链接', - sendingResetLink: '发送中...', - sendResetLinkFailed: '发送重置链接失败,请重试。', - resetEmailSent: '重置链接已发送', - resetEmailSentHint: - '如果该邮箱已注册,您将很快收到密码重置链接。请检查您的收件箱和垃圾邮件文件夹。', - backToLogin: '返回登录', - rememberedPassword: '想起密码了?', - // 重置密码 - resetPasswordTitle: '设置新密码', - resetPasswordHint: '请在下方输入您的新密码。', - newPassword: '新密码', - newPasswordPlaceholder: '输入新密码', - confirmPassword: '确认密码', - confirmPasswordPlaceholder: '再次输入新密码', - confirmPasswordRequired: '请确认您的密码', - passwordsDoNotMatch: '两次输入的密码不一致', - resetPassword: '重置密码', - resettingPassword: '重置中...', - resetPasswordFailed: '重置密码失败,请重试。', - passwordResetSuccess: '密码重置成功', - passwordResetSuccessHint: '您的密码已重置。现在可以使用新密码登录。', - invalidResetLink: '无效的重置链接', - invalidResetLinkHint: '此密码重置链接无效或已过期。请重新请求一个新链接。', - requestNewResetLink: '请求新的重置链接', - invalidOrExpiredToken: '密码重置链接无效或已过期。请重新请求一个新链接。' - }, - - // Dashboard - dashboard: { - title: '仪表盘', - welcomeMessage: '欢迎回来!这是您账户的概览。', - balance: '余额', - apiKeys: 'API 密钥', - todayRequests: '今日请求', - todayCost: '今日消费', - todayTokens: '今日 Token', - totalTokens: '累计 Token', - cacheToday: '今日缓存', - performance: '性能指标', - avgResponse: '平均响应', - averageTime: '平均时间', - timeRange: '时间范围', - granularity: '粒度', - day: '按天', - hour: '按小时', - modelDistribution: '模型分布', - groupDistribution: '分组使用分布', - platformBreakdown: '按平台拆分', - platformBreakdownEmpty: '暂无平台用量', - platformCount: '{count} 个平台', - platformOther: '其他', - platformQuota: { - title: '配额用量', - daily: '日', - weekly: '周', - monthly: '月(近30天)', - resetsAt: '{time} 重置', - noLimit: '不限制', - disabled: '已禁用', - }, - tokenUsageTrend: 'Token 使用趋势', - noDataAvailable: '暂无数据', - model: '模型', - group: '分组', - noGroup: '无分组', - requests: '请求', - tokens: 'Token', - actual: '实际', - standard: '标准', - input: '输入', - output: '输出', - cache: '缓存', - recentUsage: '最近使用', - last7Days: '近 7 天', - noUsageRecords: '暂无使用记录', - startUsingApi: '开始使用 API 后,您的使用历史将显示在这里。', - viewAllUsage: '查看全部', - quickActions: '快捷操作', - createApiKey: '创建 API 密钥', - generateNewKey: '生成新的 API 密钥', - batchImageAgent: '批量生图助手', - batchImageAgentDesc: '复制给 Agent 的任务说明', - viewUsage: '查看使用记录', - checkDetailedLogs: '查看详细的使用日志', - redeemCode: '兑换码', - addBalanceWithCode: '使用兑换码充值' - }, - - // Groups (shared) - groups: { - subscription: '订阅' - }, - - // API Keys - keys: { - title: 'API 密钥', - description: '管理您的 API 密钥和访问令牌', - searchPlaceholder: '搜索名称或Key...', - endpoints: { - title: 'API 端点', - default: '默认', - copied: '已复制', - copiedHint: '已复制到剪贴板', - clickToCopy: '点击可复制此端点', - speedTest: '测速', - }, - allGroups: '全部分组', - allStatus: '全部状态', - columnSettings: '列设置', - columnAlwaysVisible: '该列固定显示,不可隐藏', - createKey: '创建密钥', - editKey: '编辑密钥', - deleteKey: '删除密钥', - deleteConfirmMessage: "确定要删除 '{name}' 吗?此操作无法撤销。", - apiKey: 'API 密钥', - group: '分组', - currentConcurrency: '当前并发', - noGroup: '无分组', - searchGroup: '搜索分组...', - noGroupFound: '未找到匹配的分组', - created: '创建时间', - copyToClipboard: '复制到剪贴板', - copied: '已复制!', - importToCcSwitch: '导入到 CCS', - enable: '启用', - disable: '禁用', - nameLabel: '名称', - namePlaceholder: '我的 API 密钥', - groupLabel: '分组', - selectGroup: '选择分组', - statusLabel: '状态', - selectStatus: '选择状态', - saving: '保存中...', - noKeysYet: '暂无 API 密钥', - createFirstKey: '创建您的第一个 API 密钥以开始使用 API。', - keyCreatedSuccess: 'API 密钥创建成功', - keyUpdatedSuccess: 'API 密钥更新成功', - keyDeletedSuccess: 'API 密钥删除成功', - keyEnabledSuccess: 'API 密钥已启用', - keyDisabledSuccess: 'API 密钥已禁用', - failedToLoad: '加载 API 密钥失败', - failedToSave: '保存 API 密钥失败', - failedToDelete: '删除 API 密钥失败', - failedToUpdateStatus: '更新 API 密钥状态失败', - clickToChangeGroup: '点击更换分组', - groupChangedSuccess: '分组更换成功', - failedToChangeGroup: '更换分组失败', - groupRequired: '请选择分组', - usage: '用量', - today: '今日', - total: '近30天', - quota: '额度', - lastUsedAt: '上次使用时间', - useKey: '使用密钥', - useKeyModal: { - title: '使用 API 密钥', - description: '将以下环境变量添加到您的终端配置文件或直接在终端中运行。', - copy: '复制', - copied: '已复制', - note: '这些环境变量将在当前终端会话中生效。如需永久配置,请将其添加到 ~/.bashrc、~/.zshrc 或相应的配置文件中。', - noGroupTitle: '请先分配分组', - noGroupDescription: - '此 API 密钥尚未分配分组,请先在密钥列表中点击分组列进行分配,然后才能查看使用配置。', - openai: { - description: '将以下配置文件添加到 Codex CLI 配置目录中。', - configTomlHint: '请确保以下内容位于 config.toml 文件的开头部分', - note: '请确保配置目录存在。macOS/Linux 用户可运行 mkdir -p ~/.codex 创建目录。', - noteWindows: - '按 Win+R,输入 %userprofile%\\.codex 打开配置目录。如目录不存在,请先手动创建。' - }, - cliTabs: { - claudeCode: 'Claude Code', - geminiCli: 'Gemini CLI', - codexCli: 'Codex CLI', - codexCliWs: 'Codex CLI (WebSocket)', - opencode: 'OpenCode' - }, - antigravity: { - description: '为 Antigravity 分组配置 API 访问。请根据您使用的客户端选择对应的配置方式。', - claudeCode: 'Claude Code', - geminiCli: 'Gemini CLI', - claudeNote: - '这些环境变量将在当前终端会话中生效。如需永久配置,请将其添加到 ~/.bashrc、~/.zshrc 或相应的配置文件中。', - geminiNote: - '这些环境变量将在当前终端会话中生效。如需永久配置,请将其添加到 ~/.bashrc、~/.zshrc 或相应的配置文件中。' - }, - gemini: { - description: - '将以下环境变量添加到您的终端配置文件或直接在终端中运行,以配置 Gemini CLI 访问。', - modelComment: '如果你有 Gemini 3 权限可以填:gemini-3-pro-preview', - note: '这些环境变量将在当前终端会话中生效。如需永久配置,请将其添加到 ~/.bashrc、~/.zshrc 或相应的配置文件中。' - }, - opencode: { - title: 'OpenCode 配置示例', - subtitle: 'opencode.json', - hint: '配置文件路径:~/.config/opencode/opencode.json(或 opencode.jsonc),不存在需手动创建。可使用默认 provider(openai/anthropic/google)或自定义 provider_id。API Key 支持直接配置或通过客户端 /connect 命令配置。示例仅供参考,模型与选项可按需调整。' - } - }, - customKeyLabel: '自定义密钥', - customKeyPlaceholder: '输入自定义密钥(至少16个字符)', - customKeyHint: '仅允许字母、数字、下划线和连字符,最少16个字符。', - customKeyTooShort: '自定义密钥至少需要16个字符', - customKeyInvalidChars: '自定义密钥只能包含字母、数字、下划线和连字符', - customKeyRequired: '请输入自定义密钥', - ipRestriction: 'IP 限制', - ipWhitelist: 'IP 白名单', - ipWhitelistPlaceholder: '192.168.1.100\n10.0.0.0/8', - ipWhitelistHint: '每行一个 IP 或 CIDR,设置后仅允许这些 IP 使用此密钥', - ipBlacklist: 'IP 黑名单', - ipBlacklistPlaceholder: '1.2.3.4\n5.6.0.0/16', - ipBlacklistHint: '每行一个 IP 或 CIDR,这些 IP 将被禁止使用此密钥', - ipRestrictionEnabled: '已配置 IP 限制', - ccSwitchNotInstalled: - 'CC-Switch 未安装或协议处理程序未注册。请先安装 CC-Switch 或手动复制 API 密钥。', - ccsClientSelect: { - title: '选择客户端', - description: '请选择您要导入到 CC-Switch 的客户端类型:', - claudeCode: 'Claude Code', - claudeCodeDesc: '导入为 Claude Code 配置', - geminiCli: 'Gemini CLI', - geminiCliDesc: '导入为 Gemini CLI 配置' - }, - // 配额和有效期 - quotaLimit: '额度限制', - quotaAmount: '额度金额 (USD)', - quotaAmountPlaceholder: '输入 USD 额度限制', - quotaAmountHint: '设置此密钥可消费的最大金额。0 = 无限制。', - quotaUsed: '已用额度', - reset: '重置', - resetQuotaUsed: '将已用额度重置为 0', - resetQuotaTitle: '确认重置额度', - resetQuotaConfirmMessage: '确定要将密钥 "{name}" 的已用额度(${used})重置为 0 吗?此操作不可撤销。', - quotaResetSuccess: '额度重置成功', - failedToResetQuota: '重置额度失败', - rateLimitColumn: '速率限制', - rateLimitSection: '速率限制', - resetUsage: '重置', - rateLimit5h: '5小时限额 (USD)', - rateLimit1d: '日限额 (USD)', - rateLimit7d: '7天限额 (USD)', - rateLimitHint: '设置此密钥在指定时间窗口内的最大消费额。0 = 无限制。', - rateLimitUsage: '速率限制用量', - resetRateLimitUsage: '重置速率限制用量', - resetRateLimitTitle: '确认重置速率限制', - resetRateLimitConfirmMessage: '确定要重置密钥 "{name}" 的速率限制用量吗?所有时间窗口的已用额度将归零。此操作不可撤销。', - rateLimitResetSuccess: '速率限制已重置', - failedToResetRateLimit: '重置速率限制失败', - resetNow: '即将重置', - expiration: '密钥有效期', - expiresInDays: '{days} 天', - extendDays: '+{days} 天', - customDate: '自定义', - expirationDate: '过期时间', - expirationDateHint: '选择此 API 密钥的过期时间。', - currentExpiration: '当前过期时间', - expiresAt: '过期时间', - noExpiration: '永久有效', - status: { - active: '活跃', - inactive: '已停用', - quota_exhausted: '额度耗尽', - expired: '已过期' - } - }, - - // Usage - usage: { - title: '使用记录', - description: '查看和分析您的 API 使用历史', - costDetails: '费用明细', - tokenDetails: 'Token 明细', - cacheTtlOverriddenHint: '缓存 TTL Override 已启用', - cacheTtlOverriddenLabel: 'TTL 替换', - cacheTtlOverridden5m: '按 5m 计费', - cacheTtlOverridden1h: '按 1h 计费', - totalRequests: '总请求数', - totalTokens: '总 Token', - cacheTotal: '缓存', - cacheBreakdown: '缓存 Token 明细', - cacheCreationTokensLabel: '缓存创建', - cacheReadTokensLabel: '缓存读取', - totalCost: '总消费', - standardCost: '标准', - actualCost: '实际', - accountCost: '成本', - userBilled: '用户扣费', - accountBilled: '账号计费', - resetNow: '现在', - resetPending: '待刷新', - accountMultiplier: '账号倍率', - avgDuration: '平均耗时', - inSelectedRange: '所选范围内', - perRequest: '每次请求', - apiKeyFilter: 'API 密钥', - allApiKeys: '全部密钥', - timeRange: '时间范围', - exportCsv: '导出 CSV', - exportExcel: '导出 Excel', - exportingProgress: '正在导出数据...', - exportedCount: '已导出 {current}/{total} 条', - estimatedTime: '预计剩余时间:{time}', - cancelExport: '取消导出', - exportCancelled: '导出已取消', - exporting: '导出中...', - preparingExport: '正在准备导出...', - model: '模型', - requestedModel: '请求', - upstreamModel: '上游', - reasoningEffort: '推理强度', - endpoint: '端点', - endpointDistribution: '端点分布', - inbound: '入站', - upstream: '上游', - mapping: '映射', - path: '路径', - inboundEndpoint: '入站端点', - upstreamEndpoint: '上游端点', - type: '类型', - tokens: 'Token', - cost: '费用', - firstToken: '首 Token', - duration: '耗时', - time: '时间', - ws: 'WS', - stream: '流式', - sync: '同步', - cyber: '安全策略', - unknown: '未知', - in: '输入', - out: '输出', - cacheHit: '缓存命中', - cacheCreate: '缓存创建', - cacheHitRate: '缓存命中率', - inputTokenPrice: '输入单价', - outputTokenPrice: '输出单价', - perMillionTokens: '/ 1M Token', - unitPrice: '单次价格', - imageUnitPrice: '单张价格', - imageTotalPrice: '图片总价', - imageCount: '图片张数', - imageBillingSize: '计费尺寸', - imageInputSize: '输入尺寸', - imageOutputSize: '输出尺寸', - imageOutputTokens: '图片输出 Token', - imageOutputTokenPrice: '图片输出单价', - imageOutputCost: '图片输出费用', - imageSizeSource: '尺寸来源', - imageSizeBreakdown: '尺寸明细', - imageSizeSourceOutput: '上游输出', - imageSizeSourceInput: '请求输入', - imageSizeSourceDefault: '默认计费档位', - imageSizeSourceLegacy: '历史记录', - imageSizeSourceMissing: '未记录', - imageSizeNotRecorded: '未记录', - imageSizeLegacyUnstandardized: '历史非标准', - imageSizeUnknown: '未知', - cacheRead: '读取', - cacheWrite: '写入', - serviceTier: '服务档位', - serviceTierPriority: 'Fast', - serviceTierFlex: 'Flex', - serviceTierStandard: 'Standard', - rate: '倍率', - original: '原始', - billed: '计费', - noRecords: '未找到使用记录,请尝试调整筛选条件。', - failedToLoad: '加载使用记录失败', - noDataToExport: '没有可导出的数据', - exportSuccess: '使用数据导出成功', - exportFailed: '使用数据导出失败', - exportExcelSuccess: '使用数据导出成功(Excel格式)', - exportExcelFailed: '使用数据导出失败', - imageUnit: '张', - userAgent: 'User-Agent', - ipGeo: { - fetch: '获取地区', - fetching: '获取中...', - failed: '获取失败', - private: '内网地址', - refreshTitle: '刷新地区信息', - batchFetch: '批量获取地区', - batchFetching: '获取中...', - pending: '{count} 个 IP 待获取地区', - batchFailed: '批量获取地区信息失败', - detailOrg: '运营商', - detailTimezone: '时区', - detailAccuracy: '定位精度', - detailCoordinates: '坐标', - }, - tabs: { usage: '用量明细', errors: '错误请求' }, - errors: { - time: '时间', model: '模型', endpoint: '端点', status: '状态码', - category: '分类', platform: '平台', message: '错误信息', - keyName: 'Key 名称', keyDeleted: '已删除', allKeys: '全部 Key', - modelPlaceholder: '搜索模型', allCategories: '全部分类', allStatuses: '全部状态码', - empty: '暂无错误请求', failedToLoad: '加载错误请求失败', - categories: { - auth: '认证失败', rate_limit: '限流', quota: '余额/订阅', - invalid_request: '参数错误', service_unavailable: '服务暂时不可用', - upstream: '上游错误', internal: '平台错误', other: '其他', cyber: '安全策略', - }, - detail: { - title: '错误请求详情', - responseBody: '上游响应内容', - upstreamStatus: '上游状态码', - loadFailed: '加载详情失败,请稍后重试', - }, - }, - }, - - // Shared keys for channel monitor (admin + user views) - monitorCommon: { - status: { - operational: '正常', - degraded: '降级', - failed: '失败', - error: '错误', - unknown: '-' - }, - providers: { - openai: 'OpenAI', - anthropic: 'Anthropic', - gemini: 'Gemini' - }, - extraModelsHeader: '附加模型', - extraModelsEmpty: '无附加模型', - latencyEmpty: '-', - availabilityPrefix: '可用性', - dialogLatency: '对话延迟', - endpointPing: '端点 PING', - history60pts: '近 {n} 次记录', - nextUpdateIn: '{n}s 后刷新', - past: 'PAST', - now: 'NOW', - maintenancePaused: '维护中 · 已暂停时间线采集', - extraModelsCount: '+ {n} 模型', - pollEvery: '{n}s 轮询', - updatedAt: '更新于 {time}', - relativeSecondsAgo: '{n} 秒前', - relativeMinutesAgo: '{n} 分钟前', - relativeHoursAgo: '{n} 小时前', - relativeDaysAgo: '{n} 天前' - }, - - // Channel Status (user-facing read-only view) - channelStatus: { - title: '渠道状态', - description: '查看渠道可用性、延迟和近期状态', - searchPlaceholder: '搜索渠道...', - allProviders: '全部供应商', - loadError: '加载渠道状态失败', - detailLoadError: '加载渠道详情失败', - detailTitle: '渠道详情', - closeDetail: '关闭', - windowTab: { - '7d': '7 天', - '15d': '15 天', - '30d': '30 天' - }, - overall: { - operational: 'OPERATIONAL', - degraded: 'DEGRADED', - unavailable: 'UNAVAILABLE' - }, - columns: { - name: '名称', - provider: '供应商', - groupName: '分组', - primaryModel: '主模型', - availability7d: '7 天可用率', - latency: '延迟 (ms)' - }, - detailColumns: { - model: '模型', - latestStatus: '最新状态', - latestLatency: '最新延迟 (ms)', - availability7d: '7 天可用率', - availability15d: '15 天可用率', - availability30d: '30 天可用率', - avgLatency7d: '7 天平均延迟 (ms)' - }, - empty: { - title: '暂无可显示的渠道', - description: '管理员尚未配置可监控的渠道。' - } - }, - - // Available Channels (user-facing) - availableChannels: { - title: '可用渠道', - description: '查看您可访问的渠道与其支持的模型、定价', - searchPlaceholder: '搜索渠道或模型...', - empty: '暂无可用渠道', - noModels: '未配置模型', - noPricing: '未配置定价', - exclusive: '专属', - public: '公开', - exclusiveTooltip: '管理员授权给你的专属分组', - publicTooltip: '对所有用户公开的分组', - columns: { - name: '渠道名', - description: '描述', - platform: '平台', - groups: '我可访问的分组', - supportedModels: '支持模型' - }, - pricing: { - billingMode: '计费模式', - billingModeToken: '按 Token', - billingModePerRequest: '按次', - billingModeImage: '按图片', - inputPrice: '输入', - outputPrice: '输出', - cacheWritePrice: '缓存写入', - cacheReadPrice: '缓存读取', - imageOutputPrice: '图片输出', - perRequestPrice: '每次请求', - intervals: '阶梯定价', - unitPerMillion: '/ 1M token', - unitPerRequest: '/ 次' - } - }, - - affiliate: { - title: '邀请返利', - description: '邀请新用户注册,并将返利额度转入账户余额', - yourCode: '我的邀请码', - inviteLink: '邀请链接', - copyCode: '复制邀请码', - copyLink: '复制链接', - codeCopied: '邀请码已复制', - linkCopied: '邀请链接已复制', - loadFailed: '加载邀请返利数据失败', - transferFailed: '转入余额失败', - stats: { - rebateRate: '我的返利比例', - rebateRateHint: '被邀请用户每次充值后你可获得的返利比例', - invitedUsers: '邀请人数', - availableQuota: '可转返利额度', - frozenQuota: '冻结中', - frozenQuotaHint: '新产生的返利正在冻结期中', - totalQuota: '历史返利额度' - }, - transfer: { - title: '返利额度转余额', - description: '将当前可用返利额度一键转入账户余额', - button: '转入余额', - transferring: '转入中...', - empty: '当前没有可转入额度', - success: '已转入余额:{amount}' - }, - invitees: { - title: '已邀请用户', - empty: '暂无邀请记录', - columns: { - email: '邮箱', - username: '用户名', - rebate: '返利明细', - joinedAt: '注册时间' - } - }, - tips: { - title: '使用说明', - line1: '将邀请码或邀请链接分享给新用户。', - line2: '被邀请用户充值后,你可获得 {rate} 的返利额度。', - line3: '返利额度可随时转入账户余额。', - line4: '新产生的返利需要经过冻结期后才能提现。' - } - }, - - // Redeem - redeem: { - title: '兑换码', - description: '输入兑换码以充值余额或增加并发数', - currentBalance: '当前余额', - concurrency: '并发数', - requests: '请求', - redeemCodeLabel: '兑换码', - redeemCodePlaceholder: '请输入兑换码', - redeemCodeHint: '兑换码区分大小写', - redeeming: '兑换中...', - redeemButton: '兑换', - redeemSuccess: '兑换成功!', - redeemFailed: '兑换失败', - added: '已添加', - concurrentRequests: '并发请求', - newBalance: '新余额', - newConcurrency: '新并发数', - aboutCodes: '关于兑换码', - codeRule1: '每个兑换码只能使用一次', - codeRule2: '兑换码可以增加余额、并发数或试用权限', - codeRule3: '如有兑换问题,请联系客服', - codeRule4: '余额和并发数即时更新', - recentActivity: '最近活动', - historyWillAppear: '您的兑换历史将显示在这里', - balanceAddedRedeem: '余额充值(兑换)', - balanceAddedAffiliate: '余额充值(返利转入)', - balanceAddedAdmin: '余额充值(管理员)', - balanceDeductedAdmin: '余额扣除(管理员)', - concurrencyAddedRedeem: '并发增加(兑换)', - concurrencyAddedAdmin: '并发增加(管理员)', - concurrencyReducedAdmin: '并发减少(管理员)', - adminAdjustment: '管理员调整', - subscriptionAssigned: '订阅已分配', - subscriptionAssignedDesc: '您已获得 {groupName} 的访问权限', - subscriptionDays: '{days} 天', - days: '天', - codeRedeemSuccess: '兑换成功!', - failedToRedeem: '兑换失败,请检查兑换码后重试。', - subscriptionRefreshFailed: '兑换成功,但订阅状态刷新失败。', - pleaseEnterCode: '请输入兑换码' - }, - - // Profile - profile: { - title: '个人设置', - description: '管理您的账户信息和设置', - accountBalance: '账户余额', - concurrencyLimit: '并发限制', - rpmLimit: 'RPM 限制', - rpmUnlimited: '不限制', - memberSince: '注册时间', - overviewTitle: '账户总览', - overviewDescription: '快速查看账号状态、资料来源与常用设置。', - basicsTitle: '资料与头像', - basicsDescription: '维护公开展示信息,并保持头像与昵称风格一致。', - linkedProfileSources: '资料来源', - linkedProfileSourcesDescription: '部分头像和昵称可能同步自第三方登录方式。', - securityTitle: '安全设置', - securityDescription: '密码、双因素认证和通知提醒集中放在右侧。', - administrator: '管理员', - user: '用户', - username: '用户名', - email: '邮箱', - status: '状态', - role: '角色', - enterUsername: '输入用户名', - editProfile: '编辑个人资料', - updateProfile: '更新资料', - updating: '更新中...', - updateSuccess: '资料更新成功', - updateFailed: '资料更新失败', - usernameRequired: '用户名不能为空', - changePassword: '修改密码', - currentPassword: '当前密码', - newPassword: '新密码', - confirmNewPassword: '确认新密码', - passwordHint: '密码至少需要 8 个字符', - changingPassword: '修改中...', - changePasswordButton: '修改密码', - passwordsNotMatch: '两次输入的密码不一致', - passwordTooShort: '密码至少需要 8 个字符', - passwordChangeSuccess: '密码修改成功', - passwordChangeFailed: '密码修改失败', - // TOTP 2FA - totp: { - title: '双因素认证 (2FA)', - description: '使用 Google Authenticator 等应用增强账户安全', - enabled: '已启用', - enabledAt: '启用时间', - notEnabled: '未启用', - notEnabledHint: '启用双因素认证可以增强账户安全性', - enable: '启用', - disable: '禁用', - featureDisabled: '功能未开放', - featureDisabledHint: '管理员尚未开放双因素认证功能', - setupTitle: '设置双因素认证', - setupStep1: '使用认证器应用扫描下方二维码', - setupStep2: '输入应用显示的 6 位验证码', - manualEntry: '无法扫码?手动输入密钥:', - enterCode: '输入 6 位验证码', - verify: '验证', - setupFailed: '获取设置信息失败', - verifyFailed: '验证码错误,请重试', - enableSuccess: '双因素认证已启用', - disableTitle: '禁用双因素认证', - disableWarning: '禁用后,登录时将不再需要验证码。这可能会降低您的账户安全性。', - enterPassword: '请输入当前密码确认', - confirmDisable: '确认禁用', - disableSuccess: '双因素认证已禁用', - disableFailed: '禁用失败,请检查密码是否正确', - loginTitle: '双因素认证', - loginHint: '请输入您认证器应用显示的 6 位验证码', - loginFailed: '验证失败,请重试', - // New translations for email verification - verifyEmailFirst: '请先验证您的邮箱', - verifyPasswordFirst: '请先验证您的身份', - emailCode: '邮箱验证码', - enterEmailCode: '请输入 6 位验证码', - sendCode: '发送验证码', - codeSent: '验证码已发送到您的邮箱', - sendCodeFailed: '发送验证码失败' - }, - balanceNotify: { - title: '余额不足提醒', - description: '当账户余额低于阈值时发送邮件提醒', - enabled: '启用余额不足提醒', - threshold: '自定义提醒阈值', - thresholdHint: '留空使用系统默认值', - thresholdPlaceholder: '输入金额', - systemDefault: '系统默认值', - extraEmails: '通知邮箱', - extraEmailsHint: '必须添加并验证邮箱后,余额不足时才能收到提醒邮件', - primaryEmail: '主邮箱', - noExtraEmails: '暂无额外通知邮箱', - enterEmail: '输入邮箱地址', - addEmail: '添加邮箱', - emailPlaceholder: '输入邮箱地址', - sendCode: '发送验证码', - resend: '重发', - codeSent: '验证码已发送', - codeSentTo: '验证码已发送到 {email}', - enterCode: '输入验证码', - codePlaceholder: '6位验证码', - verify: '验证', - emailAdded: '邮箱已添加', - emailRemoved: '邮箱已移除', - verifySuccess: '邮箱添加成功', - removeEmail: '移除', - removeSuccess: '邮箱已移除', - emailDuplicate: '该邮箱已存在', - maxEmailsReached: '已达到通知邮箱数量上限', - unverified: '未验证', - verified: '已验证', - }, - avatar: { - title: '资料头像', - description: '仅支持上传头像图片;静态图片会自动压缩到 20KB 以内后再保存。', - uploadAction: '上传图片', - uploadHint: '上传图片时会自动压缩静态图片到 20KB 以内,GIF 需自行控制在 20KB 以内', - uploadRequired: '请先上传头像图片', - saveSuccess: '头像已更新', - deleteSuccess: '头像已删除', - invalidType: '请选择图片文件', - gifTooLarge: 'GIF 头像必须在 20KB 以内', - compressTooLarge: '无法将图片压缩到 20KB 以内,请换一张更小的图片', - compressFailed: '压缩所选图片失败', - readFailed: '读取所选图片失败', - emptyDeleteHint: '当前没有可删除的头像', - }, - authBindings: { - title: '登录方式绑定', - description: '查看当前绑定状态,并将更多第三方登录方式关联到这个账号。', - bindAction: '绑定 {providerName}', - bindSuccess: '账号绑定成功', - emailPlaceholder: '输入邮箱地址', - codePlaceholder: '输入验证码', - passwordPlaceholder: '设置登录密码', - replaceEmailPasswordPlaceholder: '输入当前密码', - sendCodeAction: '发送验证码', - manageEmailAction: '管理邮箱', - hideEmailFormAction: '收起邮箱表单', - confirmEmailBindAction: '绑定邮箱', - confirmEmailReplaceAction: '更换主邮箱', - codeSentTo: '验证码已发送到 {email}', - replaceSuccess: '主邮箱已更新', - unbindAction: '解绑', - unbindSuccess: '{providerName} 已解绑', - boundCount: '已关联 {count} 条记录', - status: { - bound: '已绑定', - notBound: '未绑定', - }, - providers: { - email: '邮箱', - linuxdo: 'LinuxDo', - dingtalk: '钉钉', - oidc: '{providerName}', - wechat: '微信', - }, - notes: { - emailManagedFromProfile: '主邮箱在资料表单中管理', - canUnbind: '你可以解绑这个登录方式。', - bindAnotherBeforeUnbind: '请先绑定其他登录方式,再解除当前绑定。', - }, - source: { - avatar: '头像当前来自 {providerName}', - username: '昵称当前来自 {providerName}', - }, - } - }, - - // Empty States - empty: { - noData: '暂无数据' - }, - - // Table - table: { - expandActions: '展开更多操作', - collapseActions: '收起操作' - }, - - // Pagination - pagination: { - showing: '显示', - to: '至', - of: '共', - results: '条结果', - page: '页', - pageOf: '第 {page} / {total} 页', - previous: '上一页', - next: '下一页', - perPage: '每页', - goToPage: '跳转到第 {page} 页', - jumpTo: '跳转页', - jumpPlaceholder: '页码', - jumpAction: '跳转' - }, - - // Errors - errors: { - somethingWentWrong: '出错了', - pageNotFound: '页面未找到', - unauthorized: '未授权', - forbidden: '禁止访问', - serverError: '服务器错误', - networkError: '网络错误', - timeout: '请求超时', - tryAgain: '请重试' - }, - - // Dates - dates: { - today: '今天', - yesterday: '昨天', - thisWeek: '本周', - lastWeek: '上周', - thisMonth: '本月', - lastMonth: '上月', - last24Hours: '近24小时', - last7Days: '近 7 天', - last14Days: '近 14 天', - last30Days: '近 30 天', - custom: '自定义', - startDate: '开始日期', - endDate: '结束日期', - apply: '应用', - selectDateRange: '选择日期范围' - }, - - // Admin - admin: { - // Dashboard - dashboard: { - title: '管理控制台', - description: '系统概览与统计数据', - apiKeys: 'API 密钥', - totalApiKeys: 'API 密钥总数', - activeApiKeys: '活跃密钥', - users: '用户', - totalUsers: '用户总数', - activeUsers: '活跃用户', - accounts: '账号', - totalAccounts: '账号总数', - activeAccounts: '活跃账号', - todayRequests: '今日请求', - totalRequests: '总请求数', - todayCost: '今日消费', - totalCost: '总消费', - actual: '实际', - standard: '标准', - accountCost: '成本', - todayTokens: '今日 Token', - totalTokens: '总 Token', - input: '输入', - output: '输出', - cacheToday: '今日缓存', - performance: '性能指标', - avgResponse: '平均响应', - averageTime: '平均时间', - timeRange: '时间范围', - granularity: '粒度', - day: '按天', - hour: '按小时', - modelDistribution: '模型分布', - groupDistribution: '分组使用分布', - metricTokens: '按 Token', - metricActualCost: '按实际消费', - tokenUsageTrend: 'Token 使用趋势', - noDataAvailable: '暂无数据', - model: '模型', - group: '分组', - noGroup: '无分组', - requests: '请求', - tokens: 'Token', - cache: '缓存', - recentUsage: '最近使用', - viewModelDistribution: '模型分布', - viewSpendingRanking: '用户消费榜', - spendingRankingTitle: '用户消费榜', - spendingRankingUser: '用户', - spendingRankingRequests: '请求', - spendingRankingTokens: 'Token', - spendingRankingSpend: '消费', - spendingRankingOther: '其他', - spendingRankingUsage: '用量', - spendShort: '消费', - requestsShort: '请求', - tokensShort: 'Token', - last7Days: '近 7 天', - noUsageRecords: '暂无使用记录', - startUsingApi: '开始使用 API 后,使用历史将显示在这里。', - viewAllUsage: '查看全部', - quickActions: '快捷操作', - manageUsers: '管理用户', - viewUserAccounts: '查看和管理用户账户', - manageAccounts: '管理账号', - configureAiAccounts: '配置 AI 平台账号', - batchImage: '批量生图', - batchImageDesc: '提交任务、复制 Agent 调用说明', - groupPricing: '分组定价', - groupPricingDesc: '设置批量折扣和冻结比例', - systemSettings: '系统设置', - configureSystem: '配置系统设置', - failedToLoad: '加载仪表盘数据失败' - }, - - backup: { - title: '数据库备份', - description: '全量数据库备份到 S3 兼容存储,支持定时备份与恢复', - s3: { - title: 'S3 存储配置', - description: '配置 S3 兼容存储(支持 Cloudflare R2)', - descriptionPrefix: '配置 S3 兼容存储(支持', - descriptionSuffix: ')', - enabled: '启用 S3 存储', - endpoint: '端点地址', - region: '区域', - bucket: '存储桶', - prefix: 'Key 前缀', - accessKeyId: 'Access Key ID', - secretAccessKey: 'Secret Access Key', - secretConfigured: '已配置,留空保持不变', - forcePathStyle: '强制路径风格', - testConnection: '测试连接', - testSuccess: 'S3 连接测试成功', - testFailed: 'S3 连接测试失败', - saved: 'S3 配置已保存' - }, - schedule: { - title: '定时备份', - description: '配置自动定时备份', - enabled: '启用定时备份', - cronExpr: 'Cron 表达式', - cronHint: '例如 "0 2 * * *" 表示每天凌晨 2 点', - retainDays: '备份过期天数', - retainDaysHint: '备份文件超过此天数后自动删除,0 = 永不过期', - retainCount: '最大保留份数', - retainCountHint: '最多保留的备份数量,0 = 不限制', - saved: '定时备份配置已保存' - }, - operations: { - title: '备份记录', - description: '创建手动备份和管理已有备份记录', - createBackup: '创建备份', - backing: '备份中...', - backupCreated: '备份创建成功', - expireDays: '过期天数', - alreadyInProgress: '已有备份正在进行中', - backupRunning: '备份进行中...', - backupFailed: '备份失败', - restoreRunning: '恢复进行中...', - restoreFailed: '恢复失败', - }, - columns: { - status: '状态', - fileName: '文件名', - size: '大小', - expiresAt: '过期时间', - triggeredBy: '触发方式', - startedAt: '开始时间', - actions: '操作' - }, - status: { - pending: '等待中', - running: '执行中', - completed: '已完成', - failed: '失败' - }, - progress: { - pending: '准备中', - dumping: '导出数据库', - uploading: '上传中', - }, - trigger: { - manual: '手动', - scheduled: '定时' - }, - neverExpire: '永不过期', - empty: '暂无备份记录', - actions: { - download: '下载', - restore: '恢复', - restoreConfirm: '确定要从此备份恢复吗?这将覆盖当前数据库!', - restorePasswordPrompt: '请输入管理员密码以确认恢复操作', - restoreSuccess: '数据库恢复成功', - deleteConfirm: '确定要删除此备份吗?', - deleted: '备份已删除' - }, - r2Guide: { - title: 'Cloudflare R2 配置教程', - intro: 'Cloudflare R2 提供 S3 兼容的对象存储,免费额度为 10GB 存储 + 每月 100 万次 A 类请求,非常适合数据库备份。', - step1: { - title: '创建 R2 存储桶', - line1: '登录 Cloudflare Dashboard (dash.cloudflare.com),左侧菜单选择「R2 对象存储」', - line2: '点击「创建存储桶」,输入名称(如 sub2api-backups),选择区域', - line3: '点击创建完成' - }, - step2: { - title: '创建 API 令牌', - line1: '在 R2 页面,点击右上角「管理 R2 API 令牌」', - line2: '点击「创建 API 令牌」,权限选择「对象读和写」', - line3: '建议指定存储桶范围(仅允许访问备份桶,更安全)', - line4: '创建后会显示 Access Key ID 和 Secret Access Key', - warning: 'Secret Access Key 只会显示一次,请立即复制保存!' - }, - step3: { - title: '获取 S3 端点地址', - desc: '在 R2 概览页面找到你的账户 ID(在 URL 或右侧面板中),端点格式为:', - accountId: '你的账户 ID' - }, - step4: { - title: '填写以下配置', - checkEnabled: '勾选', - bucketValue: '你创建的存储桶名称', - fromStep2: '第 2 步获取的值', - unchecked: '不勾选' - }, - freeTier: 'R2 免费额度:10GB 存储 + 每月 100 万次 A 类请求 + 1000 万次 B 类请求,对数据库备份完全够用。' - } - }, - - dataManagement: { - title: '数据管理', - description: '统一管理数据管理代理状态、对象存储配置和备份任务', - agent: { - title: '数据管理代理状态', - description: '系统会自动探测固定 Unix Socket,仅在可连通时启用数据管理功能。', - enabled: '数据管理代理已就绪,可继续进行数据管理操作。', - disabled: '数据管理代理不可用,当前仅可查看诊断信息。', - socketPath: 'Socket 路径', - version: '版本', - status: '状态', - uptime: '运行时长', - reasonLabel: '不可用原因', - reason: { - DATA_MANAGEMENT_AGENT_SOCKET_MISSING: '未检测到数据管理 Socket 文件', - DATA_MANAGEMENT_AGENT_UNAVAILABLE: '数据管理代理不可连通', - BACKUP_AGENT_SOCKET_MISSING: '未检测到备份 Socket 文件', - BACKUP_AGENT_UNAVAILABLE: '备份代理不可连通', - UNKNOWN: '未知原因' - } - }, - sections: { - config: { - title: '备份配置', - description: '配置备份源、保留策略与 S3 存储参数。' - }, - s3: { - title: 'S3 对象存储', - description: '配置并测试备份产物上传到标准 S3 对象存储。' - }, - backup: { - title: '备份操作', - description: '触发 PostgreSQL、Redis 与全量备份任务。' - }, - history: { - title: '备份历史', - description: '查看备份任务执行状态、错误与产物信息。' - } - }, - form: { - sourceMode: '源模式', - backupRoot: '备份根目录', - activePostgresProfile: '当前激活 PostgreSQL 配置', - activeRedisProfile: '当前激活 Redis 配置', - activeS3Profile: '当前激活 S3 账号', - retentionDays: '保留天数', - keepLast: '至少保留最近任务数', - uploadToS3: '上传到 S3', - useActivePostgresProfile: '使用当前激活 PostgreSQL 配置', - useActiveRedisProfile: '使用当前激活 Redis 配置', - useActiveS3Profile: '使用当前激活账号', - idempotencyKey: '幂等键(可选)', - secretConfigured: '已配置,留空不变', - source: { - profileID: '配置 ID(唯一)', - profileName: '配置名称', - setActive: '创建后立即设为激活配置' - }, - postgres: { - title: 'PostgreSQL', - host: '主机', - port: '端口', - user: '用户名', - password: '密码', - database: '数据库', - sslMode: 'SSL 模式', - containerName: '容器名(docker_exec 模式)' - }, - redis: { - title: 'Redis', - addr: '地址(host:port)', - username: '用户名', - password: '密码', - db: '数据库编号', - containerName: '容器名(docker_exec 模式)' - }, - s3: { - enabled: '启用 S3 上传', - profileID: '账号 ID(唯一)', - profileName: '账号名称', - endpoint: 'Endpoint(可选)', - region: 'Region', - bucket: 'Bucket', - accessKeyID: 'Access Key ID', - secretAccessKey: 'Secret Access Key', - prefix: '对象前缀', - forcePathStyle: '强制 path-style', - useSSL: '使用 SSL', - setActive: '创建后立即设为激活账号' - } - }, - sourceProfiles: { - createTitle: '创建数据源配置', - editTitle: '编辑数据源配置', - empty: '暂无配置,请先创建', - deleteConfirm: '确定删除配置 {profileID} 吗?', - columns: { - profile: '配置', - active: '激活状态', - connection: '连接信息', - database: '数据库', - updatedAt: '更新时间', - actions: '操作' - } - }, - s3Profiles: { - createTitle: '创建 S3 账号', - editTitle: '编辑 S3 账号', - empty: '暂无 S3 账号,请先创建', - editHint: '点击“编辑”将在右侧抽屉中修改账号信息。', - deleteConfirm: '确定删除 S3 账号 {profileID} 吗?', - columns: { - profile: '账号', - active: '激活状态', - storage: '存储配置', - updatedAt: '更新时间', - actions: '操作' - } - }, - history: { - total: '共 {count} 条', - empty: '暂无备份任务', - columns: { - jobID: '任务 ID', - type: '类型', - status: '状态', - triggeredBy: '触发人', - pgProfile: 'PostgreSQL 配置', - redisProfile: 'Redis 配置', - s3Profile: 'S3 账号', - finishedAt: '完成时间', - artifact: '产物', - error: '错误' - }, - status: { - queued: '排队中', - running: '执行中', - succeeded: '成功', - failed: '失败', - partial_succeeded: '部分成功' - } - }, - actions: { - refresh: '刷新状态', - disabledHint: '请先启动 datamanagementd 并确认 Socket 可连通。', - reloadConfig: '加载配置', - reloadSourceProfiles: '刷新数据源配置', - reloadProfiles: '刷新账号列表', - newSourceProfile: '新建数据源配置', - saveConfig: '保存配置', - configSaved: '配置保存成功', - testS3: '测试 S3 连接', - s3TestOK: 'S3 连接测试成功', - s3TestFailed: 'S3 连接测试失败', - newProfile: '新建账号', - saveProfile: '保存账号', - activateProfile: '设为激活', - profileIDRequired: '请输入账号 ID', - profileNameRequired: '请输入账号名称', - profileSelectRequired: '请先选择要编辑的账号', - profileCreated: 'S3 账号创建成功', - profileSaved: 'S3 账号保存成功', - profileActivated: 'S3 账号已切换为激活', - profileDeleted: 'S3 账号删除成功', - sourceProfileCreated: '数据源配置创建成功', - sourceProfileSaved: '数据源配置保存成功', - sourceProfileActivated: '数据源配置已切换为激活', - sourceProfileDeleted: '数据源配置删除成功', - createBackup: '创建备份任务', - jobCreated: '备份任务已创建:{jobID}({status})', - refreshJobs: '刷新任务', - loadMore: '加载更多' - } - }, - - affiliates: { - invitesDescription: '查看全站邀请关系和被邀请用户累计返利', - rebatesDescription: '查看每一笔产生返利的充值订单', - transfersDescription: '查看返利额度转入账户余额的提取流水', - errors: { - loadFailed: '加载邀请返利记录失败' - }, - records: { - search: '搜索', - searchPlaceholder: '邮箱、用户名、用户 ID、订单号', - startAt: '开始日期', - endAt: '结束日期', - inviter: '邀请人', - invitee: '被邀请人', - user: '用户', - affCode: '邀请码', - order: '订单', - totalRebate: '累计返利', - orderAmount: '充值金额', - payAmount: '支付金额', - rebateAmount: '返利金额', - paymentType: '支付方式', - orderStatus: '订单状态', - transferAmount: '提取金额', - balanceAfter: '提取后余额', - availableQuotaAfter: '提取后可提', - frozenQuotaAfter: '提取后冻结', - historyQuotaAfter: '提取后历史返利', - invitedAt: '邀请时间', - rebatedAt: '返利时间', - transferredAt: '提取时间' - }, - overview: { - title: '用户返利概览', - affCode: '邀请码', - rebateRate: '返利比例', - invitedCount: '邀请人数', - rebatedInviteeCount: '已产生返利人数', - availableQuota: '可提余额', - historyQuota: '历史返利' - } - }, - - // Users Management - users: { - title: '用户管理', - description: '管理用户账户和权限', - createUser: '创建用户', - editUser: '编辑用户', - deleteUser: '删除用户', - deleteConfirmMessage: "确定要删除用户 '{email}' 吗?此操作无法撤销。", - searchPlaceholder: '邮箱/用户名/备注/API Key 模糊搜索...', - searchUsers: '邮箱/用户名/备注/API Key 模糊搜索', - roleFilter: '角色筛选', - allRoles: '全部角色', - allStatus: '全部状态', - allGroups: '全部分组', - searchGroups: '搜索分组...', - fuzzySearch: '模糊搜索', - apiKeyGroupFilter: 'API Key 分组', - apiKeyGroupExclusive: '专用分组', - apiKeyGroupPublic: '公开分组', - apiKeyGroupSubscription: '订阅分组', - apiKeyGroupDisabled: '已禁用分组', - authorizedGroupFilter: '授权分组', - allAuthorizedGroups: '全部授权分组', - searchAuthorizedGroups: '搜索授权分组...', - allApiKeyGroups: '全部 API Key 分组', - searchApiKeyGroups: '搜索 API Key 分组...', - statusFilter: '状态筛选', - allStatuses: '全部状态', - admin: '管理员', - user: '用户', - disabled: '禁用', - email: '邮箱', - password: '密码', - username: '用户名', - notes: '备注', - enterEmail: '请输入邮箱', - enterPassword: '请输入密码', - enterUsername: '请输入用户名(选填)', - enterNotes: '请输入备注(仅管理员可见)', - notesHint: '此备注仅对管理员可见', - enterNewPassword: '请输入新密码(选填)', - leaveEmptyToKeep: '留空则保持原密码不变', - generatePassword: '生成随机密码', - copyPassword: '复制密码', - creating: '创建中...', - updating: '更新中...', - columns: { - user: '用户', - id: 'ID', - email: '邮箱', - username: '用户名', - notes: '备注', - role: '角色', - groups: '分组', - subscriptions: '订阅分组', - balance: '余额', - balancePlatformQuota: '余额(平台配额)', - usage: '用量', - usageAnthropic: '用量 (Claude)', - usageOpenAI: '用量 (OpenAI)', - usageGemini: '用量 (Gemini)', - usageAntigravity: '用量 (Antigravity)', - concurrency: '并发数', - status: '状态', - lastActive: '最后活跃时间', - lastUsed: '最后使用时间', - created: '创建时间', - actions: '操作' - }, - today: '今日', - total: '近30天', - sortBy: '排序方式', - sortCurrentPageOnly: '仅对本页数据排序', - noSubscription: '暂无订阅', - publicGroupCount: '+{count} 公开', - exclusiveLabel: '专属', - publicLabel: '公开', - daysRemaining: '{days}天', - expired: '已过期', - disable: '禁用', - enable: '启用', - disableUser: '禁用用户', - enableUser: '启用用户', - viewApiKeys: '查看 API 密钥', - groups: '分组', - apiKeys: 'API密钥', - userApiKeys: '用户 API 密钥', - noApiKeys: '此用户暂无 API 密钥', - group: '分组', - none: '无', - groupChangedSuccess: '分组修改成功', - groupChangedWithGrant: '分组修改成功,已自动为用户添加「{group}」分组权限', - groupChangeFailed: '分组修改失败', - noUsersYet: '暂无用户', - createFirstUser: '创建您的第一个用户以开始使用系统', - userCreated: '用户创建成功', - userUpdated: '用户更新成功', - userDeleted: '用户删除成功', - userEnabled: '用户已启用', - userDisabled: '用户已禁用', - failedToLoad: '加载用户列表失败', - failedToCreate: '创建用户失败', - failedToUpdate: '更新用户失败', - failedToDelete: '删除用户失败', - failedToToggle: '更新用户状态失败', - failedToLoadApiKeys: '加载用户 API 密钥失败', - deleteConfirm: "确定要删除用户 '{email}' 吗?此操作无法撤销。", - roles: { - admin: '管理员', - user: '用户' - }, - form: { - emailLabel: '邮箱', - emailPlaceholder: '请输入邮箱', - usernameLabel: '用户名', - usernamePlaceholder: '请输入用户名(选填)', - notesLabel: '备注', - notesPlaceholder: '请输入备注(仅管理员可见)', - notesHint: '此备注仅对管理员可见', - passwordLabel: '密码', - passwordPlaceholder: '请输入密码(留空则不修改)', - roleLabel: '角色', - selectRole: '选择角色', - balanceLabel: '余额', - concurrencyLabel: '并发数', - statusLabel: '状态', - selectStatus: '选择状态', - rpmLimit: '每分钟请求数 (RPM)', - rpmLimitPlaceholder: '0 表示不限制', - rpmLimitHint: '该用户每分钟最大请求数,0 = 不限制;仅在所用分组未设置 rpm_limit 时作为兜底生效' - }, - adjustBalance: '调整余额', - adjustConcurrency: '调整并发数', - adjustmentAmount: '调整金额', - adjustmentAmountHint: '正数增加,负数减少', - currentBalance: '当前余额', - currentConcurrency: '当前并发数', - saving: '保存中...', - noUsers: '暂无用户', - noUsersDescription: '创建您的第一个用户以开始使用系统。', - userCreatedSuccess: '用户创建成功', - userUpdatedSuccess: '用户更新成功', - userDeletedSuccess: '用户删除成功', - balanceAdjustedSuccess: '余额调整成功', - concurrencyAdjustedSuccess: '并发数调整成功', - failedToSave: '保存用户失败', - failedToAdjust: '调整失败', - emailRequired: '请输入邮箱', - concurrencyMin: '并发数不能小于1', - soraStorageQuota: 'Sora 存储配额', - soraStorageQuotaHint: '单位 GB,0 表示使用分组或系统默认配额', - amountRequired: '请输入有效金额', - insufficientBalance: '余额不足', - setAllowedGroups: '设置允许分组', - allowedGroupsHint: '选择此用户可以使用的标准分组。订阅类型分组请在订阅管理中配置。', - noStandardGroups: '暂无标准分组', - allowAllGroups: '允许全部分组', - allowAllGroupsHint: '用户可以使用任何非专属分组', - allowedGroupsUpdated: '允许分组更新成功', - failedToLoadGroups: '加载分组列表失败', - failedToUpdateAllowedGroups: '更新允许分组失败', - // 用户分组配置 - groupConfig: '用户分组配置', - groupConfigHint: '为用户 {email} 配置专属分组倍率(覆盖分组默认倍率)', - exclusiveGroups: '专属分组', - publicGroups: '公开分组(默认可用)', - defaultRate: '默认倍率', - customRate: '专属倍率', - useDefaultRate: '使用默认', - customRatePlaceholder: '留空使用默认', - groupConfigUpdated: '分组配置更新成功', - replaceGroup: '替换分组', - clickToReplace: '点击替换分组', - replaceGroupTitle: '替换专属分组', - replaceGroupHint: '选择新分组替换「{old}」,将自动迁移绑定的 Key 并更新分组权限', - replaceGroupConfirm: '确认替换', - replaceGroupSuccess: '分组替换成功,已迁移 {count} 个 Key', - selectNewGroup: '请选择目标分组', - noOtherGroups: '没有其他可用的专属分组', - deposit: '充值', - withdraw: '退款', - depositAmount: '充值金额', - withdrawAmount: '退款金额', - withdrawAll: '全部', - depositNotesPlaceholder: '例如:新用户注册奖励、活动充值、补偿充值等', - withdrawNotesPlaceholder: '例如:服务问题退款、错误充值退回、账户注销退款等', - notesOptional: '备注为可选项,有助于未来查账', - amountHint: '请输入正数金额', - newBalance: '操作后余额', - depositing: '充值中...', - withdrawing: '退款中...', - confirmDeposit: '确认充值', - confirmWithdraw: '确认退款', - depositSuccess: '充值成功', - withdrawSuccess: '退款成功', - failedToDeposit: '充值失败', - failedToWithdraw: '退款失败', - useDepositWithdrawButtons: '请使用充值/退款按钮调整余额', - // 余额变动记录 - balanceHistory: '充值记录', - balanceHistoryTip: '点击查看充值记录', - columnAlwaysVisible: '该列固定显示,不可隐藏', - // 平台用量明细(悬浮显示) - platformBreakdown: '按平台拆分', - platformBreakdownEmpty: '暂无平台明细', - platformBreakdownHint: '悬浮查看各平台用量', - platformOther: '其他', - balanceHistoryTitle: '用户充值和并发变动记录', - noBalanceHistory: '暂无变动记录', - allTypes: '全部类型', - typeBalance: '余额(兑换码)', - typeAffiliateBalance: '余额(返利转入)', - typeAdminBalance: '余额(管理员调整)', - typeConcurrency: '并发(兑换码)', - typeAdminConcurrency: '并发(管理员调整)', - typeSubscription: '订阅', - failedToLoadBalanceHistory: '加载余额记录失败', - createdAt: '创建时间', - totalRecharged: '总充值', - // Settings Dropdowns - filterSettings: '筛选设置', - columnSettings: '列设置', - filterValue: '输入值', - // User Attributes - attributes: { - title: '用户属性配置', - description: '配置用户的自定义属性字段', - configButton: '属性配置', - addAttribute: '添加属性', - editAttribute: '编辑属性', - deleteAttribute: '删除属性', - deleteConfirm: "确定要删除属性 '{name}' 吗?所有用户的该属性值将被删除。", - noAttributes: '暂无自定义属性', - noAttributesHint: '点击上方按钮添加自定义属性', - key: '属性键', - keyHint: '用于程序引用,只能包含字母、数字和下划线', - name: '显示名称', - nameHint: '在表单中显示的名称', - type: '属性类型', - fieldDescription: '描述', - fieldDescriptionHint: '属性的说明文字', - placeholder: '占位符', - placeholderHint: '输入框的提示文字', - required: '必填', - enabled: '启用', - options: '选项配置', - optionsHint: '用于单选/多选类型', - addOption: '添加选项', - optionValue: '选项值', - optionLabel: '显示文本', - validation: '验证规则', - minLength: '最小长度', - maxLength: '最大长度', - min: '最小值', - max: '最大值', - pattern: '正则表达式', - patternMessage: '验证失败提示', - types: { - text: '单行文本', - textarea: '多行文本', - number: '数字', - email: '邮箱', - url: '链接', - date: '日期', - select: '单选', - multi_select: '多选' - }, - created: '属性创建成功', - updated: '属性更新成功', - deleted: '属性删除成功', - reordered: '属性排序更新成功', - failedToLoad: '加载属性列表失败', - failedToCreate: '创建属性失败', - failedToUpdate: '更新属性失败', - keyRequired: '请输入属性键', - nameRequired: '请输入显示名称', - optionsRequired: '请至少添加一个选项', - failedToDelete: '删除属性失败', - failedToReorder: '更新排序失败', - keyExists: '属性键已存在', - dragToReorder: '拖拽排序' - }, - platformQuota: { - menuItem: '平台限额', - title: '平台限额', - subtitle: '为用户 {email} 配置各上游平台的日 / 周 / 月用量上限', - columns: { - platform: '平台', - daily: '日 (USD)', - weekly: '周 (USD)', - monthly: '月 (USD, 30天滚动)', - usage: '当前用量', - }, - placeholder: '不限制', - save: '保存', - saving: '保存中...', - cancel: '取消', - clearAll: '全部清空(取消所有限额)', - clearAllConfirm: '确认清空全部平台的日 / 周 / 月限额?所有平台将变为"无限额",本地无法撤销,需要在保存前手动重填。', - reset: { - button: '重置该窗口', - confirm: '确认重置该用户 {platform} 平台的 {window} 用量?此操作立即生效。', - success: '已重置 {platform} {window} 用量', - failed: '重置失败', - }, - updateSuccess: '平台限额已更新', - updateFailed: '保存失败', - loadFailed: '加载失败', - hint: '留空 = 不限制该窗口。', - windowDaily: '日', - windowWeekly: '周', - windowMonthly: '月', - cellNotConfigured: '未配置', - cellColumnTooltip: '仅展示已设限额的平台', - subscriptionWarning: '此用户有活跃订阅,平台限额仅在余额(标准)模式下生效,订阅模式请求不受此限额约束。', - invalidNumber: '以下字段填写不是合法数字,请修正后再保存:{fields}', - } - }, - - // Groups Management - groups: { - title: '分组管理', - description: '管理 API 密钥分组和费率配置', - searchGroups: '搜索分组...', - createGroup: '创建分组', - editGroup: '编辑分组', - deleteGroup: '删除分组', - sortOrder: '排序', - columnSettings: '列设置', - sortOrderHint: '拖拽分组调整显示顺序,排在前面的分组会优先显示', - sortOrderUpdated: '排序已更新', - failedToUpdateSortOrder: '更新排序失败', - deleteConfirm: "确定要删除分组 '{name}' 吗?所有关联的 API 密钥将不再属于任何分组。", - deleteConfirmSubscription: - "确定要删除订阅分组 '{name}' 吗?此操作会让所有绑定此订阅的用户的 API Key 失效,并删除所有相关的订阅记录。此操作无法撤销。", - columns: { - name: '名称', - platform: '平台', - rateMultiplier: '费率倍数', - rpmOverride: 'RPM 覆盖', - rpmOverrideHint: '该用户在此分组的 RPM 上限;留空 = 使用分组默认;0 = 不限制', - rateDefault: '默认', - rpmDefault: '默认', - exclusive: '独占', - type: '类型', - priority: '优先级', - apiKeys: 'API 密钥数', - accounts: '账号数', - capacity: '容量', - usage: '用量', - status: '状态', - actions: '操作', - billingType: '计费类型', - userName: '用户名', - userEmail: '邮箱', - userNotes: '备注', - userStatus: '状态' - }, - usageToday: '今日', - usageTotal: '累计', - accountsAvailable: '可用:', - accountsRateLimited: '限流:', - accountsTotal: '总量:', - accountsUnit: '个账号', - form: { - name: '名称', - description: '描述', - platform: '平台', - rateMultiplier: '费率倍数', - status: '状态', - exclusive: '专属分组', - nameLabel: '分组名称', - namePlaceholder: '请输入分组名称', - descriptionLabel: '描述', - descriptionPlaceholder: '请输入描述(可选)', - rateMultiplierLabel: '费率倍数', - rateMultiplierHint: '1.0 = 标准费率,0.5 = 半价,2.0 = 双倍', - rpmLimit: '每分钟请求数 (RPM)', - rpmLimitPlaceholder: '0 表示不限制', - rpmLimitHint: '每用户在本分组每分钟最大请求数,0 = 不限制;一旦设置即接管该用户的限流(覆盖用户级 rpm_limit)', - exclusiveLabel: '专属分组', - exclusiveHint: '专属分组,可以手动指定给用户', - platformLabel: '平台限制', - platformPlaceholder: '选择平台(留空则不限制)', - accountsLabel: '指定账号', - accountsPlaceholder: '选择账号(留空则不限制)', - priorityLabel: '优先级', - priorityHint: '数值越小优先级越高,用于账号调度', - statusLabel: '状态' - }, - exclusiveObj: { - yes: '是', - no: '否' - }, - exclusive: '专属', - exclusiveHint: '专属分组,可以手动指定给特定用户', - exclusiveTooltip: { - title: '什么是专属分组?', - description: - '开启后,用户在创建 API Key 时将无法看到此分组。只有管理员手动将用户分配到此分组后,用户才能使用。', - example: '使用场景:', - exampleContent: - '公开分组费率 0.8,您可以创建一个费率 0.7 的专属分组,手动分配给 VIP 用户,让他们享受更优惠的价格。' - }, - rateMultiplierHint: '1.0 = 标准费率,0.5 = 半价,2.0 = 双倍', - platforms: { - all: '全部平台', - anthropic: 'Anthropic', - openai: 'OpenAI', - gemini: 'Gemini', - antigravity: 'Antigravity', - grok: 'Grok', - }, - saving: '保存中...', - noGroups: '暂无分组', - noGroupsDescription: '创建分组以更好地管理 API 密钥和费率。', - groupCreatedSuccess: '分组创建成功', - groupUpdatedSuccess: '分组更新成功', - groupDeletedSuccess: '分组删除成功', - failedToLoad: '加载分组列表失败', - failedToSave: '保存分组失败', - failedToDelete: '删除分组失败', - allPlatforms: '全部平台', - allStatus: '全部状态', - allGroups: '全部分组', - exclusiveFilter: '专属', - nonExclusive: '公开', - public: '公开', - rateAndAccounts: '{rate}x 费率 · {count} 个账号', - accountsCount: '{count} 个账号', - rateLabel: '倍率', - accountFilters: { - title: '账号过滤控制', - oauthOnly: '仅允许 OAuth 账号', - oauthOnlyEnabled: '已启用 — 排除 API Key 类型账号', - privacySetOnly: '仅允许隐私保护已设置的账号', - privacySetOnlyEnabled: '已启用 — Privacy 未设置的账号将被排除', - disabled: '未启用' - }, - enterGroupName: '请输入分组名称', - optionalDescription: '可选描述', - platformHint: '选择此分组关联的平台', - platformNotEditable: '创建后不可更改平台', - noGroupsYet: '暂无分组', - createFirstGroup: '创建您的第一个分组来组织 API 密钥。', - creating: '创建中...', - updating: '更新中...', - limitDay: '日', - limitWeek: '周', - limitMonth: '月', - groupCreated: '分组创建成功', - groupUpdated: '分组更新成功', - groupDeleted: '分组删除成功', - failedToCreate: '创建分组失败', - failedToUpdate: '更新分组失败', - nameRequired: '请输入分组名称', - rateMultipliers: '专属倍率', - rateMultipliersTitle: '分组专属倍率管理', - addUserRate: '添加用户专属倍率', - rpmOverrides: '专属 RPM', - rpmOverridesTitle: '分组专属 RPM 管理', - addUserRpm: '添加用户专属 RPM', - noRpmOverrides: '暂无用户设置了专属 RPM', - rpmSaved: '专属 RPM 已保存', - groupRpmDefault: '分组默认 RPM', - searchUserPlaceholder: '搜索用户邮箱...', - noRateMultipliers: '暂无用户设置了专属倍率', - rateUpdated: '专属倍率已更新', - rateDeleted: '专属倍率已删除', - rateAdded: '专属倍率已添加', - clearAll: '全部清空', - confirmClearAll: '确定要清空该分组所有用户的专属倍率设置吗?此操作不可撤销。', - rateCleared: '已清空所有专属倍率', - batchAdjust: '批量调整倍率', - multiplierFactor: '乘数', - applyMultiplier: '应用', - rateAdjusted: '倍率已批量调整', - rateSaved: '专属倍率已保存', - finalRate: '最终倍率', - unsavedChanges: '有未保存的修改', - revertChanges: '撤销修改', - userInfo: '用户信息', - subscription: { - title: '订阅设置', - type: '计费类型', - typeHint: '标准计费从用户余额扣除。订阅模式使用配额限制。', - typeNotEditable: '分组创建后无法修改计费类型。', - standard: '标准(余额)', - subscription: '订阅(配额)', - dailyLimit: '每日限额(USD)', - weeklyLimit: '每周限额(USD)', - monthlyLimit: '每月限额(USD)', - defaultValidityDays: '默认有效期(天)', - validityHint: '分配给用户时订阅的有效天数', - noLimit: '无限制' - }, - imagePricing: { - title: '图片生成计费', - description: '配置图片生成能力和图片基础单价,留空则使用默认价格', - allowImageGeneration: '允许当前分组生图', - allowBatchImageGeneration: '允许当前分组批量生图', - independentMultiplier: '生图倍率独立', - imageMultiplier: '生图独立倍率', - batchDiscountMultiplier: '批量生图折扣倍率', - batchHoldMultiplier: '批量冻结价格比例', - batchSectionHint: '批量生图仅影响批量任务:结算价格会叠加批量折扣倍率,提交时冻结金额按普通生图原价 × 批量冻结价格比例计算。参考图也会产生上游输入 token 消耗,建议批量生图折扣倍率设置大于 0.5。', - batchDisabledHint: '请先开启当前分组生图,才能开启批量生图。', - batchGeminiOnlyHint: '批量生图当前仅支持 Gemini 分组。', - modeHint: '默认关闭独立倍率时,图片费用 = 图片价格 × 当前分组有效倍率;开启独立倍率后,图片费用 = 图片价格 × 生图独立倍率。', - finalPricePreview: '最终单张价格预览', - notConfigured: '未配置' - }, - peakRate: { - enable: '启用高峰倍率', - peakStart: '高峰开始', - peakEnd: '高峰结束', - peakMultiplier: '高峰倍率', - multiplierHint: '作用于 token 计费倍率;token 计费的图片 token 同样适用,0 表示高峰 token 请求按 0 倍计费' - }, - modelsList: { - title: '自定义 /v1/models 模型列表', - hint: '仅影响 /v1/models 展示结果,不影响白名单模型调用和账号调度。', - loading: '正在加载模型列表...', - empty: '暂无可展示模型', - selectedSummary: '已选 {selected} / {total}', - selectAll: '全选', - invertSelection: '反选' - }, - claudeCode: { - title: 'Claude Code 客户端限制', - tooltip: - '启用后,此分组仅允许 Claude Code 官方客户端访问。非 Claude Code 请求将被拒绝或降级到指定分组。', - enabled: '仅限 Claude Code', - disabled: '允许所有客户端', - fallbackGroup: '降级分组', - fallbackHint: '非 Claude Code 请求将使用此分组,留空则直接拒绝', - noFallback: '不降级(直接拒绝)' - }, - openaiMessages: { - title: 'OpenAI Messages 调度配置', - allowDispatch: '允许 /v1/messages 调度', - allowDispatchHint: '启用后,此 OpenAI 分组的 API Key 可以通过 /v1/messages 端点调度请求', - familyMappingTitle: '系列默认映射', - familyMappingHint: '当请求命中 Opus、Sonnet、Haiku 系列时,会优先使用这里配置的目标模型。', - opusModel: 'Opus 映射模型', - opusModelPlaceholder: '例如: gpt-5.4', - sonnetModel: 'Sonnet 映射模型', - sonnetModelPlaceholder: '例如: gpt-5.3-codex', - haikuModel: 'Haiku 映射模型', - haikuModelPlaceholder: '例如: gpt-5.4-mini', - exactMappingTitle: '精确模型覆盖', - exactMappingHint: '精确 Claude 模型覆盖优先级高于系列默认映射,可将某个具体 Claude 模型单独映射到不同的目标模型。', - noExactMappings: '暂无精确模型覆盖规则', - addExactMapping: '添加精确映射', - claudeModel: 'Claude 模型', - claudeModelPlaceholder: '例如: claude-sonnet-4-5-20250929', - targetModel: '目标模型', - targetModelPlaceholder: '例如: gpt-5.4', - removeExactMapping: '删除精确映射' - }, - invalidRequestFallback: { - title: '无效请求兜底分组', - hint: '仅当上游明确返回 prompt too long 时才会触发,留空表示不兜底', - noFallback: '不兜底' - }, - copyAccounts: { - title: '从分组复制账号', - tooltip: '选择一个或多个相同平台的分组,创建后会自动将这些分组的所有账号绑定到新分组(去重)。', - tooltipEdit: '选择一个或多个相同平台的分组,保存后当前分组的账号会被替换为这些分组的账号(去重)。', - selectPlaceholder: '选择分组以复制其账号...', - hint: '可选多个分组,账号会自动去重', - hintEdit: '⚠️ 注意:这会替换当前分组的所有账号绑定' - }, - modelRouting: { - title: '模型路由配置', - tooltip: - '配置特定模型请求优先路由到指定账号。支持通配符匹配,如 claude-opus-* 匹配所有 opus 模型。', - enabled: '已启用', - disabled: '已禁用', - disabledHint: '启用后,配置的路由规则才会生效', - addRule: '添加路由规则', - modelPattern: '模型模式', - modelPatternPlaceholder: 'claude-opus-*', - modelPatternHint: '支持 * 通配符,如 claude-opus-* 匹配所有 opus 模型', - accounts: '优先账号', - selectAccounts: '选择账号', - noAccounts: '此分组暂无账号', - loadingAccounts: '加载账号中...', - removeRule: '删除规则', - noRules: '暂无路由规则', - noRulesHint: '添加路由规则以将特定模型请求优先路由到指定账号', - searchAccountPlaceholder: '搜索账号...', - accountsHint: '选择此模型模式优先使用的账号' - }, - mcpXml: { - title: 'MCP XML 协议注入', - tooltip: '启用后,当请求包含 MCP 工具时,会在 system prompt 中注入 XML 格式调用协议提示词。关闭此选项可避免对某些客户端造成干扰。', - enabled: '已启用', - disabled: '已禁用' - }, - supportedScopes: { - title: '支持的模型系列', - tooltip: '选择此分组支持的模型系列。未勾选的系列将不会被路由到此分组。', - claude: 'Claude', - geminiText: 'Gemini Text', - geminiImage: 'Gemini Image', - hint: '至少选择一个模型系列' - } - }, - - // Available Channels (aggregated read-only view) - availableChannels: { - title: '可用渠道', - description: '按渠道聚合查看关联分组与支持模型(已展开通配符)', - searchPlaceholder: '搜索渠道或模型...', - columns: { - name: '渠道名', - status: '状态', - billingSource: '计费模型来源', - groups: '关联分组', - supportedModels: '支持模型' - }, - empty: '暂无数据', - noGroups: '未关联分组', - noModels: '未配置模型映射', - noPricing: '未配置定价', - statusActive: '启用', - statusDisabled: '停用', - billingSource: { - requested: '请求模型', - upstream: '上游模型', - channel_mapped: '映射后模型' - }, - pricing: { - billingMode: '计费模式', - billingModeToken: '按 Token', - billingModePerRequest: '按次', - billingModeImage: '按图片', - inputPrice: '输入', - outputPrice: '输出', - cacheWritePrice: '缓存写入', - cacheReadPrice: '缓存读取', - imageOutputPrice: '图片输出', - perRequestPrice: '每次请求', - intervals: '阶梯定价', - unitPerMillion: '/ 1M token', - unitPerRequest: '/ 次' - } - }, - - // Channel Management - channels: { - title: '渠道管理', - description: '管理渠道和自定义模型定价', - searchChannels: '搜索渠道...', - createChannel: '创建渠道', - editChannel: '编辑渠道', - deleteChannel: '删除渠道', - statusActive: '启用', - statusDisabled: '停用', - allStatus: '全部状态', - groupsUnit: '个分组', - pricingUnit: '条定价', - noChannelsYet: '暂无渠道', - createFirstChannel: '创建第一个渠道来管理模型定价', - loadError: '加载渠道列表失败', - createSuccess: '渠道创建成功', - updateSuccess: '渠道更新成功', - deleteSuccess: '渠道删除成功', - createError: '创建渠道失败', - updateError: '更新渠道失败', - deleteError: '删除渠道失败', - nameRequired: '请输入渠道名称', - duplicateModels: '模型「{0}」在多个定价条目中重复', - modelConflict: "模型模式 '{model1}' 和 '{model2}' 冲突:匹配范围重叠。模型名称按大小写不敏感匹配,已有条目已覆盖其所有大小写变体,无需重复添加。", - mappingConflict: "模型映射源 '{model1}' 和 '{model2}' 冲突:匹配范围重叠。源模式按大小写不敏感匹配,已有条目已覆盖其所有大小写变体。", - intervalValidation: { - negativeMin: '区间 #{index}:最小 token 数({value})不能为负数', - maxPositive: '区间 #{index}:最大 token 数({value})必须大于 0', - maxGreaterThanMin: '区间 #{index}:最大 token 数({max})必须大于最小 token 数({min})', - negativePrice: '区间 #{index}:{field}不能为负数', - unboundedLast: '区间 #{index}:无上限区间(最大 token 数为空)必须放在最后', - overlap: '区间 #{previousIndex} 和 #{currentIndex} 重叠:前一个上界({previousMax})大于当前下界({currentMin})', - price: { - inputPrice: '输入价格', - outputPrice: '输出价格', - cacheWritePrice: '缓存写入价格', - cacheReadPrice: '缓存读取价格', - perRequestPrice: '单次价格' - } - }, - deleteConfirm: '确定要删除渠道「{name}」吗?此操作不可撤销。', - columns: { - name: '名称', - description: '描述', - status: '状态', - groups: '分组', - pricing: '定价', - createdAt: '创建时间', - actions: '操作' - }, - billingMode: { - token: 'Token', - perRequest: '按次', - image: '图片(按次)' - }, - form: { - name: '名称', - namePlaceholder: '输入渠道名称', - description: '描述', - descriptionPlaceholder: '可选描述', - status: '状态', - groups: '关联分组', - noGroupsAvailable: '暂无可用分组', - inOtherChannel: '已属于「{name}」', - modelPricing: '模型定价', - models: '模型列表', - modelsPlaceholder: '输入完整模型名后按回车添加', - modelInputHint: '按回车添加,支持粘贴批量导入', - billingMode: '计费模式', - defaultPrices: '默认价格(未命中区间时使用)', - inputPrice: '输入', - outputPrice: '输出', - cacheWritePrice: '缓存写入', - cacheReadPrice: '缓存读取', - cacheWritePriceShort: '缓存写', - cacheReadPriceShort: '缓存读', - imageTokenPrice: '图片输出', - imageOutputPrice: '图片输出价格', - pricePlaceholder: '默认', - intervals: '上下文区间定价(可选)', - minTokens: '最小', - maxTokens: '最大', - inclusive: '(含)', - addInterval: '添加区间', - requestTiers: '按次计费层级', - imageTiers: '图片计费层级(按次)', - addTier: '添加层级', - noTiersYet: '暂无层级,点击添加配置按次计费价格', - noPricingRules: '暂无定价规则,点击"添加"创建', - perRequestPrice: '单次价格', - perRequestPriceRequired: '按次/图片计费模式必须设置默认价格或至少一个计费层级', - tierLabel: '层级', - resolution: '分辨率', - modelMapping: '模型映射', - modelMappingHint: '将请求中的模型名映射为实际模型名。在账号级别映射之前执行。', - noMappingRules: '暂无映射规则,点击"添加"创建', - mappingSource: '源模型', - mappingTarget: '目标模型', - billingModelSource: '计费基准', - billingModelSourceChannelMapped: '以渠道映射后的模型计费', - billingModelSourceRequested: '以请求模型计费', - billingModelSourceUpstream: '以最终模型计费', - billingModelSourceHint: '控制使用哪个模型名称进行定价查找', - selectedCount: '已选 {count} 个', - searchGroups: '搜索分组...', - noGroupsMatch: '没有匹配的分组', - restrictModels: '限制模型', - restrictModelsHint: '开启后,仅允许模型定价列表中的模型。不在列表中的模型请求将被拒绝。', - defaultPerRequestPrice: '默认单次价格(未命中层级时使用)', - defaultImagePrice: '默认图片价格(未命中层级时使用)', - platformConfig: '平台配置', - webSearchEmulation: 'Web Search 模拟', - webSearchEmulationHint: '⚠️ 开启后该渠道下所有 Anthropic 分组的账号将自动拦截 web_search 请求,请谨慎操作', - webSearchEmulationGlobalDisabled: '请先在系统设置 → 网关 → Web Search 模拟中启用全局开关', - codexImageGenerationBridge: 'Codex 图片生成桥接', - codexImageGenerationBridgeHint: '开启后,OpenAI 分组的 Codex /responses 文本请求可能会被自动注入 image_generation 工具。仅在路由账号支持图片生成时开启。', - bedrockCCCompat: 'Bedrock CC 兼容', - bedrockCCCompatHint: '⚠️ 开启后,该渠道下 Bedrock 账号的请求将进行 Claude Code 兼容处理(thinking 类型转换、tool_use ID 清理)', - basicSettings: '基础设置', - addPlatform: '添加平台', - noPlatforms: '点击"添加平台"开始配置渠道', - mappingCount: '条映射', - pricingEntry: '定价配置', - noModels: '未添加模型', - applyPricingToAccountStats: '应用模型定价到账号统计', - applyPricingToAccountStatsDesc: '启用后,未被自定义规则匹配的请求将使用模型定价文件中的标准价格计算账号统计费用', - accountStatsPricingRules: '自定义账号统计定价规则', - addRule: '添加规则', - noRulesConfigured: '未配置自定义规则,将使用上方的模型定价。', - ruleName: '规则名称(可选)', - ruleGroups: '分组', - ruleAccounts: '账号', - searchAccountPlaceholder: '搜索账号...', - ruleAccountsHint: '留空表示匹配所有账号', - ruleModelPricing: '模型定价', - noGroupsInChannel: '上方平台标签页中未选择分组', - unnamed: '未命名', - syncLatestModels: '同步最新模型', - syncingModels: '同步中...', - syncModelsSuccess: '已同步 {count} 个新模型', - syncModelsAlreadyUpToDate: '模型列表已是最新', - syncModelsError: '同步模型失败' - } - }, - - riskControl: { - title: '风控中心', - description: '配置内容审计策略并查看审核记录', - loadFailed: '加载风控中心失败', - saveFailed: '保存内容审计配置失败', - logsFailed: '加载审核记录失败', - saved: '内容审计配置已保存', - refresh: '刷新', - config: '内容审计配置', - configHint: '调用 OpenAI Moderations 进行请求内容评分,命中阈值后按模式处理。', - openSettings: '内容审计设置', - settingsTitle: '内容审计设置', - refreshStatus: '刷新状态', - records: '审核记录', - recordsHint: '展示命中、拦截、异常和已采样记录。', - saveConfig: '保存内容审计配置', - statusFailed: '加载运行状态失败', - enabled: '开启内容审计', - enabledHint: '关闭后即使风控中心菜单启用,也不会审核网关请求。', - mode: '全局模式', - modePreBlock: '前置拦截', - modePreBlockDesc: '每次请求先同步审核最新用户输入,命中后立即拒绝请求。', - modeObserve: '仅观察', - modeObserveDesc: '请求直接放行,最新用户输入进入异步审核队列;命中后只记录、通知和按规则累计。', - modeOff: '关闭', - modeOffDesc: '不执行内容审计,也不会写入审核记录。', - baseUrl: 'OpenAI Base URL', - model: '模型名', - apiKey: 'OpenAI API Key', - apiKeys: 'OpenAI API Keys', - apiKeyCount: '{count} 个 Key', - apiKeyPlaceholder: '请输入 API Key', - apiKeysPlaceholder: '新增 API Key,每行一个;保存后会追加到已保存 Key', - apiKeysPlaceholderReplace: '覆盖保存 API Key,每行一个;保存后会替换全部已保存 Key', - apiKeysPlaceholderKeep: '新增 API Key,每行一个;保存后会追加到已保存 Key', - apiKeysHint: '当前已保存 {count} 个 Key;输入区只用于新增,保存时会增量追加并自动去重。', - apiKeysWriteMode: '写入方式', - apiKeysModeAppend: '增量添加', - apiKeysModeReplace: '覆盖保存', - apiKeysModeAppendHint: '默认模式:保存时追加输入区 Key,并保留已保存 Key。', - apiKeysModeReplaceHint: '覆盖模式:保存时用输入区 Key 替换全部已保存 Key。', - apiKeysReplaceWarning: '覆盖模式', - apiKeysReplaceNoInput: '覆盖保存至少需要输入 1 个 API Key', - apiKeyPlaceholderKeep: '留空保持不变', - apiKeyWillClear: '保存后清除已配置 Key', - apiKeyConfigured: '已配置', - apiKeyTemporary: '待保存', - apiKeyPendingDelete: '待删除', - apiKeyPendingDeleteCount: '待删除 {count} 个 Key', - deleteApiKey: '删除这个 Key', - undoDeleteApiKey: '撤销删除', - inputApiKeyCount: '输入区 {count} 个 Key', - storedApiKeyCount: '已保存 {count} 个 Key', - testInputApiKeys: '测试输入区 Key', - testStoredApiKeys: '测试已保存 Key', - testContentWithStoredApiKey: '用已保存 Key 试跑内容', - testingApiKeys: '测试中', - apiKeyTestNoInput: '请先输入需要测试的 OpenAI API Key', - apiKeyTestDone: 'Key 测试完成,共 {count} 个', - apiKeyTestFailed: '测试 OpenAI API Key 失败', - apiKeyHealth: 'Key 可用状态', - apiKeyFreezeRule: '400 不冻结;401/403 冻结 10 分钟;429/529 冻结 1 分钟;其他 HTTP 错误冻结 10 秒。', - apiKeyRows: '{count} 个 Key', - apiKeyRowsCollapsed: '已隐藏 {count} 个 Key', - apiKeyRowsExpanded: '正在显示全部 {count} 个 Key', - expandApiKeyRows: '展开', - collapseApiKeyRows: '收起', - apiKeyHealthEmpty: '暂无 Key 状态', - apiKeyHealthEmptyHint: '保存 Key 或测试输入区 Key 后会显示可用性。', - apiKeyStatusOk: '可用', - apiKeyStatusError: '异常', - apiKeyStatusFrozen: '冻结', - apiKeyStatusUnknown: '未测试', - apiKeyFailureCount: '失败 {count} 次', - apiKeyLatency: '{ms} ms', - apiKeyHTTPStatus: 'HTTP {status}', - apiKeyFrozenUntil: '冻结至 {time}', - apiKeyLastChecked: '检查于 {time}', - apiKeyNotTested: '尚未测试', - auditTestInput: '审计试跑输入', - auditTestInputHint: '可填写提示词并上传或粘贴图片;图片以 base64 发送,不会保存文件。', - auditTestPromptPlaceholder: '输入要测试的用户提示词;留空时仅测试 Key 可用性。', - auditTestImages: '测试图片', - auditTestImagesHint: '支持上传、拖拽或粘贴图片,最多 1 张,每张不超过 8MB。', - addAuditTestImage: '添加图片', - clearAuditTest: '清空试跑', - auditTestImageLimit: '最多只能添加 {count} 张测试图片', - auditTestImageTooLarge: '单张测试图片不能超过 8MB', - auditTestImageReadFailed: '读取测试图片失败', - auditTestResult: '审计试跑结果', - auditTestHighest: '最高分类 {category},分数 {score}', - auditTestComposite: '综合评分', - auditTestFlagged: '命中阈值', - auditTestPassed: '未命中', - notConfigured: '未配置', - clearApiKey: '清除已保存 Key', - keepApiKey: '保留已保存 Key', - timeoutMs: 'HTTP 超时 (ms)', - retryCount: '失败重试次数', - sampleRate: '采样率', - recordNonHits: '记录未命中输入', - recordNonHitsHint: '开启后会记录抽样但未命中的请求摘要,摘要会先脱敏再入库。', - preHashCheck: '启用前置哈希比对', - preHashCheckHint: '异步审核命中过的输入哈希会被前置拦截;该拦截不发送邮件,也不累计封禁次数。', - flaggedHashCount: '当前哈希集合数量:{count} 个', - flaggedHashHint: '哈希永久保存在 Redis 集合中;可粘贴完整 64 位哈希删除误拦截项,或一键清空全部风险哈希。', - flaggedHashPlaceholder: '粘贴完整 64 位输入哈希', - deleteFlaggedHash: '删除指定哈希', - clearFlaggedHashes: '一键清空', - clearFlaggedHashesConfirm: '确定要清空全部风险输入哈希吗?此操作不会删除审核记录,但会取消所有历史哈希拦截。', - flaggedHashDeleted: '风险哈希已删除', - flaggedHashNotFound: '该风险哈希不存在', - flaggedHashDeleteFailed: '删除风险哈希失败', - flaggedHashesCleared: '已清空 {count} 个风险哈希', - flaggedHashesClearFailed: '清空风险哈希失败', - workerCount: 'Worker 数', - queueSize: '异步队列大小', - blockStatus: '拦截 HTTP 状态码', - blockMessage: '自定义拦截提示', - defaultBlockMessage: '内容审计命中风险规则,请调整输入后重试', - emailOnHit: '命中后发送邮件', - emailOnHitHint: '开启后每次达到阈值都会向用户发送风控提醒邮件;自动封禁通知始终发送。', - autoBan: '自动封禁用户', - autoBanHint: '命中次数达到阈值后将禁用用户账号、刷新认证缓存并发送封禁通知邮件。', - cyberPolicyExcludeBan: 'cyber_policy 不计入封号次数', - cyberPolicyExcludeBanHint: '开启后,cyber_policy 拦截不再计入自动封号的违规次数:当次不判定封号,历史累计亦排除。风控日志与通知邮件照常。', - violationNotCounted: '未计入封号', - banThreshold: '封禁触发次数', - violationWindowHours: '累计窗口(小时)', - hitRetentionDays: '命中记录保留(天)', - nonHitRetentionDays: '未命中记录保留(天,最多 3 天)', - violationCount: '{count} 次', - emailSent: '已发邮件', - emailNotSent: '未发邮件', - autoBanned: '已封禁', - unbanUser: '解封', - unbanSuccess: '用户已解封', - unbanFailed: '解封用户失败', - inputDetailTitle: '输入摘要详情', - inputDetailContent: '完整内容', - matchedKeyword: '命中关键词', - queueDelay: '排队 {ms} ms', - allGroups: '全部分组', - allGroupsHint: '当前审计全部分组', - selectedGroupsHint: '当前审计指定分组', - groupScope: '审计分组', - groupScopeHint: '开启右侧开关表示全部分组,关闭后选择指定分组。', - selectedGroups: '指定分组', - searchGroups: '搜索分组名称或平台', - noGroups: '暂无可用分组', - modelFilter: '模型范围', - modelFilterHint: '按客户端请求的模型名决定是否执行内容审计,模型映射后仍以请求模型判断。', - modelFilterAll: '所有模型', - modelFilterAllDesc: '所有模型请求都会进入内容审计。', - modelFilterInclude: '仅指定模型', - modelFilterIncludeDesc: '只有列表中的模型会执行内容审计。', - modelFilterExclude: '排除指定模型', - modelFilterExcludeDesc: '列表中的模型跳过内容审计,其余模型执行审计。', - modelFilterModels: '模型列表', - modelFilterModelCount: '已配置 {count} 个模型', - modelFilterModelsRequired: '当前模型范围至少需要配置 1 个模型', - modelFilterAllSummary: '全部模型生效', - modelFilterIncludeSummary: '仅 {count} 个模型生效', - modelFilterExcludeSummary: '排除 {count} 个模型', - emptyLogs: '暂无审核记录', - preBlockSyncStatus: '前置拦截同步状态', - preBlockSyncHint: '同步审核链路的实时计数,不包含异步写记录任务。', - preBlockActive: '同步处理中', - preBlockActiveHint: '当前正在审核', - preBlockChecked: '已检查', - preBlockCheckedHint: '进入前置拦截链路', - preBlockAllowed: '已放行', - preBlockAllowedHint: '未触发拦截', - preBlockBlocked: '已拦截', - preBlockBlockedHint: '命中后拒绝请求', - preBlockErrors: '审核异常', - preBlockErrorsHint: '失败或无可用 Key', - preBlockAvgLatency: '平均耗时', - preBlockAvgLatencyHint: '同步链路平均值', - preBlockAPIKeyLoad: '审核 Key 负载', - preBlockAPIKeyLoadHint: '同步前置拦截直接轮询可用审核 Key。', - preBlockAPIKeyLoadSummary: '同步并发 {active} / 可用 Key {available},累计 {total} 次,worker:{workerActive} / {workerTotal}', - preBlockAPIKeyTotals: '累计 {total},成功 {success},异常 {errors}', - preBlockAPIKeyLoadEmpty: '暂无审核 Key 负载数据', - preBlockKeyActiveShort: '并发', - preBlockKeyTotalShort: '累计', - preBlockKeyAvgShort: '平均', - preBlockKeyLastShort: '最近', - workerStatus: 'Worker 运行状态', - workerStatusHint: '异步审计任务和前置拦截记录任务的队列与 Worker 池状态,不包含同步前置拦截审核请求。', - workerPool: 'Worker 池', - workerPoolMeta: '{active} 个处理中,{idle} 个空闲可用,共 {total} 个', - queueUsage: '队列占用', - activeWorkers: '处理中', - idleWorkers: '空闲可用', - workerActive: '正在处理异步审计或记录任务', - workerIdle: '已启动,当前空闲可用', - workerDisabled: '风控或内容审计未启用', - processed: '已处理', - droppedErrors: '丢弃/异常', - autoRefresh: '每 15 秒自动刷新', - lastCleanup: '上次清理:{time}', - cleanupStats: '上次清理删除命中 {hit} 条,未命中 {nonHit} 条', - riskSwitchOff: '系统开关关闭', - riskThresholds: '风险阈值', - riskThresholdsHint: '按 OpenAI Moderations 分类调整命中阈值,分数达到或超过阈值即视为命中。', - riskThresholdDefault: '默认 {value}', - riskThresholdReset: '恢复默认阈值', - riskThresholdPercent: '阈值百分比', - tabs: { - basic: '基础', - scope: '审计范围', - runtime: '运行队列', - response: '命中通知', - riskThresholds: '风险阈值', - keywords: '关键词拦截', - retention: '日志保留', - }, - blockedKeywords: '拦截关键词', - blockedKeywordsPlaceholder: '每行输入一个关键词,例如:\n敏感词1\n敏感词2', - blockedKeywordsDescription: '匹配忽略大小写;命中后会按下方策略决定是否调用上游审计接口。', - blockedKeywordsPreBlockHint: '关键词拦截仅在「前置拦截」模式下生效。', - blockedKeywordsModeWarning: '当前为「{mode}」模式,关键词拦截不会生效;请切换到「前置拦截」模式后再保存关键词。', - blockedKeywordCount: '已配置 {count} 个关键词', - blockedKeywordsLimit: '最多保存 {max} 个关键词,单个长度不超过 200 个字符;重复项会自动去重。', - keywordBlockingMode: '审计策略', - keywordModeKeywordAndApi: '关键词 + API', - keywordModeKeywordAndApiDesc: '命中关键词直接拦截;未命中时再调用上游审计接口。', - keywordModeKeywordOnly: '仅关键词', - keywordModeKeywordOnlyDesc: '只用关键词判断,未命中即放行,不调用上游审计接口,可显著降低 API 用量。', - keywordModeKeywordOnlyNotice: '当前为「仅关键词」策略:未命中关键词的请求将直接放行,不调用上游审计接口。', - keywordModeApiOnly: '仅 API', - keywordModeApiOnlyDesc: '只调用上游审计接口判断,本页的关键词列表将不会生效。', - keywordModeApiOnlyNotice: '当前为「仅 API」策略:关键词列表不会生效,请求会全部交给上游审计接口判断。', - overview: { - status: '运行状态', - enabled: '已启用', - disabled: '未启用', - apiKey: 'API Key', - groupScope: '审计范围', - logs: '审核记录', - currentFilter: '当前筛选结果', - }, - filters: { - search: '按用户/Key/摘要搜索', - from: '开始时间', - to: '结束时间', - allGroups: '全部分组', - allEndpoints: '全部端点', - }, - table: { - time: '时间', - group: '分组', - user: '用户', - apiKey: 'API Key', - endpoint: '端点', - result: '结果', - highest: '最高分', - actionMeta: '处置', - latency: '上游耗时', - input: '输入摘要', - }, - result: { - all: '全部结果', - hit: '命中', - blocked: '已拦截', - pass: '未命中', - error: '异常', - }, - action: { - block: '拦截', - keywordBlock: '关键词拦截', - cyberPolicy: '网络安全策略', - error: '异常', - }, - }, - - // Channel Monitor - channelMonitor: { - title: '渠道监控', - description: '监测各渠道的可用性、延迟和状态', - searchPlaceholder: '搜索监控名称...', - allProviders: '全部供应商', - allStatus: '全部状态', - enabledFilter: '启用状态', - onlyEnabled: '仅启用', - onlyDisabled: '仅禁用', - createButton: '新增监控', - createTitle: '新增渠道监控', - editTitle: '编辑渠道监控', - runNow: '立即检测', - runSuccess: '检测完成', - runFailed: '检测失败', - apiKeyDecryptFailed: 'API Key 解密失败,请重新编辑该监控并填入新的 Key', - createSuccess: '监控创建成功', - updateSuccess: '监控更新成功', - deleteSuccess: '监控删除成功', - loadError: '加载监控列表失败', - deleteConfirm: '确定要删除监控「{name}」吗?此操作不可撤销。', - nameRequired: '请输入监控名称', - primaryModelRequired: '请输入主模型', - columns: { - name: '名称', - provider: '供应商', - primaryModel: '主模型', - availability7d: '7 天可用率', - latency: '延迟 (ms)', - enabled: '启用', - actions: '操作' - }, - form: { - name: '名称', - namePlaceholder: '输入监控名称', - provider: '平台', - apiMode: 'OpenAI 协议', - apiModeChatCompletions: 'OpenAI Compatible', - apiModeChatCompletionsHint: '使用 /v1/chat/completions,发送 messages;适合大多数兼容站。', - apiModeResponses: 'Responses API', - apiModeResponsesHint: '使用 /v1/responses,默认带 instructions + input;适合本站自检/Codex。', - endpoint: '上游地址', - endpointPlaceholder: 'https://api.example.com', - useCurrentDomain: '使用当前服务', - apiKey: 'API Key', - apiKeyPlaceholder: '请输入 API Key', - apiKeyEditPlaceholder: '留空表示不修改', - useMyKey: '使用我的 Key', - selectKeyTitle: '选择我的 API Key', - selectKeyHint: '仅显示当前账号下处于「启用」状态且未过期的 Key。', - noActiveKey: '没有可用的启用状态 Key', - primaryModel: '主模型', - primaryModelPlaceholder: 'gpt-4o-mini', - extraModels: '附加模型', - extraModelsPlaceholder: '回车添加附加模型', - groupName: '分组名称', - groupNamePlaceholder: '可选,用于在用户视图中聚合显示', - intervalSeconds: '检测间隔 (秒)', - intervalSecondsHint: '范围:15 - 3600 秒', - jitterSeconds: '随机抖动 (± 秒)', - jitterSecondsHint: '每次检测在间隔基础上正负随机偏移该秒数,0 表示固定间隔;需满足 间隔 - 抖动 ≥ 15 秒', - enabled: '启用监控', - kindRequired: '请选择供应商' - }, - runResultTitle: '检测结果', - noMonitorsYet: '暂无监控', - createFirstMonitor: '创建第一个监控来跟踪渠道可用性', - advanced: { - section: '高级(可选)', - sectionHint: '自定义请求头和请求体,用于突破上游的客户端识别限制(如仅允许 Claude Code 客户端)。', - headers: '自定义请求头', - headersPlaceholder: 'User-Agent: claude-cli/1.0.83 (external, cli)\nx-app: cli\nanthropic-beta: claude-code-20250219', - headerNamePlaceholder: 'Header 名', - headerValuePlaceholder: 'Value', - headerAddRow: '添加 Header', - headerNameInvalid: 'Header 名不能包含空格或冒号:{name}', - headersHint: '与默认请求头合并,用户值优先。hop-by-hop 类 header(Host/Content-Length/...)会被忽略。', - headersParseError: '无法解析这一行:{line}', - bodyMode: '请求体处理', - bodyModeOff: '默认', - bodyModeMerge: '合并', - bodyModeReplace: '覆盖', - bodyModeHintOff: '使用 adapter 默认请求体(带 challenge 数学题校验)。', - bodyModeHintMerge: '与默认请求体浅合并,用户字段优先;但 model / messages / contents 会被保护不允许覆盖(动这些字段请用「覆盖」模式)。', - bodyModeHintReplace: '完全用下方 JSON 作为请求体。注意:此模式下跳过 challenge 校验,改为 HTTP 2xx + 响应文本非空即视为可用。', - bodyJson: 'Body JSON', - bodyJsonFormat: '格式化', - bodyJsonHint: '失焦时自动解析校验。留空等价于没有覆盖。', - bodyJsonError: 'JSON 解析失败', - bodyJsonObjectError: '请求体必须是一个 JSON 对象(不能是数组或基本类型)' - }, - templateField: { - label: '请求模板', - none: '不使用模板', - placeholder: '选择一个模板(按当前平台过滤)', - applyHint: '选中模板后,会把模板的请求头和请求体拷贝到此监控(快照)。后续模板变动不自动同步。' - }, - template: { - manageButton: '模板管理', - managerTitle: '请求模板管理', - createButton: '新建模板', - emptyState: '当前平台下还没有请求模板', - missingName: '请输入模板名称', - createSuccess: '模板创建成功', - updateSuccess: '模板更新成功', - deleteSuccess: '模板删除成功', - applyButton: '应用到关联监控', - applyTooltip: '把当前模板配置覆盖到所有关联的监控上', - applyTitle: '应用模板', - applyConfirm: '确认应用', - applyConfirmMessage: '将把模板「{name}」的当前配置覆盖到 {n} 个关联监控。监控本地已编辑的自定义修改会被丢弃,是否继续?', - applySuccess: '已应用到 {n} 个监控', - applyPickerTitle: '应用模板「{name}」', - applyPickerHint: '勾选要覆盖请求头/请求体的监控(默认全选)。监控本地已编辑的自定义修改会被丢弃。', - applyPickerEmpty: '当前模板没有关联监控', - applyPickerConfirm: '应用到 {n} 个监控', - selectNone: '全不选', - selectedCount: '已选 {n} / {total}', - deleteConfirm: '确定要删除模板「{name}」吗?{n} 个关联监控会解除关联但保留自己的快照继续工作。', - associatedCount: '{n} 个关联监控', - headersSummary: '{n} 个自定义请求头', - form: { - name: '模板名称', - namePlaceholder: '例:Claude Code 伪装', - description: '说明', - descriptionPlaceholder: '可选:说明这个模板的用途和来源(抓包日期等)' - } - } - }, - - // Subscriptions Management - subscriptions: { - title: '订阅管理', - description: '管理用户订阅和配额限制', - assignSubscription: '分配订阅', - adjustSubscription: '调整订阅', - revokeSubscription: '撤销订阅', - restoreSubscription: '恢复订阅', - allStatus: '全部状态', - allGroups: '全部分组', - allPlatforms: '全部平台', - daily: '每日', - weekly: '每周', - monthly: '每月', - noLimits: '未配置限额', - unlimited: '无限制', - resetNow: '即将重置', - windowNotActive: '窗口未激活', - resetInMinutes: '{minutes} 分钟后重置', - resetInHoursMinutes: '{hours} 小时 {minutes} 分钟后重置', - resetInDaysHours: '{days} 天 {hours} 小时后重置', - quotaEndsInMinutes: '额度将在 {minutes} 分钟后结束', - quotaEndsInHoursMinutes: '额度将在 {hours} 小时 {minutes} 分钟后结束', - quotaEndsInDaysHours: '额度将在 {days} 天 {hours} 小时后结束', - daysRemaining: '天剩余', - remainingDays: '剩余天数', - noExpiration: '无过期时间', - status: { - active: '生效中', - expired: '已过期', - revoked: '已撤销', - suspended: '已暂停' - }, - columns: { - user: '用户', - group: '分组', - usage: '用量', - expires: '到期时间', - status: '状态', - actions: '操作' - }, - form: { - user: '用户', - group: '订阅分组', - validityDays: '有效期(天)', - adjustDays: '调整天数' - }, - selectUser: '选择用户', - selectGroup: '选择订阅分组', - groupHint: '仅显示订阅计费类型的分组', - validityHint: '订阅的有效天数', - adjustingFor: '为以下用户调整订阅', - currentExpiration: '当前到期时间', - adjustDaysPlaceholder: '正数延长,负数缩短', - adjustHint: '输入正数延长订阅,负数缩短订阅(缩短后剩余天数需大于0)', - assign: '分配', - assigning: '分配中...', - adjust: '调整', - adjusting: '调整中...', - revoke: '撤销', - restore: '恢复', - resetQuota: '重置配额', - resetQuotaTitle: '重置用量配额', - resetQuotaConfirm: "确定要重置 '{user}' 的每日、每周和每月用量配额吗?用量将归零并从今天开始重新计算。", - quotaResetSuccess: '配额重置成功', - failedToResetQuota: '重置配额失败', - noSubscriptionsYet: '暂无订阅', - assignFirstSubscription: '分配一个订阅以开始使用。', - subscriptionAssigned: '订阅分配成功', - subscriptionAdjusted: '订阅调整成功', - subscriptionRevoked: '订阅撤销成功', - subscriptionRestored: '订阅已恢复', - failedToLoad: '加载订阅列表失败', - failedToAssign: '分配订阅失败', - failedToAdjust: '调整订阅失败', - failedToRevoke: '撤销订阅失败', - failedToRestore: '恢复订阅失败', - adjustWouldExpire: '调整后剩余天数必须大于0', - adjustOutOfRange: '调整天数必须在 -36500 到 36500 之间', - pleaseSelectUser: '请选择用户', - pleaseSelectGroup: '请选择分组', - validityDaysRequired: '请输入有效的天数(至少1天)', - revokeConfirm: "确定要撤销 '{user}' 的订阅吗?可稍后在已撤销列表中恢复。", - restoreConfirm: "确定要恢复 '{user}' 的订阅吗?如果原订阅已过期,恢复后将显示为已过期。", - guide: { - title: '订阅管理教程', - subtitle: '订阅模式允许你按时间周期为用户分配使用额度,支持日/周/月配额限制。按照以下步骤即可完成配置。', - showGuide: '使用指南', - step1: { - title: '创建订阅分组', - line1: '前往「分组管理」页面,点击「创建分组」', - line2: '将计费类型设为「订阅」,配置日/周/月额度限制', - line3: '保存分组,确保状态为「正常」', - link: '前往分组管理' - }, - step2: { - title: '分配订阅给用户', - line1: '点击本页右上角「分配订阅」按钮', - line2: '在弹窗中搜索用户邮箱并选择目标用户', - line3: '选择订阅分组、设置有效期天数,点击「分配」' - }, - step3: { - title: '管理已有订阅' - }, - actions: { - adjust: '调整', - adjustDesc: '延长或缩短订阅有效期', - resetQuota: '重置配额', - resetQuotaDesc: '将日/周/月用量归零,重新开始计算', - revoke: '撤销', - revokeDesc: '立即终止该用户的订阅,可在已撤销列表中恢复' - }, - tip: '提示:订阅分组下拉列表中只会显示计费类型为「订阅」且状态为「正常」的分组。如果没有可选项,请先到分组管理中创建。' - } - }, - - // Accounts Management - accounts: { - title: '账号管理', - description: '管理 AI 平台账号和 Cookie', - createAccount: '添加账号', - autoRefresh: '自动刷新', - enableAutoRefresh: '启用自动刷新', - refreshInterval5s: '5 秒', - refreshInterval10s: '10 秒', - refreshInterval15s: '15 秒', - refreshInterval30s: '30 秒', - autoRefreshCountdown: '自动刷新:{seconds}s', - listPendingSyncHint: '列表存在待同步变更,点击同步可补齐最新数据。', - listPendingSyncAction: '立即同步', - syncFromCrs: '从 CRS 同步', - dataExport: '导出', - dataExportSelected: '导出选中', - dataExportIncludeProxies: '导出代理(导出账号关联的代理)', - dataImport: '导入', - moreActions: '更多操作', - dataActions: '数据操作', - toolActions: '工具', - viewColumns: '列显示', - selectedCount: '已选 {count}', - dataExportConfirmMessage: '导出的数据包含账号与代理的敏感信息,请妥善保存。', - dataExportConfirm: '确认导出', - dataExported: '数据导出成功', - dataExportedSkippedShadows: '数据已导出。已跳过 {count} 个 spark 影子账号:其调度配置不在备份内,还原后需在重建的影子上重新调优。', - dataExportFailed: '数据导出失败', - dataImportTitle: '导入数据', - dataImportHint: '上传导出的 JSON 文件以批量导入账号与代理。', - dataImportWarning: '导入将创建新账号与代理,分组需手工绑定;请确认已有数据不会冲突。', - dataImportFile: '数据文件', - dataImportButton: '开始导入', - dataImporting: '导入中...', - dataImportSelectFile: '请选择数据文件', - dataImportParseFailed: '数据解析失败', - dataImportParseFailedFile: '文件 {name} 解析失败', - dataImportInvalidFile: '文件 {name} 不是受支持的导出数据文件', - dataImportIgnoredFiles: '已忽略 {count} 个非 JSON 文件', - dataImportFailed: '数据导入失败', - dataImportResult: '导入结果', - dataImportResultSummary: '代理创建 {proxy_created},复用 {proxy_reused},失败 {proxy_failed};账号创建 {account_created},失败 {account_failed}', - dataImportErrors: '失败详情', - dataImportSuccess: '导入完成:账号 {account_created},失败 {account_failed}', - dataImportCompletedWithErrors: '导入完成但有错误:账号失败 {account_failed},代理失败 {proxy_failed}', - syncFromCrsTitle: '从 CRS 同步账号', - syncFromCrsDesc: - '将 claude-relay-service(CRS)中的账号同步到当前系统(不会在浏览器侧直接请求 CRS)。', - crsVersionRequirement: '⚠️ 注意:CRS 版本必须 ≥ v1.1.240 才支持此功能', - crsBaseUrl: 'CRS 服务地址', - crsBaseUrlPlaceholder: '例如:http://127.0.0.1:3000', - crsUsername: '用户名', - crsPassword: '密码', - syncProxies: '同时同步代理(按 host/port/账号匹配或自动创建)', - syncNow: '开始同步', - syncing: '同步中...', - syncMissingFields: '请填写服务地址、用户名和密码', - syncResult: '同步结果', - syncResultSummary: '创建 {created},更新 {updated},跳过 {skipped},失败 {failed}', - syncErrors: '错误/跳过详情', - syncCompleted: '同步完成:创建 {created},更新 {updated},跳过 {skipped}', - syncCompletedWithErrors: '同步完成但有错误:失败 {failed}(创建 {created},更新 {updated},跳过 {skipped})', - syncFailed: '同步失败', - crsPreview: '预览', - crsPreviewing: '预览中...', - crsPreviewFailed: '预览失败', - crsExistingAccounts: '将自动更新的已有账号', - crsNewAccounts: '新账号(可选择)', - crsSelectAll: '全选', - crsSelectNone: '全不选', - crsNoNewAccounts: '所有 CRS 账号均已同步。', - crsWillUpdate: '将更新 {count} 个已有账号。', - crsSelectedCount: '已选择 {count} 个新账号', - crsUpdateBehaviorNote: - '已有账号仅同步 CRS 返回的字段,缺失字段保持原值;凭据按键合并,不会清空未下发的键;未勾选"同步代理"时保留原有代理。', - crsBack: '返回', - editAccount: '编辑账号', - deleteAccount: '删除账号', - deleteConfirmMessage: "确定要删除账号 '{name}' 吗?", - refreshCookie: '刷新 Cookie', - testAccount: '测试账号', - searchAccounts: '搜索账号...', - notes: '备注', - notesPlaceholder: '请输入备注', - notesHint: '备注可选', - // Filter options - allPlatforms: '全部平台', - allTypes: '全部类型', - allStatus: '全部状态', - allGroups: '全部分组', - ungroupedGroup: '未分配分组', - oauthType: 'OAuth', - // Schedulable toggle - schedulable: '参与调度', - schedulableHint: '开启后账号参与API请求调度', - schedulableEnabled: '调度已开启', - schedulableDisabled: '调度已关闭', - failedToToggleSchedulable: '切换调度状态失败', - groupCountTotal: '共 {count} 个分组', - columns: { - name: '名称', - id: '账号ID', - platformType: '平台/类型', - platform: '平台', - type: '类型', - capacity: '容量', - notes: '备注', - priority: '优先级', - billingRateMultiplier: '账号倍率', - weight: '权重', - schedulerScore: '调度权值', - status: '状态', - schedulable: '调度', - todayStats: '今日统计', - groups: '分组', - usageWindows: '用量窗口', - proxy: '代理', - lastUsed: '最近使用', - createdAt: '创建时间', - expiresAt: '过期时间', - actions: '操作' - }, - schedulerScore: { - baseShort: '普通', - stickyShort: '粘性', - ungrouped: '未分组', - hint: '显示格式为“分组名 / 基础分 / 粘性加分”。基础分按当前筛选条件限定的候选账号计算,包含优先级、负载、排队、错误率、首包延迟、重置窗口、额度余量等因子;粘性加分只在开启粘性加权时用于 previous_response_id 或 session_hash。分数越大越优先。' - }, - usageWindowsHint: '“5h / 7d”是上游账号(如 OpenAI ChatGPT、Claude)官方的滚动用量窗口限制,由上游对账号设定,并非 sub2api 配置,也与你映射的模型无关。窗口滚动到期后用量会自动重置,无法在 sub2api 端解除该限制。', - allPrivacyModes: '全部Privacy状态', - privacyUnset: '未设置', - privacyTrainingOff: '已关闭训练数据共享', - privacyCfBlocked: '被 Cloudflare 拦截,训练可能仍开启', - privacyFailed: '关闭训练数据共享失败', - privacyAntigravitySet: '已关闭遥测和营销邮件', - privacyAntigravityFailed: '隐私设置失败', - setPrivacy: '设置隐私', - subscriptionAbnormal: '异常', - subscriptionExpires: '到期', - // 容量状态提示 - capacity: { - windowCost: { - blocked: '5h窗口费用超限,账号暂停调度', - stickyOnly: '5h窗口费用达阈值,仅允许粘性会话', - normal: '5h窗口费用正常' - }, - sessions: { - full: '活跃会话已满,新会话需等待(空闲超时:{idle}分钟)', - normal: '活跃会话正常(空闲超时:{idle}分钟)' - }, - rpm: { - full: '已达 RPM 上限', - warning: 'RPM 接近上限', - normal: 'RPM 正常', - tieredNormal: 'RPM 限制 (三区模型) - 正常', - tieredWarning: 'RPM 限制 (三区模型) - 接近阈值', - tieredStickyOnly: 'RPM 限制 (三区模型) - 仅粘性会话 | 缓冲区: {buffer}', - tieredBlocked: 'RPM 限制 (三区模型) - 已阻塞 | 缓冲区: {buffer}', - stickyExemptNormal: 'RPM 限制 (粘性豁免) - 正常', - stickyExemptWarning: 'RPM 限制 (粘性豁免) - 接近阈值', - stickyExemptOver: 'RPM 限制 (粘性豁免) - 超限,仅粘性会话' - }, - quota: { - exceeded: '配额已用完,账号暂停调度', - normal: '配额正常' - }, - }, - clearRateLimit: '清除速率限制', - resetQuota: '重置配额', - quotaLimit: '配额限制', - quotaLimitPlaceholder: '0 表示不限制', - quotaLimitHint: '设置日/周/总使用额度(美元),任一维度达到限额后账号暂停调度。Anthropic API Key 账号还可配置客户端亲和。修改限额不会重置已用额度。', - quotaLimitToggle: '启用配额限制', - quotaLimitToggleHint: '开启后,当账号用量达到设定额度时自动暂停调度', - quotaDailyLimit: '日限额', - quotaDailyLimitHint: '从首次使用起每 24 小时自动重置。', - quotaWeeklyLimit: '周限额', - quotaWeeklyLimitHint: '从首次使用起每 7 天自动重置。', - quotaTotalLimit: '总限额', - quotaTotalLimitHint: '累计消费上限,不会自动重置 — 使用「重置配额」手动清零。', - quotaResetMode: '重置方式', - quotaResetModeRolling: '滚动窗口', - quotaResetModeFixed: '固定时间', - quotaResetHour: '重置时间', - quotaWeeklyResetDay: '重置日', - quotaResetTimezone: '重置时区', - quotaDailyLimitHintFixed: '每天 {hour}:00({timezone})重置。', - quotaWeeklyLimitHintFixed: '每{day} {hour}:00({timezone})重置。', - dayOfWeek: { - monday: '周一', - tuesday: '周二', - wednesday: '周三', - thursday: '周四', - friday: '周五', - saturday: '周六', - sunday: '周日', - }, - quotaLimitAmount: '总限额', - quotaLimitAmountHint: '累计消费上限,不会自动重置。', - quotaNotify: { - alert: '提醒阈值', - enabled: '启用告警', - threshold: '告警金额', - thresholdPlaceholder: '输入百分比', - }, - testConnection: '测试连接', - reAuthorize: '重新授权', - refreshToken: '刷新令牌', - noAccountsYet: '暂无账号', - createFirstAccount: '添加 AI 平台账号以开始使用 API 网关。', - tokenRefreshed: 'Token 刷新成功', - accountDeleted: '账号删除成功', - rateLimitCleared: '速率限制已清除', - setupToken: 'Setup Token', - apiKey: 'API Key', - deleteConfirm: "确定要删除账号 '{name}' 吗?此操作无法撤销。", - failedToClearRateLimit: '清除速率限制失败', - platforms: { - claude: 'Claude', - openai: 'OpenAI', - anthropic: 'Anthropic', - gemini: 'Gemini', - antigravity: 'Antigravity', - grok: 'Grok', - }, - types: { - oauth: 'OAuth', - chatgptOauth: 'ChatGPT OAuth', - responsesApi: 'Responses API', - googleOauth: 'Google OAuth', - codeAssist: 'Code Assist', - antigravityOauth: 'Antigravity OAuth', - grokOauth: 'Grok OAuth', - antigravityApikey: '通过 Base URL + API Key 连接', - upstream: '对接上游', - upstreamDesc: '通过 Base URL + API Key 连接上游', - api_key: 'API Key', - cookie: 'Cookie' - }, - antigravityProjectIdLabel: 'GCP Project ID(可选)', - antigravityProjectIdPlaceholder: 'your-gcp-project-id', - antigravityProjectIdHint: - 'standard-tier 且未自动返回 project_id 的 Antigravity 账号需要填写用户自带 GCP project。', - status: { - active: '正常', - inactive: '停用', - error: '错误', - cooldown: '冷却中', - paused: '暂停', - limited: '限流', - rateLimited: '限流中', - overloaded: '过载中', - tempUnschedulable: '临时不可调度', - quotaExceeded: '配额超限', - unschedulable: '不可调度', - rateLimitedUntil: '限流中,当前不参与调度,预计 {time} 自动恢复', - rateLimitedAutoResume: '{time} 自动恢复', - modelRateLimitedUntil: '{model} 限流至 {time}', - modelCreditOveragesUntil: '{model} 正在使用 AI Credits,至 {time}', - creditsExhausted: '积分已用尽', - creditsExhaustedUntil: 'AI Credits 已用尽,预计 {time} 恢复', - overloadedUntil: '负载过重,重置时间:{time}', - viewTempUnschedDetails: '查看临时不可调度详情' - }, - tempUnschedulable: { - title: '临时不可调度', - statusTitle: '临时不可调度状态', - hint: '当错误码与关键词同时匹配时,账号会在指定时间内被临时禁用。', - notice: '规则按顺序匹配,需同时满足错误码与关键词。', - addRule: '添加规则', - ruleOrder: '规则序号', - ruleIndex: '规则 #{index}', - errorCode: '错误码', - errorCodePlaceholder: '例如 429', - durationMinutes: '持续时间(分钟)', - durationPlaceholder: '例如 30', - keywords: '关键词', - keywordsPlaceholder: '例如 overloaded, too many requests', - keywordsHint: '多个关键词用逗号分隔,匹配时必须命中其中之一。', - description: '描述', - descriptionPlaceholder: '可选,便于记忆规则用途', - rulesInvalid: '请至少填写一条包含错误码、关键词和时长的规则。', - viewDetails: '查看临时不可调度详情', - accountName: '账号', - triggeredAt: '触发时间', - until: '解除时间', - remaining: '剩余时间', - matchedKeyword: '匹配关键词', - errorMessage: '错误详情', - reset: '恢复状态', - resetSuccess: '账号状态已恢复', - resetFailed: '恢复账号状态失败', - failedToLoad: '加载临时不可调度状态失败', - notActive: '当前账号未处于临时不可调度状态。', - expired: '已到期', - remainingMinutes: '约 {minutes} 分钟', - remainingHours: '约 {hours} 小时', - remainingHoursMinutes: '约 {hours} 小时 {minutes} 分钟', - presets: { - overloadLabel: '529 过载', - overloadDesc: '服务过载 - 暂停 60 分钟', - rateLimitLabel: '429 限流', - rateLimitDesc: '触发限流 - 暂停 10 分钟', - unavailableLabel: '503 维护', - unavailableDesc: '服务不可用 - 暂停 30 分钟' - } - }, - usageWindow: { - statsTitle: '5小时窗口用量统计', - statsTitleDaily: '每日用量统计', - geminiProDaily: 'Pro', - geminiFlashDaily: 'Flash', - gemini3Pro: 'G3P', - gemini3Flash: 'G3F', - gemini3Image: 'G31FI', - claude: 'Claude', - grokRequests: '请求', - grokTokens: 'Token', - grokUnknown: 'Grok 配额需等待首次上游响应返回 xAI rate-limit 头后显示。', - grokRetryAfter: '{time} 后重试', - grokProbe: '探测', - grokProbeTooltip: '发送最小 xAI Responses 探测请求并读取配额响应头', - grokResetUnsupported: '不支持重置', - grokResetUnsupportedTooltip: 'xAI 未向 Grok OAuth 账号开放重置额度接口', - grokNoHeaders: '未观察到配额响应头', - grokLastStatus: '状态 {status}', - grokLastProbe: '探测 {time}', - grokLastHeadersSeen: '响应头 {time}', - passiveSampled: '被动采样', - activeQuery: '查询' - }, - openaiQuotaReset: { - count: '次数', - reset: '重置', - countTooltipLoad: '点击查询剩余重置次数', - countTooltipRefresh: '点击刷新剩余重置次数', - resetTooltipReady: '消耗 1 次重置次数以立即恢复当前窗口', - resetTooltipNeedQuery: '先点击「次数」加载剩余重置次数', - resetTooltipNoCredits: '没有可用的重置次数', - resetTooltipShadow: 'Spark 影子账号不能重置次数;请在母账号上重置', - expiresAt: '到期 {time}', - expiresAtFull: '重置次数到期时间: {time}', - expandExpirations: '展开其余 {count} 张重置次数到期时间', - collapseExpirations: '收起重置次数到期时间', - expirationDetails: '重置次数到期明细', - noCreditsAvailable: '没有可用的重置次数', - resetSuccess: '已重置 {windows} 个窗口', - confirmTitle: '确认重置周限', - confirmMessage: '将消耗 1 次重置次数立即恢复当前窗口,剩余 {count} 次。此操作不可撤销,确定继续吗?' - }, - tier: { - free: 'Free', - pro: 'Pro', - ultra: 'Ultra', - aiPremium: 'AI Premium', - standard: '标准版', - basic: '基础版', - personal: '个人版', - unlimited: '无限制' - }, - ineligibleWarning: - '该账号无 Antigravity 使用权限,但仍能进行 API 转发。继续使用请自行承担风险。', - forbidden: '已封禁', - forbiddenValidation: '需要验证', - forbiddenViolation: '违规封禁', - openVerification: '打开验证链接', - copyLink: '复制链接', - linkCopied: '链接已复制', - needsReauth: '需要重新授权', - rateLimited: '限流中', - usageError: '获取失败', - form: { - nameLabel: '账号名称', - namePlaceholder: '请输入账号名称', - platformLabel: '平台', - selectPlatform: '选择平台', - typeLabel: '类型', - selectType: '选择类型', - credentialsLabel: '凭证', - credentialsPlaceholder: '请输入 Cookie 或 API Key', - priorityLabel: '优先级', - priorityHint: '数值越小优先级越高', - weightLabel: '权重', - weightHint: '用于负载均衡的权重值', - statusLabel: '状态' - }, - filters: { - platform: '平台', - allPlatforms: '全部平台', - type: '类型', - allTypes: '全部类型', - status: '状态', - allStatuses: '全部状态' - }, - saving: '保存中...', - refreshing: '刷新中...', - testing: '测试中...', - noAccounts: '暂无账号', - noAccountsDescription: '添加 AI 平台账号以开始使用 API 网关。', - accountCreatedSuccess: '账号添加成功', - accountUpdatedSuccess: '账号更新成功', - accountDeletedSuccess: '账号删除成功', - bulkSchedulableEnabled: '成功启用 {count} 个账号的调度', - bulkSchedulableDisabled: '成功停止 {count} 个账号的调度', - bulkSchedulablePartial: '部分调度更新成功:成功 {success} 个,失败 {failed} 个', - bulkSchedulableResultUnknown: '批量调度结果不完整,请稍后重试或刷新列表', - bulkActions: { - selected: '已选择 {count} 个账号', - selectCurrentPage: '本页全选', - clear: '清除选择', - edit: '批量编辑账号', - delete: '批量删除', - enableScheduling: '批量启用调度', - disableScheduling: '批量停止调度', - resetStatus: '批量重置状态', - refreshToken: '批量刷新令牌', - resetStatusSuccess: '已成功重置 {count} 个账号状态', - refreshTokenSuccess: '已成功刷新 {count} 个账号令牌', - partialSuccess: '操作部分完成:{success} 成功,{failed} 失败' - }, - bulkEdit: { - title: '批量编辑账号', - selectionInfo: '已选择 {count} 个账号。只更新您勾选或填写的字段,未勾选的字段保持不变。', - baseUrlPlaceholder: 'https://api.anthropic.com 或 https://api.openai.com', - baseUrlNotice: '仅适用于 API Key 账号,留空则不修改', - submit: '批量更新', - updating: '更新中...', - success: '成功更新 {count} 个账号', - partialSuccess: '部分更新成功:成功 {success} 个,失败 {failed} 个', - failed: '批量更新失败', - noSelection: '请选择要编辑的账号', - noFieldsSelected: '请至少选择一个要更新的字段', - mixedPlatformWarning: '所选账号跨越多个平台({platforms})。显示的模型映射预设为合并结果——请确保映射对每个平台都适用。' - }, - bulkDeleteTitle: '批量删除账号', - bulkDeleteConfirm: '确定要删除选中的 {count} 个账号吗?此操作无法撤销。', - bulkDeleteSuccess: '成功删除 {count} 个账号', - bulkDeletePartial: '部分删除成功:成功 {success} 个,失败 {failed} 个', - bulkDeleteFailed: '批量删除失败', - recoverState: '恢复状态', - recoverStateHint: '用于恢复错误、限流和临时不可调度等可恢复状态。', - recoverStateSuccess: '账号状态已恢复', - recoverStateFailed: '恢复账号状态失败', - fallbackActive: '已回退', - fallbackActiveTip: '原代理 {origin} 已到期,当前使用备用代理', - revertProxy: '切回原代理', - revertProxySuccess: '已成功切回原代理', - revertProxyFailed: '切回原代理失败', - createSparkShadow: '创建 Spark 影子账号', - createSparkShadowConfirm: '为「{name}」创建链接型 Spark 影子账号?影子共享母账号凭据、仅服务 spark 模型。', - createSparkShadowSuccess: 'Spark 影子账号已创建', - createSparkShadowFailed: '创建 Spark 影子账号失败', - resetStatus: '重置状态', - statusReset: '账号状态已重置', - failedToResetStatus: '重置账号状态失败', - cookieRefreshedSuccess: 'Cookie 刷新成功', - testSuccess: '账号测试通过', - testFailed: '账号测试失败', - failedToLoad: '加载账号列表失败', - failedToSave: '保存账号失败', - failedToDelete: '删除账号失败', - failedToRefresh: '刷新 Cookie 失败', - // Create/Edit Account Modal - platform: '平台', - accountName: '账号名称', - enterAccountName: '请输入账号名称', - accountType: '账号类型', - claudeCode: 'Claude Code', - claudeConsole: 'Claude Console', - bedrockLabel: 'AWS Bedrock', - bedrockDesc: 'SigV4 / API Key', - vertexLabel: 'Vertex', - vertexDesc: 'Service Account', - vertexAnthropicHint: '使用 Google Cloud Service Account JSON 通过 Vertex AI 调用 Anthropic Claude。建议配置模型映射,将客户端 Claude 模型名映射到 Vertex 模型 ID。', - vertexGeminiHint: '使用 Google Cloud Service Account JSON 访问 Vertex AI Gemini。建议将 Vertex 账号放入独立分组,避免和 AI Studio/Gemini OAuth 同模型混调。', - vertexSaJsonLabel: 'Service Account JSON', - vertexSaJsonLoaded: '已读取 Service Account JSON', - vertexSaJsonDrop: '拖入 Service Account JSON', - vertexSaJsonKeyHidden: '密钥内容不会在表单中显示。', - vertexSaJsonDropHint: '把 .json 文件拖到这里,或点击按钮选择文件。', - vertexSaJsonSelectBtn: '选择 JSON', - vertexSaJsonUploadHint: '上传或拖入 JSON 后会自动读取 project_id,密钥内容仅用于创建账号提交。', - vertexSaJsonEditHint: 'Service Account JSON 不在编辑页显示;需要更换 JSON 时请删除账号后重新创建。', - vertexProjectIdPlaceholder: '从 JSON 自动读取', - vertexLocationHint: '不同 Vertex 模型可用 location 可能不同,这里选择账号默认 endpoint location。', - vertexLocationRequired: '请填写 Vertex location', - vertexSaJsonMissingFields: 'Service Account JSON 缺少 project_id、client_email 或 private_key', - vertexSaJsonMissingProjectId: 'Service Account JSON 缺少 project_id', - vertexSaJsonMissingClientEmail: 'Service Account JSON 缺少 client_email', - vertexSaJsonInvalid: 'Service Account JSON 格式无效', - vertexSaJsonRequired: '请上传 Service Account JSON', - oauthSetupToken: 'OAuth / Setup Token', - addMethod: '添加方式', - setupTokenLongLived: 'Setup Token(长期有效)', - baseUrl: 'Base URL', - baseUrlHint: '留空使用官方 Anthropic API', - apiKeyRequired: 'API Key *', - apiKeyPlaceholder: 'sk-ant-api03-...', - apiKeyHint: '您的 Claude Console API Key', - // OpenAI specific hints - openai: { - baseUrlHint: '留空使用官方 OpenAI API', - apiKeyHint: '您的 OpenAI API Key', - oauthPassthrough: '自动透传(仅替换认证)', - oauthPassthroughDesc: - '开启后,该 OpenAI 账号将自动透传请求与响应,仅替换认证并保留计费/并发/审计及必要安全过滤;如遇兼容性问题可随时关闭回滚。', - responsesWebsocketsV2: 'Responses WebSocket v2', - responsesWebsocketsV2Desc: - '默认关闭。开启后可启用 responses_websockets_v2 协议能力(受网关全局开关与账号类型开关约束)。', - wsMode: 'WS mode', - wsModeDesc: '仅对当前 OpenAI 账号类型生效。', - wsModeOff: '关闭(off)', - wsModeCtxPool: '上下文池(ctx_pool)', - wsModePassthrough: '透传(passthrough)', - wsModeHttpBridge: 'HTTP 桥接(http_bridge)', - wsModeShared: '共享(shared)', - wsModeDedicated: '独享(dedicated)', - wsModeConcurrencyHint: '启用 WS mode 后,该账号并发数将作为该账号 WS 连接池上限。', - wsModePassthroughHint: 'passthrough 模式不使用 WS 连接池。', - oauthResponsesWebsocketsV2: 'OAuth WebSocket Mode', - oauthResponsesWebsocketsV2Desc: - '仅对 OpenAI OAuth 生效。开启后该账号才允许使用 OpenAI WebSocket Mode 协议。', - apiKeyResponsesWebsocketsV2: 'API Key WebSocket Mode', - apiKeyResponsesWebsocketsV2Desc: - '仅对 OpenAI API Key 生效。开启后该账号才允许使用 OpenAI WebSocket Mode 协议。', - responsesWebsocketsV2PassthroughHint: '当前已开启自动透传:仅影响 HTTP 透传链路,不影响 WS mode。', - responsesMode: 'Responses API 支持', - responsesModeDesc: - '仅对 OpenAI API Key 的文本转发链路生效。自动跟随探测结果,强制模式会覆盖自动探测。', - responsesModeAuto: '自动', - responsesModeForceResponses: '强制 Responses', - responsesModeForceChatCompletions: '强制 Chat Completions', - responsesModeTextDisabledHint: '未启用 Responses / Chat Completions 端点时,此设置不适用。', - endpointCapabilities: '端点能力', - endpointCapabilitiesDesc: - '用于调度筛选。文本端点会跟随上方 Responses API 支持显示为 Responses、Chat Completions 或自动模式;Embeddings 独立控制 /v1/embeddings。', - capabilityResponses: 'Responses', - capabilityTextAuto: 'Responses / Chat Completions(自动)', - capabilityResponsesAuto: 'Responses(自动探测)', - capabilityChatCompletions: 'Chat Completions', - capabilityChatCompletionsAuto: 'Chat Completions(自动探测)', - capabilityEmbeddings: 'Embeddings', - responsesStatusAutoSupported: '自动探测:Responses', - responsesStatusAutoUnsupported: '自动探测:Chat Completions', - responsesStatusAutoUnknown: '自动探测:未探测', - responsesStatusForcedResponses: '已强制 Responses', - responsesStatusForcedChatCompletions: '已强制 Chat Completions', - codexCLIOnly: '仅允许 Codex 官方客户端', - codexCLIOnlyDesc: '仅对 OpenAI OAuth 生效。开启后仅允许 Codex 官方客户端家族访问;关闭后完全绕过并保持原逻辑。', - codexCLIOnlyAppServer: '允许 Codex app-server 客户端', - codexCLIOnlyAppServerDesc: '仅在上方开关开启时生效。开启后本账号额外放行内嵌 Codex 引擎、经 app-server 协议接入的第三方客户端(如 Claude Code 的 codex 插件),仍需通过全局引擎指纹门;与全局 app-server 开关取 OR(任一开即放行)。', - codexImageTool: 'Codex 图片工具', - codexImageToolDesc: - '统一控制 Codex /responses 文本请求的 image_generation 图片工具:是否自动注入,以及客户端自带该工具时是否放行。账号级策略优先于渠道和全局配置,不影响独立图片生成接口。', - codexImageToolInherit: '跟随渠道', - codexImageToolInheritDesc: '不写入账号覆盖,是否注入由渠道或全局策略决定;客户端自带的图片工具照常放行。', - codexImageToolEnabled: '强制注入', - codexImageToolEnabledDesc: '始终为 Codex /responses 请求注入图片工具。', - codexImageToolDisabled: '关闭注入', - codexImageToolDisabledDesc: '不自动注入;客户端自带的图片工具仍会放行。', - codexImageToolBlock: '完全阻断', - codexImageToolBlockDesc: '不注入,并移除客户端自带的图片工具及指向它的 tool_choice。', - codexImageToolBadgeInherit: '渠道策略', - codexImageToolBadgeEnabled: '强制注入', - codexImageToolBadgeDisabled: '关闭注入', - codexImageToolBadgeBlock: '完全阻断', - compactMode: 'Compact 模式', - compactModeDesc: - '控制本账号在 /responses/compact 调度中的参与方式。Auto 跟随探测结果,Force On 强制允许,Force Off 强制排除。', - compactModeAuto: '自动', - compactModeForceOn: '强制开启', - compactModeForceOff: '强制关闭', - compactModelMapping: 'Compact 专属模型映射', - compactModelMappingDesc: - '仅在 /responses/compact 请求中生效。当上游 compact 端点需要特殊 compact 模型时使用。', - compactSupported: '支持 Compact', - compactUnsupported: '不支持 Compact', - compactAuto: 'Compact Auto', - compactUnknown: 'Compact Auto', - compactLastChecked: '最近探测', - testMode: '测试模式', - testModeDefault: '常规请求', - testModeCompact: 'Compact 探测', - modelRestrictionDisabledByPassthrough: '已开启自动透传:模型白名单/映射不会生效。', - }, - grok: { - baseUrlHint: 'Grok OAuth 账号会转发到官方 xAI API Base URL。', - apiKeyHint: 'Grok 订阅支持使用 OAuth refresh token;API Key 账号不在本次范围内。' - }, - anthropic: { - apiKeyPassthrough: '自动透传(仅替换认证)', - apiKeyPassthroughDesc: - '仅对 Anthropic API Key 生效。开启后,messages/count_tokens 请求将透传上游并仅替换认证,保留计费/并发/审计及必要安全过滤;关闭即可回滚到现有兼容链路。', - apiKeyAuthScheme: '上游认证方式', - apiKeyAuthSchemeDesc: '选择转发到 Anthropic-compatible 上游时使用的 API Key 认证头。Ollama Cloud 使用 Authorization: Bearer。', - apiKeyAuthSchemeXApiKey: 'x-api-key', - apiKeyAuthSchemeBearer: 'Authorization: Bearer', - webSearchEmulation: 'Web Search 模拟', - webSearchEmulationDesc: - '为该 API Key 账号启用 web search 模拟。客户端发送纯 web_search 请求时,由网关调用第三方搜索 API 并构造响应返回。默认跟随渠道配置。', - webSearchDefault: '默认', - webSearchEnabled: '开启', - webSearchDisabled: '关闭', - }, - modelRestriction: '模型限制(可选)', - modelWhitelist: '模型白名单', - modelMapping: '模型映射', - selectAllowedModels: '选择允许的模型。留空则支持所有模型。', - mapRequestModels: '将请求模型映射到实际模型。左边是请求的模型,右边是发送到 API 的实际模型。', - selectedModels: '已选择 {count} 个模型', - supportsAllModels: '(支持所有模型)', - requestModel: '请求模型', - actualModel: '实际模型', - addMapping: '添加映射', - mappingExists: '模型 {model} 的映射已存在', - wildcardOnlyAtEnd: '通配符 * 只能放在末尾', - targetNoWildcard: '目标模型不能包含通配符 *', - searchModels: '搜索模型...', - noMatchingModels: '没有匹配的模型', - fillRelatedModels: '同步最新支持模型', - syncUpstreamModels: '同步上游支持的模型', - syncUpstreamModelsLoading: '同步上游中...', - syncUpstreamModelsSuccess: '已从上游同步 {count} 个新模型(上游共 {total} 个)', - syncUpstreamModelsNoChanges: '上游 {count} 个模型均已在白名单中', - syncUpstreamModelsEmpty: '上游没有返回可同步的模型', - syncUpstreamModelsFailed: '同步上游模型失败', - syncUpstreamModelsError: '同步上游模型失败:{message}', - clearAllModels: '清除所有模型', - customModelName: '自定义模型名称', - enterCustomModelName: '输入自定义模型名称', - addModel: '填入', - modelExists: '该模型已存在', - modelCount: '{count} 个模型', - poolMode: '池模式', - poolModeHint: '上游为账号池时启用,错误不标记本地账号状态', - poolModeInfo: - '启用后,上游 429/403/401 错误将自动重试而不标记账号限流或错误,适用于上游指向另一个 sub2api 实例的场景。', - poolModeRetryCount: '同账号重试次数', - poolModeRetryCountHint: '仅在池模式下生效。0 表示不原地重试;默认 {default},最大 {max}。', - poolModeRetryStatusCodes: '同账号重试状态码', - poolModeRetryStatusCodesHint: '仅在池模式下生效。以英文逗号分隔的 HTTP 状态码(100-599),命中时触发同账号重试。留空使用默认值({default})。', - customErrorCodes: '自定义错误码', - customErrorCodesHint: '仅对选中的错误码停止调度', - customErrorCodesWarning: '仅选中的错误码会停止调度,其他错误将返回 500。', - customErrorCodes429Warning: - '429 已有内置的限流处理机制。添加到自定义错误码后,将直接停止调度而非临时限流。确定要添加吗?', - customErrorCodes529Warning: - '529 已有内置的过载处理机制。添加到自定义错误码后,将直接停止调度而非临时标记过载。确定要添加吗?', - selectedErrorCodes: '已选择', - noneSelectedUsesDefault: '未选择(使用默认策略)', - enterErrorCode: '输入错误码 (100-599)', - invalidErrorCode: '请输入有效的 HTTP 错误码 (100-599)', - errorCodeExists: '该错误码已被选中', - interceptWarmupRequests: '拦截预热请求', - interceptWarmupRequestsDesc: '启用后,标题生成等预热请求将返回 mock 响应,不消耗上游 token', - headerOverride: { - title: '请求头覆写', - hint: '转发时用配置值覆盖同名请求头(不区分大小写)', - info: '仅对本账号的出站请求生效:配置的请求头会在转发前覆盖客户端/网关生成的同名头。认证头(authorization、x-api-key)与连接控制头不允许覆写。', - namePlaceholder: '请求头名称(如 user-agent)', - valuePlaceholder: '覆写值(留空表示不覆写)', - addRow: '添加请求头', - fillTemplate: '填入模板', - emptyValueHint: '值留空的行不会参与覆盖,仅作为待填写的占位。', - bulkDisableHint: '保存后将关闭所选账号的请求头覆写并清空已有配置。', - bulkReplaceHint: '保存后将用下方配置整体替换所选账号已有的请求头覆写配置。', - bulkEmptyRows: '请至少添加一行请求头再保存;如需清空已有配置,请关闭上方开关。', - invalidName: '请求头名称格式不正确(仅允许字母、数字和 !#$%&\'*+-.^_`|~ 字符)', - blockedName: '该请求头不允许覆写(认证头与连接控制头由系统管理)', - duplicateName: '存在重复的请求头名称(匹配不区分大小写)', - invalidValue: '请求头值不合法(不允许控制字符,长度不超过 8192)', - tooManyEntries: '请求头覆写条目过多(最多 64 条)' - }, - autoPauseOnExpired: '过期自动暂停调度', - autoPauseOnExpiredDesc: '启用后,账号过期将自动暂停调度', - autoPause5hThreshold: '5h 用量阈值(%)', - autoPause7dThreshold: '7d 用量阈值(%)', - autoPauseThresholdHint: '留空或填 0 表示使用全局默认阈值(在运维设置中配置);填具体值则覆盖全局默认。达到阈值后仅在调度时跳过账号,不修改 schedulable。', - autoPause5hDisabled: '禁用 5h 自动暂停', - autoPause7dDisabled: '禁用 7d 自动暂停', - autoPauseDisabledHint: '开启后该账号永不进入自动暂停(即使全局默认阈值已配置)。', - // Quota control (Anthropic OAuth/SetupToken only) - quotaControl: { - title: '配额控制', - hint: '配置费用窗口、会话限制、客户端亲和等调度控制。', - windowCost: { - label: '5h窗口费用控制', - hint: '限制账号在5小时窗口内的费用使用', - limit: '费用阈值', - limitPlaceholder: '50', - limitHint: '达到阈值后不参与新请求调度', - stickyReserve: '粘性预留额度', - stickyReservePlaceholder: '10', - stickyReserveHint: '为粘性会话预留的额外额度' - }, - sessionLimit: { - label: '会话数量控制', - hint: '限制同时活跃的会话数量', - maxSessions: '最大会话数', - maxSessionsPlaceholder: '3', - maxSessionsHint: '同时活跃的最大会话数量', - idleTimeout: '空闲超时', - idleTimeoutPlaceholder: '5', - idleTimeoutHint: '会话空闲超时后自动释放' - }, - rpmLimit: { - label: 'RPM 限制', - hint: '限制每分钟请求数量,保护上游账号', - baseRpm: '基础 RPM', - baseRpmPlaceholder: '15', - baseRpmHint: '每分钟最大请求数,0 或留空表示不限制', - strategy: 'RPM 策略', - strategyTiered: '三区模型', - strategyStickyExempt: '粘性豁免', - strategyTieredHint: '绿区→黄区→仅粘性→阻塞,逐步限流', - strategyStickyExemptHint: '超限后仅允许粘性会话', - strategyHint: '三区模型: 超限后逐步限制; 粘性豁免: 已有会话不受限', - stickyBuffer: '粘性缓冲区', - stickyBufferPlaceholder: '默认: base RPM 的 20%', - stickyBufferHint: '超过 base RPM 后,粘性会话额外允许的请求数。为空则使用默认值(base RPM 的 20%,最小为 1)', - userMsgQueue: '用户消息限速', - userMsgQueueHint: '对用户消息施加发送限制,避免触发上游 RPM 限制', - umqModeOff: '关闭', - umqModeThrottle: '软性限速', - umqModeSerialize: '串行队列', - }, - tlsFingerprint: { - label: 'TLS 指纹模拟', - hint: '模拟 Node.js/Claude Code 客户端的 TLS 指纹', - defaultProfile: '内置默认', - randomProfile: '随机' - }, - sessionIdMasking: { - label: '会话 ID 伪装', - hint: '启用后将在 15 分钟内固定 metadata.user_id 中的 session ID,使上游认为请求来自同一会话' - }, - cacheTTLOverride: { - label: '缓存 TTL 强制替换', - hint: '将所有缓存创建 token 强制按指定的 TTL 类型(5分钟或1小时)计费', - target: '目标 TTL', - targetHint: '选择计费使用的 TTL 类型' - }, - customBaseUrl: { - label: '自定义转发地址', - hint: '启用后将请求转发到自定义中继服务,代理地址将作为 URL 参数传递给中继服务', - urlHint: '中继服务地址(如 https://relay.example.com)', - }, - clientAffinity: { - label: '客户端亲和调度', - hint: '启用后,新会话会优先调度到该客户端之前使用过的账号,避免频繁切换账号' - } - }, - affinityNoClients: '无亲和客户端', - affinityClients: '{count} 个亲和客户端:', - affinitySection: '客户端亲和', - affinitySectionHint: '控制客户端在账号间的分布。通过配置区域阈值来平衡负载。', - affinityToggle: '启用客户端亲和', - affinityToggleHint: '新会话优先调度到该客户端之前使用过的账号', - affinityBase: '基础限额(绿区)', - affinityBasePlaceholder: '留空表示不限制', - affinityBaseHint: '绿区最大客户端数量(完整优先级调度)', - affinityBaseOffHint: '未开启绿区限制,所有客户端均享受完整优先级调度', - affinityBuffer: '缓冲区(黄区)', - affinityBufferPlaceholder: '例如 3', - affinityBufferHint: '黄区允许的额外客户端数量(降级优先级调度)', - affinityBufferInfinite: '不限制', - expired: '已过期', - proxy: '代理', - noProxy: '无代理', - concurrency: '并发数', - loadFactor: '负载因子', - loadFactorHint: '提高负载因子可以提高对账号的调度频率', - priority: '优先级', - priorityHint: '优先级越小的账号优先使用', - billingRateMultiplier: '账号计费倍率', - billingRateMultiplierHint: '0 表示不计费,仅影响账号计费', - expiresAt: '过期时间', - expiresAtHint: '留空表示不过期', - higherPriorityFirst: '数值越小优先级越高', - mixedScheduling: '在 /v1/messages 中使用', - mixedSchedulingHint: '启用后可参与 Anthropic/Gemini 分组的调度', - mixedSchedulingTooltip: - '!!注意!! Antigravity Claude 和 Anthropic Claude 无法在同个上下文中使用,如果你同时有 Anthropic 账号和 Antigravity 账号,开启此选项会导致经常 400 报错。开启后,请用分组功能做好 Antigravity 账号和 Anthropic 账号的隔离。一定要弄明白再开启!!', - aiCreditsBalance: 'AI Credits', - allowOverages: '允许超量请求 (AI Credits)', - allowOveragesTooltip: - '仅在免费配额被明确判定为耗尽后才会使用 AI Credits。普通并发 429 限流不会切换到超量请求。', - creating: '创建中...', - updating: '更新中...', - accountCreated: '账号创建成功', - accountUpdated: '账号更新成功', - failedToCreate: '创建账号失败', - failedToUpdate: '更新账号失败', - pleaseSelectStatus: '请选择有效的账号状态', - mixedChannelWarningTitle: '混合渠道警告', - mixedChannelWarning: '警告:分组 "{groupName}" 中同时包含 {currentPlatform} 和 {otherPlatform} 账号。混合使用不同渠道可能导致 thinking block 签名验证问题,会自动回退到非 thinking 模式。确定要继续吗?', - pleaseEnterAccountName: '请输入账号名称', - pleaseEnterApiKey: '请输入 API Key', - bedrockAccessKeyId: 'AWS Access Key ID', - bedrockSecretAccessKey: 'AWS Secret Access Key', - bedrockSessionToken: 'AWS Session Token', - bedrockRegion: 'AWS Region', - bedrockRegionHint: '例如 us-east-1, us-west-2, eu-west-1', - bedrockForceGlobal: '强制使用 Global 跨区域推理', - bedrockForceGlobalHint: '启用后模型 ID 使用 global. 前缀(如 global.anthropic.claude-...),请求可路由到全球任意支持的区域,获得更高可用性', - bedrockAccessKeyIdRequired: '请输入 AWS Access Key ID', - bedrockSecretAccessKeyRequired: '请输入 AWS Secret Access Key', - bedrockRegionRequired: '请选择 AWS Region', - bedrockSessionTokenHint: '可选,用于临时凭证', - bedrockSecretKeyLeaveEmpty: '留空以保持当前密钥', - bedrockAuthMode: '认证方式', - bedrockAuthModeSigv4: 'SigV4 签名', - bedrockAuthModeApikey: 'Bedrock API Key', - bedrockApiKeyLabel: 'Bedrock API Key', - bedrockApiKeyDesc: 'Bearer Token 认证', - bedrockApiKeyInput: 'API Key', - bedrockApiKeyRequired: '请输入 Bedrock API Key', - bedrockApiKeyLeaveEmpty: '留空以保持当前密钥', - apiKeyIsRequired: 'API Key 是必需的', - leaveEmptyToKeep: '留空以保持当前密钥', - // Upstream type - upstream: { - baseUrl: '上游 Base URL', - baseUrlHint: '上游 Antigravity 服务的地址,例如:https://cloudcode-pa.googleapis.com', - apiKey: '上游 API Key', - apiKeyHint: '上游服务的 API Key', - pleaseEnterBaseUrl: '请输入上游 Base URL', - pleaseEnterApiKey: '请输入上游 API Key' - }, - // OAuth flow - oauth: { - title: 'Claude 账号授权', - authMethod: '授权方式', - manualAuth: '手动授权', - cookieAutoAuth: 'Cookie 自动授权', - cookieAutoAuthDesc: '使用 claude.ai sessionKey 自动完成 OAuth 授权,无需手动打开浏览器。', - sessionKey: 'sessionKey', - keysCount: '{count} 个密钥', - batchCreateAccounts: '将批量创建 {count} 个账号', - sessionKeyPlaceholder: - '每行一个 sessionKey,例如:\nsk-ant-sid01-xxxxx...\nsk-ant-sid01-yyyyy...', - sessionKeyPlaceholderSingle: 'sk-ant-sid01-xxxxx...', - howToGetSessionKey: '如何获取 sessionKey', - step1: '在浏览器中登录 claude.ai', - step2: '按 F12 打开开发者工具', - step3: '切换到 Application 标签', - step4: '找到 Cookies → https://claude.ai', - step5: '找到 sessionKey 所在行', - step6: '复制 Value 列的值', - sessionKeyFormat: 'sessionKey 通常以 sk-ant-sid01- 开头', - startAutoAuth: '开始自动授权', - authorizing: '授权中...', - followSteps: '按照以下步骤授权您的 Claude 账号:', - step1GenerateUrl: '点击下方按钮生成授权 URL', - generateAuthUrl: '生成授权 URL', - generating: '生成中...', - regenerate: '重新生成', - step2OpenUrl: '在浏览器中打开 URL 并完成授权', - openUrlDesc: '在新标签页中打开授权 URL,登录您的 Claude 账号并授权。', - proxyWarning: '注意:如果您配置了代理,请确保浏览器使用相同的代理访问授权页面。', - step3EnterCode: '输入授权码', - authCodeDesc: '授权完成后,页面会显示一个授权码。复制并粘贴到下方:', - authCode: '授权码', - authCodePlaceholder: '粘贴 Claude 页面的授权码...', - authCodeHint: '粘贴从 Claude 页面复制的授权码', - completeAuth: '完成授权', - verifying: '验证中...', - pleaseEnterSessionKey: '请输入至少一个有效的 sessionKey', - authFailed: '授权失败', - cookieAuthFailed: 'Cookie 授权失败', - keyAuthFailed: '密钥 {index}: {error}', - successCreated: '成功创建 {count} 个账号', - batchSuccess: '成功创建 {count} 个账号', - batchPartialSuccess: '部分成功:{success} 个成功,{failed} 个失败', - batchFailed: '批量创建失败', - // OpenAI specific - openai: { - title: 'OpenAI 账户授权', - followSteps: '请按照以下步骤完成 OpenAI 账户的授权:', - step1GenerateUrl: '点击下方按钮生成授权链接', - generateAuthUrl: '生成授权链接', - step2OpenUrl: '在浏览器中打开链接并完成授权', - openUrlDesc: '请在新标签页中打开授权链接,登录您的 OpenAI 账户并授权。', - importantNotice: - '重要提示:授权后页面可能会加载较长时间,请耐心等待。当浏览器地址栏变为 http://localhost... 开头时,表示授权已完成。', - step3EnterCode: '输入授权链接或 Code', - authCodeDesc: - '授权完成后,当页面地址变为 http://localhost:xxx/auth/callback?code=... 时:', - authCode: '授权链接或 Code', - authCodePlaceholder: - '方式1:复制完整的链接\n(http://localhost:xxx/auth/callback?code=...)\n方式2:仅复制 code 参数的值', - authCodeHint: '您可以直接复制整个链接或仅复制 code 参数值,系统会自动识别', - failedToGenerateUrl: '生成 OpenAI 授权链接失败', - failedToExchangeCode: 'OpenAI 授权码兑换失败', - failedToValidateRT: '验证 Refresh Token 失败', - errors: { - OPENAI_OAUTH_PROXY_REQUIRED: - '未设置代理,当前服务器无法直连 OpenAI,导致 OpenAI OAuth 请求失败。请先选择可访问 OpenAI 的代理后重试;如果授权码已失效,请重新生成授权链接。' - }, - // Refresh Token auth - refreshTokenAuth: '手动输入 RT', - refreshTokenDesc: '输入您已有的 OpenAI Refresh Token,支持批量输入(每行一个),系统将自动验证并创建账号。', - refreshTokenPlaceholder: '粘贴您的 OpenAI Refresh Token...\n支持多个,每行一个', - codexSessionAuth: 'Codex JSON / AT 批量输入', - codexSessionDesc: '粘贴 Codex JSON 或 accessToken,按第一步配置创建账号。', - codexSessionInputLabel: 'Codex JSON 或 accessToken', - codexSessionPlaceholder: '支持多行,每行一个 token 或 JSON', - codexSessionHint: 'sessionToken 不会作为 refresh_token 保存;未包含 refresh_token 时会按 accessToken 过期时间设置账号过期,无法解析且第一步未设置过期时间时会拒绝导入。', - codexSessionImportAndCreate: '导入并创建账号', - codexSessionEmpty: '请输入 Codex JSON 或 accessToken', - codexSessionImportFailed: 'Codex 账号导入失败', - codexSessionImportSuccess: '导入完成:新增 {created},更新 {updated},跳过 {skipped}', - codexSessionImportPartial: '部分成功:新增 {created},更新 {updated},跳过 {skipped},失败 {failed}', - codexPatAuth: 'Codex Personal Access Token', - codexPatDesc: '输入 Codex at- Personal Access Token,系统会先调用 OpenAI whoami 校验后再创建账号。', - codexPatInputLabel: 'Codex PAT', - codexPatPlaceholder: 'at-...', - codexPatHint: '这是独立认证模式,不保存 refresh_token,也不会写入 OAuth access_token 过期时间。', - codexPatImportAndCreate: '校验并创建 Codex PAT 账号', - codexPatEmpty: '请输入 Codex Personal Access Token', - codexPatImportFailed: 'Codex PAT 账号创建失败', - sessionTokenAuth: '手动输入 ST', - sessionTokenDesc: '输入您已有的 Session Token,支持批量输入(每行一个),系统将自动验证并创建账号。', - sessionTokenPlaceholder: '粘贴您的 Session Token...\n支持多个,每行一个', - sessionTokenRawLabel: '原始字符串', - sessionTokenRawPlaceholder: '粘贴 /api/auth/session 原始数据或 Session Token...', - sessionTokenRawHint: '支持粘贴完整 JSON,系统会自动解析 ST 和 AT。', - openSessionUrl: '打开获取链接', - copySessionUrl: '复制链接', - sessionUrlHint: '该链接通常可获取 AT。若返回中无 sessionToken,请从浏览器 Cookie 复制 __Secure-next-auth.session-token 作为 ST。', - parsedSessionTokensLabel: '解析出的 ST', - parsedSessionTokensEmpty: '未解析到 ST,请检查输入内容', - parsedAccessTokensLabel: '解析出的 AT', - validating: '验证中...', - validateAndCreate: '验证并创建账号', - pleaseEnterRefreshToken: '请输入 Refresh Token', - pleaseEnterSessionToken: '请输入 Session Token' - }, - grok: { - title: 'Grok 账号授权', - followSteps: '请按照以下步骤授权您的 xAI/Grok 账号:', - step1GenerateUrl: '生成 xAI 授权链接', - generateAuthUrl: '生成授权链接', - step2OpenUrl: '在浏览器中打开链接并完成授权', - openUrlDesc: '在新标签页中打开授权链接,登录 xAI 并授权 API 访问。', - importantNotice: '当浏览器跳转到本地 callback URL 后,请复制完整 URL 或 code 参数回填到这里。', - step3EnterCode: '输入授权链接或 Code', - authCodeDesc: '授权完成后,粘贴 callback URL、查询字符串或授权码:', - authCode: '授权链接或 Code', - authCodePlaceholder: '粘贴完整 callback URL、?code=... 查询字符串或 code 值', - authCodeHint: '支持完整 callback URL、查询字符串或裸 code。', - refreshTokenAuth: '手动输入 RT', - refreshTokenDesc: '输入已有的 xAI refresh token,支持批量输入(每行一个)。', - refreshTokenPlaceholder: '粘贴您的 xAI refresh token...\n支持多个,每行一个', - validating: '验证中...', - validateAndCreate: '验证并创建账号', - pleaseEnterRefreshToken: '请输入 Refresh Token', - failedToGenerateUrl: '生成 Grok 授权链接失败', - missingExchangeParams: '缺少授权码、state 或 OAuth 会话', - failedToExchangeCode: 'Grok 授权码兑换失败', - failedToValidateRT: '验证 Grok refresh token 失败', - oauthOnlyHint: '首版 Grok 支持仅包含 OAuth 订阅的 Responses API 文本/推理转发。' - }, - // Gemini specific - gemini: { - title: 'Gemini 账户授权', - followSteps: '请按照以下步骤完成 Gemini 账户的授权:', - step1GenerateUrl: '生成授权链接', - generateAuthUrl: '生成授权链接', - projectIdLabel: 'Project ID(可选)', - projectIdPlaceholder: '例如:my-gcp-project 或 cloud-ai-companion-xxxxx', - projectIdHint: - '留空则在兑换授权码后自动探测;若自动探测失败,可填写后重新生成授权链接再授权。', - howToGetProjectId: '如何获取', - step2OpenUrl: '在浏览器中打开链接并完成授权', - openUrlDesc: '请在新标签页中打开授权链接,登录您的 Google 账户并授权。', - step3EnterCode: '输入回调链接或 Code', - authCodeDesc: - '授权完成后,复制浏览器跳转后的回调链接(推荐)或仅复制 code,粘贴到下方即可。', - authCode: '回调链接或 Code', - authCodePlaceholder: '方式1(推荐):粘贴回调链接\n方式2:仅粘贴 code 参数的值', - authCodeHint: '系统会自动从链接中解析 code/state。', - redirectUri: 'Redirect URI', - redirectUriHint: '需要在 Google OAuth Client 中配置,且必须与此处完全一致。', - confirmRedirectUri: '我已在 Google OAuth Client 中配置了该 Redirect URI(必须完全一致)', - invalidRedirectUri: 'Redirect URI 必须是合法的 http(s) URL', - redirectUriNotConfirmed: '请确认 Redirect URI 已在 Google OAuth Client 中正确配置', - missingRedirectUri: '缺少 Redirect URI', - failedToGenerateUrl: '生成 Gemini 授权链接失败', - missingExchangeParams: '缺少 code / session_id / state', - failedToExchangeCode: 'Gemini 授权码兑换失败', - missingProjectId: - 'GCP Project ID 获取失败:您的 Google 账号未关联有效的 GCP 项目。请前往 Google Cloud Console 激活 GCP 并绑定信用卡,或在授权时手动填写 Project ID。', - modelPassthrough: 'Gemini 直接转发模型', - modelPassthroughDesc: '所有模型请求将直接转发至 Gemini API,不进行模型限制或映射。', - stateWarningTitle: '提示', - stateWarningDesc: '建议粘贴完整回调链接(包含 code 和 state)。', - oauthTypeLabel: 'OAuth 类型', - needsProjectId: '内置授权(Code Assist)', - needsProjectIdDesc: '需要 GCP 项目与 Project ID', - noProjectIdNeeded: '自定义授权(AI Studio)', - noProjectIdNeededDesc: '需管理员配置 OAuth Client', - aiStudioNotConfiguredShort: '未配置', - aiStudioNotConfiguredTip: - 'AI Studio OAuth 未配置:请先设置 GEMINI_OAUTH_CLIENT_ID / GEMINI_OAUTH_CLIENT_SECRET,并在 Google OAuth Client 添加 Redirect URI:http://localhost:1455/auth/callback(Consent Screen scopes 需包含 https://www.googleapis.com/auth/generative-language.retriever)', - aiStudioNotConfigured: - 'AI Studio OAuth 未配置:请先设置 GEMINI_OAUTH_CLIENT_ID / GEMINI_OAUTH_CLIENT_SECRET,并在 Google OAuth Client 添加 Redirect URI:http://localhost:1455/auth/callback' - }, - // Antigravity specific - antigravity: { - title: 'Antigravity 账户授权', - followSteps: '请按照以下步骤完成 Antigravity 账户的授权:', - step1GenerateUrl: '生成授权链接', - generateAuthUrl: '生成授权链接', - step2OpenUrl: '在浏览器中打开链接并完成授权', - openUrlDesc: '请在新标签页中打开授权链接,登录您的 Google 账户并授权。', - importantNotice: - '重要提示:授权后页面可能会加载较长时间,请耐心等待。当浏览器地址栏变为 http://localhost... 开头时,表示授权已完成。', - step3EnterCode: '输入授权链接或 Code', - authCodeDesc: - '授权完成后,当页面地址变为 http://localhost:xxx/auth/callback?code=... 时:', - authCode: '授权链接或 Code', - authCodePlaceholder: - '方式1:复制完整的链接\n(http://localhost:xxx/auth/callback?code=...)\n方式2:仅复制 code 参数的值', - authCodeHint: '您可以直接复制整个链接或仅复制 code 参数值,系统会自动识别', - failedToGenerateUrl: '生成 Antigravity 授权链接失败', - missingExchangeParams: '缺少 code / session_id / state', - failedToExchangeCode: 'Antigravity 授权码兑换失败', - // Refresh Token auth - refreshTokenAuth: '手动输入 RT', - refreshTokenDesc: '输入您已有的 Antigravity Refresh Token,支持批量输入(每行一个),系统将自动验证并创建账号。', - refreshTokenPlaceholder: '粘贴您的 Antigravity Refresh Token...\n支持多个,每行一个', - validating: '验证中...', - validateAndCreate: '验证并创建账号', - pleaseEnterRefreshToken: '请输入 Refresh Token', - failedToValidateRT: '验证 Refresh Token 失败' - } - }, - // Gemini specific (platform-wide) - gemini: { - helpButton: '使用帮助', - helpDialog: { - title: 'Gemini 使用指南', - apiKeySection: 'API Key 相关链接' - }, - modelPassthrough: 'Gemini 直接转发模型', - modelPassthroughDesc: '所有模型请求将直接转发至 Gemini API,不进行模型限制或映射。', - baseUrlHint: '留空使用官方 Gemini API', - apiKeyHint: '您的 Gemini API Key(以 AIza 开头)', - tier: { - label: '账号等级', - hint: '提示:系统会优先尝试自动识别账号等级;若自动识别不可用或失败,则使用你选择的等级作为回退(本地模拟配额)。', - aiStudioHint: - 'AI Studio 的配额是按模型分别限流(Pro/Flash 独立)。若已绑卡(按量付费),请选 Pay-as-you-go。', - googleOne: { - free: 'Google One Free', - pro: 'Google One Pro', - ultra: 'Google One Ultra' - }, - gcp: { - standard: 'GCP Standard', - enterprise: 'GCP Enterprise' - }, - aiStudio: { - free: 'Google AI Free', - paid: 'Google AI Pay-as-you-go' - } - }, - accountType: { - oauthTitle: 'OAuth 授权(Gemini)', - oauthDesc: '使用 Google 账号授权,并选择 OAuth 子类型。', - apiKeyTitle: 'API 密钥(AI Studio)', - apiKeyDesc: '最快接入方式,使用 AIza API Key。', - apiKeyNote: '适合轻量测试。免费层限流严格,数据可能用于训练。', - apiKeyLink: '获取 API Key', - quotaLink: '配额说明' - }, - oauthType: { - builtInTitle: '内置授权(Gemini CLI / Code Assist)', - builtInDesc: '使用 Google 内置客户端 ID,无需管理员配置。', - builtInRequirement: '需要 GCP 项目并填写 Project ID。', - googleOneDesc: '个人账号,享受 Google One 订阅配额', - codeAssistDesc: '企业级,需要 GCP 项目', - codeAssistRequirement: '需要激活 GCP 项目并绑定信用卡', - showAdvanced: '显示高级选项(自建 OAuth Client)', - hideAdvanced: '隐藏高级选项(自建 OAuth Client)', - gcpProjectLink: '创建项目', - customTitle: '自定义授权(AI Studio OAuth)', - customDesc: '使用管理员预设的 OAuth 客户端,适合组织管理。', - customRequirement: '需管理员配置 Client ID 并加入测试用户白名单。', - badges: { - recommended: '推荐', - highConcurrency: '高并发', - individuals: '推荐个人用户', - noGcp: '无需 GCP', - enterprise: '企业用户', - noAdmin: '无需管理员配置', - orgManaged: '组织管理', - adminRequired: '需要管理员' - } - }, - setupGuide: { - title: 'Gemini 使用准备', - checklistTitle: '准备工作', - checklistItems: { - usIp: '使用美国 IP,并确保账号归属地为美国。', - age: '账号需满 18 岁。' - }, - activationTitle: '服务激活', - activationItems: { - geminiWeb: '激活 Gemini Web,避免 User not initialized。', - gcpProject: '激活 GCP 项目,获取 Code Assist 所需 Project ID。' - }, - links: { - countryCheck: '检查归属地', - countryChange: '修改归属地', - geminiWebActivation: '激活 Gemini Web', - gcpProject: '打开 GCP 控制台' - } - }, - quotaPolicy: { - title: 'Gemini 配额与限流政策(参考)', - note: '注意:Gemini 官方未提供用量查询接口。此处显示的“每日配额”是由系统根据账号等级模拟计算的估算值,仅供调度参考,请以 Google 官方实际报错为准。', - columns: { - channel: '授权通道', - account: '账号状态', - limits: '限流政策', - docs: '官方文档' - }, - docs: { - codeAssist: 'Code Assist 配额', - aiStudio: 'AI Studio 定价', - vertex: 'Vertex AI 配额' - }, - simulatedNote: '本地模拟配额,仅供参考', - rows: { - googleOne: { - channel: 'Google One OAuth(个人版 / Code Assist for Individuals)', - limitsFree: '共享池:1000 RPD / 60 RPM(不分模型)', - limitsPro: '共享池:1500 RPD / 120 RPM(不分模型)', - limitsUltra: '共享池:2000 RPD / 120 RPM(不分模型)' - }, - gcp: { - channel: 'GCP Code Assist OAuth(企业版)', - limitsStandard: '共享池:1500 RPD / 120 RPM(不分模型)', - limitsEnterprise: '共享池:2000 RPD / 120 RPM(不分模型)' - }, - cli: { - channel: 'Gemini CLI(官方 Google 登录 / Code Assist)', - free: '免费 Google 账号', - premium: 'Google One AI Premium', - limitsFree: 'RPD ~1000;RPM ~60(软限制)', - limitsPremium: 'RPD ~1500+;RPM ~60+(优先队列)' - }, - gcloud: { - channel: 'GCP Code Assist(gcloud 登录)', - account: '未购买 Code Assist 订阅', - limits: 'RPD ~1000;RPM ~60(预览期)' - }, - aiStudio: { - channel: 'AI Studio API Key / OAuth', - free: '未绑卡(免费层)', - paid: '已绑卡(按量付费)', - limitsFree: 'RPD 50;RPM 2(Pro)/ 15(Flash)', - limitsPaid: 'RPD 不限;RPM 1000(Pro)/ 2000(Flash)(按模型配额)' - }, - customOAuth: { - channel: 'Custom OAuth Client(GCP)', - free: '项目未绑卡', - paid: '项目已绑卡', - limitsFree: 'RPD 50;RPM 2(项目配额)', - limitsPaid: 'RPD 不限;RPM 1000+(项目配额)' - } - } - }, - rateLimit: { - ok: '未限流', - unlimited: '无限流', - limited: '限流 {time}', - now: '现在' - } - }, - // Re-Auth Modal - reAuthorizeAccount: '重新授权账号', - claudeCodeAccount: 'Claude Code 账号', - openaiAccount: 'OpenAI 账号', - geminiAccount: 'Gemini 账号', - antigravityAccount: 'Antigravity 账号', - grokAccount: 'Grok 账号', - inputMethod: '输入方式', - reAuthorizedSuccess: '账号重新授权成功', - // Test Modal - testAccountConnection: '测试账号连接', - account: '账号', - readyToTest: '准备测试。点击"开始测试"按钮开始...', - connectingToApi: '连接 API 中...', - testCompleted: '测试完成!', - connectedToApi: '已连接到 API', - usingModel: '使用模型:{model}', - sendingTestMessage: '发送测试消息:"hi"', - sendingImageRequest: '发送生图测试请求...', - response: '响应:', - startTest: '开始测试', - retry: '重试', - copyOutput: '复制输出', - outputCopied: '输出已复制', - startingTestForAccount: '开始测试账号:{name}', - testAccountTypeLabel: '账号类型:{type}', - selectTestModel: '选择测试模型', - testModel: '测试模型', - testPrompt: '提示词:"hi"', - imagePromptLabel: '生图提示词', - imagePromptPlaceholder: '例如:生成一只戴宇航员头盔的橘猫,像素插画风格,纯色背景。', - imagePromptDefault: 'Generate a cute orange cat astronaut sticker on a clean pastel background.', - imageTestHint: '选择图片模型后,这里会直接发起生图测试,并在下方展示返回图片。', - imageTestMode: '模式:生图测试', - imagePreview: '生成结果:', - imageReceived: '已收到第 {count} 张测试图片', - // Stats Modal - viewStats: '查看统计', - usageStatistics: '使用统计', - last30DaysUsage: '近30天使用统计(日均基于实际使用天数)', - stats: { - totalCost: '30天总费用', - accumulatedCost: '累计成本', - standardCost: '标准计费', - totalRequests: '30天总请求', - totalCalls: '累计调用次数', - avgDailyCost: '日均费用', - basedOnActualDays: '基于 {days} 天实际使用', - avgDailyRequests: '日均请求', - avgDailyUsage: '平均每日调用', - todayOverview: '今日概览', - cost: '费用', - requests: '请求', - tokens: 'Token', - highestCostDay: '最高费用日', - highestRequestDay: '最高请求日', - date: '日期', - accumulatedTokens: '累计 Token', - totalTokens: '30天总计', - dailyAvgTokens: '日均 Token', - performance: '性能', - avgResponseTime: '平均响应', - daysActive: '活跃天数', - recentActivity: '最近统计', - todayRequests: '今日请求', - todayTokens: '今日 Token', - todayCost: '今日费用', - usageTrend: '30天费用与请求趋势', - noData: '该账号暂无使用数据' - } - }, - - // Scheduled Tests - scheduledTests: { - title: '定时测试', - addPlan: '添加计划', - editPlan: '编辑计划', - deletePlan: '删除计划', - model: '模型', - cronExpression: 'Cron 表达式', - enabled: '启用', - lastRun: '上次运行', - nextRun: '下次运行', - maxResults: '最大结果数', - noPlans: '暂无定时测试计划', - confirmDelete: '确定要删除此计划吗?', - createSuccess: '计划创建成功', - updateSuccess: '计划更新成功', - deleteSuccess: '计划删除成功', - results: '测试结果', - noResults: '暂无测试结果', - responseText: '响应', - errorMessage: '错误', - success: '成功', - failed: '失败', - running: '运行中', - schedule: '定时测试', - cronHelp: '标准 5 字段 cron 表达式(例如 */30 * * * *)', - cronTooltipTitle: 'Cron 表达式示例:', - cronTooltipMeaning: '用于定义自动执行测试的时间规则,格式依次为:分钟 小时 日 月 星期。', - cronTooltipExampleEvery30Min: '*/30 * * * *:每 30 分钟运行一次', - cronTooltipExampleHourly: '0 * * * *:每小时整点运行一次', - cronTooltipExampleDaily: '0 9 * * *:每天 09:00 运行一次', - cronTooltipExampleWeekly: '0 9 * * 1:每周一 09:00 运行一次', - cronTooltipRange: '推荐填写范围:使用标准 5 字段 cron;如果只是健康检查,建议从每 30 分钟、每 1 小时或每天固定时间开始,不建议一开始就设置过高频率。', - maxResultsTooltipTitle: '最大结果数说明:', - maxResultsTooltipMeaning: '用于限制单个计划最多保留多少条历史测试结果,避免结果列表无限增长。', - maxResultsTooltipBody: '系统只会保留最近的测试结果;当保存数量超过这个值时,更早的历史记录会自动清理,避免列表过长和存储持续增长。', - maxResultsTooltipExample: '例如填写 100,表示最多保存最近 100 次测试结果;第 101 次结果写入后,最早的一条会被清理。', - maxResultsTooltipRange: '推荐填写范围:一般可填 20 到 200。只关注近期可用性时可填 20-50;需要回看较长时间的波动趋势时可填 100-200。', - autoRecover: '自动恢复', - autoRecoverHelp: '测试成功后自动恢复异常状态的账号' - }, - - // Proxies Management - proxies: { - title: 'IP管理', - description: '管理代理服务器配置', - createProxy: '添加代理', - editProxy: '编辑代理', - deleteProxy: '删除代理', - ad: { - inline: '正在寻找合适的代理 IP?' - }, - deleteConfirmMessage: "确定要删除代理 '{name}' 吗?", - testProxy: '测试代理', - dataImport: '导入', - dataExportSelected: '导出选中', - dataImportTitle: '导入代理', - dataImportHint: '上传代理导出的 JSON 文件以批量导入代理。', - dataImportWarning: '导入将创建或复用代理,保留状态并在完成后自动触发延迟检测。', - dataImportFile: '数据文件', - dataImportButton: '开始导入', - dataImporting: '导入中...', - dataImportSelectFile: '请选择数据文件', - dataImportParseFailed: '数据解析失败', - dataImportFailed: '数据导入失败', - dataImportResult: '导入结果', - dataImportResultSummary: '创建 {proxy_created},复用 {proxy_reused},失败 {proxy_failed}', - dataImportErrors: '失败详情', - dataImportSuccess: '导入完成:创建 {proxy_created},复用 {proxy_reused}', - dataImportCompletedWithErrors: '导入完成但有错误:失败 {proxy_failed}', - dataExport: '导出', - dataExportConfirmMessage: '导出的数据包含代理的敏感信息,请妥善保存。', - dataExportConfirm: '确认导出', - dataExported: '数据导出成功', - dataExportFailed: '数据导出失败', - columns: { - name: '名称', - protocol: '协议', - address: '地址', - auth: '认证', - location: '地理位置', - status: '状态', - accounts: '账号数', - latency: '延迟', - expiry: '有效期', - createdAt: '创建时间', - actions: '操作', - nameLabel: '名称', - namePlaceholder: '请输入代理名称', - protocolLabel: '协议', - selectProtocol: '选择协议', - hostLabel: '主机', - hostPlaceholder: '请输入主机地址', - portLabel: '端口', - portPlaceholder: '请输入端口', - usernameLabel: '用户名(可选)', - usernamePlaceholder: '请输入用户名', - passwordLabel: '密码(可选)', - passwordPlaceholder: '请输入密码', - priorityLabel: '优先级', - statusLabel: '状态' - }, - filters: { - protocol: '协议', - allProtocols: '全部协议', - status: '状态', - allStatuses: '全部状态' - }, - // Additional keys used in ProxiesView - copyProxyUrl: '复制代理 URL', - urlCopied: '代理 URL 已复制', - allProtocols: '全部协议', - allStatus: '全部状态', - searchProxies: '搜索代理...', - protocols: { - http: 'HTTP', - https: 'HTTPS', - socks5: 'SOCKS5', - socks5h: 'SOCKS5H (远程 DNS)', - }, - name: '名称', - protocol: '协议', - host: '主机', - port: '端口', - username: '用户名(可选)', - password: '密码(可选)', - status: '状态', - enterProxyName: '请输入代理名称', - optionalAuth: '可选认证信息', - leaveEmptyToKeep: '留空保持不变', - form: { - hostPlaceholder: '请输入主机地址', - portPlaceholder: '请输入端口' - }, - noProxiesYet: '暂无代理', - createFirstProxy: '添加您的第一个代理以开始使用。', - testConnection: '测试连接', - qualityCheck: '质量检测', - batchQualityCheck: '批量质量检测', - batchTest: '批量测试', - testFailed: '失败', - latencyFailed: '链接失败', - batchTestEmpty: '暂无可测试的代理', - batchTestDone: '批量测试完成,共测试 {count} 个代理', - batchTestFailed: '批量测试失败', - batchDeleteAction: '删除', - batchDelete: '批量删除', - batchDeleteConfirm: '确定删除选中的 {count} 个代理吗?已被账号使用的将自动跳过。', - batchDeleteDone: '已删除 {deleted} 个代理,跳过 {skipped} 个', - batchDeleteSkipped: '已跳过 {skipped} 个代理', - batchDeleteFailed: '批量删除失败', - deleteBlockedInUse: '该代理已有账号使用,无法删除', - accountsTitle: '使用该IP的账号', - accountsEmpty: '暂无账号使用此代理', - accountsFailed: '获取账号列表失败', - accountName: '账号名称', - accountPlatform: '所属平台', - accountNotes: '备注', - // Batch import - standardAdd: '标准添加', - batchAdd: '快捷添加', - batchInput: '代理列表', - batchInputPlaceholder: - "每行输入一个代理,支持以下格式:\nsocks5://user:pass{'@'}192.168.1.1:1080\nhttp://192.168.1.1:8080\nhttps://user:pass{'@'}proxy.example.com:443", - batchInputHint: "支持 http、https、socks5 协议,格式:协议://[用户名:密码{'@'}]主机:端口", - parsedCount: '有效 {count} 个', - invalidCount: '无效 {count} 个', - duplicateCount: '重复 {count} 个', - importing: '导入中...', - importProxies: '导入 {count} 个代理', - batchImportSuccess: '成功导入 {created} 个代理,跳过 {skipped} 个重复', - batchImportAllSkipped: '全部 {skipped} 个代理已存在,跳过导入', - failedToImport: '批量导入失败', - // Other messages - saving: '保存中...', - testing: '测试中...', - creating: '创建中...', - updating: '更新中...', - noProxies: '暂无代理', - noProxiesDescription: '添加代理服务器以增强 API 访问稳定性。', - proxyCreated: '代理添加成功', - proxyUpdated: '代理更新成功', - proxyDeleted: '代理删除成功', - proxyWorking: '代理连接正常', - proxyWorkingWithLatency: '代理连接正常,延迟 {latency}ms', - proxyTestFailed: '代理测试失败', - qualityCheckDone: '质量检测完成:评分 {score}({grade})', - qualityCheckFailed: '代理质量检测失败', - batchQualityDone: '批量质量检测完成,共检测 {count} 个;优质 {healthy} 个,告警 {warn} 个,挑战 {challenge} 个,异常 {failed} 个', - batchQualityFailed: '批量质量检测失败', - batchQualityEmpty: '暂无可检测质量的代理', - qualityReportTitle: '代理质量检测报告', - qualityGrade: '等级 {grade}', - qualityExitIP: '出口 IP', - qualityCountry: '出口地区', - qualityBaseLatency: '基础延迟', - qualityCheckedAt: '检测时间', - qualityTableTarget: '检测项', - qualityTableStatus: '状态', - qualityTableLatency: '延迟', - qualityTableMessage: '说明', - qualityInline: '质量 {grade}/{score}', - qualityStatusHealthy: '优质', - qualityStatusPass: '通过', - qualityStatusWarn: '告警', - qualityStatusFail: '失败', - qualityStatusChallenge: '挑战', - qualityTargetBase: '基础连通性', - proxyCreatedSuccess: '代理添加成功', - proxyUpdatedSuccess: '代理更新成功', - proxyDeletedSuccess: '代理删除成功', - testSuccess: '代理测试通过', - failedToLoad: '加载代理列表失败', - failedToSave: '保存代理失败', - failedToDelete: '删除代理失败', - failedToCreate: '创建代理失败', - failedToUpdate: '更新代理失败', - failedToTest: '测试代理失败', - nameRequired: '请输入代理名称', - hostRequired: '请输入主机地址', - portInvalid: '端口必须在 1-65535 之间', - deleteConfirm: "确定要删除代理 '{name}' 吗?使用此代理的账号将被移除代理设置。", - neverExpires: '永不过期', - expired: '已过期', - overdueDays: '已超期 {days} 天', - expiringInDays: '{days} 天后到期', - remainingDays: '剩余 {days} 天', - expiresAt: '有效期', - nDays: '{days} 天', - expiryDaysPlaceholder: '自定义天数,留空 = 永不过期', - expiryWarnDays: '到期提醒提前天数', - fallbackMode: '失败回退', - fallbackNone: '不回退', - fallbackProxy: '指定备用代理', - fallbackDirect: '回退直连', - backupProxy: '备用代理', - }, - - // Redeem Codes Management - redeem: { - title: '兑换码管理', - description: '生成和管理兑换码', - generateCodes: '生成兑换码', - columns: { - code: '兑换码', - type: '类型', - value: '面值', - status: '状态', - usedBy: '使用者', - usedAt: '使用时间', - expiresAt: '过期时间', - createdAt: '创建时间', - actions: '操作' - }, - types: { - balance: '余额', - concurrency: '并发数', - subscription: '订阅', - invitation: '邀请码', - // 管理员在用户管理页面调整余额/并发时产生的记录 - admin_balance: '余额(管理员)', - admin_concurrency: '并发数(管理员)' - }, - // 用于选择器和筛选器的直接键 - balance: '余额', - concurrency: '并发数', - subscription: '订阅', - invitation: '邀请码', - invitationHint: '邀请码用于限制用户注册,使用后自动标记为已使用。', - allTypes: '全部类型', - allStatus: '全部状态', - unused: '未使用', - used: '已使用', - searchCodes: '搜索兑换码或邮箱...', - exportCsv: '导出 CSV', - batchUpdate: '批量修改', - batchUpdateTitle: '批量修改兑换码', - selectedCount: '已选择 {count} 个兑换码', - clearSelection: '清空选择', - selectCodesFirst: '请先选择兑换码', - noBatchFieldsSelected: '请至少勾选一个要修改的字段', - batchUpdateSuccess: '成功修改 {count} 个兑换码', - failedToBatchUpdate: '批量修改兑换码失败', - batchFields: { - status: '状态', - expiresAt: '过期时间', - notes: '备注', - group: '分组' - }, - batchNotesPlaceholder: '输入新的备注,留空可清空备注', - clearGroup: '清空分组', - deleteAllUnused: '删除全部未使用', - deleteCodeConfirm: '确定要删除此兑换码吗?此操作无法撤销。', - deleteAllUnusedConfirm: '确定要删除全部未使用的兑换码吗?此操作无法撤销。', - deleteAll: '全部删除', - generateCodesTitle: '生成兑换码', - generatedSuccessfully: '生成成功', - codesCreated: '已创建 {count} 个兑换码', - codeType: '类型', - amount: '金额 ($)', - value: '面值', - count: '数量', - generate: '生成', - copyAll: '全部复制', - download: '下载', - codesExported: '兑换码导出成功', - codeDeleted: '兑换码删除成功', - codesDeleted: '成功删除 {count} 个未使用的兑换码', - noUnusedCodes: '没有未使用的兑换码可删除', - userPrefix: '用户 #{id}', - failedToExport: '导出兑换码失败', - failedToDeleteUnused: '删除未使用的兑换码失败', - failedToCopy: '复制失败', - selectGroup: '选择分组', - selectGroupPlaceholder: '选择订阅分组', - validityDays: '有效天数', - codeExpiry: '兑换码过期', - neverExpires: '永不过期', - expiryPresetDays: '{days} 天', - customExpiry: '自定义', - customExpiryDays: '自定义天数', - expiryDaysRequired: '请输入有效的过期天数', - groupRequired: '请选择订阅分组', - days: '天', - status: { - unused: '未使用', - used: '已使用', - expired: '已过期', - disabled: '已禁用' - }, - form: { - typeLabel: '类型', - selectType: '选择类型', - valueLabel: '面值', - valuePlaceholder: '请输入面值', - balanceHint: '余额金额(美元)', - concurrencyHint: '并发数增量', - countLabel: '数量', - countPlaceholder: '请输入数量', - countHint: '要生成的兑换码数量', - prefixLabel: '前缀(可选)', - prefixPlaceholder: '例如:GIFT', - expiresLabel: '过期时间(可选)' - }, - filters: { - type: '类型', - allTypes: '全部类型', - status: '状态', - allStatuses: '全部状态', - search: '搜索兑换码' - }, - generating: '生成中...', - copyCode: '复制', - copied: '已复制!', - disableCode: '禁用', - enableCode: '启用', - deleteCode: '删除', - deleteConfirmMessage: '确定要删除此兑换码吗?', - noCodes: '暂无兑换码', - noCodesDescription: '生成兑换码以向用户分发余额或并发数。', - codesGeneratedSuccess: '兑换码生成成功,共 {count} 个', - codeDisabledSuccess: '兑换码已禁用', - codeEnabledSuccess: '兑换码已启用', - codeDeletedSuccess: '兑换码删除成功', - failedToLoad: '加载兑换码列表失败', - failedToGenerate: '生成兑换码失败', - failedToUpdate: '更新兑换码失败', - failedToDelete: '删除兑换码失败' - }, - - // Announcements - announcements: { - title: '公告管理', - description: '创建公告并按条件投放', - createAnnouncement: '创建公告', - editAnnouncement: '编辑公告', - deleteAnnouncement: '删除公告', - searchAnnouncements: '搜索公告...', - status: '状态', - allStatus: '全部状态', - columns: { - title: '标题', - status: '状态', - notifyMode: '通知方式', - targeting: '展示条件', - timeRange: '有效期', - createdAt: '创建时间', - actions: '操作' - }, - statusLabels: { - draft: '草稿', - active: '展示中', - archived: '已归档' - }, - notifyModeLabels: { - silent: '静默', - popup: '弹窗' - }, - form: { - title: '标题', - content: '内容(支持 Markdown)', - status: '状态', - notifyMode: '通知方式', - notifyModeHint: '弹窗模式会自动弹出通知给用户', - startsAt: '开始时间', - endsAt: '结束时间', - startsAtHint: '留空表示立即生效', - endsAtHint: '留空表示永久生效', - targetingMode: '展示条件', - targetingAll: '所有用户', - targetingCustom: '按条件', - addOrGroup: '添加 OR 条件组', - addAndCondition: '添加 AND 条件', - conditionType: '条件类型', - conditionSubscription: '订阅套餐', - conditionBalance: '余额', - operator: '运算符', - balanceValue: '余额阈值', - selectPackages: '选择套餐' - }, - operators: { - gt: '>', - gte: '≥', - lt: '<', - lte: '≤', - eq: '=' - }, - targetingSummaryAll: '全部用户', - targetingSummaryCustom: '自定义({groups} 组)', - timeImmediate: '立即', - timeNever: '永久', - readStatus: '已读情况', - eligible: '符合条件', - readAt: '已读时间', - unread: '未读', - searchUsers: '搜索用户...', - failedToLoad: '加载公告失败', - failedToCreate: '创建公告失败', - failedToUpdate: '更新公告失败', - failedToDelete: '删除公告失败', - failedToLoadReadStatus: '加载已读情况失败', - deleteConfirm: '确定要删除该公告吗?此操作无法撤销。' - }, - - // Promo Codes - promo: { - title: '优惠码管理', - description: '创建和管理注册优惠码', - createCode: '创建优惠码', - editCode: '编辑优惠码', - deleteCode: '删除优惠码', - searchCodes: '搜索优惠码...', - allStatus: '全部状态', - columns: { - code: '优惠码', - bonusAmount: '赠送金额', - maxUses: '最大使用次数', - usedCount: '已使用', - usage: '使用量', - status: '状态', - expiresAt: '过期时间', - createdAt: '创建时间', - actions: '操作' - }, - // 表单标签(扁平结构便于模板使用) - code: '优惠码', - autoGenerate: '留空自动生成', - codePlaceholder: '输入优惠码或留空', - bonusAmount: '赠送金额 ($)', - maxUses: '最大使用次数', - zeroUnlimited: '0 = 无限制', - expiresAt: '过期时间', - notes: '备注', - notesPlaceholder: '可选备注信息', - status: '状态', - neverExpires: '永不过期', - // 状态标签 - statusActive: '启用', - statusDisabled: '禁用', - statusExpired: '已过期', - statusMaxUsed: '已用完', - // 使用记录 - usageRecords: '使用记录', - viewUsages: '查看使用记录', - noUsages: '暂无使用记录', - userPrefix: '用户 #{id}', - copied: '已复制!', - // 消息 - noCodesYet: '暂无优惠码', - createFirstCode: '创建您的第一个优惠码,为新用户提供注册奖励。', - codeCreated: '优惠码创建成功', - codeUpdated: '优惠码更新成功', - codeDeleted: '优惠码删除成功', - deleteCodeConfirm: '确定要删除此优惠码吗?此操作无法撤销。', - copyRegisterLink: '复制注册链接', - registerLinkCopied: '注册链接已复制到剪贴板', - failedToLoad: '加载优惠码失败', - failedToCreate: '创建优惠码失败', - failedToUpdate: '更新优惠码失败', - failedToDelete: '删除优惠码失败', - failedToLoadUsages: '加载使用记录失败' - }, - - // Usage Records - usage: { - title: '使用记录', - description: '查看和管理所有用户的使用记录', - userFilter: '用户', - searchUserPlaceholder: '按邮箱搜索用户...', - searchApiKeyPlaceholder: '按名称搜索 API 密钥...', - searchAccountPlaceholder: '按名称搜索账号...', - selectedUser: '已选择', - user: '用户', - account: '账户', - group: '分组', - requestId: '请求ID', - requestIdCopied: '请求ID已复制', - allModels: '全部模型', - allAccounts: '全部账户', - allGroups: '全部分组', - allTypes: '全部类型', - inputCost: '输入费用', - outputCost: '输出费用', - cacheCreationCost: '缓存创建费用', - cacheReadCost: '缓存读取费用', - inputTokens: '输入 Token', - outputTokens: '输出 Token', - cacheCreationTokens: '缓存创建 Token', - cacheCreation5mTokens: '缓存创建', - cacheCreation1hTokens: '缓存创建', - cacheReadTokens: '缓存读取 Token', - failedToLoad: '加载使用记录失败', - billingType: '计费类型', - allBillingTypes: '全部计费类型', - billingTypeBalance: '钱包余额', - billingTypeSubscription: '订阅套餐', - billingMode: '计费模式', - billingModeToken: '按量', - billingModePerRequest: '按次', - billingModeImage: '按次(图片)', - allBillingModes: '全部计费模式', - ipAddress: 'IP', - clickToViewBalance: '点击查看充值记录', - failedToLoadUser: '加载用户信息失败', - userDeletedBadge: '已删除', - cleanup: { - button: '清理', - title: '清理使用记录', - warning: '清理不可恢复,且会影响历史统计回看。', - submit: '提交清理', - submitting: '提交中...', - confirmTitle: '确认清理', - confirmMessage: '确定要提交清理任务吗?清理不可恢复。', - confirmSubmit: '确认清理', - cancel: '取消任务', - cancelConfirmTitle: '确认取消', - cancelConfirmMessage: '确定要取消该清理任务吗?', - cancelConfirm: '确认取消', - cancelSuccess: '清理任务已取消', - cancelFailed: '取消清理任务失败', - recentTasks: '最近清理任务', - loadingTasks: '正在加载任务...', - noTasks: '暂无清理任务', - range: '时间范围', - deletedRows: '删除数量', - missingRange: '请选择时间范围', - submitSuccess: '清理任务已创建', - submitFailed: '创建清理任务失败', - loadFailed: '加载清理任务失败', - status: { - pending: '待执行', - running: '执行中', - succeeded: '已完成', - failed: '失败', - canceled: '已取消' - } - } - }, - - // Ops Monitoring - ops: { - title: '运维监控', - description: '运维监控与排障', - // Dashboard - systemHealth: '系统健康', - overview: '概览', - noSystemMetrics: '尚未收集系统指标。', - collectedAt: '采集时间:', - window: '窗口', - memory: '内存', - db: '数据库', - goroutines: '协程', - jobs: '后台任务', - jobsHelp: '点击“明细”查看任务心跳与报错信息', - active: '活跃', - idle: '空闲', - waiting: '等待', - conns: '连接', - queue: '队列', - accountSwitches: '账号切换', - ok: '正常', - lastRun: '最近运行', - lastSuccess: '最近成功', - lastError: '最近错误', - result: '结果', - noData: '暂无数据', - loadingText: '加载中...', - ready: '就绪', - autoRefreshRemaining: '剩余 {seconds}s', - systemLogs: { - title: '系统日志', - description: '优先显示最新日志,可按条件筛选、搜索和清理。', - queue: '队列', - written: '已写入', - dropped: '已丢弃', - failed: '写入失败', - runtimeConfig: '运行时日志配置(立即生效)', - all: '全部', - level: '级别', - stacktraceThreshold: '堆栈阈值', - samplingInitial: '采样初始条数', - samplingThereafter: '后续采样间隔', - retentionDays: '保留天数', - caller: '调用方', - sampling: '采样', - saveAndApply: '保存并应用', - resetDefaults: '重置默认值', - latestWriteError: '最近写入错误:', - timeRange: '时间范围', - startTime: '开始时间(可选)', - endTime: '结束时间(可选)', - component: '组件', - componentPlaceholder: '例如 http.access', - keyId: 'KEY ID', - platform: '平台', - model: '模型', - keyword: '关键词', - keywordPlaceholder: 'message/request_id', - search: '搜索', - cleanCurrentFilters: '清理当前筛选结果', - refreshHealth: '刷新健康状态', - empty: '暂无系统日志', - time: '时间', - logDetails: '日志详情', - loadFailed: '加载系统日志失败', - runtimeConfigActive: '运行时日志配置已生效', - runtimeConfigSaveFailed: '保存日志配置失败', - resetRuntimeConfigConfirm: '确定要重置为启动配置(env/yaml)并立即应用吗?', - runtimeConfigReset: '已重置为启动日志配置', - runtimeConfigResetFailed: '重置日志配置失败', - cleanupConfirm: '确定要清理匹配当前筛选条件的系统日志吗?此操作不可撤销。', - cleanupSuccess: '清理完成,已删除 {count} 条日志。', - cleanupFailed: '清理系统日志失败' - }, - requestsTotal: '请求(总计)', - slaScope: 'SLA 范围:', - tokens: 'Token数', - tps: 'TPS', - current: '当前', - peak: '峰值', - average: '平均', - totalRequests: '总请求', - avgQps: '平均 QPS', - avgTps: '平均 TPS', - avgLatency: '平均请求时长', - avgTtft: '平均首 Token 延迟', - exceptions: '异常数', - requestErrors: '请求错误', - errorCount: '错误数', - upstreamErrors: '上游错误', - errorCountExcl429529: '错误数(排除429/529)', - sla: 'SLA(排除业务限制)', - businessLimited: '业务限制:', - errors: '错误', - errorRate: '错误率:', - upstreamRate: '上游错误率:', - latencyDuration: '请求时长', - ttftLabel: '首 Token 延迟(毫秒)', - p50: 'p50', - p90: 'p90', - p95: 'p95', - p99: 'p99', - avg: 'avg', - max: 'max', - requests: '请求数', - requestsTitle: '请求', - upstream: '上游', - client: '客户端', - system: '系统', - other: '其他', - errorsSla: '错误(SLA范围)', - upstreamExcl429529: '上游(排除429/529)', - failedToLoadData: '加载运维数据失败', - failedToLoadOverview: '加载概览数据失败', - failedToLoadThroughputTrend: '加载吞吐趋势失败', - failedToLoadSwitchTrend: '加载平均账号切换趋势失败', - failedToLoadLatencyHistogram: '加载请求时长分布失败', - failedToLoadErrorTrend: '加载错误趋势失败', - failedToLoadErrorDistribution: '加载错误分布失败', - failedToLoadErrorDetail: '加载错误详情失败', - retryFailed: '重试失败', - tpsK: 'TPS(千)', - top: '最高:', - throughputTrend: '吞吐趋势', - switchRateTrend: '平均账号切换趋势', - latencyHistogram: '请求时长分布', - errorTrend: '错误趋势', - errorDistribution: '错误分布', - switchRate: '平均账号切换', - // Health Score & Diagnosis - health: '健康', - healthCondition: '健康状况', - healthHelp: '基于 SLA、错误率和资源使用情况的系统整体健康评分', - healthyStatus: '健康', - riskyStatus: '风险', - idleStatus: '待机', - timeRange: { - '5m': '近5分钟', - '30m': '近30分钟', - '1h': '近1小时', - '1d': '近1天', - '15d': '近15天', - '6h': '近6小时', - '24h': '近24小时', - '7d': '近7天', - '30d': '近30天', - custom: '自定义' - }, - openaiTokenStats: { - title: 'OpenAI Token 请求统计', - viewModeTopN: 'TopN', - viewModePagination: '分页', - prevPage: '上一页', - nextPage: '下一页', - pageInfo: '第 {page}/{total} 页', - totalModels: '模型总数:{total}', - failedToLoad: '加载 OpenAI Token 统计失败', - empty: '当前筛选条件下暂无 OpenAI Token 请求统计数据', - table: { - model: '模型', - requestCount: '请求数', - avgTokensPerSec: '平均 Tokens/秒', - avgFirstTokenMs: '平均首 Token 延迟(ms)', - totalOutputTokens: '输出 Token 总数', - avgDurationMs: '平均时长(ms)', - requestsWithFirstToken: '首 Token 样本数' - } - }, - customTimeRange: { - startTime: '开始时间', - endTime: '结束时间' - }, - fullscreen: { - enter: '进入全屏' - }, - diagnosis: { - title: '智能诊断', - footer: '基于当前指标的自动诊断建议', - idle: '系统当前处于待机状态', - idleImpact: '无活跃流量', - // Resource diagnostics - dbDown: '数据库连接失败', - dbDownImpact: '所有数据库操作将失败', - dbDownAction: '检查数据库服务状态、网络连接和连接配置', - redisDown: 'Redis连接失败', - redisDownImpact: '缓存功能降级,性能可能下降', - redisDownAction: '检查Redis服务状态和网络连接', - cpuCritical: 'CPU使用率严重过高 ({usage}%)', - cpuCriticalImpact: '系统响应变慢,可能影响所有请求', - cpuCriticalAction: '检查CPU密集型任务,考虑扩容或优化代码', - cpuHigh: 'CPU使用率偏高 ({usage}%)', - cpuHighImpact: '系统负载较高,需要关注', - cpuHighAction: '监控CPU趋势,准备扩容方案', - memoryCritical: '内存使用率严重过高 ({usage}%)', - memoryCriticalImpact: '可能触发OOM,系统稳定性受威胁', - memoryCriticalAction: '检查内存泄漏,考虑增加内存或优化内存使用', - memoryHigh: '内存使用率偏高 ({usage}%)', - memoryHighImpact: '内存压力较大,需要关注', - memoryHighAction: '监控内存趋势,检查是否有内存泄漏', - ttftHigh: '首 Token 时间偏高 ({ttft}ms)', - ttftHighImpact: '用户感知时长增加', - ttftHighAction: '优化请求处理流程,减少前置逻辑耗时', - // Error rate diagnostics - upstreamCritical: '上游错误率严重偏高 ({rate}%)', - upstreamCriticalImpact: '可能影响大量用户请求', - upstreamCriticalAction: '检查上游服务健康状态,启用降级策略', - upstreamHigh: '上游错误率偏高 ({rate}%)', - upstreamHighImpact: '建议检查上游服务状态', - upstreamHighAction: '联系上游服务团队,准备降级方案', - errorHigh: '错误率过高 ({rate}%)', - errorHighImpact: '大量请求失败', - errorHighAction: '查看错误日志,定位错误根因,紧急修复', - errorElevated: '错误率偏高 ({rate}%)', - errorElevatedImpact: '建议检查错误日志', - errorElevatedAction: '分析错误类型和分布,制定修复计划', - // SLA diagnostics - slaCritical: 'SLA 严重低于目标 ({sla}%)', - slaCriticalImpact: '用户体验严重受损', - slaCriticalAction: '紧急排查错误原因,必要时采取限流保护', - slaLow: 'SLA 低于目标 ({sla}%)', - slaLowImpact: '需要关注服务质量', - slaLowAction: '分析SLA下降原因,优化系统性能', - // Health score diagnostics - healthCritical: '综合健康评分过低 ({score})', - healthCriticalImpact: '多个指标可能同时异常,建议优先排查错误与资源使用情况', - healthCriticalAction: '全面检查系统状态,优先处理critical级别问题', - healthLow: '综合健康评分偏低 ({score})', - healthLowImpact: '可能存在轻度波动,建议关注 SLA 与错误率', - healthLowAction: '监控指标趋势,预防问题恶化', - healthy: '所有系统指标正常', - healthyImpact: '服务运行稳定' - }, - // Error Log - errorLog: { - timeId: '时间 / ID', - commonErrors: { - contextDeadlineExceeded: '请求超时', - connectionRefused: '连接被拒绝', - rateLimit: '触发限流' - }, - time: '时间', - type: '类型', - context: '上下文', - platform: '平台', - model: '模型', - group: '分组', - user: '用户', - userId: '用户 ID', - apiKey: 'API Key', - keyDeletedBadge: 'Key 已删除', - account: '账号', - accountId: '账号 ID', - status: '状态码', - message: '响应内容', - ip: 'IP', - latency: '请求时长', - action: '操作', - noErrors: '该窗口内暂无错误。', - grp: 'GRP:', - acc: 'ACC:', - details: '详情', - phase: '阶段', - id: 'ID:', - typeUpstream: '上游', - typeRequest: '请求', - typeAuth: '认证', - typeRouting: '路由', - typeInternal: '内部', - endpoint: '端点', - requestType: '类型', - requestTypeSync: '同步', - requestTypeStream: '流式', - requestTypeWs: 'WS' - }, - // Error Details Modal - errorDetails: { - upstreamErrors: '上游错误', - requestErrors: '请求错误', - unresolved: '未解决', - resolved: '已解决', - viewErrors: '错误', - viewExcluded: '排除项', - statusCodeOther: '其他', - owner: { - provider: '服务商', - client: '客户端', - platform: '平台' - }, - phase: { - request: '请求', - auth: '认证', - routing: '路由', - upstream: '上游', - network: '网络', - internal: '内部' - }, - total: '总计:', - searchPlaceholder: '搜索 request_id / client_request_id / message' - }, - // Error Detail Modal - errorDetail: { - title: '错误详情', - titleWithId: '错误 #{id}', - noErrorSelected: '未选择错误。', - resolution: '已解决:', - failedToUpdateResolvedStatus: '更新解决状态失败', - classificationKeys: { - phase: '阶段', - owner: '归属方', - source: '来源', - resolvedAt: '解决时间', - resolvedBy: '解决人' - }, - source: { - upstream_http: '上游 HTTP' - }, - upstreamKeys: { - status: '状态码', - message: '消息', - detail: '详情', - upstreamErrors: '上游错误列表' - }, - upstreamEvent: { - account: '账号', - status: '状态码', - requestId: '请求ID' - }, - responsePreview: { - expand: '响应内容(点击展开)', - collapse: '响应内容(点击收起)' - }, - loading: '加载中…', - requestId: '请求 ID', - time: '时间', - phase: '阶段', - status: '状态码', - message: '消息', - basicInfo: '基本信息', - platform: '平台', - model: '模型', - group: '分组', - user: '用户', - account: '账号', - latency: '请求时长', - businessLimited: '业务限制', - requestPath: '请求路径', - inboundEndpoint: '入站端点', - upstreamEndpoint: '上游端点', - requestedModel: '请求模型', - upstreamModel: '上游模型', - requestType: '请求类型', - requestTypeUnknown: '未知', - requestTypeSync: '同步', - requestTypeStream: '流式', - requestTypeWs: 'WebSocket', - modelMapping: '模型映射', - timings: '时序信息', - auth: '认证', - routing: '路由', - upstream: '上游', - response: '响应', - classification: '错误分类', - errorBody: '错误体', - trimmed: '已截断', - markResolved: '标记已解决', - markUnresolved: '标记未解决', - tabOverview: '概览', - tabRequest: '请求详情', - tabResponse: '响应详情', - responseBody: '响应详情', - compareA: '对比 A', - compareB: '对比 B', - suggestion: '处理建议', - suggestUpstream: '⚠️ 上游服务不稳定,建议:检查上游账号状态 / 考虑切换账号', - suggestRequest: '⚠️ 客户端请求错误,建议:联系客户修正请求参数 / 手动标记已解决', - suggestAuth: '⚠️ 认证失败,建议:检查 API Key 是否有效 / 联系客户更新凭证', - suggestPlatform: '🚨 平台错误,建议立即排查修复', - suggestGeneric: '查看详情了解更多信息', - apiKeyPrefix: 'Key 前缀', - attemptedKeyPrefix: '尝试的 Key 前缀', - deletedKeyOwner: '已删除 Key 所有者', - keyDeletedBadge: 'Key 已删除' - }, - requestDetails: { - title: '请求明细', - details: '明细', - rangeLabel: '窗口:{range}', - rangeMinutes: '{n} 分钟', - rangeHours: '{n} 小时', - empty: '该窗口内暂无请求。', - emptyHint: '可尝试调整时间范围或取消部分筛选。', - failedToLoad: '加载请求明细失败', - requestIdCopied: '请求ID已复制', - copyFailed: '复制失败', - copy: '复制', - viewError: '查看错误', - kind: { - success: '成功', - error: '失败' - }, - table: { - time: '时间', - kind: '类型', - platform: '平台', - model: '模型', - duration: '耗时', - status: '状态码', - requestId: '请求ID', - actions: '操作' - } - }, - alertEvents: { - title: '告警事件', - description: '最近的告警触发/恢复记录(仅邮件通知)', - loading: '加载中...', - empty: '暂无告警事件', - loadFailed: '加载告警事件失败', - status: { - firing: '告警中', - resolved: '已恢复', - manualResolved: '手动已解决' - }, - detail: { - title: '告警详情', - loading: '加载详情中...', - empty: '暂无详情', - loadFailed: '加载告警详情失败', - manualResolve: '标记为已解决', - manualResolvedSuccess: '已标记为手动解决', - manualResolvedFailed: '标记为手动解决失败', - silence: '忽略此告警', - silenceSuccess: '已静默该告警', - silenceFailed: '静默失败', - viewRule: '查看规则', - viewLogs: '查看相关日志', - firedAt: '触发时间', - resolvedAt: '解决时间', - ruleId: '规则 ID', - dimensions: '维度信息', - historyTitle: '历史记录', - historyHint: '同一规则 + 相同维度的最近事件', - historyLoading: '加载历史中...', - historyEmpty: '暂无历史记录' - }, - table: { - time: '时间', - status: '状态', - severity: '级别', - platform: '平台', - ruleId: '规则ID', - title: '标题', - duration: '持续时间', - metric: '指标 / 阈值', - dimensions: '维度', - email: '邮件已发送', - emailSent: '已发送', - emailIgnored: '已忽略' - } - }, - alertRules: { - title: '告警规则', - description: '创建与管理系统阈值告警(仅邮件通知)', - loading: '加载中...', - empty: '暂无告警规则', - loadFailed: '加载告警规则失败', - saveSuccess: '警报规则保存成功', - saveFailed: '保存告警规则失败', - deleteSuccess: '警报规则删除成功', - deleteFailed: '删除告警规则失败', - create: '新建规则', - createTitle: '新建告警规则', - editTitle: '编辑告警规则', - deleteConfirmTitle: '确认删除该规则?', - deleteConfirmMessage: '将删除该规则及其关联的告警事件,是否继续?', - manage: '预警规则', - metricGroups: { - system: '系统指标', - group: '分组级别指标(需 group_id)', - account: '账号级别指标' - }, - metrics: { - successRate: '成功率 (%)', - errorRate: '错误率 (%)', - upstreamErrorRate: '上游错误率 (%)', - p95: 'P95 请求时长 (ms)', - p99: 'P99 请求时长 (ms)', - cpu: 'CPU 使用率 (%)', - memory: '内存使用率 (%)', - queueDepth: '并发排队深度', - groupAvailableAccounts: '分组可用账号数', - groupAvailableRatio: '分组可用比例 (%)', - groupRateLimitRatio: '分组限流比例 (%)', - accountRateLimitedCount: '限流账号数', - accountErrorCount: '错误账号数(不含临时不可调度)', - accountErrorRatio: '错误账号比例 (%)', - accountTempUnscheduledCount: '临时不可调度账号数', - overloadAccountCount: '过载账号数' - }, - metricDescriptions: { - successRate: '统计窗口内成功请求占比(0~100)。', - errorRate: '统计窗口内失败请求占比(0~100)。', - upstreamErrorRate: '统计窗口内上游错误占比(0~100)。', - p95: '统计窗口内 P95 请求耗时(毫秒)。', - p99: '统计窗口内 P99 请求耗时(毫秒)。', - cpu: '当前实例 CPU 使用率(0~100)。', - memory: '当前实例内存使用率(0~100)。', - queueDepth: '统计窗口内并发队列排队深度(等待中的请求数)。', - groupAvailableAccounts: '指定分组中当前可用账号数量(需要 group_id 过滤)。', - groupAvailableRatio: '指定分组中可用账号占比(0~100,需要 group_id 过滤)。', - groupRateLimitRatio: '指定分组中账号被限流的比例(0~100,需要 group_id 过滤)。', - accountRateLimitedCount: '统计窗口内被限流的账号数量。', - accountErrorCount: '统计窗口内产生错误的账号数量(不含临时不可调度)。', - accountErrorRatio: '统计窗口内错误账号占比(0~100)。', - accountTempUnscheduledCount: '当前处于临时不可调度状态的账号数量(如代理/凭据故障被自动摘除)。', - overloadAccountCount: '统计窗口内过载账号数量。' - }, - hints: { - recommended: '推荐:运算符 {operator},阈值 {threshold}{unit}', - groupRequired: '该指标为分组级别指标,必须选择分组(group_id)。', - groupOptional: '可选:通过 group_id 将规则限定到某个分组。' - }, - table: { - name: '名称', - metric: '指标', - severity: '级别', - enabled: '启用', - actions: '操作' - }, - form: { - name: '名称', - description: '描述', - metric: '指标', - operator: '运算符', - groupId: '分组(group_id)', - groupPlaceholder: '请选择分组', - allGroups: '全部分组', - threshold: '阈值', - severity: '级别', - window: '统计窗口(分钟)', - sustained: '连续样本数(每分钟)', - cooldown: '冷却期(分钟)', - enabled: '启用', - notifyEmail: '发送邮件通知' - }, - validation: { - title: '请先修正以下问题', - invalid: '规则不合法', - nameRequired: '名称不能为空', - metricRequired: '指标不能为空', - groupIdRequired: '分组级别指标必须指定 group_id', - operatorRequired: '运算符不能为空', - thresholdRequired: '阈值必须为数字', - windowRange: '统计窗口必须为 1 / 5 / 60 分钟之一', - sustainedRange: '连续样本数必须在 1 到 1440 之间', - cooldownRange: '冷却期必须在 0 到 1440 分钟之间' - } - }, - runtime: { - title: '运维监控运行设置', - description: '配置存储在数据库中,无需修改 config 文件即可生效。', - loading: '加载中...', - noData: '暂无运行设置', - loadFailed: '加载运行设置失败', - saveSuccess: '运行设置已保存', - saveFailed: '保存运行设置失败', - alertTitle: '告警评估器', - groupAvailabilityTitle: '分组可用性监控', - evalIntervalSeconds: '评估间隔(秒)', - silencing: { - title: '告警静默(维护模式)', - enabled: '启用静默', - globalUntil: '静默截止时间(RFC3339)', - untilHint: '建议填写截止时间,避免忘记关闭静默。', - reason: '原因', - reasonPlaceholder: '例如:计划维护', - entries: { - title: '高级:定向静默', - hint: '可选:仅静默特定规则或特定级别。字段留空表示匹配全部。', - add: '新增条目', - empty: '暂无定向静默条目', - entryTitle: '条目 #{n}', - ruleId: '规则ID(可选)', - ruleIdPlaceholder: '例如:1', - severities: '级别(可选)', - severitiesPlaceholder: '例如:P0,P1(留空=全部)', - until: '截止时间(RFC3339)', - reason: '原因', - validation: { - untilRequired: '条目截止时间不能为空', - untilFormat: '条目截止时间必须为合法的 RFC3339 时间戳', - ruleIdPositive: '条目 rule_id 必须为正整数', - severitiesFormat: '条目级别必须为 P0..P3 的逗号分隔列表' - } - }, - validation: { - timeFormat: '静默时间必须为合法的 RFC3339 时间戳' - } - }, - lockEnabled: '启用分布式锁', - lockKey: '分布式锁 Key', - lockTTLSeconds: '分布式锁 TTL(秒)', - showAdvancedDeveloperSettings: '显示高级开发者设置 (Distributed Lock)', - advancedSettingsSummary: '高级设置 (分布式锁)', - evalIntervalHint: '检测任务的执行频率,建议保持默认。', - validation: { - title: '请先修正以下问题', - invalid: '设置不合法', - evalIntervalRange: '评估间隔必须在 1 到 86400 秒之间', - lockKeyRequired: '启用分布式锁时必须填写 Lock Key', - lockKeyPrefix: '分布式锁 Key 必须以「{prefix}」开头', - lockKeyHint: '建议以「{prefix}」开头以避免冲突', - lockTtlRange: '分布式锁 TTL 必须在 1 到 86400 秒之间', - slaMinPercentRange: 'SLA 最低值必须在 0-100 之间', - ttftP99MaxRange: 'TTFT P99 最大值必须大于或等于 0', - requestErrorRateMaxRange: '请求错误率最大值必须在 0-100 之间', - upstreamErrorRateMaxRange: '上游错误率最大值必须在 0-100 之间' - } - }, - email: { - title: '邮件通知配置', - description: '配置告警/报告邮件通知(存储在数据库中)。', - loading: '加载中...', - noData: '暂无邮件通知配置', - loadFailed: '加载邮件通知配置失败', - saveSuccess: '邮件通知配置已保存', - saveFailed: '保存邮件通知配置失败', - alertTitle: '告警邮件', - reportTitle: '报告邮件', - recipients: '收件人', - recipientsHint: '若为空,系统可能会回退使用第一个管理员邮箱。', - minSeverity: '最低级别', - minSeverityAll: '全部级别', - rateLimitPerHour: '每小时限额', - batchWindowSeconds: '合并窗口(秒)', - includeResolved: '包含恢复通知', - dailySummary: '每日摘要', - weeklySummary: '每周摘要', - errorDigest: '错误摘要', - errorDigestMinCount: '错误摘要最小数量', - accountHealth: '账号健康报告', - accountHealthThreshold: '错误率阈值(%)', - cronPlaceholder: 'Cron 表达式', - reportHint: '发送时间使用 Cron 语法;留空将使用默认值。', - validation: { - title: '请先修正以下问题', - invalid: '邮件通知配置不合法', - alertRecipientsRequired: '已启用告警邮件,但未配置任何收件人', - reportRecipientsRequired: '已启用报告邮件,但未配置任何收件人', - invalidRecipients: '存在不合法的收件人邮箱', - rateLimitRange: '每小时限额必须为 ≥ 0 的数字', - batchWindowRange: '合并窗口必须在 0 到 86400 秒之间', - cronRequired: '启用定时任务时必须填写 Cron 表达式', - cronFormat: 'Cron 表达式格式可能不正确(至少应包含 5 段)', - digestMinCountRange: '错误摘要最小数量必须为 ≥ 0 的数字', - accountHealthThresholdRange: '账号健康错误率阈值必须在 0 到 100 之间' - } - }, - settings: { - title: '运维监控设置', - loadFailed: '加载设置失败', - saveSuccess: '运维监控设置保存成功', - saveFailed: '保存设置失败', - dataCollection: '数据采集', - evaluationInterval: '评估间隔(秒)', - evaluationIntervalHint: '检测任务的执行频率,建议保持默认', - alertConfig: '预警配置', - enableAlert: '开启预警', - alertRecipients: '预警接收邮箱', - emailPlaceholder: '输入邮箱地址', - recipientsHint: '若为空,系统将使用第一个管理员邮箱作为默认收件人', - minSeverity: '最低级别', - reportConfig: '评估报告配置', - enableReport: '开启评估报告', - reportRecipients: '评估报告接收邮箱', - dailySummary: '每日摘要', - weeklySummary: '每周摘要', - metricThresholds: '指标阈值配置', - metricThresholdsHint: '配置各项指标的告警阈值,超出阈值时将以红色显示', - slaMinPercent: 'SLA最低百分比', - slaMinPercentHint: 'SLA低于此值时显示为红色(默认:99.5%)', - ttftP99MaxMs: 'TTFT P99最大值(毫秒)', - ttftP99MaxMsHint: 'TTFT P99高于此值时显示为红色(默认:500ms)', - requestErrorRateMaxPercent: '请求错误率最大值(%)', - requestErrorRateMaxPercentHint: '请求错误率高于此值时显示为红色(默认:5%)', - upstreamErrorRateMaxPercent: '上游错误率最大值(%)', - upstreamErrorRateMaxPercentHint: '上游错误率高于此值时显示为红色(默认:5%)', - advancedSettings: '高级设置', - dataRetention: '数据保留策略', - enableCleanup: '启用数据清理', - cleanupSchedule: '清理计划(Cron)', - cleanupScheduleHint: '例如:0 2 * * * 表示每天凌晨2点', - errorLogRetentionDays: '错误日志保留天数', - minuteMetricsRetentionDays: '分钟指标保留天数', - hourlyMetricsRetentionDays: '小时指标保留天数', - retentionDaysHint: '建议保留 7-90 天,过长会占用存储空间;填 0 表示每次定时清理时清空所有历史', - aggregation: '预聚合任务', - enableAggregation: '启用预聚合任务', - aggregationHint: '预聚合可提升长时间窗口查询性能', - openaiQuotaAutoPause: 'OpenAI 账号配额自动暂停', - openaiQuotaAutoPauseHint: '当 OpenAI 账号 5h / 7d 用量达到阈值时,调度会自动跳过该账号;窗口滚动后自动恢复。账号级阈值优先于此全局默认值。', - openaiQuotaAutoPauseDefault5h: '默认 5h 用量阈值 (%)', - openaiQuotaAutoPauseDefault7d: '默认 7d 用量阈值 (%)', - openaiQuotaAutoPauseThresholdHint: '取值 0-100,留空或 0 表示不启用全局默认阈值。', - errorFiltering: '错误过滤', - ignoreCountTokensErrors: '忽略 count_tokens 错误', - ignoreCountTokensErrorsHint: '启用后,count_tokens 请求的错误将不会写入错误日志。', - ignoreContextCanceled: '忽略客户端断连错误', - ignoreContextCanceledHint: - '启用后,客户端主动断开连接(context canceled)的错误将不会写入错误日志。', - ignoreNoAvailableAccounts: '忽略无可用账号错误', - ignoreNoAvailableAccountsHint: '启用后,"No available accounts" 错误将不会写入错误日志(不推荐,这通常是配置问题)。', - ignoreInvalidApiKeyErrors: '忽略无效 API Key 错误', - ignoreInvalidApiKeyErrorsHint: '启用后,无效或缺失 API Key 的错误(INVALID_API_KEY、API_KEY_REQUIRED)将不会写入错误日志。', - ignoreInsufficientBalanceErrors: '忽略余额不足错误', - ignoreInsufficientBalanceErrorsHint: '启用后,账号余额不足(Insufficient balance)的错误将不会写入错误日志。', - autoRefresh: '自动刷新', - enableAutoRefresh: '启用自动刷新', - enableAutoRefreshHint: '自动刷新仪表板数据,启用后会定期拉取最新数据。', - refreshInterval: '刷新间隔', - refreshInterval15s: '15 秒', - refreshInterval30s: '30 秒', - refreshInterval60s: '60 秒', - dashboardCards: '仪表盘卡片', - displayAlertEvents: '展示告警事件', - displayAlertEventsHint: '控制运维监控仪表盘中告警事件卡片是否显示,默认开启。', - displayOpenAITokenStats: '展示 OpenAI Token 请求统计', - displayOpenAITokenStatsHint: '控制运维监控仪表盘中 OpenAI Token 请求统计卡片是否显示,默认关闭。', - autoRefreshCountdown: '自动刷新:{seconds}s', - validation: { - title: '请先修正以下问题', - retentionDaysRange: '保留天数必须在 0-365 天之间(0 = 每次清理时清空所有)', - slaMinPercentRange: 'SLA最低百分比必须在0-100之间', - ttftP99MaxRange: 'TTFT P99最大值必须大于等于0', - requestErrorRateMaxRange: '请求错误率最大值必须在0-100之间', - upstreamErrorRateMaxRange: '上游错误率最大值必须在0-100之间', - openaiQuotaAutoPauseRange: 'OpenAI 配额自动暂停阈值必须在 0-100 之间' - } - }, - concurrency: { - title: '并发 / 排队', - byPlatform: '按平台', - byGroup: '按分组', - byAccount: '按账号', - byUser: '按用户', - showByUserTooltip: '切换用户视图,显示每个用户的并发使用情况', - switchToUser: '切换到用户视图', - switchToPlatform: '切换回平台视图', - totalRows: '共 {count} 项', - disabledHint: '已在设置中关闭实时监控。', - empty: '暂无数据', - queued: '队列 {count}', - rateLimited: '限流 {count}', - errorAccounts: '异常 {count}', - loadFailed: '加载并发数据失败' - }, - realtime: { - title: '实时信息', - connected: '实时已连接', - connecting: '实时连接中', - reconnecting: '实时重连中', - offline: '实时离线', - closed: '实时已关闭', - reconnectIn: '重连 {seconds}s' - }, - queryMode: { - auto: 'Auto(自动)', - raw: 'Raw(不聚合)', - preagg: 'Preagg(聚合)' - }, - accountAvailability: { - available: '可用', - unavailable: '不可用', - accountError: '异常' - }, - tooltips: { - totalRequests: '当前时间窗口内的总请求数和Token消耗量。', - throughputTrend: '当前窗口内的请求/QPS 与 token/TPS 趋势。', - switchRateTrend: '近5小时内账号切换次数 / 请求总数的趋势(平均切换次数)。', - latencyHistogram: '成功请求的请求时长分布(毫秒)。', - errorTrend: '错误趋势(SLA 口径排除业务限制;上游错误率排除 429/529)。', - errorDistribution: '按状态码统计的错误分布(SLA 口径,排除业务限制)。', - upstreamErrors: '上游服务返回的错误,包括API提供商的错误响应(排除429/529限流错误)。', - goroutines: - 'Go 运行时的协程数量(轻量级线程)。没有绝对"安全值",建议以历史基线为准。经验参考:<2000 常见;2000-8000 需关注;>8000 且伴随队列上升时,优先排查阻塞/泄漏。', - cpu: 'CPU 使用率,显示系统处理器的负载情况。', - memory: '内存使用率,包括已使用和总可用内存。', - db: '数据库连接池状态,包括活跃连接、空闲连接和等待连接数。', - redis: 'Redis 连接池状态,显示活跃和空闲的连接数。', - jobs: '后台任务执行状态,包括最近运行时间、成功时间和错误信息。', - qps: '每秒查询数(QPS)和每秒Token数(TPS),实时显示系统吞吐量。', - tokens: '当前时间窗口内处理的总Token数量。', - sla: '服务等级协议达成率,排除业务限制(如余额不足、配额超限)的成功请求占比。', - errors: '错误统计,包括总错误数、错误率和上游错误率。', - latency: '请求时长统计,包括 p50、p90、p95、p99 等百分位数。', - ttft: '首 Token 延迟(Time To First Token),衡量流式响应的首 Token 返回速度。', - health: '系统健康评分(0-100),综合考虑 SLA、错误率和资源使用情况。' - }, - charts: { - emptyRequest: '该时间窗口内暂无请求。', - emptyError: '该时间窗口内暂无错误。', - resetZoom: '重置', - resetZoomHint: '重置缩放(若启用)', - downloadChart: '下载', - downloadChartHint: '下载图表图片' - } - }, - - // Settings - settings: { - title: '系统设置', - description: '管理注册、邮箱验证、默认值和 SMTP 设置', - tabs: { - general: '通用设置', - agreement: '登录条款', - features: '功能开关', - security: '安全与认证', - users: '用户默认值', - gateway: '网关服务', - email: '邮件设置', - backup: '数据备份', - payment: '支付设置', - }, - features: { - channelMonitor: { - title: '渠道监控', - description: '定期对配置的渠道发起健康检查,向用户展示可用性与延迟。关闭后调度器停止扫描,用户端列表为空。', - configureLink: '前往 渠道管理 > 渠道监控 配置监控项', - enabled: '启用渠道监控', - enabledHint: '关闭后后台不再执行定时检测,已有数据保留。', - defaultInterval: '默认检测间隔(秒)', - defaultIntervalHint: '新建渠道监控时表单的默认值,可被单个渠道覆盖。范围 15 – 3600 秒。', - }, - availableChannels: { - title: '可用渠道', - description: '向已登录用户展示他们能访问的渠道、模型和定价聚合视图。默认关闭。', - configureLink: '前往 渠道管理 > 渠道定价 配置模型价格', - enabled: '启用可用渠道', - enabledHint: '关闭后用户端侧边栏入口隐藏,接口返回空数组。', - }, - riskControl: { - title: '风控中心', - description: '启用内容审计菜单和全端点请求审核入口。默认关闭。', - configureLink: '前往 风控中心 配置内容审计', - enabled: '启用风控中心', - enabledHint: '关闭后管理员侧边栏入口隐藏,网关内容审计不会执行。', - cyberSessionBlock: 'cyber 会话自动屏蔽', - cyberSessionBlockHint: '开启后,被上游网络安全策略(cyber_policy)拦截的会话将在 TTL 内被本地屏蔽,不再发往上游。仅屏蔽该会话,不影响同 Key 其他会话。', - cyberSessionBlockTTL: '屏蔽时长(秒)', - }, - affiliate: { - title: '邀请返利', - description: '老用户邀请新用户注册,新用户充值后老用户按比例获得返利额度。默认关闭。', - enabled: '启用邀请返利', - enabledHint: '关闭后用户菜单中的邀请页面入口隐藏、注册时忽略邀请码、新充值不再产生返利。已有返利额度仍可转入余额。', - rebateRate: '全局返利比例', - rebateRateHint: '充值后返给邀请人的默认比例(0-100%,例如填写 10 表示返利 10%)。', - freezeHours: '返利冻结期(小时)', - freezeHoursDesc: '新产生的返利将在冻结期内无法提现。0 = 不冻结。', - durationDays: '返利有效期(天)', - durationDaysDesc: '被邀请用户注册后多少天内的充值产生返利。0 = 永久有效。', - perInviteeCap: '单人返利上限', - perInviteeCapDesc: '每个被邀请用户最多产生的返利总额。0 = 无上限。', - customUsers: { - title: '专属用户配置', - description: '为指定用户设置专属邀请码或专属返利比例。仅展示已设置过专属配置的用户。', - addButton: '添加专属用户', - searchPlaceholder: '搜索邮箱或用户名', - batchButton: '批量设置比例(已选 {count})', - empty: '暂无专属配置用户', - customBadge: '自定义', - useGlobal: '沿用全局', - resetTitle: '重置该用户的专属配置', - resetMessage: '确认将 {email} 的专属配置全部重置为默认?\n• 专属返利比例将清除(沿用全局)\n• 邀请码将重新生成为系统随机码(已分发的旧邀请链接将失效)', - totalLabel: '共 {total} 条', - col: { - email: '邮箱', - username: '用户名', - code: '邀请码', - rate: '专属比例', - actions: '操作', - }, - }, - modal: { - addTitle: '添加专属用户', - editTitle: '编辑专属配置', - userLabel: '用户', - userPlaceholder: '搜索邮箱或用户名', - changeUser: '更换用户', - codeLabel: '专属邀请码(可选)', - codePlaceholder: '例如 VIP2026', - codeHint: '4-32 位,仅支持大写字母、数字、下划线、连字符;留空表示不修改;输入将自动转大写。', - rateLabel: '专属返利比例(可选)', - ratePlaceholder: '例如 30', - rateHint: '0-100%;留空(编辑模式下)表示清除专属比例并沿用全局。', - errorBadRate: '请输入 0-100 之间的比例', - errorEmpty: '至少填写一项:专属邀请码或专属返利比例', - }, - batchModal: { - title: '批量设置专属比例(已选 {count} 个用户)', - hint: '为所选用户统一设置专属返利比例。', - placeholder: '例如 30', - clearHint: '留空提交将清除所选用户的专属比例。', - }, - }, - }, - emailTabDisabledTitle: '邮箱验证未启用', - emailTabDisabledHint: '请在「安全与认证」选项卡中启用邮箱验证后,再配置 SMTP 设置。', - registration: { - title: '注册设置', - description: '控制用户注册和验证', - enableRegistration: '开放注册', - enableRegistrationHint: '允许新用户注册', - emailVerification: '邮箱验证', - emailVerificationHint: '新用户注册时需要验证邮箱', - emailSuffixWhitelist: '邮箱域名白名单', - emailSuffixWhitelistHint: - "仅允许使用指定域名的邮箱注册账号(例如 {'@'}qq.com, {'@'}gmail.com, *.edu.cn)", - emailSuffixWhitelistPlaceholder: "{'@'}example.com, *.edu.cn", - emailSuffixWhitelistInputHint: '留空则不限制。使用 *.edu.cn 可匹配 edu.cn 及其子域名。', - promoCode: '优惠码', - promoCodeHint: '允许用户在注册时使用优惠码', - invitationCode: '邀请码注册', - invitationCodeHint: '开启后,用户注册时需要填写有效的邀请码', - passwordReset: '忘记密码', - passwordResetHint: '允许用户通过邮箱重置密码', - frontendUrl: '前端地址', - frontendUrlPlaceholder: 'https://example.com', - frontendUrlHint: '用于生成邮件中的密码重置链接,例如 https://example.com', - totp: '双因素认证 (2FA)', - totpHint: '允许用户使用 Google Authenticator 等应用进行二次验证', - totpKeyNotConfigured: - '请先在环境变量中配置 TOTP_ENCRYPTION_KEY。使用命令 openssl rand -hex 32 生成密钥。' - }, - turnstile: { - title: 'Cloudflare Turnstile', - description: '登录和注册的机器人防护', - enableTurnstile: '启用 Turnstile', - enableTurnstileHint: '需要 Cloudflare Turnstile 验证', - siteKey: '站点密钥', - secretKey: '私密密钥', - siteKeyHint: '从 Cloudflare Dashboard 获取', - cloudflareDashboard: 'Cloudflare Dashboard', - secretKeyHint: '服务端验证密钥(请保密)', - secretKeyConfiguredHint: '密钥已配置,留空以保留当前值。' - }, - apiKeyAcl: { - title: 'API Key IP 访问控制', - description: '控制 API Key 白名单和黑名单使用哪个客户端 IP 判断', - trustForwardedIp: '信任反代传递的客户端 IP', - trustForwardedIpHint: - '默认关闭。仅在源站只允许 Cloudflare 或 Nginx 反代访问时开启;开启后 API Key IP 白/黑名单会使用 CF-Connecting-IP、X-Real-IP 或 X-Forwarded-For,与使用记录中的请求 IP 保持一致。' - }, - linuxdo: { - title: 'LinuxDo Connect 登录', - description: '配置 LinuxDo Connect OAuth,用于 Sub2API 用户登录', - enable: '启用 LinuxDo 登录', - enableHint: '在登录/注册页面显示 LinuxDo 登录入口', - clientId: 'Client ID', - clientIdPlaceholder: '例如:hprJ5pC3...', - clientIdHint: '从 Connect.Linux.Do 后台获取', - clientSecret: 'Client Secret', - clientSecretPlaceholder: '********', - clientSecretHint: '用于后端交换 token(请保密)', - clientSecretConfiguredPlaceholder: '********', - clientSecretConfiguredHint: '密钥已配置,留空以保留当前值。', - redirectUrl: '回调地址(Redirect URL)', - redirectUrlPlaceholder: 'https://your-domain.com/api/v1/auth/oauth/linuxdo/callback', - redirectUrlHint: '需与 Connect.Linux.Do 中配置的回调地址一致(必须是 http(s) 完整 URL)', - quickSetCopy: '使用当前站点生成并复制', - redirectUrlSetAndCopied: '已使用当前站点生成回调地址并复制到剪贴板' - }, - dingtalk: { - title: '钉钉登录', - description: '配置钉钉 OAuth,用于 Sub2API 用户登录', - enable: '启用钉钉登录-企业内部应用', - enableHint: '在登录/注册页面显示钉钉登录入口', - clientId: 'Client ID(AppKey)', - clientIdPlaceholder: '例如:dingxxxxxxxxxxxxxxxx', - clientIdHint: '从钉钉开放平台应用详情页获取', - clientSecret: 'Client Secret(AppSecret)', - clientSecretPlaceholder: '********', - clientSecretHint: '用于后端交换 token(请保密)', - clientSecretConfiguredPlaceholder: '********', - clientSecretConfiguredHint: '密钥已配置,留空以保留当前值。', - redirectUrl: '回调地址(Redirect URL)', - redirectUrlPlaceholder: 'https://your-domain.com/api/v1/auth/oauth/dingtalk/callback', - redirectUrlHint: '需与钉钉开放平台中配置的回调地址一致(必须是 http(s) 完整 URL)', - corpPolicy: { - label: '企业限制策略', - hint: '控制哪些钉钉账号(企业)可以登录', - none: '不限制(所有钉钉账号均可登录)', - internalOnly: '仅本企业(Internal Only)' - }, - bypassRegistration: '开放钉钉注册', - bypassRegistrationHint: '即使「开放注册」关闭时也可以通过钉钉登录来注册', - syncDisplayName: '同步钉钉姓名', - syncDisplayNameHint: '登录时将钉钉姓名写入 username 字段(同时记录到 dingtalk_name 属性)', - syncCorpEmail: '同步企业邮箱', - syncCorpEmailHint: '登录时将钉钉企业邮箱写入 dingtalk_email 属性(不影响登录邮箱)', - syncCorpEmailPermissionHint: '需在钉钉开放平台 → 应用 → 权限管理中为本应用申请「邮箱等个人信息(fieldEmail)」权限,否则 OAPI 不会返回企业邮箱字段', - syncDept: '同步部门', - syncDeptHint: '登录时将钉钉首个部门完整路径写入 dingtalk_department 属性(每次登录实时拉取)', - syncDeptPermissionHint: '需在钉钉开放平台 → 应用 → 权限管理中为本应用申请「通讯录部门信息读权限(qyapi_get_department_list)」,否则无法递归出部门路径', - syncDisplayNameTarget: '属性键', - syncDisplayNameTargetHint: '默认 dingtalk_name / 钉钉姓名;保存设置时按上述属性键和显示名称自动创建用户属性(已存在则仅同步显示名称)', - syncCorpEmailTarget: '属性键', - syncCorpEmailTargetHint: '默认 dingtalk_email / 钉钉企业邮箱;保存设置时按上述属性键和显示名称自动创建用户属性(已存在则仅同步显示名称)', - syncDeptTarget: '属性键', - syncDeptTargetHint: '默认 dingtalk_department / 钉钉部门;保存设置时按上述属性键和显示名称自动创建用户属性(已存在则仅同步显示名称)', - syncAttrDisplayName: '显示名称' - }, - oidc: { - title: 'OIDC 登录', - description: '配置标准 OIDC Provider(例如 Keycloak)', - enable: '启用 OIDC 登录', - enableHint: '在登录/注册页面显示 OIDC 登录入口', - providerName: 'Provider 名称', - providerNamePlaceholder: '例如 Keycloak', - clientId: 'Client ID', - clientIdPlaceholder: 'OIDC client id', - clientSecret: 'Client Secret', - clientSecretPlaceholder: '********', - clientSecretHint: '用于后端交换 token(请保密)', - clientSecretConfiguredPlaceholder: '********', - clientSecretConfiguredHint: '密钥已配置,留空以保留当前值。', - issuerUrl: 'Issuer URL', - issuerUrlPlaceholder: 'https://id.example.com/realms/main', - discoveryUrl: 'Discovery URL', - discoveryUrlPlaceholder: '可选,留空将基于 issuer 自动推导', - authorizeUrl: 'Authorize URL', - authorizeUrlPlaceholder: '可选,可通过 discovery 自动获取', - tokenUrl: 'Token URL', - tokenUrlPlaceholder: '可选,可通过 discovery 自动获取', - userinfoUrl: 'UserInfo URL', - userinfoUrlPlaceholder: '可选,可通过 discovery 自动获取', - jwksUrl: 'JWKS URL', - jwksUrlPlaceholder: '可选;启用严格 ID Token 校验时必填', - scopes: 'Scopes', - scopesPlaceholder: 'openid email profile', - scopesHint: '必须包含 openid', - redirectUrl: '后端回调地址(Redirect URL)', - redirectUrlPlaceholder: 'https://your-domain.com/api/v1/auth/oauth/oidc/callback', - redirectUrlHint: '必须与 OIDC Provider 中配置的回调地址一致', - quickSetCopy: '使用当前站点生成并复制', - redirectUrlSetAndCopied: '已使用当前站点生成回调地址并复制到剪贴板', - frontendRedirectUrl: '前端回调路径', - frontendRedirectUrlPlaceholder: '/auth/oidc/callback', - frontendRedirectUrlHint: '后端回调完成后重定向到此前端路径', - tokenAuthMethod: 'Token 鉴权方式', - clockSkewSeconds: '时钟偏移(秒)', - allowedSigningAlgs: '允许的签名算法', - allowedSigningAlgsPlaceholder: 'RS256,ES256,PS256', - usePkce: '启用 PKCE', - validateIdToken: '校验 ID Token', - requireEmailVerified: '要求邮箱已验证', - userinfoEmailPath: 'UserInfo 邮箱字段路径', - userinfoEmailPathPlaceholder: '例如 data.email', - userinfoIdPath: 'UserInfo ID 字段路径', - userinfoIdPathPlaceholder: '例如 data.id', - userinfoUsernamePath: 'UserInfo 用户名字段路径', - userinfoUsernamePathPlaceholder: '例如 data.username' - }, - defaults: { - title: '用户默认设置', - description: '新用户的默认值', - defaultBalance: '默认余额', - defaultBalanceHint: '新用户的初始余额', - affiliateRebateRate: '邀请返利比例', - affiliateRebateRateHint: '充值后返给邀请人的比例(0-100%,例如填写 10 表示返利 10%)', - defaultConcurrency: '默认并发数', - defaultConcurrencyHint: '新用户的最大并发请求数', - defaultUserRpmLimit: '默认用户 RPM 限制', - defaultUserRpmLimitHint: '新用户默认每分钟最大请求数,0 = 不限制;仅作用于新用户创建时初始化', - defaultSubscriptions: '默认订阅列表', - defaultSubscriptionsHint: '新用户创建或注册时自动分配这些订阅', - addDefaultSubscription: '添加默认订阅', - defaultSubscriptionsEmpty: '未配置默认订阅。新用户不会自动获得订阅套餐。', - defaultSubscriptionsDuplicate: '默认订阅存在重复分组:{groupId}。每个分组只能出现一次。', - subscriptionGroup: '订阅分组', - subscriptionValidityDays: '有效期(天)', - defaultPlatformQuotas: '默认平台限额(注册时分配)', - defaultPlatformQuotasHint: '新用户注册时自动写入平台限额记录;已有用户不受影响。留空 = 该平台该窗口不限制。', - platformQuotaNotice: '月限额为 30 天滚动窗口,非自然月', - }, - platformQuota: { - platform: '平台', - daily: '日限额 (USD)', - weekly: '周限额 (USD)', - monthly: '月限额 (USD, 30天滚动)', - placeholder: '不限', - }, - claudeCode: { - title: 'Claude Code 设置', - description: '控制 Claude Code 客户端访问要求', - minVersion: '最低版本号', - minVersionPlaceholder: '例如 2.1.63', - minVersionHint: '拒绝低于此版本的 Claude Code 客户端请求(semver 格式)。留空则不检查版本。', - maxVersion: '最高版本号', - maxVersionPlaceholder: '例如 2.5.0', - maxVersionHint: '拒绝高于此版本的 Claude Code 客户端请求(semver 格式)。留空则不限制最高版本。' - }, - scheduling: { - title: '网关调度设置', - description: '控制 API Key 的调度行为', - allowUngroupedKey: '允许未分组 Key 调度', - allowUngroupedKeyHint: '关闭后,未分配到任何分组的 API Key 将无法发起请求(返回 403)。建议保持关闭以确保所有 Key 都归属明确的分组。' - }, - gatewayForwarding: { - title: '请求转发行为', - description: '控制请求转发到上游 OAuth 账号时的行为', - fingerprintUnification: '指纹统一化', - fingerprintUnificationHint: '统一共享同一 OAuth 账号的用户的 X-Stainless-* 请求头。关闭后透传客户端原始请求头。', - metadataPassthrough: 'Metadata 透传', - metadataPassthroughHint: '透传客户端原始 metadata.user_id,不进行重写。可能提高上游缓存命中率。', - cchSigning: 'CCH 签名', - cchSigningHint: '对转发请求的 billing header 进行 CCH 哈希签名。关闭时保留原始占位符。', - claudeOAuthSystemPromptInjection: 'Claude OAuth System 注入', - claudeOAuthSystemPromptInjectionHint: '为非 Claude Code 客户端的 Claude OAuth 请求注入 Claude Code 形态的 system blocks。默认开启。', - claudeOAuthSystemPrompt: 'Claude OAuth 扩展提示词', - claudeOAuthSystemPromptPlaceholder: '留空时使用内置 Claude Code 扩展提示词。', - claudeOAuthSystemPromptHint: '兼容旧配置:仅控制第三个注入的 system block。', - claudeOAuthSystemPromptBlocks: 'Claude OAuth System Blocks', - claudeOAuthSystemPromptBlocksPlaceholder: '留空时使用内置 3 个 blocks。支持数组或 {"blocks": [...]}。', - claudeOAuthSystemPromptBlocksHint: '每个 block 会保存为带 enabled、type、text、可选 cache_control 的 JSON。{billing_header} 会按请求动态生成;Claude Code 身份提示词和扩展提示词可直接编辑,也可用预设恢复默认值。', - systemBlockTitle: 'System Block {index}', - systemBlockPreset: '预设', - systemBlockPresetBilling: 'Billing Header', - systemBlockPresetIdentity: 'Claude Code 身份提示词', - systemBlockPresetExpansion: 'Claude Code 扩展提示词', - systemBlockPresetCustom: '自定义', - systemBlockType: '类型', - systemBlockTypeText: '文本', - systemBlockText: '内容', - systemBlockCacheControl: 'Cache Control', - systemBlockHide: '隐藏 block 详情', - systemBlockShow: '展示 block 详情', - addSystemBlock: '添加 block', - resetSystemBlocks: '恢复默认', - cacheTTL5m: '5 分钟', - cacheTTL1h: '1 小时', - anthropicCacheTTL1hInjection: 'Anthropic 缓存 TTL 注入', - anthropicCacheTTL1hInjectionHint: '开启后,对 Anthropic OAuth/Setup Token 请求体中已有的 ephemeral 缓存块强制写入 1h;响应 usage 默认按 5m 回写计费,账号级 TTL 计费设置优先。', - rewriteMessageCacheControl: '改写消息缓存断点', - rewriteMessageCacheControlHint: '默认关闭,保留客户端在 messages 内容块中的 cache_control。开启后会清除客户端断点并注入代理断点,适合不自行管理缓存策略的客户端。', - clientDatelineNormalization: '客户端 dateline 归一化', - clientDatelineNormalizationHint: '默认开启。将 Anthropic OAuth/Setup Token 请求体中 "Today\'s date is …" 语句里的撇号与日期分隔符还原为 ASCII 撇号 + 短横线 (2026-07-01) 的规范形态,抹除某些客户端在检测到非官方 base URL 时注入的隐写指纹位。仅作用于 system prompt 与 块内,API Key 账号不受影响。', - antigravityUserAgentVersion: 'Antigravity UA 版本', - antigravityUserAgentVersionPlaceholder: '1.23.2', - antigravityUserAgentVersionHint: '留空时使用 ANTIGRAVITY_USER_AGENT_VERSION 或内置默认值 1.23.2;填写后后台设置优先。', - openaiCodexUserAgent: 'OpenAI Codex UA', - openaiCodexUserAgentPlaceholder: 'codex-tui/0.125.0 (Ubuntu 22.4.0; x86_64) xterm-256color (codex-tui; 0.125.0)', - openaiCodexUserAgentHint: '用于规避 OpenAI 上游 Cloudflare 对浏览器 UA 的访问质询。仅在检测到客户端 User-Agent 为浏览器(Mozilla/...)时生效,其他客户端原样透传。留空使用内置默认值。', - codexHardeningTitle: 'Codex 设置', - codexClientRestrictionTitle: 'Codex 客户端限制', - codexHardeningDesc: - '仅对已开启「仅允许 Codex 官方客户端」的 OpenAI OAuth 账号生效(全局)。在 User-Agent/Originator 之外,用版本区间、引擎指纹门与黑/白名单巩固判定。', - minCodexVersion: '最低 Codex 版本', - minCodexVersionPlaceholder: '例如 0.142.0', - maxCodexVersion: '最高 Codex 版本', - maxCodexVersionPlaceholder: '例如 0.200.0', - codexVersionHint: - '仅对官方客户端生效,校验其版本是否落在 [最低, 最高] 区间。留空表示该侧不限制。', - codexFingerprintSignals: 'Codex 引擎指纹信号', - codexFingerprintSignalsDesc: - '定义引擎指纹信号:勾「必须」的信号需全部命中(AND),每条 / 分隔的变体取或(OR);一条都不勾即不校验。默认只勾 x-codex- 前缀。类型:头精确 / 头前缀 / body 路径。', - codexFpTypeHeaderExact: '头精确', - codexFpTypeHeaderPrefix: '头前缀', - codexFpTypeBodyPath: 'body 路径', - codexFpMatchPlaceholder: '匹配,变体用 / 分隔(如 session-id / session_id 或 x-codex-)', - codexFpRequired: '必须', - codexFingerprintNoRequiredWarn: '未勾选任何「必须」信号——引擎指纹门当前不生效,等于放行所有通过身份/版本的候选。如需启用校验,请至少勾选一条信号。', - codexAllowAppServer: 'Codex app-server', - codexAllowAppServerDesc: - '放行内嵌 Codex 引擎、经 app-server 协议接入的第三方客户端(如 Claude Code 的 codex 插件)。默认关闭;开启后此类客户端通过引擎指纹门(下方信号列表)即放行,关闭则仅放行官方客户端与白名单。', - codexBlacklist: 'User-Agent/Originator 黑名单', - codexBlacklistDesc: - '命中任一字段即拒,优先于一切放行。originator 精确匹配,User-Agent 为包含匹配(多个用逗号分隔)。', - codexWhitelist: 'User-Agent/Originator 白名单', - codexWhitelistDesc: - '放行官方集之外的客户端:需 originator 精确,且每个 User-Agent 标记都命中。默认仍需过引擎指纹门,勾「跳过引擎指纹」可免。', - codexWhitelistSkipFingerprint: '跳过引擎指纹', - codexWhitelistSkipFingerprintTooltip: - '风险:勾选后该条仅凭 originator + User-Agent(均可伪造)放行,不再要求引擎指纹兜底。仅用于确属可信、但本身不发 codex 引擎指纹的第三方客户端。', - codexOriginatorPlaceholder: 'originator(精确,如 opencode)', - codexUaContainsPlaceholder: 'User-Agent 包含标记,逗号分隔(如 opencode/)', - codexAddRow: '添加一条', - codexRemoveRow: '删除', - }, - webSearchEmulation: { - title: 'Web Search 模拟', - description: '为不原生支持搜索的 Anthropic API Key 账号注入 web search 能力', - enabled: '启用 Web Search 模拟', - enabledHint: '全局开关。关闭后所有渠道和账号的 web search 模拟均不生效。', - providers: '搜索服务商', - addProvider: '添加服务商', - providerType: '服务商类型', - apiKey: 'API Key', - apiKeyPlaceholder: '输入 API Key', - apiKeyConfigured: '已配置', - showApiKey: '显示', - hideApiKey: '隐藏', - copyApiKey: '复制', - copied: '已复制', - quotaLimit: '配额上限', - quotaLimitHint: '留空表示无限制;填写时必须大于 0', - quotaLimitMustBePositive: '配额上限必须大于 0', - subscribedAt: '订阅时间', - subscribedAtHint: '配额从此日期起每月自动重置;留空则不自动重置', - quotaUsage: '用量', - resetUsage: '重置', - resetUsageConfirm: '确定要重置此服务商的用量计数吗?', - resetUsageSuccess: '用量已重置', - proxy: '代理', - removeProvider: '删除', - noProviders: '未配置搜索服务商', - test: '测试', - testDefaultQuery: '搜索今年世界大事件', - testing: '搜索中...', - testResultTitle: '搜索结果', - testResultProvider: '服务商', - testNoResults: '无搜索结果', - }, - site: { - title: '站点设置', - description: '自定义站点品牌', - backendMode: 'Backend 模式', - backendModeDescription: - '禁用用户注册、公开页面和自助服务功能。仅管理员可以登录和管理平台。', - siteName: '站点名称', - siteNameHint: '显示在邮件和页面标题中', - siteNamePlaceholder: 'Sub2API', - siteSubtitle: '站点副标题', - siteSubtitleHint: '显示在登录和注册页面', - siteSubtitlePlaceholder: '订阅转 API 转换平台', - apiBaseUrl: 'API 端点地址', - apiBaseUrlHint: '用于"使用密钥"、"导入到 CC Switch"和回调地址建议,留空则使用当前站点地址', - apiBaseUrlPlaceholder: 'https://api.example.com', - tablePreferencesTitle: '通用表格设置', - tablePreferencesDescription: '设置后台与用户侧表格组件的默认分页行为', - tableDefaultPageSize: '默认每页条数', - tableDefaultPageSizeHint: '必须为 5-1000 之间的整数', - tablePageSizeOptions: '可选每页条数列表', - tablePageSizeOptionsPlaceholder: '10, 20, 50, 100', - tablePageSizeOptionsHint: '使用英文逗号分隔,取值范围 5-1000,保存时会自动去重并排序', - tableDefaultPageSizeRangeError: '默认每页条数必须在 {min}-{max} 之间', - tablePageSizeOptionsFormatError: '可选每页条数格式无效,请输入 {min}-{max} 之间的整数并用英文逗号分隔', - customEndpoints: { - title: '自定义端点', - description: '添加额外的 API 端点地址,用户可在「API Keys」页面快速复制', - itemLabel: '端点 #{n}', - name: '名称', - namePlaceholder: '如:OpenAI Compatible', - endpointUrl: '端点地址', - endpointUrlPlaceholder: 'https://api2.example.com', - descriptionLabel: '介绍', - descriptionPlaceholder: '如:支持 OpenAI 格式请求', - add: '添加端点', - }, - contactInfo: '客服联系方式', - contactInfoPlaceholder: '例如:QQ: 123456789', - contactInfoHint: '填写客服联系方式,将展示在兑换页面、个人资料等位置', - docUrl: '文档链接', - docUrlHint: '文档网站的链接。留空则隐藏文档链接。', - docUrlPlaceholder: 'https://docs.example.com', - siteLogo: '站点Logo', - uploadImage: '上传图片', - remove: '移除', - logoHint: 'PNG、JPG 或 SVG 格式,最大 300KB。建议:80x80px 正方形图片。', - logoSizeError: '图片大小超过 300KB 限制({size}KB)', - logoTypeError: '请选择图片文件', - logoReadError: '读取图片文件失败', - homeContent: '首页内容', - homeContentPlaceholder: - '在此输入首页内容,支持 Markdown & HTML 代码。如果输入的是一个链接,则会使用该链接作为 iframe 的 src 属性。', - homeContentHint: - '自定义首页内容,支持 Markdown/HTML。如果输入的是链接(以 http:// 或 https:// 开头),则会使用该链接作为 iframe 的 src 属性,这允许你设置任意网页作为首页。设置后首页的状态信息将不再显示。', - homeContentIframeWarning: - '⚠️ iframe 模式提示:部分网站设置了 X-Frame-Options 或 CSP 安全策略,禁止被嵌入到 iframe 中。如果页面显示空白或报错,请确认目标网站允许被嵌入,或考虑使用 HTML 模式自行构建页面内容。', - hideCcsImportButton: '隐藏 CCS 导入按钮', - hideCcsImportButtonHint: '启用后将在 API Keys 页面隐藏"导入 CCS"按钮' - }, - purchase: { - title: '充值/订阅页面', - description: '在侧边栏展示“充值/订阅”入口,并在页面内通过 iframe 打开指定链接', - enabled: '显示充值/订阅入口', - enabledHint: '仅在标准模式(非简单模式)下展示', - url: '充值/订阅页面 URL', - urlPlaceholder: 'https://example.com/purchase', - urlHint: '必须是完整的 http(s) 链接', - iframeWarning: - '⚠️ iframe 提示:部分网站会通过 X-Frame-Options 或 CSP(frame-ancestors)禁止被 iframe 嵌入,出现空白时可引导用户使用”新窗口打开”。', - integrationDoc: '支付集成文档', - integrationDocHint: '包含接口说明、幂等语义及示例代码' - }, - soraClient: { - title: 'Sora 客户端', - description: '控制是否在侧边栏展示 Sora 客户端入口', - enabled: '启用 Sora 客户端', - enabledHint: '开启后,侧边栏将显示 Sora 入口,用户可访问 Sora 功能' - }, - customMenu: { - title: '自定义菜单页面', - description: '添加自定义 iframe 页面到侧边栏导航。每个页面可以设置为普通用户或管理员可见。', - itemLabel: '菜单项 #{n}', - name: '菜单名称', - namePlaceholder: '如:帮助中心', - url: '页面 URL', - urlPlaceholder: 'https://example.com/page', - iconSvg: 'SVG 图标', - iconSvgPlaceholder: '...', - iconPreview: '图标预览', - uploadSvg: '上传 SVG', - removeSvg: '清除', - visibility: '可见角色', - visibilityUser: '普通用户', - visibilityAdmin: '管理员', - add: '添加菜单项', - remove: '删除', - moveUp: '上移', - moveDown: '下移', - }, - payment: { - title: '支付设置', - description: '配置支付系统选项', - configGuide: '支付配置指南', - enabled: '启用支付', - enabledHint: '启用或禁用支付系统', - enabledPaymentTypes: '启用的服务商', - enabledPaymentTypesHint: '禁用服务商将同时禁用对应的实例。', - findProvider: '正在寻找合适的易支付服务商?', - minAmount: '最低金额', - maxAmount: '最高金额', - dailyLimit: '每日限额', - balanceRechargeMultiplier: '余额充值倍率', - balanceRechargeMultiplierHint: '用户每支付 1 CNY 可获得多少 USD 余额', - balanceRechargePreview: '预览:1 CNY = {usd} USD', - subscriptionUsdToCnyRate: '订阅 CNY 换算汇率', - subscriptionUsdToCnyRateHint: - 'CNY 支付通道下,套餐每 1 USD 价格收取多少 CNY(如 7.15)。0 或留空 = 不换算,订阅按 price 数值直接收款。启用后所有套餐 price 必须按 USD 定价', - subscriptionUsdToCnyRateDisabled: '未启用(按 price 直付)', - rechargeFeeRate: '充值手续费率', - rechargeFeeRateHint: '用户充值时额外收取的手续费百分比,0 表示不收取手续费', - rechargeFeePreview: '预览:充值 100 元,手续费 {fee} 元', - orderTimeout: '订单超时时间', - orderTimeoutHint: '单位:分钟,至少 1 分钟', - maxPendingOrders: '最大待支付订单数', - cancelRateLimit: '限制取消频率', - cancelRateLimitHint: '启用后,用户在时间窗口内取消订单次数超限将无法创建新订单', - cancelRateLimitEvery: '每', - cancelRateLimitAllowMax: '最多', - cancelRateLimitTimes: '次', - cancelRateLimitWindow: '时间窗口', - cancelRateLimitUnit: '周期', - cancelRateLimitMax: '最大取消次数', - cancelRateLimitUnitMinute: '分钟', - cancelRateLimitUnitHour: '小时', - cancelRateLimitUnitDay: '天', - cancelRateLimitWindowMode: '窗口模式', - cancelRateLimitWindowModeRolling: '滚动', - cancelRateLimitWindowModeFixed: '固定', - alipayForceQRCode: '支付宝强制二维码支付', - alipayForceQRCodeHint: '启用后,移动端支付宝用户将统一使用二维码扫码支付,不再跳转至手机网站支付', - helpText: '帮助文本', - helpImageUrl: '帮助图片链接', - manageProviders: '管理服务商', - balancePaymentDisabled: '禁用余额充值', - noLimit: '留空表示不限制', - helpImage: '帮助图片', - helpImagePlaceholder: '上传或输入图片链接', - helpTextPlaceholder: '输入帮助说明文本...', - providerEasypay: '易支付', - providerAlipay: '支付宝官方', - providerWxpay: '微信官方', - providerStripe: 'Stripe', - providerAirwallex: 'Airwallex', - typeDisabled: '类型已禁用', - enableTypesFirst: '请先在上方启用至少一种服务商', - easypayRedirect: '跳转', - paymentMode: '支付模式', - modeRedirect: '跳转', - modeQRCode: '二维码', - modePopup: '弹窗', - validationNameRequired: '服务商名称不能为空', - validationTypesRequired: '请至少选择一种支持的支付方式', - validationFieldRequired: '{field} 不能为空', - validationEasyPayCustomMethodRequired: '每个易支付自定义方式都必须填写支付方式和上游 type', - validationEasyPayCustomMethodTypeInvalid: '易支付自定义支付方式只能包含小写字母、数字、下划线和短横线', - validationEasyPayCustomMethodUpstreamTypeInvalid: '易支付上游 type 只能包含小写字母、数字、下划线和短横线', - validationEasyPayCustomMethodReserved: '易支付自定义支付方式不能使用内置的 alipay 或 wxpay', - validationEasyPayCustomMethodPrefixReserved: '易支付自定义支付方式不能以 alipay 或 wxpay 开头', - validationEasyPayCustomMethodDuplicate: '易支付自定义支付方式不能重复', - field_apiBase: 'API 基础地址', - field_notifyUrl: '异步通知地址', - field_returnUrl: '同步跳转地址', - callbackBaseUrl: '回调基础地址', - field_privateKey: '私钥', - field_publicKey: '公钥', - field_mpAppId: '公众号 App ID', - field_mchId: '商户号', - field_apiV3Key: 'API v3 密钥', - field_publicKeyId: '公钥 ID', - field_certSerial: '证书序列号', - field_h5AppName: 'H5 应用名称', - field_h5AppUrl: 'H5 应用地址', - wxpayConfigHint: '微信支付通常只需要填写 App ID。公众号 App ID、H5 应用名称、H5 应用地址仅在公众号支付或 H5 场景有特殊要求时再填写。', - wxpayAdvancedOptions: '微信支付高级可选项', - field_secretKey: '密钥', - field_clientId: 'Client ID', - field_apiKey: 'API Key', - field_publishableKey: '公开密钥', - field_webhookSecret: 'Webhook 密钥', - field_countryCode: '国家/地区代码', - field_currency: '支付币种', - field_accountId: 'Airwallex 账户 ID', - field_airwallexApiBaseHint: '必须和 API Key 所属环境一致:沙箱/测试密钥使用 https://api-demo.airwallex.com/api/v1,生产密钥使用 https://api.airwallex.com/api/v1。环境混用会返回 credentials_invalid / Access Denied。', - field_paymentCurrencyHint: '默认 CNY。Stripe 和 Airwallex 可按账户支持从下拉项选择 HKD、USD 等币种;微信、支付宝、易支付仍按 CNY。', - field_accountIdHint: '不涉及多账户、组织级密钥或连接账户收款时可以不填;单账户 Scoped API Key 会默认使用所选账户。', - field_cid: '支付渠道 ID', - field_cidAlipay: '支付宝渠道 ID', - field_cidWxpay: '微信渠道 ID', - easypayCustomMethods: '易支付自定义支付方式', - easypayCustomMethodsHint: '添加当前易支付服务商额外支持的支付方式。支付方式会记录到 Sub2API 订单中,上游 type 会作为易支付 type 参数提交。', - addCustomMethod: '添加方式', - customMethodType: '支付方式', - customMethodUpstreamType: '上游 type', - customMethodDisplayName: '显示名称', - stripeWebhookHint: '请在 Stripe Dashboard 中将以下地址配置为 Webhook 端点:', - stripeWebhookApiVersionHint: 'Webhook 端点的 API 版本请与当前集成的 Stripe SDK 对齐,建议选择 {version};版本不一致可能导致回调事件解析失败。', - airwallexWebhookHint: '请在 Airwallex 后台将以下地址配置为 Webhook 端点;事件至少选择 Payment Intent -> Succeeded(payment_intent.succeeded),建议同时选择 Payment Intent -> Cancelled(payment_intent.cancelled);API version 选择账户默认或最新稳定版本。', - airwallexGuideSummary: '创建 Airwallex Scoped API 密钥时,建议只在账户级权限中为 Payment Acceptance 勾选读取和写入。', - airwallexGuideNote: '不需要勾选 Spend、Payouts、Transfers、Funds Splits、POS 终端等与在线收款无关的权限。Webhook 事件至少选择 payment_intent.succeeded,建议同时选择 payment_intent.cancelled;API version 选择账户默认或最新稳定版本。', - limitsTitle: '限额配置', - limitSingleMin: '单笔最低', - limitSingleMax: '单笔最高', - limitDaily: '每日限额', - limitsHint: '全部留空使用全局配置,部分填写时留空项表示不限制', - limitsUseGlobal: '使用全局配置', - limitsNoLimit: '不限制', - productNamePrefix: '商品名前缀', - productNameSuffix: '商品名后缀', - preview: '预览', - loadBalanceStrategy: '负载均衡策略', - strategyRoundRobin: '轮询', - strategyLeastAmount: '最少金额', - providerManagement: '服务商管理', - providerManagementDesc: '管理支付服务商实例', - createProvider: '添加服务商', - editProvider: '编辑服务商', - deleteProvider: '删除服务商', - deleteProviderConfirm: '确定要删除此服务商吗?', - providerName: '服务商名称', - providerKey: '服务商类型', - selectProviderKey: '选择服务商类型', - providerConfig: '凭证配置', - paymentGuideTrigger: '查看支付方式说明', - guideOpenLabel: '开通:', - guideCallLabel: '调用:', - guideFallbackLabel: '降级:', - alipayGuideSummary: '桌面优先扫码单,失败再走收银台;移动优先手机网站支付。', - alipayGuideFaceToFaceTitle: '当面付 / 扫码支付', - alipayGuideFaceToFaceOpen: '需开通当面付或扫码支付能力。', - alipayGuideFaceToFaceCall: '桌面端下单时优先调用 alipay.trade.precreate,前台直接渲染二维码。', - alipayGuideFaceToFaceFallback: '接口不可用或返回失败时,自动降级到电脑网站支付。', - alipayGuidePagePayTitle: '电脑网站支付', - alipayGuidePagePayOpen: '需开通电脑网站支付。', - alipayGuidePagePayCall: '桌面端当面付不可用时调用 alipay.trade.page.pay,并继续把返回链接渲染成二维码。', - alipayGuidePagePayFallback: '同时保留打开收银台入口,用户可手动重新拉起支付页。', - alipayGuideWapTitle: '手机网站支付', - alipayGuideWapOpen: '需开通手机网站支付。', - alipayGuideWapCall: '移动端优先调用 alipay.trade.wap.pay,跳转支付宝收银台。', - alipayGuideWapFallback: '未开通或返回异常时,前端自动改走扫码支付并提示未开通移动支付。', - wxpayGuideSummary: '桌面优先 Native 扫码,移动端按浏览器环境走 JSAPI 或 H5。', - wxpayGuideNote: '当前表单默认共用一个 App ID,适合同主体下统一配置网页、移动和公众号场景。', - wxpayGuideNativeTitle: 'Native / 扫码支付', - wxpayGuideNativeOpen: '需开通 Native 或扫码支付能力。', - wxpayGuideNativeCall: '桌面端默认调用 Native,下发二维码内容给前台渲染。', - wxpayGuideNativeFallback: '移动端无法走 JSAPI 或 H5 时,也会自动回退到这里。', - wxpayGuideJsapiTitle: 'JSAPI / 公众号支付', - wxpayGuideJsapiOpen: '需开通公众号支付,并保证当前浏览器在微信内且能拿到 OpenID。', - wxpayGuideJsapiCall: '微信内浏览器完成授权后调用 JSAPI,直接拉起微信支付。', - wxpayGuideJsapiFallback: '未配置、Bridge 不可用或拉起失败时,自动改走扫码支付。', - wxpayGuideH5Title: 'H5 支付', - wxpayGuideH5Open: '需开通 H5 支付。', - wxpayGuideH5Call: '移动端非微信浏览器且有客户端 IP 时调用 H5 支付,跳转微信收银台。', - wxpayGuideH5Fallback: '未开通 H5 或下单失败时,自动改走扫码支付。', - noProviders: '暂无服务商实例', - supportedTypes: '支持的支付方式', - supportedTypesHint: '逗号分隔,如 alipay,wxpay', - refundEnabled: '允许退款', - allowUserRefund: '允许用户退款', - enableConflict: '{method} 已有启用中的服务商实例:{provider}。请先停用现有实例后再启用或切换。', - }, - balanceNotify: { - title: '余额不足提醒', - description: '当用户余额低于阈值时发送邮件提醒', - enabled: '启用余额不足提醒', - threshold: '默认提醒阈值', - thresholdHint: '用户未自定义时使用此值', - thresholdPlaceholder: '输入金额', - rechargeUrl: '充值页面 URL', - rechargeUrlPlaceholder: 'https://example.com/payment', - rechargeUrlHint: '设置后邮件中将包含充值链接按钮', - }, - quotaNotify: { - title: '账号限额通知', - description: '当账号配额用量达到告警阈值时通知管理员', - enabled: '启用账号限额通知', - emails: '通知邮箱', - emailsHint: '留空则不发送通知', - addEmail: '添加邮箱', - emailPlaceholder: '输入邮箱地址', - }, - subscriptionExpiryNotify: { - title: '订阅到期提醒', - description: '控制是否向用户发送订阅即将到期的邮件提醒。', - enabled: '启用订阅到期提醒', - enabledHint: '开启后,系统会在订阅到期前 7 天、3 天、1 天各发送一次提醒。' - }, - smtp: { - title: 'SMTP 设置', - description: '配置用于发送验证码的邮件服务', - testConnection: '测试连接', - testing: '测试中...', - host: 'SMTP 主机', - hostPlaceholder: 'smtp.gmail.com', - port: 'SMTP 端口', - portPlaceholder: '587', - username: 'SMTP 用户名', - usernamePlaceholder: "your-email{'@'}gmail.com", - password: 'SMTP 密码', - passwordPlaceholder: '********', - passwordHint: '留空以保留现有密码', - passwordConfiguredPlaceholder: '********', - passwordConfiguredHint: '密码已配置,留空以保留当前值。', - fromEmail: '发件人邮箱', - fromEmailPlaceholder: "noreply{'@'}example.com", - fromName: '发件人名称', - fromNamePlaceholder: 'Sub2API', - useTls: '使用 TLS', - useTlsHint: '为 SMTP 连接启用 TLS 加密' - }, - testEmail: { - title: '发送测试邮件', - description: '发送测试邮件以验证 SMTP 配置', - recipientEmail: '收件人邮箱', - recipientEmailPlaceholder: "test{'@'}example.com", - sendTestEmail: '发送测试邮件', - sending: '发送中...', - enterRecipientHint: '请输入收件人邮箱地址' - }, - emailTemplates: { - title: '邮件模板', - description: '按事件和语言自定义通知邮件主题与 HTML 内容。', - event: '事件', - locale: '语言', - localeEn: '英文', - localeZh: '中文', - subject: '主题', - subjectPlaceholder: '输入邮件主题', - html: 'HTML 模板', - htmlPlaceholder: '编辑邮件 HTML 模板', - placeholders: '可用占位符', - placeholdersHelp: '点击占位符可复制。后端发送邮件时会替换这些值。', - livePreview: '实时预览', - previewSecurityHint: '预览 HTML 由后端预览接口生成,并在禁用脚本的沙盒 iframe 中展示。', - preview: '预览 / 刷新', - previewing: '预览中...', - save: '保存模板', - saving: '保存中...', - restoreOfficial: '恢复官方模板', - restoring: '恢复中...', - restoreConfirm: '确定恢复此事件和语言的官方模板吗?当前自定义版本将被替换。', - restoreSuccess: '已恢复官方模板', - saveSuccess: '邮件模板已保存', - placeholderCopied: '占位符已复制', - validationRequired: '主题和 HTML 模板不能为空', - empty: '暂无可用的邮件模板事件或语言。', - noPreview: '刷新预览后查看渲染后的邮件主题。', - customized: '已自定义' - }, - opsMonitoring: { - title: '运维监控', - description: '启用运维监控模块,用于排障与健康可视化', - disabled: '运维监控已关闭', - enabled: '启用运维监控', - enabledHint: '启用运维监控模块(仅管理员可见)', - realtimeEnabled: '启用实时监控', - realtimeEnabledHint: '启用实时请求速率和指标推送(WebSocket)', - queryMode: '默认查询模式', - queryModeHint: '运维监控默认查询模式(自动/原始/预聚合)', - queryModeAuto: '自动(推荐)', - queryModeRaw: '原始(最准确,但较慢)', - queryModePreagg: '预聚合(最快,需预聚合)', - metricsInterval: '采集频率(秒)', - metricsIntervalHint: '系统/请求指标采集频率(60-3600 秒)' - }, - adminApiKey: { - title: '管理员 API Key', - description: '用于外部系统集成的全局 API Key,拥有完整的管理员权限', - notConfigured: '尚未配置管理员 API Key', - configured: '管理员 API Key 已启用', - currentKey: '当前密钥', - regenerate: '重新生成', - regenerating: '生成中...', - delete: '删除', - deleting: '删除中...', - create: '创建密钥', - creating: '创建中...', - regenerateConfirm: '确定要重新生成吗?当前密钥将立即失效。', - deleteConfirm: '确定要删除管理员 API Key 吗?外部集成将停止工作。', - keyGenerated: '新的管理员 API Key 已生成', - keyDeleted: '管理员 API Key 已删除', - copyKey: '复制密钥', - keyCopied: '密钥已复制到剪贴板', - keyWarning: '此密钥仅显示一次,请立即复制保存。', - securityWarning: '警告:此密钥拥有完整的管理员权限,请妥善保管。', - usage: '使用方法:在请求头中添加 x-api-key: ' - }, - soraS3: { - title: 'Sora 存储配置', - description: '以多配置列表管理 Sora 媒体存储,支持 S3 和 Google Drive', - newProfile: '新建配置', - reloadProfiles: '刷新列表', - empty: '暂无存储配置,请先创建', - createTitle: '新建存储配置', - editTitle: '编辑存储配置', - selectProvider: '选择存储类型', - providerS3Desc: 'S3 兼容对象存储', - providerGDriveDesc: 'Google Drive 云盘', - profileID: '配置 ID', - profileName: '配置名称', - setActive: '创建后设为生效', - saveProfile: '保存配置', - activateProfile: '设为生效', - profileCreated: '存储配置创建成功', - profileSaved: '存储配置保存成功', - profileDeleted: '存储配置删除成功', - profileActivated: '生效配置已切换', - profileIDRequired: '请填写配置 ID', - profileNameRequired: '请填写配置名称', - profileSelectRequired: '请先选择配置', - endpointRequired: '启用时必须填写 S3 端点', - bucketRequired: '启用时必须填写存储桶', - accessKeyRequired: '启用时必须填写 Access Key ID', - deleteConfirm: '确定删除存储配置 {profileID} 吗?', - columns: { - profile: '配置', - profileId: 'Profile ID', - name: '名称', - provider: '存储类型', - active: '生效状态', - endpoint: '端点', - bucket: '存储桶', - storagePath: '存储路径', - capacityUsage: '容量 / 已用', - capacityUnlimited: '无限制', - videoCount: '视频数', - videoCompleted: '完成', - videoInProgress: '进行中', - quota: '默认配额', - updatedAt: '更新时间', - actions: '操作', - rootFolder: '根目录', - testInTable: '测试', - testingInTable: '测试中...', - testTimeout: '测试超时(15秒)' - }, - enabled: '启用存储', - enabledHint: '启用后,Sora 生成的媒体文件将自动上传到存储', - endpoint: 'S3 端点', - region: '区域', - bucket: '存储桶', - prefix: '对象前缀', - accessKeyId: 'Access Key ID', - secretAccessKey: 'Secret Access Key', - secretConfigured: '(已配置,留空保持不变)', - cdnUrl: 'CDN URL', - cdnUrlHint: '可选,配置后使用 CDN URL 访问文件', - forcePathStyle: '强制路径风格(Path Style)', - defaultQuota: '默认存储配额', - defaultQuotaHint: '未在用户或分组级别指定配额时的默认值,0 表示无限制', - testConnection: '测试连接', - testing: '测试中...', - testSuccess: '连接测试成功', - testFailed: '连接测试失败', - saved: '存储设置保存成功', - saveFailed: '保存存储设置失败', - gdrive: { - authType: '认证方式', - serviceAccount: '服务账号', - clientId: 'Client ID', - clientSecret: 'Client Secret', - clientSecretConfigured: '(已配置,留空保持不变)', - refreshToken: 'Refresh Token', - refreshTokenConfigured: '(已配置,留空保持不变)', - serviceAccountJson: '服务账号 JSON', - serviceAccountConfigured: '(已配置,留空保持不变)', - folderId: 'Folder ID(可选)', - authorize: '授权 Google Drive', - authorizeHint: '通过 OAuth2 获取 Refresh Token', - oauthFieldsRequired: '请先填写 Client ID 和 Client Secret', - oauthSuccess: 'Google Drive 授权成功', - oauthFailed: 'Google Drive 授权失败', - closeWindow: '此窗口将自动关闭', - processing: '正在处理授权...', - testStorage: '测试存储', - testSuccess: 'Google Drive 存储测试成功(上传、访问、删除均正常)', - testFailed: 'Google Drive 存储测试失败' - } - }, - overloadCooldown: { - title: '529 过载冷却', - description: '配置上游返回 529(过载)时的账号调度暂停策略', - enabled: '启用过载冷却', - enabledHint: '收到 529 错误时暂停该账号的调度,冷却后自动恢复', - cooldownMinutes: '冷却时长(分钟)', - cooldownMinutesHint: '账号暂停调度的持续时间(1-120 分钟)', - saved: '过载冷却设置保存成功', - saveFailed: '保存过载冷却设置失败' - }, - rateLimit429Cooldown: { - title: '429 默认回避', - description: '配置上游返回 429 且没有明确重置时间时的默认账号回避策略', - enabled: '启用 429 默认回避', - enabledHint: '收到无重置时间的 429 时暂停该账号调度,冷却后自动恢复', - cooldownSeconds: '回避时长(秒)', - cooldownSecondsHint: '默认回避持续时间(1-7200 秒);上游返回明确 reset 时仍优先使用上游时间', - saved: '429 默认回避设置保存成功', - saveFailed: '保存 429 默认回避设置失败' - }, - streamTimeout: { - title: '流超时处理', - description: '配置上游响应超时时的账户处理策略,避免问题账户持续被选中', - enabled: '启用流超时处理', - enabledHint: '当上游响应超时时,自动处理问题账户', - timeoutSeconds: '超时阈值(秒)', - timeoutSecondsHint: '流数据间隔超过此时间视为超时(30-300秒)', - action: '处理方式', - actionTempUnsched: '临时不可调度', - actionError: '标记为错误状态', - actionNone: '不处理', - actionHint: '超时后对账户执行的操作', - tempUnschedMinutes: '暂停时长(分钟)', - tempUnschedMinutesHint: '临时不可调度的持续时间(1-60分钟)', - thresholdCount: '触发阈值(次数)', - thresholdCountHint: '累计超时多少次后触发处理(1-10次)', - thresholdWindowMinutes: '阈值窗口(分钟)', - thresholdWindowMinutesHint: '超时计数的时间窗口(1-60分钟)', - saved: '流超时设置保存成功', - saveFailed: '保存流超时设置失败' - }, - rectifier: { - title: '请求整流器', - description: '当上游返回特定错误时,自动修正请求参数并重试,提高请求成功率', - enabled: '启用请求整流器', - enabledHint: '总开关,关闭后所有整流功能均不生效', - thinkingSignature: 'Thinking 签名整流', - thinkingSignatureHint: '当上游返回 thinking block 签名校验错误时,自动去除签名并重试', - thinkingBudget: 'Thinking Budget 整流', - thinkingBudgetHint: '当上游返回 budget_tokens 约束错误(≥1024)时,自动将 budget 设为 32000 并重试', - apikeySignature: 'API Key 签名整流', - apikeySignatureHint: - '当 API Key 账号的上游返回签名相关错误时,自动去除签名并重试(内置规则始终生效)', - apikeyPatterns: '自定义匹配关键词', - apikeyPatternsHint: - '额外的关键词,匹配响应体中的内容(不区分大小写)。内置规则始终生效,此处用于补充额外匹配。', - apikeyPatternPlaceholder: '例如:thinking_error 或 签名无效', - addPattern: '添加关键词', - saved: '整流器设置保存成功', - saveFailed: '保存整流器设置失败' - }, - betaPolicy: { - title: 'Beta 策略', - description: '配置转发 Anthropic API 请求时如何处理 Beta 特性。仅适用于 /v1/messages 接口。', - action: '处理方式', - actionPass: '透传(不处理)', - actionFilter: '过滤(移除)', - actionBlock: '拦截(拒绝请求)', - scope: '生效范围', - scopeAll: '全部账号', - scopeOAuth: '仅 OAuth 账号', - scopeAPIKey: '仅 API Key 账号', - scopeBedrock: '仅 Bedrock 账号', - errorMessage: '错误消息', - errorMessagePlaceholder: '拦截时返回的自定义错误消息', - errorMessageHint: '留空则使用默认错误消息', - saved: 'Beta 策略设置保存成功', - saveFailed: '保存 Beta 策略设置失败', - modelWhitelist: '模型白名单', - modelWhitelistHint: '留空则对所有模型生效。支持精确匹配和通配符前缀(如 claude-opus-*)', - modelPatternPlaceholder: '例如: claude-opus-* 或 claude-opus-4-6', - addModelPattern: '添加模型规则', - removePattern: '移除', - fallbackAction: '未匹配模型处理方式', - fallbackActionHint: '当请求模型不在白名单中时的处理方式', - fallbackErrorMessagePlaceholder: '未匹配模型被拦截时返回的自定义错误消息', - quickPresets: '快捷预设', - presetOpusOnly: '仅 Opus 允许 1M', - presetOpusOnlyDesc: 'Opus 透传,其他模型过滤', - commonPatterns: '常用模式' - }, - openaiFastPolicy: { - title: 'OpenAI Fast/Flex 策略', - description: '基于请求体 service_tier 字段拦截/过滤/透传 OpenAI fast(priority) 与 flex 请求;仅作用于 OpenAI 网关。', - empty: '尚未配置任何规则。点击下方按钮新增。', - ruleHeader: '规则 #{index}', - removeRule: '删除规则', - addRule: '新增规则', - saveHint: '保存时随系统设置一起提交(点击页面底部「保存」按钮)。', - serviceTier: 'service_tier 匹配', - tierAll: '全部 tier', - tierPriority: 'priority(fast)', - tierFlex: 'flex', - action: '处理方式', - actionPass: '透传(保留 service_tier)', - actionFilter: '过滤(移除 service_tier)', - actionForcePriority: '强制设置 priority(fast)', - actionBlock: '拦截(拒绝请求)', - scope: '生效范围', - scopeAll: '全部账号', - scopeOAuth: '仅 OAuth 账号', - scopeAPIKey: '仅 API Key 账号', - scopeBedrock: '仅 Bedrock 账号', - errorMessage: '错误消息', - errorMessagePlaceholder: '拦截时返回的自定义错误消息', - errorMessageHint: '留空则使用默认错误消息。', - modelWhitelist: '模型白名单', - modelWhitelistHint: '留空表示对所有模型生效;支持精确匹配与通配符(如 gpt-5.5*)。', - modelPatternPlaceholder: '例如: gpt-5.5 或 gpt-5.5*', - addModelPattern: '添加模型规则', - fallbackAction: '未匹配模型处理方式', - fallbackActionHint: '当请求模型不在白名单中时的处理方式。', - fallbackErrorMessagePlaceholder: '未匹配模型被拦截时返回的自定义错误消息' - }, - wechatConnect: { - title: '微信登录', - description: '用于微信开放平台或公众号/小程序的第三方登录配置。', - enabledLabel: '启用微信登录', - enabledHint: '开启后可使用微信第三方登录回调与授权配置。', - appIdLabel: 'AppID', - appIdPlaceholder: '微信开放平台 AppID', - appSecretLabel: 'AppSecret', - appSecretConfiguredPlaceholder: '密钥已配置,留空以保留当前值。', - appSecretPlaceholder: '微信开放平台 AppSecret', - appSecretConfiguredHint: '密钥已配置,留空以保留当前值。', - appSecretHint: '填写后会覆盖当前微信密钥。', - modeLabel: '模式', - openModeLabel: '非微信环境使用开放平台', - openModeHint: '浏览器不在微信内时,自动走开放平台扫码授权。', - mpModeLabel: '微信环境使用公众号', - mpModeHint: '浏览器在微信内时,自动走公众号授权。', - redirectUrlLabel: '回调地址', - redirectUrlPlaceholder: 'https://your-site.com/api/v1/auth/oauth/wechat/callback', - generateAndCopy: '使用当前站点生成并复制', - redirectUrlSetAndCopied: '已使用当前站点生成回调地址并复制到剪贴板', - frontendRedirectUrlLabel: '前端回调地址', - frontendRedirectUrlPlaceholder: '/auth/wechat/callback', - frontendRedirectUrlHint: '通常用于前端路由回调地址,需与后端配置保持一致。' - }, - authSourceDefaults: { - title: '认证来源默认值', - description: '按注册来源配置新用户默认余额、并发、订阅与授权策略。', - requireEmailLabel: '第三方注册强制补充邮箱', - requireEmailHint: '启用后,Linux DO、OIDC、微信注册缺少邮箱时必须先补充邮箱地址。', - enabledHint: '以下默认值会在该来源注册新用户时发放;首次绑定时授权仅作用于已有账号绑定该来源。', - sources: { - email: { - title: '邮箱注册', - description: '适用于邮箱密码注册的新用户默认配额。' - }, - linuxdo: { - title: 'Linux DO 登录', - description: '适用于 Linux DO 第三方注册的新用户默认配额。' - }, - oidc: { - title: 'OIDC 登录', - description: '适用于 OIDC 第三方注册的新用户默认配额。' - }, - wechat: { - title: '微信登录', - description: '适用于微信第三方注册的新用户默认配额。' - } - }, - grantOnFirstBindLabel: '首次绑定时授权', - grantOnFirstBindHint: '已有账号首次绑定该来源时发放默认权益。', - defaultSubscriptionsLabel: '默认订阅', - defaultSubscriptionsHint: '仅对当前认证来源生效,未配置时不追加来源专属订阅。', - noSourceSubscriptions: '当前来源未配置专属默认订阅。', - platformQuotasOverride: '平台限额覆盖', - platformQuotasOverrideHint: '留空的字段继承「系统默认平台限额」;填 0 表示禁止该窗口使用。', - }, - paymentVisibleMethods: { - methodLabel: '{title} 可见方式', - methodHint: '控制前台结算页是否展示该方式,以及展示时使用的来源键。', - sourceLabel: '支付来源', - sourceHint: '启用后必须明确选择一个来源;未配置状态不会对外展示该支付方式。', - sourceRequiredError: '{title} 已启用,请先选择支付来源。' - }, - openaiExperimentalScheduler: { - title: 'OpenAI 实验调度策略', - description: '默认关闭。开启后仅影响本网关在 OpenAI 账号间的实验性调度选择逻辑,不代表上游 OpenAI 官方能力。', - stickyWeightedTitle: '粘性加权', - stickyWeightedDescription: '开启后 previous_response_id 和 session_hash 粘性进入高级调度打分;关闭时仍按旧逻辑硬命中粘性账号。', - subscriptionPriorityTitle: '订阅优先', - subscriptionPriorityDescription: '开启后先在 ChatGPT 订阅账号池中按权值选取;订阅池拿不到席位时再回退到非订阅账号池。', - weightsTitle: '调度权值覆盖', - weightsDescription: '留空时使用配置/环境变量值;配置未设置时使用内置默认值。页面非空设置优先。', - defaultPlaceholder: '配置/默认:{value}', - topKLabel: 'TopK', - priorityWeight: '优先级', - loadWeight: '负载', - queueWeight: '排队', - errorRateWeight: '错误率', - ttftWeight: '首包延迟', - resetWeight: '重置窗口', - quotaHeadroomWeight: '额度余量', - previousResponseWeight: 'previous_response 粘性', - sessionStickyWeight: 'session_hash 粘性' - }, - usageRecords: { - title: '使用记录', - description: '与终端用户可见的用量及失败请求记录相关的设置。', - }, - user_error_view: { - label: '允许用户查看自己的错误请求', - description: '开启后,用户可在用量页查看自己失败请求的精简信息(不含内部/上游错误细节)。需运维监控开启才有数据。', - }, - saveSettings: '保存设置', - saving: '保存中...', - settingsSaved: '设置保存成功', - smtpConnectionSuccess: 'SMTP 连接成功', - testEmailSent: '测试邮件发送成功', - failedToLoad: '加载设置失败', - failedToSave: '保存设置失败', - failedToTestSmtp: 'SMTP 连接测试失败', - failedToSendTestEmail: '发送测试邮件失败' - }, - - // Error Passthrough Rules - errorPassthrough: { - title: '错误透传规则', - description: '配置上游错误如何返回给客户端', - createRule: '创建规则', - editRule: '编辑规则', - deleteRule: '删除规则', - noRules: '暂无规则', - createFirstRule: '创建第一条错误透传规则', - allPlatforms: '所有平台', - passthrough: '透传', - custom: '自定义', - code: '状态码', - body: '消息体', - skipMonitoring: '跳过监控', - - // Columns - columns: { - priority: '优先级', - name: '名称', - conditions: '匹配条件', - platforms: '平台', - behavior: '响应行为', - status: '状态', - actions: '操作' - }, - - // Match Mode - matchMode: { - any: '错误码 或 关键词', - all: '错误码 且 关键词', - anyHint: '状态码匹配任一错误码,或消息包含任一关键词', - allHint: '状态码匹配任一错误码,且消息包含任一关键词' - }, - - // Form - form: { - name: '规则名称', - namePlaceholder: '例如:上下文超限透传', - priority: '优先级', - priorityHint: '数值越小优先级越高,优先匹配', - description: '规则描述', - descriptionPlaceholder: '描述此规则的用途...', - matchConditions: '匹配条件', - errorCodes: '错误码', - errorCodesPlaceholder: '422, 400, 429', - errorCodesHint: '多个错误码用逗号分隔', - keywords: '关键词', - keywordsPlaceholder: '每行一个关键词\ncontext limit\nmodel not supported', - keywordsHint: '每行一个关键词,不区分大小写', - matchMode: '匹配模式', - platforms: '适用平台', - platformsHint: '不选择表示适用于所有平台', - responseBehavior: '响应行为', - passthroughCode: '透传上游状态码', - responseCode: '自定义状态码', - passthroughBody: '透传上游错误信息', - customMessage: '自定义错误信息', - customMessagePlaceholder: '返回给客户端的错误信息...', - skipMonitoring: '跳过运维监控记录', - skipMonitoringHint: '开启后,匹配此规则的错误不会被记录到运维监控中', - enabled: '启用此规则' - }, - - // Messages - nameRequired: '请输入规则名称', - conditionsRequired: '请至少配置一个错误码或关键词', - ruleCreated: '规则创建成功', - ruleUpdated: '规则更新成功', - ruleDeleted: '规则删除成功', - deleteConfirm: '确定要删除规则 "{name}" 吗?', - failedToLoad: '加载规则失败', - failedToSave: '保存规则失败', - failedToDelete: '删除规则失败', - failedToToggle: '切换状态失败' - }, - - // TLS 指纹模板 - tlsFingerprintProfiles: { - title: 'TLS 指纹模板', - description: '管理 TLS 指纹模板,用于模拟特定客户端的 TLS 握手特征', - createProfile: '创建模板', - editProfile: '编辑模板', - deleteProfile: '删除模板', - noProfiles: '暂无模板', - createFirstProfile: '创建你的第一个 TLS 指纹模板', - - columns: { - name: '名称', - description: '描述', - grease: 'GREASE', - alpn: 'ALPN', - actions: '操作' - }, - - form: { - pasteYaml: '粘贴 YAML 配置', - pasteYamlPlaceholder: '将 TLS 指纹采集器复制的 YAML 粘贴到这里...', - pasteYamlHint: '粘贴从 TLS 指纹采集器复制的 YAML 配置,自动填充所有字段。', - openCollector: '打开采集器', - parseYaml: '解析 YAML', - yamlParsed: 'YAML 解析成功,字段已自动填充', - yamlParseFailed: 'YAML 解析失败:未找到 name 字段', - name: '模板名称', - namePlaceholder: '例如 macOS Node.js v24', - description: '描述', - descriptionPlaceholder: '可选的模板描述', - enableGrease: '启用 GREASE', - enableGreaseHint: '在 TLS ClientHello 扩展中插入 GREASE 值', - cipherSuites: '密码套件', - cipherSuitesHint: '逗号分隔的十六进制值,例如 0x1301, 0x1302, 0xc02c', - curves: '椭圆曲线', - curvesHint: '逗号分隔的曲线 ID', - pointFormats: '点格式', - signatureAlgorithms: '签名算法', - alpnProtocols: 'ALPN 协议', - alpnProtocolsHint: '逗号分隔,例如 h2, http/1.1', - supportedVersions: '支持的 TLS 版本', - keyShareGroups: '密钥共享组', - pskModes: 'PSK 模式', - extensions: '扩展' - }, - - deleteConfirm: '删除模板', - deleteConfirmMessage: '确定要删除模板 "{name}" 吗?使用此模板的账号将回退到内置默认值。', - createSuccess: '模板创建成功', - updateSuccess: '模板更新成功', - deleteSuccess: '模板删除成功', - loadFailed: '加载模板失败', - saveFailed: '保存模板失败', - deleteFailed: '删除模板失败' - } - }, - - // Subscription Progress (Header component) - subscriptionProgress: { - title: '我的订阅', - viewDetails: '查看订阅详情', - activeCount: '{count} 个有效订阅', - daily: '每日', - weekly: '每周', - monthly: '每月', - daysRemaining: '剩余 {days} 天', - expired: '已过期', - expiresToday: '今天到期', - expiresTomorrow: '明天到期', - viewAll: '查看全部订阅', - noSubscriptions: '暂无有效订阅', - unlimited: '无限制' - }, - - // Version Badge - version: { - currentVersion: '当前版本', - latestVersion: '最新版本', - upToDate: '已是最新版本', - updateAvailable: '有新版本可用!', - releaseNotes: '更新日志', - noReleaseNotes: '暂无更新日志', - viewUpdate: '查看更新', - viewRelease: '查看发布', - viewChangelog: '查看更新日志', - refresh: '刷新', - sourceMode: '源码构建', - sourceModeHint: '源码构建请使用 git pull 更新', - updateNow: '立即更新', - updating: '正在更新...', - updateComplete: '更新完成', - updateFailed: '更新失败', - restartRequired: '请重启服务以应用更新', - restartNow: '立即重启', - restarting: '正在重启...', - retry: '重试' - }, - - // Recharge / Subscription Page - purchase: { - title: '充值/订阅', - description: '通过内嵌页面完成充值/订阅', - openInNewTab: '新窗口打开', - notEnabledTitle: '该功能未开启', - notEnabledDesc: '管理员暂未开启充值/订阅入口,请联系管理员。', - notConfiguredTitle: '充值/订阅链接未配置', - notConfiguredDesc: '管理员已开启入口,但尚未配置充值/订阅链接,请联系管理员。' - }, - - // Custom Page (iframe embed) - customPage: { - title: '自定义页面', - openInNewTab: '新窗口打开', - notFoundTitle: '页面不存在', - notFoundDesc: '该自定义页面不存在或已被删除。', - notConfiguredTitle: '页面链接未配置', - notConfiguredDesc: '该自定义页面的 URL 未正确配置。', - tableOfContents: '目录', - copyCode: '复制', - copiedCode: '已复制', - copyCodeFailed: '失败' - }, - - // Announcements Page - announcements: { - title: '公告', - description: '查看系统公告', - unreadOnly: '仅显示未读', - markRead: '标记已读', - markAllRead: '全部已读', - viewAll: '查看全部公告', - markedAsRead: '已标记为已读', - allMarkedAsRead: '所有公告已标记为已读', - newCount: '有 {count} 条新公告', - readAt: '已读时间', - read: '已读', - unread: '未读', - startsAt: '开始时间', - endsAt: '结束时间', - empty: '暂无公告', - emptyUnread: '暂无未读公告', - total: '条公告', - emptyDescription: '暂时没有任何系统公告', - readStatus: '您已阅读此公告', - markReadHint: '点击"已读"标记此公告' - }, - - // User Subscriptions Page - userSubscriptions: { - title: '我的订阅', - description: '查看您的订阅计划和用量', - noActiveSubscriptions: '暂无有效订阅', - noActiveSubscriptionsDesc: '您没有任何有效订阅。请联系管理员获取订阅。', - failedToLoad: '加载订阅失败', - status: { - active: '有效', - expired: '已过期', - revoked: '已撤销' - }, - usage: '用量', - expires: '到期时间', - noExpiration: '无到期时间', - unlimited: '无限制', - unlimitedDesc: '该订阅无用量限制', - daily: '每日', - weekly: '每周', - monthly: '每月', - daysRemaining: '剩余 {days} 天', - expiresOn: '{date} 到期', - resetIn: '{time} 后重置', - quotaEndsIn: '额度将在 {time} 后结束', - windowNotActive: '等待首次使用', - usageOf: '已用 {used} / {limit}' - }, - - // Onboarding Tour - onboarding: { - restartTour: '重新查看新手引导', - dontShowAgain: '不再提示', - dontShowAgainTitle: '永久关闭新手引导', - confirmDontShow: '确定不再显示新手引导吗?\n\n您可以随时在右上角头像菜单中重新开启。', - confirmExit: '确定要退出新手引导吗?您可以随时在右上角菜单重新开始。', - interactiveHint: '按 Enter 或点击继续', - navigation: { - flipPage: '翻页', - exit: '退出' - }, - // Admin tour steps - admin: { - welcome: { - title: '👋 欢迎使用 Sub2API', - description: - '

Sub2API 是一个强大的 AI 服务中转平台,让您轻松管理和分发 AI 服务。

🎯 核心功能:

  • 📦 分组管理 - 创建不同的服务套餐(VIP、免费试用等)
  • 🔗 账号池 - 连接多个上游 AI 服务商账号
  • 🔑 密钥分发 - 为用户生成独立的 API Key
  • 💰 计费管理 - 灵活的费率和配额控制

接下来,我们将用 3 分钟带您完成首次配置 →

', - nextBtn: '开始配置 🚀', - prevBtn: '跳过' - }, - groupManage: { - title: '📦 第一步:分组管理', - description: - '

什么是分组?

分组是 Sub2API 的核心概念,它就像一个"服务套餐":

  • 🎯 每个分组可以包含多个上游账号
  • 💰 每个分组有独立的计费倍率
  • 👥 可以设置为公开或专属分组

💡 示例:您可以创建"VIP专线"(高倍率)和"免费试用"(低倍率)两个分组

👉 点击左侧的"分组管理"开始

' - }, - createGroup: { - title: '➕ 创建新分组', - description: - '

现在让我们创建第一个分组。

📝 提示:建议先创建一个测试分组,熟悉流程后再创建正式分组

👉 点击"创建分组"按钮

' - }, - groupName: { - title: '✏️ 1. 分组名称', - description: - '

为您的分组起一个易于识别的名称。

💡 命名建议:
  • "测试分组" - 用于测试
  • "VIP专线" - 高质量服务
  • "免费试用" - 体验版

填写完成后点击"下一步"继续

', - nextBtn: '下一步' - }, - groupPlatform: { - title: '🤖 2. 选择平台', - description: - '

选择该分组支持的 AI 平台。

📌 平台说明:
  • Anthropic - Claude 系列模型
  • OpenAI - GPT 系列模型
  • Google - Gemini 系列模型

一个分组只能选择一个平台

', - nextBtn: '下一步' - }, - groupMultiplier: { - title: '💰 3. 费率倍数', - description: - '

设置该分组的计费倍率,控制用户的实际扣费。

⚙️ 计费规则:
  • 1.0 - 原价计费(成本价)
  • 1.5 - 用户消耗 $1,扣除 $1.5
  • 2.0 - 用户消耗 $1,扣除 $2
  • 0.8 - 补贴模式(亏本运营)

建议测试分组设置为 1.0

', - nextBtn: '下一步' - }, - groupExclusive: { - title: '🔒 4. 专属分组(可选)', - description: - '

控制分组的可见性和访问权限。

🔐 权限说明:
  • 关闭 - 公开分组,所有用户可见
  • 开启 - 专属分组,仅指定用户可见

💡 使用场景:VIP 用户专属、内部测试、特殊客户等

', - nextBtn: '下一步' - }, - groupSubmit: { - title: '✅ 保存分组', - description: - '

确认信息无误后,点击创建按钮保存分组。

⚠️ 注意:分组创建后,平台类型不可修改,其他信息可以随时编辑

📌 下一步:创建成功后,我们将添加上游账号到这个分组

👉 点击"创建"按钮

' - }, - accountManage: { - title: '🔗 第二步:添加账号', - description: - '

太棒了!分组已创建成功 🎉

现在需要添加上游 AI 服务商的账号,让分组能够实际提供服务。

🔑 账号的作用:
  • 连接到上游 AI 服务(Claude、GPT 等)
  • 一个分组可以包含多个账号(负载均衡)
  • 支持 OAuth 和 Session Key 两种方式

👉 点击左侧的"账号管理"

' - }, - createAccount: { - title: '➕ 添加新账号', - description: - '

点击按钮开始添加您的第一个上游账号。

💡 提示:建议使用 OAuth 方式,更安全且无需手动提取密钥

👉 点击"添加账号"按钮

' - }, - accountName: { - title: '✏️ 1. 账号名称', - description: - '

为账号设置一个便于识别的名称。

💡 命名建议:"Claude主账号"、"GPT备用1"、"测试账号" 等

', - nextBtn: '下一步' - }, - accountPlatform: { - title: '🤖 2. 选择平台', - description: - '

选择该账号对应的服务商平台。

⚠️ 重要:平台必须与刚才创建的分组平台一致

', - nextBtn: '下一步' - }, - accountType: { - title: '🔐 3. 授权方式', - description: - '

选择账号的授权方式。

✅ 推荐:OAuth 方式
  • 无需手动提取密钥
  • 更安全,支持自动刷新
  • 适用于 Claude Code、ChatGPT OAuth
📌 Session Key 方式
  • 需要手动从浏览器提取
  • 可能需要定期更新
  • 适用于不支持 OAuth 的平台
', - nextBtn: '下一步' - }, - accountPriority: { - title: '⚖️ 4. 优先级(可选)', - description: - '

设置账号的调用优先级。

📊 优先级规则:
  • 数字越小,优先级越高
  • 系统优先使用低数值账号
  • 相同优先级则随机选择

💡 使用场景:主账号设置低数值,备用账号设置高数值

', - nextBtn: '下一步' - }, - accountGroups: { - title: '🎯 5. 分配分组', - description: - '

关键步骤!将账号分配到刚才创建的分组。

⚠️ 重要提醒:
  • 必须勾选至少一个分组
  • 未分配分组的账号无法使用
  • 一个账号可以分配给多个分组

💡 提示:请勾选刚才创建的测试分组

', - nextBtn: '下一步' - }, - accountSubmit: { - title: '✅ 保存账号', - description: - '

确认信息无误后,点击保存按钮。

📌 OAuth 授权流程:
  • 点击保存后会跳转到服务商页面
  • 在服务商页面完成登录授权
  • 授权成功后自动返回

📌 下一步:账号添加成功后,我们将创建 API 密钥

👉 点击"保存"按钮

' - }, - keyManage: { - title: '🔑 第三步:生成密钥', - description: - '

恭喜!账号配置完成 🎉

最后一步,生成 API Key 来测试服务是否正常工作。

🔑 API Key 的作用:
  • 用于调用 AI 服务的凭证
  • 每个 Key 绑定一个分组
  • 可以设置配额和有效期
  • 支持独立的使用统计

👉 点击左侧的"API 密钥"

' - }, - createKey: { - title: '➕ 创建密钥', - description: - '

点击按钮创建您的第一个 API Key。

💡 提示:创建后请立即复制保存,密钥只显示一次

👉 点击"创建密钥"按钮

' - }, - keyName: { - title: '✏️ 1. 密钥名称', - description: - '

为密钥设置一个便于管理的名称。

💡 命名建议:"测试密钥"、"生产环境"、"移动端" 等

', - nextBtn: '下一步' - }, - keyGroup: { - title: '🎯 2. 选择分组', - description: - '

选择刚才配置好的分组。

📌 分组决定:
  • 该密钥可以使用哪些账号
  • 计费倍率是多少
  • 是否为专属密钥

💡 提示:选择刚才创建的测试分组

', - nextBtn: '下一步' - }, - keySubmit: { - title: '🎉 生成并复制', - description: - '

点击创建后,系统会生成完整的 API Key。

⚠️ 重要提醒:
  • 密钥只显示一次,请立即复制
  • 丢失后需要重新生成
  • 妥善保管,不要泄露给他人
🚀 下一步:
  • 复制生成的 sk-xxx 密钥
  • 在支持 OpenAI 接口的客户端中使用
  • 开始体验 AI 服务!

👉 点击"创建"按钮

' - } - }, - // User tour steps - user: { - welcome: { - title: '👋 欢迎使用 Sub2API', - description: - '

您好!欢迎来到 Sub2API AI 服务平台。

🎯 快速开始:

  • 🔑 创建 API 密钥
  • 📋 复制密钥到您的应用
  • 🚀 开始使用 AI 服务

只需 1 分钟,让我们开始吧 →

', - nextBtn: '开始 🚀', - prevBtn: '跳过' - }, - keyManage: { - title: '🔑 API 密钥管理', - description: - '

在这里管理您的所有 API 访问密钥。

📌 什么是 API 密钥?
API 密钥是您访问 AI 服务的凭证,就像一把钥匙,让您的应用能够调用 AI 能力。

👉 点击进入密钥页面

' - }, - createKey: { - title: '➕ 创建新密钥', - description: - '

点击按钮创建您的第一个 API 密钥。

💡 提示:创建后密钥只显示一次,请务必复制保存

👉 点击"创建密钥"

' - }, - keyName: { - title: '✏️ 密钥名称', - description: - '

为密钥起一个便于识别的名称。

💡 示例:"我的第一个密钥"、"测试用" 等

', - nextBtn: '下一步' - }, - keyGroup: { - title: '🎯 选择分组', - description: - '

选择管理员为您分配的服务分组。

📌 分组说明:
不同分组可能有不同的服务质量和计费标准,请根据需要选择。

', - nextBtn: '下一步' - }, - keySubmit: { - title: '🎉 完成创建', - description: - '

点击确认创建您的 API 密钥。

⚠️ 重要:
  • 创建后请立即复制密钥(sk-xxx)
  • 密钥只显示一次,丢失需重新生成

🚀 如何使用:
将密钥配置到支持 OpenAI 接口的任何客户端(如 ChatBox、OpenCat 等),即可开始使用!

👉 点击"创建"按钮

' - } - } - }, - - // Payment System - payment: { - title: '充值/订阅', - amountLabel: '充值金额', - paymentAmount: '支付金额', - creditedBalance: '到账余额', - quickAmounts: '快捷金额', - customAmount: '自定义金额', - enterAmount: '输入金额', - paymentMethod: '支付方式', - fee: '手续费', - actualPay: '实付金额', - createOrder: '确认支付', - methods: { - easypay: '易支付', - alipay: '支付宝', - wxpay: '微信支付', - stripe: 'Stripe', - airwallex: 'Airwallex', - card: '银行卡', - link: 'Link', - alipay_direct: '支付宝(直连)', - wxpay_direct: '微信支付(直连)', - }, - status: { - pending: '待支付', - paid: '已支付', - recharging: '充值中', - completed: '已完成', - expired: '已过期', - cancelled: '已取消', - failed: '失败', - refund_requested: '退款申请中', - refunding: '退款中', - refund_pending: '退款处理中', - refunded: '已退款', - partially_refunded: '部分退款', - refund_failed: '退款失败', - }, - qr: { - scanToPay: '请扫码支付', - scanAlipay: '支付宝扫码支付', - scanWxpay: '微信扫码支付', - scanAlipayHint: '请使用手机打开支付宝,扫描二维码完成支付', - scanWxpayHint: '请使用手机打开微信,扫描二维码完成支付', - payInNewWindow: '请在新窗口中完成支付', - payInNewWindowHint: '支付页面已在新窗口打开,请在新窗口中完成支付后返回此页面', - openPayWindow: '重新打开支付页面', - expiresIn: '剩余支付时间', - expired: '订单已过期', - expiredDesc: '订单已超时,请重新创建订单', - cancelled: '订单已取消', - cancelledDesc: '您已取消本次支付', - waitingPayment: '等待支付...', - cancelOrder: '取消订单', - }, - orders: { - title: '我的订单', - empty: '暂无订单', - orderId: '订单 ID', - orderNo: '订单编号', - amount: '金额', - payAmount: '实付', - creditedAmount: '到账金额', - fee: '手续费', - baseAmount: '充值金额', - includedInPayAmount: '已含在实付金额中', - status: '状态', - paymentMethod: '支付方式', - createdAt: '创建时间', - cancel: '取消订单', - userId: '用户 ID', - orderType: '订单类型', - actions: '操作', - requestRefund: '申请退款', - }, - result: { - success: '支付成功', - subscriptionSuccess: '订阅成功', - processing: '支付处理中', - processingHint: '支付结果仍在确认中,页面会自动刷新。', - failed: '支付失败', - backToRecharge: '返回充值', - viewOrders: '查看订单', - }, - currentBalance: '当前余额', - groupFallback: '分组 #{id}', - rechargeAccount: '充值账户', - activeSubscription: '当前订阅', - noActiveSubscription: '暂无有效订阅', - tabTopUp: '充值', - tabSubscribe: '订阅', - noPlans: '暂无可用订阅套餐', - notAvailable: '充值功能暂未开放', - confirmSubscription: '确认订阅', - confirmCancel: '确定要取消此订单吗?', - amountTooLow: '最低金额为 {min}', - amountTooHigh: '最高金额为 {max}', - amountNoMethod: '该金额没有可用的支付方式', - rechargeRatePreview: '当前倍率:1 CNY = {usd} USD', - refundReason: '退款原因', - refundReasonPlaceholder: '请描述您的退款原因', - stripeLoadFailed: '支付组件加载失败,请刷新页面重试', - stripeMissingParams: '缺少订单ID或支付密钥', - stripeNotConfigured: 'Stripe 未配置', - airwallexLoadFailed: 'Airwallex 支付组件加载失败,请刷新页面重试', - airwallexMissingParams: '缺少 Airwallex 支付参数', - errors: { - tooManyPending: '待支付订单过多(最多 {max} 个),请先完成或取消现有订单', - cancelRateLimited: '取消订单过于频繁,请稍后再试', - wechatH5NotAuthorized: '当前商户未开通微信 H5 支付,请在微信中打开当前页面继续支付。', - wechatPaymentMpNotConfigured: '当前站点未完成公众号/JSAPI 支付配置,暂时无法在微信内直接拉起支付。', - wechatJsapiUnavailable: '当前环境未能拉起微信支付,请确认正在微信内打开本页后重试。', - wechatJsapiFailed: '微信支付未完成,请重新拉起支付或改用扫码支付。', - wechatUnavailable: '当前微信支付暂不可用,请稍后重试。', - wechatOpenInWeChatHint: '请复制当前页面链接到微信内打开,或直接改用电脑端微信扫码支付。', - wechatScanOnDesktopHint: '电脑端请直接使用微信扫一扫完成支付;移动端请在微信内打开当前页面。', - wechatSwitchBrowserHint: '请改用电脑端微信扫码,或在外部浏览器重新打开本页后再试。', - mobilePaymentFallbackToQr: '当前商户未开通移动支付,已自动切换为扫码支付。', - alipayDesktopUnavailable: '当前支付宝桌面支付未成功生成二维码。', - alipayDesktopQrHint: '电脑端支付宝应展示扫码单,请刷新后重试,或确认浏览器未拦截当前支付页。', - alipayMobileUnavailable: '当前页面未成功跳转到支付宝。', - alipayMobileOpenHint: '请允许当前页面打开支付宝 App,或改用系统浏览器重新发起支付。', - // Structured error codes (reason strings from backend ApplicationError) - PAYMENT_DISABLED: '支付系统已关闭', - USER_INACTIVE: '账号已被禁用', - BALANCE_PAYMENT_DISABLED: '余额充值功能已关闭', - INVALID_AMOUNT: '金额无效', - INVALID_INPUT: '参数有误', - PLAN_NOT_AVAILABLE: '套餐不存在或已下架', - GROUP_NOT_FOUND: '订阅分组不可用', - GROUP_TYPE_MISMATCH: '分组类型不是订阅类型', - TOO_MANY_PENDING: '待支付订单过多(最多 {max} 个),请先完成或取消现有订单', - DAILY_LIMIT_EXCEEDED: '今日充值已达上限,剩余额度 {remaining}', - PAYMENT_GATEWAY_ERROR: '支付方式不可用', - NO_AVAILABLE_INSTANCE: '暂无可用的支付通道', - PAYMENT_PROVIDER_MISCONFIGURED: '支付通道配置错误,请联系管理员', - WXPAY_CONFIG_MISSING_KEY: '微信支付配置缺少必填项:{key}', - WXPAY_CONFIG_INVALID_KEY_LENGTH: '微信支付 {key} 长度错误,应为 {expected} 字节(实际 {actual})', - WXPAY_CONFIG_INVALID_KEY: '微信支付 {key} 格式错误,请确认复制了完整的 PEM 内容', - PENDING_ORDERS: '该服务商有未完成的订单,请等待订单完成后再操作', - PAYMENT_PROVIDER_CONFLICT: '该支付方式已有其他启用中的服务商实例,请先停用后再继续。', - CANCEL_RATE_LIMITED: '取消订单过于频繁,请稍后再试', - NOT_FOUND: '订单不存在', - FORBIDDEN: '无权限操作此订单', - CONFLICT: '订单状态已变更,请刷新', - INVALID_ORDER_TYPE: '仅余额订单可申请退款', - INVALID_STATUS: '当前订单状态不允许此操作', - BALANCE_NOT_ENOUGH: '退款金额超过余额', - REFUND_AMOUNT_EXCEEDED: '退款金额超过充值金额', - REFUND_FAILED: '退款失败', - }, - airwallexPay: 'Airwallex 支付', - stripePay: '立即支付', - stripeSuccessProcessing: '支付成功,正在处理订单...', - stripePopup: { - redirecting: '正在跳转到支付页面...', - loadingQr: '正在获取微信支付二维码...', - timeout: '等待支付凭证超时,请重试', - qrFailed: '未能获取微信支付二维码', - }, - subscribeNow: '立即开通', - renewNow: '续费', - selectPlan: '选择套餐', - planFeatures: '功能特性', - planCard: { - rate: '倍率', - peakRate: '高峰倍率', - dailyLimit: '日限额', - weeklyLimit: '周限额', - monthlyLimit: '月限额', - quota: '配额', - unlimited: '无限制', - models: '模型', - }, - days: '天', - months: '个月', - years: '年', - oneMonth: '1 个月', - oneYear: '1 年', - perMonth: '月', - perYear: '年', - admin: { - tabs: { - overview: '概览', - orders: '订单管理', - channels: '支付渠道', - plans: '订阅套餐', - }, - todayRevenue: '今日收入', - totalRevenue: '总收入', - todayOrders: '今日订单', - orderCount: '订单数', - avgAmount: '平均金额', - revenue: '收入', - dailyRevenue: '每日收入', - paymentDistribution: '支付方式分布', - colUser: '用户', - topUsers: '消费排行', - noData: '暂无数据', - days: '天', - weeks: '周', - months: '月', - searchOrders: '搜索订单...', - allStatuses: '全部状态', - allPaymentTypes: '全部支付方式', - allOrderTypes: '全部订单类型', - orderDetail: '订单详情', - orderType: '订单类型', - orders: '订单', - balanceOrder: '余额充值', - subscriptionOrder: '订阅', - paidAt: '支付时间', - completedAt: '完成时间', - expiresAt: '过期时间', - feeRate: '手续费率', - refund: '退款', - refundOrder: '退款订单', - refundAmount: '退款金额', - maxRefundable: '最大可退金额', - refundReason: '退款原因', - refundReasonPlaceholder: '请输入退款原因', - confirmRefund: '确认退款', - refundSuccess: '退款成功', - refundPending: '退款处理中,待网关确认', - queryRefundStatus: '查询退款状态', - refundInfo: '退款信息', - refundEnabled: '允许退款', - alreadyRefunded: '已退款', - deductBalance: '扣除余额', - deductBalanceHint: '从用户余额中扣回充值金额', - userBalance: '用户余额', - orderAmount: '订单金额', - insufficientBalance: '余额不足,将扣至 $0', - noDeduction: '将不扣除用户余额', - forceRefund: '强制退款(忽略余额检查)', - orderCancelled: '订单已取消', - retry: '重试', - retrySuccess: '重试成功', - approveRefund: '批准退款', - retryRefund: '重试退款', - refundRequestInfo: '退款申请信息', - refundRequestedAt: '申请时间', - refundRequestedBy: '申请人', - refundRequestReason: '申请原因', - auditLogs: '操作日志', - operator: '操作人', - channelName: '渠道名称', - channelDescription: '渠道描述', - createChannel: '创建渠道', - editChannel: '编辑渠道', - deleteChannel: '删除渠道', - deleteChannelConfirm: '确定要删除此渠道吗?', - planName: '套餐名称', - planDescription: '套餐描述', - createPlan: '创建套餐', - editPlan: '编辑套餐', - deletePlan: '删除套餐', - deletePlanConfirm: '确定要删除此套餐吗?', - originalPrice: '原价', - price: '价格', - subscriptionCnyPayPreview: 'CNY 通道实扣预览:{amount}', - subscriptionCnyPayPreviewWithFee: '(含 {feeRate}% 手续费:{total})', - validityDays: '有效期(天)', - validityUnit: '有效期单位', - sortOrder: '排序', - forSale: '上架状态', - onSale: '上架', - offSale: '下架', - group: '分组', - groupId: '分组 ID', - features: '功能特性', - featuresHint: '每行一个特性', - featuresPlaceholder: '输入套餐特性...', - providerManagement: '服务商管理', - providerManagementDesc: '管理支付服务商实例', - createProvider: '创建服务商', - editProvider: '编辑服务商', - deleteProvider: '删除服务商', - deleteProviderConfirm: '确定要删除此服务商吗?', - providerName: '服务商名称', - providerKey: '服务商标识', - selectProviderKey: '选择服务商标识', - providerConfig: '服务商配置', - noProviders: '暂无服务商', - noProvidersHint: '创建一个服务商实例以开始接受支付', - supportedTypes: '支持的支付方式', - supportedTypesHint: '选择此服务商支持的支付方式', - rateMultiplier: '费率倍数', - dashboardTitle: '支付概览', - dashboardDesc: '充值订单统计与分析', - daySuffix: '天', - paymentConfigTitle: '支付配置', - paymentConfigDesc: '管理支付服务商与相关设置', - plansPageTitle: '订阅套餐管理', - plansPageDesc: '管理订阅套餐配置', - tabPlanConfig: '套餐配置', - tabUserSubs: '用户订阅', - selectGroup: '请选择分组', - groupRequired: '请选择订阅分组', - priceRequired: '价格必须大于 0', - validityDaysRequired: '有效期天数必须大于 0', - groupMissing: '缺失', - groupInfo: '分组信息', - platform: '平台', - rateMultiplierLabel: '倍率', - dailyLimit: '日限额', - weeklyLimit: '周限额', - monthlyLimit: '月限额', - unlimited: '无限制', - searchUserSubs: '搜索用户订阅...', - daily: '日', - weekly: '周', - monthly: '月', - subsStatus: { - active: '生效中', - expired: '已过期', - revoked: '已撤销', - }, - }, - }, - -} diff --git a/frontend/src/i18n/locales/zh/admin/accounts.ts b/frontend/src/i18n/locales/zh/admin/accounts.ts new file mode 100644 index 0000000000..c664386d4d --- /dev/null +++ b/frontend/src/i18n/locales/zh/admin/accounts.ts @@ -0,0 +1,1257 @@ +export default { + accounts: { + title: '账号管理', + description: '管理 AI 平台账号和 Cookie', + createAccount: '添加账号', + autoRefresh: '自动刷新', + enableAutoRefresh: '启用自动刷新', + refreshInterval5s: '5 秒', + refreshInterval10s: '10 秒', + refreshInterval15s: '15 秒', + refreshInterval30s: '30 秒', + autoRefreshCountdown: '自动刷新:{seconds}s', + listPendingSyncHint: '列表存在待同步变更,点击同步可补齐最新数据。', + listPendingSyncAction: '立即同步', + syncFromCrs: '从 CRS 同步', + dataExport: '导出', + dataExportSelected: '导出选中', + dataExportIncludeProxies: '导出代理(导出账号关联的代理)', + dataImport: '导入', + moreActions: '更多操作', + dataActions: '数据操作', + toolActions: '工具', + viewColumns: '列显示', + selectedCount: '已选 {count}', + dataExportConfirmMessage: '导出的数据包含账号与代理的敏感信息,请妥善保存。', + dataExportConfirm: '确认导出', + dataExported: '数据导出成功', + dataExportedSkippedShadows: '数据已导出。已跳过 {count} 个 spark 影子账号:其调度配置不在备份内,还原后需在重建的影子上重新调优。', + dataExportFailed: '数据导出失败', + dataImportTitle: '导入数据', + dataImportHint: '上传导出的 JSON 文件以批量导入账号与代理。', + dataImportWarning: '导入将创建新账号与代理,分组需手工绑定;请确认已有数据不会冲突。', + dataImportFile: '数据文件', + dataImportButton: '开始导入', + dataImporting: '导入中...', + dataImportSelectFile: '请选择数据文件', + dataImportParseFailed: '数据解析失败', + dataImportParseFailedFile: '文件 {name} 解析失败', + dataImportInvalidFile: '文件 {name} 不是受支持的导出数据文件', + dataImportIgnoredFiles: '已忽略 {count} 个非 JSON 文件', + dataImportFailed: '数据导入失败', + dataImportResult: '导入结果', + dataImportResultSummary: '代理创建 {proxy_created},复用 {proxy_reused},失败 {proxy_failed};账号创建 {account_created},失败 {account_failed}', + dataImportErrors: '失败详情', + dataImportSuccess: '导入完成:账号 {account_created},失败 {account_failed}', + dataImportCompletedWithErrors: '导入完成但有错误:账号失败 {account_failed},代理失败 {proxy_failed}', + syncFromCrsTitle: '从 CRS 同步账号', + syncFromCrsDesc: + '将 claude-relay-service(CRS)中的账号同步到当前系统(不会在浏览器侧直接请求 CRS)。', + crsVersionRequirement: '⚠️ 注意:CRS 版本必须 ≥ v1.1.240 才支持此功能', + crsBaseUrl: 'CRS 服务地址', + crsBaseUrlPlaceholder: '例如:http://127.0.0.1:3000', + crsUsername: '用户名', + crsPassword: '密码', + syncProxies: '同时同步代理(按 host/port/账号匹配或自动创建)', + syncNow: '开始同步', + syncing: '同步中...', + syncMissingFields: '请填写服务地址、用户名和密码', + syncResult: '同步结果', + syncResultSummary: '创建 {created},更新 {updated},跳过 {skipped},失败 {failed}', + syncErrors: '错误/跳过详情', + syncCompleted: '同步完成:创建 {created},更新 {updated},跳过 {skipped}', + syncCompletedWithErrors: '同步完成但有错误:失败 {failed}(创建 {created},更新 {updated},跳过 {skipped})', + syncFailed: '同步失败', + crsPreview: '预览', + crsPreviewing: '预览中...', + crsPreviewFailed: '预览失败', + crsExistingAccounts: '将自动更新的已有账号', + crsNewAccounts: '新账号(可选择)', + crsSelectAll: '全选', + crsSelectNone: '全不选', + crsNoNewAccounts: '所有 CRS 账号均已同步。', + crsWillUpdate: '将更新 {count} 个已有账号。', + crsSelectedCount: '已选择 {count} 个新账号', + crsUpdateBehaviorNote: + '已有账号仅同步 CRS 返回的字段,缺失字段保持原值;凭据按键合并,不会清空未下发的键;未勾选"同步代理"时保留原有代理。', + crsBack: '返回', + editAccount: '编辑账号', + deleteAccount: '删除账号', + deleteConfirmMessage: "确定要删除账号 '{name}' 吗?", + refreshCookie: '刷新 Cookie', + testAccount: '测试账号', + searchAccounts: '搜索账号...', + notes: '备注', + notesPlaceholder: '请输入备注', + notesHint: '备注可选', + // Filter options + allPlatforms: '全部平台', + allTypes: '全部类型', + allStatus: '全部状态', + allGroups: '全部分组', + ungroupedGroup: '未分配分组', + oauthType: 'OAuth', + // Schedulable toggle + schedulable: '参与调度', + schedulableHint: '开启后账号参与API请求调度', + schedulableEnabled: '调度已开启', + schedulableDisabled: '调度已关闭', + failedToToggleSchedulable: '切换调度状态失败', + groupCountTotal: '共 {count} 个分组', + columns: { + name: '名称', + id: '账号ID', + platformType: '平台/类型', + platform: '平台', + type: '类型', + capacity: '容量', + notes: '备注', + priority: '优先级', + billingRateMultiplier: '账号倍率', + weight: '权重', + schedulerScore: '调度权值', + status: '状态', + schedulable: '调度', + todayStats: '今日统计', + groups: '分组', + usageWindows: '用量窗口', + proxy: '代理', + lastUsed: '最近使用', + createdAt: '创建时间', + expiresAt: '过期时间', + actions: '操作' + }, + schedulerScore: { + baseShort: '普通', + stickyShort: '粘性', + ungrouped: '未分组', + hint: '显示格式为“分组名 / 基础分 / 粘性加分”。基础分按当前筛选条件限定的候选账号计算,包含优先级、负载、排队、错误率、首包延迟、重置窗口、额度余量等因子;粘性加分只在开启粘性加权时用于 previous_response_id 或 session_hash。分数越大越优先。' + }, + usageWindowsHint: '“5h / 7d”是上游账号(如 OpenAI ChatGPT、Claude)官方的滚动用量窗口限制,由上游对账号设定,并非 sub2api 配置,也与你映射的模型无关。窗口滚动到期后用量会自动重置,无法在 sub2api 端解除该限制。', + allPrivacyModes: '全部Privacy状态', + privacyUnset: '未设置', + privacyTrainingOff: '已关闭训练数据共享', + privacyCfBlocked: '被 Cloudflare 拦截,训练可能仍开启', + privacyFailed: '关闭训练数据共享失败', + privacyAntigravitySet: '已关闭遥测和营销邮件', + privacyAntigravityFailed: '隐私设置失败', + setPrivacy: '设置隐私', + subscriptionAbnormal: '异常', + subscriptionExpires: '到期', + // 容量状态提示 + capacity: { + windowCost: { + blocked: '5h窗口费用超限,账号暂停调度', + stickyOnly: '5h窗口费用达阈值,仅允许粘性会话', + normal: '5h窗口费用正常' + }, + sessions: { + full: '活跃会话已满,新会话需等待(空闲超时:{idle}分钟)', + normal: '活跃会话正常(空闲超时:{idle}分钟)' + }, + rpm: { + full: '已达 RPM 上限', + warning: 'RPM 接近上限', + normal: 'RPM 正常', + tieredNormal: 'RPM 限制 (三区模型) - 正常', + tieredWarning: 'RPM 限制 (三区模型) - 接近阈值', + tieredStickyOnly: 'RPM 限制 (三区模型) - 仅粘性会话 | 缓冲区: {buffer}', + tieredBlocked: 'RPM 限制 (三区模型) - 已阻塞 | 缓冲区: {buffer}', + stickyExemptNormal: 'RPM 限制 (粘性豁免) - 正常', + stickyExemptWarning: 'RPM 限制 (粘性豁免) - 接近阈值', + stickyExemptOver: 'RPM 限制 (粘性豁免) - 超限,仅粘性会话' + }, + quota: { + exceeded: '配额已用完,账号暂停调度', + normal: '配额正常' + }, + }, + clearRateLimit: '清除速率限制', + resetQuota: '重置配额', + quotaLimit: '配额限制', + quotaLimitPlaceholder: '0 表示不限制', + quotaLimitHint: '设置日/周/总使用额度(美元),任一维度达到限额后账号暂停调度。Anthropic API Key 账号还可配置客户端亲和。修改限额不会重置已用额度。', + quotaLimitToggle: '启用配额限制', + quotaLimitToggleHint: '开启后,当账号用量达到设定额度时自动暂停调度', + quotaDailyLimit: '日限额', + quotaDailyLimitHint: '从首次使用起每 24 小时自动重置。', + quotaWeeklyLimit: '周限额', + quotaWeeklyLimitHint: '从首次使用起每 7 天自动重置。', + quotaTotalLimit: '总限额', + quotaTotalLimitHint: '累计消费上限,不会自动重置 — 使用「重置配额」手动清零。', + quotaResetMode: '重置方式', + quotaResetModeRolling: '滚动窗口', + quotaResetModeFixed: '固定时间', + quotaResetHour: '重置时间', + quotaWeeklyResetDay: '重置日', + quotaResetTimezone: '重置时区', + quotaDailyLimitHintFixed: '每天 {hour}:00({timezone})重置。', + quotaWeeklyLimitHintFixed: '每{day} {hour}:00({timezone})重置。', + dayOfWeek: { + monday: '周一', + tuesday: '周二', + wednesday: '周三', + thursday: '周四', + friday: '周五', + saturday: '周六', + sunday: '周日', + }, + quotaLimitAmount: '总限额', + quotaLimitAmountHint: '累计消费上限,不会自动重置。', + quotaNotify: { + alert: '提醒阈值', + enabled: '启用告警', + threshold: '告警金额', + thresholdPlaceholder: '输入百分比', + }, + testConnection: '测试连接', + reAuthorize: '重新授权', + refreshToken: '刷新令牌', + noAccountsYet: '暂无账号', + createFirstAccount: '添加 AI 平台账号以开始使用 API 网关。', + tokenRefreshed: 'Token 刷新成功', + accountDeleted: '账号删除成功', + rateLimitCleared: '速率限制已清除', + setupToken: 'Setup Token', + apiKey: 'API Key', + deleteConfirm: "确定要删除账号 '{name}' 吗?此操作无法撤销。", + failedToClearRateLimit: '清除速率限制失败', + platforms: { + claude: 'Claude', + openai: 'OpenAI', + anthropic: 'Anthropic', + gemini: 'Gemini', + antigravity: 'Antigravity', + grok: 'Grok', + }, + types: { + oauth: 'OAuth', + chatgptOauth: 'ChatGPT OAuth', + responsesApi: 'Responses API', + googleOauth: 'Google OAuth', + codeAssist: 'Code Assist', + antigravityOauth: 'Antigravity OAuth', + grokOauth: 'Grok OAuth', + antigravityApikey: '通过 Base URL + API Key 连接', + upstream: '对接上游', + upstreamDesc: '通过 Base URL + API Key 连接上游', + api_key: 'API Key', + cookie: 'Cookie' + }, + antigravityProjectIdLabel: 'GCP Project ID(可选)', + antigravityProjectIdPlaceholder: 'your-gcp-project-id', + antigravityProjectIdHint: + 'standard-tier 且未自动返回 project_id 的 Antigravity 账号需要填写用户自带 GCP project。', + status: { + active: '正常', + inactive: '停用', + error: '错误', + cooldown: '冷却中', + paused: '暂停', + limited: '限流', + rateLimited: '限流中', + overloaded: '过载中', + tempUnschedulable: '临时不可调度', + quotaExceeded: '配额超限', + unschedulable: '不可调度', + rateLimitedUntil: '限流中,当前不参与调度,预计 {time} 自动恢复', + rateLimitedAutoResume: '{time} 自动恢复', + modelRateLimitedUntil: '{model} 限流至 {time}', + modelCreditOveragesUntil: '{model} 正在使用 AI Credits,至 {time}', + creditsExhausted: '积分已用尽', + creditsExhaustedUntil: 'AI Credits 已用尽,预计 {time} 恢复', + overloadedUntil: '负载过重,重置时间:{time}', + viewTempUnschedDetails: '查看临时不可调度详情' + }, + tempUnschedulable: { + title: '临时不可调度', + statusTitle: '临时不可调度状态', + hint: '当错误码与关键词同时匹配时,账号会在指定时间内被临时禁用。', + notice: '规则按顺序匹配,需同时满足错误码与关键词。', + addRule: '添加规则', + ruleOrder: '规则序号', + ruleIndex: '规则 #{index}', + errorCode: '错误码', + errorCodePlaceholder: '例如 429', + durationMinutes: '持续时间(分钟)', + durationPlaceholder: '例如 30', + keywords: '关键词', + keywordsPlaceholder: '例如 overloaded, too many requests', + keywordsHint: '多个关键词用逗号分隔,匹配时必须命中其中之一。', + description: '描述', + descriptionPlaceholder: '可选,便于记忆规则用途', + rulesInvalid: '请至少填写一条包含错误码、关键词和时长的规则。', + viewDetails: '查看临时不可调度详情', + accountName: '账号', + triggeredAt: '触发时间', + until: '解除时间', + remaining: '剩余时间', + matchedKeyword: '匹配关键词', + errorMessage: '错误详情', + reset: '恢复状态', + resetSuccess: '账号状态已恢复', + resetFailed: '恢复账号状态失败', + failedToLoad: '加载临时不可调度状态失败', + notActive: '当前账号未处于临时不可调度状态。', + expired: '已到期', + remainingMinutes: '约 {minutes} 分钟', + remainingHours: '约 {hours} 小时', + remainingHoursMinutes: '约 {hours} 小时 {minutes} 分钟', + presets: { + overloadLabel: '529 过载', + overloadDesc: '服务过载 - 暂停 60 分钟', + rateLimitLabel: '429 限流', + rateLimitDesc: '触发限流 - 暂停 10 分钟', + unavailableLabel: '503 维护', + unavailableDesc: '服务不可用 - 暂停 30 分钟' + } + }, + usageWindow: { + statsTitle: '5小时窗口用量统计', + statsTitleDaily: '每日用量统计', + geminiProDaily: 'Pro', + geminiFlashDaily: 'Flash', + gemini3Pro: 'G3P', + gemini3Flash: 'G3F', + gemini3Image: 'G31FI', + claude: 'Claude', + grokRequests: '请求', + grokTokens: 'Token', + grokUnknown: 'Grok 配额需等待首次上游响应返回 xAI rate-limit 头后显示。', + grokRetryAfter: '{time} 后重试', + grokProbe: '探测', + grokProbeTooltip: '发送最小 xAI Responses 探测请求并读取配额响应头', + grokResetUnsupported: '不支持重置', + grokResetUnsupportedTooltip: 'xAI 未向 Grok OAuth 账号开放重置额度接口', + grokNoHeaders: '未观察到配额响应头', + grokLastStatus: '状态 {status}', + grokLastProbe: '探测 {time}', + grokLastHeadersSeen: '响应头 {time}', + passiveSampled: '被动采样', + activeQuery: '查询' + }, + openaiQuotaReset: { + count: '次数', + reset: '重置', + countTooltipLoad: '点击查询剩余重置次数', + countTooltipRefresh: '点击刷新剩余重置次数', + resetTooltipReady: '消耗 1 次重置次数以立即恢复当前窗口', + resetTooltipNeedQuery: '先点击「次数」加载剩余重置次数', + resetTooltipNoCredits: '没有可用的重置次数', + resetTooltipShadow: 'Spark 影子账号不能重置次数;请在母账号上重置', + expiresAt: '到期 {time}', + expiresAtFull: '重置次数到期时间: {time}', + expandExpirations: '展开其余 {count} 张重置次数到期时间', + collapseExpirations: '收起重置次数到期时间', + expirationDetails: '重置次数到期明细', + noCreditsAvailable: '没有可用的重置次数', + resetSuccess: '已重置 {windows} 个窗口', + confirmTitle: '确认重置周限', + confirmMessage: '将消耗 1 次重置次数立即恢复当前窗口,剩余 {count} 次。此操作不可撤销,确定继续吗?' + }, + tier: { + free: 'Free', + pro: 'Pro', + ultra: 'Ultra', + aiPremium: 'AI Premium', + standard: '标准版', + basic: '基础版', + personal: '个人版', + unlimited: '无限制' + }, + ineligibleWarning: + '该账号无 Antigravity 使用权限,但仍能进行 API 转发。继续使用请自行承担风险。', + forbidden: '已封禁', + forbiddenValidation: '需要验证', + forbiddenViolation: '违规封禁', + openVerification: '打开验证链接', + copyLink: '复制链接', + linkCopied: '链接已复制', + needsReauth: '需要重新授权', + rateLimited: '限流中', + usageError: '获取失败', + form: { + nameLabel: '账号名称', + namePlaceholder: '请输入账号名称', + platformLabel: '平台', + selectPlatform: '选择平台', + typeLabel: '类型', + selectType: '选择类型', + credentialsLabel: '凭证', + credentialsPlaceholder: '请输入 Cookie 或 API Key', + priorityLabel: '优先级', + priorityHint: '数值越小优先级越高', + weightLabel: '权重', + weightHint: '用于负载均衡的权重值', + statusLabel: '状态' + }, + filters: { + platform: '平台', + allPlatforms: '全部平台', + type: '类型', + allTypes: '全部类型', + status: '状态', + allStatuses: '全部状态' + }, + saving: '保存中...', + refreshing: '刷新中...', + testing: '测试中...', + noAccounts: '暂无账号', + noAccountsDescription: '添加 AI 平台账号以开始使用 API 网关。', + accountCreatedSuccess: '账号添加成功', + accountUpdatedSuccess: '账号更新成功', + accountDeletedSuccess: '账号删除成功', + bulkSchedulableEnabled: '成功启用 {count} 个账号的调度', + bulkSchedulableDisabled: '成功停止 {count} 个账号的调度', + bulkSchedulablePartial: '部分调度更新成功:成功 {success} 个,失败 {failed} 个', + bulkSchedulableResultUnknown: '批量调度结果不完整,请稍后重试或刷新列表', + bulkActions: { + selected: '已选择 {count} 个账号', + selectCurrentPage: '本页全选', + clear: '清除选择', + edit: '批量编辑账号', + delete: '批量删除', + enableScheduling: '批量启用调度', + disableScheduling: '批量停止调度', + resetStatus: '批量重置状态', + refreshToken: '批量刷新令牌', + resetStatusSuccess: '已成功重置 {count} 个账号状态', + refreshTokenSuccess: '已成功刷新 {count} 个账号令牌', + partialSuccess: '操作部分完成:{success} 成功,{failed} 失败' + }, + bulkEdit: { + title: '批量编辑账号', + selectionInfo: '已选择 {count} 个账号。只更新您勾选或填写的字段,未勾选的字段保持不变。', + baseUrlPlaceholder: 'https://api.anthropic.com 或 https://api.openai.com', + baseUrlNotice: '仅适用于 API Key 账号,留空则不修改', + submit: '批量更新', + updating: '更新中...', + success: '成功更新 {count} 个账号', + partialSuccess: '部分更新成功:成功 {success} 个,失败 {failed} 个', + failed: '批量更新失败', + noSelection: '请选择要编辑的账号', + noFieldsSelected: '请至少选择一个要更新的字段', + mixedPlatformWarning: '所选账号跨越多个平台({platforms})。显示的模型映射预设为合并结果——请确保映射对每个平台都适用。' + }, + bulkDeleteTitle: '批量删除账号', + bulkDeleteConfirm: '确定要删除选中的 {count} 个账号吗?此操作无法撤销。', + bulkDeleteSuccess: '成功删除 {count} 个账号', + bulkDeletePartial: '部分删除成功:成功 {success} 个,失败 {failed} 个', + bulkDeleteFailed: '批量删除失败', + recoverState: '恢复状态', + recoverStateHint: '用于恢复错误、限流和临时不可调度等可恢复状态。', + recoverStateSuccess: '账号状态已恢复', + recoverStateFailed: '恢复账号状态失败', + fallbackActive: '已回退', + fallbackActiveTip: '原代理 {origin} 已到期,当前使用备用代理', + revertProxy: '切回原代理', + revertProxySuccess: '已成功切回原代理', + revertProxyFailed: '切回原代理失败', + createSparkShadow: '创建 Spark 影子账号', + createSparkShadowConfirm: '为「{name}」创建链接型 Spark 影子账号?影子共享母账号凭据、仅服务 spark 模型。', + createSparkShadowSuccess: 'Spark 影子账号已创建', + createSparkShadowFailed: '创建 Spark 影子账号失败', + resetStatus: '重置状态', + statusReset: '账号状态已重置', + failedToResetStatus: '重置账号状态失败', + cookieRefreshedSuccess: 'Cookie 刷新成功', + testSuccess: '账号测试通过', + testFailed: '账号测试失败', + failedToLoad: '加载账号列表失败', + failedToSave: '保存账号失败', + failedToDelete: '删除账号失败', + failedToRefresh: '刷新 Cookie 失败', + // Create/Edit Account Modal + platform: '平台', + accountName: '账号名称', + enterAccountName: '请输入账号名称', + accountType: '账号类型', + claudeCode: 'Claude Code', + claudeConsole: 'Claude Console', + bedrockLabel: 'AWS Bedrock', + bedrockDesc: 'SigV4 / API Key', + vertexLabel: 'Vertex', + vertexDesc: 'Service Account', + vertexAnthropicHint: '使用 Google Cloud Service Account JSON 通过 Vertex AI 调用 Anthropic Claude。建议配置模型映射,将客户端 Claude 模型名映射到 Vertex 模型 ID。', + vertexGeminiHint: '使用 Google Cloud Service Account JSON 访问 Vertex AI Gemini。建议将 Vertex 账号放入独立分组,避免和 AI Studio/Gemini OAuth 同模型混调。', + vertexSaJsonLabel: 'Service Account JSON', + vertexSaJsonLoaded: '已读取 Service Account JSON', + vertexSaJsonDrop: '拖入 Service Account JSON', + vertexSaJsonKeyHidden: '密钥内容不会在表单中显示。', + vertexSaJsonDropHint: '把 .json 文件拖到这里,或点击按钮选择文件。', + vertexSaJsonSelectBtn: '选择 JSON', + vertexSaJsonUploadHint: '上传或拖入 JSON 后会自动读取 project_id,密钥内容仅用于创建账号提交。', + vertexSaJsonEditHint: 'Service Account JSON 不在编辑页显示;需要更换 JSON 时请删除账号后重新创建。', + vertexProjectIdPlaceholder: '从 JSON 自动读取', + vertexLocationHint: '不同 Vertex 模型可用 location 可能不同,这里选择账号默认 endpoint location。', + vertexLocationRequired: '请填写 Vertex location', + vertexSaJsonMissingFields: 'Service Account JSON 缺少 project_id、client_email 或 private_key', + vertexSaJsonMissingProjectId: 'Service Account JSON 缺少 project_id', + vertexSaJsonMissingClientEmail: 'Service Account JSON 缺少 client_email', + vertexSaJsonInvalid: 'Service Account JSON 格式无效', + vertexSaJsonRequired: '请上传 Service Account JSON', + oauthSetupToken: 'OAuth / Setup Token', + addMethod: '添加方式', + setupTokenLongLived: 'Setup Token(长期有效)', + baseUrl: 'Base URL', + baseUrlHint: '留空使用官方 Anthropic API', + apiKeyRequired: 'API Key *', + apiKeyPlaceholder: 'sk-ant-api03-...', + apiKeyHint: '您的 Claude Console API Key', + // OpenAI specific hints + openai: { + baseUrlHint: '留空使用官方 OpenAI API', + apiKeyHint: '您的 OpenAI API Key', + oauthPassthrough: '自动透传(仅替换认证)', + oauthPassthroughDesc: + '开启后,该 OpenAI 账号将自动透传请求与响应,仅替换认证并保留计费/并发/审计及必要安全过滤;如遇兼容性问题可随时关闭回滚。', + responsesWebsocketsV2: 'Responses WebSocket v2', + responsesWebsocketsV2Desc: + '默认关闭。开启后可启用 responses_websockets_v2 协议能力(受网关全局开关与账号类型开关约束)。', + wsMode: 'WS mode', + wsModeDesc: '仅对当前 OpenAI 账号类型生效。', + wsModeOff: '关闭(off)', + wsModeCtxPool: '上下文池(ctx_pool)', + wsModePassthrough: '透传(passthrough)', + wsModeHttpBridge: 'HTTP 桥接(http_bridge)', + wsModeShared: '共享(shared)', + wsModeDedicated: '独享(dedicated)', + wsModeConcurrencyHint: '启用 WS mode 后,该账号并发数将作为该账号 WS 连接池上限。', + wsModePassthroughHint: 'passthrough 模式不使用 WS 连接池。', + oauthResponsesWebsocketsV2: 'OAuth WebSocket Mode', + oauthResponsesWebsocketsV2Desc: + '仅对 OpenAI OAuth 生效。开启后该账号才允许使用 OpenAI WebSocket Mode 协议。', + apiKeyResponsesWebsocketsV2: 'API Key WebSocket Mode', + apiKeyResponsesWebsocketsV2Desc: + '仅对 OpenAI API Key 生效。开启后该账号才允许使用 OpenAI WebSocket Mode 协议。', + responsesWebsocketsV2PassthroughHint: '当前已开启自动透传:仅影响 HTTP 透传链路,不影响 WS mode。', + responsesMode: 'Responses API 支持', + responsesModeDesc: + '仅对 OpenAI API Key 的文本转发链路生效。自动跟随探测结果,强制模式会覆盖自动探测。', + responsesModeAuto: '自动', + responsesModeForceResponses: '强制 Responses', + responsesModeForceChatCompletions: '强制 Chat Completions', + responsesModeTextDisabledHint: '未启用 Responses / Chat Completions 端点时,此设置不适用。', + endpointCapabilities: '端点能力', + endpointCapabilitiesDesc: + '用于调度筛选。文本端点会跟随上方 Responses API 支持显示为 Responses、Chat Completions 或自动模式;Embeddings 独立控制 /v1/embeddings。', + capabilityResponses: 'Responses', + capabilityTextAuto: 'Responses / Chat Completions(自动)', + capabilityResponsesAuto: 'Responses(自动探测)', + capabilityChatCompletions: 'Chat Completions', + capabilityChatCompletionsAuto: 'Chat Completions(自动探测)', + capabilityEmbeddings: 'Embeddings', + responsesStatusAutoSupported: '自动探测:Responses', + responsesStatusAutoUnsupported: '自动探测:Chat Completions', + responsesStatusAutoUnknown: '自动探测:未探测', + responsesStatusForcedResponses: '已强制 Responses', + responsesStatusForcedChatCompletions: '已强制 Chat Completions', + codexCLIOnly: '仅允许 Codex 官方客户端', + codexCLIOnlyDesc: '仅对 OpenAI OAuth 生效。开启后仅允许 Codex 官方客户端家族访问;关闭后完全绕过并保持原逻辑。', + codexCLIOnlyAppServer: '允许 Codex app-server 客户端', + codexCLIOnlyAppServerDesc: '仅在上方开关开启时生效。开启后本账号额外放行内嵌 Codex 引擎、经 app-server 协议接入的第三方客户端(如 Claude Code 的 codex 插件),仍需通过全局引擎指纹门;与全局 app-server 开关取 OR(任一开即放行)。', + codexImageTool: 'Codex 图片工具', + codexImageToolDesc: + '统一控制 Codex /responses 文本请求的 image_generation 图片工具:是否自动注入,以及客户端自带该工具时是否放行。账号级策略优先于渠道和全局配置,不影响独立图片生成接口。', + codexImageToolInherit: '跟随渠道', + codexImageToolInheritDesc: '不写入账号覆盖,是否注入由渠道或全局策略决定;客户端自带的图片工具照常放行。', + codexImageToolEnabled: '强制注入', + codexImageToolEnabledDesc: '始终为 Codex /responses 请求注入图片工具。', + codexImageToolDisabled: '关闭注入', + codexImageToolDisabledDesc: '不自动注入;客户端自带的图片工具仍会放行。', + codexImageToolBlock: '完全阻断', + codexImageToolBlockDesc: '不注入,并移除客户端自带的图片工具及指向它的 tool_choice。', + codexImageToolBadgeInherit: '渠道策略', + codexImageToolBadgeEnabled: '强制注入', + codexImageToolBadgeDisabled: '关闭注入', + codexImageToolBadgeBlock: '完全阻断', + compactMode: 'Compact 模式', + compactModeDesc: + '控制本账号在 /responses/compact 调度中的参与方式。Auto 跟随探测结果,Force On 强制允许,Force Off 强制排除。', + compactModeAuto: '自动', + compactModeForceOn: '强制开启', + compactModeForceOff: '强制关闭', + compactModelMapping: 'Compact 专属模型映射', + compactModelMappingDesc: + '仅在 /responses/compact 请求中生效。当上游 compact 端点需要特殊 compact 模型时使用。', + compactSupported: '支持 Compact', + compactUnsupported: '不支持 Compact', + compactAuto: 'Compact Auto', + compactUnknown: 'Compact Auto', + compactLastChecked: '最近探测', + testMode: '测试模式', + testModeDefault: '常规请求', + testModeCompact: 'Compact 探测', + modelRestrictionDisabledByPassthrough: '已开启自动透传:模型白名单/映射不会生效。', + }, + grok: { + baseUrlHint: 'Grok OAuth 账号会转发到官方 xAI API Base URL。', + apiKeyHint: 'Grok 订阅支持使用 OAuth refresh token;API Key 账号不在本次范围内。' + }, + anthropic: { + apiKeyPassthrough: '自动透传(仅替换认证)', + apiKeyPassthroughDesc: + '仅对 Anthropic API Key 生效。开启后,messages/count_tokens 请求将透传上游并仅替换认证,保留计费/并发/审计及必要安全过滤;关闭即可回滚到现有兼容链路。', + apiKeyAuthScheme: '上游认证方式', + apiKeyAuthSchemeDesc: '选择转发到 Anthropic-compatible 上游时使用的 API Key 认证头。Ollama Cloud 使用 Authorization: Bearer。', + apiKeyAuthSchemeXApiKey: 'x-api-key', + apiKeyAuthSchemeBearer: 'Authorization: Bearer', + webSearchEmulation: 'Web Search 模拟', + webSearchEmulationDesc: + '为该 API Key 账号启用 web search 模拟。客户端发送纯 web_search 请求时,由网关调用第三方搜索 API 并构造响应返回。默认跟随渠道配置。', + webSearchDefault: '默认', + webSearchEnabled: '开启', + webSearchDisabled: '关闭', + }, + modelRestriction: '模型限制(可选)', + modelWhitelist: '模型白名单', + modelMapping: '模型映射', + selectAllowedModels: '选择允许的模型。留空则支持所有模型。', + mapRequestModels: '将请求模型映射到实际模型。左边是请求的模型,右边是发送到 API 的实际模型。', + selectedModels: '已选择 {count} 个模型', + supportsAllModels: '(支持所有模型)', + requestModel: '请求模型', + actualModel: '实际模型', + addMapping: '添加映射', + mappingExists: '模型 {model} 的映射已存在', + wildcardOnlyAtEnd: '通配符 * 只能放在末尾', + targetNoWildcard: '目标模型不能包含通配符 *', + searchModels: '搜索模型...', + noMatchingModels: '没有匹配的模型', + fillRelatedModels: '同步最新支持模型', + syncUpstreamModels: '同步上游支持的模型', + syncUpstreamModelsLoading: '同步上游中...', + syncUpstreamModelsSuccess: '已从上游同步 {count} 个新模型(上游共 {total} 个)', + syncUpstreamModelsNoChanges: '上游 {count} 个模型均已在白名单中', + syncUpstreamModelsEmpty: '上游没有返回可同步的模型', + syncUpstreamModelsFailed: '同步上游模型失败', + syncUpstreamModelsError: '同步上游模型失败:{message}', + clearAllModels: '清除所有模型', + customModelName: '自定义模型名称', + enterCustomModelName: '输入自定义模型名称', + addModel: '填入', + modelExists: '该模型已存在', + modelCount: '{count} 个模型', + poolMode: '池模式', + poolModeHint: '上游为账号池时启用,错误不标记本地账号状态', + poolModeInfo: + '启用后,上游 429/403/401 错误将自动重试而不标记账号限流或错误,适用于上游指向另一个 sub2api 实例的场景。', + poolModeRetryCount: '同账号重试次数', + poolModeRetryCountHint: '仅在池模式下生效。0 表示不原地重试;默认 {default},最大 {max}。', + poolModeRetryStatusCodes: '同账号重试状态码', + poolModeRetryStatusCodesHint: '仅在池模式下生效。以英文逗号分隔的 HTTP 状态码(100-599),命中时触发同账号重试。留空使用默认值({default})。', + customErrorCodes: '自定义错误码', + customErrorCodesHint: '仅对选中的错误码停止调度', + customErrorCodesWarning: '仅选中的错误码会停止调度,其他错误将返回 500。', + customErrorCodes429Warning: + '429 已有内置的限流处理机制。添加到自定义错误码后,将直接停止调度而非临时限流。确定要添加吗?', + customErrorCodes529Warning: + '529 已有内置的过载处理机制。添加到自定义错误码后,将直接停止调度而非临时标记过载。确定要添加吗?', + selectedErrorCodes: '已选择', + noneSelectedUsesDefault: '未选择(使用默认策略)', + enterErrorCode: '输入错误码 (100-599)', + invalidErrorCode: '请输入有效的 HTTP 错误码 (100-599)', + errorCodeExists: '该错误码已被选中', + interceptWarmupRequests: '拦截预热请求', + interceptWarmupRequestsDesc: '启用后,标题生成等预热请求将返回 mock 响应,不消耗上游 token', + headerOverride: { + title: '请求头覆写', + hint: '转发时用配置值覆盖同名请求头(不区分大小写)', + info: '仅对本账号的出站请求生效:配置的请求头会在转发前覆盖客户端/网关生成的同名头。认证头(authorization、x-api-key)与连接控制头不允许覆写。', + namePlaceholder: '请求头名称(如 user-agent)', + valuePlaceholder: '覆写值(留空表示不覆写)', + addRow: '添加请求头', + fillTemplate: '填入模板', + emptyValueHint: '值留空的行不会参与覆盖,仅作为待填写的占位。', + bulkDisableHint: '保存后将关闭所选账号的请求头覆写并清空已有配置。', + bulkReplaceHint: '保存后将用下方配置整体替换所选账号已有的请求头覆写配置。', + bulkEmptyRows: '请至少添加一行请求头再保存;如需清空已有配置,请关闭上方开关。', + invalidName: '请求头名称格式不正确(仅允许字母、数字和 !#$%&\'*+-.^_`|~ 字符)', + blockedName: '该请求头不允许覆写(认证头与连接控制头由系统管理)', + duplicateName: '存在重复的请求头名称(匹配不区分大小写)', + invalidValue: '请求头值不合法(不允许控制字符,长度不超过 8192)', + tooManyEntries: '请求头覆写条目过多(最多 64 条)' + }, + autoPauseOnExpired: '过期自动暂停调度', + autoPauseOnExpiredDesc: '启用后,账号过期将自动暂停调度', + autoPause5hThreshold: '5h 用量阈值(%)', + autoPause7dThreshold: '7d 用量阈值(%)', + autoPauseThresholdHint: '留空或填 0 表示使用全局默认阈值(在运维设置中配置);填具体值则覆盖全局默认。达到阈值后仅在调度时跳过账号,不修改 schedulable。', + autoPause5hDisabled: '禁用 5h 自动暂停', + autoPause7dDisabled: '禁用 7d 自动暂停', + autoPauseDisabledHint: '开启后该账号永不进入自动暂停(即使全局默认阈值已配置)。', + // Quota control (Anthropic OAuth/SetupToken only) + quotaControl: { + title: '配额控制', + hint: '配置费用窗口、会话限制、客户端亲和等调度控制。', + windowCost: { + label: '5h窗口费用控制', + hint: '限制账号在5小时窗口内的费用使用', + limit: '费用阈值', + limitPlaceholder: '50', + limitHint: '达到阈值后不参与新请求调度', + stickyReserve: '粘性预留额度', + stickyReservePlaceholder: '10', + stickyReserveHint: '为粘性会话预留的额外额度' + }, + sessionLimit: { + label: '会话数量控制', + hint: '限制同时活跃的会话数量', + maxSessions: '最大会话数', + maxSessionsPlaceholder: '3', + maxSessionsHint: '同时活跃的最大会话数量', + idleTimeout: '空闲超时', + idleTimeoutPlaceholder: '5', + idleTimeoutHint: '会话空闲超时后自动释放' + }, + rpmLimit: { + label: 'RPM 限制', + hint: '限制每分钟请求数量,保护上游账号', + baseRpm: '基础 RPM', + baseRpmPlaceholder: '15', + baseRpmHint: '每分钟最大请求数,0 或留空表示不限制', + strategy: 'RPM 策略', + strategyTiered: '三区模型', + strategyStickyExempt: '粘性豁免', + strategyTieredHint: '绿区→黄区→仅粘性→阻塞,逐步限流', + strategyStickyExemptHint: '超限后仅允许粘性会话', + strategyHint: '三区模型: 超限后逐步限制; 粘性豁免: 已有会话不受限', + stickyBuffer: '粘性缓冲区', + stickyBufferPlaceholder: '默认: base RPM 的 20%', + stickyBufferHint: '超过 base RPM 后,粘性会话额外允许的请求数。为空则使用默认值(base RPM 的 20%,最小为 1)', + userMsgQueue: '用户消息限速', + userMsgQueueHint: '对用户消息施加发送限制,避免触发上游 RPM 限制', + umqModeOff: '关闭', + umqModeThrottle: '软性限速', + umqModeSerialize: '串行队列', + }, + tlsFingerprint: { + label: 'TLS 指纹模拟', + hint: '模拟 Node.js/Claude Code 客户端的 TLS 指纹', + defaultProfile: '内置默认', + randomProfile: '随机' + }, + sessionIdMasking: { + label: '会话 ID 伪装', + hint: '启用后将在 15 分钟内固定 metadata.user_id 中的 session ID,使上游认为请求来自同一会话' + }, + cacheTTLOverride: { + label: '缓存 TTL 强制替换', + hint: '将所有缓存创建 token 强制按指定的 TTL 类型(5分钟或1小时)计费', + target: '目标 TTL', + targetHint: '选择计费使用的 TTL 类型' + }, + customBaseUrl: { + label: '自定义转发地址', + hint: '启用后将请求转发到自定义中继服务,代理地址将作为 URL 参数传递给中继服务', + urlHint: '中继服务地址(如 https://relay.example.com)', + }, + clientAffinity: { + label: '客户端亲和调度', + hint: '启用后,新会话会优先调度到该客户端之前使用过的账号,避免频繁切换账号' + } + }, + affinityNoClients: '无亲和客户端', + affinityClients: '{count} 个亲和客户端:', + affinitySection: '客户端亲和', + affinitySectionHint: '控制客户端在账号间的分布。通过配置区域阈值来平衡负载。', + affinityToggle: '启用客户端亲和', + affinityToggleHint: '新会话优先调度到该客户端之前使用过的账号', + affinityBase: '基础限额(绿区)', + affinityBasePlaceholder: '留空表示不限制', + affinityBaseHint: '绿区最大客户端数量(完整优先级调度)', + affinityBaseOffHint: '未开启绿区限制,所有客户端均享受完整优先级调度', + affinityBuffer: '缓冲区(黄区)', + affinityBufferPlaceholder: '例如 3', + affinityBufferHint: '黄区允许的额外客户端数量(降级优先级调度)', + affinityBufferInfinite: '不限制', + expired: '已过期', + proxy: '代理', + noProxy: '无代理', + concurrency: '并发数', + loadFactor: '负载因子', + loadFactorHint: '提高负载因子可以提高对账号的调度频率', + priority: '优先级', + priorityHint: '优先级越小的账号优先使用', + billingRateMultiplier: '账号计费倍率', + billingRateMultiplierHint: '0 表示不计费,仅影响账号计费', + expiresAt: '过期时间', + expiresAtHint: '留空表示不过期', + higherPriorityFirst: '数值越小优先级越高', + mixedScheduling: '在 /v1/messages 中使用', + mixedSchedulingHint: '启用后可参与 Anthropic/Gemini 分组的调度', + mixedSchedulingTooltip: + '!!注意!! Antigravity Claude 和 Anthropic Claude 无法在同个上下文中使用,如果你同时有 Anthropic 账号和 Antigravity 账号,开启此选项会导致经常 400 报错。开启后,请用分组功能做好 Antigravity 账号和 Anthropic 账号的隔离。一定要弄明白再开启!!', + aiCreditsBalance: 'AI Credits', + allowOverages: '允许超量请求 (AI Credits)', + allowOveragesTooltip: + '仅在免费配额被明确判定为耗尽后才会使用 AI Credits。普通并发 429 限流不会切换到超量请求。', + creating: '创建中...', + updating: '更新中...', + accountCreated: '账号创建成功', + accountUpdated: '账号更新成功', + failedToCreate: '创建账号失败', + failedToUpdate: '更新账号失败', + pleaseSelectStatus: '请选择有效的账号状态', + mixedChannelWarningTitle: '混合渠道警告', + mixedChannelWarning: '警告:分组 "{groupName}" 中同时包含 {currentPlatform} 和 {otherPlatform} 账号。混合使用不同渠道可能导致 thinking block 签名验证问题,会自动回退到非 thinking 模式。确定要继续吗?', + pleaseEnterAccountName: '请输入账号名称', + pleaseEnterApiKey: '请输入 API Key', + bedrockAccessKeyId: 'AWS Access Key ID', + bedrockSecretAccessKey: 'AWS Secret Access Key', + bedrockSessionToken: 'AWS Session Token', + bedrockRegion: 'AWS Region', + bedrockRegionHint: '例如 us-east-1, us-west-2, eu-west-1', + bedrockForceGlobal: '强制使用 Global 跨区域推理', + bedrockForceGlobalHint: '启用后模型 ID 使用 global. 前缀(如 global.anthropic.claude-...),请求可路由到全球任意支持的区域,获得更高可用性', + bedrockAccessKeyIdRequired: '请输入 AWS Access Key ID', + bedrockSecretAccessKeyRequired: '请输入 AWS Secret Access Key', + bedrockRegionRequired: '请选择 AWS Region', + bedrockSessionTokenHint: '可选,用于临时凭证', + bedrockSecretKeyLeaveEmpty: '留空以保持当前密钥', + bedrockAuthMode: '认证方式', + bedrockAuthModeSigv4: 'SigV4 签名', + bedrockAuthModeApikey: 'Bedrock API Key', + bedrockApiKeyLabel: 'Bedrock API Key', + bedrockApiKeyDesc: 'Bearer Token 认证', + bedrockApiKeyInput: 'API Key', + bedrockApiKeyRequired: '请输入 Bedrock API Key', + bedrockApiKeyLeaveEmpty: '留空以保持当前密钥', + apiKeyIsRequired: 'API Key 是必需的', + leaveEmptyToKeep: '留空以保持当前密钥', + // Upstream type + upstream: { + baseUrl: '上游 Base URL', + baseUrlHint: '上游 Antigravity 服务的地址,例如:https://cloudcode-pa.googleapis.com', + apiKey: '上游 API Key', + apiKeyHint: '上游服务的 API Key', + pleaseEnterBaseUrl: '请输入上游 Base URL', + pleaseEnterApiKey: '请输入上游 API Key' + }, + // OAuth flow + oauth: { + title: 'Claude 账号授权', + authMethod: '授权方式', + manualAuth: '手动授权', + cookieAutoAuth: 'Cookie 自动授权', + cookieAutoAuthDesc: '使用 claude.ai sessionKey 自动完成 OAuth 授权,无需手动打开浏览器。', + sessionKey: 'sessionKey', + keysCount: '{count} 个密钥', + batchCreateAccounts: '将批量创建 {count} 个账号', + sessionKeyPlaceholder: + '每行一个 sessionKey,例如:\nsk-ant-sid01-xxxxx...\nsk-ant-sid01-yyyyy...', + sessionKeyPlaceholderSingle: 'sk-ant-sid01-xxxxx...', + howToGetSessionKey: '如何获取 sessionKey', + step1: '在浏览器中登录 claude.ai', + step2: '按 F12 打开开发者工具', + step3: '切换到 Application 标签', + step4: '找到 Cookies → https://claude.ai', + step5: '找到 sessionKey 所在行', + step6: '复制 Value 列的值', + sessionKeyFormat: 'sessionKey 通常以 sk-ant-sid01- 开头', + startAutoAuth: '开始自动授权', + authorizing: '授权中...', + followSteps: '按照以下步骤授权您的 Claude 账号:', + step1GenerateUrl: '点击下方按钮生成授权 URL', + generateAuthUrl: '生成授权 URL', + generating: '生成中...', + regenerate: '重新生成', + step2OpenUrl: '在浏览器中打开 URL 并完成授权', + openUrlDesc: '在新标签页中打开授权 URL,登录您的 Claude 账号并授权。', + proxyWarning: '注意:如果您配置了代理,请确保浏览器使用相同的代理访问授权页面。', + step3EnterCode: '输入授权码', + authCodeDesc: '授权完成后,页面会显示一个授权码。复制并粘贴到下方:', + authCode: '授权码', + authCodePlaceholder: '粘贴 Claude 页面的授权码...', + authCodeHint: '粘贴从 Claude 页面复制的授权码', + completeAuth: '完成授权', + verifying: '验证中...', + pleaseEnterSessionKey: '请输入至少一个有效的 sessionKey', + authFailed: '授权失败', + cookieAuthFailed: 'Cookie 授权失败', + keyAuthFailed: '密钥 {index}: {error}', + successCreated: '成功创建 {count} 个账号', + batchSuccess: '成功创建 {count} 个账号', + batchPartialSuccess: '部分成功:{success} 个成功,{failed} 个失败', + batchFailed: '批量创建失败', + // OpenAI specific + openai: { + title: 'OpenAI 账户授权', + followSteps: '请按照以下步骤完成 OpenAI 账户的授权:', + step1GenerateUrl: '点击下方按钮生成授权链接', + generateAuthUrl: '生成授权链接', + step2OpenUrl: '在浏览器中打开链接并完成授权', + openUrlDesc: '请在新标签页中打开授权链接,登录您的 OpenAI 账户并授权。', + importantNotice: + '重要提示:授权后页面可能会加载较长时间,请耐心等待。当浏览器地址栏变为 http://localhost... 开头时,表示授权已完成。', + step3EnterCode: '输入授权链接或 Code', + authCodeDesc: + '授权完成后,当页面地址变为 http://localhost:xxx/auth/callback?code=... 时:', + authCode: '授权链接或 Code', + authCodePlaceholder: + '方式1:复制完整的链接\n(http://localhost:xxx/auth/callback?code=...)\n方式2:仅复制 code 参数的值', + authCodeHint: '您可以直接复制整个链接或仅复制 code 参数值,系统会自动识别', + failedToGenerateUrl: '生成 OpenAI 授权链接失败', + failedToExchangeCode: 'OpenAI 授权码兑换失败', + failedToValidateRT: '验证 Refresh Token 失败', + errors: { + OPENAI_OAUTH_PROXY_REQUIRED: + '未设置代理,当前服务器无法直连 OpenAI,导致 OpenAI OAuth 请求失败。请先选择可访问 OpenAI 的代理后重试;如果授权码已失效,请重新生成授权链接。' + }, + // Refresh Token auth + refreshTokenAuth: '手动输入 RT', + refreshTokenDesc: '输入您已有的 OpenAI Refresh Token,支持批量输入(每行一个),系统将自动验证并创建账号。', + refreshTokenPlaceholder: '粘贴您的 OpenAI Refresh Token...\n支持多个,每行一个', + codexSessionAuth: 'Codex JSON / AT 批量输入', + codexSessionDesc: '粘贴 Codex JSON 或 accessToken,按第一步配置创建账号。', + codexSessionInputLabel: 'Codex JSON 或 accessToken', + codexSessionPlaceholder: '支持多行,每行一个 token 或 JSON', + codexSessionHint: 'sessionToken 不会作为 refresh_token 保存;未包含 refresh_token 时会按 accessToken 过期时间设置账号过期,无法解析且第一步未设置过期时间时会拒绝导入。', + codexSessionImportAndCreate: '导入并创建账号', + codexSessionEmpty: '请输入 Codex JSON 或 accessToken', + codexSessionImportFailed: 'Codex 账号导入失败', + codexSessionImportSuccess: '导入完成:新增 {created},更新 {updated},跳过 {skipped}', + codexSessionImportPartial: '部分成功:新增 {created},更新 {updated},跳过 {skipped},失败 {failed}', + codexPatAuth: 'Codex Personal Access Token', + codexPatDesc: '输入 Codex at- Personal Access Token,系统会先调用 OpenAI whoami 校验后再创建账号。', + codexPatInputLabel: 'Codex PAT', + codexPatPlaceholder: 'at-...', + codexPatHint: '这是独立认证模式,不保存 refresh_token,也不会写入 OAuth access_token 过期时间。', + codexPatImportAndCreate: '校验并创建 Codex PAT 账号', + codexPatEmpty: '请输入 Codex Personal Access Token', + codexPatImportFailed: 'Codex PAT 账号创建失败', + sessionTokenAuth: '手动输入 ST', + sessionTokenDesc: '输入您已有的 Session Token,支持批量输入(每行一个),系统将自动验证并创建账号。', + sessionTokenPlaceholder: '粘贴您的 Session Token...\n支持多个,每行一个', + sessionTokenRawLabel: '原始字符串', + sessionTokenRawPlaceholder: '粘贴 /api/auth/session 原始数据或 Session Token...', + sessionTokenRawHint: '支持粘贴完整 JSON,系统会自动解析 ST 和 AT。', + openSessionUrl: '打开获取链接', + copySessionUrl: '复制链接', + sessionUrlHint: '该链接通常可获取 AT。若返回中无 sessionToken,请从浏览器 Cookie 复制 __Secure-next-auth.session-token 作为 ST。', + parsedSessionTokensLabel: '解析出的 ST', + parsedSessionTokensEmpty: '未解析到 ST,请检查输入内容', + parsedAccessTokensLabel: '解析出的 AT', + validating: '验证中...', + validateAndCreate: '验证并创建账号', + pleaseEnterRefreshToken: '请输入 Refresh Token', + pleaseEnterSessionToken: '请输入 Session Token' + }, + grok: { + title: 'Grok 账号授权', + followSteps: '请按照以下步骤授权您的 xAI/Grok 账号:', + step1GenerateUrl: '生成 xAI 授权链接', + generateAuthUrl: '生成授权链接', + step2OpenUrl: '在浏览器中打开链接并完成授权', + openUrlDesc: '在新标签页中打开授权链接,登录 xAI 并授权 API 访问。', + importantNotice: '当浏览器跳转到本地 callback URL 后,请复制完整 URL 或 code 参数回填到这里。', + step3EnterCode: '输入授权链接或 Code', + authCodeDesc: '授权完成后,粘贴 callback URL、查询字符串或授权码:', + authCode: '授权链接或 Code', + authCodePlaceholder: '粘贴完整 callback URL、?code=... 查询字符串或 code 值', + authCodeHint: '支持完整 callback URL、查询字符串或裸 code。', + refreshTokenAuth: '手动输入 RT', + refreshTokenDesc: '输入已有的 xAI refresh token,支持批量输入(每行一个)。', + refreshTokenPlaceholder: '粘贴您的 xAI refresh token...\n支持多个,每行一个', + validating: '验证中...', + validateAndCreate: '验证并创建账号', + pleaseEnterRefreshToken: '请输入 Refresh Token', + failedToGenerateUrl: '生成 Grok 授权链接失败', + missingExchangeParams: '缺少授权码、state 或 OAuth 会话', + failedToExchangeCode: 'Grok 授权码兑换失败', + failedToValidateRT: '验证 Grok refresh token 失败', + oauthOnlyHint: '首版 Grok 支持仅包含 OAuth 订阅的 Responses API 文本/推理转发。' + }, + // Gemini specific + gemini: { + title: 'Gemini 账户授权', + followSteps: '请按照以下步骤完成 Gemini 账户的授权:', + step1GenerateUrl: '生成授权链接', + generateAuthUrl: '生成授权链接', + projectIdLabel: 'Project ID(可选)', + projectIdPlaceholder: '例如:my-gcp-project 或 cloud-ai-companion-xxxxx', + projectIdHint: + '留空则在兑换授权码后自动探测;若自动探测失败,可填写后重新生成授权链接再授权。', + howToGetProjectId: '如何获取', + step2OpenUrl: '在浏览器中打开链接并完成授权', + openUrlDesc: '请在新标签页中打开授权链接,登录您的 Google 账户并授权。', + step3EnterCode: '输入回调链接或 Code', + authCodeDesc: + '授权完成后,复制浏览器跳转后的回调链接(推荐)或仅复制 code,粘贴到下方即可。', + authCode: '回调链接或 Code', + authCodePlaceholder: '方式1(推荐):粘贴回调链接\n方式2:仅粘贴 code 参数的值', + authCodeHint: '系统会自动从链接中解析 code/state。', + redirectUri: 'Redirect URI', + redirectUriHint: '需要在 Google OAuth Client 中配置,且必须与此处完全一致。', + confirmRedirectUri: '我已在 Google OAuth Client 中配置了该 Redirect URI(必须完全一致)', + invalidRedirectUri: 'Redirect URI 必须是合法的 http(s) URL', + redirectUriNotConfirmed: '请确认 Redirect URI 已在 Google OAuth Client 中正确配置', + missingRedirectUri: '缺少 Redirect URI', + failedToGenerateUrl: '生成 Gemini 授权链接失败', + missingExchangeParams: '缺少 code / session_id / state', + failedToExchangeCode: 'Gemini 授权码兑换失败', + missingProjectId: + 'GCP Project ID 获取失败:您的 Google 账号未关联有效的 GCP 项目。请前往 Google Cloud Console 激活 GCP 并绑定信用卡,或在授权时手动填写 Project ID。', + modelPassthrough: 'Gemini 直接转发模型', + modelPassthroughDesc: '所有模型请求将直接转发至 Gemini API,不进行模型限制或映射。', + stateWarningTitle: '提示', + stateWarningDesc: '建议粘贴完整回调链接(包含 code 和 state)。', + oauthTypeLabel: 'OAuth 类型', + needsProjectId: '内置授权(Code Assist)', + needsProjectIdDesc: '需要 GCP 项目与 Project ID', + noProjectIdNeeded: '自定义授权(AI Studio)', + noProjectIdNeededDesc: '需管理员配置 OAuth Client', + aiStudioNotConfiguredShort: '未配置', + aiStudioNotConfiguredTip: + 'AI Studio OAuth 未配置:请先设置 GEMINI_OAUTH_CLIENT_ID / GEMINI_OAUTH_CLIENT_SECRET,并在 Google OAuth Client 添加 Redirect URI:http://localhost:1455/auth/callback(Consent Screen scopes 需包含 https://www.googleapis.com/auth/generative-language.retriever)', + aiStudioNotConfigured: + 'AI Studio OAuth 未配置:请先设置 GEMINI_OAUTH_CLIENT_ID / GEMINI_OAUTH_CLIENT_SECRET,并在 Google OAuth Client 添加 Redirect URI:http://localhost:1455/auth/callback' + }, + // Antigravity specific + antigravity: { + title: 'Antigravity 账户授权', + followSteps: '请按照以下步骤完成 Antigravity 账户的授权:', + step1GenerateUrl: '生成授权链接', + generateAuthUrl: '生成授权链接', + step2OpenUrl: '在浏览器中打开链接并完成授权', + openUrlDesc: '请在新标签页中打开授权链接,登录您的 Google 账户并授权。', + importantNotice: + '重要提示:授权后页面可能会加载较长时间,请耐心等待。当浏览器地址栏变为 http://localhost... 开头时,表示授权已完成。', + step3EnterCode: '输入授权链接或 Code', + authCodeDesc: + '授权完成后,当页面地址变为 http://localhost:xxx/auth/callback?code=... 时:', + authCode: '授权链接或 Code', + authCodePlaceholder: + '方式1:复制完整的链接\n(http://localhost:xxx/auth/callback?code=...)\n方式2:仅复制 code 参数的值', + authCodeHint: '您可以直接复制整个链接或仅复制 code 参数值,系统会自动识别', + failedToGenerateUrl: '生成 Antigravity 授权链接失败', + missingExchangeParams: '缺少 code / session_id / state', + failedToExchangeCode: 'Antigravity 授权码兑换失败', + // Refresh Token auth + refreshTokenAuth: '手动输入 RT', + refreshTokenDesc: '输入您已有的 Antigravity Refresh Token,支持批量输入(每行一个),系统将自动验证并创建账号。', + refreshTokenPlaceholder: '粘贴您的 Antigravity Refresh Token...\n支持多个,每行一个', + validating: '验证中...', + validateAndCreate: '验证并创建账号', + pleaseEnterRefreshToken: '请输入 Refresh Token', + failedToValidateRT: '验证 Refresh Token 失败' + } + }, + // Gemini specific (platform-wide) + gemini: { + helpButton: '使用帮助', + helpDialog: { + title: 'Gemini 使用指南', + apiKeySection: 'API Key 相关链接' + }, + modelPassthrough: 'Gemini 直接转发模型', + modelPassthroughDesc: '所有模型请求将直接转发至 Gemini API,不进行模型限制或映射。', + baseUrlHint: '留空使用官方 Gemini API', + apiKeyHint: '您的 Gemini API Key(以 AIza 开头)', + tier: { + label: '账号等级', + hint: '提示:系统会优先尝试自动识别账号等级;若自动识别不可用或失败,则使用你选择的等级作为回退(本地模拟配额)。', + aiStudioHint: + 'AI Studio 的配额是按模型分别限流(Pro/Flash 独立)。若已绑卡(按量付费),请选 Pay-as-you-go。', + googleOne: { + free: 'Google One Free', + pro: 'Google One Pro', + ultra: 'Google One Ultra' + }, + gcp: { + standard: 'GCP Standard', + enterprise: 'GCP Enterprise' + }, + aiStudio: { + free: 'Google AI Free', + paid: 'Google AI Pay-as-you-go' + } + }, + accountType: { + oauthTitle: 'OAuth 授权(Gemini)', + oauthDesc: '使用 Google 账号授权,并选择 OAuth 子类型。', + apiKeyTitle: 'API 密钥(AI Studio)', + apiKeyDesc: '最快接入方式,使用 AIza API Key。', + apiKeyNote: '适合轻量测试。免费层限流严格,数据可能用于训练。', + apiKeyLink: '获取 API Key', + quotaLink: '配额说明' + }, + oauthType: { + builtInTitle: '内置授权(Gemini CLI / Code Assist)', + builtInDesc: '使用 Google 内置客户端 ID,无需管理员配置。', + builtInRequirement: '需要 GCP 项目并填写 Project ID。', + googleOneDesc: '个人账号,享受 Google One 订阅配额', + codeAssistDesc: '企业级,需要 GCP 项目', + codeAssistRequirement: '需要激活 GCP 项目并绑定信用卡', + showAdvanced: '显示高级选项(自建 OAuth Client)', + hideAdvanced: '隐藏高级选项(自建 OAuth Client)', + gcpProjectLink: '创建项目', + customTitle: '自定义授权(AI Studio OAuth)', + customDesc: '使用管理员预设的 OAuth 客户端,适合组织管理。', + customRequirement: '需管理员配置 Client ID 并加入测试用户白名单。', + badges: { + recommended: '推荐', + highConcurrency: '高并发', + individuals: '推荐个人用户', + noGcp: '无需 GCP', + enterprise: '企业用户', + noAdmin: '无需管理员配置', + orgManaged: '组织管理', + adminRequired: '需要管理员' + } + }, + setupGuide: { + title: 'Gemini 使用准备', + checklistTitle: '准备工作', + checklistItems: { + usIp: '使用美国 IP,并确保账号归属地为美国。', + age: '账号需满 18 岁。' + }, + activationTitle: '服务激活', + activationItems: { + geminiWeb: '激活 Gemini Web,避免 User not initialized。', + gcpProject: '激活 GCP 项目,获取 Code Assist 所需 Project ID。' + }, + links: { + countryCheck: '检查归属地', + countryChange: '修改归属地', + geminiWebActivation: '激活 Gemini Web', + gcpProject: '打开 GCP 控制台' + } + }, + quotaPolicy: { + title: 'Gemini 配额与限流政策(参考)', + note: '注意:Gemini 官方未提供用量查询接口。此处显示的“每日配额”是由系统根据账号等级模拟计算的估算值,仅供调度参考,请以 Google 官方实际报错为准。', + columns: { + channel: '授权通道', + account: '账号状态', + limits: '限流政策', + docs: '官方文档' + }, + docs: { + codeAssist: 'Code Assist 配额', + aiStudio: 'AI Studio 定价', + vertex: 'Vertex AI 配额' + }, + simulatedNote: '本地模拟配额,仅供参考', + rows: { + googleOne: { + channel: 'Google One OAuth(个人版 / Code Assist for Individuals)', + limitsFree: '共享池:1000 RPD / 60 RPM(不分模型)', + limitsPro: '共享池:1500 RPD / 120 RPM(不分模型)', + limitsUltra: '共享池:2000 RPD / 120 RPM(不分模型)' + }, + gcp: { + channel: 'GCP Code Assist OAuth(企业版)', + limitsStandard: '共享池:1500 RPD / 120 RPM(不分模型)', + limitsEnterprise: '共享池:2000 RPD / 120 RPM(不分模型)' + }, + cli: { + channel: 'Gemini CLI(官方 Google 登录 / Code Assist)', + free: '免费 Google 账号', + premium: 'Google One AI Premium', + limitsFree: 'RPD ~1000;RPM ~60(软限制)', + limitsPremium: 'RPD ~1500+;RPM ~60+(优先队列)' + }, + gcloud: { + channel: 'GCP Code Assist(gcloud 登录)', + account: '未购买 Code Assist 订阅', + limits: 'RPD ~1000;RPM ~60(预览期)' + }, + aiStudio: { + channel: 'AI Studio API Key / OAuth', + free: '未绑卡(免费层)', + paid: '已绑卡(按量付费)', + limitsFree: 'RPD 50;RPM 2(Pro)/ 15(Flash)', + limitsPaid: 'RPD 不限;RPM 1000(Pro)/ 2000(Flash)(按模型配额)' + }, + customOAuth: { + channel: 'Custom OAuth Client(GCP)', + free: '项目未绑卡', + paid: '项目已绑卡', + limitsFree: 'RPD 50;RPM 2(项目配额)', + limitsPaid: 'RPD 不限;RPM 1000+(项目配额)' + } + } + }, + rateLimit: { + ok: '未限流', + unlimited: '无限流', + limited: '限流 {time}', + now: '现在' + } + }, + // Re-Auth Modal + reAuthorizeAccount: '重新授权账号', + claudeCodeAccount: 'Claude Code 账号', + openaiAccount: 'OpenAI 账号', + geminiAccount: 'Gemini 账号', + antigravityAccount: 'Antigravity 账号', + grokAccount: 'Grok 账号', + inputMethod: '输入方式', + reAuthorizedSuccess: '账号重新授权成功', + // Test Modal + testAccountConnection: '测试账号连接', + account: '账号', + readyToTest: '准备测试。点击"开始测试"按钮开始...', + connectingToApi: '连接 API 中...', + testCompleted: '测试完成!', + connectedToApi: '已连接到 API', + usingModel: '使用模型:{model}', + sendingTestMessage: '发送测试消息:"hi"', + sendingImageRequest: '发送生图测试请求...', + response: '响应:', + startTest: '开始测试', + retry: '重试', + copyOutput: '复制输出', + outputCopied: '输出已复制', + startingTestForAccount: '开始测试账号:{name}', + testAccountTypeLabel: '账号类型:{type}', + selectTestModel: '选择测试模型', + testModel: '测试模型', + testPrompt: '提示词:"hi"', + imagePromptLabel: '生图提示词', + imagePromptPlaceholder: '例如:生成一只戴宇航员头盔的橘猫,像素插画风格,纯色背景。', + imagePromptDefault: 'Generate a cute orange cat astronaut sticker on a clean pastel background.', + imageTestHint: '选择图片模型后,这里会直接发起生图测试,并在下方展示返回图片。', + imageTestMode: '模式:生图测试', + imagePreview: '生成结果:', + imageReceived: '已收到第 {count} 张测试图片', + // Stats Modal + viewStats: '查看统计', + usageStatistics: '使用统计', + last30DaysUsage: '近30天使用统计(日均基于实际使用天数)', + stats: { + totalCost: '30天总费用', + accumulatedCost: '累计成本', + standardCost: '标准计费', + totalRequests: '30天总请求', + totalCalls: '累计调用次数', + avgDailyCost: '日均费用', + basedOnActualDays: '基于 {days} 天实际使用', + avgDailyRequests: '日均请求', + avgDailyUsage: '平均每日调用', + todayOverview: '今日概览', + cost: '费用', + requests: '请求', + tokens: 'Token', + highestCostDay: '最高费用日', + highestRequestDay: '最高请求日', + date: '日期', + accumulatedTokens: '累计 Token', + totalTokens: '30天总计', + dailyAvgTokens: '日均 Token', + performance: '性能', + avgResponseTime: '平均响应', + daysActive: '活跃天数', + recentActivity: '最近统计', + todayRequests: '今日请求', + todayTokens: '今日 Token', + todayCost: '今日费用', + usageTrend: '30天费用与请求趋势', + noData: '该账号暂无使用数据' + } + }, + + // Scheduled Tests +} diff --git a/frontend/src/i18n/locales/zh/admin/channels.ts b/frontend/src/i18n/locales/zh/admin/channels.ts new file mode 100644 index 0000000000..036f3f77e0 --- /dev/null +++ b/frontend/src/i18n/locales/zh/admin/channels.ts @@ -0,0 +1,712 @@ +export default { + availableChannels: { + title: '可用渠道', + description: '按渠道聚合查看关联分组与支持模型(已展开通配符)', + searchPlaceholder: '搜索渠道或模型...', + columns: { + name: '渠道名', + status: '状态', + billingSource: '计费模型来源', + groups: '关联分组', + supportedModels: '支持模型' + }, + empty: '暂无数据', + noGroups: '未关联分组', + noModels: '未配置模型映射', + noPricing: '未配置定价', + statusActive: '启用', + statusDisabled: '停用', + billingSource: { + requested: '请求模型', + upstream: '上游模型', + channel_mapped: '映射后模型' + }, + pricing: { + billingMode: '计费模式', + billingModeToken: '按 Token', + billingModePerRequest: '按次', + billingModeImage: '按图片', + inputPrice: '输入', + outputPrice: '输出', + cacheWritePrice: '缓存写入', + cacheReadPrice: '缓存读取', + imageOutputPrice: '图片输出', + perRequestPrice: '每次请求', + intervals: '阶梯定价', + unitPerMillion: '/ 1M token', + unitPerRequest: '/ 次' + } + }, + + // Channel Management + channels: { + title: '渠道管理', + description: '管理渠道和自定义模型定价', + searchChannels: '搜索渠道...', + createChannel: '创建渠道', + editChannel: '编辑渠道', + deleteChannel: '删除渠道', + statusActive: '启用', + statusDisabled: '停用', + allStatus: '全部状态', + groupsUnit: '个分组', + pricingUnit: '条定价', + noChannelsYet: '暂无渠道', + createFirstChannel: '创建第一个渠道来管理模型定价', + loadError: '加载渠道列表失败', + createSuccess: '渠道创建成功', + updateSuccess: '渠道更新成功', + deleteSuccess: '渠道删除成功', + createError: '创建渠道失败', + updateError: '更新渠道失败', + deleteError: '删除渠道失败', + nameRequired: '请输入渠道名称', + duplicateModels: '模型「{0}」在多个定价条目中重复', + modelConflict: "模型模式 '{model1}' 和 '{model2}' 冲突:匹配范围重叠。模型名称按大小写不敏感匹配,已有条目已覆盖其所有大小写变体,无需重复添加。", + mappingConflict: "模型映射源 '{model1}' 和 '{model2}' 冲突:匹配范围重叠。源模式按大小写不敏感匹配,已有条目已覆盖其所有大小写变体。", + intervalValidation: { + negativeMin: '区间 #{index}:最小 token 数({value})不能为负数', + maxPositive: '区间 #{index}:最大 token 数({value})必须大于 0', + maxGreaterThanMin: '区间 #{index}:最大 token 数({max})必须大于最小 token 数({min})', + negativePrice: '区间 #{index}:{field}不能为负数', + unboundedLast: '区间 #{index}:无上限区间(最大 token 数为空)必须放在最后', + overlap: '区间 #{previousIndex} 和 #{currentIndex} 重叠:前一个上界({previousMax})大于当前下界({currentMin})', + price: { + inputPrice: '输入价格', + outputPrice: '输出价格', + cacheWritePrice: '缓存写入价格', + cacheReadPrice: '缓存读取价格', + perRequestPrice: '单次价格' + } + }, + deleteConfirm: '确定要删除渠道「{name}」吗?此操作不可撤销。', + columns: { + name: '名称', + description: '描述', + status: '状态', + groups: '分组', + pricing: '定价', + createdAt: '创建时间', + actions: '操作' + }, + billingMode: { + token: 'Token', + perRequest: '按次', + image: '图片(按次)' + }, + form: { + name: '名称', + namePlaceholder: '输入渠道名称', + description: '描述', + descriptionPlaceholder: '可选描述', + status: '状态', + groups: '关联分组', + noGroupsAvailable: '暂无可用分组', + inOtherChannel: '已属于「{name}」', + modelPricing: '模型定价', + models: '模型列表', + modelsPlaceholder: '输入完整模型名后按回车添加', + modelInputHint: '按回车添加,支持粘贴批量导入', + billingMode: '计费模式', + defaultPrices: '默认价格(未命中区间时使用)', + inputPrice: '输入', + outputPrice: '输出', + cacheWritePrice: '缓存写入', + cacheReadPrice: '缓存读取', + cacheWritePriceShort: '缓存写', + cacheReadPriceShort: '缓存读', + imageTokenPrice: '图片输出', + imageOutputPrice: '图片输出价格', + pricePlaceholder: '默认', + intervals: '上下文区间定价(可选)', + minTokens: '最小', + maxTokens: '最大', + inclusive: '(含)', + addInterval: '添加区间', + requestTiers: '按次计费层级', + imageTiers: '图片计费层级(按次)', + addTier: '添加层级', + noTiersYet: '暂无层级,点击添加配置按次计费价格', + noPricingRules: '暂无定价规则,点击"添加"创建', + perRequestPrice: '单次价格', + perRequestPriceRequired: '按次/图片计费模式必须设置默认价格或至少一个计费层级', + tierLabel: '层级', + resolution: '分辨率', + modelMapping: '模型映射', + modelMappingHint: '将请求中的模型名映射为实际模型名。在账号级别映射之前执行。', + noMappingRules: '暂无映射规则,点击"添加"创建', + mappingSource: '源模型', + mappingTarget: '目标模型', + billingModelSource: '计费基准', + billingModelSourceChannelMapped: '以渠道映射后的模型计费', + billingModelSourceRequested: '以请求模型计费', + billingModelSourceUpstream: '以最终模型计费', + billingModelSourceHint: '控制使用哪个模型名称进行定价查找', + selectedCount: '已选 {count} 个', + searchGroups: '搜索分组...', + noGroupsMatch: '没有匹配的分组', + restrictModels: '限制模型', + restrictModelsHint: '开启后,仅允许模型定价列表中的模型。不在列表中的模型请求将被拒绝。', + defaultPerRequestPrice: '默认单次价格(未命中层级时使用)', + defaultImagePrice: '默认图片价格(未命中层级时使用)', + platformConfig: '平台配置', + webSearchEmulation: 'Web Search 模拟', + webSearchEmulationHint: '⚠️ 开启后该渠道下所有 Anthropic 分组的账号将自动拦截 web_search 请求,请谨慎操作', + webSearchEmulationGlobalDisabled: '请先在系统设置 → 网关 → Web Search 模拟中启用全局开关', + codexImageGenerationBridge: 'Codex 图片生成桥接', + codexImageGenerationBridgeHint: '开启后,OpenAI 分组的 Codex /responses 文本请求可能会被自动注入 image_generation 工具。仅在路由账号支持图片生成时开启。', + bedrockCCCompat: 'Bedrock CC 兼容', + bedrockCCCompatHint: '⚠️ 开启后,该渠道下 Bedrock 账号的请求将进行 Claude Code 兼容处理(thinking 类型转换、tool_use ID 清理)', + basicSettings: '基础设置', + addPlatform: '添加平台', + noPlatforms: '点击"添加平台"开始配置渠道', + mappingCount: '条映射', + pricingEntry: '定价配置', + noModels: '未添加模型', + applyPricingToAccountStats: '应用模型定价到账号统计', + applyPricingToAccountStatsDesc: '启用后,未被自定义规则匹配的请求将使用模型定价文件中的标准价格计算账号统计费用', + accountStatsPricingRules: '自定义账号统计定价规则', + addRule: '添加规则', + noRulesConfigured: '未配置自定义规则,将使用上方的模型定价。', + ruleName: '规则名称(可选)', + ruleGroups: '分组', + ruleAccounts: '账号', + searchAccountPlaceholder: '搜索账号...', + ruleAccountsHint: '留空表示匹配所有账号', + ruleModelPricing: '模型定价', + noGroupsInChannel: '上方平台标签页中未选择分组', + unnamed: '未命名', + syncLatestModels: '同步最新模型', + syncingModels: '同步中...', + syncModelsSuccess: '已同步 {count} 个新模型', + syncModelsAlreadyUpToDate: '模型列表已是最新', + syncModelsError: '同步模型失败' + } + }, + + riskControl: { + title: '风控中心', + description: '配置内容审计策略并查看审核记录', + loadFailed: '加载风控中心失败', + saveFailed: '保存内容审计配置失败', + logsFailed: '加载审核记录失败', + saved: '内容审计配置已保存', + refresh: '刷新', + config: '内容审计配置', + configHint: '调用 OpenAI Moderations 进行请求内容评分,命中阈值后按模式处理。', + openSettings: '内容审计设置', + settingsTitle: '内容审计设置', + refreshStatus: '刷新状态', + records: '审核记录', + recordsHint: '展示命中、拦截、异常和已采样记录。', + saveConfig: '保存内容审计配置', + statusFailed: '加载运行状态失败', + enabled: '开启内容审计', + enabledHint: '关闭后即使风控中心菜单启用,也不会审核网关请求。', + mode: '全局模式', + modePreBlock: '前置拦截', + modePreBlockDesc: '每次请求先同步审核最新用户输入,命中后立即拒绝请求。', + modeObserve: '仅观察', + modeObserveDesc: '请求直接放行,最新用户输入进入异步审核队列;命中后只记录、通知和按规则累计。', + modeOff: '关闭', + modeOffDesc: '不执行内容审计,也不会写入审核记录。', + baseUrl: 'OpenAI Base URL', + model: '模型名', + apiKey: 'OpenAI API Key', + apiKeys: 'OpenAI API Keys', + apiKeyCount: '{count} 个 Key', + apiKeyPlaceholder: '请输入 API Key', + apiKeysPlaceholder: '新增 API Key,每行一个;保存后会追加到已保存 Key', + apiKeysPlaceholderReplace: '覆盖保存 API Key,每行一个;保存后会替换全部已保存 Key', + apiKeysPlaceholderKeep: '新增 API Key,每行一个;保存后会追加到已保存 Key', + apiKeysHint: '当前已保存 {count} 个 Key;输入区只用于新增,保存时会增量追加并自动去重。', + apiKeysWriteMode: '写入方式', + apiKeysModeAppend: '增量添加', + apiKeysModeReplace: '覆盖保存', + apiKeysModeAppendHint: '默认模式:保存时追加输入区 Key,并保留已保存 Key。', + apiKeysModeReplaceHint: '覆盖模式:保存时用输入区 Key 替换全部已保存 Key。', + apiKeysReplaceWarning: '覆盖模式', + apiKeysReplaceNoInput: '覆盖保存至少需要输入 1 个 API Key', + apiKeyPlaceholderKeep: '留空保持不变', + apiKeyWillClear: '保存后清除已配置 Key', + apiKeyConfigured: '已配置', + apiKeyTemporary: '待保存', + apiKeyPendingDelete: '待删除', + apiKeyPendingDeleteCount: '待删除 {count} 个 Key', + deleteApiKey: '删除这个 Key', + undoDeleteApiKey: '撤销删除', + inputApiKeyCount: '输入区 {count} 个 Key', + storedApiKeyCount: '已保存 {count} 个 Key', + testInputApiKeys: '测试输入区 Key', + testStoredApiKeys: '测试已保存 Key', + testContentWithStoredApiKey: '用已保存 Key 试跑内容', + testingApiKeys: '测试中', + apiKeyTestNoInput: '请先输入需要测试的 OpenAI API Key', + apiKeyTestDone: 'Key 测试完成,共 {count} 个', + apiKeyTestFailed: '测试 OpenAI API Key 失败', + apiKeyHealth: 'Key 可用状态', + apiKeyFreezeRule: '400 不冻结;401/403 冻结 10 分钟;429/529 冻结 1 分钟;其他 HTTP 错误冻结 10 秒。', + apiKeyRows: '{count} 个 Key', + apiKeyRowsCollapsed: '已隐藏 {count} 个 Key', + apiKeyRowsExpanded: '正在显示全部 {count} 个 Key', + expandApiKeyRows: '展开', + collapseApiKeyRows: '收起', + apiKeyHealthEmpty: '暂无 Key 状态', + apiKeyHealthEmptyHint: '保存 Key 或测试输入区 Key 后会显示可用性。', + apiKeyStatusOk: '可用', + apiKeyStatusError: '异常', + apiKeyStatusFrozen: '冻结', + apiKeyStatusUnknown: '未测试', + apiKeyFailureCount: '失败 {count} 次', + apiKeyLatency: '{ms} ms', + apiKeyHTTPStatus: 'HTTP {status}', + apiKeyFrozenUntil: '冻结至 {time}', + apiKeyLastChecked: '检查于 {time}', + apiKeyNotTested: '尚未测试', + auditTestInput: '审计试跑输入', + auditTestInputHint: '可填写提示词并上传或粘贴图片;图片以 base64 发送,不会保存文件。', + auditTestPromptPlaceholder: '输入要测试的用户提示词;留空时仅测试 Key 可用性。', + auditTestImages: '测试图片', + auditTestImagesHint: '支持上传、拖拽或粘贴图片,最多 1 张,每张不超过 8MB。', + addAuditTestImage: '添加图片', + clearAuditTest: '清空试跑', + auditTestImageLimit: '最多只能添加 {count} 张测试图片', + auditTestImageTooLarge: '单张测试图片不能超过 8MB', + auditTestImageReadFailed: '读取测试图片失败', + auditTestResult: '审计试跑结果', + auditTestHighest: '最高分类 {category},分数 {score}', + auditTestComposite: '综合评分', + auditTestFlagged: '命中阈值', + auditTestPassed: '未命中', + notConfigured: '未配置', + clearApiKey: '清除已保存 Key', + keepApiKey: '保留已保存 Key', + timeoutMs: 'HTTP 超时 (ms)', + retryCount: '失败重试次数', + sampleRate: '采样率', + recordNonHits: '记录未命中输入', + recordNonHitsHint: '开启后会记录抽样但未命中的请求摘要,摘要会先脱敏再入库。', + preHashCheck: '启用前置哈希比对', + preHashCheckHint: '异步审核命中过的输入哈希会被前置拦截;该拦截不发送邮件,也不累计封禁次数。', + flaggedHashCount: '当前哈希集合数量:{count} 个', + flaggedHashHint: '哈希永久保存在 Redis 集合中;可粘贴完整 64 位哈希删除误拦截项,或一键清空全部风险哈希。', + flaggedHashPlaceholder: '粘贴完整 64 位输入哈希', + deleteFlaggedHash: '删除指定哈希', + clearFlaggedHashes: '一键清空', + clearFlaggedHashesConfirm: '确定要清空全部风险输入哈希吗?此操作不会删除审核记录,但会取消所有历史哈希拦截。', + flaggedHashDeleted: '风险哈希已删除', + flaggedHashNotFound: '该风险哈希不存在', + flaggedHashDeleteFailed: '删除风险哈希失败', + flaggedHashesCleared: '已清空 {count} 个风险哈希', + flaggedHashesClearFailed: '清空风险哈希失败', + workerCount: 'Worker 数', + queueSize: '异步队列大小', + blockStatus: '拦截 HTTP 状态码', + blockMessage: '自定义拦截提示', + defaultBlockMessage: '内容审计命中风险规则,请调整输入后重试', + emailOnHit: '命中后发送邮件', + emailOnHitHint: '开启后每次达到阈值都会向用户发送风控提醒邮件;自动封禁通知始终发送。', + autoBan: '自动封禁用户', + autoBanHint: '命中次数达到阈值后将禁用用户账号、刷新认证缓存并发送封禁通知邮件。', + cyberPolicyExcludeBan: 'cyber_policy 不计入封号次数', + cyberPolicyExcludeBanHint: '开启后,cyber_policy 拦截不再计入自动封号的违规次数:当次不判定封号,历史累计亦排除。风控日志与通知邮件照常。', + violationNotCounted: '未计入封号', + banThreshold: '封禁触发次数', + violationWindowHours: '累计窗口(小时)', + hitRetentionDays: '命中记录保留(天)', + nonHitRetentionDays: '未命中记录保留(天,最多 3 天)', + violationCount: '{count} 次', + emailSent: '已发邮件', + emailNotSent: '未发邮件', + autoBanned: '已封禁', + unbanUser: '解封', + unbanSuccess: '用户已解封', + unbanFailed: '解封用户失败', + inputDetailTitle: '输入摘要详情', + inputDetailContent: '完整内容', + matchedKeyword: '命中关键词', + queueDelay: '排队 {ms} ms', + allGroups: '全部分组', + allGroupsHint: '当前审计全部分组', + selectedGroupsHint: '当前审计指定分组', + groupScope: '审计分组', + groupScopeHint: '开启右侧开关表示全部分组,关闭后选择指定分组。', + selectedGroups: '指定分组', + searchGroups: '搜索分组名称或平台', + noGroups: '暂无可用分组', + modelFilter: '模型范围', + modelFilterHint: '按客户端请求的模型名决定是否执行内容审计,模型映射后仍以请求模型判断。', + modelFilterAll: '所有模型', + modelFilterAllDesc: '所有模型请求都会进入内容审计。', + modelFilterInclude: '仅指定模型', + modelFilterIncludeDesc: '只有列表中的模型会执行内容审计。', + modelFilterExclude: '排除指定模型', + modelFilterExcludeDesc: '列表中的模型跳过内容审计,其余模型执行审计。', + modelFilterModels: '模型列表', + modelFilterModelCount: '已配置 {count} 个模型', + modelFilterModelsRequired: '当前模型范围至少需要配置 1 个模型', + modelFilterAllSummary: '全部模型生效', + modelFilterIncludeSummary: '仅 {count} 个模型生效', + modelFilterExcludeSummary: '排除 {count} 个模型', + emptyLogs: '暂无审核记录', + preBlockSyncStatus: '前置拦截同步状态', + preBlockSyncHint: '同步审核链路的实时计数,不包含异步写记录任务。', + preBlockActive: '同步处理中', + preBlockActiveHint: '当前正在审核', + preBlockChecked: '已检查', + preBlockCheckedHint: '进入前置拦截链路', + preBlockAllowed: '已放行', + preBlockAllowedHint: '未触发拦截', + preBlockBlocked: '已拦截', + preBlockBlockedHint: '命中后拒绝请求', + preBlockErrors: '审核异常', + preBlockErrorsHint: '失败或无可用 Key', + preBlockAvgLatency: '平均耗时', + preBlockAvgLatencyHint: '同步链路平均值', + preBlockAPIKeyLoad: '审核 Key 负载', + preBlockAPIKeyLoadHint: '同步前置拦截直接轮询可用审核 Key。', + preBlockAPIKeyLoadSummary: '同步并发 {active} / 可用 Key {available},累计 {total} 次,worker:{workerActive} / {workerTotal}', + preBlockAPIKeyTotals: '累计 {total},成功 {success},异常 {errors}', + preBlockAPIKeyLoadEmpty: '暂无审核 Key 负载数据', + preBlockKeyActiveShort: '并发', + preBlockKeyTotalShort: '累计', + preBlockKeyAvgShort: '平均', + preBlockKeyLastShort: '最近', + workerStatus: 'Worker 运行状态', + workerStatusHint: '异步审计任务和前置拦截记录任务的队列与 Worker 池状态,不包含同步前置拦截审核请求。', + workerPool: 'Worker 池', + workerPoolMeta: '{active} 个处理中,{idle} 个空闲可用,共 {total} 个', + queueUsage: '队列占用', + activeWorkers: '处理中', + idleWorkers: '空闲可用', + workerActive: '正在处理异步审计或记录任务', + workerIdle: '已启动,当前空闲可用', + workerDisabled: '风控或内容审计未启用', + processed: '已处理', + droppedErrors: '丢弃/异常', + autoRefresh: '每 15 秒自动刷新', + lastCleanup: '上次清理:{time}', + cleanupStats: '上次清理删除命中 {hit} 条,未命中 {nonHit} 条', + riskSwitchOff: '系统开关关闭', + riskThresholds: '风险阈值', + riskThresholdsHint: '按 OpenAI Moderations 分类调整命中阈值,分数达到或超过阈值即视为命中。', + riskThresholdDefault: '默认 {value}', + riskThresholdReset: '恢复默认阈值', + riskThresholdPercent: '阈值百分比', + tabs: { + basic: '基础', + scope: '审计范围', + runtime: '运行队列', + response: '命中通知', + riskThresholds: '风险阈值', + keywords: '关键词拦截', + retention: '日志保留', + }, + blockedKeywords: '拦截关键词', + blockedKeywordsPlaceholder: '每行输入一个关键词,例如:\n敏感词1\n敏感词2', + blockedKeywordsDescription: '匹配忽略大小写;命中后会按下方策略决定是否调用上游审计接口。', + blockedKeywordsPreBlockHint: '关键词拦截仅在「前置拦截」模式下生效。', + blockedKeywordsModeWarning: '当前为「{mode}」模式,关键词拦截不会生效;请切换到「前置拦截」模式后再保存关键词。', + blockedKeywordCount: '已配置 {count} 个关键词', + blockedKeywordsLimit: '最多保存 {max} 个关键词,单个长度不超过 200 个字符;重复项会自动去重。', + keywordBlockingMode: '审计策略', + keywordModeKeywordAndApi: '关键词 + API', + keywordModeKeywordAndApiDesc: '命中关键词直接拦截;未命中时再调用上游审计接口。', + keywordModeKeywordOnly: '仅关键词', + keywordModeKeywordOnlyDesc: '只用关键词判断,未命中即放行,不调用上游审计接口,可显著降低 API 用量。', + keywordModeKeywordOnlyNotice: '当前为「仅关键词」策略:未命中关键词的请求将直接放行,不调用上游审计接口。', + keywordModeApiOnly: '仅 API', + keywordModeApiOnlyDesc: '只调用上游审计接口判断,本页的关键词列表将不会生效。', + keywordModeApiOnlyNotice: '当前为「仅 API」策略:关键词列表不会生效,请求会全部交给上游审计接口判断。', + overview: { + status: '运行状态', + enabled: '已启用', + disabled: '未启用', + apiKey: 'API Key', + groupScope: '审计范围', + logs: '审核记录', + currentFilter: '当前筛选结果', + }, + filters: { + search: '按用户/Key/摘要搜索', + from: '开始时间', + to: '结束时间', + allGroups: '全部分组', + allEndpoints: '全部端点', + }, + table: { + time: '时间', + group: '分组', + user: '用户', + apiKey: 'API Key', + endpoint: '端点', + result: '结果', + highest: '最高分', + actionMeta: '处置', + latency: '上游耗时', + input: '输入摘要', + }, + result: { + all: '全部结果', + hit: '命中', + blocked: '已拦截', + pass: '未命中', + error: '异常', + }, + action: { + block: '拦截', + keywordBlock: '关键词拦截', + cyberPolicy: '网络安全策略', + error: '异常', + }, + }, + + // Channel Monitor + channelMonitor: { + title: '渠道监控', + description: '监测各渠道的可用性、延迟和状态', + searchPlaceholder: '搜索监控名称...', + allProviders: '全部供应商', + allStatus: '全部状态', + enabledFilter: '启用状态', + onlyEnabled: '仅启用', + onlyDisabled: '仅禁用', + createButton: '新增监控', + createTitle: '新增渠道监控', + editTitle: '编辑渠道监控', + runNow: '立即检测', + runSuccess: '检测完成', + runFailed: '检测失败', + apiKeyDecryptFailed: 'API Key 解密失败,请重新编辑该监控并填入新的 Key', + createSuccess: '监控创建成功', + updateSuccess: '监控更新成功', + deleteSuccess: '监控删除成功', + loadError: '加载监控列表失败', + deleteConfirm: '确定要删除监控「{name}」吗?此操作不可撤销。', + nameRequired: '请输入监控名称', + primaryModelRequired: '请输入主模型', + columns: { + name: '名称', + provider: '供应商', + primaryModel: '主模型', + availability7d: '7 天可用率', + latency: '延迟 (ms)', + enabled: '启用', + actions: '操作' + }, + form: { + name: '名称', + namePlaceholder: '输入监控名称', + provider: '平台', + apiMode: 'OpenAI 协议', + apiModeChatCompletions: 'OpenAI Compatible', + apiModeChatCompletionsHint: '使用 /v1/chat/completions,发送 messages;适合大多数兼容站。', + apiModeResponses: 'Responses API', + apiModeResponsesHint: '使用 /v1/responses,默认带 instructions + input;适合本站自检/Codex。', + endpoint: '上游地址', + endpointPlaceholder: 'https://api.example.com', + useCurrentDomain: '使用当前服务', + apiKey: 'API Key', + apiKeyPlaceholder: '请输入 API Key', + apiKeyEditPlaceholder: '留空表示不修改', + useMyKey: '使用我的 Key', + selectKeyTitle: '选择我的 API Key', + selectKeyHint: '仅显示当前账号下处于「启用」状态且未过期的 Key。', + noActiveKey: '没有可用的启用状态 Key', + primaryModel: '主模型', + primaryModelPlaceholder: 'gpt-4o-mini', + extraModels: '附加模型', + extraModelsPlaceholder: '回车添加附加模型', + groupName: '分组名称', + groupNamePlaceholder: '可选,用于在用户视图中聚合显示', + intervalSeconds: '检测间隔 (秒)', + intervalSecondsHint: '范围:15 - 3600 秒', + jitterSeconds: '随机抖动 (± 秒)', + jitterSecondsHint: '每次检测在间隔基础上正负随机偏移该秒数,0 表示固定间隔;需满足 间隔 - 抖动 ≥ 15 秒', + enabled: '启用监控', + kindRequired: '请选择供应商' + }, + runResultTitle: '检测结果', + noMonitorsYet: '暂无监控', + createFirstMonitor: '创建第一个监控来跟踪渠道可用性', + advanced: { + section: '高级(可选)', + sectionHint: '自定义请求头和请求体,用于突破上游的客户端识别限制(如仅允许 Claude Code 客户端)。', + headers: '自定义请求头', + headersPlaceholder: 'User-Agent: claude-cli/1.0.83 (external, cli)\nx-app: cli\nanthropic-beta: claude-code-20250219', + headerNamePlaceholder: 'Header 名', + headerValuePlaceholder: 'Value', + headerAddRow: '添加 Header', + headerNameInvalid: 'Header 名不能包含空格或冒号:{name}', + headersHint: '与默认请求头合并,用户值优先。hop-by-hop 类 header(Host/Content-Length/...)会被忽略。', + headersParseError: '无法解析这一行:{line}', + bodyMode: '请求体处理', + bodyModeOff: '默认', + bodyModeMerge: '合并', + bodyModeReplace: '覆盖', + bodyModeHintOff: '使用 adapter 默认请求体(带 challenge 数学题校验)。', + bodyModeHintMerge: '与默认请求体浅合并,用户字段优先;但 model / messages / contents 会被保护不允许覆盖(动这些字段请用「覆盖」模式)。', + bodyModeHintReplace: '完全用下方 JSON 作为请求体。注意:此模式下跳过 challenge 校验,改为 HTTP 2xx + 响应文本非空即视为可用。', + bodyJson: 'Body JSON', + bodyJsonFormat: '格式化', + bodyJsonHint: '失焦时自动解析校验。留空等价于没有覆盖。', + bodyJsonError: 'JSON 解析失败', + bodyJsonObjectError: '请求体必须是一个 JSON 对象(不能是数组或基本类型)' + }, + templateField: { + label: '请求模板', + none: '不使用模板', + placeholder: '选择一个模板(按当前平台过滤)', + applyHint: '选中模板后,会把模板的请求头和请求体拷贝到此监控(快照)。后续模板变动不自动同步。' + }, + template: { + manageButton: '模板管理', + managerTitle: '请求模板管理', + createButton: '新建模板', + emptyState: '当前平台下还没有请求模板', + missingName: '请输入模板名称', + createSuccess: '模板创建成功', + updateSuccess: '模板更新成功', + deleteSuccess: '模板删除成功', + applyButton: '应用到关联监控', + applyTooltip: '把当前模板配置覆盖到所有关联的监控上', + applyTitle: '应用模板', + applyConfirm: '确认应用', + applyConfirmMessage: '将把模板「{name}」的当前配置覆盖到 {n} 个关联监控。监控本地已编辑的自定义修改会被丢弃,是否继续?', + applySuccess: '已应用到 {n} 个监控', + applyPickerTitle: '应用模板「{name}」', + applyPickerHint: '勾选要覆盖请求头/请求体的监控(默认全选)。监控本地已编辑的自定义修改会被丢弃。', + applyPickerEmpty: '当前模板没有关联监控', + applyPickerConfirm: '应用到 {n} 个监控', + selectNone: '全不选', + selectedCount: '已选 {n} / {total}', + deleteConfirm: '确定要删除模板「{name}」吗?{n} 个关联监控会解除关联但保留自己的快照继续工作。', + associatedCount: '{n} 个关联监控', + headersSummary: '{n} 个自定义请求头', + form: { + name: '模板名称', + namePlaceholder: '例:Claude Code 伪装', + description: '说明', + descriptionPlaceholder: '可选:说明这个模板的用途和来源(抓包日期等)' + } + } + }, + + // Subscriptions Management + subscriptions: { + title: '订阅管理', + description: '管理用户订阅和配额限制', + assignSubscription: '分配订阅', + adjustSubscription: '调整订阅', + revokeSubscription: '撤销订阅', + restoreSubscription: '恢复订阅', + allStatus: '全部状态', + allGroups: '全部分组', + allPlatforms: '全部平台', + daily: '每日', + weekly: '每周', + monthly: '每月', + noLimits: '未配置限额', + unlimited: '无限制', + resetNow: '即将重置', + windowNotActive: '窗口未激活', + resetInMinutes: '{minutes} 分钟后重置', + resetInHoursMinutes: '{hours} 小时 {minutes} 分钟后重置', + resetInDaysHours: '{days} 天 {hours} 小时后重置', + quotaEndsInMinutes: '额度将在 {minutes} 分钟后结束', + quotaEndsInHoursMinutes: '额度将在 {hours} 小时 {minutes} 分钟后结束', + quotaEndsInDaysHours: '额度将在 {days} 天 {hours} 小时后结束', + daysRemaining: '天剩余', + remainingDays: '剩余天数', + noExpiration: '无过期时间', + status: { + active: '生效中', + expired: '已过期', + revoked: '已撤销', + suspended: '已暂停' + }, + columns: { + user: '用户', + group: '分组', + usage: '用量', + expires: '到期时间', + status: '状态', + actions: '操作' + }, + form: { + user: '用户', + group: '订阅分组', + validityDays: '有效期(天)', + adjustDays: '调整天数' + }, + selectUser: '选择用户', + selectGroup: '选择订阅分组', + groupHint: '仅显示订阅计费类型的分组', + validityHint: '订阅的有效天数', + adjustingFor: '为以下用户调整订阅', + currentExpiration: '当前到期时间', + adjustDaysPlaceholder: '正数延长,负数缩短', + adjustHint: '输入正数延长订阅,负数缩短订阅(缩短后剩余天数需大于0)', + assign: '分配', + assigning: '分配中...', + adjust: '调整', + adjusting: '调整中...', + revoke: '撤销', + restore: '恢复', + resetQuota: '重置配额', + resetQuotaTitle: '重置用量配额', + resetQuotaConfirm: "确定要重置 '{user}' 的每日、每周和每月用量配额吗?用量将归零并从今天开始重新计算。", + quotaResetSuccess: '配额重置成功', + failedToResetQuota: '重置配额失败', + noSubscriptionsYet: '暂无订阅', + assignFirstSubscription: '分配一个订阅以开始使用。', + subscriptionAssigned: '订阅分配成功', + subscriptionAdjusted: '订阅调整成功', + subscriptionRevoked: '订阅撤销成功', + subscriptionRestored: '订阅已恢复', + failedToLoad: '加载订阅列表失败', + failedToAssign: '分配订阅失败', + failedToAdjust: '调整订阅失败', + failedToRevoke: '撤销订阅失败', + failedToRestore: '恢复订阅失败', + adjustWouldExpire: '调整后剩余天数必须大于0', + adjustOutOfRange: '调整天数必须在 -36500 到 36500 之间', + pleaseSelectUser: '请选择用户', + pleaseSelectGroup: '请选择分组', + validityDaysRequired: '请输入有效的天数(至少1天)', + revokeConfirm: "确定要撤销 '{user}' 的订阅吗?可稍后在已撤销列表中恢复。", + restoreConfirm: "确定要恢复 '{user}' 的订阅吗?如果原订阅已过期,恢复后将显示为已过期。", + guide: { + title: '订阅管理教程', + subtitle: '订阅模式允许你按时间周期为用户分配使用额度,支持日/周/月配额限制。按照以下步骤即可完成配置。', + showGuide: '使用指南', + step1: { + title: '创建订阅分组', + line1: '前往「分组管理」页面,点击「创建分组」', + line2: '将计费类型设为「订阅」,配置日/周/月额度限制', + line3: '保存分组,确保状态为「正常」', + link: '前往分组管理' + }, + step2: { + title: '分配订阅给用户', + line1: '点击本页右上角「分配订阅」按钮', + line2: '在弹窗中搜索用户邮箱并选择目标用户', + line3: '选择订阅分组、设置有效期天数,点击「分配」' + }, + step3: { + title: '管理已有订阅' + }, + actions: { + adjust: '调整', + adjustDesc: '延长或缩短订阅有效期', + resetQuota: '重置配额', + resetQuotaDesc: '将日/周/月用量归零,重新开始计算', + revoke: '撤销', + revokeDesc: '立即终止该用户的订阅,可在已撤销列表中恢复' + }, + tip: '提示:订阅分组下拉列表中只会显示计费类型为「订阅」且状态为「正常」的分组。如果没有可选项,请先到分组管理中创建。' + } + }, + + // Accounts Management +} diff --git a/frontend/src/i18n/locales/zh/admin/index.ts b/frontend/src/i18n/locales/zh/admin/index.ts new file mode 100644 index 0000000000..e4e9bba438 --- /dev/null +++ b/frontend/src/i18n/locales/zh/admin/index.ts @@ -0,0 +1,15 @@ +import overview from './overview' +import channels from './channels' +import accounts from './accounts' +import resources from './resources' +import ops from './ops' +import settings from './settings' + +export default { + ...overview, + ...channels, + ...accounts, + ...resources, + ...ops, + ...settings, +} diff --git a/frontend/src/i18n/locales/zh/admin/ops.ts b/frontend/src/i18n/locales/zh/admin/ops.ts new file mode 100644 index 0000000000..97b974fd8f --- /dev/null +++ b/frontend/src/i18n/locales/zh/admin/ops.ts @@ -0,0 +1,810 @@ +export default { + ops: { + title: '运维监控', + description: '运维监控与排障', + // Dashboard + systemHealth: '系统健康', + overview: '概览', + noSystemMetrics: '尚未收集系统指标。', + collectedAt: '采集时间:', + window: '窗口', + memory: '内存', + db: '数据库', + goroutines: '协程', + jobs: '后台任务', + jobsHelp: '点击“明细”查看任务心跳与报错信息', + active: '活跃', + idle: '空闲', + waiting: '等待', + conns: '连接', + queue: '队列', + accountSwitches: '账号切换', + ok: '正常', + lastRun: '最近运行', + lastSuccess: '最近成功', + lastError: '最近错误', + result: '结果', + noData: '暂无数据', + loadingText: '加载中...', + ready: '就绪', + autoRefreshRemaining: '剩余 {seconds}s', + systemLogs: { + title: '系统日志', + description: '优先显示最新日志,可按条件筛选、搜索和清理。', + queue: '队列', + written: '已写入', + dropped: '已丢弃', + failed: '写入失败', + runtimeConfig: '运行时日志配置(立即生效)', + all: '全部', + level: '级别', + stacktraceThreshold: '堆栈阈值', + samplingInitial: '采样初始条数', + samplingThereafter: '后续采样间隔', + retentionDays: '保留天数', + caller: '调用方', + sampling: '采样', + saveAndApply: '保存并应用', + resetDefaults: '重置默认值', + latestWriteError: '最近写入错误:', + timeRange: '时间范围', + startTime: '开始时间(可选)', + endTime: '结束时间(可选)', + component: '组件', + componentPlaceholder: '例如 http.access', + keyId: 'KEY ID', + platform: '平台', + model: '模型', + keyword: '关键词', + keywordPlaceholder: 'message/request_id', + search: '搜索', + cleanCurrentFilters: '清理当前筛选结果', + refreshHealth: '刷新健康状态', + empty: '暂无系统日志', + time: '时间', + logDetails: '日志详情', + loadFailed: '加载系统日志失败', + runtimeConfigActive: '运行时日志配置已生效', + runtimeConfigSaveFailed: '保存日志配置失败', + resetRuntimeConfigConfirm: '确定要重置为启动配置(env/yaml)并立即应用吗?', + runtimeConfigReset: '已重置为启动日志配置', + runtimeConfigResetFailed: '重置日志配置失败', + cleanupConfirm: '确定要清理匹配当前筛选条件的系统日志吗?此操作不可撤销。', + cleanupSuccess: '清理完成,已删除 {count} 条日志。', + cleanupFailed: '清理系统日志失败' + }, + requestsTotal: '请求(总计)', + slaScope: 'SLA 范围:', + tokens: 'Token数', + tps: 'TPS', + current: '当前', + peak: '峰值', + average: '平均', + totalRequests: '总请求', + avgQps: '平均 QPS', + avgTps: '平均 TPS', + avgLatency: '平均请求时长', + avgTtft: '平均首 Token 延迟', + exceptions: '异常数', + requestErrors: '请求错误', + errorCount: '错误数', + upstreamErrors: '上游错误', + errorCountExcl429529: '错误数(排除429/529)', + sla: 'SLA(排除业务限制)', + businessLimited: '业务限制:', + errors: '错误', + errorRate: '错误率:', + upstreamRate: '上游错误率:', + latencyDuration: '请求时长', + ttftLabel: '首 Token 延迟(毫秒)', + p50: 'p50', + p90: 'p90', + p95: 'p95', + p99: 'p99', + avg: 'avg', + max: 'max', + requests: '请求数', + requestsTitle: '请求', + upstream: '上游', + client: '客户端', + system: '系统', + other: '其他', + errorsSla: '错误(SLA范围)', + upstreamExcl429529: '上游(排除429/529)', + failedToLoadData: '加载运维数据失败', + failedToLoadOverview: '加载概览数据失败', + failedToLoadThroughputTrend: '加载吞吐趋势失败', + failedToLoadSwitchTrend: '加载平均账号切换趋势失败', + failedToLoadLatencyHistogram: '加载请求时长分布失败', + failedToLoadErrorTrend: '加载错误趋势失败', + failedToLoadErrorDistribution: '加载错误分布失败', + failedToLoadErrorDetail: '加载错误详情失败', + retryFailed: '重试失败', + tpsK: 'TPS(千)', + top: '最高:', + throughputTrend: '吞吐趋势', + switchRateTrend: '平均账号切换趋势', + latencyHistogram: '请求时长分布', + errorTrend: '错误趋势', + errorDistribution: '错误分布', + switchRate: '平均账号切换', + // Health Score & Diagnosis + health: '健康', + healthCondition: '健康状况', + healthHelp: '基于 SLA、错误率和资源使用情况的系统整体健康评分', + healthyStatus: '健康', + riskyStatus: '风险', + idleStatus: '待机', + timeRange: { + '5m': '近5分钟', + '30m': '近30分钟', + '1h': '近1小时', + '1d': '近1天', + '15d': '近15天', + '6h': '近6小时', + '24h': '近24小时', + '7d': '近7天', + '30d': '近30天', + custom: '自定义' + }, + openaiTokenStats: { + title: 'OpenAI Token 请求统计', + viewModeTopN: 'TopN', + viewModePagination: '分页', + prevPage: '上一页', + nextPage: '下一页', + pageInfo: '第 {page}/{total} 页', + totalModels: '模型总数:{total}', + failedToLoad: '加载 OpenAI Token 统计失败', + empty: '当前筛选条件下暂无 OpenAI Token 请求统计数据', + table: { + model: '模型', + requestCount: '请求数', + avgTokensPerSec: '平均 Tokens/秒', + avgFirstTokenMs: '平均首 Token 延迟(ms)', + totalOutputTokens: '输出 Token 总数', + avgDurationMs: '平均时长(ms)', + requestsWithFirstToken: '首 Token 样本数' + } + }, + customTimeRange: { + startTime: '开始时间', + endTime: '结束时间' + }, + fullscreen: { + enter: '进入全屏' + }, + diagnosis: { + title: '智能诊断', + footer: '基于当前指标的自动诊断建议', + idle: '系统当前处于待机状态', + idleImpact: '无活跃流量', + // Resource diagnostics + dbDown: '数据库连接失败', + dbDownImpact: '所有数据库操作将失败', + dbDownAction: '检查数据库服务状态、网络连接和连接配置', + redisDown: 'Redis连接失败', + redisDownImpact: '缓存功能降级,性能可能下降', + redisDownAction: '检查Redis服务状态和网络连接', + cpuCritical: 'CPU使用率严重过高 ({usage}%)', + cpuCriticalImpact: '系统响应变慢,可能影响所有请求', + cpuCriticalAction: '检查CPU密集型任务,考虑扩容或优化代码', + cpuHigh: 'CPU使用率偏高 ({usage}%)', + cpuHighImpact: '系统负载较高,需要关注', + cpuHighAction: '监控CPU趋势,准备扩容方案', + memoryCritical: '内存使用率严重过高 ({usage}%)', + memoryCriticalImpact: '可能触发OOM,系统稳定性受威胁', + memoryCriticalAction: '检查内存泄漏,考虑增加内存或优化内存使用', + memoryHigh: '内存使用率偏高 ({usage}%)', + memoryHighImpact: '内存压力较大,需要关注', + memoryHighAction: '监控内存趋势,检查是否有内存泄漏', + ttftHigh: '首 Token 时间偏高 ({ttft}ms)', + ttftHighImpact: '用户感知时长增加', + ttftHighAction: '优化请求处理流程,减少前置逻辑耗时', + // Error rate diagnostics + upstreamCritical: '上游错误率严重偏高 ({rate}%)', + upstreamCriticalImpact: '可能影响大量用户请求', + upstreamCriticalAction: '检查上游服务健康状态,启用降级策略', + upstreamHigh: '上游错误率偏高 ({rate}%)', + upstreamHighImpact: '建议检查上游服务状态', + upstreamHighAction: '联系上游服务团队,准备降级方案', + errorHigh: '错误率过高 ({rate}%)', + errorHighImpact: '大量请求失败', + errorHighAction: '查看错误日志,定位错误根因,紧急修复', + errorElevated: '错误率偏高 ({rate}%)', + errorElevatedImpact: '建议检查错误日志', + errorElevatedAction: '分析错误类型和分布,制定修复计划', + // SLA diagnostics + slaCritical: 'SLA 严重低于目标 ({sla}%)', + slaCriticalImpact: '用户体验严重受损', + slaCriticalAction: '紧急排查错误原因,必要时采取限流保护', + slaLow: 'SLA 低于目标 ({sla}%)', + slaLowImpact: '需要关注服务质量', + slaLowAction: '分析SLA下降原因,优化系统性能', + // Health score diagnostics + healthCritical: '综合健康评分过低 ({score})', + healthCriticalImpact: '多个指标可能同时异常,建议优先排查错误与资源使用情况', + healthCriticalAction: '全面检查系统状态,优先处理critical级别问题', + healthLow: '综合健康评分偏低 ({score})', + healthLowImpact: '可能存在轻度波动,建议关注 SLA 与错误率', + healthLowAction: '监控指标趋势,预防问题恶化', + healthy: '所有系统指标正常', + healthyImpact: '服务运行稳定' + }, + // Error Log + errorLog: { + timeId: '时间 / ID', + commonErrors: { + contextDeadlineExceeded: '请求超时', + connectionRefused: '连接被拒绝', + rateLimit: '触发限流' + }, + time: '时间', + type: '类型', + context: '上下文', + platform: '平台', + model: '模型', + group: '分组', + user: '用户', + userId: '用户 ID', + apiKey: 'API Key', + keyDeletedBadge: 'Key 已删除', + account: '账号', + accountId: '账号 ID', + status: '状态码', + message: '响应内容', + ip: 'IP', + latency: '请求时长', + action: '操作', + noErrors: '该窗口内暂无错误。', + grp: 'GRP:', + acc: 'ACC:', + details: '详情', + phase: '阶段', + id: 'ID:', + typeUpstream: '上游', + typeRequest: '请求', + typeAuth: '认证', + typeRouting: '路由', + typeInternal: '内部', + endpoint: '端点', + requestType: '类型', + requestTypeSync: '同步', + requestTypeStream: '流式', + requestTypeWs: 'WS' + }, + // Error Details Modal + errorDetails: { + upstreamErrors: '上游错误', + requestErrors: '请求错误', + unresolved: '未解决', + resolved: '已解决', + viewErrors: '错误', + viewExcluded: '排除项', + statusCodeOther: '其他', + owner: { + provider: '服务商', + client: '客户端', + platform: '平台' + }, + phase: { + request: '请求', + auth: '认证', + routing: '路由', + upstream: '上游', + network: '网络', + internal: '内部' + }, + total: '总计:', + searchPlaceholder: '搜索 request_id / client_request_id / message' + }, + // Error Detail Modal + errorDetail: { + title: '错误详情', + titleWithId: '错误 #{id}', + noErrorSelected: '未选择错误。', + resolution: '已解决:', + failedToUpdateResolvedStatus: '更新解决状态失败', + classificationKeys: { + phase: '阶段', + owner: '归属方', + source: '来源', + resolvedAt: '解决时间', + resolvedBy: '解决人' + }, + source: { + upstream_http: '上游 HTTP' + }, + upstreamKeys: { + status: '状态码', + message: '消息', + detail: '详情', + upstreamErrors: '上游错误列表' + }, + upstreamEvent: { + account: '账号', + status: '状态码', + requestId: '请求ID' + }, + responsePreview: { + expand: '响应内容(点击展开)', + collapse: '响应内容(点击收起)' + }, + loading: '加载中…', + requestId: '请求 ID', + time: '时间', + phase: '阶段', + status: '状态码', + message: '消息', + basicInfo: '基本信息', + platform: '平台', + model: '模型', + group: '分组', + user: '用户', + account: '账号', + latency: '请求时长', + businessLimited: '业务限制', + requestPath: '请求路径', + inboundEndpoint: '入站端点', + upstreamEndpoint: '上游端点', + requestedModel: '请求模型', + upstreamModel: '上游模型', + requestType: '请求类型', + requestTypeUnknown: '未知', + requestTypeSync: '同步', + requestTypeStream: '流式', + requestTypeWs: 'WebSocket', + modelMapping: '模型映射', + timings: '时序信息', + auth: '认证', + routing: '路由', + upstream: '上游', + response: '响应', + classification: '错误分类', + errorBody: '错误体', + trimmed: '已截断', + markResolved: '标记已解决', + markUnresolved: '标记未解决', + tabOverview: '概览', + tabRequest: '请求详情', + tabResponse: '响应详情', + responseBody: '响应详情', + compareA: '对比 A', + compareB: '对比 B', + suggestion: '处理建议', + suggestUpstream: '⚠️ 上游服务不稳定,建议:检查上游账号状态 / 考虑切换账号', + suggestRequest: '⚠️ 客户端请求错误,建议:联系客户修正请求参数 / 手动标记已解决', + suggestAuth: '⚠️ 认证失败,建议:检查 API Key 是否有效 / 联系客户更新凭证', + suggestPlatform: '🚨 平台错误,建议立即排查修复', + suggestGeneric: '查看详情了解更多信息', + apiKeyPrefix: 'Key 前缀', + attemptedKeyPrefix: '尝试的 Key 前缀', + deletedKeyOwner: '已删除 Key 所有者', + keyDeletedBadge: 'Key 已删除' + }, + requestDetails: { + title: '请求明细', + details: '明细', + rangeLabel: '窗口:{range}', + rangeMinutes: '{n} 分钟', + rangeHours: '{n} 小时', + empty: '该窗口内暂无请求。', + emptyHint: '可尝试调整时间范围或取消部分筛选。', + failedToLoad: '加载请求明细失败', + requestIdCopied: '请求ID已复制', + copyFailed: '复制失败', + copy: '复制', + viewError: '查看错误', + kind: { + success: '成功', + error: '失败' + }, + table: { + time: '时间', + kind: '类型', + platform: '平台', + model: '模型', + duration: '耗时', + status: '状态码', + requestId: '请求ID', + actions: '操作' + } + }, + alertEvents: { + title: '告警事件', + description: '最近的告警触发/恢复记录(仅邮件通知)', + loading: '加载中...', + empty: '暂无告警事件', + loadFailed: '加载告警事件失败', + status: { + firing: '告警中', + resolved: '已恢复', + manualResolved: '手动已解决' + }, + detail: { + title: '告警详情', + loading: '加载详情中...', + empty: '暂无详情', + loadFailed: '加载告警详情失败', + manualResolve: '标记为已解决', + manualResolvedSuccess: '已标记为手动解决', + manualResolvedFailed: '标记为手动解决失败', + silence: '忽略此告警', + silenceSuccess: '已静默该告警', + silenceFailed: '静默失败', + viewRule: '查看规则', + viewLogs: '查看相关日志', + firedAt: '触发时间', + resolvedAt: '解决时间', + ruleId: '规则 ID', + dimensions: '维度信息', + historyTitle: '历史记录', + historyHint: '同一规则 + 相同维度的最近事件', + historyLoading: '加载历史中...', + historyEmpty: '暂无历史记录' + }, + table: { + time: '时间', + status: '状态', + severity: '级别', + platform: '平台', + ruleId: '规则ID', + title: '标题', + duration: '持续时间', + metric: '指标 / 阈值', + dimensions: '维度', + email: '邮件已发送', + emailSent: '已发送', + emailIgnored: '已忽略' + } + }, + alertRules: { + title: '告警规则', + description: '创建与管理系统阈值告警(仅邮件通知)', + loading: '加载中...', + empty: '暂无告警规则', + loadFailed: '加载告警规则失败', + saveSuccess: '警报规则保存成功', + saveFailed: '保存告警规则失败', + deleteSuccess: '警报规则删除成功', + deleteFailed: '删除告警规则失败', + create: '新建规则', + createTitle: '新建告警规则', + editTitle: '编辑告警规则', + deleteConfirmTitle: '确认删除该规则?', + deleteConfirmMessage: '将删除该规则及其关联的告警事件,是否继续?', + manage: '预警规则', + metricGroups: { + system: '系统指标', + group: '分组级别指标(需 group_id)', + account: '账号级别指标' + }, + metrics: { + successRate: '成功率 (%)', + errorRate: '错误率 (%)', + upstreamErrorRate: '上游错误率 (%)', + p95: 'P95 请求时长 (ms)', + p99: 'P99 请求时长 (ms)', + cpu: 'CPU 使用率 (%)', + memory: '内存使用率 (%)', + queueDepth: '并发排队深度', + groupAvailableAccounts: '分组可用账号数', + groupAvailableRatio: '分组可用比例 (%)', + groupRateLimitRatio: '分组限流比例 (%)', + accountRateLimitedCount: '限流账号数', + accountErrorCount: '错误账号数(不含临时不可调度)', + accountErrorRatio: '错误账号比例 (%)', + accountTempUnscheduledCount: '临时不可调度账号数', + overloadAccountCount: '过载账号数' + }, + metricDescriptions: { + successRate: '统计窗口内成功请求占比(0~100)。', + errorRate: '统计窗口内失败请求占比(0~100)。', + upstreamErrorRate: '统计窗口内上游错误占比(0~100)。', + p95: '统计窗口内 P95 请求耗时(毫秒)。', + p99: '统计窗口内 P99 请求耗时(毫秒)。', + cpu: '当前实例 CPU 使用率(0~100)。', + memory: '当前实例内存使用率(0~100)。', + queueDepth: '统计窗口内并发队列排队深度(等待中的请求数)。', + groupAvailableAccounts: '指定分组中当前可用账号数量(需要 group_id 过滤)。', + groupAvailableRatio: '指定分组中可用账号占比(0~100,需要 group_id 过滤)。', + groupRateLimitRatio: '指定分组中账号被限流的比例(0~100,需要 group_id 过滤)。', + accountRateLimitedCount: '统计窗口内被限流的账号数量。', + accountErrorCount: '统计窗口内产生错误的账号数量(不含临时不可调度)。', + accountErrorRatio: '统计窗口内错误账号占比(0~100)。', + accountTempUnscheduledCount: '当前处于临时不可调度状态的账号数量(如代理/凭据故障被自动摘除)。', + overloadAccountCount: '统计窗口内过载账号数量。' + }, + hints: { + recommended: '推荐:运算符 {operator},阈值 {threshold}{unit}', + groupRequired: '该指标为分组级别指标,必须选择分组(group_id)。', + groupOptional: '可选:通过 group_id 将规则限定到某个分组。' + }, + table: { + name: '名称', + metric: '指标', + severity: '级别', + enabled: '启用', + actions: '操作' + }, + form: { + name: '名称', + description: '描述', + metric: '指标', + operator: '运算符', + groupId: '分组(group_id)', + groupPlaceholder: '请选择分组', + allGroups: '全部分组', + threshold: '阈值', + severity: '级别', + window: '统计窗口(分钟)', + sustained: '连续样本数(每分钟)', + cooldown: '冷却期(分钟)', + enabled: '启用', + notifyEmail: '发送邮件通知' + }, + validation: { + title: '请先修正以下问题', + invalid: '规则不合法', + nameRequired: '名称不能为空', + metricRequired: '指标不能为空', + groupIdRequired: '分组级别指标必须指定 group_id', + operatorRequired: '运算符不能为空', + thresholdRequired: '阈值必须为数字', + windowRange: '统计窗口必须为 1 / 5 / 60 分钟之一', + sustainedRange: '连续样本数必须在 1 到 1440 之间', + cooldownRange: '冷却期必须在 0 到 1440 分钟之间' + } + }, + runtime: { + title: '运维监控运行设置', + description: '配置存储在数据库中,无需修改 config 文件即可生效。', + loading: '加载中...', + noData: '暂无运行设置', + loadFailed: '加载运行设置失败', + saveSuccess: '运行设置已保存', + saveFailed: '保存运行设置失败', + alertTitle: '告警评估器', + groupAvailabilityTitle: '分组可用性监控', + evalIntervalSeconds: '评估间隔(秒)', + silencing: { + title: '告警静默(维护模式)', + enabled: '启用静默', + globalUntil: '静默截止时间(RFC3339)', + untilHint: '建议填写截止时间,避免忘记关闭静默。', + reason: '原因', + reasonPlaceholder: '例如:计划维护', + entries: { + title: '高级:定向静默', + hint: '可选:仅静默特定规则或特定级别。字段留空表示匹配全部。', + add: '新增条目', + empty: '暂无定向静默条目', + entryTitle: '条目 #{n}', + ruleId: '规则ID(可选)', + ruleIdPlaceholder: '例如:1', + severities: '级别(可选)', + severitiesPlaceholder: '例如:P0,P1(留空=全部)', + until: '截止时间(RFC3339)', + reason: '原因', + validation: { + untilRequired: '条目截止时间不能为空', + untilFormat: '条目截止时间必须为合法的 RFC3339 时间戳', + ruleIdPositive: '条目 rule_id 必须为正整数', + severitiesFormat: '条目级别必须为 P0..P3 的逗号分隔列表' + } + }, + validation: { + timeFormat: '静默时间必须为合法的 RFC3339 时间戳' + } + }, + lockEnabled: '启用分布式锁', + lockKey: '分布式锁 Key', + lockTTLSeconds: '分布式锁 TTL(秒)', + showAdvancedDeveloperSettings: '显示高级开发者设置 (Distributed Lock)', + advancedSettingsSummary: '高级设置 (分布式锁)', + evalIntervalHint: '检测任务的执行频率,建议保持默认。', + validation: { + title: '请先修正以下问题', + invalid: '设置不合法', + evalIntervalRange: '评估间隔必须在 1 到 86400 秒之间', + lockKeyRequired: '启用分布式锁时必须填写 Lock Key', + lockKeyPrefix: '分布式锁 Key 必须以「{prefix}」开头', + lockKeyHint: '建议以「{prefix}」开头以避免冲突', + lockTtlRange: '分布式锁 TTL 必须在 1 到 86400 秒之间', + slaMinPercentRange: 'SLA 最低值必须在 0-100 之间', + ttftP99MaxRange: 'TTFT P99 最大值必须大于或等于 0', + requestErrorRateMaxRange: '请求错误率最大值必须在 0-100 之间', + upstreamErrorRateMaxRange: '上游错误率最大值必须在 0-100 之间' + } + }, + email: { + title: '邮件通知配置', + description: '配置告警/报告邮件通知(存储在数据库中)。', + loading: '加载中...', + noData: '暂无邮件通知配置', + loadFailed: '加载邮件通知配置失败', + saveSuccess: '邮件通知配置已保存', + saveFailed: '保存邮件通知配置失败', + alertTitle: '告警邮件', + reportTitle: '报告邮件', + recipients: '收件人', + recipientsHint: '若为空,系统可能会回退使用第一个管理员邮箱。', + minSeverity: '最低级别', + minSeverityAll: '全部级别', + rateLimitPerHour: '每小时限额', + batchWindowSeconds: '合并窗口(秒)', + includeResolved: '包含恢复通知', + dailySummary: '每日摘要', + weeklySummary: '每周摘要', + errorDigest: '错误摘要', + errorDigestMinCount: '错误摘要最小数量', + accountHealth: '账号健康报告', + accountHealthThreshold: '错误率阈值(%)', + cronPlaceholder: 'Cron 表达式', + reportHint: '发送时间使用 Cron 语法;留空将使用默认值。', + validation: { + title: '请先修正以下问题', + invalid: '邮件通知配置不合法', + alertRecipientsRequired: '已启用告警邮件,但未配置任何收件人', + reportRecipientsRequired: '已启用报告邮件,但未配置任何收件人', + invalidRecipients: '存在不合法的收件人邮箱', + rateLimitRange: '每小时限额必须为 ≥ 0 的数字', + batchWindowRange: '合并窗口必须在 0 到 86400 秒之间', + cronRequired: '启用定时任务时必须填写 Cron 表达式', + cronFormat: 'Cron 表达式格式可能不正确(至少应包含 5 段)', + digestMinCountRange: '错误摘要最小数量必须为 ≥ 0 的数字', + accountHealthThresholdRange: '账号健康错误率阈值必须在 0 到 100 之间' + } + }, + settings: { + title: '运维监控设置', + loadFailed: '加载设置失败', + saveSuccess: '运维监控设置保存成功', + saveFailed: '保存设置失败', + dataCollection: '数据采集', + evaluationInterval: '评估间隔(秒)', + evaluationIntervalHint: '检测任务的执行频率,建议保持默认', + alertConfig: '预警配置', + enableAlert: '开启预警', + alertRecipients: '预警接收邮箱', + emailPlaceholder: '输入邮箱地址', + recipientsHint: '若为空,系统将使用第一个管理员邮箱作为默认收件人', + minSeverity: '最低级别', + reportConfig: '评估报告配置', + enableReport: '开启评估报告', + reportRecipients: '评估报告接收邮箱', + dailySummary: '每日摘要', + weeklySummary: '每周摘要', + metricThresholds: '指标阈值配置', + metricThresholdsHint: '配置各项指标的告警阈值,超出阈值时将以红色显示', + slaMinPercent: 'SLA最低百分比', + slaMinPercentHint: 'SLA低于此值时显示为红色(默认:99.5%)', + ttftP99MaxMs: 'TTFT P99最大值(毫秒)', + ttftP99MaxMsHint: 'TTFT P99高于此值时显示为红色(默认:500ms)', + requestErrorRateMaxPercent: '请求错误率最大值(%)', + requestErrorRateMaxPercentHint: '请求错误率高于此值时显示为红色(默认:5%)', + upstreamErrorRateMaxPercent: '上游错误率最大值(%)', + upstreamErrorRateMaxPercentHint: '上游错误率高于此值时显示为红色(默认:5%)', + advancedSettings: '高级设置', + dataRetention: '数据保留策略', + enableCleanup: '启用数据清理', + cleanupSchedule: '清理计划(Cron)', + cleanupScheduleHint: '例如:0 2 * * * 表示每天凌晨2点', + errorLogRetentionDays: '错误日志保留天数', + minuteMetricsRetentionDays: '分钟指标保留天数', + hourlyMetricsRetentionDays: '小时指标保留天数', + retentionDaysHint: '建议保留 7-90 天,过长会占用存储空间;填 0 表示每次定时清理时清空所有历史', + aggregation: '预聚合任务', + enableAggregation: '启用预聚合任务', + aggregationHint: '预聚合可提升长时间窗口查询性能', + openaiQuotaAutoPause: 'OpenAI 账号配额自动暂停', + openaiQuotaAutoPauseHint: '当 OpenAI 账号 5h / 7d 用量达到阈值时,调度会自动跳过该账号;窗口滚动后自动恢复。账号级阈值优先于此全局默认值。', + openaiQuotaAutoPauseDefault5h: '默认 5h 用量阈值 (%)', + openaiQuotaAutoPauseDefault7d: '默认 7d 用量阈值 (%)', + openaiQuotaAutoPauseThresholdHint: '取值 0-100,留空或 0 表示不启用全局默认阈值。', + errorFiltering: '错误过滤', + ignoreCountTokensErrors: '忽略 count_tokens 错误', + ignoreCountTokensErrorsHint: '启用后,count_tokens 请求的错误将不会写入错误日志。', + ignoreContextCanceled: '忽略客户端断连错误', + ignoreContextCanceledHint: + '启用后,客户端主动断开连接(context canceled)的错误将不会写入错误日志。', + ignoreNoAvailableAccounts: '忽略无可用账号错误', + ignoreNoAvailableAccountsHint: '启用后,"No available accounts" 错误将不会写入错误日志(不推荐,这通常是配置问题)。', + ignoreInvalidApiKeyErrors: '忽略无效 API Key 错误', + ignoreInvalidApiKeyErrorsHint: '启用后,无效或缺失 API Key 的错误(INVALID_API_KEY、API_KEY_REQUIRED)将不会写入错误日志。', + ignoreInsufficientBalanceErrors: '忽略余额不足错误', + ignoreInsufficientBalanceErrorsHint: '启用后,账号余额不足(Insufficient balance)的错误将不会写入错误日志。', + autoRefresh: '自动刷新', + enableAutoRefresh: '启用自动刷新', + enableAutoRefreshHint: '自动刷新仪表板数据,启用后会定期拉取最新数据。', + refreshInterval: '刷新间隔', + refreshInterval15s: '15 秒', + refreshInterval30s: '30 秒', + refreshInterval60s: '60 秒', + dashboardCards: '仪表盘卡片', + displayAlertEvents: '展示告警事件', + displayAlertEventsHint: '控制运维监控仪表盘中告警事件卡片是否显示,默认开启。', + displayOpenAITokenStats: '展示 OpenAI Token 请求统计', + displayOpenAITokenStatsHint: '控制运维监控仪表盘中 OpenAI Token 请求统计卡片是否显示,默认关闭。', + autoRefreshCountdown: '自动刷新:{seconds}s', + validation: { + title: '请先修正以下问题', + retentionDaysRange: '保留天数必须在 0-365 天之间(0 = 每次清理时清空所有)', + slaMinPercentRange: 'SLA最低百分比必须在0-100之间', + ttftP99MaxRange: 'TTFT P99最大值必须大于等于0', + requestErrorRateMaxRange: '请求错误率最大值必须在0-100之间', + upstreamErrorRateMaxRange: '上游错误率最大值必须在0-100之间', + openaiQuotaAutoPauseRange: 'OpenAI 配额自动暂停阈值必须在 0-100 之间' + } + }, + concurrency: { + title: '并发 / 排队', + byPlatform: '按平台', + byGroup: '按分组', + byAccount: '按账号', + byUser: '按用户', + showByUserTooltip: '切换用户视图,显示每个用户的并发使用情况', + switchToUser: '切换到用户视图', + switchToPlatform: '切换回平台视图', + totalRows: '共 {count} 项', + disabledHint: '已在设置中关闭实时监控。', + empty: '暂无数据', + queued: '队列 {count}', + rateLimited: '限流 {count}', + errorAccounts: '异常 {count}', + loadFailed: '加载并发数据失败' + }, + realtime: { + title: '实时信息', + connected: '实时已连接', + connecting: '实时连接中', + reconnecting: '实时重连中', + offline: '实时离线', + closed: '实时已关闭', + reconnectIn: '重连 {seconds}s' + }, + queryMode: { + auto: 'Auto(自动)', + raw: 'Raw(不聚合)', + preagg: 'Preagg(聚合)' + }, + accountAvailability: { + available: '可用', + unavailable: '不可用', + accountError: '异常' + }, + tooltips: { + totalRequests: '当前时间窗口内的总请求数和Token消耗量。', + throughputTrend: '当前窗口内的请求/QPS 与 token/TPS 趋势。', + switchRateTrend: '近5小时内账号切换次数 / 请求总数的趋势(平均切换次数)。', + latencyHistogram: '成功请求的请求时长分布(毫秒)。', + errorTrend: '错误趋势(SLA 口径排除业务限制;上游错误率排除 429/529)。', + errorDistribution: '按状态码统计的错误分布(SLA 口径,排除业务限制)。', + upstreamErrors: '上游服务返回的错误,包括API提供商的错误响应(排除429/529限流错误)。', + goroutines: + 'Go 运行时的协程数量(轻量级线程)。没有绝对"安全值",建议以历史基线为准。经验参考:<2000 常见;2000-8000 需关注;>8000 且伴随队列上升时,优先排查阻塞/泄漏。', + cpu: 'CPU 使用率,显示系统处理器的负载情况。', + memory: '内存使用率,包括已使用和总可用内存。', + db: '数据库连接池状态,包括活跃连接、空闲连接和等待连接数。', + redis: 'Redis 连接池状态,显示活跃和空闲的连接数。', + jobs: '后台任务执行状态,包括最近运行时间、成功时间和错误信息。', + qps: '每秒查询数(QPS)和每秒Token数(TPS),实时显示系统吞吐量。', + tokens: '当前时间窗口内处理的总Token数量。', + sla: '服务等级协议达成率,排除业务限制(如余额不足、配额超限)的成功请求占比。', + errors: '错误统计,包括总错误数、错误率和上游错误率。', + latency: '请求时长统计,包括 p50、p90、p95、p99 等百分位数。', + ttft: '首 Token 延迟(Time To First Token),衡量流式响应的首 Token 返回速度。', + health: '系统健康评分(0-100),综合考虑 SLA、错误率和资源使用情况。' + }, + charts: { + emptyRequest: '该时间窗口内暂无请求。', + emptyError: '该时间窗口内暂无错误。', + resetZoom: '重置', + resetZoomHint: '重置缩放(若启用)', + downloadChart: '下载', + downloadChartHint: '下载图表图片' + } + }, + + // Settings +} diff --git a/frontend/src/i18n/locales/zh/admin/overview.ts b/frontend/src/i18n/locales/zh/admin/overview.ts new file mode 100644 index 0000000000..c15cc52990 --- /dev/null +++ b/frontend/src/i18n/locales/zh/admin/overview.ts @@ -0,0 +1,1021 @@ +export default { + // Dashboard + dashboard: { + title: '管理控制台', + description: '系统概览与统计数据', + apiKeys: 'API 密钥', + totalApiKeys: 'API 密钥总数', + activeApiKeys: '活跃密钥', + users: '用户', + totalUsers: '用户总数', + activeUsers: '活跃用户', + accounts: '账号', + totalAccounts: '账号总数', + activeAccounts: '活跃账号', + todayRequests: '今日请求', + totalRequests: '总请求数', + todayCost: '今日消费', + totalCost: '总消费', + actual: '实际', + standard: '标准', + accountCost: '成本', + todayTokens: '今日 Token', + totalTokens: '总 Token', + input: '输入', + output: '输出', + cacheToday: '今日缓存', + performance: '性能指标', + avgResponse: '平均响应', + averageTime: '平均时间', + timeRange: '时间范围', + granularity: '粒度', + day: '按天', + hour: '按小时', + modelDistribution: '模型分布', + groupDistribution: '分组使用分布', + metricTokens: '按 Token', + metricActualCost: '按实际消费', + tokenUsageTrend: 'Token 使用趋势', + noDataAvailable: '暂无数据', + model: '模型', + group: '分组', + noGroup: '无分组', + requests: '请求', + tokens: 'Token', + cache: '缓存', + recentUsage: '最近使用', + viewModelDistribution: '模型分布', + viewSpendingRanking: '用户消费榜', + spendingRankingTitle: '用户消费榜', + spendingRankingUser: '用户', + spendingRankingRequests: '请求', + spendingRankingTokens: 'Token', + spendingRankingSpend: '消费', + spendingRankingOther: '其他', + spendingRankingUsage: '用量', + spendShort: '消费', + requestsShort: '请求', + tokensShort: 'Token', + last7Days: '近 7 天', + noUsageRecords: '暂无使用记录', + startUsingApi: '开始使用 API 后,使用历史将显示在这里。', + viewAllUsage: '查看全部', + quickActions: '快捷操作', + manageUsers: '管理用户', + viewUserAccounts: '查看和管理用户账户', + manageAccounts: '管理账号', + configureAiAccounts: '配置 AI 平台账号', + batchImage: '批量生图', + batchImageDesc: '提交任务、复制 Agent 调用说明', + groupPricing: '分组定价', + groupPricingDesc: '设置批量折扣和冻结比例', + systemSettings: '系统设置', + configureSystem: '配置系统设置', + failedToLoad: '加载仪表盘数据失败' + }, + + backup: { + title: '数据库备份', + description: '全量数据库备份到 S3 兼容存储,支持定时备份与恢复', + s3: { + title: 'S3 存储配置', + description: '配置 S3 兼容存储(支持 Cloudflare R2)', + descriptionPrefix: '配置 S3 兼容存储(支持', + descriptionSuffix: ')', + enabled: '启用 S3 存储', + endpoint: '端点地址', + region: '区域', + bucket: '存储桶', + prefix: 'Key 前缀', + accessKeyId: 'Access Key ID', + secretAccessKey: 'Secret Access Key', + secretConfigured: '已配置,留空保持不变', + forcePathStyle: '强制路径风格', + testConnection: '测试连接', + testSuccess: 'S3 连接测试成功', + testFailed: 'S3 连接测试失败', + saved: 'S3 配置已保存' + }, + schedule: { + title: '定时备份', + description: '配置自动定时备份', + enabled: '启用定时备份', + cronExpr: 'Cron 表达式', + cronHint: '例如 "0 2 * * *" 表示每天凌晨 2 点', + retainDays: '备份过期天数', + retainDaysHint: '备份文件超过此天数后自动删除,0 = 永不过期', + retainCount: '最大保留份数', + retainCountHint: '最多保留的备份数量,0 = 不限制', + saved: '定时备份配置已保存' + }, + operations: { + title: '备份记录', + description: '创建手动备份和管理已有备份记录', + createBackup: '创建备份', + backing: '备份中...', + backupCreated: '备份创建成功', + expireDays: '过期天数', + alreadyInProgress: '已有备份正在进行中', + backupRunning: '备份进行中...', + backupFailed: '备份失败', + restoreRunning: '恢复进行中...', + restoreFailed: '恢复失败', + }, + columns: { + status: '状态', + fileName: '文件名', + size: '大小', + expiresAt: '过期时间', + triggeredBy: '触发方式', + startedAt: '开始时间', + actions: '操作' + }, + status: { + pending: '等待中', + running: '执行中', + completed: '已完成', + failed: '失败' + }, + progress: { + pending: '准备中', + dumping: '导出数据库', + uploading: '上传中', + }, + trigger: { + manual: '手动', + scheduled: '定时' + }, + neverExpire: '永不过期', + empty: '暂无备份记录', + actions: { + download: '下载', + restore: '恢复', + restoreConfirm: '确定要从此备份恢复吗?这将覆盖当前数据库!', + restorePasswordPrompt: '请输入管理员密码以确认恢复操作', + restoreSuccess: '数据库恢复成功', + deleteConfirm: '确定要删除此备份吗?', + deleted: '备份已删除' + }, + r2Guide: { + title: 'Cloudflare R2 配置教程', + intro: 'Cloudflare R2 提供 S3 兼容的对象存储,免费额度为 10GB 存储 + 每月 100 万次 A 类请求,非常适合数据库备份。', + step1: { + title: '创建 R2 存储桶', + line1: '登录 Cloudflare Dashboard (dash.cloudflare.com),左侧菜单选择「R2 对象存储」', + line2: '点击「创建存储桶」,输入名称(如 sub2api-backups),选择区域', + line3: '点击创建完成' + }, + step2: { + title: '创建 API 令牌', + line1: '在 R2 页面,点击右上角「管理 R2 API 令牌」', + line2: '点击「创建 API 令牌」,权限选择「对象读和写」', + line3: '建议指定存储桶范围(仅允许访问备份桶,更安全)', + line4: '创建后会显示 Access Key ID 和 Secret Access Key', + warning: 'Secret Access Key 只会显示一次,请立即复制保存!' + }, + step3: { + title: '获取 S3 端点地址', + desc: '在 R2 概览页面找到你的账户 ID(在 URL 或右侧面板中),端点格式为:', + accountId: '你的账户 ID' + }, + step4: { + title: '填写以下配置', + checkEnabled: '勾选', + bucketValue: '你创建的存储桶名称', + fromStep2: '第 2 步获取的值', + unchecked: '不勾选' + }, + freeTier: 'R2 免费额度:10GB 存储 + 每月 100 万次 A 类请求 + 1000 万次 B 类请求,对数据库备份完全够用。' + } + }, + + dataManagement: { + title: '数据管理', + description: '统一管理数据管理代理状态、对象存储配置和备份任务', + agent: { + title: '数据管理代理状态', + description: '系统会自动探测固定 Unix Socket,仅在可连通时启用数据管理功能。', + enabled: '数据管理代理已就绪,可继续进行数据管理操作。', + disabled: '数据管理代理不可用,当前仅可查看诊断信息。', + socketPath: 'Socket 路径', + version: '版本', + status: '状态', + uptime: '运行时长', + reasonLabel: '不可用原因', + reason: { + DATA_MANAGEMENT_AGENT_SOCKET_MISSING: '未检测到数据管理 Socket 文件', + DATA_MANAGEMENT_AGENT_UNAVAILABLE: '数据管理代理不可连通', + BACKUP_AGENT_SOCKET_MISSING: '未检测到备份 Socket 文件', + BACKUP_AGENT_UNAVAILABLE: '备份代理不可连通', + UNKNOWN: '未知原因' + } + }, + sections: { + config: { + title: '备份配置', + description: '配置备份源、保留策略与 S3 存储参数。' + }, + s3: { + title: 'S3 对象存储', + description: '配置并测试备份产物上传到标准 S3 对象存储。' + }, + backup: { + title: '备份操作', + description: '触发 PostgreSQL、Redis 与全量备份任务。' + }, + history: { + title: '备份历史', + description: '查看备份任务执行状态、错误与产物信息。' + } + }, + form: { + sourceMode: '源模式', + backupRoot: '备份根目录', + activePostgresProfile: '当前激活 PostgreSQL 配置', + activeRedisProfile: '当前激活 Redis 配置', + activeS3Profile: '当前激活 S3 账号', + retentionDays: '保留天数', + keepLast: '至少保留最近任务数', + uploadToS3: '上传到 S3', + useActivePostgresProfile: '使用当前激活 PostgreSQL 配置', + useActiveRedisProfile: '使用当前激活 Redis 配置', + useActiveS3Profile: '使用当前激活账号', + idempotencyKey: '幂等键(可选)', + secretConfigured: '已配置,留空不变', + source: { + profileID: '配置 ID(唯一)', + profileName: '配置名称', + setActive: '创建后立即设为激活配置' + }, + postgres: { + title: 'PostgreSQL', + host: '主机', + port: '端口', + user: '用户名', + password: '密码', + database: '数据库', + sslMode: 'SSL 模式', + containerName: '容器名(docker_exec 模式)' + }, + redis: { + title: 'Redis', + addr: '地址(host:port)', + username: '用户名', + password: '密码', + db: '数据库编号', + containerName: '容器名(docker_exec 模式)' + }, + s3: { + enabled: '启用 S3 上传', + profileID: '账号 ID(唯一)', + profileName: '账号名称', + endpoint: 'Endpoint(可选)', + region: 'Region', + bucket: 'Bucket', + accessKeyID: 'Access Key ID', + secretAccessKey: 'Secret Access Key', + prefix: '对象前缀', + forcePathStyle: '强制 path-style', + useSSL: '使用 SSL', + setActive: '创建后立即设为激活账号' + } + }, + sourceProfiles: { + createTitle: '创建数据源配置', + editTitle: '编辑数据源配置', + empty: '暂无配置,请先创建', + deleteConfirm: '确定删除配置 {profileID} 吗?', + columns: { + profile: '配置', + active: '激活状态', + connection: '连接信息', + database: '数据库', + updatedAt: '更新时间', + actions: '操作' + } + }, + s3Profiles: { + createTitle: '创建 S3 账号', + editTitle: '编辑 S3 账号', + empty: '暂无 S3 账号,请先创建', + editHint: '点击“编辑”将在右侧抽屉中修改账号信息。', + deleteConfirm: '确定删除 S3 账号 {profileID} 吗?', + columns: { + profile: '账号', + active: '激活状态', + storage: '存储配置', + updatedAt: '更新时间', + actions: '操作' + } + }, + history: { + total: '共 {count} 条', + empty: '暂无备份任务', + columns: { + jobID: '任务 ID', + type: '类型', + status: '状态', + triggeredBy: '触发人', + pgProfile: 'PostgreSQL 配置', + redisProfile: 'Redis 配置', + s3Profile: 'S3 账号', + finishedAt: '完成时间', + artifact: '产物', + error: '错误' + }, + status: { + queued: '排队中', + running: '执行中', + succeeded: '成功', + failed: '失败', + partial_succeeded: '部分成功' + } + }, + actions: { + refresh: '刷新状态', + disabledHint: '请先启动 datamanagementd 并确认 Socket 可连通。', + reloadConfig: '加载配置', + reloadSourceProfiles: '刷新数据源配置', + reloadProfiles: '刷新账号列表', + newSourceProfile: '新建数据源配置', + saveConfig: '保存配置', + configSaved: '配置保存成功', + testS3: '测试 S3 连接', + s3TestOK: 'S3 连接测试成功', + s3TestFailed: 'S3 连接测试失败', + newProfile: '新建账号', + saveProfile: '保存账号', + activateProfile: '设为激活', + profileIDRequired: '请输入账号 ID', + profileNameRequired: '请输入账号名称', + profileSelectRequired: '请先选择要编辑的账号', + profileCreated: 'S3 账号创建成功', + profileSaved: 'S3 账号保存成功', + profileActivated: 'S3 账号已切换为激活', + profileDeleted: 'S3 账号删除成功', + sourceProfileCreated: '数据源配置创建成功', + sourceProfileSaved: '数据源配置保存成功', + sourceProfileActivated: '数据源配置已切换为激活', + sourceProfileDeleted: '数据源配置删除成功', + createBackup: '创建备份任务', + jobCreated: '备份任务已创建:{jobID}({status})', + refreshJobs: '刷新任务', + loadMore: '加载更多' + } + }, + + affiliates: { + invitesDescription: '查看全站邀请关系和被邀请用户累计返利', + rebatesDescription: '查看每一笔产生返利的充值订单', + transfersDescription: '查看返利额度转入账户余额的提取流水', + errors: { + loadFailed: '加载邀请返利记录失败' + }, + records: { + search: '搜索', + searchPlaceholder: '邮箱、用户名、用户 ID、订单号', + startAt: '开始日期', + endAt: '结束日期', + inviter: '邀请人', + invitee: '被邀请人', + user: '用户', + affCode: '邀请码', + order: '订单', + totalRebate: '累计返利', + orderAmount: '充值金额', + payAmount: '支付金额', + rebateAmount: '返利金额', + paymentType: '支付方式', + orderStatus: '订单状态', + transferAmount: '提取金额', + balanceAfter: '提取后余额', + availableQuotaAfter: '提取后可提', + frozenQuotaAfter: '提取后冻结', + historyQuotaAfter: '提取后历史返利', + invitedAt: '邀请时间', + rebatedAt: '返利时间', + transferredAt: '提取时间' + }, + overview: { + title: '用户返利概览', + affCode: '邀请码', + rebateRate: '返利比例', + invitedCount: '邀请人数', + rebatedInviteeCount: '已产生返利人数', + availableQuota: '可提余额', + historyQuota: '历史返利' + } + }, + + // Users Management + users: { + title: '用户管理', + description: '管理用户账户和权限', + createUser: '创建用户', + editUser: '编辑用户', + deleteUser: '删除用户', + deleteConfirmMessage: "确定要删除用户 '{email}' 吗?此操作无法撤销。", + searchPlaceholder: '邮箱/用户名/备注/API Key 模糊搜索...', + searchUsers: '邮箱/用户名/备注/API Key 模糊搜索', + roleFilter: '角色筛选', + allRoles: '全部角色', + allStatus: '全部状态', + allGroups: '全部分组', + searchGroups: '搜索分组...', + fuzzySearch: '模糊搜索', + apiKeyGroupFilter: 'API Key 分组', + apiKeyGroupExclusive: '专用分组', + apiKeyGroupPublic: '公开分组', + apiKeyGroupSubscription: '订阅分组', + apiKeyGroupDisabled: '已禁用分组', + authorizedGroupFilter: '授权分组', + allAuthorizedGroups: '全部授权分组', + searchAuthorizedGroups: '搜索授权分组...', + allApiKeyGroups: '全部 API Key 分组', + searchApiKeyGroups: '搜索 API Key 分组...', + statusFilter: '状态筛选', + allStatuses: '全部状态', + admin: '管理员', + user: '用户', + disabled: '禁用', + email: '邮箱', + password: '密码', + username: '用户名', + notes: '备注', + enterEmail: '请输入邮箱', + enterPassword: '请输入密码', + enterUsername: '请输入用户名(选填)', + enterNotes: '请输入备注(仅管理员可见)', + notesHint: '此备注仅对管理员可见', + enterNewPassword: '请输入新密码(选填)', + leaveEmptyToKeep: '留空则保持原密码不变', + generatePassword: '生成随机密码', + copyPassword: '复制密码', + creating: '创建中...', + updating: '更新中...', + columns: { + user: '用户', + id: 'ID', + email: '邮箱', + username: '用户名', + notes: '备注', + role: '角色', + groups: '分组', + subscriptions: '订阅分组', + balance: '余额', + balancePlatformQuota: '余额(平台配额)', + usage: '用量', + usageAnthropic: '用量 (Claude)', + usageOpenAI: '用量 (OpenAI)', + usageGemini: '用量 (Gemini)', + usageAntigravity: '用量 (Antigravity)', + concurrency: '并发数', + status: '状态', + lastActive: '最后活跃时间', + lastUsed: '最后使用时间', + created: '创建时间', + actions: '操作' + }, + today: '今日', + total: '近30天', + sortBy: '排序方式', + sortCurrentPageOnly: '仅对本页数据排序', + noSubscription: '暂无订阅', + publicGroupCount: '+{count} 公开', + exclusiveLabel: '专属', + publicLabel: '公开', + daysRemaining: '{days}天', + expired: '已过期', + disable: '禁用', + enable: '启用', + disableUser: '禁用用户', + enableUser: '启用用户', + viewApiKeys: '查看 API 密钥', + groups: '分组', + apiKeys: 'API密钥', + userApiKeys: '用户 API 密钥', + noApiKeys: '此用户暂无 API 密钥', + group: '分组', + none: '无', + groupChangedSuccess: '分组修改成功', + groupChangedWithGrant: '分组修改成功,已自动为用户添加「{group}」分组权限', + groupChangeFailed: '分组修改失败', + noUsersYet: '暂无用户', + createFirstUser: '创建您的第一个用户以开始使用系统', + userCreated: '用户创建成功', + userUpdated: '用户更新成功', + userDeleted: '用户删除成功', + userEnabled: '用户已启用', + userDisabled: '用户已禁用', + failedToLoad: '加载用户列表失败', + failedToCreate: '创建用户失败', + failedToUpdate: '更新用户失败', + failedToDelete: '删除用户失败', + failedToToggle: '更新用户状态失败', + failedToLoadApiKeys: '加载用户 API 密钥失败', + deleteConfirm: "确定要删除用户 '{email}' 吗?此操作无法撤销。", + roles: { + admin: '管理员', + user: '用户' + }, + form: { + emailLabel: '邮箱', + emailPlaceholder: '请输入邮箱', + usernameLabel: '用户名', + usernamePlaceholder: '请输入用户名(选填)', + notesLabel: '备注', + notesPlaceholder: '请输入备注(仅管理员可见)', + notesHint: '此备注仅对管理员可见', + passwordLabel: '密码', + passwordPlaceholder: '请输入密码(留空则不修改)', + roleLabel: '角色', + selectRole: '选择角色', + balanceLabel: '余额', + concurrencyLabel: '并发数', + statusLabel: '状态', + selectStatus: '选择状态', + rpmLimit: '每分钟请求数 (RPM)', + rpmLimitPlaceholder: '0 表示不限制', + rpmLimitHint: '该用户每分钟最大请求数,0 = 不限制;仅在所用分组未设置 rpm_limit 时作为兜底生效' + }, + adjustBalance: '调整余额', + adjustConcurrency: '调整并发数', + adjustmentAmount: '调整金额', + adjustmentAmountHint: '正数增加,负数减少', + currentBalance: '当前余额', + currentConcurrency: '当前并发数', + saving: '保存中...', + noUsers: '暂无用户', + noUsersDescription: '创建您的第一个用户以开始使用系统。', + userCreatedSuccess: '用户创建成功', + userUpdatedSuccess: '用户更新成功', + userDeletedSuccess: '用户删除成功', + balanceAdjustedSuccess: '余额调整成功', + concurrencyAdjustedSuccess: '并发数调整成功', + failedToSave: '保存用户失败', + failedToAdjust: '调整失败', + emailRequired: '请输入邮箱', + concurrencyMin: '并发数不能小于1', + soraStorageQuota: 'Sora 存储配额', + soraStorageQuotaHint: '单位 GB,0 表示使用分组或系统默认配额', + amountRequired: '请输入有效金额', + insufficientBalance: '余额不足', + setAllowedGroups: '设置允许分组', + allowedGroupsHint: '选择此用户可以使用的标准分组。订阅类型分组请在订阅管理中配置。', + noStandardGroups: '暂无标准分组', + allowAllGroups: '允许全部分组', + allowAllGroupsHint: '用户可以使用任何非专属分组', + allowedGroupsUpdated: '允许分组更新成功', + failedToLoadGroups: '加载分组列表失败', + failedToUpdateAllowedGroups: '更新允许分组失败', + // 用户分组配置 + groupConfig: '用户分组配置', + groupConfigHint: '为用户 {email} 配置专属分组倍率(覆盖分组默认倍率)', + exclusiveGroups: '专属分组', + publicGroups: '公开分组(默认可用)', + defaultRate: '默认倍率', + customRate: '专属倍率', + useDefaultRate: '使用默认', + customRatePlaceholder: '留空使用默认', + groupConfigUpdated: '分组配置更新成功', + replaceGroup: '替换分组', + clickToReplace: '点击替换分组', + replaceGroupTitle: '替换专属分组', + replaceGroupHint: '选择新分组替换「{old}」,将自动迁移绑定的 Key 并更新分组权限', + replaceGroupConfirm: '确认替换', + replaceGroupSuccess: '分组替换成功,已迁移 {count} 个 Key', + selectNewGroup: '请选择目标分组', + noOtherGroups: '没有其他可用的专属分组', + deposit: '充值', + withdraw: '退款', + depositAmount: '充值金额', + withdrawAmount: '退款金额', + withdrawAll: '全部', + depositNotesPlaceholder: '例如:新用户注册奖励、活动充值、补偿充值等', + withdrawNotesPlaceholder: '例如:服务问题退款、错误充值退回、账户注销退款等', + notesOptional: '备注为可选项,有助于未来查账', + amountHint: '请输入正数金额', + newBalance: '操作后余额', + depositing: '充值中...', + withdrawing: '退款中...', + confirmDeposit: '确认充值', + confirmWithdraw: '确认退款', + depositSuccess: '充值成功', + withdrawSuccess: '退款成功', + failedToDeposit: '充值失败', + failedToWithdraw: '退款失败', + useDepositWithdrawButtons: '请使用充值/退款按钮调整余额', + // 余额变动记录 + balanceHistory: '充值记录', + balanceHistoryTip: '点击查看充值记录', + columnAlwaysVisible: '该列固定显示,不可隐藏', + // 平台用量明细(悬浮显示) + platformBreakdown: '按平台拆分', + platformBreakdownEmpty: '暂无平台明细', + platformBreakdownHint: '悬浮查看各平台用量', + platformOther: '其他', + balanceHistoryTitle: '用户充值和并发变动记录', + noBalanceHistory: '暂无变动记录', + allTypes: '全部类型', + typeBalance: '余额(兑换码)', + typeAffiliateBalance: '余额(返利转入)', + typeAdminBalance: '余额(管理员调整)', + typeConcurrency: '并发(兑换码)', + typeAdminConcurrency: '并发(管理员调整)', + typeSubscription: '订阅', + failedToLoadBalanceHistory: '加载余额记录失败', + createdAt: '创建时间', + totalRecharged: '总充值', + // Settings Dropdowns + filterSettings: '筛选设置', + columnSettings: '列设置', + filterValue: '输入值', + // User Attributes + attributes: { + title: '用户属性配置', + description: '配置用户的自定义属性字段', + configButton: '属性配置', + addAttribute: '添加属性', + editAttribute: '编辑属性', + deleteAttribute: '删除属性', + deleteConfirm: "确定要删除属性 '{name}' 吗?所有用户的该属性值将被删除。", + noAttributes: '暂无自定义属性', + noAttributesHint: '点击上方按钮添加自定义属性', + key: '属性键', + keyHint: '用于程序引用,只能包含字母、数字和下划线', + name: '显示名称', + nameHint: '在表单中显示的名称', + type: '属性类型', + fieldDescription: '描述', + fieldDescriptionHint: '属性的说明文字', + placeholder: '占位符', + placeholderHint: '输入框的提示文字', + required: '必填', + enabled: '启用', + options: '选项配置', + optionsHint: '用于单选/多选类型', + addOption: '添加选项', + optionValue: '选项值', + optionLabel: '显示文本', + validation: '验证规则', + minLength: '最小长度', + maxLength: '最大长度', + min: '最小值', + max: '最大值', + pattern: '正则表达式', + patternMessage: '验证失败提示', + types: { + text: '单行文本', + textarea: '多行文本', + number: '数字', + email: '邮箱', + url: '链接', + date: '日期', + select: '单选', + multi_select: '多选' + }, + created: '属性创建成功', + updated: '属性更新成功', + deleted: '属性删除成功', + reordered: '属性排序更新成功', + failedToLoad: '加载属性列表失败', + failedToCreate: '创建属性失败', + failedToUpdate: '更新属性失败', + keyRequired: '请输入属性键', + nameRequired: '请输入显示名称', + optionsRequired: '请至少添加一个选项', + failedToDelete: '删除属性失败', + failedToReorder: '更新排序失败', + keyExists: '属性键已存在', + dragToReorder: '拖拽排序' + }, + platformQuota: { + menuItem: '平台限额', + title: '平台限额', + subtitle: '为用户 {email} 配置各上游平台的日 / 周 / 月用量上限', + columns: { + platform: '平台', + daily: '日 (USD)', + weekly: '周 (USD)', + monthly: '月 (USD, 30天滚动)', + usage: '当前用量', + }, + placeholder: '不限制', + save: '保存', + saving: '保存中...', + cancel: '取消', + clearAll: '全部清空(取消所有限额)', + clearAllConfirm: '确认清空全部平台的日 / 周 / 月限额?所有平台将变为"无限额",本地无法撤销,需要在保存前手动重填。', + reset: { + button: '重置该窗口', + confirm: '确认重置该用户 {platform} 平台的 {window} 用量?此操作立即生效。', + success: '已重置 {platform} {window} 用量', + failed: '重置失败', + }, + updateSuccess: '平台限额已更新', + updateFailed: '保存失败', + loadFailed: '加载失败', + hint: '留空 = 不限制该窗口。', + windowDaily: '日', + windowWeekly: '周', + windowMonthly: '月', + cellNotConfigured: '未配置', + cellColumnTooltip: '仅展示已设限额的平台', + subscriptionWarning: '此用户有活跃订阅,平台限额仅在余额(标准)模式下生效,订阅模式请求不受此限额约束。', + invalidNumber: '以下字段填写不是合法数字,请修正后再保存:{fields}', + } + }, + + // Groups Management + groups: { + title: '分组管理', + description: '管理 API 密钥分组和费率配置', + searchGroups: '搜索分组...', + createGroup: '创建分组', + editGroup: '编辑分组', + deleteGroup: '删除分组', + sortOrder: '排序', + columnSettings: '列设置', + sortOrderHint: '拖拽分组调整显示顺序,排在前面的分组会优先显示', + sortOrderUpdated: '排序已更新', + failedToUpdateSortOrder: '更新排序失败', + deleteConfirm: "确定要删除分组 '{name}' 吗?所有关联的 API 密钥将不再属于任何分组。", + deleteConfirmSubscription: + "确定要删除订阅分组 '{name}' 吗?此操作会让所有绑定此订阅的用户的 API Key 失效,并删除所有相关的订阅记录。此操作无法撤销。", + columns: { + name: '名称', + platform: '平台', + rateMultiplier: '费率倍数', + rpmOverride: 'RPM 覆盖', + rpmOverrideHint: '该用户在此分组的 RPM 上限;留空 = 使用分组默认;0 = 不限制', + rateDefault: '默认', + rpmDefault: '默认', + exclusive: '独占', + type: '类型', + priority: '优先级', + apiKeys: 'API 密钥数', + accounts: '账号数', + capacity: '容量', + usage: '用量', + status: '状态', + actions: '操作', + billingType: '计费类型', + userName: '用户名', + userEmail: '邮箱', + userNotes: '备注', + userStatus: '状态' + }, + usageToday: '今日', + usageTotal: '累计', + accountsAvailable: '可用:', + accountsRateLimited: '限流:', + accountsTotal: '总量:', + accountsUnit: '个账号', + form: { + name: '名称', + description: '描述', + platform: '平台', + rateMultiplier: '费率倍数', + status: '状态', + exclusive: '专属分组', + nameLabel: '分组名称', + namePlaceholder: '请输入分组名称', + descriptionLabel: '描述', + descriptionPlaceholder: '请输入描述(可选)', + rateMultiplierLabel: '费率倍数', + rateMultiplierHint: '1.0 = 标准费率,0.5 = 半价,2.0 = 双倍', + rpmLimit: '每分钟请求数 (RPM)', + rpmLimitPlaceholder: '0 表示不限制', + rpmLimitHint: '每用户在本分组每分钟最大请求数,0 = 不限制;一旦设置即接管该用户的限流(覆盖用户级 rpm_limit)', + exclusiveLabel: '专属分组', + exclusiveHint: '专属分组,可以手动指定给用户', + platformLabel: '平台限制', + platformPlaceholder: '选择平台(留空则不限制)', + accountsLabel: '指定账号', + accountsPlaceholder: '选择账号(留空则不限制)', + priorityLabel: '优先级', + priorityHint: '数值越小优先级越高,用于账号调度', + statusLabel: '状态' + }, + exclusiveObj: { + yes: '是', + no: '否' + }, + exclusive: '专属', + exclusiveHint: '专属分组,可以手动指定给特定用户', + exclusiveTooltip: { + title: '什么是专属分组?', + description: + '开启后,用户在创建 API Key 时将无法看到此分组。只有管理员手动将用户分配到此分组后,用户才能使用。', + example: '使用场景:', + exampleContent: + '公开分组费率 0.8,您可以创建一个费率 0.7 的专属分组,手动分配给 VIP 用户,让他们享受更优惠的价格。' + }, + rateMultiplierHint: '1.0 = 标准费率,0.5 = 半价,2.0 = 双倍', + platforms: { + all: '全部平台', + anthropic: 'Anthropic', + openai: 'OpenAI', + gemini: 'Gemini', + antigravity: 'Antigravity', + grok: 'Grok', + }, + saving: '保存中...', + noGroups: '暂无分组', + noGroupsDescription: '创建分组以更好地管理 API 密钥和费率。', + groupCreatedSuccess: '分组创建成功', + groupUpdatedSuccess: '分组更新成功', + groupDeletedSuccess: '分组删除成功', + failedToLoad: '加载分组列表失败', + failedToSave: '保存分组失败', + failedToDelete: '删除分组失败', + allPlatforms: '全部平台', + allStatus: '全部状态', + allGroups: '全部分组', + exclusiveFilter: '专属', + nonExclusive: '公开', + public: '公开', + rateAndAccounts: '{rate}x 费率 · {count} 个账号', + accountsCount: '{count} 个账号', + rateLabel: '倍率', + accountFilters: { + title: '账号过滤控制', + oauthOnly: '仅允许 OAuth 账号', + oauthOnlyEnabled: '已启用 — 排除 API Key 类型账号', + privacySetOnly: '仅允许隐私保护已设置的账号', + privacySetOnlyEnabled: '已启用 — Privacy 未设置的账号将被排除', + disabled: '未启用' + }, + enterGroupName: '请输入分组名称', + optionalDescription: '可选描述', + platformHint: '选择此分组关联的平台', + platformNotEditable: '创建后不可更改平台', + noGroupsYet: '暂无分组', + createFirstGroup: '创建您的第一个分组来组织 API 密钥。', + creating: '创建中...', + updating: '更新中...', + limitDay: '日', + limitWeek: '周', + limitMonth: '月', + groupCreated: '分组创建成功', + groupUpdated: '分组更新成功', + groupDeleted: '分组删除成功', + failedToCreate: '创建分组失败', + failedToUpdate: '更新分组失败', + nameRequired: '请输入分组名称', + rateMultipliers: '专属倍率', + rateMultipliersTitle: '分组专属倍率管理', + addUserRate: '添加用户专属倍率', + rpmOverrides: '专属 RPM', + rpmOverridesTitle: '分组专属 RPM 管理', + addUserRpm: '添加用户专属 RPM', + noRpmOverrides: '暂无用户设置了专属 RPM', + rpmSaved: '专属 RPM 已保存', + groupRpmDefault: '分组默认 RPM', + searchUserPlaceholder: '搜索用户邮箱...', + noRateMultipliers: '暂无用户设置了专属倍率', + rateUpdated: '专属倍率已更新', + rateDeleted: '专属倍率已删除', + rateAdded: '专属倍率已添加', + clearAll: '全部清空', + confirmClearAll: '确定要清空该分组所有用户的专属倍率设置吗?此操作不可撤销。', + rateCleared: '已清空所有专属倍率', + batchAdjust: '批量调整倍率', + multiplierFactor: '乘数', + applyMultiplier: '应用', + rateAdjusted: '倍率已批量调整', + rateSaved: '专属倍率已保存', + finalRate: '最终倍率', + unsavedChanges: '有未保存的修改', + revertChanges: '撤销修改', + userInfo: '用户信息', + subscription: { + title: '订阅设置', + type: '计费类型', + typeHint: '标准计费从用户余额扣除。订阅模式使用配额限制。', + typeNotEditable: '分组创建后无法修改计费类型。', + standard: '标准(余额)', + subscription: '订阅(配额)', + dailyLimit: '每日限额(USD)', + weeklyLimit: '每周限额(USD)', + monthlyLimit: '每月限额(USD)', + defaultValidityDays: '默认有效期(天)', + validityHint: '分配给用户时订阅的有效天数', + noLimit: '无限制' + }, + imagePricing: { + title: '图片生成计费', + description: '配置图片生成能力和图片基础单价,留空则使用默认价格', + allowImageGeneration: '允许当前分组生图', + allowBatchImageGeneration: '允许当前分组批量生图', + independentMultiplier: '生图倍率独立', + imageMultiplier: '生图独立倍率', + batchDiscountMultiplier: '批量生图折扣倍率', + batchHoldMultiplier: '批量冻结价格比例', + batchSectionHint: '批量生图仅影响批量任务:结算价格会叠加批量折扣倍率,提交时冻结金额按普通生图原价 × 批量冻结价格比例计算。参考图也会产生上游输入 token 消耗,建议批量生图折扣倍率设置大于 0.5。', + batchDisabledHint: '请先开启当前分组生图,才能开启批量生图。', + batchGeminiOnlyHint: '批量生图当前仅支持 Gemini 分组。', + modeHint: '默认关闭独立倍率时,图片费用 = 图片价格 × 当前分组有效倍率;开启独立倍率后,图片费用 = 图片价格 × 生图独立倍率。', + finalPricePreview: '最终单张价格预览', + notConfigured: '未配置' + }, + peakRate: { + enable: '启用高峰倍率', + peakStart: '高峰开始', + peakEnd: '高峰结束', + peakMultiplier: '高峰倍率', + multiplierHint: '作用于 token 计费倍率;token 计费的图片 token 同样适用,0 表示高峰 token 请求按 0 倍计费' + }, + modelsList: { + title: '自定义 /v1/models 模型列表', + hint: '仅影响 /v1/models 展示结果,不影响白名单模型调用和账号调度。', + loading: '正在加载模型列表...', + empty: '暂无可展示模型', + selectedSummary: '已选 {selected} / {total}', + selectAll: '全选', + invertSelection: '反选' + }, + claudeCode: { + title: 'Claude Code 客户端限制', + tooltip: + '启用后,此分组仅允许 Claude Code 官方客户端访问。非 Claude Code 请求将被拒绝或降级到指定分组。', + enabled: '仅限 Claude Code', + disabled: '允许所有客户端', + fallbackGroup: '降级分组', + fallbackHint: '非 Claude Code 请求将使用此分组,留空则直接拒绝', + noFallback: '不降级(直接拒绝)' + }, + openaiMessages: { + title: 'OpenAI Messages 调度配置', + allowDispatch: '允许 /v1/messages 调度', + allowDispatchHint: '启用后,此 OpenAI 分组的 API Key 可以通过 /v1/messages 端点调度请求', + familyMappingTitle: '系列默认映射', + familyMappingHint: '当请求命中 Opus、Sonnet、Haiku 系列时,会优先使用这里配置的目标模型。', + opusModel: 'Opus 映射模型', + opusModelPlaceholder: '例如: gpt-5.4', + sonnetModel: 'Sonnet 映射模型', + sonnetModelPlaceholder: '例如: gpt-5.3-codex', + haikuModel: 'Haiku 映射模型', + haikuModelPlaceholder: '例如: gpt-5.4-mini', + exactMappingTitle: '精确模型覆盖', + exactMappingHint: '精确 Claude 模型覆盖优先级高于系列默认映射,可将某个具体 Claude 模型单独映射到不同的目标模型。', + noExactMappings: '暂无精确模型覆盖规则', + addExactMapping: '添加精确映射', + claudeModel: 'Claude 模型', + claudeModelPlaceholder: '例如: claude-sonnet-4-5-20250929', + targetModel: '目标模型', + targetModelPlaceholder: '例如: gpt-5.4', + removeExactMapping: '删除精确映射' + }, + invalidRequestFallback: { + title: '无效请求兜底分组', + hint: '仅当上游明确返回 prompt too long 时才会触发,留空表示不兜底', + noFallback: '不兜底' + }, + copyAccounts: { + title: '从分组复制账号', + tooltip: '选择一个或多个相同平台的分组,创建后会自动将这些分组的所有账号绑定到新分组(去重)。', + tooltipEdit: '选择一个或多个相同平台的分组,保存后当前分组的账号会被替换为这些分组的账号(去重)。', + selectPlaceholder: '选择分组以复制其账号...', + hint: '可选多个分组,账号会自动去重', + hintEdit: '⚠️ 注意:这会替换当前分组的所有账号绑定' + }, + modelRouting: { + title: '模型路由配置', + tooltip: + '配置特定模型请求优先路由到指定账号。支持通配符匹配,如 claude-opus-* 匹配所有 opus 模型。', + enabled: '已启用', + disabled: '已禁用', + disabledHint: '启用后,配置的路由规则才会生效', + addRule: '添加路由规则', + modelPattern: '模型模式', + modelPatternPlaceholder: 'claude-opus-*', + modelPatternHint: '支持 * 通配符,如 claude-opus-* 匹配所有 opus 模型', + accounts: '优先账号', + selectAccounts: '选择账号', + noAccounts: '此分组暂无账号', + loadingAccounts: '加载账号中...', + removeRule: '删除规则', + noRules: '暂无路由规则', + noRulesHint: '添加路由规则以将特定模型请求优先路由到指定账号', + searchAccountPlaceholder: '搜索账号...', + accountsHint: '选择此模型模式优先使用的账号' + }, + mcpXml: { + title: 'MCP XML 协议注入', + tooltip: '启用后,当请求包含 MCP 工具时,会在 system prompt 中注入 XML 格式调用协议提示词。关闭此选项可避免对某些客户端造成干扰。', + enabled: '已启用', + disabled: '已禁用' + }, + supportedScopes: { + title: '支持的模型系列', + tooltip: '选择此分组支持的模型系列。未勾选的系列将不会被路由到此分组。', + claude: 'Claude', + geminiText: 'Gemini Text', + geminiImage: 'Gemini Image', + hint: '至少选择一个模型系列' + } + }, + + // Available Channels (aggregated read-only view) +} diff --git a/frontend/src/i18n/locales/zh/admin/resources.ts b/frontend/src/i18n/locales/zh/admin/resources.ts new file mode 100644 index 0000000000..ec4753cf4b --- /dev/null +++ b/frontend/src/i18n/locales/zh/admin/resources.ts @@ -0,0 +1,581 @@ +export default { + scheduledTests: { + title: '定时测试', + addPlan: '添加计划', + editPlan: '编辑计划', + deletePlan: '删除计划', + model: '模型', + cronExpression: 'Cron 表达式', + enabled: '启用', + lastRun: '上次运行', + nextRun: '下次运行', + maxResults: '最大结果数', + noPlans: '暂无定时测试计划', + confirmDelete: '确定要删除此计划吗?', + createSuccess: '计划创建成功', + updateSuccess: '计划更新成功', + deleteSuccess: '计划删除成功', + results: '测试结果', + noResults: '暂无测试结果', + responseText: '响应', + errorMessage: '错误', + success: '成功', + failed: '失败', + running: '运行中', + schedule: '定时测试', + cronHelp: '标准 5 字段 cron 表达式(例如 */30 * * * *)', + cronTooltipTitle: 'Cron 表达式示例:', + cronTooltipMeaning: '用于定义自动执行测试的时间规则,格式依次为:分钟 小时 日 月 星期。', + cronTooltipExampleEvery30Min: '*/30 * * * *:每 30 分钟运行一次', + cronTooltipExampleHourly: '0 * * * *:每小时整点运行一次', + cronTooltipExampleDaily: '0 9 * * *:每天 09:00 运行一次', + cronTooltipExampleWeekly: '0 9 * * 1:每周一 09:00 运行一次', + cronTooltipRange: '推荐填写范围:使用标准 5 字段 cron;如果只是健康检查,建议从每 30 分钟、每 1 小时或每天固定时间开始,不建议一开始就设置过高频率。', + maxResultsTooltipTitle: '最大结果数说明:', + maxResultsTooltipMeaning: '用于限制单个计划最多保留多少条历史测试结果,避免结果列表无限增长。', + maxResultsTooltipBody: '系统只会保留最近的测试结果;当保存数量超过这个值时,更早的历史记录会自动清理,避免列表过长和存储持续增长。', + maxResultsTooltipExample: '例如填写 100,表示最多保存最近 100 次测试结果;第 101 次结果写入后,最早的一条会被清理。', + maxResultsTooltipRange: '推荐填写范围:一般可填 20 到 200。只关注近期可用性时可填 20-50;需要回看较长时间的波动趋势时可填 100-200。', + autoRecover: '自动恢复', + autoRecoverHelp: '测试成功后自动恢复异常状态的账号' + }, + + // Proxies Management + proxies: { + title: 'IP管理', + description: '管理代理服务器配置', + createProxy: '添加代理', + editProxy: '编辑代理', + deleteProxy: '删除代理', + ad: { + inline: '正在寻找合适的代理 IP?' + }, + deleteConfirmMessage: "确定要删除代理 '{name}' 吗?", + testProxy: '测试代理', + dataImport: '导入', + dataExportSelected: '导出选中', + dataImportTitle: '导入代理', + dataImportHint: '上传代理导出的 JSON 文件以批量导入代理。', + dataImportWarning: '导入将创建或复用代理,保留状态并在完成后自动触发延迟检测。', + dataImportFile: '数据文件', + dataImportButton: '开始导入', + dataImporting: '导入中...', + dataImportSelectFile: '请选择数据文件', + dataImportParseFailed: '数据解析失败', + dataImportFailed: '数据导入失败', + dataImportResult: '导入结果', + dataImportResultSummary: '创建 {proxy_created},复用 {proxy_reused},失败 {proxy_failed}', + dataImportErrors: '失败详情', + dataImportSuccess: '导入完成:创建 {proxy_created},复用 {proxy_reused}', + dataImportCompletedWithErrors: '导入完成但有错误:失败 {proxy_failed}', + dataExport: '导出', + dataExportConfirmMessage: '导出的数据包含代理的敏感信息,请妥善保存。', + dataExportConfirm: '确认导出', + dataExported: '数据导出成功', + dataExportFailed: '数据导出失败', + columns: { + name: '名称', + protocol: '协议', + address: '地址', + auth: '认证', + location: '地理位置', + status: '状态', + accounts: '账号数', + latency: '延迟', + expiry: '有效期', + createdAt: '创建时间', + actions: '操作', + nameLabel: '名称', + namePlaceholder: '请输入代理名称', + protocolLabel: '协议', + selectProtocol: '选择协议', + hostLabel: '主机', + hostPlaceholder: '请输入主机地址', + portLabel: '端口', + portPlaceholder: '请输入端口', + usernameLabel: '用户名(可选)', + usernamePlaceholder: '请输入用户名', + passwordLabel: '密码(可选)', + passwordPlaceholder: '请输入密码', + priorityLabel: '优先级', + statusLabel: '状态' + }, + filters: { + protocol: '协议', + allProtocols: '全部协议', + status: '状态', + allStatuses: '全部状态' + }, + // Additional keys used in ProxiesView + copyProxyUrl: '复制代理 URL', + urlCopied: '代理 URL 已复制', + allProtocols: '全部协议', + allStatus: '全部状态', + searchProxies: '搜索代理...', + protocols: { + http: 'HTTP', + https: 'HTTPS', + socks5: 'SOCKS5', + socks5h: 'SOCKS5H (远程 DNS)', + }, + name: '名称', + protocol: '协议', + host: '主机', + port: '端口', + username: '用户名(可选)', + password: '密码(可选)', + status: '状态', + enterProxyName: '请输入代理名称', + optionalAuth: '可选认证信息', + leaveEmptyToKeep: '留空保持不变', + form: { + hostPlaceholder: '请输入主机地址', + portPlaceholder: '请输入端口' + }, + noProxiesYet: '暂无代理', + createFirstProxy: '添加您的第一个代理以开始使用。', + testConnection: '测试连接', + qualityCheck: '质量检测', + batchQualityCheck: '批量质量检测', + batchTest: '批量测试', + testFailed: '失败', + latencyFailed: '链接失败', + batchTestEmpty: '暂无可测试的代理', + batchTestDone: '批量测试完成,共测试 {count} 个代理', + batchTestFailed: '批量测试失败', + batchDeleteAction: '删除', + batchDelete: '批量删除', + batchDeleteConfirm: '确定删除选中的 {count} 个代理吗?已被账号使用的将自动跳过。', + batchDeleteDone: '已删除 {deleted} 个代理,跳过 {skipped} 个', + batchDeleteSkipped: '已跳过 {skipped} 个代理', + batchDeleteFailed: '批量删除失败', + deleteBlockedInUse: '该代理已有账号使用,无法删除', + accountsTitle: '使用该IP的账号', + accountsEmpty: '暂无账号使用此代理', + accountsFailed: '获取账号列表失败', + accountName: '账号名称', + accountPlatform: '所属平台', + accountNotes: '备注', + // Batch import + standardAdd: '标准添加', + batchAdd: '快捷添加', + batchInput: '代理列表', + batchInputPlaceholder: + "每行输入一个代理,支持以下格式:\nsocks5://user:pass{'@'}192.168.1.1:1080\nhttp://192.168.1.1:8080\nhttps://user:pass{'@'}proxy.example.com:443", + batchInputHint: "支持 http、https、socks5 协议,格式:协议://[用户名:密码{'@'}]主机:端口", + parsedCount: '有效 {count} 个', + invalidCount: '无效 {count} 个', + duplicateCount: '重复 {count} 个', + importing: '导入中...', + importProxies: '导入 {count} 个代理', + batchImportSuccess: '成功导入 {created} 个代理,跳过 {skipped} 个重复', + batchImportAllSkipped: '全部 {skipped} 个代理已存在,跳过导入', + failedToImport: '批量导入失败', + // Other messages + saving: '保存中...', + testing: '测试中...', + creating: '创建中...', + updating: '更新中...', + noProxies: '暂无代理', + noProxiesDescription: '添加代理服务器以增强 API 访问稳定性。', + proxyCreated: '代理添加成功', + proxyUpdated: '代理更新成功', + proxyDeleted: '代理删除成功', + proxyWorking: '代理连接正常', + proxyWorkingWithLatency: '代理连接正常,延迟 {latency}ms', + proxyTestFailed: '代理测试失败', + qualityCheckDone: '质量检测完成:评分 {score}({grade})', + qualityCheckFailed: '代理质量检测失败', + batchQualityDone: '批量质量检测完成,共检测 {count} 个;优质 {healthy} 个,告警 {warn} 个,挑战 {challenge} 个,异常 {failed} 个', + batchQualityFailed: '批量质量检测失败', + batchQualityEmpty: '暂无可检测质量的代理', + qualityReportTitle: '代理质量检测报告', + qualityGrade: '等级 {grade}', + qualityExitIP: '出口 IP', + qualityCountry: '出口地区', + qualityBaseLatency: '基础延迟', + qualityCheckedAt: '检测时间', + qualityTableTarget: '检测项', + qualityTableStatus: '状态', + qualityTableLatency: '延迟', + qualityTableMessage: '说明', + qualityInline: '质量 {grade}/{score}', + qualityStatusHealthy: '优质', + qualityStatusPass: '通过', + qualityStatusWarn: '告警', + qualityStatusFail: '失败', + qualityStatusChallenge: '挑战', + qualityTargetBase: '基础连通性', + proxyCreatedSuccess: '代理添加成功', + proxyUpdatedSuccess: '代理更新成功', + proxyDeletedSuccess: '代理删除成功', + testSuccess: '代理测试通过', + failedToLoad: '加载代理列表失败', + failedToSave: '保存代理失败', + failedToDelete: '删除代理失败', + failedToCreate: '创建代理失败', + failedToUpdate: '更新代理失败', + failedToTest: '测试代理失败', + nameRequired: '请输入代理名称', + hostRequired: '请输入主机地址', + portInvalid: '端口必须在 1-65535 之间', + deleteConfirm: "确定要删除代理 '{name}' 吗?使用此代理的账号将被移除代理设置。", + neverExpires: '永不过期', + expired: '已过期', + overdueDays: '已超期 {days} 天', + expiringInDays: '{days} 天后到期', + remainingDays: '剩余 {days} 天', + expiresAt: '有效期', + nDays: '{days} 天', + expiryDaysPlaceholder: '自定义天数,留空 = 永不过期', + expiryWarnDays: '到期提醒提前天数', + fallbackMode: '失败回退', + fallbackNone: '不回退', + fallbackProxy: '指定备用代理', + fallbackDirect: '回退直连', + backupProxy: '备用代理', + }, + + // Redeem Codes Management + redeem: { + title: '兑换码管理', + description: '生成和管理兑换码', + generateCodes: '生成兑换码', + columns: { + code: '兑换码', + type: '类型', + value: '面值', + status: '状态', + usedBy: '使用者', + usedAt: '使用时间', + expiresAt: '过期时间', + createdAt: '创建时间', + actions: '操作' + }, + types: { + balance: '余额', + concurrency: '并发数', + subscription: '订阅', + invitation: '邀请码', + // 管理员在用户管理页面调整余额/并发时产生的记录 + admin_balance: '余额(管理员)', + admin_concurrency: '并发数(管理员)' + }, + // 用于选择器和筛选器的直接键 + balance: '余额', + concurrency: '并发数', + subscription: '订阅', + invitation: '邀请码', + invitationHint: '邀请码用于限制用户注册,使用后自动标记为已使用。', + allTypes: '全部类型', + allStatus: '全部状态', + unused: '未使用', + used: '已使用', + searchCodes: '搜索兑换码或邮箱...', + exportCsv: '导出 CSV', + batchUpdate: '批量修改', + batchUpdateTitle: '批量修改兑换码', + selectedCount: '已选择 {count} 个兑换码', + clearSelection: '清空选择', + selectCodesFirst: '请先选择兑换码', + noBatchFieldsSelected: '请至少勾选一个要修改的字段', + batchUpdateSuccess: '成功修改 {count} 个兑换码', + failedToBatchUpdate: '批量修改兑换码失败', + batchFields: { + status: '状态', + expiresAt: '过期时间', + notes: '备注', + group: '分组' + }, + batchNotesPlaceholder: '输入新的备注,留空可清空备注', + clearGroup: '清空分组', + deleteAllUnused: '删除全部未使用', + deleteCodeConfirm: '确定要删除此兑换码吗?此操作无法撤销。', + deleteAllUnusedConfirm: '确定要删除全部未使用的兑换码吗?此操作无法撤销。', + deleteAll: '全部删除', + generateCodesTitle: '生成兑换码', + generatedSuccessfully: '生成成功', + codesCreated: '已创建 {count} 个兑换码', + codeType: '类型', + amount: '金额 ($)', + value: '面值', + count: '数量', + generate: '生成', + copyAll: '全部复制', + download: '下载', + codesExported: '兑换码导出成功', + codeDeleted: '兑换码删除成功', + codesDeleted: '成功删除 {count} 个未使用的兑换码', + noUnusedCodes: '没有未使用的兑换码可删除', + userPrefix: '用户 #{id}', + failedToExport: '导出兑换码失败', + failedToDeleteUnused: '删除未使用的兑换码失败', + failedToCopy: '复制失败', + selectGroup: '选择分组', + selectGroupPlaceholder: '选择订阅分组', + validityDays: '有效天数', + codeExpiry: '兑换码过期', + neverExpires: '永不过期', + expiryPresetDays: '{days} 天', + customExpiry: '自定义', + customExpiryDays: '自定义天数', + expiryDaysRequired: '请输入有效的过期天数', + groupRequired: '请选择订阅分组', + days: '天', + status: { + unused: '未使用', + used: '已使用', + expired: '已过期', + disabled: '已禁用' + }, + form: { + typeLabel: '类型', + selectType: '选择类型', + valueLabel: '面值', + valuePlaceholder: '请输入面值', + balanceHint: '余额金额(美元)', + concurrencyHint: '并发数增量', + countLabel: '数量', + countPlaceholder: '请输入数量', + countHint: '要生成的兑换码数量', + prefixLabel: '前缀(可选)', + prefixPlaceholder: '例如:GIFT', + expiresLabel: '过期时间(可选)' + }, + filters: { + type: '类型', + allTypes: '全部类型', + status: '状态', + allStatuses: '全部状态', + search: '搜索兑换码' + }, + generating: '生成中...', + copyCode: '复制', + copied: '已复制!', + disableCode: '禁用', + enableCode: '启用', + deleteCode: '删除', + deleteConfirmMessage: '确定要删除此兑换码吗?', + noCodes: '暂无兑换码', + noCodesDescription: '生成兑换码以向用户分发余额或并发数。', + codesGeneratedSuccess: '兑换码生成成功,共 {count} 个', + codeDisabledSuccess: '兑换码已禁用', + codeEnabledSuccess: '兑换码已启用', + codeDeletedSuccess: '兑换码删除成功', + failedToLoad: '加载兑换码列表失败', + failedToGenerate: '生成兑换码失败', + failedToUpdate: '更新兑换码失败', + failedToDelete: '删除兑换码失败' + }, + + // Announcements + announcements: { + title: '公告管理', + description: '创建公告并按条件投放', + createAnnouncement: '创建公告', + editAnnouncement: '编辑公告', + deleteAnnouncement: '删除公告', + searchAnnouncements: '搜索公告...', + status: '状态', + allStatus: '全部状态', + columns: { + title: '标题', + status: '状态', + notifyMode: '通知方式', + targeting: '展示条件', + timeRange: '有效期', + createdAt: '创建时间', + actions: '操作' + }, + statusLabels: { + draft: '草稿', + active: '展示中', + archived: '已归档' + }, + notifyModeLabels: { + silent: '静默', + popup: '弹窗' + }, + form: { + title: '标题', + content: '内容(支持 Markdown)', + status: '状态', + notifyMode: '通知方式', + notifyModeHint: '弹窗模式会自动弹出通知给用户', + startsAt: '开始时间', + endsAt: '结束时间', + startsAtHint: '留空表示立即生效', + endsAtHint: '留空表示永久生效', + targetingMode: '展示条件', + targetingAll: '所有用户', + targetingCustom: '按条件', + addOrGroup: '添加 OR 条件组', + addAndCondition: '添加 AND 条件', + conditionType: '条件类型', + conditionSubscription: '订阅套餐', + conditionBalance: '余额', + operator: '运算符', + balanceValue: '余额阈值', + selectPackages: '选择套餐' + }, + operators: { + gt: '>', + gte: '≥', + lt: '<', + lte: '≤', + eq: '=' + }, + targetingSummaryAll: '全部用户', + targetingSummaryCustom: '自定义({groups} 组)', + timeImmediate: '立即', + timeNever: '永久', + readStatus: '已读情况', + eligible: '符合条件', + readAt: '已读时间', + unread: '未读', + searchUsers: '搜索用户...', + failedToLoad: '加载公告失败', + failedToCreate: '创建公告失败', + failedToUpdate: '更新公告失败', + failedToDelete: '删除公告失败', + failedToLoadReadStatus: '加载已读情况失败', + deleteConfirm: '确定要删除该公告吗?此操作无法撤销。' + }, + + // Promo Codes + promo: { + title: '优惠码管理', + description: '创建和管理注册优惠码', + createCode: '创建优惠码', + editCode: '编辑优惠码', + deleteCode: '删除优惠码', + searchCodes: '搜索优惠码...', + allStatus: '全部状态', + columns: { + code: '优惠码', + bonusAmount: '赠送金额', + maxUses: '最大使用次数', + usedCount: '已使用', + usage: '使用量', + status: '状态', + expiresAt: '过期时间', + createdAt: '创建时间', + actions: '操作' + }, + // 表单标签(扁平结构便于模板使用) + code: '优惠码', + autoGenerate: '留空自动生成', + codePlaceholder: '输入优惠码或留空', + bonusAmount: '赠送金额 ($)', + maxUses: '最大使用次数', + zeroUnlimited: '0 = 无限制', + expiresAt: '过期时间', + notes: '备注', + notesPlaceholder: '可选备注信息', + status: '状态', + neverExpires: '永不过期', + // 状态标签 + statusActive: '启用', + statusDisabled: '禁用', + statusExpired: '已过期', + statusMaxUsed: '已用完', + // 使用记录 + usageRecords: '使用记录', + viewUsages: '查看使用记录', + noUsages: '暂无使用记录', + userPrefix: '用户 #{id}', + copied: '已复制!', + // 消息 + noCodesYet: '暂无优惠码', + createFirstCode: '创建您的第一个优惠码,为新用户提供注册奖励。', + codeCreated: '优惠码创建成功', + codeUpdated: '优惠码更新成功', + codeDeleted: '优惠码删除成功', + deleteCodeConfirm: '确定要删除此优惠码吗?此操作无法撤销。', + copyRegisterLink: '复制注册链接', + registerLinkCopied: '注册链接已复制到剪贴板', + failedToLoad: '加载优惠码失败', + failedToCreate: '创建优惠码失败', + failedToUpdate: '更新优惠码失败', + failedToDelete: '删除优惠码失败', + failedToLoadUsages: '加载使用记录失败' + }, + + // Usage Records + usage: { + title: '使用记录', + description: '查看和管理所有用户的使用记录', + userFilter: '用户', + searchUserPlaceholder: '按邮箱搜索用户...', + searchApiKeyPlaceholder: '按名称搜索 API 密钥...', + searchAccountPlaceholder: '按名称搜索账号...', + selectedUser: '已选择', + user: '用户', + account: '账户', + group: '分组', + requestId: '请求ID', + requestIdCopied: '请求ID已复制', + allModels: '全部模型', + allAccounts: '全部账户', + allGroups: '全部分组', + allTypes: '全部类型', + inputCost: '输入费用', + outputCost: '输出费用', + cacheCreationCost: '缓存创建费用', + cacheReadCost: '缓存读取费用', + inputTokens: '输入 Token', + outputTokens: '输出 Token', + cacheCreationTokens: '缓存创建 Token', + cacheCreation5mTokens: '缓存创建', + cacheCreation1hTokens: '缓存创建', + cacheReadTokens: '缓存读取 Token', + failedToLoad: '加载使用记录失败', + billingType: '计费类型', + allBillingTypes: '全部计费类型', + billingTypeBalance: '钱包余额', + billingTypeSubscription: '订阅套餐', + billingMode: '计费模式', + billingModeToken: '按量', + billingModePerRequest: '按次', + billingModeImage: '按次(图片)', + allBillingModes: '全部计费模式', + ipAddress: 'IP', + clickToViewBalance: '点击查看充值记录', + failedToLoadUser: '加载用户信息失败', + userDeletedBadge: '已删除', + cleanup: { + button: '清理', + title: '清理使用记录', + warning: '清理不可恢复,且会影响历史统计回看。', + submit: '提交清理', + submitting: '提交中...', + confirmTitle: '确认清理', + confirmMessage: '确定要提交清理任务吗?清理不可恢复。', + confirmSubmit: '确认清理', + cancel: '取消任务', + cancelConfirmTitle: '确认取消', + cancelConfirmMessage: '确定要取消该清理任务吗?', + cancelConfirm: '确认取消', + cancelSuccess: '清理任务已取消', + cancelFailed: '取消清理任务失败', + recentTasks: '最近清理任务', + loadingTasks: '正在加载任务...', + noTasks: '暂无清理任务', + range: '时间范围', + deletedRows: '删除数量', + missingRange: '请选择时间范围', + submitSuccess: '清理任务已创建', + submitFailed: '创建清理任务失败', + loadFailed: '加载清理任务失败', + status: { + pending: '待执行', + running: '执行中', + succeeded: '已完成', + failed: '失败', + canceled: '已取消' + } + } + }, + + // Ops Monitoring +} diff --git a/frontend/src/i18n/locales/zh/admin/settings.ts b/frontend/src/i18n/locales/zh/admin/settings.ts new file mode 100644 index 0000000000..bf848d1cc3 --- /dev/null +++ b/frontend/src/i18n/locales/zh/admin/settings.ts @@ -0,0 +1,1264 @@ +export default { + settings: { + title: '系统设置', + description: '管理注册、邮箱验证、默认值和 SMTP 设置', + tabs: { + general: '通用设置', + agreement: '登录条款', + features: '功能开关', + security: '安全与认证', + users: '用户默认值', + gateway: '网关服务', + email: '邮件设置', + backup: '数据备份', + payment: '支付设置', + }, + features: { + channelMonitor: { + title: '渠道监控', + description: '定期对配置的渠道发起健康检查,向用户展示可用性与延迟。关闭后调度器停止扫描,用户端列表为空。', + configureLink: '前往 渠道管理 > 渠道监控 配置监控项', + enabled: '启用渠道监控', + enabledHint: '关闭后后台不再执行定时检测,已有数据保留。', + defaultInterval: '默认检测间隔(秒)', + defaultIntervalHint: '新建渠道监控时表单的默认值,可被单个渠道覆盖。范围 15 – 3600 秒。', + }, + availableChannels: { + title: '可用渠道', + description: '向已登录用户展示他们能访问的渠道、模型和定价聚合视图。默认关闭。', + configureLink: '前往 渠道管理 > 渠道定价 配置模型价格', + enabled: '启用可用渠道', + enabledHint: '关闭后用户端侧边栏入口隐藏,接口返回空数组。', + }, + riskControl: { + title: '风控中心', + description: '启用内容审计菜单和全端点请求审核入口。默认关闭。', + configureLink: '前往 风控中心 配置内容审计', + enabled: '启用风控中心', + enabledHint: '关闭后管理员侧边栏入口隐藏,网关内容审计不会执行。', + cyberSessionBlock: 'cyber 会话自动屏蔽', + cyberSessionBlockHint: '开启后,被上游网络安全策略(cyber_policy)拦截的会话将在 TTL 内被本地屏蔽,不再发往上游。仅屏蔽该会话,不影响同 Key 其他会话。', + cyberSessionBlockTTL: '屏蔽时长(秒)', + }, + affiliate: { + title: '邀请返利', + description: '老用户邀请新用户注册,新用户充值后老用户按比例获得返利额度。默认关闭。', + enabled: '启用邀请返利', + enabledHint: '关闭后用户菜单中的邀请页面入口隐藏、注册时忽略邀请码、新充值不再产生返利。已有返利额度仍可转入余额。', + rebateRate: '全局返利比例', + rebateRateHint: '充值后返给邀请人的默认比例(0-100%,例如填写 10 表示返利 10%)。', + freezeHours: '返利冻结期(小时)', + freezeHoursDesc: '新产生的返利将在冻结期内无法提现。0 = 不冻结。', + durationDays: '返利有效期(天)', + durationDaysDesc: '被邀请用户注册后多少天内的充值产生返利。0 = 永久有效。', + perInviteeCap: '单人返利上限', + perInviteeCapDesc: '每个被邀请用户最多产生的返利总额。0 = 无上限。', + customUsers: { + title: '专属用户配置', + description: '为指定用户设置专属邀请码或专属返利比例。仅展示已设置过专属配置的用户。', + addButton: '添加专属用户', + searchPlaceholder: '搜索邮箱或用户名', + batchButton: '批量设置比例(已选 {count})', + empty: '暂无专属配置用户', + customBadge: '自定义', + useGlobal: '沿用全局', + resetTitle: '重置该用户的专属配置', + resetMessage: '确认将 {email} 的专属配置全部重置为默认?\n• 专属返利比例将清除(沿用全局)\n• 邀请码将重新生成为系统随机码(已分发的旧邀请链接将失效)', + totalLabel: '共 {total} 条', + col: { + email: '邮箱', + username: '用户名', + code: '邀请码', + rate: '专属比例', + actions: '操作', + }, + }, + modal: { + addTitle: '添加专属用户', + editTitle: '编辑专属配置', + userLabel: '用户', + userPlaceholder: '搜索邮箱或用户名', + changeUser: '更换用户', + codeLabel: '专属邀请码(可选)', + codePlaceholder: '例如 VIP2026', + codeHint: '4-32 位,仅支持大写字母、数字、下划线、连字符;留空表示不修改;输入将自动转大写。', + rateLabel: '专属返利比例(可选)', + ratePlaceholder: '例如 30', + rateHint: '0-100%;留空(编辑模式下)表示清除专属比例并沿用全局。', + errorBadRate: '请输入 0-100 之间的比例', + errorEmpty: '至少填写一项:专属邀请码或专属返利比例', + }, + batchModal: { + title: '批量设置专属比例(已选 {count} 个用户)', + hint: '为所选用户统一设置专属返利比例。', + placeholder: '例如 30', + clearHint: '留空提交将清除所选用户的专属比例。', + }, + }, + }, + emailTabDisabledTitle: '邮箱验证未启用', + emailTabDisabledHint: '请在「安全与认证」选项卡中启用邮箱验证后,再配置 SMTP 设置。', + registration: { + title: '注册设置', + description: '控制用户注册和验证', + enableRegistration: '开放注册', + enableRegistrationHint: '允许新用户注册', + emailVerification: '邮箱验证', + emailVerificationHint: '新用户注册时需要验证邮箱', + emailSuffixWhitelist: '邮箱域名白名单', + emailSuffixWhitelistHint: + "仅允许使用指定域名的邮箱注册账号(例如 {'@'}qq.com, {'@'}gmail.com, *.edu.cn)", + emailSuffixWhitelistPlaceholder: "{'@'}example.com, *.edu.cn", + emailSuffixWhitelistInputHint: '留空则不限制。使用 *.edu.cn 可匹配 edu.cn 及其子域名。', + promoCode: '优惠码', + promoCodeHint: '允许用户在注册时使用优惠码', + invitationCode: '邀请码注册', + invitationCodeHint: '开启后,用户注册时需要填写有效的邀请码', + passwordReset: '忘记密码', + passwordResetHint: '允许用户通过邮箱重置密码', + frontendUrl: '前端地址', + frontendUrlPlaceholder: 'https://example.com', + frontendUrlHint: '用于生成邮件中的密码重置链接,例如 https://example.com', + totp: '双因素认证 (2FA)', + totpHint: '允许用户使用 Google Authenticator 等应用进行二次验证', + totpKeyNotConfigured: + '请先在环境变量中配置 TOTP_ENCRYPTION_KEY。使用命令 openssl rand -hex 32 生成密钥。' + }, + turnstile: { + title: 'Cloudflare Turnstile', + description: '登录和注册的机器人防护', + enableTurnstile: '启用 Turnstile', + enableTurnstileHint: '需要 Cloudflare Turnstile 验证', + siteKey: '站点密钥', + secretKey: '私密密钥', + siteKeyHint: '从 Cloudflare Dashboard 获取', + cloudflareDashboard: 'Cloudflare Dashboard', + secretKeyHint: '服务端验证密钥(请保密)', + secretKeyConfiguredHint: '密钥已配置,留空以保留当前值。' + }, + apiKeyAcl: { + title: 'API Key IP 访问控制', + description: '控制 API Key 白名单和黑名单使用哪个客户端 IP 判断', + trustForwardedIp: '信任反代传递的客户端 IP', + trustForwardedIpHint: + '默认关闭。仅在源站只允许 Cloudflare 或 Nginx 反代访问时开启;开启后 API Key IP 白/黑名单会使用 CF-Connecting-IP、X-Real-IP 或 X-Forwarded-For,与使用记录中的请求 IP 保持一致。' + }, + linuxdo: { + title: 'LinuxDo Connect 登录', + description: '配置 LinuxDo Connect OAuth,用于 Sub2API 用户登录', + enable: '启用 LinuxDo 登录', + enableHint: '在登录/注册页面显示 LinuxDo 登录入口', + clientId: 'Client ID', + clientIdPlaceholder: '例如:hprJ5pC3...', + clientIdHint: '从 Connect.Linux.Do 后台获取', + clientSecret: 'Client Secret', + clientSecretPlaceholder: '********', + clientSecretHint: '用于后端交换 token(请保密)', + clientSecretConfiguredPlaceholder: '********', + clientSecretConfiguredHint: '密钥已配置,留空以保留当前值。', + redirectUrl: '回调地址(Redirect URL)', + redirectUrlPlaceholder: 'https://your-domain.com/api/v1/auth/oauth/linuxdo/callback', + redirectUrlHint: '需与 Connect.Linux.Do 中配置的回调地址一致(必须是 http(s) 完整 URL)', + quickSetCopy: '使用当前站点生成并复制', + redirectUrlSetAndCopied: '已使用当前站点生成回调地址并复制到剪贴板' + }, + dingtalk: { + title: '钉钉登录', + description: '配置钉钉 OAuth,用于 Sub2API 用户登录', + enable: '启用钉钉登录-企业内部应用', + enableHint: '在登录/注册页面显示钉钉登录入口', + clientId: 'Client ID(AppKey)', + clientIdPlaceholder: '例如:dingxxxxxxxxxxxxxxxx', + clientIdHint: '从钉钉开放平台应用详情页获取', + clientSecret: 'Client Secret(AppSecret)', + clientSecretPlaceholder: '********', + clientSecretHint: '用于后端交换 token(请保密)', + clientSecretConfiguredPlaceholder: '********', + clientSecretConfiguredHint: '密钥已配置,留空以保留当前值。', + redirectUrl: '回调地址(Redirect URL)', + redirectUrlPlaceholder: 'https://your-domain.com/api/v1/auth/oauth/dingtalk/callback', + redirectUrlHint: '需与钉钉开放平台中配置的回调地址一致(必须是 http(s) 完整 URL)', + corpPolicy: { + label: '企业限制策略', + hint: '控制哪些钉钉账号(企业)可以登录', + none: '不限制(所有钉钉账号均可登录)', + internalOnly: '仅本企业(Internal Only)' + }, + bypassRegistration: '开放钉钉注册', + bypassRegistrationHint: '即使「开放注册」关闭时也可以通过钉钉登录来注册', + syncDisplayName: '同步钉钉姓名', + syncDisplayNameHint: '登录时将钉钉姓名写入 username 字段(同时记录到 dingtalk_name 属性)', + syncCorpEmail: '同步企业邮箱', + syncCorpEmailHint: '登录时将钉钉企业邮箱写入 dingtalk_email 属性(不影响登录邮箱)', + syncCorpEmailPermissionHint: '需在钉钉开放平台 → 应用 → 权限管理中为本应用申请「邮箱等个人信息(fieldEmail)」权限,否则 OAPI 不会返回企业邮箱字段', + syncDept: '同步部门', + syncDeptHint: '登录时将钉钉首个部门完整路径写入 dingtalk_department 属性(每次登录实时拉取)', + syncDeptPermissionHint: '需在钉钉开放平台 → 应用 → 权限管理中为本应用申请「通讯录部门信息读权限(qyapi_get_department_list)」,否则无法递归出部门路径', + syncDisplayNameTarget: '属性键', + syncDisplayNameTargetHint: '默认 dingtalk_name / 钉钉姓名;保存设置时按上述属性键和显示名称自动创建用户属性(已存在则仅同步显示名称)', + syncCorpEmailTarget: '属性键', + syncCorpEmailTargetHint: '默认 dingtalk_email / 钉钉企业邮箱;保存设置时按上述属性键和显示名称自动创建用户属性(已存在则仅同步显示名称)', + syncDeptTarget: '属性键', + syncDeptTargetHint: '默认 dingtalk_department / 钉钉部门;保存设置时按上述属性键和显示名称自动创建用户属性(已存在则仅同步显示名称)', + syncAttrDisplayName: '显示名称' + }, + oidc: { + title: 'OIDC 登录', + description: '配置标准 OIDC Provider(例如 Keycloak)', + enable: '启用 OIDC 登录', + enableHint: '在登录/注册页面显示 OIDC 登录入口', + providerName: 'Provider 名称', + providerNamePlaceholder: '例如 Keycloak', + clientId: 'Client ID', + clientIdPlaceholder: 'OIDC client id', + clientSecret: 'Client Secret', + clientSecretPlaceholder: '********', + clientSecretHint: '用于后端交换 token(请保密)', + clientSecretConfiguredPlaceholder: '********', + clientSecretConfiguredHint: '密钥已配置,留空以保留当前值。', + issuerUrl: 'Issuer URL', + issuerUrlPlaceholder: 'https://id.example.com/realms/main', + discoveryUrl: 'Discovery URL', + discoveryUrlPlaceholder: '可选,留空将基于 issuer 自动推导', + authorizeUrl: 'Authorize URL', + authorizeUrlPlaceholder: '可选,可通过 discovery 自动获取', + tokenUrl: 'Token URL', + tokenUrlPlaceholder: '可选,可通过 discovery 自动获取', + userinfoUrl: 'UserInfo URL', + userinfoUrlPlaceholder: '可选,可通过 discovery 自动获取', + jwksUrl: 'JWKS URL', + jwksUrlPlaceholder: '可选;启用严格 ID Token 校验时必填', + scopes: 'Scopes', + scopesPlaceholder: 'openid email profile', + scopesHint: '必须包含 openid', + redirectUrl: '后端回调地址(Redirect URL)', + redirectUrlPlaceholder: 'https://your-domain.com/api/v1/auth/oauth/oidc/callback', + redirectUrlHint: '必须与 OIDC Provider 中配置的回调地址一致', + quickSetCopy: '使用当前站点生成并复制', + redirectUrlSetAndCopied: '已使用当前站点生成回调地址并复制到剪贴板', + frontendRedirectUrl: '前端回调路径', + frontendRedirectUrlPlaceholder: '/auth/oidc/callback', + frontendRedirectUrlHint: '后端回调完成后重定向到此前端路径', + tokenAuthMethod: 'Token 鉴权方式', + clockSkewSeconds: '时钟偏移(秒)', + allowedSigningAlgs: '允许的签名算法', + allowedSigningAlgsPlaceholder: 'RS256,ES256,PS256', + usePkce: '启用 PKCE', + validateIdToken: '校验 ID Token', + requireEmailVerified: '要求邮箱已验证', + userinfoEmailPath: 'UserInfo 邮箱字段路径', + userinfoEmailPathPlaceholder: '例如 data.email', + userinfoIdPath: 'UserInfo ID 字段路径', + userinfoIdPathPlaceholder: '例如 data.id', + userinfoUsernamePath: 'UserInfo 用户名字段路径', + userinfoUsernamePathPlaceholder: '例如 data.username' + }, + defaults: { + title: '用户默认设置', + description: '新用户的默认值', + defaultBalance: '默认余额', + defaultBalanceHint: '新用户的初始余额', + affiliateRebateRate: '邀请返利比例', + affiliateRebateRateHint: '充值后返给邀请人的比例(0-100%,例如填写 10 表示返利 10%)', + defaultConcurrency: '默认并发数', + defaultConcurrencyHint: '新用户的最大并发请求数', + defaultUserRpmLimit: '默认用户 RPM 限制', + defaultUserRpmLimitHint: '新用户默认每分钟最大请求数,0 = 不限制;仅作用于新用户创建时初始化', + defaultSubscriptions: '默认订阅列表', + defaultSubscriptionsHint: '新用户创建或注册时自动分配这些订阅', + addDefaultSubscription: '添加默认订阅', + defaultSubscriptionsEmpty: '未配置默认订阅。新用户不会自动获得订阅套餐。', + defaultSubscriptionsDuplicate: '默认订阅存在重复分组:{groupId}。每个分组只能出现一次。', + subscriptionGroup: '订阅分组', + subscriptionValidityDays: '有效期(天)', + defaultPlatformQuotas: '默认平台限额(注册时分配)', + defaultPlatformQuotasHint: '新用户注册时自动写入平台限额记录;已有用户不受影响。留空 = 该平台该窗口不限制。', + platformQuotaNotice: '月限额为 30 天滚动窗口,非自然月', + }, + platformQuota: { + platform: '平台', + daily: '日限额 (USD)', + weekly: '周限额 (USD)', + monthly: '月限额 (USD, 30天滚动)', + placeholder: '不限', + }, + claudeCode: { + title: 'Claude Code 设置', + description: '控制 Claude Code 客户端访问要求', + minVersion: '最低版本号', + minVersionPlaceholder: '例如 2.1.63', + minVersionHint: '拒绝低于此版本的 Claude Code 客户端请求(semver 格式)。留空则不检查版本。', + maxVersion: '最高版本号', + maxVersionPlaceholder: '例如 2.5.0', + maxVersionHint: '拒绝高于此版本的 Claude Code 客户端请求(semver 格式)。留空则不限制最高版本。' + }, + scheduling: { + title: '网关调度设置', + description: '控制 API Key 的调度行为', + allowUngroupedKey: '允许未分组 Key 调度', + allowUngroupedKeyHint: '关闭后,未分配到任何分组的 API Key 将无法发起请求(返回 403)。建议保持关闭以确保所有 Key 都归属明确的分组。' + }, + gatewayForwarding: { + title: '请求转发行为', + description: '控制请求转发到上游 OAuth 账号时的行为', + fingerprintUnification: '指纹统一化', + fingerprintUnificationHint: '统一共享同一 OAuth 账号的用户的 X-Stainless-* 请求头。关闭后透传客户端原始请求头。', + metadataPassthrough: 'Metadata 透传', + metadataPassthroughHint: '透传客户端原始 metadata.user_id,不进行重写。可能提高上游缓存命中率。', + cchSigning: 'CCH 签名', + cchSigningHint: '对转发请求的 billing header 进行 CCH 哈希签名。关闭时保留原始占位符。', + claudeOAuthSystemPromptInjection: 'Claude OAuth System 注入', + claudeOAuthSystemPromptInjectionHint: '为非 Claude Code 客户端的 Claude OAuth 请求注入 Claude Code 形态的 system blocks。默认开启。', + claudeOAuthSystemPrompt: 'Claude OAuth 扩展提示词', + claudeOAuthSystemPromptPlaceholder: '留空时使用内置 Claude Code 扩展提示词。', + claudeOAuthSystemPromptHint: '兼容旧配置:仅控制第三个注入的 system block。', + claudeOAuthSystemPromptBlocks: 'Claude OAuth System Blocks', + claudeOAuthSystemPromptBlocksPlaceholder: '留空时使用内置 3 个 blocks。支持数组或 {"blocks": [...]}。', + claudeOAuthSystemPromptBlocksHint: '每个 block 会保存为带 enabled、type、text、可选 cache_control 的 JSON。{billing_header} 会按请求动态生成;Claude Code 身份提示词和扩展提示词可直接编辑,也可用预设恢复默认值。', + systemBlockTitle: 'System Block {index}', + systemBlockPreset: '预设', + systemBlockPresetBilling: 'Billing Header', + systemBlockPresetIdentity: 'Claude Code 身份提示词', + systemBlockPresetExpansion: 'Claude Code 扩展提示词', + systemBlockPresetCustom: '自定义', + systemBlockType: '类型', + systemBlockTypeText: '文本', + systemBlockText: '内容', + systemBlockCacheControl: 'Cache Control', + systemBlockHide: '隐藏 block 详情', + systemBlockShow: '展示 block 详情', + addSystemBlock: '添加 block', + resetSystemBlocks: '恢复默认', + cacheTTL5m: '5 分钟', + cacheTTL1h: '1 小时', + anthropicCacheTTL1hInjection: 'Anthropic 缓存 TTL 注入', + anthropicCacheTTL1hInjectionHint: '开启后,对 Anthropic OAuth/Setup Token 请求体中已有的 ephemeral 缓存块强制写入 1h;响应 usage 默认按 5m 回写计费,账号级 TTL 计费设置优先。', + rewriteMessageCacheControl: '改写消息缓存断点', + rewriteMessageCacheControlHint: '默认关闭,保留客户端在 messages 内容块中的 cache_control。开启后会清除客户端断点并注入代理断点,适合不自行管理缓存策略的客户端。', + clientDatelineNormalization: '客户端 dateline 归一化', + clientDatelineNormalizationHint: '默认开启。将 Anthropic OAuth/Setup Token 请求体中 "Today\'s date is …" 语句里的撇号与日期分隔符还原为 ASCII 撇号 + 短横线 (2026-07-01) 的规范形态,抹除某些客户端在检测到非官方 base URL 时注入的隐写指纹位。仅作用于 system prompt 与 块内,API Key 账号不受影响。', + antigravityUserAgentVersion: 'Antigravity UA 版本', + antigravityUserAgentVersionPlaceholder: '1.23.2', + antigravityUserAgentVersionHint: '留空时使用 ANTIGRAVITY_USER_AGENT_VERSION 或内置默认值 1.23.2;填写后后台设置优先。', + openaiCodexUserAgent: 'OpenAI Codex UA', + openaiCodexUserAgentPlaceholder: 'codex-tui/0.125.0 (Ubuntu 22.4.0; x86_64) xterm-256color (codex-tui; 0.125.0)', + openaiCodexUserAgentHint: '用于规避 OpenAI 上游 Cloudflare 对浏览器 UA 的访问质询。仅在检测到客户端 User-Agent 为浏览器(Mozilla/...)时生效,其他客户端原样透传。留空使用内置默认值。', + codexHardeningTitle: 'Codex 设置', + codexClientRestrictionTitle: 'Codex 客户端限制', + codexHardeningDesc: + '仅对已开启「仅允许 Codex 官方客户端」的 OpenAI OAuth 账号生效(全局)。在 User-Agent/Originator 之外,用版本区间、引擎指纹门与黑/白名单巩固判定。', + minCodexVersion: '最低 Codex 版本', + minCodexVersionPlaceholder: '例如 0.142.0', + maxCodexVersion: '最高 Codex 版本', + maxCodexVersionPlaceholder: '例如 0.200.0', + codexVersionHint: + '仅对官方客户端生效,校验其版本是否落在 [最低, 最高] 区间。留空表示该侧不限制。', + codexFingerprintSignals: 'Codex 引擎指纹信号', + codexFingerprintSignalsDesc: + '定义引擎指纹信号:勾「必须」的信号需全部命中(AND),每条 / 分隔的变体取或(OR);一条都不勾即不校验。默认只勾 x-codex- 前缀。类型:头精确 / 头前缀 / body 路径。', + codexFpTypeHeaderExact: '头精确', + codexFpTypeHeaderPrefix: '头前缀', + codexFpTypeBodyPath: 'body 路径', + codexFpMatchPlaceholder: '匹配,变体用 / 分隔(如 session-id / session_id 或 x-codex-)', + codexFpRequired: '必须', + codexFingerprintNoRequiredWarn: '未勾选任何「必须」信号——引擎指纹门当前不生效,等于放行所有通过身份/版本的候选。如需启用校验,请至少勾选一条信号。', + codexAllowAppServer: 'Codex app-server', + codexAllowAppServerDesc: + '放行内嵌 Codex 引擎、经 app-server 协议接入的第三方客户端(如 Claude Code 的 codex 插件)。默认关闭;开启后此类客户端通过引擎指纹门(下方信号列表)即放行,关闭则仅放行官方客户端与白名单。', + codexBlacklist: 'User-Agent/Originator 黑名单', + codexBlacklistDesc: + '命中任一字段即拒,优先于一切放行。originator 精确匹配,User-Agent 为包含匹配(多个用逗号分隔)。', + codexWhitelist: 'User-Agent/Originator 白名单', + codexWhitelistDesc: + '放行官方集之外的客户端:需 originator 精确,且每个 User-Agent 标记都命中。默认仍需过引擎指纹门,勾「跳过引擎指纹」可免。', + codexWhitelistSkipFingerprint: '跳过引擎指纹', + codexWhitelistSkipFingerprintTooltip: + '风险:勾选后该条仅凭 originator + User-Agent(均可伪造)放行,不再要求引擎指纹兜底。仅用于确属可信、但本身不发 codex 引擎指纹的第三方客户端。', + codexOriginatorPlaceholder: 'originator(精确,如 opencode)', + codexUaContainsPlaceholder: 'User-Agent 包含标记,逗号分隔(如 opencode/)', + codexAddRow: '添加一条', + codexRemoveRow: '删除', + }, + webSearchEmulation: { + title: 'Web Search 模拟', + description: '为不原生支持搜索的 Anthropic API Key 账号注入 web search 能力', + enabled: '启用 Web Search 模拟', + enabledHint: '全局开关。关闭后所有渠道和账号的 web search 模拟均不生效。', + providers: '搜索服务商', + addProvider: '添加服务商', + providerType: '服务商类型', + apiKey: 'API Key', + apiKeyPlaceholder: '输入 API Key', + apiKeyConfigured: '已配置', + showApiKey: '显示', + hideApiKey: '隐藏', + copyApiKey: '复制', + copied: '已复制', + quotaLimit: '配额上限', + quotaLimitHint: '留空表示无限制;填写时必须大于 0', + quotaLimitMustBePositive: '配额上限必须大于 0', + subscribedAt: '订阅时间', + subscribedAtHint: '配额从此日期起每月自动重置;留空则不自动重置', + quotaUsage: '用量', + resetUsage: '重置', + resetUsageConfirm: '确定要重置此服务商的用量计数吗?', + resetUsageSuccess: '用量已重置', + proxy: '代理', + removeProvider: '删除', + noProviders: '未配置搜索服务商', + test: '测试', + testDefaultQuery: '搜索今年世界大事件', + testing: '搜索中...', + testResultTitle: '搜索结果', + testResultProvider: '服务商', + testNoResults: '无搜索结果', + }, + site: { + title: '站点设置', + description: '自定义站点品牌', + backendMode: 'Backend 模式', + backendModeDescription: + '禁用用户注册、公开页面和自助服务功能。仅管理员可以登录和管理平台。', + siteName: '站点名称', + siteNameHint: '显示在邮件和页面标题中', + siteNamePlaceholder: 'Sub2API', + siteSubtitle: '站点副标题', + siteSubtitleHint: '显示在登录和注册页面', + siteSubtitlePlaceholder: '订阅转 API 转换平台', + apiBaseUrl: 'API 端点地址', + apiBaseUrlHint: '用于"使用密钥"、"导入到 CC Switch"和回调地址建议,留空则使用当前站点地址', + apiBaseUrlPlaceholder: 'https://api.example.com', + tablePreferencesTitle: '通用表格设置', + tablePreferencesDescription: '设置后台与用户侧表格组件的默认分页行为', + tableDefaultPageSize: '默认每页条数', + tableDefaultPageSizeHint: '必须为 5-1000 之间的整数', + tablePageSizeOptions: '可选每页条数列表', + tablePageSizeOptionsPlaceholder: '10, 20, 50, 100', + tablePageSizeOptionsHint: '使用英文逗号分隔,取值范围 5-1000,保存时会自动去重并排序', + tableDefaultPageSizeRangeError: '默认每页条数必须在 {min}-{max} 之间', + tablePageSizeOptionsFormatError: '可选每页条数格式无效,请输入 {min}-{max} 之间的整数并用英文逗号分隔', + customEndpoints: { + title: '自定义端点', + description: '添加额外的 API 端点地址,用户可在「API Keys」页面快速复制', + itemLabel: '端点 #{n}', + name: '名称', + namePlaceholder: '如:OpenAI Compatible', + endpointUrl: '端点地址', + endpointUrlPlaceholder: 'https://api2.example.com', + descriptionLabel: '介绍', + descriptionPlaceholder: '如:支持 OpenAI 格式请求', + add: '添加端点', + }, + contactInfo: '客服联系方式', + contactInfoPlaceholder: '例如:QQ: 123456789', + contactInfoHint: '填写客服联系方式,将展示在兑换页面、个人资料等位置', + docUrl: '文档链接', + docUrlHint: '文档网站的链接。留空则隐藏文档链接。', + docUrlPlaceholder: 'https://docs.example.com', + siteLogo: '站点Logo', + uploadImage: '上传图片', + remove: '移除', + logoHint: 'PNG、JPG 或 SVG 格式,最大 300KB。建议:80x80px 正方形图片。', + logoSizeError: '图片大小超过 300KB 限制({size}KB)', + logoTypeError: '请选择图片文件', + logoReadError: '读取图片文件失败', + homeContent: '首页内容', + homeContentPlaceholder: + '在此输入首页内容,支持 Markdown & HTML 代码。如果输入的是一个链接,则会使用该链接作为 iframe 的 src 属性。', + homeContentHint: + '自定义首页内容,支持 Markdown/HTML。如果输入的是链接(以 http:// 或 https:// 开头),则会使用该链接作为 iframe 的 src 属性,这允许你设置任意网页作为首页。设置后首页的状态信息将不再显示。', + homeContentIframeWarning: + '⚠️ iframe 模式提示:部分网站设置了 X-Frame-Options 或 CSP 安全策略,禁止被嵌入到 iframe 中。如果页面显示空白或报错,请确认目标网站允许被嵌入,或考虑使用 HTML 模式自行构建页面内容。', + hideCcsImportButton: '隐藏 CCS 导入按钮', + hideCcsImportButtonHint: '启用后将在 API Keys 页面隐藏"导入 CCS"按钮' + }, + purchase: { + title: '充值/订阅页面', + description: '在侧边栏展示“充值/订阅”入口,并在页面内通过 iframe 打开指定链接', + enabled: '显示充值/订阅入口', + enabledHint: '仅在标准模式(非简单模式)下展示', + url: '充值/订阅页面 URL', + urlPlaceholder: 'https://example.com/purchase', + urlHint: '必须是完整的 http(s) 链接', + iframeWarning: + '⚠️ iframe 提示:部分网站会通过 X-Frame-Options 或 CSP(frame-ancestors)禁止被 iframe 嵌入,出现空白时可引导用户使用”新窗口打开”。', + integrationDoc: '支付集成文档', + integrationDocHint: '包含接口说明、幂等语义及示例代码' + }, + soraClient: { + title: 'Sora 客户端', + description: '控制是否在侧边栏展示 Sora 客户端入口', + enabled: '启用 Sora 客户端', + enabledHint: '开启后,侧边栏将显示 Sora 入口,用户可访问 Sora 功能' + }, + customMenu: { + title: '自定义菜单页面', + description: '添加自定义 iframe 页面到侧边栏导航。每个页面可以设置为普通用户或管理员可见。', + itemLabel: '菜单项 #{n}', + name: '菜单名称', + namePlaceholder: '如:帮助中心', + url: '页面 URL', + urlPlaceholder: 'https://example.com/page', + iconSvg: 'SVG 图标', + iconSvgPlaceholder: '...', + iconPreview: '图标预览', + uploadSvg: '上传 SVG', + removeSvg: '清除', + visibility: '可见角色', + visibilityUser: '普通用户', + visibilityAdmin: '管理员', + add: '添加菜单项', + remove: '删除', + moveUp: '上移', + moveDown: '下移', + }, + payment: { + title: '支付设置', + description: '配置支付系统选项', + configGuide: '支付配置指南', + enabled: '启用支付', + enabledHint: '启用或禁用支付系统', + enabledPaymentTypes: '启用的服务商', + enabledPaymentTypesHint: '禁用服务商将同时禁用对应的实例。', + findProvider: '正在寻找合适的易支付服务商?', + minAmount: '最低金额', + maxAmount: '最高金额', + dailyLimit: '每日限额', + balanceRechargeMultiplier: '余额充值倍率', + balanceRechargeMultiplierHint: '用户每支付 1 CNY 可获得多少 USD 余额', + balanceRechargePreview: '预览:1 CNY = {usd} USD', + subscriptionUsdToCnyRate: '订阅 CNY 换算汇率', + subscriptionUsdToCnyRateHint: + 'CNY 支付通道下,套餐每 1 USD 价格收取多少 CNY(如 7.15)。0 或留空 = 不换算,订阅按 price 数值直接收款。启用后所有套餐 price 必须按 USD 定价', + subscriptionUsdToCnyRateDisabled: '未启用(按 price 直付)', + rechargeFeeRate: '充值手续费率', + rechargeFeeRateHint: '用户充值时额外收取的手续费百分比,0 表示不收取手续费', + rechargeFeePreview: '预览:充值 100 元,手续费 {fee} 元', + orderTimeout: '订单超时时间', + orderTimeoutHint: '单位:分钟,至少 1 分钟', + maxPendingOrders: '最大待支付订单数', + cancelRateLimit: '限制取消频率', + cancelRateLimitHint: '启用后,用户在时间窗口内取消订单次数超限将无法创建新订单', + cancelRateLimitEvery: '每', + cancelRateLimitAllowMax: '最多', + cancelRateLimitTimes: '次', + cancelRateLimitWindow: '时间窗口', + cancelRateLimitUnit: '周期', + cancelRateLimitMax: '最大取消次数', + cancelRateLimitUnitMinute: '分钟', + cancelRateLimitUnitHour: '小时', + cancelRateLimitUnitDay: '天', + cancelRateLimitWindowMode: '窗口模式', + cancelRateLimitWindowModeRolling: '滚动', + cancelRateLimitWindowModeFixed: '固定', + alipayForceQRCode: '支付宝强制二维码支付', + alipayForceQRCodeHint: '启用后,移动端支付宝用户将统一使用二维码扫码支付,不再跳转至手机网站支付', + helpText: '帮助文本', + helpImageUrl: '帮助图片链接', + manageProviders: '管理服务商', + balancePaymentDisabled: '禁用余额充值', + noLimit: '留空表示不限制', + helpImage: '帮助图片', + helpImagePlaceholder: '上传或输入图片链接', + helpTextPlaceholder: '输入帮助说明文本...', + providerEasypay: '易支付', + providerAlipay: '支付宝官方', + providerWxpay: '微信官方', + providerStripe: 'Stripe', + providerAirwallex: 'Airwallex', + typeDisabled: '类型已禁用', + enableTypesFirst: '请先在上方启用至少一种服务商', + easypayRedirect: '跳转', + paymentMode: '支付模式', + modeRedirect: '跳转', + modeQRCode: '二维码', + modePopup: '弹窗', + validationNameRequired: '服务商名称不能为空', + validationTypesRequired: '请至少选择一种支持的支付方式', + validationFieldRequired: '{field} 不能为空', + validationEasyPayCustomMethodRequired: '每个易支付自定义方式都必须填写支付方式和上游 type', + validationEasyPayCustomMethodTypeInvalid: '易支付自定义支付方式只能包含小写字母、数字、下划线和短横线', + validationEasyPayCustomMethodUpstreamTypeInvalid: '易支付上游 type 只能包含小写字母、数字、下划线和短横线', + validationEasyPayCustomMethodReserved: '易支付自定义支付方式不能使用内置的 alipay 或 wxpay', + validationEasyPayCustomMethodPrefixReserved: '易支付自定义支付方式不能以 alipay 或 wxpay 开头', + validationEasyPayCustomMethodDuplicate: '易支付自定义支付方式不能重复', + field_apiBase: 'API 基础地址', + field_notifyUrl: '异步通知地址', + field_returnUrl: '同步跳转地址', + callbackBaseUrl: '回调基础地址', + field_privateKey: '私钥', + field_publicKey: '公钥', + field_mpAppId: '公众号 App ID', + field_mchId: '商户号', + field_apiV3Key: 'API v3 密钥', + field_publicKeyId: '公钥 ID', + field_certSerial: '证书序列号', + field_h5AppName: 'H5 应用名称', + field_h5AppUrl: 'H5 应用地址', + wxpayConfigHint: '微信支付通常只需要填写 App ID。公众号 App ID、H5 应用名称、H5 应用地址仅在公众号支付或 H5 场景有特殊要求时再填写。', + wxpayAdvancedOptions: '微信支付高级可选项', + field_secretKey: '密钥', + field_clientId: 'Client ID', + field_apiKey: 'API Key', + field_publishableKey: '公开密钥', + field_webhookSecret: 'Webhook 密钥', + field_countryCode: '国家/地区代码', + field_currency: '支付币种', + field_accountId: 'Airwallex 账户 ID', + field_airwallexApiBaseHint: '必须和 API Key 所属环境一致:沙箱/测试密钥使用 https://api-demo.airwallex.com/api/v1,生产密钥使用 https://api.airwallex.com/api/v1。环境混用会返回 credentials_invalid / Access Denied。', + field_paymentCurrencyHint: '默认 CNY。Stripe 和 Airwallex 可按账户支持从下拉项选择 HKD、USD 等币种;微信、支付宝、易支付仍按 CNY。', + field_accountIdHint: '不涉及多账户、组织级密钥或连接账户收款时可以不填;单账户 Scoped API Key 会默认使用所选账户。', + field_cid: '支付渠道 ID', + field_cidAlipay: '支付宝渠道 ID', + field_cidWxpay: '微信渠道 ID', + easypayCustomMethods: '易支付自定义支付方式', + easypayCustomMethodsHint: '添加当前易支付服务商额外支持的支付方式。支付方式会记录到 Sub2API 订单中,上游 type 会作为易支付 type 参数提交。', + addCustomMethod: '添加方式', + customMethodType: '支付方式', + customMethodUpstreamType: '上游 type', + customMethodDisplayName: '显示名称', + stripeWebhookHint: '请在 Stripe Dashboard 中将以下地址配置为 Webhook 端点:', + stripeWebhookApiVersionHint: 'Webhook 端点的 API 版本请与当前集成的 Stripe SDK 对齐,建议选择 {version};版本不一致可能导致回调事件解析失败。', + airwallexWebhookHint: '请在 Airwallex 后台将以下地址配置为 Webhook 端点;事件至少选择 Payment Intent -> Succeeded(payment_intent.succeeded),建议同时选择 Payment Intent -> Cancelled(payment_intent.cancelled);API version 选择账户默认或最新稳定版本。', + airwallexGuideSummary: '创建 Airwallex Scoped API 密钥时,建议只在账户级权限中为 Payment Acceptance 勾选读取和写入。', + airwallexGuideNote: '不需要勾选 Spend、Payouts、Transfers、Funds Splits、POS 终端等与在线收款无关的权限。Webhook 事件至少选择 payment_intent.succeeded,建议同时选择 payment_intent.cancelled;API version 选择账户默认或最新稳定版本。', + limitsTitle: '限额配置', + limitSingleMin: '单笔最低', + limitSingleMax: '单笔最高', + limitDaily: '每日限额', + limitsHint: '全部留空使用全局配置,部分填写时留空项表示不限制', + limitsUseGlobal: '使用全局配置', + limitsNoLimit: '不限制', + productNamePrefix: '商品名前缀', + productNameSuffix: '商品名后缀', + preview: '预览', + loadBalanceStrategy: '负载均衡策略', + strategyRoundRobin: '轮询', + strategyLeastAmount: '最少金额', + providerManagement: '服务商管理', + providerManagementDesc: '管理支付服务商实例', + createProvider: '添加服务商', + editProvider: '编辑服务商', + deleteProvider: '删除服务商', + deleteProviderConfirm: '确定要删除此服务商吗?', + providerName: '服务商名称', + providerKey: '服务商类型', + selectProviderKey: '选择服务商类型', + providerConfig: '凭证配置', + paymentGuideTrigger: '查看支付方式说明', + guideOpenLabel: '开通:', + guideCallLabel: '调用:', + guideFallbackLabel: '降级:', + alipayGuideSummary: '桌面优先扫码单,失败再走收银台;移动优先手机网站支付。', + alipayGuideFaceToFaceTitle: '当面付 / 扫码支付', + alipayGuideFaceToFaceOpen: '需开通当面付或扫码支付能力。', + alipayGuideFaceToFaceCall: '桌面端下单时优先调用 alipay.trade.precreate,前台直接渲染二维码。', + alipayGuideFaceToFaceFallback: '接口不可用或返回失败时,自动降级到电脑网站支付。', + alipayGuidePagePayTitle: '电脑网站支付', + alipayGuidePagePayOpen: '需开通电脑网站支付。', + alipayGuidePagePayCall: '桌面端当面付不可用时调用 alipay.trade.page.pay,并继续把返回链接渲染成二维码。', + alipayGuidePagePayFallback: '同时保留打开收银台入口,用户可手动重新拉起支付页。', + alipayGuideWapTitle: '手机网站支付', + alipayGuideWapOpen: '需开通手机网站支付。', + alipayGuideWapCall: '移动端优先调用 alipay.trade.wap.pay,跳转支付宝收银台。', + alipayGuideWapFallback: '未开通或返回异常时,前端自动改走扫码支付并提示未开通移动支付。', + wxpayGuideSummary: '桌面优先 Native 扫码,移动端按浏览器环境走 JSAPI 或 H5。', + wxpayGuideNote: '当前表单默认共用一个 App ID,适合同主体下统一配置网页、移动和公众号场景。', + wxpayGuideNativeTitle: 'Native / 扫码支付', + wxpayGuideNativeOpen: '需开通 Native 或扫码支付能力。', + wxpayGuideNativeCall: '桌面端默认调用 Native,下发二维码内容给前台渲染。', + wxpayGuideNativeFallback: '移动端无法走 JSAPI 或 H5 时,也会自动回退到这里。', + wxpayGuideJsapiTitle: 'JSAPI / 公众号支付', + wxpayGuideJsapiOpen: '需开通公众号支付,并保证当前浏览器在微信内且能拿到 OpenID。', + wxpayGuideJsapiCall: '微信内浏览器完成授权后调用 JSAPI,直接拉起微信支付。', + wxpayGuideJsapiFallback: '未配置、Bridge 不可用或拉起失败时,自动改走扫码支付。', + wxpayGuideH5Title: 'H5 支付', + wxpayGuideH5Open: '需开通 H5 支付。', + wxpayGuideH5Call: '移动端非微信浏览器且有客户端 IP 时调用 H5 支付,跳转微信收银台。', + wxpayGuideH5Fallback: '未开通 H5 或下单失败时,自动改走扫码支付。', + noProviders: '暂无服务商实例', + supportedTypes: '支持的支付方式', + supportedTypesHint: '逗号分隔,如 alipay,wxpay', + refundEnabled: '允许退款', + allowUserRefund: '允许用户退款', + enableConflict: '{method} 已有启用中的服务商实例:{provider}。请先停用现有实例后再启用或切换。', + }, + balanceNotify: { + title: '余额不足提醒', + description: '当用户余额低于阈值时发送邮件提醒', + enabled: '启用余额不足提醒', + threshold: '默认提醒阈值', + thresholdHint: '用户未自定义时使用此值', + thresholdPlaceholder: '输入金额', + rechargeUrl: '充值页面 URL', + rechargeUrlPlaceholder: 'https://example.com/payment', + rechargeUrlHint: '设置后邮件中将包含充值链接按钮', + }, + quotaNotify: { + title: '账号限额通知', + description: '当账号配额用量达到告警阈值时通知管理员', + enabled: '启用账号限额通知', + emails: '通知邮箱', + emailsHint: '留空则不发送通知', + addEmail: '添加邮箱', + emailPlaceholder: '输入邮箱地址', + }, + subscriptionExpiryNotify: { + title: '订阅到期提醒', + description: '控制是否向用户发送订阅即将到期的邮件提醒。', + enabled: '启用订阅到期提醒', + enabledHint: '开启后,系统会在订阅到期前 7 天、3 天、1 天各发送一次提醒。' + }, + smtp: { + title: 'SMTP 设置', + description: '配置用于发送验证码的邮件服务', + testConnection: '测试连接', + testing: '测试中...', + host: 'SMTP 主机', + hostPlaceholder: 'smtp.gmail.com', + port: 'SMTP 端口', + portPlaceholder: '587', + username: 'SMTP 用户名', + usernamePlaceholder: "your-email{'@'}gmail.com", + password: 'SMTP 密码', + passwordPlaceholder: '********', + passwordHint: '留空以保留现有密码', + passwordConfiguredPlaceholder: '********', + passwordConfiguredHint: '密码已配置,留空以保留当前值。', + fromEmail: '发件人邮箱', + fromEmailPlaceholder: "noreply{'@'}example.com", + fromName: '发件人名称', + fromNamePlaceholder: 'Sub2API', + useTls: '使用 TLS', + useTlsHint: '为 SMTP 连接启用 TLS 加密' + }, + testEmail: { + title: '发送测试邮件', + description: '发送测试邮件以验证 SMTP 配置', + recipientEmail: '收件人邮箱', + recipientEmailPlaceholder: "test{'@'}example.com", + sendTestEmail: '发送测试邮件', + sending: '发送中...', + enterRecipientHint: '请输入收件人邮箱地址' + }, + emailTemplates: { + title: '邮件模板', + description: '按事件和语言自定义通知邮件主题与 HTML 内容。', + event: '事件', + locale: '语言', + localeEn: '英文', + localeZh: '中文', + subject: '主题', + subjectPlaceholder: '输入邮件主题', + html: 'HTML 模板', + htmlPlaceholder: '编辑邮件 HTML 模板', + placeholders: '可用占位符', + placeholdersHelp: '点击占位符可复制。后端发送邮件时会替换这些值。', + livePreview: '实时预览', + previewSecurityHint: '预览 HTML 由后端预览接口生成,并在禁用脚本的沙盒 iframe 中展示。', + preview: '预览 / 刷新', + previewing: '预览中...', + save: '保存模板', + saving: '保存中...', + restoreOfficial: '恢复官方模板', + restoring: '恢复中...', + restoreConfirm: '确定恢复此事件和语言的官方模板吗?当前自定义版本将被替换。', + restoreSuccess: '已恢复官方模板', + saveSuccess: '邮件模板已保存', + placeholderCopied: '占位符已复制', + validationRequired: '主题和 HTML 模板不能为空', + empty: '暂无可用的邮件模板事件或语言。', + noPreview: '刷新预览后查看渲染后的邮件主题。', + customized: '已自定义' + }, + opsMonitoring: { + title: '运维监控', + description: '启用运维监控模块,用于排障与健康可视化', + disabled: '运维监控已关闭', + enabled: '启用运维监控', + enabledHint: '启用运维监控模块(仅管理员可见)', + realtimeEnabled: '启用实时监控', + realtimeEnabledHint: '启用实时请求速率和指标推送(WebSocket)', + queryMode: '默认查询模式', + queryModeHint: '运维监控默认查询模式(自动/原始/预聚合)', + queryModeAuto: '自动(推荐)', + queryModeRaw: '原始(最准确,但较慢)', + queryModePreagg: '预聚合(最快,需预聚合)', + metricsInterval: '采集频率(秒)', + metricsIntervalHint: '系统/请求指标采集频率(60-3600 秒)' + }, + adminApiKey: { + title: '管理员 API Key', + description: '用于外部系统集成的全局 API Key,拥有完整的管理员权限', + notConfigured: '尚未配置管理员 API Key', + configured: '管理员 API Key 已启用', + currentKey: '当前密钥', + regenerate: '重新生成', + regenerating: '生成中...', + delete: '删除', + deleting: '删除中...', + create: '创建密钥', + creating: '创建中...', + regenerateConfirm: '确定要重新生成吗?当前密钥将立即失效。', + deleteConfirm: '确定要删除管理员 API Key 吗?外部集成将停止工作。', + keyGenerated: '新的管理员 API Key 已生成', + keyDeleted: '管理员 API Key 已删除', + copyKey: '复制密钥', + keyCopied: '密钥已复制到剪贴板', + keyWarning: '此密钥仅显示一次,请立即复制保存。', + securityWarning: '警告:此密钥拥有完整的管理员权限,请妥善保管。', + usage: '使用方法:在请求头中添加 x-api-key: ' + }, + soraS3: { + title: 'Sora 存储配置', + description: '以多配置列表管理 Sora 媒体存储,支持 S3 和 Google Drive', + newProfile: '新建配置', + reloadProfiles: '刷新列表', + empty: '暂无存储配置,请先创建', + createTitle: '新建存储配置', + editTitle: '编辑存储配置', + selectProvider: '选择存储类型', + providerS3Desc: 'S3 兼容对象存储', + providerGDriveDesc: 'Google Drive 云盘', + profileID: '配置 ID', + profileName: '配置名称', + setActive: '创建后设为生效', + saveProfile: '保存配置', + activateProfile: '设为生效', + profileCreated: '存储配置创建成功', + profileSaved: '存储配置保存成功', + profileDeleted: '存储配置删除成功', + profileActivated: '生效配置已切换', + profileIDRequired: '请填写配置 ID', + profileNameRequired: '请填写配置名称', + profileSelectRequired: '请先选择配置', + endpointRequired: '启用时必须填写 S3 端点', + bucketRequired: '启用时必须填写存储桶', + accessKeyRequired: '启用时必须填写 Access Key ID', + deleteConfirm: '确定删除存储配置 {profileID} 吗?', + columns: { + profile: '配置', + profileId: 'Profile ID', + name: '名称', + provider: '存储类型', + active: '生效状态', + endpoint: '端点', + bucket: '存储桶', + storagePath: '存储路径', + capacityUsage: '容量 / 已用', + capacityUnlimited: '无限制', + videoCount: '视频数', + videoCompleted: '完成', + videoInProgress: '进行中', + quota: '默认配额', + updatedAt: '更新时间', + actions: '操作', + rootFolder: '根目录', + testInTable: '测试', + testingInTable: '测试中...', + testTimeout: '测试超时(15秒)' + }, + enabled: '启用存储', + enabledHint: '启用后,Sora 生成的媒体文件将自动上传到存储', + endpoint: 'S3 端点', + region: '区域', + bucket: '存储桶', + prefix: '对象前缀', + accessKeyId: 'Access Key ID', + secretAccessKey: 'Secret Access Key', + secretConfigured: '(已配置,留空保持不变)', + cdnUrl: 'CDN URL', + cdnUrlHint: '可选,配置后使用 CDN URL 访问文件', + forcePathStyle: '强制路径风格(Path Style)', + defaultQuota: '默认存储配额', + defaultQuotaHint: '未在用户或分组级别指定配额时的默认值,0 表示无限制', + testConnection: '测试连接', + testing: '测试中...', + testSuccess: '连接测试成功', + testFailed: '连接测试失败', + saved: '存储设置保存成功', + saveFailed: '保存存储设置失败', + gdrive: { + authType: '认证方式', + serviceAccount: '服务账号', + clientId: 'Client ID', + clientSecret: 'Client Secret', + clientSecretConfigured: '(已配置,留空保持不变)', + refreshToken: 'Refresh Token', + refreshTokenConfigured: '(已配置,留空保持不变)', + serviceAccountJson: '服务账号 JSON', + serviceAccountConfigured: '(已配置,留空保持不变)', + folderId: 'Folder ID(可选)', + authorize: '授权 Google Drive', + authorizeHint: '通过 OAuth2 获取 Refresh Token', + oauthFieldsRequired: '请先填写 Client ID 和 Client Secret', + oauthSuccess: 'Google Drive 授权成功', + oauthFailed: 'Google Drive 授权失败', + closeWindow: '此窗口将自动关闭', + processing: '正在处理授权...', + testStorage: '测试存储', + testSuccess: 'Google Drive 存储测试成功(上传、访问、删除均正常)', + testFailed: 'Google Drive 存储测试失败' + } + }, + overloadCooldown: { + title: '529 过载冷却', + description: '配置上游返回 529(过载)时的账号调度暂停策略', + enabled: '启用过载冷却', + enabledHint: '收到 529 错误时暂停该账号的调度,冷却后自动恢复', + cooldownMinutes: '冷却时长(分钟)', + cooldownMinutesHint: '账号暂停调度的持续时间(1-120 分钟)', + saved: '过载冷却设置保存成功', + saveFailed: '保存过载冷却设置失败' + }, + rateLimit429Cooldown: { + title: '429 默认回避', + description: '配置上游返回 429 且没有明确重置时间时的默认账号回避策略', + enabled: '启用 429 默认回避', + enabledHint: '收到无重置时间的 429 时暂停该账号调度,冷却后自动恢复', + cooldownSeconds: '回避时长(秒)', + cooldownSecondsHint: '默认回避持续时间(1-7200 秒);上游返回明确 reset 时仍优先使用上游时间', + saved: '429 默认回避设置保存成功', + saveFailed: '保存 429 默认回避设置失败' + }, + streamTimeout: { + title: '流超时处理', + description: '配置上游响应超时时的账户处理策略,避免问题账户持续被选中', + enabled: '启用流超时处理', + enabledHint: '当上游响应超时时,自动处理问题账户', + timeoutSeconds: '超时阈值(秒)', + timeoutSecondsHint: '流数据间隔超过此时间视为超时(30-300秒)', + action: '处理方式', + actionTempUnsched: '临时不可调度', + actionError: '标记为错误状态', + actionNone: '不处理', + actionHint: '超时后对账户执行的操作', + tempUnschedMinutes: '暂停时长(分钟)', + tempUnschedMinutesHint: '临时不可调度的持续时间(1-60分钟)', + thresholdCount: '触发阈值(次数)', + thresholdCountHint: '累计超时多少次后触发处理(1-10次)', + thresholdWindowMinutes: '阈值窗口(分钟)', + thresholdWindowMinutesHint: '超时计数的时间窗口(1-60分钟)', + saved: '流超时设置保存成功', + saveFailed: '保存流超时设置失败' + }, + rectifier: { + title: '请求整流器', + description: '当上游返回特定错误时,自动修正请求参数并重试,提高请求成功率', + enabled: '启用请求整流器', + enabledHint: '总开关,关闭后所有整流功能均不生效', + thinkingSignature: 'Thinking 签名整流', + thinkingSignatureHint: '当上游返回 thinking block 签名校验错误时,自动去除签名并重试', + thinkingBudget: 'Thinking Budget 整流', + thinkingBudgetHint: '当上游返回 budget_tokens 约束错误(≥1024)时,自动将 budget 设为 32000 并重试', + apikeySignature: 'API Key 签名整流', + apikeySignatureHint: + '当 API Key 账号的上游返回签名相关错误时,自动去除签名并重试(内置规则始终生效)', + apikeyPatterns: '自定义匹配关键词', + apikeyPatternsHint: + '额外的关键词,匹配响应体中的内容(不区分大小写)。内置规则始终生效,此处用于补充额外匹配。', + apikeyPatternPlaceholder: '例如:thinking_error 或 签名无效', + addPattern: '添加关键词', + saved: '整流器设置保存成功', + saveFailed: '保存整流器设置失败' + }, + betaPolicy: { + title: 'Beta 策略', + description: '配置转发 Anthropic API 请求时如何处理 Beta 特性。仅适用于 /v1/messages 接口。', + action: '处理方式', + actionPass: '透传(不处理)', + actionFilter: '过滤(移除)', + actionBlock: '拦截(拒绝请求)', + scope: '生效范围', + scopeAll: '全部账号', + scopeOAuth: '仅 OAuth 账号', + scopeAPIKey: '仅 API Key 账号', + scopeBedrock: '仅 Bedrock 账号', + errorMessage: '错误消息', + errorMessagePlaceholder: '拦截时返回的自定义错误消息', + errorMessageHint: '留空则使用默认错误消息', + saved: 'Beta 策略设置保存成功', + saveFailed: '保存 Beta 策略设置失败', + modelWhitelist: '模型白名单', + modelWhitelistHint: '留空则对所有模型生效。支持精确匹配和通配符前缀(如 claude-opus-*)', + modelPatternPlaceholder: '例如: claude-opus-* 或 claude-opus-4-6', + addModelPattern: '添加模型规则', + removePattern: '移除', + fallbackAction: '未匹配模型处理方式', + fallbackActionHint: '当请求模型不在白名单中时的处理方式', + fallbackErrorMessagePlaceholder: '未匹配模型被拦截时返回的自定义错误消息', + quickPresets: '快捷预设', + presetOpusOnly: '仅 Opus 允许 1M', + presetOpusOnlyDesc: 'Opus 透传,其他模型过滤', + commonPatterns: '常用模式' + }, + openaiFastPolicy: { + title: 'OpenAI Fast/Flex 策略', + description: '基于请求体 service_tier 字段拦截/过滤/透传 OpenAI fast(priority) 与 flex 请求;仅作用于 OpenAI 网关。', + empty: '尚未配置任何规则。点击下方按钮新增。', + ruleHeader: '规则 #{index}', + removeRule: '删除规则', + addRule: '新增规则', + saveHint: '保存时随系统设置一起提交(点击页面底部「保存」按钮)。', + serviceTier: 'service_tier 匹配', + tierAll: '全部 tier', + tierPriority: 'priority(fast)', + tierFlex: 'flex', + action: '处理方式', + actionPass: '透传(保留 service_tier)', + actionFilter: '过滤(移除 service_tier)', + actionForcePriority: '强制设置 priority(fast)', + actionBlock: '拦截(拒绝请求)', + scope: '生效范围', + scopeAll: '全部账号', + scopeOAuth: '仅 OAuth 账号', + scopeAPIKey: '仅 API Key 账号', + scopeBedrock: '仅 Bedrock 账号', + errorMessage: '错误消息', + errorMessagePlaceholder: '拦截时返回的自定义错误消息', + errorMessageHint: '留空则使用默认错误消息。', + modelWhitelist: '模型白名单', + modelWhitelistHint: '留空表示对所有模型生效;支持精确匹配与通配符(如 gpt-5.5*)。', + modelPatternPlaceholder: '例如: gpt-5.5 或 gpt-5.5*', + addModelPattern: '添加模型规则', + fallbackAction: '未匹配模型处理方式', + fallbackActionHint: '当请求模型不在白名单中时的处理方式。', + fallbackErrorMessagePlaceholder: '未匹配模型被拦截时返回的自定义错误消息' + }, + wechatConnect: { + title: '微信登录', + description: '用于微信开放平台或公众号/小程序的第三方登录配置。', + enabledLabel: '启用微信登录', + enabledHint: '开启后可使用微信第三方登录回调与授权配置。', + appIdLabel: 'AppID', + appIdPlaceholder: '微信开放平台 AppID', + appSecretLabel: 'AppSecret', + appSecretConfiguredPlaceholder: '密钥已配置,留空以保留当前值。', + appSecretPlaceholder: '微信开放平台 AppSecret', + appSecretConfiguredHint: '密钥已配置,留空以保留当前值。', + appSecretHint: '填写后会覆盖当前微信密钥。', + modeLabel: '模式', + openModeLabel: '非微信环境使用开放平台', + openModeHint: '浏览器不在微信内时,自动走开放平台扫码授权。', + mpModeLabel: '微信环境使用公众号', + mpModeHint: '浏览器在微信内时,自动走公众号授权。', + redirectUrlLabel: '回调地址', + redirectUrlPlaceholder: 'https://your-site.com/api/v1/auth/oauth/wechat/callback', + generateAndCopy: '使用当前站点生成并复制', + redirectUrlSetAndCopied: '已使用当前站点生成回调地址并复制到剪贴板', + frontendRedirectUrlLabel: '前端回调地址', + frontendRedirectUrlPlaceholder: '/auth/wechat/callback', + frontendRedirectUrlHint: '通常用于前端路由回调地址,需与后端配置保持一致。' + }, + authSourceDefaults: { + title: '认证来源默认值', + description: '按注册来源配置新用户默认余额、并发、订阅与授权策略。', + requireEmailLabel: '第三方注册强制补充邮箱', + requireEmailHint: '启用后,Linux DO、OIDC、微信注册缺少邮箱时必须先补充邮箱地址。', + enabledHint: '以下默认值会在该来源注册新用户时发放;首次绑定时授权仅作用于已有账号绑定该来源。', + sources: { + email: { + title: '邮箱注册', + description: '适用于邮箱密码注册的新用户默认配额。' + }, + linuxdo: { + title: 'Linux DO 登录', + description: '适用于 Linux DO 第三方注册的新用户默认配额。' + }, + oidc: { + title: 'OIDC 登录', + description: '适用于 OIDC 第三方注册的新用户默认配额。' + }, + wechat: { + title: '微信登录', + description: '适用于微信第三方注册的新用户默认配额。' + } + }, + grantOnFirstBindLabel: '首次绑定时授权', + grantOnFirstBindHint: '已有账号首次绑定该来源时发放默认权益。', + defaultSubscriptionsLabel: '默认订阅', + defaultSubscriptionsHint: '仅对当前认证来源生效,未配置时不追加来源专属订阅。', + noSourceSubscriptions: '当前来源未配置专属默认订阅。', + platformQuotasOverride: '平台限额覆盖', + platformQuotasOverrideHint: '留空的字段继承「系统默认平台限额」;填 0 表示禁止该窗口使用。', + }, + paymentVisibleMethods: { + methodLabel: '{title} 可见方式', + methodHint: '控制前台结算页是否展示该方式,以及展示时使用的来源键。', + sourceLabel: '支付来源', + sourceHint: '启用后必须明确选择一个来源;未配置状态不会对外展示该支付方式。', + sourceRequiredError: '{title} 已启用,请先选择支付来源。' + }, + openaiExperimentalScheduler: { + title: 'OpenAI 实验调度策略', + description: '默认关闭。开启后仅影响本网关在 OpenAI 账号间的实验性调度选择逻辑,不代表上游 OpenAI 官方能力。', + stickyWeightedTitle: '粘性加权', + stickyWeightedDescription: '开启后 previous_response_id 和 session_hash 粘性进入高级调度打分;关闭时仍按旧逻辑硬命中粘性账号。', + subscriptionPriorityTitle: '订阅优先', + subscriptionPriorityDescription: '开启后先在 ChatGPT 订阅账号池中按权值选取;订阅池拿不到席位时再回退到非订阅账号池。', + weightsTitle: '调度权值覆盖', + weightsDescription: '留空时使用配置/环境变量值;配置未设置时使用内置默认值。页面非空设置优先。', + defaultPlaceholder: '配置/默认:{value}', + topKLabel: 'TopK', + priorityWeight: '优先级', + loadWeight: '负载', + queueWeight: '排队', + errorRateWeight: '错误率', + ttftWeight: '首包延迟', + resetWeight: '重置窗口', + quotaHeadroomWeight: '额度余量', + previousResponseWeight: 'previous_response 粘性', + sessionStickyWeight: 'session_hash 粘性' + }, + usageRecords: { + title: '使用记录', + description: '与终端用户可见的用量及失败请求记录相关的设置。', + }, + user_error_view: { + label: '允许用户查看自己的错误请求', + description: '开启后,用户可在用量页查看自己失败请求的精简信息(不含内部/上游错误细节)。需运维监控开启才有数据。', + }, + saveSettings: '保存设置', + saving: '保存中...', + settingsSaved: '设置保存成功', + smtpConnectionSuccess: 'SMTP 连接成功', + testEmailSent: '测试邮件发送成功', + failedToLoad: '加载设置失败', + failedToSave: '保存设置失败', + failedToTestSmtp: 'SMTP 连接测试失败', + failedToSendTestEmail: '发送测试邮件失败' + }, + + // Error Passthrough Rules + errorPassthrough: { + title: '错误透传规则', + description: '配置上游错误如何返回给客户端', + createRule: '创建规则', + editRule: '编辑规则', + deleteRule: '删除规则', + noRules: '暂无规则', + createFirstRule: '创建第一条错误透传规则', + allPlatforms: '所有平台', + passthrough: '透传', + custom: '自定义', + code: '状态码', + body: '消息体', + skipMonitoring: '跳过监控', + + // Columns + columns: { + priority: '优先级', + name: '名称', + conditions: '匹配条件', + platforms: '平台', + behavior: '响应行为', + status: '状态', + actions: '操作' + }, + + // Match Mode + matchMode: { + any: '错误码 或 关键词', + all: '错误码 且 关键词', + anyHint: '状态码匹配任一错误码,或消息包含任一关键词', + allHint: '状态码匹配任一错误码,且消息包含任一关键词' + }, + + // Form + form: { + name: '规则名称', + namePlaceholder: '例如:上下文超限透传', + priority: '优先级', + priorityHint: '数值越小优先级越高,优先匹配', + description: '规则描述', + descriptionPlaceholder: '描述此规则的用途...', + matchConditions: '匹配条件', + errorCodes: '错误码', + errorCodesPlaceholder: '422, 400, 429', + errorCodesHint: '多个错误码用逗号分隔', + keywords: '关键词', + keywordsPlaceholder: '每行一个关键词\ncontext limit\nmodel not supported', + keywordsHint: '每行一个关键词,不区分大小写', + matchMode: '匹配模式', + platforms: '适用平台', + platformsHint: '不选择表示适用于所有平台', + responseBehavior: '响应行为', + passthroughCode: '透传上游状态码', + responseCode: '自定义状态码', + passthroughBody: '透传上游错误信息', + customMessage: '自定义错误信息', + customMessagePlaceholder: '返回给客户端的错误信息...', + skipMonitoring: '跳过运维监控记录', + skipMonitoringHint: '开启后,匹配此规则的错误不会被记录到运维监控中', + enabled: '启用此规则' + }, + + // Messages + nameRequired: '请输入规则名称', + conditionsRequired: '请至少配置一个错误码或关键词', + ruleCreated: '规则创建成功', + ruleUpdated: '规则更新成功', + ruleDeleted: '规则删除成功', + deleteConfirm: '确定要删除规则 "{name}" 吗?', + failedToLoad: '加载规则失败', + failedToSave: '保存规则失败', + failedToDelete: '删除规则失败', + failedToToggle: '切换状态失败' + }, + + // TLS 指纹模板 + tlsFingerprintProfiles: { + title: 'TLS 指纹模板', + description: '管理 TLS 指纹模板,用于模拟特定客户端的 TLS 握手特征', + createProfile: '创建模板', + editProfile: '编辑模板', + deleteProfile: '删除模板', + noProfiles: '暂无模板', + createFirstProfile: '创建你的第一个 TLS 指纹模板', + + columns: { + name: '名称', + description: '描述', + grease: 'GREASE', + alpn: 'ALPN', + actions: '操作' + }, + + form: { + pasteYaml: '粘贴 YAML 配置', + pasteYamlPlaceholder: '将 TLS 指纹采集器复制的 YAML 粘贴到这里...', + pasteYamlHint: '粘贴从 TLS 指纹采集器复制的 YAML 配置,自动填充所有字段。', + openCollector: '打开采集器', + parseYaml: '解析 YAML', + yamlParsed: 'YAML 解析成功,字段已自动填充', + yamlParseFailed: 'YAML 解析失败:未找到 name 字段', + name: '模板名称', + namePlaceholder: '例如 macOS Node.js v24', + description: '描述', + descriptionPlaceholder: '可选的模板描述', + enableGrease: '启用 GREASE', + enableGreaseHint: '在 TLS ClientHello 扩展中插入 GREASE 值', + cipherSuites: '密码套件', + cipherSuitesHint: '逗号分隔的十六进制值,例如 0x1301, 0x1302, 0xc02c', + curves: '椭圆曲线', + curvesHint: '逗号分隔的曲线 ID', + pointFormats: '点格式', + signatureAlgorithms: '签名算法', + alpnProtocols: 'ALPN 协议', + alpnProtocolsHint: '逗号分隔,例如 h2, http/1.1', + supportedVersions: '支持的 TLS 版本', + keyShareGroups: '密钥共享组', + pskModes: 'PSK 模式', + extensions: '扩展' + }, + + deleteConfirm: '删除模板', + deleteConfirmMessage: '确定要删除模板 "{name}" 吗?使用此模板的账号将回退到内置默认值。', + createSuccess: '模板创建成功', + updateSuccess: '模板更新成功', + deleteSuccess: '模板删除成功', + loadFailed: '加载模板失败', + saveFailed: '保存模板失败', + deleteFailed: '删除模板失败' + } +} diff --git a/frontend/src/i18n/locales/zh/common.ts b/frontend/src/i18n/locales/zh/common.ts new file mode 100644 index 0000000000..44fafef7a4 --- /dev/null +++ b/frontend/src/i18n/locales/zh/common.ts @@ -0,0 +1,416 @@ +export default { + common: { + loading: '加载中...', + submitting: '提交中...', + justNow: '刚刚', + peakRateTooltip: '高峰倍率:{window}', + peakRateImageNote: ';token 计费的图片 token 同样适用,图片按次计费不受高峰影响', + save: '保存', + saved: '保存成功', + deleted: '删除成功', + cancel: '取消', + delete: '删除', + edit: '编辑', + create: '创建', + update: '更新', + confirm: '确认', + reset: '重置', + search: '搜索', + filter: '筛选', + export: '导出', + import: '导入', + actions: '操作', + status: '状态', + name: '名称', + email: '邮箱', + password: '密码', + submit: '提交', + back: '返回', + next: '下一步', + yes: '是', + no: '否', + all: '全部', + none: '无', + selectAll: '全选', + noData: '暂无数据', + expand: '展开', + collapse: '收起', + success: '成功', + error: '错误', + critical: '严重', + warning: '警告', + info: '提示', + active: '启用', + inactive: '禁用', + more: '更多', + close: '关闭', + enabled: '已启用', + disabled: '已禁用', + total: '总计', + balance: '余额', + availableBalance: '可用余额', + frozenBalance: '冻结金额', + totalBalance: '总余额', + available: '可用', + copiedToClipboard: '已复制到剪贴板', + copied: '已复制', + copyFailed: '复制失败', + verifying: '验证中...', + processing: '处理中...', + contactSupport: '联系客服', + add: '添加', + invalidEmail: '请输入有效的邮箱地址', + optional: '可选', + selectOption: '请选择', + searchPlaceholder: '搜索...', + noOptionsFound: '无匹配选项', + noGroupsAvailable: '无可用分组', + unknownError: '发生未知错误', + saving: '保存中...', + selectedCount: '(已选 {count} 个)', + refresh: '刷新', + autoRefresh: { + title: '自动刷新', + enable: '启用自动刷新', + countdown: '自动刷新: {seconds}s', + seconds: '{n} 秒', + }, + view: '查看', + settings: '设置', + chooseFile: '选择文件', + copy: '复制', + notAvailable: '不可用', + now: '现在', + today: '今天', + tomorrow: '明天', + unknown: '未知', + minutes: '分钟', + time: { + never: '从未', + justNow: '刚刚', + minutesAgo: '{n}分钟前', + hoursAgo: '{n}小时前', + daysAgo: '{n}天前', + countdown: { + daysHours: '{d}d {h}h', + hoursMinutes: '{h}h {m}m', + minutes: '{m}m', + withSuffix: '{time} 后解除' + } + } + }, + + adminCompliance: { + title: '部署与运营合规确认', + blockingNotice: '继续使用控制台前,须完成部署与运营合规确认。', + riskNotice: '本确认用于以清晰、显著、可留痕的方式提示自部署实例的合规义务与运营风险。', + version: '协议版本', + openDocument: '在 GitHub 查看协议文件', + documentSource: '协议正文来自本项目仓库中的 Markdown 文件。修改协议内容时必须同步递增协议版本;已确认的旧版本将失效,控制台使用者须重新确认。', + inputLabel: '请逐字输入以下确认短语', + inputPlaceholder: '输入确认短语以继续', + inputMismatch: '确认短语不匹配,请逐字输入提示内容。', + legalNote: '本确认用于明确自部署实例与开源项目、著作权人、贡献者及维护者之间的非关联关系和责任边界;部署、运营或控制相关实例的主体应独立承担其适用义务。', + logout: '退出登录', + accept: '确认并继续', + accepted: '合规确认已记录', + acceptFailed: '提交确认失败' + }, + + legal: { + loadFailed: '文档加载失败', + retryLater: '请稍后刷新页面重试。', + notFound: '文档不存在', + notFoundDescription: '当前条款文档不存在或已被管理员移除。', + updatedAt: '更新日期:{date}', + empty: '暂无正文内容', + loginAgreement: '登录条款', + adminCompliance: '部署与运营合规承诺', + loginAgreementPrompt: { + checkboxPrefix: '我已阅读并同意', + documentSeparator: '、', + noticeTitle: '继续登录前需要先同意最新条款。', + noticeDescription: '未同意前,账号密码输入和快捷登录会保持禁用。', + viewTerms: '查看条款', + dialogTitle: '条款更新通知', + dialogDescription: '我们的服务条款已于 {date} 更新。在继续使用服务之前,请仔细阅读并同意以下条款。', + recently: '近期', + relatedDocuments: '相关文档', + reject: '拒绝', + accept: '同意并继续', + loginRejectedWarning: '未同意最新条款前,无法输入账号密码或使用快捷登录。', + loginRequiredWarning: '请先阅读并同意最新条款后再登录。', + registerRejectedWarning: '未同意最新条款前,无法注册或使用快捷登录。', + registerRequiredWarning: '请先阅读并同意最新条款后再注册。' + } + }, + + // Navigation + nav: { + dashboard: '仪表盘', + announcements: '公告', + apiKeys: 'API 密钥', + batchImage: '批量生图', + usage: '使用记录', + redeem: '兑换', + affiliate: '邀请返利', + affiliateManagement: '邀请返利', + affiliateInviteRecords: '邀请记录', + affiliateRebateRecords: '返利记录', + affiliateTransferRecords: '提取记录', + profile: '个人资料', + users: '用户管理', + groups: '分组管理', + channels: '渠道管理', + availableChannels: '可用渠道', + subscriptions: '订阅管理', + accounts: '账号管理', + proxies: 'IP管理', + redeemCodes: '兑换码', + ops: '运维监控', + promoCodes: '优惠码', + settings: '系统设置', + myAccount: '我的账户', + lightMode: '浅色模式', + darkMode: '深色模式', + collapse: '收起', + expand: '展开', + logout: '退出登录', + github: 'GitHub', + mySubscriptions: '我的订阅', + buySubscription: '充值/订阅', + docs: '文档', + myOrders: '我的订单', + orderManagement: '订单管理', + paymentDashboard: '支付概览', + paymentConfig: '支付配置', + paymentPlans: '订阅套餐', + channelManagement: '渠道管理', + channelPricing: '渠道定价', + channelMonitor: '渠道监控', + channelStatus: '渠道状态', + riskControl: '风控中心', + }, + + // Auth + auth: { + welcomeBack: '欢迎回来', + signInToAccount: '登录您的账户以继续', + signIn: '登录', + signingIn: '登录中...', + createAccount: '创建账户', + signUpToStart: '注册以开始使用 {siteName}', + signUp: '注册', + processing: '处理中...', + continue: '继续', + rememberMe: '记住我', + dontHaveAccount: '还没有账户?', + alreadyHaveAccount: '已有账户?', + registrationDisabled: '注册功能暂时关闭,请联系管理员。', + emailLabel: '邮箱', + emailPlaceholder: '请输入邮箱', + passwordLabel: '密码', + passwordPlaceholder: '请输入密码', + createPasswordPlaceholder: '创建一个安全的密码', + passwordHint: '至少 6 个字符', + emailRequired: '请输入邮箱', + invalidEmail: '请输入有效的邮箱地址', + passwordRequired: '请输入密码', + passwordMinLength: '密码至少需要 6 个字符', + loginFailed: '登录失败,请检查您的凭据后重试。', + errors: { + USER_NOT_ACTIVE: '账号已被禁用', + }, + registrationFailed: '注册失败,请重试。', + emailSuffixNotAllowed: '该邮箱域名不在允许注册范围内。', + emailSuffixNotAllowedWithAllowed: '该邮箱域名不被允许。可用域名:{suffixes}', + emailSuffixAllowedMore: '等 {count} 项', + loginSuccess: '登录成功!欢迎回来。', + accountCreatedSuccess: '账户创建成功!欢迎使用 {siteName}。', + reloginRequired: '会话已过期,请重新登录。', + turnstileExpired: '验证已过期,请重试', + turnstileFailed: '验证失败,请重试', + completeVerification: '请完成验证', + verifyYourEmail: '验证您的邮箱', + sessionExpired: '会话已过期', + sessionExpiredDesc: '请返回注册页面重新开始。', + verificationCode: '验证码', + verificationCodeHint: '请输入发送到您邮箱的6位验证码', + sendingCode: '发送中...', + sendCode: '发送验证码', + clickToResend: '点击重新发送验证码', + resendCode: '重新发送验证码', + sendCodeDesc: '我们将发送验证码到', + codeSentSuccess: '验证码已发送!请查收您的邮箱。', + verifying: '验证中...', + verifyAndCreate: '验证并创建账户', + resendCountdown: '{countdown}秒后可重新发送', + backToRegistration: '返回注册', + sendCodeFailed: '发送验证码失败,请重试。', + verifyFailed: '验证失败,请重试。', + codeRequired: '请输入验证码', + invalidCode: '请输入有效的6位验证码', + promoCodeLabel: '优惠码', + promoCodePlaceholder: '输入优惠码(可选)', + promoCodeValid: '有效!注册后将获得 ${amount} 赠送余额', + promoCodeInvalid: '无效的优惠码', + promoCodeNotFound: '优惠码不存在', + promoCodeExpired: '此优惠码已过期', + promoCodeDisabled: '此优惠码已被禁用', + promoCodeMaxUsed: '此优惠码已达到使用上限', + promoCodeAlreadyUsed: '您已使用过此优惠码', + promoCodeValidating: '优惠码正在验证中,请稍候', + promoCodeInvalidCannotRegister: '优惠码无效,请检查后重试或清空优惠码', + invitationCodeLabel: '邀请码', + invitationCodePlaceholder: '请输入邀请码', + invitationCodeRequired: '请输入邀请码', + invitationCodeValid: '邀请码有效', + invitationCodeInvalid: '邀请码无效或已被使用', + invitationCodeValidating: '正在验证邀请码...', + invitationCodeInvalidCannotRegister: '邀请码无效,请检查后重试', + oauthOrContinue: '或使用其他继续', + linuxdo: { + signIn: '使用 Linux.do 登录', + orContinue: '或使用邮箱密码继续', + callbackTitle: '正在完成登录', + callbackProcessing: '正在验证登录信息,请稍候...', + callbackHint: '如果页面未自动跳转,请返回登录页重试。', + callbackMissingToken: '登录信息缺失,请返回重试。', + backToLogin: '返回登录', + invitationRequired: '该 Linux.do 账号尚未注册,站点已开启邀请码注册,请输入邀请码以完成注册。', + invalidPendingToken: '注册凭证已失效,请重新使用 Linux.do 登录。', + completeRegistration: '完成注册', + completing: '正在完成注册...', + completeRegistrationFailed: '注册失败,请检查邀请码后重试。' + }, + dingtalk: { + signIn: '钉钉登录', + callbackTitle: '正在完成钉钉登录', + callbackProcessing: '正在验证钉钉登录信息,请稍候...', + callbackHint: '如果页面未自动跳转,请返回登录页重试。', + callbackMissingToken: '登录信息缺失,请返回重试。', + backToLogin: '返回登录', + invitationRequired: '该钉钉账号尚未注册,站点已开启邀请码注册,请输入邀请码以完成注册。', + invalidPendingToken: '注册凭证已失效,请重新使用钉钉登录。', + completeRegistration: '完成注册', + completing: '正在完成注册...', + completeRegistrationFailed: '注册失败,请检查邀请码后重试。', + createAccountTitle: '创建钉钉账户', + registrationDisabledRedirectToBind: '当前已禁止注册新账户,请使用已有账户邮箱和密码绑定钉钉登录', + error: { + title: '钉钉登录失败', + csrf: '登录会话已过期,请重新扫码登录', + corp_rejected: '您的钉钉账号不属于本企业,请联系管理员', + dingtalk_not_enabled: '钉钉登录暂未启用', + upstream_error: '钉钉服务暂时不可用,请稍后重试', + missing_browser_session: '浏览器会话丢失,请重新登录', + missing_params: '请求参数不完整', + invalid_state: '登录状态异常', + provider_error: '钉钉授权失败', + session_error: '会话创建失败,请重试', + retry: '重新登录' + } + }, + emailOAuth: { + signIn: '使用 {providerName} 登录' + }, + oidc: { + signIn: '使用 {providerName} 登录', + callbackTitle: '正在完成 {providerName} 登录', + callbackProcessing: '正在验证 {providerName} 登录信息,请稍候...', + callbackHint: '如果页面未自动跳转,请返回登录页重试。', + callbackMissingToken: '登录信息缺失,请返回重试。', + backToLogin: '返回登录', + invitationRequired: '该 {providerName} 账号尚未注册,站点已开启邀请码注册,请输入邀请码以完成注册。', + invalidPendingToken: '注册凭证已失效,请重新登录。', + completeRegistration: '完成注册', + completing: '正在完成注册...', + completeRegistrationFailed: '注册失败,请检查邀请码后重试。' + }, + oauthFlow: { + profileDetailsTitle: '使用 {providerName} 资料', + profileDetailsDescription: '选择是否将 {providerName} 的昵称或头像应用到当前账户。', + useDisplayName: '使用昵称', + useAvatar: '使用头像', + avatarAlt: '{providerName} 头像', + reviewProfileBeforeContinue: '请先确认 {providerName} 资料后再继续。', + chooseHowToContinue: '选择后续操作', + chooseAccountActionHint: '请选择绑定已有账户,或创建一个新账户。', + suggestedEmail: '建议邮箱:{email}', + bindExistingAccount: '绑定已有账户', + createNewAccount: '创建新账户', + createAccountHint: '请输入邮箱地址以创建账户并继续。', + bindLoginHint: '登录一个已有账户以绑定此次 {providerName} 登录。', + signInThenBindDescription: '请先登录已有账户,再将此次 {providerName} 登录绑定到该账户。', + bindSignInToExistingAccount: '将此次 {providerName} 登录绑定到已有账户。', + bindCurrentAccountTitle: '绑定当前账户', + bindCurrentAccountDescription: '将此次 {providerName} 登录绑定到当前浏览器已登录的账户。', + bindCurrentAccount: '绑定当前账户', + logInAndBind: '登录并绑定', + useDifferentEmail: '使用其他邮箱', + backToOptions: '返回选项', + yourAccount: '当前账户', + totpHint: '请输入 {account} 的 6 位验证码,以完成此次 {providerName} 登录绑定。', + verifyAndContinue: '验证并继续', + wechatAvailabilityUnknown: '暂时无法确认微信登录可用性,请刷新后重试。', + wechatSystemBrowserOnly: '当前微信登录流程仅支持在系统浏览器中继续。', + wechatBrowserOnly: '当前微信登录流程仅支持在微信内置浏览器中继续。', + wechatNotConfigured: '微信登录尚未配置。' + }, + linuxdoCallbackPageTitle: 'LinuxDo 登录回调', + dingtalkCallbackPageTitle: '钉钉登录回调', + dingtalkProviderName: '钉钉', + oidcCallbackPageTitle: 'OIDC 登录回调', + oauthCallbackPageTitle: 'OAuth 回调', + wechatProviderName: '微信', + wechatCallbackPageTitle: '微信登录回调', + wechatPaymentCallbackPageTitle: '微信支付回调', + wechatPayment: { + callbackTitle: '正在恢复微信支付', + callbackProcessing: '正在恢复微信支付...', + backToPayment: '返回支付页', + callbackMissingResumeToken: '微信支付回调缺少恢复令牌。' + }, + oauth: { + callbackTitle: 'OAuth 回调', + callbackHint: '按需将授权码和状态值复制回后台授权流程。', + invalidCallbackTitle: '无效的登录回调', + invalidCallbackHint: '当前页面缺少有效的授权结果,请返回登录页重新发起快捷登录。', + code: '授权码', + state: '状态', + fullUrl: '完整URL' + }, + // 忘记密码 + forgotPassword: '忘记密码?', + forgotPasswordTitle: '重置密码', + forgotPasswordHint: '输入您的邮箱地址,我们将向您发送密码重置链接。', + sendResetLink: '发送重置链接', + sendingResetLink: '发送中...', + sendResetLinkFailed: '发送重置链接失败,请重试。', + resetEmailSent: '重置链接已发送', + resetEmailSentHint: + '如果该邮箱已注册,您将很快收到密码重置链接。请检查您的收件箱和垃圾邮件文件夹。', + backToLogin: '返回登录', + rememberedPassword: '想起密码了?', + // 重置密码 + resetPasswordTitle: '设置新密码', + resetPasswordHint: '请在下方输入您的新密码。', + newPassword: '新密码', + newPasswordPlaceholder: '输入新密码', + confirmPassword: '确认密码', + confirmPasswordPlaceholder: '再次输入新密码', + confirmPasswordRequired: '请确认您的密码', + passwordsDoNotMatch: '两次输入的密码不一致', + resetPassword: '重置密码', + resettingPassword: '重置中...', + resetPasswordFailed: '重置密码失败,请重试。', + passwordResetSuccess: '密码重置成功', + passwordResetSuccessHint: '您的密码已重置。现在可以使用新密码登录。', + invalidResetLink: '无效的重置链接', + invalidResetLinkHint: '此密码重置链接无效或已过期。请重新请求一个新链接。', + requestNewResetLink: '请求新的重置链接', + invalidOrExpiredToken: '密码重置链接无效或已过期。请重新请求一个新链接。' + }, + + // Dashboard +} diff --git a/frontend/src/i18n/locales/zh/dashboard.ts b/frontend/src/i18n/locales/zh/dashboard.ts new file mode 100644 index 0000000000..2ce3eda5bb --- /dev/null +++ b/frontend/src/i18n/locales/zh/dashboard.ts @@ -0,0 +1,816 @@ +export default { + dashboard: { + title: '仪表盘', + welcomeMessage: '欢迎回来!这是您账户的概览。', + balance: '余额', + apiKeys: 'API 密钥', + todayRequests: '今日请求', + todayCost: '今日消费', + todayTokens: '今日 Token', + totalTokens: '累计 Token', + cacheToday: '今日缓存', + performance: '性能指标', + avgResponse: '平均响应', + averageTime: '平均时间', + timeRange: '时间范围', + granularity: '粒度', + day: '按天', + hour: '按小时', + modelDistribution: '模型分布', + groupDistribution: '分组使用分布', + platformBreakdown: '按平台拆分', + platformBreakdownEmpty: '暂无平台用量', + platformCount: '{count} 个平台', + platformOther: '其他', + platformQuota: { + title: '配额用量', + daily: '日', + weekly: '周', + monthly: '月(近30天)', + resetsAt: '{time} 重置', + noLimit: '不限制', + disabled: '已禁用', + }, + tokenUsageTrend: 'Token 使用趋势', + noDataAvailable: '暂无数据', + model: '模型', + group: '分组', + noGroup: '无分组', + requests: '请求', + tokens: 'Token', + actual: '实际', + standard: '标准', + input: '输入', + output: '输出', + cache: '缓存', + recentUsage: '最近使用', + last7Days: '近 7 天', + noUsageRecords: '暂无使用记录', + startUsingApi: '开始使用 API 后,您的使用历史将显示在这里。', + viewAllUsage: '查看全部', + quickActions: '快捷操作', + createApiKey: '创建 API 密钥', + generateNewKey: '生成新的 API 密钥', + batchImageAgent: '批量生图助手', + batchImageAgentDesc: '复制给 Agent 的任务说明', + viewUsage: '查看使用记录', + checkDetailedLogs: '查看详细的使用日志', + redeemCode: '兑换码', + addBalanceWithCode: '使用兑换码充值' + }, + + // Groups (shared) + groups: { + subscription: '订阅' + }, + + // API Keys + keys: { + title: 'API 密钥', + description: '管理您的 API 密钥和访问令牌', + searchPlaceholder: '搜索名称或Key...', + endpoints: { + title: 'API 端点', + default: '默认', + copied: '已复制', + copiedHint: '已复制到剪贴板', + clickToCopy: '点击可复制此端点', + speedTest: '测速', + }, + allGroups: '全部分组', + allStatus: '全部状态', + columnSettings: '列设置', + columnAlwaysVisible: '该列固定显示,不可隐藏', + createKey: '创建密钥', + editKey: '编辑密钥', + deleteKey: '删除密钥', + deleteConfirmMessage: "确定要删除 '{name}' 吗?此操作无法撤销。", + apiKey: 'API 密钥', + group: '分组', + currentConcurrency: '当前并发', + noGroup: '无分组', + searchGroup: '搜索分组...', + noGroupFound: '未找到匹配的分组', + created: '创建时间', + copyToClipboard: '复制到剪贴板', + copied: '已复制!', + importToCcSwitch: '导入到 CCS', + enable: '启用', + disable: '禁用', + nameLabel: '名称', + namePlaceholder: '我的 API 密钥', + groupLabel: '分组', + selectGroup: '选择分组', + statusLabel: '状态', + selectStatus: '选择状态', + saving: '保存中...', + noKeysYet: '暂无 API 密钥', + createFirstKey: '创建您的第一个 API 密钥以开始使用 API。', + keyCreatedSuccess: 'API 密钥创建成功', + keyUpdatedSuccess: 'API 密钥更新成功', + keyDeletedSuccess: 'API 密钥删除成功', + keyEnabledSuccess: 'API 密钥已启用', + keyDisabledSuccess: 'API 密钥已禁用', + failedToLoad: '加载 API 密钥失败', + failedToSave: '保存 API 密钥失败', + failedToDelete: '删除 API 密钥失败', + failedToUpdateStatus: '更新 API 密钥状态失败', + clickToChangeGroup: '点击更换分组', + groupChangedSuccess: '分组更换成功', + failedToChangeGroup: '更换分组失败', + groupRequired: '请选择分组', + usage: '用量', + today: '今日', + total: '近30天', + quota: '额度', + lastUsedAt: '上次使用时间', + useKey: '使用密钥', + useKeyModal: { + title: '使用 API 密钥', + description: '将以下环境变量添加到您的终端配置文件或直接在终端中运行。', + copy: '复制', + copied: '已复制', + note: '这些环境变量将在当前终端会话中生效。如需永久配置,请将其添加到 ~/.bashrc、~/.zshrc 或相应的配置文件中。', + noGroupTitle: '请先分配分组', + noGroupDescription: + '此 API 密钥尚未分配分组,请先在密钥列表中点击分组列进行分配,然后才能查看使用配置。', + openai: { + description: '将以下配置文件添加到 Codex CLI 配置目录中。', + configTomlHint: '请确保以下内容位于 config.toml 文件的开头部分', + note: '请确保配置目录存在。macOS/Linux 用户可运行 mkdir -p ~/.codex 创建目录。', + noteWindows: + '按 Win+R,输入 %userprofile%\\.codex 打开配置目录。如目录不存在,请先手动创建。' + }, + cliTabs: { + claudeCode: 'Claude Code', + geminiCli: 'Gemini CLI', + codexCli: 'Codex CLI', + codexCliWs: 'Codex CLI (WebSocket)', + opencode: 'OpenCode' + }, + antigravity: { + description: '为 Antigravity 分组配置 API 访问。请根据您使用的客户端选择对应的配置方式。', + claudeCode: 'Claude Code', + geminiCli: 'Gemini CLI', + claudeNote: + '这些环境变量将在当前终端会话中生效。如需永久配置,请将其添加到 ~/.bashrc、~/.zshrc 或相应的配置文件中。', + geminiNote: + '这些环境变量将在当前终端会话中生效。如需永久配置,请将其添加到 ~/.bashrc、~/.zshrc 或相应的配置文件中。' + }, + gemini: { + description: + '将以下环境变量添加到您的终端配置文件或直接在终端中运行,以配置 Gemini CLI 访问。', + modelComment: '如果你有 Gemini 3 权限可以填:gemini-3-pro-preview', + note: '这些环境变量将在当前终端会话中生效。如需永久配置,请将其添加到 ~/.bashrc、~/.zshrc 或相应的配置文件中。' + }, + opencode: { + title: 'OpenCode 配置示例', + subtitle: 'opencode.json', + hint: '配置文件路径:~/.config/opencode/opencode.json(或 opencode.jsonc),不存在需手动创建。可使用默认 provider(openai/anthropic/google)或自定义 provider_id。API Key 支持直接配置或通过客户端 /connect 命令配置。示例仅供参考,模型与选项可按需调整。' + } + }, + customKeyLabel: '自定义密钥', + customKeyPlaceholder: '输入自定义密钥(至少16个字符)', + customKeyHint: '仅允许字母、数字、下划线和连字符,最少16个字符。', + customKeyTooShort: '自定义密钥至少需要16个字符', + customKeyInvalidChars: '自定义密钥只能包含字母、数字、下划线和连字符', + customKeyRequired: '请输入自定义密钥', + ipRestriction: 'IP 限制', + ipWhitelist: 'IP 白名单', + ipWhitelistPlaceholder: '192.168.1.100\n10.0.0.0/8', + ipWhitelistHint: '每行一个 IP 或 CIDR,设置后仅允许这些 IP 使用此密钥', + ipBlacklist: 'IP 黑名单', + ipBlacklistPlaceholder: '1.2.3.4\n5.6.0.0/16', + ipBlacklistHint: '每行一个 IP 或 CIDR,这些 IP 将被禁止使用此密钥', + ipRestrictionEnabled: '已配置 IP 限制', + ccSwitchNotInstalled: + 'CC-Switch 未安装或协议处理程序未注册。请先安装 CC-Switch 或手动复制 API 密钥。', + ccsClientSelect: { + title: '选择客户端', + description: '请选择您要导入到 CC-Switch 的客户端类型:', + claudeCode: 'Claude Code', + claudeCodeDesc: '导入为 Claude Code 配置', + geminiCli: 'Gemini CLI', + geminiCliDesc: '导入为 Gemini CLI 配置' + }, + // 配额和有效期 + quotaLimit: '额度限制', + quotaAmount: '额度金额 (USD)', + quotaAmountPlaceholder: '输入 USD 额度限制', + quotaAmountHint: '设置此密钥可消费的最大金额。0 = 无限制。', + quotaUsed: '已用额度', + reset: '重置', + resetQuotaUsed: '将已用额度重置为 0', + resetQuotaTitle: '确认重置额度', + resetQuotaConfirmMessage: '确定要将密钥 "{name}" 的已用额度(${used})重置为 0 吗?此操作不可撤销。', + quotaResetSuccess: '额度重置成功', + failedToResetQuota: '重置额度失败', + rateLimitColumn: '速率限制', + rateLimitSection: '速率限制', + resetUsage: '重置', + rateLimit5h: '5小时限额 (USD)', + rateLimit1d: '日限额 (USD)', + rateLimit7d: '7天限额 (USD)', + rateLimitHint: '设置此密钥在指定时间窗口内的最大消费额。0 = 无限制。', + rateLimitUsage: '速率限制用量', + resetRateLimitUsage: '重置速率限制用量', + resetRateLimitTitle: '确认重置速率限制', + resetRateLimitConfirmMessage: '确定要重置密钥 "{name}" 的速率限制用量吗?所有时间窗口的已用额度将归零。此操作不可撤销。', + rateLimitResetSuccess: '速率限制已重置', + failedToResetRateLimit: '重置速率限制失败', + resetNow: '即将重置', + expiration: '密钥有效期', + expiresInDays: '{days} 天', + extendDays: '+{days} 天', + customDate: '自定义', + expirationDate: '过期时间', + expirationDateHint: '选择此 API 密钥的过期时间。', + currentExpiration: '当前过期时间', + expiresAt: '过期时间', + noExpiration: '永久有效', + status: { + active: '活跃', + inactive: '已停用', + quota_exhausted: '额度耗尽', + expired: '已过期' + } + }, + + // Usage + usage: { + title: '使用记录', + description: '查看和分析您的 API 使用历史', + costDetails: '费用明细', + tokenDetails: 'Token 明细', + cacheTtlOverriddenHint: '缓存 TTL Override 已启用', + cacheTtlOverriddenLabel: 'TTL 替换', + cacheTtlOverridden5m: '按 5m 计费', + cacheTtlOverridden1h: '按 1h 计费', + totalRequests: '总请求数', + totalTokens: '总 Token', + cacheTotal: '缓存', + cacheBreakdown: '缓存 Token 明细', + cacheCreationTokensLabel: '缓存创建', + cacheReadTokensLabel: '缓存读取', + totalCost: '总消费', + standardCost: '标准', + actualCost: '实际', + accountCost: '成本', + userBilled: '用户扣费', + accountBilled: '账号计费', + resetNow: '现在', + resetPending: '待刷新', + accountMultiplier: '账号倍率', + avgDuration: '平均耗时', + inSelectedRange: '所选范围内', + perRequest: '每次请求', + apiKeyFilter: 'API 密钥', + allApiKeys: '全部密钥', + timeRange: '时间范围', + exportCsv: '导出 CSV', + exportExcel: '导出 Excel', + exportingProgress: '正在导出数据...', + exportedCount: '已导出 {current}/{total} 条', + estimatedTime: '预计剩余时间:{time}', + cancelExport: '取消导出', + exportCancelled: '导出已取消', + exporting: '导出中...', + preparingExport: '正在准备导出...', + model: '模型', + requestedModel: '请求', + upstreamModel: '上游', + reasoningEffort: '推理强度', + endpoint: '端点', + endpointDistribution: '端点分布', + inbound: '入站', + upstream: '上游', + mapping: '映射', + path: '路径', + inboundEndpoint: '入站端点', + upstreamEndpoint: '上游端点', + type: '类型', + tokens: 'Token', + cost: '费用', + firstToken: '首 Token', + duration: '耗时', + time: '时间', + ws: 'WS', + stream: '流式', + sync: '同步', + cyber: '安全策略', + unknown: '未知', + in: '输入', + out: '输出', + cacheHit: '缓存命中', + cacheCreate: '缓存创建', + cacheHitRate: '缓存命中率', + inputTokenPrice: '输入单价', + outputTokenPrice: '输出单价', + perMillionTokens: '/ 1M Token', + unitPrice: '单次价格', + imageUnitPrice: '单张价格', + imageTotalPrice: '图片总价', + imageCount: '图片张数', + imageBillingSize: '计费尺寸', + imageInputSize: '输入尺寸', + imageOutputSize: '输出尺寸', + imageOutputTokens: '图片输出 Token', + imageOutputTokenPrice: '图片输出单价', + imageOutputCost: '图片输出费用', + imageSizeSource: '尺寸来源', + imageSizeBreakdown: '尺寸明细', + imageSizeSourceOutput: '上游输出', + imageSizeSourceInput: '请求输入', + imageSizeSourceDefault: '默认计费档位', + imageSizeSourceLegacy: '历史记录', + imageSizeSourceMissing: '未记录', + imageSizeNotRecorded: '未记录', + imageSizeLegacyUnstandardized: '历史非标准', + imageSizeUnknown: '未知', + cacheRead: '读取', + cacheWrite: '写入', + serviceTier: '服务档位', + serviceTierPriority: 'Fast', + serviceTierFlex: 'Flex', + serviceTierStandard: 'Standard', + rate: '倍率', + original: '原始', + billed: '计费', + noRecords: '未找到使用记录,请尝试调整筛选条件。', + failedToLoad: '加载使用记录失败', + noDataToExport: '没有可导出的数据', + exportSuccess: '使用数据导出成功', + exportFailed: '使用数据导出失败', + exportExcelSuccess: '使用数据导出成功(Excel格式)', + exportExcelFailed: '使用数据导出失败', + imageUnit: '张', + userAgent: 'User-Agent', + ipGeo: { + fetch: '获取地区', + fetching: '获取中...', + failed: '获取失败', + private: '内网地址', + refreshTitle: '刷新地区信息', + batchFetch: '批量获取地区', + batchFetching: '获取中...', + pending: '{count} 个 IP 待获取地区', + batchFailed: '批量获取地区信息失败', + detailOrg: '运营商', + detailTimezone: '时区', + detailAccuracy: '定位精度', + detailCoordinates: '坐标', + }, + tabs: { usage: '用量明细', errors: '错误请求' }, + errors: { + time: '时间', model: '模型', endpoint: '端点', status: '状态码', + category: '分类', platform: '平台', message: '错误信息', + keyName: 'Key 名称', keyDeleted: '已删除', allKeys: '全部 Key', + modelPlaceholder: '搜索模型', allCategories: '全部分类', allStatuses: '全部状态码', + empty: '暂无错误请求', failedToLoad: '加载错误请求失败', + categories: { + auth: '认证失败', rate_limit: '限流', quota: '余额/订阅', + invalid_request: '参数错误', service_unavailable: '服务暂时不可用', + upstream: '上游错误', internal: '平台错误', other: '其他', cyber: '安全策略', + }, + detail: { + title: '错误请求详情', + responseBody: '上游响应内容', + upstreamStatus: '上游状态码', + loadFailed: '加载详情失败,请稍后重试', + }, + }, + }, + + // Shared keys for channel monitor (admin + user views) + monitorCommon: { + status: { + operational: '正常', + degraded: '降级', + failed: '失败', + error: '错误', + unknown: '-' + }, + providers: { + openai: 'OpenAI', + anthropic: 'Anthropic', + gemini: 'Gemini' + }, + extraModelsHeader: '附加模型', + extraModelsEmpty: '无附加模型', + latencyEmpty: '-', + availabilityPrefix: '可用性', + dialogLatency: '对话延迟', + endpointPing: '端点 PING', + history60pts: '近 {n} 次记录', + nextUpdateIn: '{n}s 后刷新', + past: 'PAST', + now: 'NOW', + maintenancePaused: '维护中 · 已暂停时间线采集', + extraModelsCount: '+ {n} 模型', + pollEvery: '{n}s 轮询', + updatedAt: '更新于 {time}', + relativeSecondsAgo: '{n} 秒前', + relativeMinutesAgo: '{n} 分钟前', + relativeHoursAgo: '{n} 小时前', + relativeDaysAgo: '{n} 天前' + }, + + // Channel Status (user-facing read-only view) + channelStatus: { + title: '渠道状态', + description: '查看渠道可用性、延迟和近期状态', + searchPlaceholder: '搜索渠道...', + allProviders: '全部供应商', + loadError: '加载渠道状态失败', + detailLoadError: '加载渠道详情失败', + detailTitle: '渠道详情', + closeDetail: '关闭', + windowTab: { + '7d': '7 天', + '15d': '15 天', + '30d': '30 天' + }, + overall: { + operational: 'OPERATIONAL', + degraded: 'DEGRADED', + unavailable: 'UNAVAILABLE' + }, + columns: { + name: '名称', + provider: '供应商', + groupName: '分组', + primaryModel: '主模型', + availability7d: '7 天可用率', + latency: '延迟 (ms)' + }, + detailColumns: { + model: '模型', + latestStatus: '最新状态', + latestLatency: '最新延迟 (ms)', + availability7d: '7 天可用率', + availability15d: '15 天可用率', + availability30d: '30 天可用率', + avgLatency7d: '7 天平均延迟 (ms)' + }, + empty: { + title: '暂无可显示的渠道', + description: '管理员尚未配置可监控的渠道。' + } + }, + + // Available Channels (user-facing) + availableChannels: { + title: '可用渠道', + description: '查看您可访问的渠道与其支持的模型、定价', + searchPlaceholder: '搜索渠道或模型...', + empty: '暂无可用渠道', + noModels: '未配置模型', + noPricing: '未配置定价', + exclusive: '专属', + public: '公开', + exclusiveTooltip: '管理员授权给你的专属分组', + publicTooltip: '对所有用户公开的分组', + columns: { + name: '渠道名', + description: '描述', + platform: '平台', + groups: '我可访问的分组', + supportedModels: '支持模型' + }, + pricing: { + billingMode: '计费模式', + billingModeToken: '按 Token', + billingModePerRequest: '按次', + billingModeImage: '按图片', + inputPrice: '输入', + outputPrice: '输出', + cacheWritePrice: '缓存写入', + cacheReadPrice: '缓存读取', + imageOutputPrice: '图片输出', + perRequestPrice: '每次请求', + intervals: '阶梯定价', + unitPerMillion: '/ 1M token', + unitPerRequest: '/ 次' + } + }, + + affiliate: { + title: '邀请返利', + description: '邀请新用户注册,并将返利额度转入账户余额', + yourCode: '我的邀请码', + inviteLink: '邀请链接', + copyCode: '复制邀请码', + copyLink: '复制链接', + codeCopied: '邀请码已复制', + linkCopied: '邀请链接已复制', + loadFailed: '加载邀请返利数据失败', + transferFailed: '转入余额失败', + stats: { + rebateRate: '我的返利比例', + rebateRateHint: '被邀请用户每次充值后你可获得的返利比例', + invitedUsers: '邀请人数', + availableQuota: '可转返利额度', + frozenQuota: '冻结中', + frozenQuotaHint: '新产生的返利正在冻结期中', + totalQuota: '历史返利额度' + }, + transfer: { + title: '返利额度转余额', + description: '将当前可用返利额度一键转入账户余额', + button: '转入余额', + transferring: '转入中...', + empty: '当前没有可转入额度', + success: '已转入余额:{amount}' + }, + invitees: { + title: '已邀请用户', + empty: '暂无邀请记录', + columns: { + email: '邮箱', + username: '用户名', + rebate: '返利明细', + joinedAt: '注册时间' + } + }, + tips: { + title: '使用说明', + line1: '将邀请码或邀请链接分享给新用户。', + line2: '被邀请用户充值后,你可获得 {rate} 的返利额度。', + line3: '返利额度可随时转入账户余额。', + line4: '新产生的返利需要经过冻结期后才能提现。' + } + }, + + // Redeem + redeem: { + title: '兑换码', + description: '输入兑换码以充值余额或增加并发数', + currentBalance: '当前余额', + concurrency: '并发数', + requests: '请求', + redeemCodeLabel: '兑换码', + redeemCodePlaceholder: '请输入兑换码', + redeemCodeHint: '兑换码区分大小写', + redeeming: '兑换中...', + redeemButton: '兑换', + redeemSuccess: '兑换成功!', + redeemFailed: '兑换失败', + added: '已添加', + concurrentRequests: '并发请求', + newBalance: '新余额', + newConcurrency: '新并发数', + aboutCodes: '关于兑换码', + codeRule1: '每个兑换码只能使用一次', + codeRule2: '兑换码可以增加余额、并发数或试用权限', + codeRule3: '如有兑换问题,请联系客服', + codeRule4: '余额和并发数即时更新', + recentActivity: '最近活动', + historyWillAppear: '您的兑换历史将显示在这里', + balanceAddedRedeem: '余额充值(兑换)', + balanceAddedAffiliate: '余额充值(返利转入)', + balanceAddedAdmin: '余额充值(管理员)', + balanceDeductedAdmin: '余额扣除(管理员)', + concurrencyAddedRedeem: '并发增加(兑换)', + concurrencyAddedAdmin: '并发增加(管理员)', + concurrencyReducedAdmin: '并发减少(管理员)', + adminAdjustment: '管理员调整', + subscriptionAssigned: '订阅已分配', + subscriptionAssignedDesc: '您已获得 {groupName} 的访问权限', + subscriptionDays: '{days} 天', + days: '天', + codeRedeemSuccess: '兑换成功!', + failedToRedeem: '兑换失败,请检查兑换码后重试。', + subscriptionRefreshFailed: '兑换成功,但订阅状态刷新失败。', + pleaseEnterCode: '请输入兑换码' + }, + + // Profile + profile: { + title: '个人设置', + description: '管理您的账户信息和设置', + accountBalance: '账户余额', + concurrencyLimit: '并发限制', + rpmLimit: 'RPM 限制', + rpmUnlimited: '不限制', + memberSince: '注册时间', + overviewTitle: '账户总览', + overviewDescription: '快速查看账号状态、资料来源与常用设置。', + basicsTitle: '资料与头像', + basicsDescription: '维护公开展示信息,并保持头像与昵称风格一致。', + linkedProfileSources: '资料来源', + linkedProfileSourcesDescription: '部分头像和昵称可能同步自第三方登录方式。', + securityTitle: '安全设置', + securityDescription: '密码、双因素认证和通知提醒集中放在右侧。', + administrator: '管理员', + user: '用户', + username: '用户名', + email: '邮箱', + status: '状态', + role: '角色', + enterUsername: '输入用户名', + editProfile: '编辑个人资料', + updateProfile: '更新资料', + updating: '更新中...', + updateSuccess: '资料更新成功', + updateFailed: '资料更新失败', + usernameRequired: '用户名不能为空', + changePassword: '修改密码', + currentPassword: '当前密码', + newPassword: '新密码', + confirmNewPassword: '确认新密码', + passwordHint: '密码至少需要 8 个字符', + changingPassword: '修改中...', + changePasswordButton: '修改密码', + passwordsNotMatch: '两次输入的密码不一致', + passwordTooShort: '密码至少需要 8 个字符', + passwordChangeSuccess: '密码修改成功', + passwordChangeFailed: '密码修改失败', + // TOTP 2FA + totp: { + title: '双因素认证 (2FA)', + description: '使用 Google Authenticator 等应用增强账户安全', + enabled: '已启用', + enabledAt: '启用时间', + notEnabled: '未启用', + notEnabledHint: '启用双因素认证可以增强账户安全性', + enable: '启用', + disable: '禁用', + featureDisabled: '功能未开放', + featureDisabledHint: '管理员尚未开放双因素认证功能', + setupTitle: '设置双因素认证', + setupStep1: '使用认证器应用扫描下方二维码', + setupStep2: '输入应用显示的 6 位验证码', + manualEntry: '无法扫码?手动输入密钥:', + enterCode: '输入 6 位验证码', + verify: '验证', + setupFailed: '获取设置信息失败', + verifyFailed: '验证码错误,请重试', + enableSuccess: '双因素认证已启用', + disableTitle: '禁用双因素认证', + disableWarning: '禁用后,登录时将不再需要验证码。这可能会降低您的账户安全性。', + enterPassword: '请输入当前密码确认', + confirmDisable: '确认禁用', + disableSuccess: '双因素认证已禁用', + disableFailed: '禁用失败,请检查密码是否正确', + loginTitle: '双因素认证', + loginHint: '请输入您认证器应用显示的 6 位验证码', + loginFailed: '验证失败,请重试', + // New translations for email verification + verifyEmailFirst: '请先验证您的邮箱', + verifyPasswordFirst: '请先验证您的身份', + emailCode: '邮箱验证码', + enterEmailCode: '请输入 6 位验证码', + sendCode: '发送验证码', + codeSent: '验证码已发送到您的邮箱', + sendCodeFailed: '发送验证码失败' + }, + balanceNotify: { + title: '余额不足提醒', + description: '当账户余额低于阈值时发送邮件提醒', + enabled: '启用余额不足提醒', + threshold: '自定义提醒阈值', + thresholdHint: '留空使用系统默认值', + thresholdPlaceholder: '输入金额', + systemDefault: '系统默认值', + extraEmails: '通知邮箱', + extraEmailsHint: '必须添加并验证邮箱后,余额不足时才能收到提醒邮件', + primaryEmail: '主邮箱', + noExtraEmails: '暂无额外通知邮箱', + enterEmail: '输入邮箱地址', + addEmail: '添加邮箱', + emailPlaceholder: '输入邮箱地址', + sendCode: '发送验证码', + resend: '重发', + codeSent: '验证码已发送', + codeSentTo: '验证码已发送到 {email}', + enterCode: '输入验证码', + codePlaceholder: '6位验证码', + verify: '验证', + emailAdded: '邮箱已添加', + emailRemoved: '邮箱已移除', + verifySuccess: '邮箱添加成功', + removeEmail: '移除', + removeSuccess: '邮箱已移除', + emailDuplicate: '该邮箱已存在', + maxEmailsReached: '已达到通知邮箱数量上限', + unverified: '未验证', + verified: '已验证', + }, + avatar: { + title: '资料头像', + description: '仅支持上传头像图片;静态图片会自动压缩到 20KB 以内后再保存。', + uploadAction: '上传图片', + uploadHint: '上传图片时会自动压缩静态图片到 20KB 以内,GIF 需自行控制在 20KB 以内', + uploadRequired: '请先上传头像图片', + saveSuccess: '头像已更新', + deleteSuccess: '头像已删除', + invalidType: '请选择图片文件', + gifTooLarge: 'GIF 头像必须在 20KB 以内', + compressTooLarge: '无法将图片压缩到 20KB 以内,请换一张更小的图片', + compressFailed: '压缩所选图片失败', + readFailed: '读取所选图片失败', + emptyDeleteHint: '当前没有可删除的头像', + }, + authBindings: { + title: '登录方式绑定', + description: '查看当前绑定状态,并将更多第三方登录方式关联到这个账号。', + bindAction: '绑定 {providerName}', + bindSuccess: '账号绑定成功', + emailPlaceholder: '输入邮箱地址', + codePlaceholder: '输入验证码', + passwordPlaceholder: '设置登录密码', + replaceEmailPasswordPlaceholder: '输入当前密码', + sendCodeAction: '发送验证码', + manageEmailAction: '管理邮箱', + hideEmailFormAction: '收起邮箱表单', + confirmEmailBindAction: '绑定邮箱', + confirmEmailReplaceAction: '更换主邮箱', + codeSentTo: '验证码已发送到 {email}', + replaceSuccess: '主邮箱已更新', + unbindAction: '解绑', + unbindSuccess: '{providerName} 已解绑', + boundCount: '已关联 {count} 条记录', + status: { + bound: '已绑定', + notBound: '未绑定', + }, + providers: { + email: '邮箱', + linuxdo: 'LinuxDo', + dingtalk: '钉钉', + oidc: '{providerName}', + wechat: '微信', + }, + notes: { + emailManagedFromProfile: '主邮箱在资料表单中管理', + canUnbind: '你可以解绑这个登录方式。', + bindAnotherBeforeUnbind: '请先绑定其他登录方式,再解除当前绑定。', + }, + source: { + avatar: '头像当前来自 {providerName}', + username: '昵称当前来自 {providerName}', + }, + } + }, + + // Empty States + empty: { + noData: '暂无数据' + }, + + // Table + table: { + expandActions: '展开更多操作', + collapseActions: '收起操作' + }, + + // Pagination + pagination: { + showing: '显示', + to: '至', + of: '共', + results: '条结果', + page: '页', + pageOf: '第 {page} / {total} 页', + previous: '上一页', + next: '下一页', + perPage: '每页', + goToPage: '跳转到第 {page} 页', + jumpTo: '跳转页', + jumpPlaceholder: '页码', + jumpAction: '跳转' + }, + + // Errors + errors: { + somethingWentWrong: '出错了', + pageNotFound: '页面未找到', + unauthorized: '未授权', + forbidden: '禁止访问', + serverError: '服务器错误', + networkError: '网络错误', + timeout: '请求超时', + tryAgain: '请重试' + }, + + // Dates + dates: { + today: '今天', + yesterday: '昨天', + thisWeek: '本周', + lastWeek: '上周', + thisMonth: '本月', + lastMonth: '上月', + last24Hours: '近24小时', + last7Days: '近 7 天', + last14Days: '近 14 天', + last30Days: '近 30 天', + custom: '自定义', + startDate: '开始日期', + endDate: '结束日期', + apply: '应用', + selectDateRange: '选择日期范围' + }, + + // Admin +} diff --git a/frontend/src/i18n/locales/zh/index.ts b/frontend/src/i18n/locales/zh/index.ts new file mode 100644 index 0000000000..377c67aec7 --- /dev/null +++ b/frontend/src/i18n/locales/zh/index.ts @@ -0,0 +1,13 @@ +import landing from './landing' +import common from './common' +import dashboard from './dashboard' +import admin from './admin' +import misc from './misc' + +export default { + ...landing, + ...common, + ...dashboard, + admin, + ...misc, +} diff --git a/frontend/src/i18n/locales/zh/landing.ts b/frontend/src/i18n/locales/zh/landing.ts new file mode 100644 index 0000000000..85b7ef48c9 --- /dev/null +++ b/frontend/src/i18n/locales/zh/landing.ts @@ -0,0 +1,255 @@ +export default { + batchImageGuide: { + title: '图片批量生成', + description: '一次提交多条提示词,任务完成后可统一下载图片结果' + }, + // Home Page + home: { + viewOnGithub: '在 GitHub 上查看', + viewDocs: '查看文档', + docs: '文档', + switchToLight: '切换到浅色模式', + switchToDark: '切换到深色模式', + dashboard: '控制台', + login: '登录', + getStarted: '立即开始', + goToDashboard: '进入控制台', + // 新增:面向用户的价值主张 + heroSubtitle: '一个密钥,畅用多个 AI 模型', + heroDescription: '无需管理多个订阅账号,一站式接入 Claude、GPT、Gemini 等主流 AI 服务', + tags: { + subscriptionToApi: '订阅转 API', + stickySession: '会话保持', + realtimeBilling: '按量计费' + }, + // 用户痛点区块 + painPoints: { + title: '你是否也遇到这些问题?', + items: { + expensive: { + title: '订阅费用高', + desc: '每个 AI 服务都要单独订阅,每月支出越来越多' + }, + complex: { + title: '多账号难管理', + desc: '不同平台的账号、密钥分散各处,管理起来很麻烦' + }, + unstable: { + title: '服务不稳定', + desc: '单一账号容易触发限制,影响正常使用' + }, + noControl: { + title: '用量无法控制', + desc: '不知道钱花在哪了,也无法限制团队成员的使用' + } + } + }, + // 解决方案区块 + solutions: { + title: '我们帮你解决', + subtitle: '简单三步,开始省心使用 AI' + }, + features: { + unifiedGateway: '一键接入', + unifiedGatewayDesc: '获取一个 API 密钥,即可调用所有已接入的 AI 模型,无需分别申请。', + multiAccount: '稳定可靠', + multiAccountDesc: '智能调度多个上游账号,自动切换和负载均衡,告别频繁报错。', + balanceQuota: '用多少付多少', + balanceQuotaDesc: '按实际使用量计费,支持设置配额上限,团队用量一目了然。' + }, + // 优势对比 + comparison: { + title: '为什么选择我们?', + headers: { + feature: '对比项', + official: '官方订阅', + us: '本平台' + }, + items: { + pricing: { + feature: '付费方式', + official: '固定月费,用不完也付', + us: '按量付费,用多少付多少' + }, + models: { + feature: '模型选择', + official: '单一服务商', + us: '多模型随意切换' + }, + management: { + feature: '账号管理', + official: '每个服务单独管理', + us: '统一密钥,一站管理' + }, + stability: { + feature: '服务稳定性', + official: '单账号易触发限制', + us: '多账号池,自动切换' + }, + control: { + feature: '用量控制', + official: '无法限制', + us: '可设配额、查明细' + } + } + }, + providers: { + title: '已支持的 AI 模型', + description: '一个 API,多种选择', + supported: '已支持', + soon: '即将推出', + claude: 'Claude', + gemini: 'Gemini', + antigravity: 'Antigravity', + more: '更多' + }, + // CTA 区块 + cta: { + title: '准备好开始了吗?', + description: '注册即可获得免费试用额度,体验一站式 AI 服务', + button: '免费注册' + }, + footer: { + allRightsReserved: '保留所有权利。' + } + }, + + // Key Usage Query Page + keyUsage: { + title: 'API Key 用量查询', + subtitle: '输入您的 API Key 以查看实时消费金额与使用状态', + placeholder: 'sk-ant-mirror-xxxxxxxxxxxx', + query: '查询', + querying: '查询中...', + privacyNote: '您的 Key 仅在浏览器本地处理,不会被存储', + dateRange: '统计范围:', + dateRangeToday: '今日', + dateRange7d: '7 天', + dateRange30d: '30 天', + dateRange90d: '90 天', + dateRangeCustom: '自定义', + apply: '应用', + used: '已使用', + detailInfo: '详细信息', + tokenStats: 'Token 统计', + dailyDetail: '按日明细', + modelStats: '模型用量统计', + // Table headers + date: '日期', + model: '模型', + requests: '请求数', + inputTokens: '输入 Tokens', + outputTokens: '输出 Tokens', + cacheCreationTokens: '缓存创建', + cacheReadTokens: '缓存读取', + cacheWriteTokens: '缓存写入', + totalTokens: '总 Tokens', + cost: '费用', + // Status + quotaMode: 'Key 限额模式', + walletBalance: '钱包余额', + // Ring card titles + totalQuota: '总额度', + limit5h: '5 小时限额', + limitDaily: '日限额', + limit7d: '7 天限额', + limitWeekly: '周限额', + limitMonthly: '月限额', + // Detail rows + remainingQuota: '剩余额度', + expiresAt: '过期时间', + todayExpires: '(今日到期)', + daysLeft: '({days} 天)', + usedQuota: '已用额度', + resetNow: '即将重置', + subscriptionType: '订阅类型', + subscriptionExpires: '订阅到期', + // Usage stat cells + todayRequests: '今日请求', + todayInputTokens: '今日输入', + todayOutputTokens: '今日输出', + todayTokens: '今日 Tokens', + todayCacheCreation: '今日缓存创建', + todayCacheRead: '今日缓存读取', + todayCost: '今日费用', + rpmTpm: 'RPM / TPM', + totalRequests: '累计请求', + totalInputTokens: '累计输入', + totalOutputTokens: '累计输出', + totalTokensLabel: '累计 Tokens', + totalCacheCreation: '累计缓存创建', + totalCacheRead: '累计缓存读取', + totalCost: '累计费用', + avgDuration: '平均耗时', + // Messages + enterApiKey: '请输入 API Key', + querySuccess: '查询成功', + queryFailed: '查询失败', + queryFailedRetry: '查询失败,请稍后重试', + noDailyUsage: '暂无按日用量数据', + }, + + // Setup Wizard + setup: { + title: 'Sub2API 安装向导', + description: '配置您的 Sub2API 实例', + database: { + title: '数据库配置', + description: '连接到您的 PostgreSQL 数据库', + host: '主机', + port: '端口', + username: '用户名', + password: '密码', + databaseName: '数据库名称', + sslMode: 'SSL 模式', + passwordPlaceholder: '密码', + ssl: { + disable: '禁用', + require: '要求', + verifyCa: '验证 CA', + verifyFull: '完全验证' + } + }, + redis: { + title: 'Redis 配置', + description: '连接到您的 Redis 服务器', + host: '主机', + port: '端口', + password: '密码(可选)', + database: '数据库', + passwordPlaceholder: '密码', + enableTls: '启用 TLS', + enableTlsHint: '连接 Redis 时使用 TLS(公共 CA 证书)' + }, + admin: { + title: '管理员账户', + description: '创建您的管理员账户', + email: '邮箱', + password: '密码', + confirmPassword: '确认密码', + passwordPlaceholder: '至少 8 个字符', + confirmPasswordPlaceholder: '确认密码', + passwordMismatch: '密码不匹配' + }, + ready: { + title: '准备安装', + description: '检查您的配置并完成安装', + database: '数据库', + redis: 'Redis', + adminEmail: '管理员邮箱' + }, + status: { + testing: '测试中...', + success: '连接成功', + testConnection: '测试连接', + installing: '安装中...', + completeInstallation: '完成安装', + completed: '安装完成!', + redirecting: '正在跳转到登录页面...', + restarting: '服务正在重启,请稍候...', + timeout: '服务重启时间超出预期,请手动刷新页面。' + } + }, + + // Common +} diff --git a/frontend/src/i18n/locales/zh/misc.ts b/frontend/src/i18n/locales/zh/misc.ts new file mode 100644 index 0000000000..67dffc92bd --- /dev/null +++ b/frontend/src/i18n/locales/zh/misc.ts @@ -0,0 +1,615 @@ +export default { + + // Subscription Progress (Header component) + subscriptionProgress: { + title: '我的订阅', + viewDetails: '查看订阅详情', + activeCount: '{count} 个有效订阅', + daily: '每日', + weekly: '每周', + monthly: '每月', + daysRemaining: '剩余 {days} 天', + expired: '已过期', + expiresToday: '今天到期', + expiresTomorrow: '明天到期', + viewAll: '查看全部订阅', + noSubscriptions: '暂无有效订阅', + unlimited: '无限制' + }, + + // Version Badge + version: { + currentVersion: '当前版本', + latestVersion: '最新版本', + upToDate: '已是最新版本', + updateAvailable: '有新版本可用!', + releaseNotes: '更新日志', + noReleaseNotes: '暂无更新日志', + viewUpdate: '查看更新', + viewRelease: '查看发布', + viewChangelog: '查看更新日志', + refresh: '刷新', + sourceMode: '源码构建', + sourceModeHint: '源码构建请使用 git pull 更新', + updateNow: '立即更新', + updating: '正在更新...', + updateComplete: '更新完成', + updateFailed: '更新失败', + restartRequired: '请重启服务以应用更新', + restartNow: '立即重启', + restarting: '正在重启...', + retry: '重试' + }, + + // Recharge / Subscription Page + purchase: { + title: '充值/订阅', + description: '通过内嵌页面完成充值/订阅', + openInNewTab: '新窗口打开', + notEnabledTitle: '该功能未开启', + notEnabledDesc: '管理员暂未开启充值/订阅入口,请联系管理员。', + notConfiguredTitle: '充值/订阅链接未配置', + notConfiguredDesc: '管理员已开启入口,但尚未配置充值/订阅链接,请联系管理员。' + }, + + // Custom Page (iframe embed) + customPage: { + title: '自定义页面', + openInNewTab: '新窗口打开', + notFoundTitle: '页面不存在', + notFoundDesc: '该自定义页面不存在或已被删除。', + notConfiguredTitle: '页面链接未配置', + notConfiguredDesc: '该自定义页面的 URL 未正确配置。', + tableOfContents: '目录', + copyCode: '复制', + copiedCode: '已复制', + copyCodeFailed: '失败' + }, + + // Announcements Page + announcements: { + title: '公告', + description: '查看系统公告', + unreadOnly: '仅显示未读', + markRead: '标记已读', + markAllRead: '全部已读', + viewAll: '查看全部公告', + markedAsRead: '已标记为已读', + allMarkedAsRead: '所有公告已标记为已读', + newCount: '有 {count} 条新公告', + readAt: '已读时间', + read: '已读', + unread: '未读', + startsAt: '开始时间', + endsAt: '结束时间', + empty: '暂无公告', + emptyUnread: '暂无未读公告', + total: '条公告', + emptyDescription: '暂时没有任何系统公告', + readStatus: '您已阅读此公告', + markReadHint: '点击"已读"标记此公告' + }, + + // User Subscriptions Page + userSubscriptions: { + title: '我的订阅', + description: '查看您的订阅计划和用量', + noActiveSubscriptions: '暂无有效订阅', + noActiveSubscriptionsDesc: '您没有任何有效订阅。请联系管理员获取订阅。', + failedToLoad: '加载订阅失败', + status: { + active: '有效', + expired: '已过期', + revoked: '已撤销' + }, + usage: '用量', + expires: '到期时间', + noExpiration: '无到期时间', + unlimited: '无限制', + unlimitedDesc: '该订阅无用量限制', + daily: '每日', + weekly: '每周', + monthly: '每月', + daysRemaining: '剩余 {days} 天', + expiresOn: '{date} 到期', + resetIn: '{time} 后重置', + quotaEndsIn: '额度将在 {time} 后结束', + windowNotActive: '等待首次使用', + usageOf: '已用 {used} / {limit}' + }, + + // Onboarding Tour + onboarding: { + restartTour: '重新查看新手引导', + dontShowAgain: '不再提示', + dontShowAgainTitle: '永久关闭新手引导', + confirmDontShow: '确定不再显示新手引导吗?\n\n您可以随时在右上角头像菜单中重新开启。', + confirmExit: '确定要退出新手引导吗?您可以随时在右上角菜单重新开始。', + interactiveHint: '按 Enter 或点击继续', + navigation: { + flipPage: '翻页', + exit: '退出' + }, + // Admin tour steps + admin: { + welcome: { + title: '👋 欢迎使用 Sub2API', + description: + '

Sub2API 是一个强大的 AI 服务中转平台,让您轻松管理和分发 AI 服务。

🎯 核心功能:

  • 📦 分组管理 - 创建不同的服务套餐(VIP、免费试用等)
  • 🔗 账号池 - 连接多个上游 AI 服务商账号
  • 🔑 密钥分发 - 为用户生成独立的 API Key
  • 💰 计费管理 - 灵活的费率和配额控制

接下来,我们将用 3 分钟带您完成首次配置 →

', + nextBtn: '开始配置 🚀', + prevBtn: '跳过' + }, + groupManage: { + title: '📦 第一步:分组管理', + description: + '

什么是分组?

分组是 Sub2API 的核心概念,它就像一个"服务套餐":

  • 🎯 每个分组可以包含多个上游账号
  • 💰 每个分组有独立的计费倍率
  • 👥 可以设置为公开或专属分组

💡 示例:您可以创建"VIP专线"(高倍率)和"免费试用"(低倍率)两个分组

👉 点击左侧的"分组管理"开始

' + }, + createGroup: { + title: '➕ 创建新分组', + description: + '

现在让我们创建第一个分组。

📝 提示:建议先创建一个测试分组,熟悉流程后再创建正式分组

👉 点击"创建分组"按钮

' + }, + groupName: { + title: '✏️ 1. 分组名称', + description: + '

为您的分组起一个易于识别的名称。

💡 命名建议:
  • "测试分组" - 用于测试
  • "VIP专线" - 高质量服务
  • "免费试用" - 体验版

填写完成后点击"下一步"继续

', + nextBtn: '下一步' + }, + groupPlatform: { + title: '🤖 2. 选择平台', + description: + '

选择该分组支持的 AI 平台。

📌 平台说明:
  • Anthropic - Claude 系列模型
  • OpenAI - GPT 系列模型
  • Google - Gemini 系列模型

一个分组只能选择一个平台

', + nextBtn: '下一步' + }, + groupMultiplier: { + title: '💰 3. 费率倍数', + description: + '

设置该分组的计费倍率,控制用户的实际扣费。

⚙️ 计费规则:
  • 1.0 - 原价计费(成本价)
  • 1.5 - 用户消耗 $1,扣除 $1.5
  • 2.0 - 用户消耗 $1,扣除 $2
  • 0.8 - 补贴模式(亏本运营)

建议测试分组设置为 1.0

', + nextBtn: '下一步' + }, + groupExclusive: { + title: '🔒 4. 专属分组(可选)', + description: + '

控制分组的可见性和访问权限。

🔐 权限说明:
  • 关闭 - 公开分组,所有用户可见
  • 开启 - 专属分组,仅指定用户可见

💡 使用场景:VIP 用户专属、内部测试、特殊客户等

', + nextBtn: '下一步' + }, + groupSubmit: { + title: '✅ 保存分组', + description: + '

确认信息无误后,点击创建按钮保存分组。

⚠️ 注意:分组创建后,平台类型不可修改,其他信息可以随时编辑

📌 下一步:创建成功后,我们将添加上游账号到这个分组

👉 点击"创建"按钮

' + }, + accountManage: { + title: '🔗 第二步:添加账号', + description: + '

太棒了!分组已创建成功 🎉

现在需要添加上游 AI 服务商的账号,让分组能够实际提供服务。

🔑 账号的作用:
  • 连接到上游 AI 服务(Claude、GPT 等)
  • 一个分组可以包含多个账号(负载均衡)
  • 支持 OAuth 和 Session Key 两种方式

👉 点击左侧的"账号管理"

' + }, + createAccount: { + title: '➕ 添加新账号', + description: + '

点击按钮开始添加您的第一个上游账号。

💡 提示:建议使用 OAuth 方式,更安全且无需手动提取密钥

👉 点击"添加账号"按钮

' + }, + accountName: { + title: '✏️ 1. 账号名称', + description: + '

为账号设置一个便于识别的名称。

💡 命名建议:"Claude主账号"、"GPT备用1"、"测试账号" 等

', + nextBtn: '下一步' + }, + accountPlatform: { + title: '🤖 2. 选择平台', + description: + '

选择该账号对应的服务商平台。

⚠️ 重要:平台必须与刚才创建的分组平台一致

', + nextBtn: '下一步' + }, + accountType: { + title: '🔐 3. 授权方式', + description: + '

选择账号的授权方式。

✅ 推荐:OAuth 方式
  • 无需手动提取密钥
  • 更安全,支持自动刷新
  • 适用于 Claude Code、ChatGPT OAuth
📌 Session Key 方式
  • 需要手动从浏览器提取
  • 可能需要定期更新
  • 适用于不支持 OAuth 的平台
', + nextBtn: '下一步' + }, + accountPriority: { + title: '⚖️ 4. 优先级(可选)', + description: + '

设置账号的调用优先级。

📊 优先级规则:
  • 数字越小,优先级越高
  • 系统优先使用低数值账号
  • 相同优先级则随机选择

💡 使用场景:主账号设置低数值,备用账号设置高数值

', + nextBtn: '下一步' + }, + accountGroups: { + title: '🎯 5. 分配分组', + description: + '

关键步骤!将账号分配到刚才创建的分组。

⚠️ 重要提醒:
  • 必须勾选至少一个分组
  • 未分配分组的账号无法使用
  • 一个账号可以分配给多个分组

💡 提示:请勾选刚才创建的测试分组

', + nextBtn: '下一步' + }, + accountSubmit: { + title: '✅ 保存账号', + description: + '

确认信息无误后,点击保存按钮。

📌 OAuth 授权流程:
  • 点击保存后会跳转到服务商页面
  • 在服务商页面完成登录授权
  • 授权成功后自动返回

📌 下一步:账号添加成功后,我们将创建 API 密钥

👉 点击"保存"按钮

' + }, + keyManage: { + title: '🔑 第三步:生成密钥', + description: + '

恭喜!账号配置完成 🎉

最后一步,生成 API Key 来测试服务是否正常工作。

🔑 API Key 的作用:
  • 用于调用 AI 服务的凭证
  • 每个 Key 绑定一个分组
  • 可以设置配额和有效期
  • 支持独立的使用统计

👉 点击左侧的"API 密钥"

' + }, + createKey: { + title: '➕ 创建密钥', + description: + '

点击按钮创建您的第一个 API Key。

💡 提示:创建后请立即复制保存,密钥只显示一次

👉 点击"创建密钥"按钮

' + }, + keyName: { + title: '✏️ 1. 密钥名称', + description: + '

为密钥设置一个便于管理的名称。

💡 命名建议:"测试密钥"、"生产环境"、"移动端" 等

', + nextBtn: '下一步' + }, + keyGroup: { + title: '🎯 2. 选择分组', + description: + '

选择刚才配置好的分组。

📌 分组决定:
  • 该密钥可以使用哪些账号
  • 计费倍率是多少
  • 是否为专属密钥

💡 提示:选择刚才创建的测试分组

', + nextBtn: '下一步' + }, + keySubmit: { + title: '🎉 生成并复制', + description: + '

点击创建后,系统会生成完整的 API Key。

⚠️ 重要提醒:
  • 密钥只显示一次,请立即复制
  • 丢失后需要重新生成
  • 妥善保管,不要泄露给他人
🚀 下一步:
  • 复制生成的 sk-xxx 密钥
  • 在支持 OpenAI 接口的客户端中使用
  • 开始体验 AI 服务!

👉 点击"创建"按钮

' + } + }, + // User tour steps + user: { + welcome: { + title: '👋 欢迎使用 Sub2API', + description: + '

您好!欢迎来到 Sub2API AI 服务平台。

🎯 快速开始:

  • 🔑 创建 API 密钥
  • 📋 复制密钥到您的应用
  • 🚀 开始使用 AI 服务

只需 1 分钟,让我们开始吧 →

', + nextBtn: '开始 🚀', + prevBtn: '跳过' + }, + keyManage: { + title: '🔑 API 密钥管理', + description: + '

在这里管理您的所有 API 访问密钥。

📌 什么是 API 密钥?
API 密钥是您访问 AI 服务的凭证,就像一把钥匙,让您的应用能够调用 AI 能力。

👉 点击进入密钥页面

' + }, + createKey: { + title: '➕ 创建新密钥', + description: + '

点击按钮创建您的第一个 API 密钥。

💡 提示:创建后密钥只显示一次,请务必复制保存

👉 点击"创建密钥"

' + }, + keyName: { + title: '✏️ 密钥名称', + description: + '

为密钥起一个便于识别的名称。

💡 示例:"我的第一个密钥"、"测试用" 等

', + nextBtn: '下一步' + }, + keyGroup: { + title: '🎯 选择分组', + description: + '

选择管理员为您分配的服务分组。

📌 分组说明:
不同分组可能有不同的服务质量和计费标准,请根据需要选择。

', + nextBtn: '下一步' + }, + keySubmit: { + title: '🎉 完成创建', + description: + '

点击确认创建您的 API 密钥。

⚠️ 重要:
  • 创建后请立即复制密钥(sk-xxx)
  • 密钥只显示一次,丢失需重新生成

🚀 如何使用:
将密钥配置到支持 OpenAI 接口的任何客户端(如 ChatBox、OpenCat 等),即可开始使用!

👉 点击"创建"按钮

' + } + } + }, + + // Payment System + payment: { + title: '充值/订阅', + amountLabel: '充值金额', + paymentAmount: '支付金额', + creditedBalance: '到账余额', + quickAmounts: '快捷金额', + customAmount: '自定义金额', + enterAmount: '输入金额', + paymentMethod: '支付方式', + fee: '手续费', + actualPay: '实付金额', + createOrder: '确认支付', + methods: { + easypay: '易支付', + alipay: '支付宝', + wxpay: '微信支付', + stripe: 'Stripe', + airwallex: 'Airwallex', + card: '银行卡', + link: 'Link', + alipay_direct: '支付宝(直连)', + wxpay_direct: '微信支付(直连)', + }, + status: { + pending: '待支付', + paid: '已支付', + recharging: '充值中', + completed: '已完成', + expired: '已过期', + cancelled: '已取消', + failed: '失败', + refund_requested: '退款申请中', + refunding: '退款中', + refund_pending: '退款处理中', + refunded: '已退款', + partially_refunded: '部分退款', + refund_failed: '退款失败', + }, + qr: { + scanToPay: '请扫码支付', + scanAlipay: '支付宝扫码支付', + scanWxpay: '微信扫码支付', + scanAlipayHint: '请使用手机打开支付宝,扫描二维码完成支付', + scanWxpayHint: '请使用手机打开微信,扫描二维码完成支付', + payInNewWindow: '请在新窗口中完成支付', + payInNewWindowHint: '支付页面已在新窗口打开,请在新窗口中完成支付后返回此页面', + openPayWindow: '重新打开支付页面', + expiresIn: '剩余支付时间', + expired: '订单已过期', + expiredDesc: '订单已超时,请重新创建订单', + cancelled: '订单已取消', + cancelledDesc: '您已取消本次支付', + waitingPayment: '等待支付...', + cancelOrder: '取消订单', + }, + orders: { + title: '我的订单', + empty: '暂无订单', + orderId: '订单 ID', + orderNo: '订单编号', + amount: '金额', + payAmount: '实付', + creditedAmount: '到账金额', + fee: '手续费', + baseAmount: '充值金额', + includedInPayAmount: '已含在实付金额中', + status: '状态', + paymentMethod: '支付方式', + createdAt: '创建时间', + cancel: '取消订单', + userId: '用户 ID', + orderType: '订单类型', + actions: '操作', + requestRefund: '申请退款', + }, + result: { + success: '支付成功', + subscriptionSuccess: '订阅成功', + processing: '支付处理中', + processingHint: '支付结果仍在确认中,页面会自动刷新。', + failed: '支付失败', + backToRecharge: '返回充值', + viewOrders: '查看订单', + }, + currentBalance: '当前余额', + groupFallback: '分组 #{id}', + rechargeAccount: '充值账户', + activeSubscription: '当前订阅', + noActiveSubscription: '暂无有效订阅', + tabTopUp: '充值', + tabSubscribe: '订阅', + noPlans: '暂无可用订阅套餐', + notAvailable: '充值功能暂未开放', + confirmSubscription: '确认订阅', + confirmCancel: '确定要取消此订单吗?', + amountTooLow: '最低金额为 {min}', + amountTooHigh: '最高金额为 {max}', + amountNoMethod: '该金额没有可用的支付方式', + rechargeRatePreview: '当前倍率:1 CNY = {usd} USD', + refundReason: '退款原因', + refundReasonPlaceholder: '请描述您的退款原因', + stripeLoadFailed: '支付组件加载失败,请刷新页面重试', + stripeMissingParams: '缺少订单ID或支付密钥', + stripeNotConfigured: 'Stripe 未配置', + airwallexLoadFailed: 'Airwallex 支付组件加载失败,请刷新页面重试', + airwallexMissingParams: '缺少 Airwallex 支付参数', + errors: { + tooManyPending: '待支付订单过多(最多 {max} 个),请先完成或取消现有订单', + cancelRateLimited: '取消订单过于频繁,请稍后再试', + wechatH5NotAuthorized: '当前商户未开通微信 H5 支付,请在微信中打开当前页面继续支付。', + wechatPaymentMpNotConfigured: '当前站点未完成公众号/JSAPI 支付配置,暂时无法在微信内直接拉起支付。', + wechatJsapiUnavailable: '当前环境未能拉起微信支付,请确认正在微信内打开本页后重试。', + wechatJsapiFailed: '微信支付未完成,请重新拉起支付或改用扫码支付。', + wechatUnavailable: '当前微信支付暂不可用,请稍后重试。', + wechatOpenInWeChatHint: '请复制当前页面链接到微信内打开,或直接改用电脑端微信扫码支付。', + wechatScanOnDesktopHint: '电脑端请直接使用微信扫一扫完成支付;移动端请在微信内打开当前页面。', + wechatSwitchBrowserHint: '请改用电脑端微信扫码,或在外部浏览器重新打开本页后再试。', + mobilePaymentFallbackToQr: '当前商户未开通移动支付,已自动切换为扫码支付。', + alipayDesktopUnavailable: '当前支付宝桌面支付未成功生成二维码。', + alipayDesktopQrHint: '电脑端支付宝应展示扫码单,请刷新后重试,或确认浏览器未拦截当前支付页。', + alipayMobileUnavailable: '当前页面未成功跳转到支付宝。', + alipayMobileOpenHint: '请允许当前页面打开支付宝 App,或改用系统浏览器重新发起支付。', + // Structured error codes (reason strings from backend ApplicationError) + PAYMENT_DISABLED: '支付系统已关闭', + USER_INACTIVE: '账号已被禁用', + BALANCE_PAYMENT_DISABLED: '余额充值功能已关闭', + INVALID_AMOUNT: '金额无效', + INVALID_INPUT: '参数有误', + PLAN_NOT_AVAILABLE: '套餐不存在或已下架', + GROUP_NOT_FOUND: '订阅分组不可用', + GROUP_TYPE_MISMATCH: '分组类型不是订阅类型', + TOO_MANY_PENDING: '待支付订单过多(最多 {max} 个),请先完成或取消现有订单', + DAILY_LIMIT_EXCEEDED: '今日充值已达上限,剩余额度 {remaining}', + PAYMENT_GATEWAY_ERROR: '支付方式不可用', + NO_AVAILABLE_INSTANCE: '暂无可用的支付通道', + PAYMENT_PROVIDER_MISCONFIGURED: '支付通道配置错误,请联系管理员', + WXPAY_CONFIG_MISSING_KEY: '微信支付配置缺少必填项:{key}', + WXPAY_CONFIG_INVALID_KEY_LENGTH: '微信支付 {key} 长度错误,应为 {expected} 字节(实际 {actual})', + WXPAY_CONFIG_INVALID_KEY: '微信支付 {key} 格式错误,请确认复制了完整的 PEM 内容', + PENDING_ORDERS: '该服务商有未完成的订单,请等待订单完成后再操作', + PAYMENT_PROVIDER_CONFLICT: '该支付方式已有其他启用中的服务商实例,请先停用后再继续。', + CANCEL_RATE_LIMITED: '取消订单过于频繁,请稍后再试', + NOT_FOUND: '订单不存在', + FORBIDDEN: '无权限操作此订单', + CONFLICT: '订单状态已变更,请刷新', + INVALID_ORDER_TYPE: '仅余额订单可申请退款', + INVALID_STATUS: '当前订单状态不允许此操作', + BALANCE_NOT_ENOUGH: '退款金额超过余额', + REFUND_AMOUNT_EXCEEDED: '退款金额超过充值金额', + REFUND_FAILED: '退款失败', + }, + airwallexPay: 'Airwallex 支付', + stripePay: '立即支付', + stripeSuccessProcessing: '支付成功,正在处理订单...', + stripePopup: { + redirecting: '正在跳转到支付页面...', + loadingQr: '正在获取微信支付二维码...', + timeout: '等待支付凭证超时,请重试', + qrFailed: '未能获取微信支付二维码', + }, + subscribeNow: '立即开通', + renewNow: '续费', + selectPlan: '选择套餐', + planFeatures: '功能特性', + planCard: { + rate: '倍率', + peakRate: '高峰倍率', + dailyLimit: '日限额', + weeklyLimit: '周限额', + monthlyLimit: '月限额', + quota: '配额', + unlimited: '无限制', + models: '模型', + }, + days: '天', + months: '个月', + years: '年', + oneMonth: '1 个月', + oneYear: '1 年', + perMonth: '月', + perYear: '年', + admin: { + tabs: { + overview: '概览', + orders: '订单管理', + channels: '支付渠道', + plans: '订阅套餐', + }, + todayRevenue: '今日收入', + totalRevenue: '总收入', + todayOrders: '今日订单', + orderCount: '订单数', + avgAmount: '平均金额', + revenue: '收入', + dailyRevenue: '每日收入', + paymentDistribution: '支付方式分布', + colUser: '用户', + topUsers: '消费排行', + noData: '暂无数据', + days: '天', + weeks: '周', + months: '月', + searchOrders: '搜索订单...', + allStatuses: '全部状态', + allPaymentTypes: '全部支付方式', + allOrderTypes: '全部订单类型', + orderDetail: '订单详情', + orderType: '订单类型', + orders: '订单', + balanceOrder: '余额充值', + subscriptionOrder: '订阅', + paidAt: '支付时间', + completedAt: '完成时间', + expiresAt: '过期时间', + feeRate: '手续费率', + refund: '退款', + refundOrder: '退款订单', + refundAmount: '退款金额', + maxRefundable: '最大可退金额', + refundReason: '退款原因', + refundReasonPlaceholder: '请输入退款原因', + confirmRefund: '确认退款', + refundSuccess: '退款成功', + refundPending: '退款处理中,待网关确认', + queryRefundStatus: '查询退款状态', + refundInfo: '退款信息', + refundEnabled: '允许退款', + alreadyRefunded: '已退款', + deductBalance: '扣除余额', + deductBalanceHint: '从用户余额中扣回充值金额', + userBalance: '用户余额', + orderAmount: '订单金额', + insufficientBalance: '余额不足,将扣至 $0', + noDeduction: '将不扣除用户余额', + forceRefund: '强制退款(忽略余额检查)', + orderCancelled: '订单已取消', + retry: '重试', + retrySuccess: '重试成功', + approveRefund: '批准退款', + retryRefund: '重试退款', + refundRequestInfo: '退款申请信息', + refundRequestedAt: '申请时间', + refundRequestedBy: '申请人', + refundRequestReason: '申请原因', + auditLogs: '操作日志', + operator: '操作人', + channelName: '渠道名称', + channelDescription: '渠道描述', + createChannel: '创建渠道', + editChannel: '编辑渠道', + deleteChannel: '删除渠道', + deleteChannelConfirm: '确定要删除此渠道吗?', + planName: '套餐名称', + planDescription: '套餐描述', + createPlan: '创建套餐', + editPlan: '编辑套餐', + deletePlan: '删除套餐', + deletePlanConfirm: '确定要删除此套餐吗?', + originalPrice: '原价', + price: '价格', + subscriptionCnyPayPreview: 'CNY 通道实扣预览:{amount}', + subscriptionCnyPayPreviewWithFee: '(含 {feeRate}% 手续费:{total})', + validityDays: '有效期(天)', + validityUnit: '有效期单位', + sortOrder: '排序', + forSale: '上架状态', + onSale: '上架', + offSale: '下架', + group: '分组', + groupId: '分组 ID', + features: '功能特性', + featuresHint: '每行一个特性', + featuresPlaceholder: '输入套餐特性...', + providerManagement: '服务商管理', + providerManagementDesc: '管理支付服务商实例', + createProvider: '创建服务商', + editProvider: '编辑服务商', + deleteProvider: '删除服务商', + deleteProviderConfirm: '确定要删除此服务商吗?', + providerName: '服务商名称', + providerKey: '服务商标识', + selectProviderKey: '选择服务商标识', + providerConfig: '服务商配置', + noProviders: '暂无服务商', + noProvidersHint: '创建一个服务商实例以开始接受支付', + supportedTypes: '支持的支付方式', + supportedTypesHint: '选择此服务商支持的支付方式', + rateMultiplier: '费率倍数', + dashboardTitle: '支付概览', + dashboardDesc: '充值订单统计与分析', + daySuffix: '天', + paymentConfigTitle: '支付配置', + paymentConfigDesc: '管理支付服务商与相关设置', + plansPageTitle: '订阅套餐管理', + plansPageDesc: '管理订阅套餐配置', + tabPlanConfig: '套餐配置', + tabUserSubs: '用户订阅', + selectGroup: '请选择分组', + groupRequired: '请选择订阅分组', + priceRequired: '价格必须大于 0', + validityDaysRequired: '有效期天数必须大于 0', + groupMissing: '缺失', + groupInfo: '分组信息', + platform: '平台', + rateMultiplierLabel: '倍率', + dailyLimit: '日限额', + weeklyLimit: '周限额', + monthlyLimit: '月限额', + unlimited: '无限制', + searchUserSubs: '搜索用户订阅...', + daily: '日', + weekly: '周', + monthly: '月', + subsStatus: { + active: '生效中', + expired: '已过期', + revoked: '已撤销', + }, + }, + }, + +}