refactor(i18n): 拆分 zh/en 语言包为域模块(深比较等价校验通过)

This commit is contained in:
shaw
2026-07-08 08:49:19 +08:00
parent 317339e716
commit d9e514f982
26 changed files with 15373 additions and 15285 deletions
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -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
}
@@ -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,
}
+803
View File
@@ -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
}
@@ -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)
}
@@ -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
}
File diff suppressed because it is too large Load Diff
+417
View File
@@ -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
}
+811
View File
@@ -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
}
+13
View File
@@ -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,
}
+255
View File
@@ -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
}
+591
View File
@@ -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: '<div style="line-height: 1.8;"><p style="margin-bottom: 16px;">Sub2API is a powerful AI service gateway platform that helps you easily manage and distribute AI services.</p><p style="margin-bottom: 12px;"><b>🎯 Core Features:</b></p><ul style="margin-left: 20px; margin-bottom: 16px;"><li>📦 <b>Group Management</b> - Create service tiers (VIP, Free Trial, etc.)</li><li>🔗 <b>Account Pool</b> - Connect multiple upstream AI service accounts</li><li>🔑 <b>Key Distribution</b> - Generate independent API Keys for users</li><li>💰 <b>Billing Control</b> - Flexible rate and quota management</li></ul><p style="color: #10b981; font-weight: 600;">Let\'s complete the initial setup in 3 minutes →</p></div>',
nextBtn: 'Start Setup 🚀',
prevBtn: 'Skip'
},
groupManage: {
title: '📦 Step 1: Group Management',
description: '<div style="line-height: 1.7;"><p style="margin-bottom: 12px;"><b>What is a Group?</b></p><p style="margin-bottom: 12px;">Groups are the core concept of Sub2API, like a "service package":</p><ul style="margin-left: 20px; margin-bottom: 12px; font-size: 13px;"><li>🎯 Each group can contain multiple upstream accounts</li><li>💰 Each group has independent billing multiplier</li><li>👥 Can be set as public or exclusive</li></ul><p style="margin-top: 12px; padding: 8px 12px; background: #f0fdf4; border-left: 3px solid #10b981; border-radius: 4px; font-size: 13px;"><b>💡 Example:</b> You can create "VIP Premium" (high rate) and "Free Trial" (low rate) groups</p><p style="margin-top: 16px; color: #10b981; font-weight: 600;">👉 Click "Group Management" on the left sidebar</p></div>'
},
createGroup: {
title: '➕ Create New Group',
description: '<div style="line-height: 1.7;"><p style="margin-bottom: 12px;">Let\'s create your first group.</p><p style="padding: 8px 12px; background: #eff6ff; border-left: 3px solid #3b82f6; border-radius: 4px; font-size: 13px; margin-bottom: 12px;"><b>📝 Tip:</b> Recommend creating a test group first to familiarize yourself with the process</p><p style="color: #10b981; font-weight: 600;">👉 Click the "Create Group" button</p></div>'
},
groupName: {
title: '✏️ 1. Group Name',
description: '<div style="line-height: 1.7;"><p style="margin-bottom: 12px;">Give your group an easy-to-identify name.</p><div style="padding: 8px 12px; background: #f0fdf4; border-left: 3px solid #10b981; border-radius: 4px; font-size: 13px; margin-bottom: 12px;"><b>💡 Naming Suggestions:</b><ul style="margin: 8px 0 0 16px;"><li>"Test Group" - For testing</li><li>"VIP Premium" - High-quality service</li><li>"Free Trial" - Trial version</li></ul></div><p style="font-size: 13px; color: #6b7280;">Click "Next" when done</p></div>',
nextBtn: 'Next'
},
groupPlatform: {
title: '🤖 2. Select Platform',
description: '<div style="line-height: 1.7;"><p style="margin-bottom: 12px;">Choose the AI platform this group supports.</p><div style="padding: 8px 12px; background: #eff6ff; border-left: 3px solid #3b82f6; border-radius: 4px; font-size: 13px; margin-bottom: 12px;"><b>📌 Platform Guide:</b><ul style="margin: 8px 0 0 16px;"><li><b>Anthropic</b> - Claude models</li><li><b>OpenAI</b> - GPT models</li><li><b>Google</b> - Gemini models</li></ul></div><p style="font-size: 13px; color: #6b7280;">One group can only have one platform</p></div>',
nextBtn: 'Next'
},
groupMultiplier: {
title: '💰 3. Rate Multiplier',
description: '<div style="line-height: 1.7;"><p style="margin-bottom: 12px;">Set the billing multiplier to control user charges.</p><div style="padding: 8px 12px; background: #fef3c7; border-left: 3px solid #f59e0b; border-radius: 4px; font-size: 13px; margin-bottom: 12px;"><b>⚙️ Billing Rules:</b><ul style="margin: 8px 0 0 16px;"><li><b>1.0</b> - Original price (cost price)</li><li><b>1.5</b> - User consumes $1, charged $1.5</li><li><b>2.0</b> - User consumes $1, charged $2</li><li><b>0.8</b> - Subsidy mode (loss-making)</li></ul></div><p style="font-size: 13px; color: #6b7280;">Recommend setting test group to 1.0</p></div>',
nextBtn: 'Next'
},
groupExclusive: {
title: '🔒 4. Exclusive Group (Optional)',
description: '<div style="line-height: 1.7;"><p style="margin-bottom: 12px;">Control group visibility and access permissions.</p><div style="padding: 8px 12px; background: #eff6ff; border-left: 3px solid #3b82f6; border-radius: 4px; font-size: 13px; margin-bottom: 12px;"><b>🔐 Permission Guide:</b><ul style="margin: 8px 0 0 16px;"><li><b>Off</b> - Public group, visible to all users</li><li><b>On</b> - Exclusive group, only for specified users</li></ul></div><p style="padding: 8px 12px; background: #f0fdf4; border-left: 3px solid #10b981; border-radius: 4px; font-size: 13px;"><b>💡 Use Cases:</b> VIP exclusive, internal testing, special customers</p></div>',
nextBtn: 'Next'
},
groupSubmit: {
title: '✅ Save Group',
description: '<div style="line-height: 1.7;"><p style="margin-bottom: 12px;">Confirm the information and click create to save the group.</p><p style="padding: 8px 12px; background: #fef3c7; border-left: 3px solid #f59e0b; border-radius: 4px; font-size: 13px; margin-bottom: 12px;"><b>⚠️ Note:</b> Platform type cannot be changed after creation, but other settings can be edited anytime</p><p style="padding: 8px 12px; background: #f0fdf4; border-left: 3px solid #10b981; border-radius: 4px; font-size: 13px;"><b>📌 Next Step:</b> After creation, we\'ll add upstream accounts to this group</p><p style="margin-top: 12px; color: #10b981; font-weight: 600;">👉 Click "Create" button</p></div>'
},
accountManage: {
title: '🔗 Step 2: Add Account',
description: '<div style="line-height: 1.7;"><p style="margin-bottom: 12px;"><b>Great! Group created successfully 🎉</b></p><p style="margin-bottom: 12px;">Now add upstream AI service accounts to enable actual service delivery.</p><div style="padding: 8px 12px; background: #eff6ff; border-left: 3px solid #3b82f6; border-radius: 4px; font-size: 13px; margin-bottom: 12px;"><b>🔑 Account Purpose:</b><ul style="margin: 8px 0 0 16px;"><li>Connect to upstream AI services (Claude, GPT, etc.)</li><li>One group can contain multiple accounts (load balancing)</li><li>Supports OAuth and Session Key methods</li></ul></div><p style="margin-top: 16px; color: #10b981; font-weight: 600;">👉 Click "Account Management" on the left sidebar</p></div>'
},
createAccount: {
title: '➕ Add New Account',
description: '<div style="line-height: 1.7;"><p style="margin-bottom: 12px;">Click the button to start adding your first upstream account.</p><p style="padding: 8px 12px; background: #f0fdf4; border-left: 3px solid #10b981; border-radius: 4px; font-size: 13px;"><b>💡 Tip:</b> Recommend using OAuth method - more secure and no manual key extraction needed</p><p style="margin-top: 12px; color: #10b981; font-weight: 600;">👉 Click "Add Account" button</p></div>'
},
accountName: {
title: '✏️ 1. Account Name',
description: '<div style="line-height: 1.7;"><p style="margin-bottom: 12px;">Set an easy-to-identify name for the account.</p><p style="padding: 8px 12px; background: #f0fdf4; border-left: 3px solid #10b981; border-radius: 4px; font-size: 13px;"><b>💡 Naming Suggestions:</b> "Claude Main", "GPT Backup 1", "Test Account", etc.</p></div>',
nextBtn: 'Next'
},
accountPlatform: {
title: '🤖 2. Select Platform',
description: '<div style="line-height: 1.7;"><p style="margin-bottom: 12px;">Choose the service provider platform for this account.</p><p style="padding: 8px 12px; background: #fef3c7; border-left: 3px solid #f59e0b; border-radius: 4px; font-size: 13px;"><b>⚠️ Important:</b> Platform must match the group you just created</p></div>',
nextBtn: 'Next'
},
accountType: {
title: '🔐 3. Authorization Method',
description: '<div style="line-height: 1.7;"><p style="margin-bottom: 12px;">Choose the account authorization method.</p><div style="padding: 8px 12px; background: #f0fdf4; border-left: 3px solid #10b981; border-radius: 4px; font-size: 13px; margin-bottom: 12px;"><b>✅ Recommended: OAuth Method</b><ul style="margin: 8px 0 0 16px;"><li>No manual key extraction needed</li><li>More secure with auto-refresh support</li><li>Works with Claude Code, ChatGPT OAuth</li></ul></div><div style="padding: 8px 12px; background: #eff6ff; border-left: 3px solid #3b82f6; border-radius: 4px; font-size: 13px;"><b>📌 Session Key Method</b><ul style="margin: 8px 0 0 16px;"><li>Requires manual extraction from browser</li><li>May need periodic updates</li><li>For platforms without OAuth support</li></ul></div></div>',
nextBtn: 'Next'
},
accountPriority: {
title: '⚖️ 4. Priority (Optional)',
description: '<div style="line-height: 1.7;"><p style="margin-bottom: 12px;">Set the account call priority.</p><div style="padding: 8px 12px; background: #eff6ff; border-left: 3px solid #3b82f6; border-radius: 4px; font-size: 13px; margin-bottom: 12px;"><b>📊 Priority Rules:</b><ul style="margin: 8px 0 0 16px;"><li>Lower number = higher priority</li><li>System uses low-value accounts first</li><li>Same priority = random selection</li></ul></div><p style="padding: 8px 12px; background: #f0fdf4; border-left: 3px solid #10b981; border-radius: 4px; font-size: 13px;"><b>💡 Use Case:</b> Set main account to lower value, backup accounts to higher value</p></div>',
nextBtn: 'Next'
},
accountGroups: {
title: '🎯 5. Assign Groups',
description: '<div style="line-height: 1.7;"><p style="margin-bottom: 12px;"><b>Key Step!</b> Assign the account to the group you just created.</p><div style="padding: 8px 12px; background: #fee2e2; border-left: 3px solid #ef4444; border-radius: 4px; font-size: 13px; margin-bottom: 12px;"><b>⚠️ Important Reminder:</b><ul style="margin: 8px 0 0 16px;"><li>Must select at least one group</li><li>Unassigned accounts cannot be used</li><li>One account can be assigned to multiple groups</li></ul></div><p style="padding: 8px 12px; background: #f0fdf4; border-left: 3px solid #10b981; border-radius: 4px; font-size: 13px;"><b>💡 Tip:</b> Select the test group you just created</p></div>',
nextBtn: 'Next'
},
accountSubmit: {
title: '✅ Save Account',
description: '<div style="line-height: 1.7;"><p style="margin-bottom: 12px;">Confirm the information and click save.</p><div style="padding: 8px 12px; background: #eff6ff; border-left: 3px solid #3b82f6; border-radius: 4px; font-size: 13px; margin-bottom: 12px;"><b>📌 OAuth Flow:</b><ul style="margin: 8px 0 0 16px;"><li>Will redirect to service provider page after clicking save</li><li>Complete login and authorization on provider page</li><li>Auto-return after successful authorization</li></ul></div><p style="padding: 8px 12px; background: #f0fdf4; border-left: 3px solid #10b981; border-radius: 4px; font-size: 13px;"><b>📌 Next Step:</b> After adding account, we\'ll create an API key</p><p style="margin-top: 12px; color: #10b981; font-weight: 600;">👉 Click "Save" button</p></div>'
},
keyManage: {
title: '🔑 Step 3: Generate Key',
description: '<div style="line-height: 1.7;"><p style="margin-bottom: 12px;"><b>Congratulations! Account setup complete 🎉</b></p><p style="margin-bottom: 12px;">Final step: generate an API Key to test if the service works properly.</p><div style="padding: 8px 12px; background: #eff6ff; border-left: 3px solid #3b82f6; border-radius: 4px; font-size: 13px; margin-bottom: 12px;"><b>🔑 API Key Purpose:</b><ul style="margin: 8px 0 0 16px;"><li>Credential for calling AI services</li><li>Each key is bound to one group</li><li>Can set quota and expiration</li><li>Supports independent usage statistics</li></ul></div><p style="margin-top: 16px; color: #10b981; font-weight: 600;">👉 Click "API Keys" on the left sidebar</p></div>'
},
createKey: {
title: '➕ Create Key',
description: '<div style="line-height: 1.7;"><p style="margin-bottom: 12px;">Click the button to create your first API Key.</p><p style="padding: 8px 12px; background: #f0fdf4; border-left: 3px solid #10b981; border-radius: 4px; font-size: 13px;"><b>💡 Tip:</b> Copy and save immediately after creation - key is only shown once</p><p style="margin-top: 12px; color: #10b981; font-weight: 600;">👉 Click "Create Key" button</p></div>'
},
keyName: {
title: '✏️ 1. Key Name',
description: '<div style="line-height: 1.7;"><p style="margin-bottom: 12px;">Set an easy-to-manage name for the key.</p><p style="padding: 8px 12px; background: #f0fdf4; border-left: 3px solid #10b981; border-radius: 4px; font-size: 13px;"><b>💡 Naming Suggestions:</b> "Test Key", "Production", "Mobile", etc.</p></div>',
nextBtn: 'Next'
},
keyGroup: {
title: '🎯 2. Select Group',
description: '<div style="line-height: 1.7;"><p style="margin-bottom: 12px;">Select the group you just configured.</p><div style="padding: 8px 12px; background: #eff6ff; border-left: 3px solid #3b82f6; border-radius: 4px; font-size: 13px; margin-bottom: 12px;"><b>📌 Group Determines:</b><ul style="margin: 8px 0 0 16px;"><li>Which accounts this key can use</li><li>What billing multiplier applies</li><li>Whether it\'s an exclusive key</li></ul></div><p style="padding: 8px 12px; background: #f0fdf4; border-left: 3px solid #10b981; border-radius: 4px; font-size: 13px;"><b>💡 Tip:</b> Select the test group you just created</p></div>',
nextBtn: 'Next'
},
keySubmit: {
title: '🎉 Generate and Copy',
description: '<div style="line-height: 1.7;"><p style="margin-bottom: 12px;">System will generate a complete API Key after clicking create.</p><div style="padding: 8px 12px; background: #fee2e2; border-left: 3px solid #ef4444; border-radius: 4px; font-size: 13px; margin-bottom: 12px;"><b>⚠️ Important Reminder:</b><ul style="margin: 8px 0 0 16px;"><li>Key is only shown once, copy immediately</li><li>Need to regenerate if lost</li><li>Keep it safe, don\'t share with others</li></ul></div><div style="padding: 8px 12px; background: #f0fdf4; border-left: 3px solid #10b981; border-radius: 4px; font-size: 13px; margin-bottom: 12px;"><b>🚀 Next Steps:</b><ul style="margin: 8px 0 0 16px;"><li>Copy the generated sk-xxx key</li><li>Use in any OpenAI-compatible client</li><li>Start experiencing AI services!</li></ul></div><p style="margin-top: 12px; color: #10b981; font-weight: 600;">👉 Click "Create" button</p></div>'
}
},
// User tour steps
user: {
welcome: {
title: '👋 Welcome to Sub2API',
description: '<div style="line-height: 1.8;"><p style="margin-bottom: 16px;">Hello! Welcome to the Sub2API AI service platform.</p><p style="margin-bottom: 12px;"><b>🎯 Quick Start:</b></p><ul style="margin-left: 20px; margin-bottom: 16px;"><li>🔑 Create API Key</li><li>📋 Copy key to your application</li><li>🚀 Start using AI services</li></ul><p style="color: #10b981; font-weight: 600;">Just 1 minute, let\'s get started →</p></div>',
nextBtn: 'Start 🚀',
prevBtn: 'Skip'
},
keyManage: {
title: '🔑 API Key Management',
description: '<div style="line-height: 1.7;"><p style="margin-bottom: 12px;">Manage all your API access keys here.</p><p style="padding: 8px 12px; background: #eff6ff; border-left: 3px solid #3b82f6; border-radius: 4px; font-size: 13px;"><b>📌 What is an API Key?</b><br/>An API key is your credential for accessing AI services, like a key that allows your application to call AI capabilities.</p><p style="margin-top: 12px; color: #10b981; font-weight: 600;">👉 Click to enter key page</p></div>'
},
createKey: {
title: '➕ Create New Key',
description: '<div style="line-height: 1.7;"><p style="margin-bottom: 12px;">Click the button to create your first API key.</p><p style="padding: 8px 12px; background: #f0fdf4; border-left: 3px solid #10b981; border-radius: 4px; font-size: 13px;"><b>💡 Tip:</b> Key is only shown once after creation, make sure to copy and save</p><p style="margin-top: 12px; color: #10b981; font-weight: 600;">👉 Click "Create Key"</p></div>'
},
keyName: {
title: '✏️ Key Name',
description: '<div style="line-height: 1.7;"><p style="margin-bottom: 12px;">Give your key an easy-to-identify name.</p><p style="padding: 8px 12px; background: #f0fdf4; border-left: 3px solid #10b981; border-radius: 4px; font-size: 13px;"><b>💡 Examples:</b> "My First Key", "For Testing", etc.</p></div>',
nextBtn: 'Next'
},
keyGroup: {
title: '🎯 Select Group',
description: '<div style="line-height: 1.7;"><p style="margin-bottom: 12px;">Select the service group assigned by the administrator.</p><p style="padding: 8px 12px; background: #eff6ff; border-left: 3px solid #3b82f6; border-radius: 4px; font-size: 13px;"><b>📌 Group Info:</b><br/>Different groups may have different service quality and billing rates, choose according to your needs.</p></div>',
nextBtn: 'Next'
},
keySubmit: {
title: '🎉 Complete Creation',
description: '<div style="line-height: 1.7;"><p style="margin-bottom: 12px;">Click to confirm and create your API key.</p><div style="padding: 8px 12px; background: #fee2e2; border-left: 3px solid #ef4444; border-radius: 4px; font-size: 13px; margin-bottom: 12px;"><b>⚠️ Important:</b><ul style="margin: 8px 0 0 16px;"><li>Copy the key (sk-xxx) immediately after creation</li><li>Key is only shown once, need to regenerate if lost</li></ul></div><p style="padding: 8px 12px; background: #f0fdf4; border-left: 3px solid #10b981; border-radius: 4px; font-size: 13px;"><b>🚀 How to Use:</b><br/>Configure the key in any OpenAI-compatible client (like ChatBox, OpenCat, etc.) and start using!</p><p style="margin-top: 12px; color: #10b981; font-weight: 600;">👉 Click "Create" button</p></div>'
}
}
},
// 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',
},
},
},
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -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
}
@@ -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,
}
+810
View File
@@ -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
}
File diff suppressed because it is too large Load Diff
@@ -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
}
File diff suppressed because it is too large Load Diff
+416
View File
@@ -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
}
+816
View File
@@ -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
}
+13
View File
@@ -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,
}
+255
View File
@@ -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
}
+615
View File
@@ -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:
'<div style="line-height: 1.8;"><p style="margin-bottom: 16px;">Sub2API 是一个强大的 AI 服务中转平台,让您轻松管理和分发 AI 服务。</p><p style="margin-bottom: 12px;"><b>🎯 核心功能:</b></p><ul style="margin-left: 20px; margin-bottom: 16px;"><li>📦 <b>分组管理</b> - 创建不同的服务套餐(VIP、免费试用等)</li><li>🔗 <b>账号池</b> - 连接多个上游 AI 服务商账号</li><li>🔑 <b>密钥分发</b> - 为用户生成独立的 API Key</li><li>💰 <b>计费管理</b> - 灵活的费率和配额控制</li></ul><p style="color: #10b981; font-weight: 600;">接下来,我们将用 3 分钟带您完成首次配置 →</p></div>',
nextBtn: '开始配置 🚀',
prevBtn: '跳过'
},
groupManage: {
title: '📦 第一步:分组管理',
description:
'<div style="line-height: 1.7;"><p style="margin-bottom: 12px;"><b>什么是分组?</b></p><p style="margin-bottom: 12px;">分组是 Sub2API 的核心概念,它就像一个"服务套餐":</p><ul style="margin-left: 20px; margin-bottom: 12px; font-size: 13px;"><li>🎯 每个分组可以包含多个上游账号</li><li>💰 每个分组有独立的计费倍率</li><li>👥 可以设置为公开或专属分组</li></ul><p style="margin-top: 12px; padding: 8px 12px; background: #f0fdf4; border-left: 3px solid #10b981; border-radius: 4px; font-size: 13px;"><b>💡 示例:</b>您可以创建"VIP专线"(高倍率)和"免费试用"(低倍率)两个分组</p><p style="margin-top: 16px; color: #10b981; font-weight: 600;">👉 点击左侧的"分组管理"开始</p></div>'
},
createGroup: {
title: '➕ 创建新分组',
description:
'<div style="line-height: 1.7;"><p style="margin-bottom: 12px;">现在让我们创建第一个分组。</p><p style="padding: 8px 12px; background: #eff6ff; border-left: 3px solid #3b82f6; border-radius: 4px; font-size: 13px; margin-bottom: 12px;"><b>📝 提示:</b>建议先创建一个测试分组,熟悉流程后再创建正式分组</p><p style="color: #10b981; font-weight: 600;">👉 点击"创建分组"按钮</p></div>'
},
groupName: {
title: '✏️ 1. 分组名称',
description:
'<div style="line-height: 1.7;"><p style="margin-bottom: 12px;">为您的分组起一个易于识别的名称。</p><div style="padding: 8px 12px; background: #f0fdf4; border-left: 3px solid #10b981; border-radius: 4px; font-size: 13px; margin-bottom: 12px;"><b>💡 命名建议:</b><ul style="margin: 8px 0 0 16px;"><li>"测试分组" - 用于测试</li><li>"VIP专线" - 高质量服务</li><li>"免费试用" - 体验版</li></ul></div><p style="font-size: 13px; color: #6b7280;">填写完成后点击"下一步"继续</p></div>',
nextBtn: '下一步'
},
groupPlatform: {
title: '🤖 2. 选择平台',
description:
'<div style="line-height: 1.7;"><p style="margin-bottom: 12px;">选择该分组支持的 AI 平台。</p><div style="padding: 8px 12px; background: #eff6ff; border-left: 3px solid #3b82f6; border-radius: 4px; font-size: 13px; margin-bottom: 12px;"><b>📌 平台说明:</b><ul style="margin: 8px 0 0 16px;"><li><b>Anthropic</b> - Claude 系列模型</li><li><b>OpenAI</b> - GPT 系列模型</li><li><b>Google</b> - Gemini 系列模型</li></ul></div><p style="font-size: 13px; color: #6b7280;">一个分组只能选择一个平台</p></div>',
nextBtn: '下一步'
},
groupMultiplier: {
title: '💰 3. 费率倍数',
description:
'<div style="line-height: 1.7;"><p style="margin-bottom: 12px;">设置该分组的计费倍率,控制用户的实际扣费。</p><div style="padding: 8px 12px; background: #fef3c7; border-left: 3px solid #f59e0b; border-radius: 4px; font-size: 13px; margin-bottom: 12px;"><b>⚙️ 计费规则:</b><ul style="margin: 8px 0 0 16px;"><li><b>1.0</b> - 原价计费(成本价)</li><li><b>1.5</b> - 用户消耗 $1,扣除 $1.5</li><li><b>2.0</b> - 用户消耗 $1,扣除 $2</li><li><b>0.8</b> - 补贴模式(亏本运营)</li></ul></div><p style="font-size: 13px; color: #6b7280;">建议测试分组设置为 1.0</p></div>',
nextBtn: '下一步'
},
groupExclusive: {
title: '🔒 4. 专属分组(可选)',
description:
'<div style="line-height: 1.7;"><p style="margin-bottom: 12px;">控制分组的可见性和访问权限。</p><div style="padding: 8px 12px; background: #eff6ff; border-left: 3px solid #3b82f6; border-radius: 4px; font-size: 13px; margin-bottom: 12px;"><b>🔐 权限说明:</b><ul style="margin: 8px 0 0 16px;"><li><b>关闭</b> - 公开分组,所有用户可见</li><li><b>开启</b> - 专属分组,仅指定用户可见</li></ul></div><p style="padding: 8px 12px; background: #f0fdf4; border-left: 3px solid #10b981; border-radius: 4px; font-size: 13px;"><b>💡 使用场景:</b>VIP 用户专属、内部测试、特殊客户等</p></div>',
nextBtn: '下一步'
},
groupSubmit: {
title: '✅ 保存分组',
description:
'<div style="line-height: 1.7;"><p style="margin-bottom: 12px;">确认信息无误后,点击创建按钮保存分组。</p><p style="padding: 8px 12px; background: #fef3c7; border-left: 3px solid #f59e0b; border-radius: 4px; font-size: 13px; margin-bottom: 12px;"><b>⚠️ 注意:</b>分组创建后,平台类型不可修改,其他信息可以随时编辑</p><p style="padding: 8px 12px; background: #f0fdf4; border-left: 3px solid #10b981; border-radius: 4px; font-size: 13px;"><b>📌 下一步:</b>创建成功后,我们将添加上游账号到这个分组</p><p style="margin-top: 12px; color: #10b981; font-weight: 600;">👉 点击"创建"按钮</p></div>'
},
accountManage: {
title: '🔗 第二步:添加账号',
description:
'<div style="line-height: 1.7;"><p style="margin-bottom: 12px;"><b>太棒了!分组已创建成功 🎉</b></p><p style="margin-bottom: 12px;">现在需要添加上游 AI 服务商的账号,让分组能够实际提供服务。</p><div style="padding: 8px 12px; background: #eff6ff; border-left: 3px solid #3b82f6; border-radius: 4px; font-size: 13px; margin-bottom: 12px;"><b>🔑 账号的作用:</b><ul style="margin: 8px 0 0 16px;"><li>连接到上游 AI 服务(Claude、GPT 等)</li><li>一个分组可以包含多个账号(负载均衡)</li><li>支持 OAuth 和 Session Key 两种方式</li></ul></div><p style="margin-top: 16px; color: #10b981; font-weight: 600;">👉 点击左侧的"账号管理"</p></div>'
},
createAccount: {
title: '➕ 添加新账号',
description:
'<div style="line-height: 1.7;"><p style="margin-bottom: 12px;">点击按钮开始添加您的第一个上游账号。</p><p style="padding: 8px 12px; background: #f0fdf4; border-left: 3px solid #10b981; border-radius: 4px; font-size: 13px;"><b>💡 提示:</b>建议使用 OAuth 方式,更安全且无需手动提取密钥</p><p style="margin-top: 12px; color: #10b981; font-weight: 600;">👉 点击"添加账号"按钮</p></div>'
},
accountName: {
title: '✏️ 1. 账号名称',
description:
'<div style="line-height: 1.7;"><p style="margin-bottom: 12px;">为账号设置一个便于识别的名称。</p><p style="padding: 8px 12px; background: #f0fdf4; border-left: 3px solid #10b981; border-radius: 4px; font-size: 13px;"><b>💡 命名建议:</b>"Claude主账号"、"GPT备用1"、"测试账号" 等</p></div>',
nextBtn: '下一步'
},
accountPlatform: {
title: '🤖 2. 选择平台',
description:
'<div style="line-height: 1.7;"><p style="margin-bottom: 12px;">选择该账号对应的服务商平台。</p><p style="padding: 8px 12px; background: #fef3c7; border-left: 3px solid #f59e0b; border-radius: 4px; font-size: 13px;"><b>⚠️ 重要:</b>平台必须与刚才创建的分组平台一致</p></div>',
nextBtn: '下一步'
},
accountType: {
title: '🔐 3. 授权方式',
description:
'<div style="line-height: 1.7;"><p style="margin-bottom: 12px;">选择账号的授权方式。</p><div style="padding: 8px 12px; background: #f0fdf4; border-left: 3px solid #10b981; border-radius: 4px; font-size: 13px; margin-bottom: 12px;"><b>✅ 推荐:OAuth 方式</b><ul style="margin: 8px 0 0 16px;"><li>无需手动提取密钥</li><li>更安全,支持自动刷新</li><li>适用于 Claude Code、ChatGPT OAuth</li></ul></div><div style="padding: 8px 12px; background: #eff6ff; border-left: 3px solid #3b82f6; border-radius: 4px; font-size: 13px;"><b>📌 Session Key 方式</b><ul style="margin: 8px 0 0 16px;"><li>需要手动从浏览器提取</li><li>可能需要定期更新</li><li>适用于不支持 OAuth 的平台</li></ul></div></div>',
nextBtn: '下一步'
},
accountPriority: {
title: '⚖️ 4. 优先级(可选)',
description:
'<div style="line-height: 1.7;"><p style="margin-bottom: 12px;">设置账号的调用优先级。</p><div style="padding: 8px 12px; background: #eff6ff; border-left: 3px solid #3b82f6; border-radius: 4px; font-size: 13px; margin-bottom: 12px;"><b>📊 优先级规则:</b><ul style="margin: 8px 0 0 16px;"><li>数字越小,优先级越高</li><li>系统优先使用低数值账号</li><li>相同优先级则随机选择</li></ul></div><p style="padding: 8px 12px; background: #f0fdf4; border-left: 3px solid #10b981; border-radius: 4px; font-size: 13px;"><b>💡 使用场景:</b>主账号设置低数值,备用账号设置高数值</p></div>',
nextBtn: '下一步'
},
accountGroups: {
title: '🎯 5. 分配分组',
description:
'<div style="line-height: 1.7;"><p style="margin-bottom: 12px;"><b>关键步骤!</b>将账号分配到刚才创建的分组。</p><div style="padding: 8px 12px; background: #fee2e2; border-left: 3px solid #ef4444; border-radius: 4px; font-size: 13px; margin-bottom: 12px;"><b>⚠️ 重要提醒:</b><ul style="margin: 8px 0 0 16px;"><li>必须勾选至少一个分组</li><li>未分配分组的账号无法使用</li><li>一个账号可以分配给多个分组</li></ul></div><p style="padding: 8px 12px; background: #f0fdf4; border-left: 3px solid #10b981; border-radius: 4px; font-size: 13px;"><b>💡 提示:</b>请勾选刚才创建的测试分组</p></div>',
nextBtn: '下一步'
},
accountSubmit: {
title: '✅ 保存账号',
description:
'<div style="line-height: 1.7;"><p style="margin-bottom: 12px;">确认信息无误后,点击保存按钮。</p><div style="padding: 8px 12px; background: #eff6ff; border-left: 3px solid #3b82f6; border-radius: 4px; font-size: 13px; margin-bottom: 12px;"><b>📌 OAuth 授权流程:</b><ul style="margin: 8px 0 0 16px;"><li>点击保存后会跳转到服务商页面</li><li>在服务商页面完成登录授权</li><li>授权成功后自动返回</li></ul></div><p style="padding: 8px 12px; background: #f0fdf4; border-left: 3px solid #10b981; border-radius: 4px; font-size: 13px;"><b>📌 下一步:</b>账号添加成功后,我们将创建 API 密钥</p><p style="margin-top: 12px; color: #10b981; font-weight: 600;">👉 点击"保存"按钮</p></div>'
},
keyManage: {
title: '🔑 第三步:生成密钥',
description:
'<div style="line-height: 1.7;"><p style="margin-bottom: 12px;"><b>恭喜!账号配置完成 🎉</b></p><p style="margin-bottom: 12px;">最后一步,生成 API Key 来测试服务是否正常工作。</p><div style="padding: 8px 12px; background: #eff6ff; border-left: 3px solid #3b82f6; border-radius: 4px; font-size: 13px; margin-bottom: 12px;"><b>🔑 API Key 的作用:</b><ul style="margin: 8px 0 0 16px;"><li>用于调用 AI 服务的凭证</li><li>每个 Key 绑定一个分组</li><li>可以设置配额和有效期</li><li>支持独立的使用统计</li></ul></div><p style="margin-top: 16px; color: #10b981; font-weight: 600;">👉 点击左侧的"API 密钥"</p></div>'
},
createKey: {
title: '➕ 创建密钥',
description:
'<div style="line-height: 1.7;"><p style="margin-bottom: 12px;">点击按钮创建您的第一个 API Key。</p><p style="padding: 8px 12px; background: #f0fdf4; border-left: 3px solid #10b981; border-radius: 4px; font-size: 13px;"><b>💡 提示:</b>创建后请立即复制保存,密钥只显示一次</p><p style="margin-top: 12px; color: #10b981; font-weight: 600;">👉 点击"创建密钥"按钮</p></div>'
},
keyName: {
title: '✏️ 1. 密钥名称',
description:
'<div style="line-height: 1.7;"><p style="margin-bottom: 12px;">为密钥设置一个便于管理的名称。</p><p style="padding: 8px 12px; background: #f0fdf4; border-left: 3px solid #10b981; border-radius: 4px; font-size: 13px;"><b>💡 命名建议:</b>"测试密钥"、"生产环境"、"移动端" 等</p></div>',
nextBtn: '下一步'
},
keyGroup: {
title: '🎯 2. 选择分组',
description:
'<div style="line-height: 1.7;"><p style="margin-bottom: 12px;">选择刚才配置好的分组。</p><div style="padding: 8px 12px; background: #eff6ff; border-left: 3px solid #3b82f6; border-radius: 4px; font-size: 13px; margin-bottom: 12px;"><b>📌 分组决定:</b><ul style="margin: 8px 0 0 16px;"><li>该密钥可以使用哪些账号</li><li>计费倍率是多少</li><li>是否为专属密钥</li></ul></div><p style="padding: 8px 12px; background: #f0fdf4; border-left: 3px solid #10b981; border-radius: 4px; font-size: 13px;"><b>💡 提示:</b>选择刚才创建的测试分组</p></div>',
nextBtn: '下一步'
},
keySubmit: {
title: '🎉 生成并复制',
description:
'<div style="line-height: 1.7;"><p style="margin-bottom: 12px;">点击创建后,系统会生成完整的 API Key。</p><div style="padding: 8px 12px; background: #fee2e2; border-left: 3px solid #ef4444; border-radius: 4px; font-size: 13px; margin-bottom: 12px;"><b>⚠️ 重要提醒:</b><ul style="margin: 8px 0 0 16px;"><li>密钥只显示一次,请立即复制</li><li>丢失后需要重新生成</li><li>妥善保管,不要泄露给他人</li></ul></div><div style="padding: 8px 12px; background: #f0fdf4; border-left: 3px solid #10b981; border-radius: 4px; font-size: 13px; margin-bottom: 12px;"><b>🚀 下一步:</b><ul style="margin: 8px 0 0 16px;"><li>复制生成的 sk-xxx 密钥</li><li>在支持 OpenAI 接口的客户端中使用</li><li>开始体验 AI 服务!</li></ul></div><p style="margin-top: 12px; color: #10b981; font-weight: 600;">👉 点击"创建"按钮</p></div>'
}
},
// User tour steps
user: {
welcome: {
title: '👋 欢迎使用 Sub2API',
description:
'<div style="line-height: 1.8;"><p style="margin-bottom: 16px;">您好!欢迎来到 Sub2API AI 服务平台。</p><p style="margin-bottom: 12px;"><b>🎯 快速开始:</b></p><ul style="margin-left: 20px; margin-bottom: 16px;"><li>🔑 创建 API 密钥</li><li>📋 复制密钥到您的应用</li><li>🚀 开始使用 AI 服务</li></ul><p style="color: #10b981; font-weight: 600;">只需 1 分钟,让我们开始吧 →</p></div>',
nextBtn: '开始 🚀',
prevBtn: '跳过'
},
keyManage: {
title: '🔑 API 密钥管理',
description:
'<div style="line-height: 1.7;"><p style="margin-bottom: 12px;">在这里管理您的所有 API 访问密钥。</p><p style="padding: 8px 12px; background: #eff6ff; border-left: 3px solid #3b82f6; border-radius: 4px; font-size: 13px;"><b>📌 什么是 API 密钥?</b><br/>API 密钥是您访问 AI 服务的凭证,就像一把钥匙,让您的应用能够调用 AI 能力。</p><p style="margin-top: 12px; color: #10b981; font-weight: 600;">👉 点击进入密钥页面</p></div>'
},
createKey: {
title: '➕ 创建新密钥',
description:
'<div style="line-height: 1.7;"><p style="margin-bottom: 12px;">点击按钮创建您的第一个 API 密钥。</p><p style="padding: 8px 12px; background: #f0fdf4; border-left: 3px solid #10b981; border-radius: 4px; font-size: 13px;"><b>💡 提示:</b>创建后密钥只显示一次,请务必复制保存</p><p style="margin-top: 12px; color: #10b981; font-weight: 600;">👉 点击"创建密钥"</p></div>'
},
keyName: {
title: '✏️ 密钥名称',
description:
'<div style="line-height: 1.7;"><p style="margin-bottom: 12px;">为密钥起一个便于识别的名称。</p><p style="padding: 8px 12px; background: #f0fdf4; border-left: 3px solid #10b981; border-radius: 4px; font-size: 13px;"><b>💡 示例:</b>"我的第一个密钥"、"测试用" 等</p></div>',
nextBtn: '下一步'
},
keyGroup: {
title: '🎯 选择分组',
description:
'<div style="line-height: 1.7;"><p style="margin-bottom: 12px;">选择管理员为您分配的服务分组。</p><p style="padding: 8px 12px; background: #eff6ff; border-left: 3px solid #3b82f6; border-radius: 4px; font-size: 13px;"><b>📌 分组说明:</b><br/>不同分组可能有不同的服务质量和计费标准,请根据需要选择。</p></div>',
nextBtn: '下一步'
},
keySubmit: {
title: '🎉 完成创建',
description:
'<div style="line-height: 1.7;"><p style="margin-bottom: 12px;">点击确认创建您的 API 密钥。</p><div style="padding: 8px 12px; background: #fee2e2; border-left: 3px solid #ef4444; border-radius: 4px; font-size: 13px; margin-bottom: 12px;"><b>⚠️ 重要:</b><ul style="margin: 8px 0 0 16px;"><li>创建后请立即复制密钥(sk-xxx)</li><li>密钥只显示一次,丢失需重新生成</li></ul></div><p style="padding: 8px 12px; background: #f0fdf4; border-left: 3px solid #10b981; border-radius: 4px; font-size: 13px;"><b>🚀 如何使用:</b><br/>将密钥配置到支持 OpenAI 接口的任何客户端(如 ChatBox、OpenCat 等),即可开始使用!</p><p style="margin-top: 12px; color: #10b981; font-weight: 600;">👉 点击"创建"按钮</p></div>'
}
}
},
// 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: '已撤销',
},
},
},
}