export const siteConfig = { pageTitle: 'Site Configuration', pageDescription: 'Manage global site properties, branding, request rules, and security policies', reset: 'Reset', saving: 'Saving...', saved: 'Saved', saveConfig: 'Save Config', loading: 'Loading configuration...', basicInfo: 'Basic Info', siteTitle: 'Site Title', siteTitlePlaceholder: 'Enter the site title', icpNumber: 'ICP Filing Number', icpPlaceholder: 'Enter the ICP filing number', gonganNumber: 'Public Security Filing Number', gonganPlaceholder: 'Enter the public security filing number', showBeianIcon: 'Show Filing Icon', showBeianIconDesc: 'Show the filing icon before the public security filing number', siteDescription: 'Site Description', siteDescriptionPlaceholder: 'Enter the site description', statisticsCode: 'Site Statistics Code', statisticsCodeEnabled: 'Enable Site Statistics Code', statisticsCodeEnabledDesc: 'Only injects the snippet below into pages when enabled', statisticsCodePlaceholder: 'Paste your statistics snippet (e.g. Baidu Analytics, 51.la, Clarity, Google Analytics)', statisticsCodeHint: 'Injected into the of every page. Supports any HTML/JS statistics code. Takes effect within about 1 minute after saving.', themeSettings: 'Theme Settings', themeSettingsDesc: 'Control each theme separately and use the star to choose the default theme.', setDefaultTheme: 'Set as default theme', enableTheme: 'Enable theme', disableTheme: 'Disable theme', systemThemeHint: 'Requires both Classic Dark and Classic Light to be enabled', systemThemeAutoEnabled: 'Follow System requires both classic themes; they have been enabled automatically.', defaultTheme: 'Default Theme', defaultThemeDesc: 'Used when a user has not selected a theme; user preferences take priority.', enabledThemes: 'Enabled Themes', enabledThemesDesc: 'Disabled themes cannot be selected by users; keep at least one enabled.', defaultThemeMustBeEnabled: 'The default theme must be enabled', systemThemeRequiresBothClassic: 'Classic Dark and Classic Light must both be enabled when Follow System is enabled', visualIdentity: 'Visual Identity', siteLogoUrl: 'Site Logo URL', siteLogoPlaceholder: 'Enter the logo image URL', schoolLogoHome: 'Home School Logo URL (Dark)', schoolLogoHomePlaceholder: 'Enter the dark-mode school logo URL first', schoolLogoHomeLight: 'Home School Logo URL (Light)', schoolLogoHomeLightPlaceholder: 'Enter the light-mode school logo URL', schoolLogoPrint: 'Print Schedule Logo URL (Small)', schoolLogoPrintPlaceholder: 'Enter the print page school logo URL', submissionLogic: 'Request Rules', enableCollaborative: 'Enable Collaboration', enableCollaborativeDesc: 'Allow users to add collaborators and submit requests together', enableRemarks: 'Enable Request Remarks', enableRemarksDesc: 'Allow users to add public or admin-only notes when submitting requests', submissionNoteRequiresApproval: 'Require Approval for Public Notes', submissionNoteRequiresApprovalDesc: 'When enabled, public notes submitted with song requests must be approved in Song Management before being shown to other users', enableCardCodeRequests: 'Enable Request Cards', enableCardCodeLimitBypass: 'Request cards bypass limits', enableCardCodeLimitBypassDesc: 'Valid request-card submissions do not use regular quota and remain available after daily, weekly, or monthly limits are reached.', enableCardCodeRequestsDesc: 'Allow users to use request cards when submitting song requests', requireCardCodeForRequests: 'Require Request Cards', requireCardCodeForRequestsDesc: 'When enabled, all users must provide a valid request card before submitting.', enableReplay: 'Enable Replay Requests', enableReplayDesc: 'Allow users to request songs that have already been played this semester', enableLimit: 'Enable Request Limits', enableLimitDesc: 'Limit how frequently a single user can submit song requests', dailyLimit: 'Daily Limit', weeklyLimit: 'Weekly Limit', monthlyLimit: 'Monthly Limit', limitUnit: 'songs / user', dailyLimitLabel: 'Daily', weeklyLimitLabel: 'Weekly', monthlyLimitLabel: 'Monthly', limitLabelPrefix: '', limitLabelSuffix: ' Request Limit', // Schedule visibility range scheduleVisibility: 'Schedule Visibility', scheduleVisibilityDesc: 'Controls the broadcast schedule range visible to regular users on the frontend. Administrators are not affected.', daysBeforeEnabled: 'Enable Past Schedule Limit', daysBeforeEnabledDesc: 'When enabled, users can only view schedules within the configured past days.', daysAfterEnabled: 'Enable Future Schedule Limit', daysAfterEnabledDesc: 'When enabled, users can only view schedules within the configured future days.', daysBefore: 'Visible Past Days', daysAfter: 'Visible Future Days', daysUnit: 'days', securityPrivacy: 'Security and Privacy', captchaEnabled: 'Enable Login CAPTCHA', captchaEnabledDesc: 'Helps prevent brute-force attempts and bot registration.', captchaType: 'CAPTCHA Type', captchaGraphic: 'Image CAPTCHA', captchaTurnstile: 'Cloudflare Turnstile', captchaMaxFailures: 'Failure Threshold', captchaMaxFailuresPlaceholder: 'e.g. 3', captchaMaxFailuresDesc: 'Require CAPTCHA after this many consecutive password failures. Recommended: 3-5.', turnstileSiteKey: 'Site Key', turnstileSiteKeyPlaceholder: 'Enter the Turnstile Site Key', turnstileSecretKey: 'Secret Key', turnstileSecretKeyPlaceholder: 'Enter the Turnstile Secret Key (leave blank to keep unchanged)', turnstileSecretKeyDesc: 'When Turnstile is enabled, all users must pass verification on each login.', allowRegister: 'Allow User Registration', allowRegisterDesc: 'When enabled, a registration entry appears on the login page so users can sign up with a username and password.', registerRequiresApproval: 'Require Admin Approval for Registration', registerRequiresApprovalDesc: 'When enabled, new registrations must be approved by an admin before they can sign in.', oauthRegisterRequiresApproval: 'Require Admin Approval for Third-party Registration', oauthRegisterRequiresApprovalDesc: 'When enabled, accounts created via third-party login (GitHub, aggregate login, etc.) also require admin approval before sign-in.', registerEmailRequired: 'Require Email on Registration', registerEmailRequiredDesc: 'When enabled, the registration form requires an email address verified by a code. SMTP must be configured first; the toggle is disabled otherwise.', registerEmailSmtpRequired: 'Configure the SMTP service in Email Notification settings before enabling this option', registerRequiresGradeClass: 'Require Grade and Class on Registration', registerRequiresGradeClassDesc: 'When enabled, the registration form (including third-party account creation) requires selecting an existing grade and class. Maintain options in Grade/Class management first.', showBlacklistKeywords: 'Show Exact Keyword', showBlacklistKeywordsDesc: 'Show the exact conflicting keyword when a request hits the blacklist; otherwise only show a generic keyword warning.', forcePasswordChangeOnFirstLogin: 'Force Password Change on First Login', forcePasswordChangeOnFirstLoginDesc: 'When enabled, new users who have never set a password must set one on first login and cannot access other features until done.', hideStudentInfo: 'Hide Student Details', hideStudentInfoDesc: 'Hide full student IDs and real names from non-admin users in request lists and schedule previews.', telemetryEnabled: 'Enable Error Tracking', telemetryEnabledDesc: 'Enabled by default. When on, the system sends frontend/backend error events and instance-online events to Sentry for instance statistics and deployment diagnostics.', telemetryPrivacy: 'No personal identity information, user data, or business content is collected.', configWarning: 'Site configuration takes effect immediately after saving. Review the impact before changing key business rules such as request limits.', submissionGuidelines: 'Submission Guidelines', guidelinesPlaceholder: 'Enter submission guidelines (Markdown supported)', guidelinesEdit: 'Edit', guidelinesPreview: 'Preview', defaultSiteTitle: 'Campus Radio Request System', defaultSubmissionGuidelines: `1. Do not include book-title brackets when submitting. 2. Songs of all genres except DJ tracks are accepted, including songs in minority languages. 3. Songs containing prohibited content are not allowed. 4. Requested songs are reviewed by administrators. 5. Approved songs will be scheduled during broadcast periods. 6. Submitting means you have read the guidelines and understand the song may not be played. 7. This system only provides music search and playback management. It does not store music files. All music comes from third-party platforms and copyright belongs to the original platforms and rights holders. Please follow the relevant platform terms and respect copyrights. We encourage supporting official music sources. 8. The radio station reserves the final right of interpretation.`, loadFailed: 'Failed to load configuration', saveFailed: 'Failed to save configuration', saveFailedRetry: 'Failed to save configuration. Please try again.', saveSuccess: 'Configuration saved successfully.', fetchFailed: 'Failed to fetch configuration', logs: { loadFailed: 'Failed to load configuration:', apiErrorResponse: 'API error response:', parseApiErrorFailed: 'Failed to parse API error response:', saveFailed: 'Failed to save configuration:' }, // Platform names platformNetease: 'NetEase Cloud Music', platformTencent: 'QQ Music', platformBilibili: 'Bilibili', platformMigu: 'Migu Music', platformEnabled: 'Enabled', platformDisabled: 'Disabled', serverMessages: { oauthRedirectCallbackInvalid: 'oauthRedirectUri must be a callback URL, for example https://yourdomain.com/api/auth/[provider]/callback', oauthRedirectUrlInvalid: 'oauthRedirectUri is not a valid URL, for example https://yourdomain.com/api/auth/[provider]/callback', raw: { oauthRedirectCallbackInvalid: 'oauthRedirectUri 必须是回调地址,例如 https://yourdomain.com/api/auth/[provider]/callback', oauthRedirectUrlInvalid: 'oauthRedirectUri 不是合法URL,示例:https://yourdomain.com/api/auth/[provider]/callback', unauthorized: '未授权访问', adminOnly: '只有管理员才能更新系统设置', captchaProviderInvalid: 'captchaProvider 必须是 graphic 或 turnstile', turnstileRequired: '启用 Turnstile 验证前,请先配置 Site Key 和 Secret Key', smtpPortInvalid: 'smtpPort 必须是1-65535之间的整数', oauthBaseRequired: '启用 OAuth 登录方式前,请先在管理员后台配置 OAuth 重定向 URI 和 OAuth State 密钥', githubClientIdRequired: '启用 GitHub 登录时必须提供 Client ID', githubClientSecretRequired: '启用 GitHub 登录时必须提供 Client Secret', casdoorServerUrlRequired: '启用 Casdoor 登录时必须提供服务器 URL', casdoorClientIdRequired: '启用 Casdoor 登录时必须提供 Client ID', casdoorClientSecretRequired: '启用 Casdoor 登录时必须提供 Client Secret', casdoorOrganizationRequired: '启用 Casdoor 登录时必须提供组织名称', googleClientIdRequired: '启用 Google 登录时必须提供 Client ID', googleClientSecretRequired: '启用 Google 登录时必须提供 Client Secret', onlyOneLimit: '每日限额、每周限额和每月限额只能选择其中一种,其他必须设置为空', updateFailed: '更新系统设置失败', booleanSuffix: ' 必须是布尔值', positiveIntegerSuffix: ' 必须是正整数', nonNegativeIntegerOrNullSuffix: ' 必须是非负整数或null', customOAuthRequiredPrefix: '启用自定义 OAuth2 登录时必须提供 ', invalidUrlSuffix: ' 不是合法URL', customOAuthAuthorizeUrlLabel: '授权端点 URL', customOAuthTokenUrlLabel: 'Token 端点 URL', customOAuthUserInfoUrlLabel: '用户信息端点 URL', customOAuthClientIdLabel: 'Client ID', customOAuthClientSecretLabel: 'Client Secret', customOAuthUserIdFieldLabel: '用户 ID 字段名' }, unauthorized: 'Unauthorized access', adminOnly: 'Only administrators can update system settings', mustBeBoolean: (field: string) => `${field} must be a boolean`, mustBePositiveInteger: (field: string) => `${field} must be a positive integer`, mustBeNonNegativeIntegerOrNull: (field: string) => `${field} must be a non-negative integer or null`, captchaProviderInvalid: 'captchaProvider must be graphic or turnstile', turnstileRequired: 'Configure the Site Key and Secret Key before enabling Turnstile verification', smtpPortInvalid: 'smtpPort must be an integer between 1 and 65535', oauthBaseRequired: 'Configure the OAuth redirect URI and OAuth state secret in the admin panel before enabling OAuth login', githubClientIdRequired: 'Client ID is required when enabling GitHub login', githubClientSecretRequired: 'Client Secret is required when enabling GitHub login', casdoorServerUrlRequired: 'Server URL is required when enabling Casdoor login', casdoorClientIdRequired: 'Client ID is required when enabling Casdoor login', casdoorClientSecretRequired: 'Client Secret is required when enabling Casdoor login', casdoorOrganizationRequired: 'Organization name is required when enabling Casdoor login', googleClientIdRequired: 'Client ID is required when enabling Google login', googleClientSecretRequired: 'Client Secret is required when enabling Google login', customOAuthFieldRequired: (field: string) => `${field} is required when enabling custom OAuth2 login`, invalidUrl: (field: string) => `${field} is not a valid URL`, onlyOneLimit: 'Only one of daily, weekly, and monthly limits can be selected; the others must be empty', updateFailed: 'Failed to update system settings', fields: { customOAuthAuthorizeUrl: 'Authorization Endpoint URL', customOAuthTokenUrl: 'Token Endpoint URL', customOAuthUserInfoUrl: 'User Info Endpoint URL', customOAuthClientId: 'Client ID', customOAuthClientSecret: 'Client Secret', customOAuthUserIdField: 'User ID Field Name' } } } as const export const changePassword = { welcomeTitle: 'Welcome to VoiceHub', welcomeDesc: 'Set a new password to keep your account secure', securityTitle: 'Account Security', securityDesc: 'Updating your password regularly helps protect your account', tipsTitle: 'Password Tips', tipMinLength: 'At least 6 characters', tipCase: 'Use uppercase and lowercase letters', tipSpecial: 'Include numbers and special characters', setNewPassword: 'Set New Password', changePasswordTitle: 'Change Password', setNewPasswordDesc: 'Create a secure password', updatePasswordDesc: 'Update your login password', backToHome: 'Back to Home', logout: 'Log Out' } as const export const common = { language: 'Language', followSystem: 'Follow System', workerIn: 'Worker in', cancel: 'Cancel', confirm: 'Confirm', close: 'Close', retry: 'Retry', edit: 'Edit', delete: 'Delete', actions: 'Actions', refreshData: 'Refresh Data', selectedItems: (count: number) => `${count} selected`, noData: 'No data', loadingData: 'Loading data...', searchPlaceholder: 'Search...', selectPlaceholder: 'Select', to: 'to', clearFilters: 'Clear Filters', selectedCount: (count: number) => `${count} selected`, processing: 'Processing...', confirmOperation: 'Confirm Action', passwordStrengthLevels: { weak: 'Weak', medium: 'Medium', strong: 'Strong', veryStrong: 'Very Strong' }, searchUsers: 'Search Users', searchUsersPlaceholder: 'Search username or name...', noMatchingUsers: 'No matching users found', selectedUsers: (count: number) => `${count} selected`, time: { justNow: 'Just now', minutesAgo: (count: number) => `${count} min ago`, hoursAgo: (count: number) => `${count} hr ago`, daysAgo: (count: number) => `${count} days ago` }, scheduleForm: { title: (title: string) => `Create schedule for "${title}"`, playDate: 'Play Date', playTime: 'Play Time', unspecified: 'Unspecified', preferredPlayTime: 'Preferred play time:', creating: 'Creating...', create: 'Create Schedule', startAt: (time: string) => `${time} start`, endAt: (time: string) => `${time} end`, unlimited: 'Unlimited', dateRequired: 'Please select a play date' }, votersModal: { title: 'Voters', votes: (count: number) => `${count} votes`, loading: 'Loading voters...', voters: (count: number) => `Voters (${count})`, empty: 'No votes yet', fetchFailed: 'Failed to fetch voters' }, userSongsModal: { title: 'User Song Information', loading: 'Loading data...', allSemesters: 'All Semesters', semesterFilter: 'Semester Filter', empty: 'No matching records', votes: (count: number) => `${count} votes`, requests: (count: number) => `${count} requests`, requester: 'Requester:', tabs: { submitted: 'Submitted Songs', voted: 'Voted Songs', replay: 'Replay Requests' }, fetchFailed: 'Failed to fetch user song information', voted: 'voted', requested: 'requested', statuses: { played: 'Played', scheduled: 'Scheduled', pending: 'Pending' } } } as const export const pages = { authError: { title: 'Authentication Failed', errorCode: 'Error Code', retryLogin: 'Try Again', backHome: 'Back to Home', contactAdmin: 'If you believe this is an error, please contact the system administrator', defaultMessage: 'An unknown error occurred while signing in with a third-party account. Please try again.', aggregateUnavailableTitle: 'This sign-in method is currently unavailable', aggregateUnavailableAction: 'Choose Another Method' }, resetPassword: { title: 'Reset Password', subtitle: 'Set your new password', goLogin: 'Go to Login', invalidTokenFull: 'The reset link is invalid or missing a token. Please request a new email.', invalidToken: 'Invalid reset link', newPassword: 'New Password', passwordPlaceholder: 'At least 6 characters', passwordStrength: 'Password Strength', confirmPassword: 'Confirm New Password', confirmPasswordPlaceholder: 'Enter the new password again', resetting: 'Resetting...', submit: 'Reset Password', backLogin: 'Back to Login', passwordMismatch: 'The two passwords do not match', passwordTooShort: 'Password must be at least 6 characters', success: 'Password reset successfully', failed: 'Reset failed. Please try again.' }, forgotPassword: { title: 'Forgot Password', subtitle: 'Enter your username and bound email address', backLogin: 'Back to Login', username: 'Username', usernamePlaceholder: 'Enter your username', verifyEmail: 'Verify Email', detectedEmailPrefix: 'The system found a bound email for this account: ', emailPlaceholder: 'Complete the full email address', verifying: 'Verifying...', sending: 'Sending...', nextStep: 'Next', sendResetEmail: 'Send Reset Email', previousStep: 'Previous', usernameRequired: 'Please enter your username', emailRequired: 'Please complete the email address', mailSent: 'Reset email sent', requestFailed: 'Request failed. Please try again later.', serverMessages: { raw: { rateLimitedPattern: '操作过于频繁,请等待\\s*(\\d+)\\s*分钟后再试', usernameMissing: '请提供账号名', userNotFound: '找不到该账号名,请检查是否拼写错误', emailNotBound: '该账号未绑定安全邮箱,无法通过此方式找回密码。请联系管理员。', emailRequiredFull: '请输入完整的邮箱地址以验证身份', mailSentFull: '如果账号名和邮箱匹配,重置密码链接已发送至您的邮箱。请查收并按照邮件中的说明重置密码。', systemError: '系统错误,请稍后重试' }, rateLimited: (minutes: string | number) => `Too many requests. Please wait ${minutes} minute(s) and try again.`, usernameMissing: 'Please provide your username', userNotFound: 'No account was found for that username. Please check the spelling.', emailNotBound: 'This account has no security email bound, so password recovery is unavailable. Please contact an administrator.', emailRequiredFull: 'Please enter the full email address to verify your identity', mailSentFull: 'If the username and email match, a password reset link has been sent to your inbox. Please follow the email instructions to reset your password.', systemError: 'System error. Please try again later.' } }, account: { title: 'Account Management', subtitle: 'Account Management', oauthBinding: 'Third-party Account Binding', oauthBindingDesc: 'Bind social accounts for faster sign-in', changePassword: 'Change Password', changePasswordDesc: 'For account security, update your password regularly with a strong one', sessions: { title: 'Login Sessions', desc: 'View and sign out devices currently using your account.', refresh: 'Refresh', loading: 'Loading sessions...', empty: 'No active login sessions', current: 'Current', ip: 'IP', loginMethod: 'Method', lastActive: 'Last active', expiresAt: 'Expires', logout: 'Sign out', logoutOthers: 'Sign out other sessions', logoutSuccess: 'Session signed out', logoutOthersSuccess: 'Other sessions signed out', logoutFailed: 'Failed to sign out session', loadFailed: 'Failed to load sessions', methods: { password: 'Password', twoFactor: '2FA', oauth: 'OAuth', webauthn: 'Passkey', legacy: 'Legacy' } }, social: { title: 'Social Account Binding', desc: 'Bind social accounts to receive real-time push notifications', loading: 'Loading binding status...', emailNotifyTitle: 'Email Notifications', currentEmail: 'Current Bound Email', verified: 'Verified', pendingVerify: 'Pending', emailUnboundDesc: 'After binding an email, you can receive real-time email notifications.', emailPlaceholder: 'Enter email address', pleaseWait: 'Please wait...', bindNow: 'Bind Now', emailCodeSentTip: 'A verification code has been sent to your email. Complete verification within 5 minutes. Check spam if you did not receive it.', emailCodePlaceholder: 'Enter 6-digit code', verifying: 'Verifying...', confirmVerify: 'Verify', sending: 'Sending...', resend: 'Resend', changeEmailAddress: 'Change Email Address', changeEmail: 'Change Email', unbinding: 'Unbinding...', unbindEmail: 'Unbind Email', meowPushTitle: 'MeoW App Push', currentBoundId: 'Current Bound ID', connected: 'Connected', meowDesc: 'Receive faster system notifications and song status updates through the MeoW client.', meowIdPlaceholder: 'Enter MeoW User ID', sendCode: 'Send Code', meowCodeSentPrefix: 'Verification code sent to MeoW ID:', meowCodeSentSuffix: '. Please check it in the client.', codePlaceholder: 'Enter 6-digit code', confirmBind: 'Bind', cancel: 'Cancel', unbindMeow: 'Unbind MeoW Account', loadFailedLog: 'Failed to load settings:', loadFailed: 'Failed to load settings. Please refresh and try again.', meowIdRequired: 'Please enter your MeoW User ID', meowCodeSent: 'Verification code sent to your MeoW account', sendCodeFailed: 'Failed to send verification code', codeRequired: 'Please enter the 6-digit verification code', meowBindSuccess: 'MeoW account bound successfully.', verifyFailed: 'Verification failed', unbindMeowTitle: 'Unbind MeoW Account', unbindMeowMessage: 'Are you sure you want to unbind your MeoW account? You will no longer receive push notifications.', meowUnbound: 'MeoW account unbound', unbindFailed: 'Failed to unbind', emailRequired: 'Please enter an email address', emailInvalid: 'Please enter a valid email address', emailCodeSent: 'Verification code sent. Please check your email.', bindFailed: 'Binding failed', emailVerifySuccess: 'Email verified successfully', changeEmailTitle: 'Change Email', changeEmailMessage: 'Changing email will clear the currently bound email and require verification again. Continue?', emailCleared: 'Email information cleared', emailCodeResent: 'Verification code resent', sendFailed: 'Failed to send', unbindEmailTitle: 'Confirm Email Unbind', unbindEmailMessage: 'You will no longer receive email notifications after unbinding. Continue?', emailUnbound: 'Email unbound' }, personalApiKey: { title: 'Personal API Key', desc: 'For personal integrations and song requests', create: 'Create API Key', loading: 'Loading API keys...', emptyTitle: 'No personal API keys yet', emptyDesc: 'Create one to use personal integrations and song request submissions.', noDescription: 'No description', delete: 'Delete', cancel: 'Cancel', keyPrefix: 'Key Prefix', createdAt: 'Created At', lastUsedAt: 'Last Used', neverUsed: 'Never used', usageCount: 'Calls', expiresAt: 'Expires At', neverExpires: 'Never expires', deleteTitle: 'Delete Personal API Key', deleteMessageDefault: 'Delete this personal API key? Related integrations will stop working.', deleteMessage: (name: string) => `Delete personal API key "${name}"? Related integrations will stop working.`, createdTitle: 'API Key Created', createdDesc: 'The full key is shown only once', copyWarning: 'Copy and save it now. VoiceHub will not show the full key again after this window is closed.', fullKey: 'Full Key', closeSaved: 'Saved, close', logsTitle: 'Call Logs', logsSubtitle: (name: string, total: number) => `${name} · ${total} records`, defaultName: 'Personal API Key', defaultDescription: 'For personal integrations and song requests', loadingLogs: 'Loading call logs...', noLogs: 'No call logs', noLogsDesc: 'This token has not made any API calls yet.', logColumns: { time: 'Time', method: 'Method', endpoint: 'Endpoint', status: 'Status', duration: 'Duration', error: 'Error' }, none: 'None', pageInfo: (page: number, totalPages: number) => `Page ${page} / ${totalPages}`, previousPage: 'Previous', nextPage: 'Next', status: { active: 'Active', inactive: 'Inactive', expired: 'Expired' }, loadFailed: 'Failed to load personal API keys', createSuccess: 'Personal API key created', createFailed: 'Failed to create personal API key', deleteSuccess: 'Personal API key deleted', deleteFailed: 'Failed to delete personal API key', copied: 'Copied to clipboard', copyFailed: 'Copy failed. Select the key and copy it manually.', logsFailed: 'Failed to load call logs' }, musicSource: { title: 'Music Source', desc: 'Configure your personal source API key for higher-quality playback', keyLabel: 'ChKSz API Key', keyPlaceholder: 'Paste your ChKSz API key', showKey: 'Show key', hideKey: 'Hide key', save: 'Save', clear: 'Clear', test: 'Test', saveSuccess: 'Source API key saved (this browser only)', clearSuccess: 'Source API key cleared', testSuccess: 'Test passed: key is valid and the source is available', testInvalid: 'Test failed: key is invalid or restricted', testTimeout: 'Test timed out, please try again later', testNetwork: 'Network error: cannot reach the source service', testEmpty: 'Enter an API key first', hint: 'The key is stored in this browser only and never uploaded to the server. Get one at:' }, roles: { ADMIN: 'Administrator', SUPER_ADMIN: 'Super Administrator', SONG_ADMIN: 'Song Reviewer', USER: 'User' } }, dashboard: { backToTop: 'Back to Top', fallbackTitle: 'Admin Dashboard', defaultSiteTitle: 'Campus Radio Song Request System', tabs: { overview: 'Overview', songs: 'Song Management', schedule: 'Schedule Management', print: 'Print Schedule', users: 'User Management', notifications: 'Notification Management', 'smtp-config': 'Email Configuration', playtimes: 'Play Times', 'request-times': 'Request Management', semesters: 'Semester Management', blacklist: 'Blacklist Management', 'site-config': 'Site Configuration', 'music-source': 'Music Source Control', database: 'Database Operations', 'api-keys': 'API Key Management', 'card-codes': 'Card Codes' }, roles: { USER: 'User', SONG_ADMIN: 'Song Administrator', ADMIN: 'Super Administrator', SUPER_ADMIN: 'Super Administrator' } }, notificationSettings: { title: 'Notification Settings', subtitle: 'Notification Settings', saving: 'Saving...', saveSettings: 'Save Settings', loading: 'Loading settings...', loadingShort: 'Loading...', retry: 'Retry', inAppTitle: 'In-app Notifications', inAppDesc: 'Choose which messages you want to receive in the system', songSelectedTitle: 'Song Selected Notifications', songSelectedDesc: 'Notify you when a submitted song is selected for playback', songPlayedTitle: 'Song Played Notifications', songPlayedDesc: 'Notify you when a submitted song has finished playing', songVotedTitle: 'Song Vote Notifications', songVotedDesc: 'Notify you when a submitted song receives votes', systemTitle: 'System Notifications', systemDesc: 'Receive important system notifications and announcements', voteThresholdTitle: 'Vote Notification Threshold', voteThresholdDesc: 'Only send notifications when votes reach this threshold', voteThresholdText: (count: number) => `Notify every ${count} votes`, voteUnit: 'votes', refreshTitle: 'Notification Refresh Interval', refreshDesc: 'Set how often the system checks for new notifications', seconds: (seconds: number) => `${seconds}s`, minutes: (minutes: number) => `${minutes}m`, minutesSeconds: (minutes: number, seconds: number) => `${minutes}m ${seconds}s`, loadFailedLog: 'Failed to load settings:', loadFailed: 'Failed to load settings. Please refresh and try again.', saveSuccess: 'Settings saved successfully', saveFailed: 'Failed to save' }, home: { schoolLogoAlt: 'School Logo', userFallback: 'User', account: 'Account', dashboard: 'Dashboard', logout: 'Log Out', login: 'Log In', subtitle: 'VoiceHub Campus Radio System', tabs: { schedule: 'Schedule', songs: 'Songs', request: 'Request', notification: 'Messages' }, loginRequired: 'Login Required', loginRequiredDesc: 'You need to log in to view notifications', loginNow: 'Log In Now', notificationCenter: 'Notification Center', unreadOverview: 'Unread overview', unreadCountSummary: (count: number) => `${count} unread notification${count === 1 ? '' : 's'}`, viewUnreadNotifications: 'View unread', notificationFilterLabel: 'Notification filter', allNotifications: 'All', searchNotifications: 'Search notifications', searchNotificationsPlaceholder: 'Search notification title or content...', clearNotificationSearch: 'Clear notification search', loading: 'Loading...', noNotifications: 'No notifications', noUnreadNotifications: 'No unread notifications', noMatchingNotifications: 'No notifications contain this keyword', read: 'Read', unread: 'Unread', sender: 'Sender', systemSender: 'System', notificationTypes: { SONG_SELECTED: 'Song Selected', SONG_PLAYED: 'Song Played', SONG_VOTED: 'New Vote', SONG_REJECTED: 'Song Rejected', COLLABORATION_INVITE: 'Collaboration Invite', COLLABORATION_RESPONSE: 'Collaboration Response', SYSTEM_NOTICE: 'System Notification', SYSTEM: 'System Notification' }, inviteStatus: { accepted: '- Accepted', invalid: '- Expired', rejected: '- Rejected' }, processing: 'Processing...', acceptInvite: 'Accept Invite', reject: 'Reject', delete: 'Delete', paginationInfo: (total: number, current: number, pages: number) => `${total} notifications, page ${current} / ${pages}`, pageSize: 'Per page:', previousPage: 'Previous Page', nextPage: 'Next Page', markAllRead: 'Mark All Read', markAllReadSuccess: 'All notifications marked as read', clearAllMessages: 'Clear All', clearAllSuccess: 'All messages cleared', rulesTitle: 'Request Rules', rulesDesc: 'Please read these rules before submitting', submissionGuidelines: 'Submission Guidelines', defaultRules: [ 'Do not include book-title brackets when submitting', 'All song types except DJ tracks are accepted, including minority languages', 'Do not submit songs containing prohibited content' ], playbackTime: 'Playback Time', playbackTimeDesc: 'Before evening self-study quiet time every day', gotIt: 'Got it', roles: { ADMIN: 'Administrator', SUPER_ADMIN: 'Super Administrator', SONG_ADMIN: 'Song Reviewer', USER: 'User' }, defaultRole: 'Administrator', classFallback: 'Student', bootMessages: { START: 'Preparing the music environment', CONFIG: 'Loading site configuration', AUTH: 'Checking login status', SLOW_NETWORK: 'Network is slow; still syncing data', CONTENT: 'Syncing schedules and song lists', FALLBACK: 'Opening home with available data', FINALIZING: 'Finalizing playback data', COMPLETE: 'Loaded. Opening home' }, pageSizeOptions: { five: '5', ten: '10', twenty: '20', fifty: '50' }, confirm: { ok: 'OK', cancel: 'Cancel', deleteTitle: 'Delete Message', deleteMessage: 'Are you sure you want to delete this message?', clearTitle: 'Clear All Messages', clearMessage: 'Are you sure you want to clear all messages? This cannot be undone.', clearConfirm: 'Clear' }, time: { justNow: 'Just now', minutesAgo: (minutes: number) => `${minutes} min ago`, hoursAgo: (hours: number) => `${hours} hr ago`, daysAgo: (days: number) => `${days} days ago`, weekdays: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'], dateFormat: (year: number, month: number, date: number, weekday: string) => `${year}-${String(month).padStart(2, '0')}-${String(date).padStart(2, '0')} ${weekday}`, seconds: (seconds: number) => `${seconds}s`, minutes: (minutes: number) => `${minutes}m`, minutesSeconds: (minutes: number, seconds: number) => `${minutes}m ${seconds}s` }, messages: { requestLogin: 'Please log in to request songs', requestSuccess: (title: string, artist: string) => `"${title} - ${artist}" submitted successfully.`, requestFailed: 'Song request failed', voteLogin: 'Please log in before voting', cancelReplayLogin: 'Please log in to cancel replay requests', requestReplayLogin: 'Please log in to request a replay', withdrawLogin: 'Please log in to withdraw your submission', inviteAccepted: 'Collaboration invite accepted', inviteRejected: 'Collaboration invite rejected', operationFailed: 'Operation failed', notificationLogin: 'Please log in to view notifications', changePasswordTip: 'For account security, please change your password. You can use the change-password entry in the top-right menu.' }, titleHome: 'Home', titleLoading: 'Loading' }, yearReview: { pageTitle: 'My Year in Review - VoiceHub', loading: 'Generating your yearly memories...', errorTitle: 'Failed to load data', errorDesc: 'There may have been a small network hiccup', retry: 'Retry', emptyTitle: 'No song request records this year yet', emptyDesc: 'Go request a song you like', goRequest: 'Request a Song', scrollHint: 'Swipe up to start', reportTitle: 'Annual Song Request Report', firstSongBadge: 'First Encounter', firstSongIntro: 'Your first song request of the year was', date: (month: number, day: number) => `${month}/${day}`, favoriteArtist: 'Your Favorite', topArtistIntro: 'Your artist of the year is', artistDesc: 'Their voice kept drawing you back and stayed with you throughout the year.', future: 'The Best Is Yet to Come', futureDesc: 'May music continue to accompany you every day', backHome: 'Back to Home', commonPlatform: 'Top Platform', mostActive: 'Most Active', activeMonth: (month: number) => `Month ${month}`, votesCast: 'Votes Cast', musicJourney: 'Music Journey', totalRequestsIntro: 'This year, you made', requestCount: 'song requests', playedSummary: (count: number) => `${count} of them`, playedDesc: 'were played by the station and shared with more listeners', pendingPlayedDesc: 'We hope your songs will be played soon', requests: 'Requests', played: 'Played', votes: 'Votes', footerSlogan: 'Remember every moment through music', platforms: { netease: 'NetEase Cloud Music', bilibili: 'Bilibili', qq: 'QQ Music', kugou: 'Kugou Music', kuwo: 'Kuwo Music', migu: 'Migu Music', tencent: 'QQ Music' } }, login: { title: 'Log In', schoolLogoAlt: 'School Logo' }, auth: { loginForm: { bindExisting: 'Bind Account', createAccount: 'Create New Account', bindProvider: (providerName: string, providerUsername: string) => `Binding ${providerName} account: ${providerUsername}`, createWithProvider: (providerName: string) => `Create a new account with ${providerName}`, loginSubtitle: 'Sign in to your VoiceHub account', setUsername: 'Set Username', accountName: 'Username', usernamePlaceholder: 'Enter username', usernamePattern: '3-30 characters; letters, numbers, underscores, and hyphens are allowed', usernameHint: 'Username must be unique and cannot be changed after registration', realName: 'Real Name', realNamePlaceholder: 'Enter your real name', gradeLabel: 'Grade', classLabel: 'Class', optional: 'Optional', selectGrade: 'Select grade', selectClass: 'Select class', selectGradeFirst: 'Select grade first', classHint: 'Optional. Only existing grades and classes can be selected', noClassHint: 'No grade/class options yet. You can skip this.', classHintRequired: 'Required. Only existing grades and classes can be selected', noClassHintRequired: 'No grade/class options yet. Ask an admin to configure them before registering', setPassword: 'Set Password', password: 'Password', forgotPassword: 'Forgot password?', passwordPlaceholder: 'Enter password', createPasswordPlaceholder: 'At least 6 characters', passwordStrength: 'Password Strength', confirmPassword: 'Confirm Password', confirmPasswordPlaceholder: 'Enter password again', welcomeBack: 'Welcome Back', bindAccount: 'Bind Account', createNewAccount: 'Create Account', registerTitle: 'Register', registerSubtitle: 'Create your VoiceHub account', toRegister: 'No account? Register', toLogin: 'Already have an account? Sign in', register: 'Register', registering: 'Registering...', remarkLabel: 'Remark', remarkPlaceholder: 'Optional, e.g. I am from Class 3, Senior 1', emailLabel: 'Email', emailPlaceholder: 'For verification codes and registration notices', emailCodeLabel: 'Email verification code', emailCodePlaceholder: '6-digit code', sendCode: 'Send code', codeSent: 'Verification code sent. Please check your email.', codeSendFailed: 'Failed to send verification code. Please try again later.', codeCountdown: (seconds: number) => `Resend in ${seconds}s`, emailCodeRequired: 'Please request and enter the email verification code', emailInvalid: 'Please enter a valid email address', emailRequired: 'Please enter your email address', registerSuccessPending: 'Registration successful. Awaiting admin approval.', binding: 'Binding...', loggingIn: 'Logging in...', bindAndLogin: 'Bind and Log In', login: 'Log In', or: 'or', webauthn: 'Sign in with Passkey', platformNote: 'Accounts are not shared across different VoiceHub platforms', fullLoginInfo: 'Please complete your login information', fullRegisterInfo: 'Please complete your registration information', gradeClassRequired: 'Grade and class must be selected together, or both left empty', gradeClassRequiredByConfig: 'Grade and class are required to register', bindFailed: 'Binding failed. Please check your username and password.', loginFailed: 'Login failed. Please check your username and password.', registerFailed: 'Registration failed. Please try again later.', passkeyFailed: 'Passkey login failed', passkeyLoginUnsupported: 'This browser does not support Passkey sign-in', passkeyAuthenticationIncomplete: 'Passkey authentication was not completed', passkeySecurityInvalid: 'The Passkey domain or secure-context configuration is invalid', passkeyOperationUnavailable: 'The Passkey operation was cancelled, timed out, or no usable credential was available', passkeyCleanupNotified: 'This Passkey is no longer valid. The system has been notified; please add it again.', passkeyCleanupRequired: 'This Passkey was removed from VoiceHub. Remove it from your device password manager before adding it again.', usernameLengthInvalid: 'Username must be 3-30 characters long', usernamePatternInvalid: 'Username may only contain letters, numbers, underscores, and hyphens', registerPasswordTooShort: 'Password must be at least 6 characters', registerPasswordMismatch: 'The two passwords do not match', confirmBind: 'Confirm Bind', confirmBindTitle: 'Confirm Binding', confirmBindMessage: (providerName: string, providerUsername: string, username: string) => `Bind the ${providerName} account "${providerUsername}" to "${username}"? You can then sign in with ${providerName} directly.` }, oauthButtons: { divider: 'Or sign in with a third-party account', loginWith: (providerName: string) => `Sign in with ${providerName}`, customOAuthProvider: 'Third-Party OAuth', aggregateOAuthProvider: 'Aggregate OAuth', aggregateProviderName: (providerName: string) => `${providerName} Sign-In`, quickLoginWx: 'WeChat Sign-In', quickLoginQq: 'QQ Sign-In', aggregateLoginTypes: { qq: 'QQ', wx: 'WeChat', alipay: 'Alipay', sina: 'Weibo', baidu: 'Baidu', douyin: 'Douyin', huawei: 'Huawei', xiaomi: 'Xiaomi', gitee: 'Gitee', gitea: 'Gitea', bilibili: 'Bilibili', kuaishou: 'Kuaishou' } }, bindReminder: { title: (providerName: string) => `Bind ${providerName} for one-tap sign-in`, description: (providerName: string) => `You are browsing VoiceHub inside ${providerName}, and this account is not linked to ${providerName} yet. Link it now to sign in with one tap next time.`, bindNow: 'Bind Now', ignore: 'Ignore', neverRemind: "Don't Ask Again" }, changePasswordForm: { currentPassword: 'Current Password', currentPasswordPlaceholder: 'Enter current password', newPassword: 'New Password', newPasswordPlaceholder: 'Enter new password', passwordStrength: 'Password Strength', confirmPassword: 'Confirm New Password', confirmPasswordPlaceholder: 'Enter new password again', mismatch: 'Passwords do not match', matched: 'Passwords match', processing: 'Processing...', setInitial: 'Set Initial Password', submitChange: 'Change Password', weak: 'Weak', medium: 'Medium', strong: 'Strong', veryStrong: 'Very Strong', passwordTooShort: 'Password must be at least 6 characters', newPasswordMismatch: 'New password and confirmation do not match', newPasswordTooShort: 'New password must be at least 6 characters', initialSuccess: 'Password set successfully. Redirecting...', changeSuccess: 'Password changed successfully. Please log in again.', failed: 'Operation failed. Please try again.', showCurrentPassword: 'Show current password', hideCurrentPassword: 'Hide current password', showNewPassword: 'Show new password', hideNewPassword: 'Hide new password', showConfirmPassword: 'Show confirm password', hideConfirmPassword: 'Hide confirm password' }, captchaInput: { label: 'Verification Code', refreshTitle: 'Click to refresh verification code', alt: 'Verification code', placeholder: 'Enter verification code', refresh: 'Refresh', loadFailed: 'Failed to load verification code' }, oauthBindingCard: { loading: 'Loading binding status...', unbound: 'Not bound', processing: 'Processing...', redirecting: 'Redirecting...', unbind: 'Unbind', bindNow: 'Bind Now', avatarTitle: 'Avatar', currentAvatar: 'Current', setAsAvatar: 'Set Avatar', avatarSetSuccess: 'Avatar updated', avatarSetFailed: 'Failed to set avatar', boundDevices: 'Bound', devices: 'devices', addDevice: 'Add Device', httpsRequired: 'HTTPS required', rename: 'Rename', addedAt: 'Added at', save: 'Save', cancel: 'Cancel', remove: 'Remove', unbindTitle: 'Unbind Account', unbindMessage: (providerName: string) => `Are you sure you want to unbind ${providerName}? You will no longer be able to sign in with this account.`, removePasskeyTitle: 'Remove Passkey', removePasskeyMessage: (deviceName: string) => `Are you sure you want to remove "${deviceName}"? VoiceHub will try to notify this device to remove it as well. If unsupported, you must remove it manually from your password manager.`, unbindSuccess: 'Unbound successfully', unbindFailed: 'Failed to unbind', passkeyCleanupRequired: 'Removed from VoiceHub. Also remove the corresponding Passkey from your device password manager.', browserNotSupported: 'Your browser does not support Windows Hello / Passkey', userCancelled: 'The operation was cancelled', passkeyOperationUnavailable: 'The Passkey operation was cancelled, timed out, or no usable credential was available', passkeyAlreadyRegistered: 'This Passkey is already registered to the current account', passkeyAlgorithmUnsupported: 'This device does not support the Passkey algorithm provided by the server', passkeySecurityInvalid: 'The Passkey domain or secure-context configuration is invalid', passkeyRegistrationUnsupported: 'This browser does not support creating Passkeys', passkeyCreationIncomplete: 'Passkey creation was not completed', addDeviceSuccess: 'Device added successfully', addDeviceFailed: 'Failed to add device', nameRequired: 'Device name is required', nameTooLong: 'Device name is too long (max 50 characters)', renameSuccess: 'Device name updated', renameFailed: 'Failed to rename' }, twoFactorVerify: { title: 'Two-Factor Authentication', totpDesc: 'Enter the 6-digit code from your authenticator app', emailDesc: 'Enter the code sent to your email', emailLabel: 'Complete your email address to receive the code', maskedEmailTip: 'Hint:', emailPlaceholder: 'Enter full email address', resendCountdown: (seconds: number) => `Resend in ${seconds}s`, resendCode: 'Resend code', sendCode: 'Send code', verifyLogin: 'Verify Login', switchToEmail: 'Use email instead', switchToTotp: 'Use authenticator app', backLogin: 'Back to Login', emailRequired: 'Please enter your email address', codeSent: 'Verification code sent', sendFailed: 'Failed to send', verifyFailed: 'Verification failed' }, twoFactorSetup: { title: 'Two-Factor Authentication (2FA)', enabledDesc: 'Your account is protected by two-factor authentication', disabledDesc: 'Not enabled. We recommend enabling it for better account security', enable: 'Enable', disable: 'Disable', setupTitle: 'Set Up Two-Factor Authentication', setupDesc: 'Scan the QR code below with Google Authenticator or another authenticator app', manualKey: 'Manual Key', copyKey: 'Copy key', code: 'Verification Code', codePlaceholder: 'Enter 6-digit code', verifyAndEnable: 'Verify and Enable', disableTitle: 'Disable Two-Factor Authentication', disableContent: 'After disabling, your account will no longer be protected by two-factor authentication. Enter your login password to confirm.', verifyAndDisable: 'Verify and Disable', passwordPlaceholder: 'Enter current login password', fetchFailed: 'Failed to fetch verification code', copied: 'Key copied', copyFailed: 'Failed to copy', enabled: 'Two-factor authentication enabled', verifyFailed: 'Verification failed', passwordRequired: 'Please enter your password', disabled: 'Two-factor authentication disabled', disableFailed: 'Failed to disable' }, appLoadingScreen: { ariaLabel: 'VoiceHub is loading', message: 'Syncing schedules, songs, and user status' } }, ui: { errorBoundary: { retrying: 'Retrying', retry: 'Retry', hideDetails: 'Hide details', showDetails: 'Show details', debugInfo: 'System debug info', defaultTitle: 'Load failed', defaultMessage: 'An error occurred while loading data. Please try again later.', retryFailed: 'Retry failed:' }, loadingState: { progress: 'Loading progress', cancel: 'Cancel loading', defaultMessage: 'Loading...' }, pagination: { info: (current: number, total: number) => `Page ${current} of ${total}`, totalItems: (total: number, itemName: string) => `${total} ${itemName}`, first: 'First', prev: 'Previous', next: 'Next', last: 'Last', pagePlaceholder: 'Page', jump: 'Go', defaultItemName: 'items' }, lyrics: { loading: 'Loading lyrics...', empty: 'No lyrics' }, playerControls: { previous: 'Previous', noPrevious: 'No previous track', next: 'Next', noNext: 'No next track' }, playerInfo: { openLyrics: 'Open lyrics', coverAlt: 'Cover', lyrics: 'Lyrics', textCoverFallback: 'M' }, lyricsModal: { unknownSong: 'Unknown Song', unknownArtist: 'Unknown Artist', lyrics: 'Lyrics', comments: 'Comments', settings: 'Lyrics settings', amStyleLyrics: 'Apple Music style lyrics', fontSize: 'Font size', lyricOffset: 'Lyric offset (ms)', showTranslation: 'Show translation', showRoma: 'Show romanization', showYrc: 'Word-by-word lyrics (YRC)', amllNormalizeSpaces: 'Normalize spaces', amllResetLineTimestamps: 'Reset line times', amllConvertBgLines: 'Merge bg vocals', amllSyncBgLines: 'Sync bg lines', amllCleanOverlaps: 'Clean overlaps', amllTryAdvanceStart: 'Advance lines', quality: 'Quality', lyricSource: 'Lyric source', lyricSourceAuto: 'Auto', playMode: { loopOne: 'Repeat One', order: 'Repeat Playlist', off: 'Play Once' }, notifications: { loopOne: 'Switched to Repeat One', off: 'Switched to Play Once', order: 'Switched to Repeat Playlist' } }, audioQuality: { unknown: 'Unknown quality', options: { standard: { label: 'Standard', description: 'Suitable when data is limited' }, neteaseHq: { label: 'HQ High', description: 'High-quality music experience' }, neteaseSq: { label: 'SQ Lossless', description: 'Lossless quality, larger files' }, neteaseHiRes: { label: 'Hi-Res', description: 'High-resolution lossless' }, neteaseMaster: { label: 'Master', description: 'Highest quality' }, tencentHq: { label: 'HQ High', description: 'High-quality music experience' }, tencentSq: { label: 'SQ Lossless', description: 'Lossless quality, larger files' }, tencentHiRes: { label: 'Hi-Res', description: 'High-resolution quality' }, tencentMaster: { label: 'Master 2.0', description: 'Highest quality' }, bilibiliDefault: { label: 'Default', description: 'Default quality' }, miguHq: { label: 'HQ High', description: 'High-quality music experience' }, miguSq: { label: 'SQ Lossless', description: 'Lossless quality, larger files' }, miguZq24: { label: 'ZQ24 Lossless', description: 'High-resolution quality, larger files, more data' } } }, bilibiliIframe: { openInNewTab: 'Open in new tab', openInBilibili: 'Open on Bilibili', close: 'Close', loadFailed: 'Unable to load video' }, songComments: { eyebrow: 'NetEase Comments', qqEyebrow: 'QQ Music Comments', title: 'Listener Comments', latest: 'Latest', hotSort: 'Popular', refresh: 'Refresh comments', noSource: 'No NetEase comment source for this song', qqNoSource: 'No QQ Music comment source for this song', loading: 'Loading comments', retry: 'Retry', commentsCount: (count: string) => `${count} comments`, empty: 'No comments are available for this song yet', userAvatar: 'User avatar', neteaseUser: 'NetEase user', qqUser: 'QQ Music user', locationLabel: 'From', commentImage: 'Comment image', openImagePreview: 'View full image', closeImagePreview: 'Close image', originalComment: 'Original comment', showMoreReplies: 'Show more replies', hideReplies: 'Collapse replies', unlike: 'Unlike', like: 'Like comment', loadingMore: 'Loading...', loadMore: 'Load more comments', tenThousand: 'w', justNow: 'Just now', minutesAgo: (minutes: number) => `${minutes} min ago`, hoursAgo: (hours: number) => `${hours} hr ago`, loginRequiredToLike: 'Please sign in to NetEase Cloud Music before liking comments', likeFailed: 'Failed to like comment', loadFailed: 'Failed to load comments', dateLocale: 'en-US' } }, composableErrors: { adminOnly: (action: string) => `Only administrators can ${action}`, loginRequired: (action: string) => `Please log in to ${action}`, failed: (action: string) => `Failed to ${action}`, actions: { createSchedule: 'create schedules', removeSchedule: 'remove schedules', updateScheduleSequence: 'update schedule order', markPlayed: 'mark songs as played', sendNotification: 'send system notifications', getSystemSettings: 'get system settings', updateSystemSettings: 'update system settings', getPlayTimes: 'get play times', createPlayTime: 'create play times', updatePlayTime: 'update play times', deletePlayTime: 'delete play times', deleteSong: 'delete songs', updateSong: 'update songs', addSong: 'add songs', updateNotificationSettings: 'update notification settings', markNotification: 'mark notifications', markAllNotifications: 'mark all notifications', deleteNotification: 'delete notifications', clearNotifications: 'clear notifications', getSemesters: 'get the semester list', getCurrentSemester: 'get the current semester', createSemester: 'create semesters', setActiveSemester: 'set the active semester', updateSemester: 'update semesters', deleteSemester: 'delete semesters', getSemesterOptions: 'get semester options' }, lyrics: { missingParams: 'Required parameters are missing', notFound: 'No lyrics were found', unavailable: 'No lyrics available', fetchFailed: 'Failed to load lyrics' } }, audioPlayer: { autoplayBlocked: 'Autoplay was blocked by your browser. Use the play button to continue.', noPrevious: 'No previous song', previousFailed: 'Failed to play the previous song', noNext: 'No next song', nextFailed: 'Failed to play the next song', playFailed: (message: string) => `Playback failed: ${message}`, popupBlocked: 'The browser blocked the new tab. Allow pop-ups and try again.', originalLinkOpened: 'The original link was opened', fallbackSource: 'The current link is invalid. Switched to a fallback source.', consecutiveFailures: 'Several songs failed to play. Autoplay has stopped.', bilibiliSkipped: 'The Bilibili video failed to play and was skipped', loopOneEnabled: 'Repeat one enabled', singleEnabled: 'Single-play mode enabled', listLoopEnabled: 'Playlist loop enabled' }, songs: { actions: { loginRequest: 'Please log in to request a song', loginVote: 'Please log in to vote', alreadyVoted: 'You have already voted for this song', voteCancelled: (title: string) => `Your vote for "${title}" was removed`, voteSucceeded: (title: string) => `Your vote for "${title}" was added`, loginWithdraw: 'Please log in to withdraw a song', loginDelete: 'Please log in to delete a song', deleteSucceeded: 'Song deleted successfully', loginMarkPlayed: 'Please log in to mark a song as played', markPlayedSucceeded: 'Song marked as played', loginUnmarkPlayed: 'Please log in to undo the played status', unmarkPlayedSucceeded: 'Played status removed', loginRequestReplay: 'Please log in to request a replay', replaySucceeded: 'Replay requested successfully', replayAlreadyRequested: 'You have already requested a replay for this song', loginCancelReplay: 'Please log in to cancel a replay request', replayCancelled: 'Replay request cancelled', leaveCollaborationSucceeded: (title: string) => `You have left the collaboration on "${title}"`, withdrawSucceeded: (title: string) => `Withdrew your submission of "${title}"`, withdrawSucceededQuota: (title: string) => `Withdrew your submission of "${title}"; your submission quota has been refunded`, fallbackSongTitle: 'this song' }, duplicateSongModal: { title: 'Song Already Exists', desc: 'This song is already in the list and cannot be submitted again', tip: 'You can like it to show support, or try searching for another song.', votes: (count: number) => `${count} votes`, played: 'Played', pending: 'Pending', requester: 'Requester: ', back: 'Choose Again', liked: 'Liked', likeNow: 'Like Now' }, bilibiliEpisodesModal: { uploader: 'UP: ', empty: 'No episodes', pause: 'Pause', preview: 'Preview', played: 'Played', scheduled: 'Scheduled', submitted: 'Submitted', liked: 'Liked', like: 'Like', submitting: 'Submitting...', select: 'Select', submit: 'Submit' }, musicLoginModal: { neteaseTitle: 'NetEase QR Login', neteaseDesc: 'Scan the QR code to securely sign in', qqTitle: 'QQ Music QR Login', qqDesc: 'Scan the QR code to enable account playback', loadingQr: 'Fetching QR code...', qrExpired: 'QR code expired', clickRefresh: 'Click to refresh', expiredRefresh: 'QR code expired. Click to refresh', neteaseWaiting: 'Scan with the NetEase Cloud Music app', qqWaiting: 'Scan with QQ', scanSuccess: 'Scanned successfully. Confirm on your phone', neteaseSuccess: 'Login successful. Redirecting...', qqSuccess: 'Login successful. Closing...', neteaseTip: 'Note: Login state is saved in your browser for podcast search and related features.', qqTip: 'Login state is saved only in this browser for QQ Music playback and lyric parsing.', channelQq: 'QQ Scan', channelWx: 'WeChat Scan', wxWaiting: 'Scan the QR code with WeChat', wxScanned: 'Scanned. Confirm on your phone', wxTip: 'Scan with the WeChat app to sign in. The login state is also kept only in this browser.', retry: 'Retry', qqAccount: 'QQ Music Account', qqQrIncomplete: 'QQ login QR response is incomplete', qqQrFailed: 'Failed to fetch QQ login QR code', qqMissingCookie: 'QQ login succeeded but no cookie was returned', qqCheckFailed: 'Failed to check QQ login status' }, mediaModals: { recentTitle: 'Recently Played - Songs', recentDesc: 'View and quickly submit songs you recently played on NetEase Cloud Music', loadingRecent: 'Fetching play history...', retryLoad: 'Retry', noRecent: 'No recent play records', checking: 'Checking', played: 'Played', scheduled: 'Scheduled', inList: 'Already in list', processing: 'Processing...', continueSubmit: 'Continue Submit', liked: 'Liked', like: 'Like', submitting: 'Submitting...', submitLoading: 'Submitting...', selectSubmit: 'Select Submit', close: 'Close', programListTitle: (radioName: string) => `${radioName} - Programs`, loadingPrograms: 'Loading programs...', retry: 'Retry', noPrograms: 'No programs', existing: 'Exists', loadingMore: 'Loading...', loadMore: 'Load More', fetchProgramsFailed: 'Failed to fetch programs', networkFailed: 'Network request failed', unknownDj: 'Unknown DJ', playedCannotLike: 'Played songs cannot be liked', scheduledCannotLike: 'Scheduled songs cannot be liked', fetchRecentFailed: 'Failed to fetch recent plays', justNow: 'Just now', minutesAgo: (minutes: number) => `${minutes} min ago`, hoursAgo: (hours: number) => `${hours} hr ago`, monthDay: (month: number, day: number) => `${month}/${day}`, tenThousand: '万' }, scheduleList: { addToPlaylist: 'Add to Playlist', chooseDate: 'Choose Date', noScheduleDates: 'No schedule dates', loading: 'Loading...', emptyAll: 'No schedules yet', emptyAllDesc: 'Submit a song and wait for admins to schedule it', emptyDate: 'No schedules on this date', emptyDateDesc: 'Choose another date to view', unspecifiedPlayTime: 'Unspecified Time Slot', play: 'Play', pause: 'Pause', replaySong: 'Replay Song', viewSubmissionNote: 'View Note', replayApplicants: 'Replay applicants: ', applicant: 'Applicant: ', requester: 'Requester: ', mainRequesterTitle: 'Main requester: ', requesterTitle: 'Requester: ', collaboratorsTitle: 'Collaborators: ', unknown: 'Unknown', replay: 'Replay', heat: 'Heat', cardCodeUsed: 'Request Card', loginRequiredForPlaylist: 'Sign in to NetEase Cloud Music to manage playlists', loginNow: 'Sign In', currentAccount: 'Current Account', neteaseUser: 'NetEase User', targetPlaylist: 'Target Playlist', playlistPlaceholder: 'Select playlist', refreshPlaylist: 'Refresh playlists', deletePlaylist: 'Delete Playlist', or: 'or', createPlaylist: 'Create Playlist', newPlaylistPlaceholder: 'New playlist name', create: 'Create', privatePlaylist: 'Make playlist private', selectSongs: 'Select Songs', selectAll: 'Select All', clear: 'Clear', noNeteaseSongs: 'No NetEase songs on this date', playlistLoadFailed: 'Failed to load playlists', playlistLoadFailedWithMessage: (message: string) => `Failed to load playlists: ${message}`, playlistCreateSuccess: 'Playlist created', playlistCreateFailed: 'Failed to create playlist', playlistCreateFailedWithMessage: (message: string) => `Failed to create playlist: ${message}`, deletePlaylistTitle: 'Delete Playlist', deletePlaylistMessage: 'Delete the current playlist? This action cannot be undone.', playlistDeleteSuccess: 'Playlist deleted', playlistDeleteFailed: 'Failed to delete playlist', playlistDeleteFailedWithMessage: (message: string) => `Failed to delete playlist: ${message}`, playlistAddSuccess: (count: number) => `Added ${count} songs to playlist`, playlistAddFailed: 'Failed to add songs to playlist', playlistAddFailedWithMessage: (message: string) => `Failed to add songs to playlist: ${message}`, musicUrlUnavailable: 'Unable to get music playback URL. Please try again later', musicUrlFailed: 'Failed to get music playback URL', invalidDateFormat: 'Invalid date format', invalidDate: 'Invalid date', missingMusicInfo: 'The song is missing music platform or music ID information, so the playback URL cannot be fetched', mobileDate: (_month: number, _day: number, weekday: string, formatted: string) => `${formatted} ${weekday}`, fullDate: (_year: number, _month: number, _day: number, formatted: string) => formatted, playTimeStart: (time: string) => `Starts at ${time}`, playTimeEnd: (time: string) => `Ends at ${time}`, cancel: 'Cancel', adding: 'Adding...', confirmAdd: 'Confirm Add', submissionNote: 'Submission Note', publicNote: 'Public Note', privateNote: 'Admin Only', songsCount: (count: number) => `${count} songs` }, songList: { mobileSearchPlaceholder: 'Search requests...', searchPlaceholder: 'Search songs', tabs: { all: 'All Requests', mine: 'My Requests', replays: 'My Replays' }, currentSemester: 'Current semester: ', refreshing: 'Refreshing...', refresh: 'Refresh song list', loading: 'Loading...', emptyMine: 'You have not submitted any songs yet. Go request one!', emptyReplays: 'You have not requested any replays yet. Check played songs!', emptyAll: 'No songs yet. Go request one!', play: 'Play', pause: 'Pause', playDate: (date: string) => `Played on: ${date}`, scheduleDate: (date: string) => `Scheduled on: ${date}`, played: 'Played', scheduled: 'Scheduled', replay: 'Replay', replaySong: 'Replay Song', cardCodeUsed: 'Request Card', viewSubmissionNote: 'View Note', mainRequesterTitle: 'Main requester: ', requesterTitle: 'Requester: ', collaboratorsTitle: 'Collaborators: ', replayRequest: (count: number) => `Replay request (${count}): `, requester: 'Requester: ', heat: 'Heat', likeAlt: 'Like', requestedAt: 'Requested at: ', withdrawSubmission: 'Withdraw request', leaveCollaboration: 'Leave collaboration', withdraw: 'Withdraw', cancelReplayTitle: 'Withdraw replay request', cancelReplay: 'Withdraw Request', itemName: 'songs', submissionNote: 'Submission Note', publicNote: 'Public Note', privateNote: 'Admin Only', unknownSong: 'Unknown Song', unknownArtist: 'Unknown Artist', unknownTime: 'Unknown Time', unknownUser: 'Unknown User', textCoverFallback: '♪', like: 'Like', inactiveSemester: 'Inactive semester', notCurrentSemester: 'Not current semester', playedCannotLike: 'Played songs cannot be liked', scheduledCannotLike: 'Scheduled songs cannot be liked', selfLikeDisabled: 'You cannot like your own song', unlike: 'Click to unlike', loginBeforeLike: 'Please sign in before liking', withdrawTitle: 'Withdraw Request', withdrawMessage: (title: string) => `Withdraw your request for "${title}"? This also removes all collaborators.`, leaveTitle: 'Leave Collaboration', leaveMessage: (title: string) => `Leave the collaboration for "${title}"?`, cancelReplayConfirmTitle: 'Cancel Replay Request', cancelReplayMessage: (title: string) => `Cancel replay request for "${title}"?`, requestReplayTitle: 'Request Replay', requestReplayMessage: (title: string) => `Request replay for "${title}"?`, processing: 'Processing...', requestReplay: 'Request Replay', replayRequesting: 'Requesting...', replayRejectedCooldown: (hours: number) => `Rejected (${hours}h before retry)`, replayed: 'Replayed', onlyCurrentSemesterReplay: 'Only current-semester songs can be replayed', replayRejectedTooltip: (hours: number) => `Rejected. Wait ${hours} hours before requesting again`, alreadyReplayed: 'This song has been replayed', alreadyRequestedReplay: 'Replay already requested', musicUrlFailed: 'Failed to get music playback URL', semesterLoadFailed: 'Failed to load semester info. Please refresh and try again', platformAutoSwitched: (name: string) => `Current platform is unavailable, switched to ${name}` }, requestForm: { guidelinesTitle: 'Submission Guidelines', defaultGuidelines: [ 'Do not include book-title brackets when submitting.', 'Except DJ tracks, other song types are accepted, including minority languages.', 'Do not submit songs containing prohibited content.', 'Submitted songs will be reviewed by admins.', 'Approved songs will be scheduled for playback.', 'Submitting means you have read the guidelines and understand the song may not be played.', 'This system only provides music search and playback management. It does not store music files. All music content comes from third-party platforms and belongs to their platforms and rights holders. Please follow platform terms and respect copyright. We encourage supporting official music sources.', 'The broadcasting station reserves the final interpretation right.' ], mobileDefaultGuidelines: [ 'Do not include book-title brackets when submitting.', 'Except DJ tracks, other song types are accepted, including minority languages.', 'Do not submit songs containing prohibited content.', 'Submitted songs will be reviewed by admins.', 'Approved songs will be scheduled for playback.', 'Submitting means you have read the guidelines and understand the song may not be played.', 'This system only provides music search and playback management. It does not store music files. All music content comes from third-party platforms and belongs to their platforms and rights holders.', 'The broadcasting station reserves the final interpretation right.' ], searchLabel: 'Search', searchPlaceholder: 'Enter song name', processing: 'Processing...', search: 'Search', audioMatch: 'Recognize Song', audioMatchShort: 'Recognize', importFromPast: 'Import Prev', collaborators: 'Collaborators', add: 'Add', adminUnlimited: 'You are an admin and not limited by submission quotas', outsideRequestTime: 'Submissions are not open right now', submissionClosed: 'Submissions are closed', currentPeriod: 'Current period: ', accepted: 'Accepted', todayRequests: 'Today: ', weeklyRequests: 'This week: ', monthlyRequests: 'This month: ', remaining: 'Remaining', platforms: { netease: 'NetEase Cloud Music', tencent: 'QQ Music', bilibili: 'Bilibili', migu: 'Migu Music' }, refreshing: 'Refreshing', neteaseLoginTitle: 'Sign in to NetEase for full features', qqLoginTitle: 'Sign in to QQ Music for stable playback', loginNow: 'Sign In', importData: 'Import Data', loggedIn: 'Signed In', qqLoggedIn: 'QQ Music Signed In', single: 'Song', podcast: 'Podcast', recent: 'Recent', playlist: 'Playlist', exportCookie: 'Export Cookie', logout: 'Sign Out', logoutQQ: 'Sign out of QQ Music', qqLoginExpired: 'QQ Music session expired, please sign in again', preferredPlayTime: 'Preferred Time Slot', choosePlayTime: 'Choose time slot', submissionNote: 'Submission Note', cardCode: 'Request Card', required: 'Required', optional: 'Optional', editCardCode: 'Edit', addCardCode: 'Add', clearCardCode: 'Clear request card', cardCodeLabel: 'Code', clear: 'Clear', save: 'Save', validatingCardCode: 'Validating request card...', validatingShort: 'Validating', cardCodeRequiredHelper: 'A valid request card is required when mandatory request cards are enabled.', cardCodeOptionalHelper: 'Add a request card to redeem it with this submission.', cardCodeRequiredBypassHelper: 'A valid request card is required; request-card submissions do not use regular quota.', cardCodeOptionalBypassHelper: 'A valid request card can bypass your personal submission limit without using regular quota.', cardCodeAvailableBypass: 'Request card accepted; regular quota will not be used', cardCodePlaceholder: 'Enter request card code', cardCodeWillValidate: 'Request card entered; it will be validated before submission', cardCodeRequiredStatus: 'Add a valid request card before submitting', cardCodeOptionalStatus: 'Request card is optional', cardCodeInvalid: 'Invalid request card', cardCodeAvailable: 'Request card available', cardCodeFilled: 'Request card added', cardCodeRequiredShort: 'Request card required', cardCodeOptionalShort: 'Request card optional', cardCodeSaveHint: 'The request card will be validated before saving.', cardCodeValidateFailed: 'Failed to validate request card. Please try again later', cardCodeRequiredWarning: 'Please enter a request card first', cardCodeAuthRequired: 'Please sign in before validating a request card', cardCodeDisabled: 'Request card submissions are not enabled', cardCodeInvalidOrUsed: 'This request card is invalid or has already been used', cardCodeLockedOrUnavailable: 'This request card is locked or unavailable', cardCodeRequiredForSite: 'This site requires a valid request card to submit songs', cardCodeTooLong: 'Request card code cannot exceed 100 characters', notifications: { restrictionSameSong: 'This song is in a submission cooldown period. Please try again later', restrictionSameArtist: 'This artist is in a submission cooldown period. Please try again later', restrictionGeneric: 'This song or artist is in a submission cooldown period. Please try again later', exportSuccess: 'Export successful', validatingCookie: 'Validating cookie...', importSuccess: 'Import successful', cookieInvalid: 'Imported cookie is invalid or expired', fileFormatError: 'Invalid file format', importFailed: (message: string) => `Import failed: ${message}`, missingSongId: 'Cannot vote: missing song ID', likeFailed: 'Like failed', songNameRequired: 'Song name is required', musicUrlFailedRetry: 'Failed to get music URL. Please try again later', songUnavailable: 'This song cannot be played. It may be paid content', playFailed: 'Playback failed. Please try again later', songSelectedMaybeLimited: 'Song selected, but full playback may be unavailable', duplicateSong: 'This song is already in the list. Please support it by liking it!', blacklistedSong: (reasons: string) => `This song cannot be requested: ${reasons}`, submitFailed: 'Submission failed. Please try again later', likeSuccess: 'Liked!', likeFailedRetry: 'Like failed. Please try again later', completeSongInfoRequired: 'Please enter complete song information', fixCoverUrl: 'Fix the cover URL before submitting', fixPlayUrl: 'Fix the play URL before submitting', replayAlreadyRequested: 'Replay has already been requested for this song', replayRequestSuccess: 'Replay request submitted', replayRequestFailed: (message: string) => `Replay request failed: ${message}`, replayOriginalNotFound: 'Original song for the replay request was not found', periodQuotaFull: (accepted: number, expected: number) => `Current period quota is full (${accepted}/${expected})`, dailyLimitReached: (used: number, limit: number) => `Daily submission limit reached (${used}/${limit})`, weeklyLimitReached: (used: number, limit: number) => `Weekly submission limit reached (${used}/${limit})`, monthlyLimitReached: (used: number, limit: number) => `Monthly submission limit reached (${used}/${limit})` }, publicToUsers: 'Public to signed-in users', submissionNoteApprovalHint: 'Public note will show after admin approval.', coverAlt: 'Cover', albumDetailsTitle: 'Click to view album details', album: 'Album: ', uploadToNeteaseCloud: 'Upload to Cloud Drive', allEpisodesSubmitted: 'All episodes exist', partialEpisodesSubmitted: 'Some episodes exist', chooseEpisodes: 'Choose Episodes', songPlayed: 'Song played', songScheduled: 'Song scheduled', songExists: 'Song exists', continueSubmit: 'Continue Submit', playedCannotLike: 'Played songs cannot be liked', scheduledCannotLike: 'Scheduled songs cannot be liked', liked: 'Liked', like: 'Like', played: 'Played', scheduled: 'Scheduled', chooseProgram: 'Choose Program', chooseSubmit: 'Submit', requestReplay: 'Request Replay', manualSubmitLong: 'Not found? Submit manually', loginRequiredToSubmit: 'Sign in to submit', loginRequiredNotice: 'You are not signed in and cannot submit requests', cardCodeBypassesLimit: 'Does not use regular quota', noResults: 'No songs found', noResultsHint: 'Try another keyword or platform', noMatchingSongs: 'No matching songs found', searchRequestFailed: 'Search request failed. Please try again later', manualSubmit: 'Manual Submit', searchSongsAlt: 'Search songs', addCollaboratorTitle: 'Add Collaborators', albumModal: { albumFallback: 'Album', unknownArtist: 'Unknown Artist', songCount: (count: number) => `${count} songs`, publishTime: (date: string) => `Released: ${date}`, company: (company: string) => `Label: ${company}`, viewFullIntro: 'Click to view full introduction', searchPlaceholder: 'Search songs in this album...', loading: 'Loading...', noMatches: 'No matching songs found', noSongs: 'No songs', pause: 'Pause', preview: 'Preview', submitted: 'Submitted', submit: 'Submit', albumIntro: 'Album Introduction', loadFailed: 'Failed to load. Please try again later', fetchFailed: 'Failed to fetch album details', unsupportedPlatform: 'Unsupported platform', tencentUnsupported: 'QQ Music album details are not supported yet' }, importSongsModal: { resultTitle: 'Import Result', resultDesc: 'Song import operation completed', successCount: 'Imported', failedCount: 'Failed / Skipped', details: 'Detailed Processing Records', allSuccess: 'All songs imported successfully!', done: 'Done', title: 'Import from Prev Semester', desc: 'Choose songs from previous semesters and import them into the current semester', selectSemester: 'Select Semester', semesterPlaceholder: 'Select target semester', loadingSongs: 'Loading songs...', retry: 'Retry', selectSemesterFirst: 'Please select a semester first', noSongs: 'No matching songs found', collected: 'Collected', notCollected: 'Not Collected', unplayed: 'Not Collected', played: 'Collected', all: 'All', selectAll: 'Select All Current', cancelSelectAll: 'Cancel Select All', selectedSongs: (count: number) => `${count} songs selected`, importFailed: 'Import failed', loadSemestersFailed: 'Failed to load semester list', loadSongsFailed: 'Failed to load song list', importing: 'Importing...', confirmImport: 'Confirm Import' }, neteaseUpload: { title: 'Upload to NetEase Cloud Music', loginRequired: 'Please sign in to NetEase Cloud Music first', loginNow: 'Sign In', quality: 'Select Quality', songInfo: 'Song Info', coverAlt: 'Cover', cancel: 'Cancel', uploading: 'Uploading...', startUpload: 'Start Upload', unknownSong: 'Unknown Song', unknownArtist: 'Unknown Artist', unknownAlbum: 'Unknown Album', statusGetDownloadUrl: 'Getting download URL', statusDownloading: 'Downloading audio', statusHashing: 'Calculating file fingerprint', statusToken: 'Getting upload token', statusUploading: 'Uploading to NetEase Cloud Music', statusExists: 'File already exists, instant upload completed', statusSaving: 'Saving cloud drive info', statusPreparing: 'Preparing', statusDone: 'Upload complete', statusFailed: 'Upload failed', fetchingQQUrl: 'Getting audio URL from QQ Music...', downloadingAudio: 'Downloading audio file...', uploadingCloud: 'Uploading to NetEase Cloud Music Drive...', uploadSuccess: 'Song uploaded to your NetEase Cloud Music Drive', missingSongId: 'Missing song ID (strMediaMid)', invalidPlayUrl: 'Unable to get a valid playback URL', emptyResponse: 'Response body is empty', missingResolvedId: 'Unable to get song ID. Please try again', missingMusicUrl: 'Unable to get music playback URL', unknownError: 'Unknown error', uploadFailed: (message: string) => `Upload failed: ${message}`, requestFailed: (status: number | string) => `Request failed: ${status}`, downloadFailed: (status: number | string) => `Download failed: ${status}`, tokenFailed: (message: string | number) => `Failed to get token: ${message}`, nosFailed: (status: number | string) => `NOS upload failed: ${status}`, nosNetworkError: 'NOS network error', saveMaybeFailed: (code: string | number) => `Cloud drive info may not have been saved (error code: ${code}). Check whether the song was uploaded successfully.`, publishFailed: (message: string | number) => `Publish failed: ${message}` }, playlistModal: { selectPlaylist: 'Select Playlist', playlistDetails: 'Playlist Details', processing: 'Processing...', retry: 'Retry', emptyPlaylists: 'No playlists', favSongs: 'Liked Songs', songCount: (count: number) => `${count} songs`, emptyPlaylist: 'Playlist is empty', exists: 'Exists', loadMore: 'Load More', loadingMore: 'Loading...', fetchPlaylistsFailed: 'Failed to fetch playlists', fetchSongsFailed: 'Failed to fetch playlist songs', networkFailed: 'Network request failed' }, audioMatchHint: 'Play audio nearby and record 3 seconds to recognize the song', audioMatchScriptLoadFailed: (src: string) => `Failed to load recognition resource: ${src}`, audioMatchEngineFailed: 'Recognition engine failed to initialize', unknownSong: 'Unknown Song', unknownArtist: 'Unknown Artist', audioMatchGenerating: 'Generating fingerprint and recognizing...', audioMatchNoMatch: 'No matching song recognized. Try a clearer chorus segment', audioMatchDone: (count: number) => `Recognition complete. Found ${count} candidates`, audioMatchFailed: 'Song recognition failed. Please try again later', microphoneRequesting: 'Requesting microphone permission...', microphoneUnsupported: 'Microphone access is not supported. Use HTTPS or localhost', audioMatchRecordingProgress: (current: string, total: number) => `Recording ${current}s / ${total}s`, microphoneTimeout: 'Microphone permission timed out. Check system permissions', microphoneReady: 'Microphone connected. Click to start recognition', audioMatchInitFailed: 'Cannot initialize recognition. Check microphone permission', microphoneAccessFailed: 'Cannot access microphone. Please try again later', preparing: 'Preparing...', recognizing: 'Recognizing...', startAudioMatch: 'Start Recognition', recording: 'Recording...', cancel: 'Cancel', audioMatchResults: 'Recognition Results', manualTitle: 'Manual Song Info', songName: 'Song Name', artistName: 'Artist Name', artistPlaceholder: 'Enter artist name', coverUrl: 'Cover URL (optional)', coverPlaceholder: 'Enter song cover image URL', playUrl: 'Play URL (optional)', playUrlPlaceholder: 'Enter song play URL', validUrl: 'Valid URL', submitting: 'Submitting...', confirmSubmit: 'Confirm Submit' } } } as const export const auth = pages.auth export const ui = pages.ui export const songs = pages.songs export const yearReview = pages.yearReview export const composableErrors = pages.composableErrors export const audioPlayer = pages.audioPlayer export const importantNotification = { label: 'Important Notice', unread: 'Unread', read: 'Read', sender: 'Sender', systemSender: 'System', close: 'Mark as read', closing: 'Marking as read...', justNow: 'Just now', minutesAgo: (minutes: number) => `${minutes} min ago`, hoursAgo: (hours: number) => `${hours} hr ago`, daysAgo: (days: number) => `${days} days ago`, loadFailed: 'Failed to load the important notice. Please try again later.', closeFailed: 'Failed to mark the notice as read. Please try again.', retry: 'Retry' } export const admin = { sidebar: { console: 'Admin Console', logout: 'Sign Out', adminFallback: 'Admin', avatarFallback: 'A', sections: { overview: 'Overview', content: 'Content', users: 'Users', api: 'API', system: 'System', account: 'Account' }, menu: { overview: 'Overview', schedule: 'Schedule', print: 'Print', songs: 'Songs', dataAnalysis: 'Analytics', users: 'Users', gradeClass: 'Grade / Class', apiKeys: 'API Keys', notifications: 'Notifications', smtpConfig: 'Mail Config', playtimes: 'Play Times', requestTimes: 'Requests', semesters: 'Semesters', blacklist: 'Blacklist', cardCodes: 'Request Cards', musicSource: 'Music Source', siteConfig: 'Site Config', database: 'Database', password: 'Change Password' }, roles: { USER: 'User', SONG_ADMIN: 'Song Admin', ADMIN: 'Admin', SUPER_ADMIN: 'Super Admin' } }, overview: { recentActivities: 'Recent Activities', loading: 'Loading...', noActivities: 'No activities yet', systemStatus: 'System Status', online: 'Online', offline: 'Offline', instanceId: 'Instance ID', noInstanceId: 'No Instance ID', quickActions: 'Quick Actions', statCards: { totalSongs: 'Total Songs', registeredUsers: 'Registered Users', todaySchedules: 'Today Schedules', weeklyRequests: 'Weekly Requests' }, statusItems: { database: 'Database', api: 'API Service', version: 'System Version', semester: 'Current Semester', blacklist: 'Blacklist Items' }, actions: { schedule: 'Manage Schedule', notifications: 'Send Notifications', users: 'Manage Users', blacklist: 'Blacklist' }, copied: 'Instance ID copied', copyFailed: 'Copy failed', loadFailed: 'Failed to load data', normal: 'Normal', abnormal: 'Abnormal', unset: 'Unset', itemUnit: (count: number) => `${count} items`, justNow: 'Just now', minutesAgo: (minutes: number) => `${minutes} min ago`, hoursAgo: (hours: number) => `${hours} hr ago`, daysAgo: (days: number) => `${days} days ago`, activityTypes: { song: 'Song Submission', schedule: 'Schedule Update', user: 'User Action', system: 'System Event' } }, blacklist: { title: 'Song Blacklist', desc: 'Block specific songs or keywords to keep broadcast content healthy and compliant', addTitle: 'Add Blacklist Item', type: 'Block Type', song: 'Song', keyword: 'Keyword', language: 'Language', genre: 'Genre', allTypes: 'All Types', songLabel: 'Song Title - Artist', keywordLabel: 'Keyword', languageLabel: 'Language', genreLabel: 'Genre', languageHint: 'NetEase/QQ sources only. "Other" matches songs whose language is determined but outside the listed ones. Undetermined or manual submissions are not blocked.', genreHint: 'NetEase/QQ sources: QQ genres come from official category names; genres outside the listed ones fall into "Other". Undetermined songs are not blocked.', songPlaceholder: 'e.g. Song Title - Artist', keywordPlaceholder: 'e.g. sensitive word, specific style', reason: 'Reason (optional)', reasonPlaceholder: 'Describe why this item is blacklisted', processing: 'Processing...', addButton: 'Add to Blacklist', searchPlaceholder: 'Search blacklist...', typeFilter: 'Type Filter', listTitle: (total: number) => `Blacklist (${total} items)`, reasonPrefix: 'Reason:', noReason: 'No reason provided', createdAt: 'Created:', disable: 'Disable', enable: 'Enable', loading: 'Loading...', emptyTitle: 'No matching blacklist items', emptyDesc: 'Try changing filters or adding a new item', pagination: (page: number, pages: number) => `Page ${page} / ${pages}`, removeTitle: 'Remove from Blacklist', removeMessage: (value: string) => `Remove "${value}" from the blacklist? It will become available again.`, cancel: 'Cancel', deleting: 'Deleting...', confirmRemove: 'Confirm Remove', fetchFailed: 'Failed to fetch blacklist', addSuccess: 'Blacklist item added', addFailed: 'Failed to add blacklist item', addFailedPrefix: 'Add failed: ', statusSuccess: (enabled: boolean) => `Blacklist item ${enabled ? 'enabled' : 'disabled'}`, updateFailed: 'Failed to update status', deleteSuccess: 'Blacklist item deleted', deleteFailed: 'Delete failed' }, gradeClass: { title: 'Grade / Class Management', desc: 'Maintain grade/class options for registration, batch updates and notifications. Falls back to existing users when unconfigured.', gradeLabel: 'Grade', classLabel: 'Class', gradePlaceholder: 'e.g. Senior 1', classPlaceholder: 'e.g. Class 1', addButton: 'Add', deleteButton: 'Delete', batchTitle: 'Batch Generate', batchGradePlaceholder: 'Separate grades with commas, e.g. Senior 1,Senior 2,Senior 3', batchClassPlaceholder: 'Separate classes with commas; number ranges supported, e.g. Class 1,Class 2 or 1-10', batchButton: 'Generate', batchResult: (added: number, skipped: number) => `Added ${added} item(s)${skipped > 0 ? `, skipped ${skipped} existing` : ''}`, batchInvalid: 'Enter at least one grade and one class', batchLimit: 'Batch size is limited to 500 items. Please reduce the grade or class range.', initTitle: 'Initialize from Existing Users', initDesc: 'Extract grade/class combinations from all active users and add them to the config; existing combinations are skipped automatically.', initButton: 'Extract from Users', initResult: (added: number, skipped: number) => `Extracted ${added} item(s)${skipped > 0 ? `, skipped ${skipped} existing` : ''}`, initFailed: 'Initialization failed', batchFailed: 'Batch generation failed', guideTitle: 'How to use', guideSingle: 'Single add: enter one grade and one class on the left, then click Add', guideBatchLine1: 'Batch: separate multiple grades with commas, e.g. "Senior 1,Senior 2,Senior 3"', guideBatchLine2: 'Classes support commas and number ranges, e.g. "1-10" generates Class 1 through Class 10', guideDedup: 'Duplicate combinations are skipped automatically; deleting a config does not affect existing users', classCount: (count: number) => `${count} class(es)`, personCount: (count: number) => `${count} users`, deleteGradeButton: 'Delete Grade', deleteGradeTitle: 'Delete Entire Grade', deleteGradeMessage: (grade: string, count: number) => `Delete all ${count} class(es) under "${grade}"? It only affects the option list, not existing user data.`, deleteGradeSuccess: 'Grade configuration deleted', addSuccess: 'Item added', addFailed: 'Failed to add', duplicate: 'This grade/class combination already exists', deleteConfirm: 'Delete this item? It only affects the option list, not existing user data.', deleteSuccess: 'Item deleted', deleteFailed: 'Failed to delete', loading: 'Loading...', empty: 'No grade/class configured', fallbackHint: 'No configuration yet; options will fall back to existing user data', loadFailed: 'Failed to load config' }, songManagement: { title: 'Song Management', stats: { total: 'Total', played: 'Played', pending: 'Pending' }, actions: { download: 'Download', delete: 'Delete', addManual: 'Add Manually', refresh: 'Refresh', viewRemark: 'View Remark', edit: 'Edit Song', markPlayed: 'Mark as Played', markUnplayed: 'Mark as Unplayed', reject: 'Reject Song', deleteSong: 'Delete Song' }, filters: { semester: 'Semester', selectSemester: 'Select Semester', playTime: 'Play Time', selectPlayTime: 'Select Time', status: 'Status', selectStatus: 'Select Status', sort: 'Sort', selectSort: 'Select Sort' }, options: { allStatus: 'All Status', pending: 'Pending', scheduled: 'Scheduled', played: 'Played', hasNote: 'Has Remark', newest: 'Newest', oldest: 'Oldest', hotDesc: 'Most Popular', hotAsc: 'Least Popular', titleAsc: 'Title A-Z', titleDesc: 'Title Z-A' }, table: { songInfo: 'Song Info', requester: 'Requester', stats: 'Stats', status: 'Status', actions: 'Actions', selectPage: 'Select Page' }, empty: { search: 'No matching songs found', default: 'No song data' }, dialog: { cancel: 'Cancel', confirmDelete: 'Confirm Delete', deleteSongTitle: 'Delete Song', deleteSongMessage: (title: string) => `Delete song "${title}"? This action cannot be undone.`, batchDeleteTitle: 'Batch Delete Songs', batchDeleteMessage: (count: number) => `Delete the selected ${count} songs? This action cannot be undone.` }, rejectDialog: { title: 'Reject Song', requester: (name: string) => `Requester: ${name}`, reason: 'Reject Reason', reasonPlaceholder: 'Enter the reason. It will be sent to the requester as a system notification...', addToBlacklist: 'Also add this song to the blacklist', blacklistHint: 'After blacklisting, this song cannot be requested again', processing: 'Processing...', confirm: 'Confirm Reject' }, editModal: { editTitle: 'Edit Song', addTitle: 'Add Song Manually', titleLabel: 'Song Title', titlePlaceholder: 'Enter song title', artistLabel: 'Artist', artistPlaceholder: 'Enter artist name', requester: 'Requester', userSearchPlaceholder: 'Search user name or username', selectedUser: (name: string) => `Selected: ${name}`, collaborators: 'Collaborators', collaboratorSearchPlaceholder: 'Search and add collaborators', collaboratorHint: 'Add multiple collaborators; changes take effect after saving', preferredPlayTime: 'Preferred Time (optional)', selectPreferredPlayTime: 'Select preferred play time', submissionNote: 'Song Remark', clearNote: 'Clear Remark', cancelClearNote: 'Undo Clear', submissionNotePlaceholder: 'Enter song remark', publicNote: 'Public Remark', publicNoteHint: 'Other users can view this remark when public', clearNoteHint: 'The current remark will be cleared after saving. You can add a reason and send a notification', clearNoteReasonPlaceholder: 'Optional: enter a reason, e.g. inappropriate remark content', notifyAfterClear: 'Notify after clearing', notifyAfterClearHint: 'Notify the main requester and current collaborators', musicPlatform: 'Music Platform (optional)', selectPlatform: 'Select platform', musicId: 'Music ID (optional)', musicIdPlaceholder: 'Enter the platform identifier', coverUrl: 'Cover URL (optional)', playUrl: 'Play URL (optional)', duration: 'Duration (sec)', durationPlaceholder: 'e.g. 240', durationHint: 'Range 30–3600 sec; use the button to fetch from the platform', refreshDuration: 'Refresh', validUrl: 'Valid URL', saving: 'Saving...', saveChanges: 'Save Changes', adding: 'Adding...', addSong: 'Add Song' }, songDownloadDialog: { title: 'Download Songs', quality: 'Select Quality', advancedOptions: 'Advanced Options', mergeIntoOne: 'Merge into One', mergeDesc: 'Merge selected songs into a single audio file in order', normalizeAudio: 'Audio Normalization', normalizeDesc: 'Normalize peak volume independently from export format', saveDbPresetTitle: 'Save current volume settings (including enabled state) as default', saved: 'Saved!', savePreset: 'Save Preset', exportFormat: 'Export Format', mergeNeedsFormat: 'Merged files require an output format', transcodeSingle: 'Transcode to selected format', format: 'Format', customFilename: 'Custom Filename', saveAsDefaultPreset: 'Save as Default', filenamePlaceholder: 'e.g. Episode XX - {songs}', insertAllSongs: 'Insert All', insertDate: 'Insert current date', placeholders: 'Available placeholders:', allSongs: 'all song names', date: 'date', songList: 'Song List', estimatedDuration: (duration: string) => `Estimated total duration: ${duration}`, estimateSelectedTitle: 'Quickly estimate the total duration of selected songs', estimatingDuration: 'Estimating...', estimateDuration: 'Estimate duration', allSelectedHaveDuration: 'All selected songs already have duration information', durationFromPlaybackUrlFailed: 'Unable to read duration from playback URL', previewDurationDetected: 'Preview duration detected', estimateSuccess: (count: number) => `Estimated duration for ${count} songs`, estimateFailedCount: (count: number) => `${count} songs failed to estimate`, estimateFailed: 'Failed to estimate duration', preloadSelectedTitle: 'Preload selected songs into browser cache', preloadSelected: 'Preload Selected', selectAll: 'Select All', deselectAll: 'Deselect All', deleteCache: 'Delete Cache', preloadSong: 'Preload this song', emptySongs: 'No songs', processProgress: 'Processing Progress', downloadProgress: 'Download Progress', downloadingSong: (song: string) => `Downloading: ${song}`, preparing: 'Preparing...', completeWithErrors: 'Download complete with some failures', complete: 'Download complete', downloadFailedCount: (count: number) => `Download failed (${count})`, selectedSongsCount: (count: number) => `${count} songs selected`, cancel: 'Cancel', close: 'Close', processing: 'Processing...', downloading: 'Downloading...', startProcessing: 'Start Processing', startDownload: 'Start Download', qualityDescriptions: { 2: 'Data saving', 3: 'High quality', 4: 'Very high quality', 5: 'Lossless', 6: 'Hi-Res lossless', 9: 'Ultra HD master', default: 'Standard quality' }, preloadFailed: (title: string) => `Preload failed: ${title}`, mergeTitle: 'Audio Merge', mergeFailed: 'Merge failed', musicUrlMissing: 'Unable to get music playback URL', musicUrlFailed: (message: string) => `Failed to get music playback URL: ${message}`, downloadingProgress: (title: string, percent: number) => `Downloading: ${title} (${percent}%)`, usingPreloadCache: (title: string) => `Using preload cache: ${title}`, downloadingTitle: (title: string) => `Downloading: ${title}`, mergedFilename: (date: string, count: number, ext: string) => `schedule_merge_${date}_${count}_songs.${ext}`, preprocessingAudio: (value: number | string) => `Preprocessing audio: ${value}%`, mergingAudio: (value: number | string) => `Merging audio: ${value}%`, encodingAudio: (format: string, value: number | string) => `Encoding ${format}: ${value}%`, encodingFailed: 'Encoding failed', encoderWorkerFailed: 'Encoder Worker failed', encoderWorkerTerminated: 'Encoder Worker terminated', encodingCanceled: 'Encoding task canceled', decoding: (title: string) => `Decoding: ${title}`, normalizing: (title: string) => `Normalizing: ${title}`, converting: (title: string) => `Converting format: ${title}`, fastPreparing: (title: string) => `Fast preparing: ${title}`, noProcessedAudio: 'No audio was processed successfully', fastMerging: 'Fast merging and encoding...', completedFile: (filename: string) => `Completed: ${filename}`, preparingSong: (title: string) => `Preparing: ${title}`, decodeFailed: (message: string) => `Decode failed: ${message}`, writingMergedFile: (title: string) => `Writing merged file: ${title}`, writeMergedFileFailed: (message: string) => `Failed to write merged file: ${message}`, finishingMergedFile: 'Finishing merged file...', compatibleMerge: 'Merging with compatibility mode...', fastMerge: 'Merging with fast mode...', fastMergeMemoryFallback: 'Fast merge ran out of memory; switched to compatibility mode', activeWorkers: (title: string, active: number, concurrency: number) => `${title} (${active}/${concurrency} active)`, remainingSongs: (count: number) => `${count} remaining`, processComplete: 'Processing complete', downloadSuccess: (count: number) => `${count} songs downloaded`, downloadCompleteWithCount: (success: number, failed: number) => `Download complete: ${success} succeeded, ${failed} failed` }, searchPlaceholder: 'Search song title, artist, or requester...', loading: 'Loading...', unknown: 'Unknown', unknownUser: 'Unknown User', unknownError: 'Unknown error', allSemesters: 'All Semesters', allPlayTimes: 'All Times', unspecifiedPlayTime: 'Unspecified', scheduledWithDate: (date: string) => `Pending (${date})`, platforms: { netease: 'NetEase Cloud Music', tencent: 'QQ Music', bilibili: 'Bilibili', migu: 'Migu Music' }, timeAgo: { justNow: 'Just now', minutes: (minutes: number) => `${minutes}m ago`, hours: (hours: number) => `${hours}h ago`, days: (days: number) => `${days}d ago` }, selected: 'Selected', selectedItems: (count: number) => `${count} selected`, selectedSongs: (count: number) => `${count} songs`, preferredPlayTime: (name: string) => `Preferred: ${name}`, paginationItemName: 'songs', messages: { remarkVisibilityUpdated: 'Remark visibility updated', remarkApproved: 'Remark approved', remarkRejected: 'Remark rejected', deleteSuccess: 'Song deleted', batchDeleteSuccess: 'Songs deleted', rejectSuccess: 'Song rejected and requester notified', validatingUrl: 'Validating URL, please wait...', validatingCoverUrl: 'Validating cover URL, please wait...', validatingPlayUrl: 'Validating play URL, please wait...', updateSuccess: 'Song updated', addSuccess: 'Song added', durationRefreshed: 'Duration fetched and filled in' }, errors: { remarkVisibilityUpdateFailed: 'Failed to update remark visibility', markFailed: (message: string) => `Mark failed: ${message}`, deleteFailed: (message: string) => `Delete failed: ${message}`, batchDeleteFailed: (message: string) => `Batch delete failed: ${message}`, rejectReasonRequired: 'Please enter a rejection reason', rejectFailed: (message: string) => `Reject failed: ${message}`, remarkUpdateFailed: 'Failed to update remark approval status', songInfoRequired: 'Please enter song title and artist', coverUrlInvalidOrPending: 'Wait for cover URL validation or fix the invalid URL', playUrlInvalidOrPending: 'Wait for play URL validation or fix the invalid URL', updateFailed: 'Update failed', requesterRequired: 'Please select a requester', semesterRequired: 'Please select a semester', coverUrlInvalid: 'Invalid cover URL. Please check and try again', playUrlInvalid: 'Invalid play URL. Please check and try again', addFailed: 'Add failed', durationRefreshFailed: (message: string) => `Failed to refresh duration: ${message || 'unknown error'}`, durationInvalidRange: 'Duration must be between 30 and 3600 seconds', durationPlatformRequired: 'Fill in music platform and ID first to refresh duration' } }, dataAnalysis: { title: 'Data Center', desc: 'Understand interaction trends and influence behind campus voices', loadingDesc: 'Fetching the latest statistics...', loadFailed: 'Failed to Load Data', retryNow: 'Retry Now', retry: 'Retry', realtimeMode: 'Realtime', selectSemester: 'Select Semester', allSemesters: 'All Semesters', stable: 'Stable', currentActiveUsers: 'Current Active Users', onlineUnit: 'online', todayRequests: 'Today Requests', songUnit: 'songs', trendTitle: 'Request Trend Analysis', trendDesc: 'Submission volume over the last 7 days', loadingTrends: 'Loading trend data...', noTrendData: 'No trend data', topSongsTitle: 'Top Songs', likes: 'Likes', replays: 'Replays', times: 'Times', noData: 'No Data', activeUsersTitle: 'Active Users', activity: 'Activity', noActiveUsers: 'No active users', semesterComparisonTitle: 'Semester Comparison', currentSemester: 'Current Semester', historyBaseline: 'Historical Baseline', totalSongs: 'Total Songs', totalSchedules: 'Total Schedules', totalLikes: 'Total Likes', noComparisonData: 'No comparison data', activeUserDetails: 'Active User Details', noOnlineUsers: 'No online users', syncingRealtime: 'Syncing realtime data', countSongs: (count: number) => `${count} songs`, userActivity: (contributions: number, likes: number) => `${contributions} submissions · ${likes} likes`, onlineCount: (count: number) => `${count} online`, andMoreUsers: (count: number) => `and ${count} more users`, loadingSteps: ['Fetching semesters', 'Loading statistics', 'Fetching chart data', 'Loading realtime data'], kpi: { totalUsers: 'Registered Users', activeSongs: 'Active Song Library', scheduleDays: 'Schedule Days This Semester', totalRequests: 'Total Requests' }, messages: { updated: 'Data updated', refreshSuccess: 'Data refreshed' }, errors: { topSongs: 'Failed to load top songs', loadData: 'Failed to load data. Please try again later', stats: 'Failed to load statistics', trends: 'Failed to load trend data', activeUsers: 'Failed to load active users', userEngagement: 'Failed to load user engagement', semesterComparison: 'Failed to load semester comparison', init: 'Initialization failed. Please refresh the page', initNotify: 'Data initialization failed', retry: 'Retry failed' } }, schedulePrinter: { title: 'Print Schedule', desc: 'Customize print settings, preview, and export schedule sheets', noPermissionTitle: 'Permission Denied', noPermissionDesc: 'You do not have permission to print schedules. Contact an administrator for access.', settingsTitle: 'Print Settings', paperSize: 'Paper Size', selectPaperSize: 'Select Paper Size', orientation: 'Orientation', portrait: 'Portrait', landscape: 'Landscape', layoutStyle: 'Layout Style', classicList: 'Classic List', tableLayout: 'Table Layout', listColumns: 'Columns', oneColumn: '1 Column', twoColumns: '2 Columns', dateRange: 'Date Range', displayContent: 'Display Content', schoolLogo: 'School Logo', remark: 'Remark', remarkPlaceholder: 'Enter optional remarks', refreshPreview: 'Refresh Preview', print: 'Print', printing: 'Printing...', exportPdf: 'Export PDF', exportImage: 'Export Image', exporting: 'Exporting...', previewArea: 'Preview', noData: 'No Data', filtered: 'Filtered', printTitle: 'Broadcast Schedule', emptyTitle: 'No Schedule Data', emptyNoSchedules: 'Add schedules in Schedule Management before printing.', emptyFiltered: 'No schedules in the selected date range. Adjust the date range or schedule settings.', footer: 'Generated by VoiceHub | Broadcast Management System', songCount: (count: number) => `${count} songs`, countOnly: (count: number) => `${count} songs`, generatedAt: (time: string) => `Generated at: ${time}`, remarkPrefix: (remark: string) => `Remark: ${remark}`, dateRanges: { today: 'Today', tomorrow: 'Tomorrow', thisWeek: 'This Week', nextWeek: 'Next Week' }, contentOptions: { cover: 'Song Cover', title: 'Song Title', artist: 'Artist', requester: 'Requester', votes: 'Popularity', sequence: 'Play Order', playTime: 'Play Time' }, sequence: 'No.', replay: 'Replay', crossSemester: 'Cross-semester', requesterPrefix: 'Requester: ', replayRequestsPrefix: 'Replay requests: ', popularityPrefix: 'Popularity: ', replayCount: (count: number) => `${count} people`, replayCountShort: (count: number) => `Replay: ${count}`, popularityCountShort: (count: number) => `Popularity: ${count}`, allSchedules: 'All Schedules', rangeStart: 'Start', rangeEnd: 'End', rangeTo: (start: string, end: string) => `${start}\nto ${end}`, unspecifiedPlayTime: 'Unspecified Time', playTimeStarts: (name: string, time: string) => `${name} (starts at ${time})`, playTimeEnds: (name: string, time: string) => `${name} (ends at ${time})`, noSchedulesNotice: 'There are no schedules yet. Add one in Schedule Management first.', loadFailed: (message: string) => `Failed to load schedules: ${message}`, previewRefreshed: 'Preview refreshed', preparingPrint: 'Preparing to print...', printFailed: (message: string) => `Print failed: ${message}`, unknownError: 'Unknown error', previewNotFound: 'Preview content not found', preparingPdf: 'Preparing PDF...', pdfExported: 'PDF exported successfully', pdfExportFailed: (message: string) => `PDF export failed: ${message}`, imageExported: 'Image exported successfully', imageExportFailed: (message: string) => `Image export failed: ${message}`, segmentedExport: (count: number) => `Segmented export complete: ${count} images`, autoSegmenting: 'The schedule is too long and will be exported in sections' }, scheduleManager: { jumpToday: 'Jump to today', selectSpecificDate: 'Select a specific date', selectDate: 'Select Date', cancel: 'Cancel', confirm: 'Confirm', playTime: 'Play Time', loadingTitle: 'Loading schedule data', loadingMessage: 'Please wait...', pendingSongs: 'Pending Songs', poolList: 'Backup List', poolItemName: 'backup songs', addAllPending: 'Move all pending songs to backup pool', addCurrentPage: 'Move this page to backup pool', moveAllToPool: 'Move All', addSingleToPool: 'Add to Pool', alreadyInPool: 'Already in pool', removeFromPool: 'Remove from Pool', addToSchedule: 'Add to Schedule', poolEmptyHint: 'Backup pool is empty, click + in pending list to add', addAllPendingSuccess: (count) => `Moved ${count} song(s) to backup pool`, addAllPendingSkipped: (count) => `${count} song(s) could not be added`, addAllPendingFailed: 'Failed to move songs to pool', addAllPendingConfirmTitle: 'Move All to Pool', addAllPendingConfirmMessage: (count) => `Move all ${count} pending songs to the song pool?`, addCurrentPageConfirmTitle: 'Move This Page to Pool', addCurrentPageConfirmMessage: (count, page) => `Move ${count} song(s) from page ${page} to the backup pool?`, addCurrentPageSuccess: (count) => `Moved ${count} song(s) from this page to the backup pool`, addSingleToPoolSuccess: 'Added to backup pool', addSingleToPoolSkipped: 'Failed to add to backup pool', removeFromPoolSuccess: 'Removed from backup pool', removeFromPoolFailed: 'Failed to remove', addedBy: 'Added by', playSong: 'Play', removeFromSchedule: 'Remove from schedule', addToScheduleSuccess: 'Added to schedule', playlist: 'Playlist', playOrder: 'Play Order', expectedTotalDuration: 'Expected Total Duration', searchAndFilter: 'Search & Filter', searchPlaceholder: 'Search songs or artists...', currentSemester: 'Current Semester', preferredTime: 'Preferred Time', grade: 'Grade', sort: 'Sort', playlistFilter: 'Duplicate Filter', playlistFilterApplied: 'Filter Applied', viewRemark: 'View Remark', cardPending: 'Card Pending', cardCode: 'Request Card', view: 'View', rejectRequest: 'Reject Request', saveDraft: 'Save Draft', refreshDuration: 'Refresh song duration from platform', refreshAllDurations: 'Refresh all song durations', refreshPageDurations: 'Refresh this page song durations from platform', refreshCandidateDurations: 'Refresh candidate song durations from platform', downloadSongs: 'Download Songs', markAllPlayed: 'Mark All Played', moveDate: 'Move Date', copyDate: 'Copy Schedule', copyDateSourceRange: 'Source Range', copyDateTargetRange: 'Target Range', copyDateRangeStart: 'Start Date', copyDateRangeEnd: 'End Date', copyDateModeLabel: 'Copy Mode', singleDayCopy: 'Single Day', cycleCopy: 'Cycle', cycleCopySourceDate: 'Source Date', cycleCopyTargetStart: 'Target Start Date', cycleCopyTargetEnd: 'Target End Date', cycleCopyTargetSingleDate: 'Target Date', clearList: 'Clear List', publishSchedule: 'Publish Schedule', publishOnly: 'Publish Only', saveAndPublish: 'Save & Publish', addSongsHint: 'Add songs from the pending library', autoSchedule: 'Auto Schedule', autoScheduleTitle: 'Auto Schedule', targetDuration: 'Target Duration', targetDurationPlaceholder: 'Enter target duration (minutes)', targetSongCount: 'Target Song Count', targetSongCountPlaceholder: 'Optional; prioritize song count', songCountUnit: 'songs', targetRequesterCount: 'Target Requesters', targetRequesterCountPlaceholder: 'Optional; prioritize distinct requesters', requesterCountUnit: 'people', scheduleDirection: 'Duration Direction', directionUnder: 'Under Target', directionMiddle: 'Closest', directionOver: 'Over Target', availableCount: (count) => `${count} candidate song(s) under current filters`, autoScheduleRun: 'Schedule', resultTitle: 'Suggested Result', totalDuration: 'Total', resultUnderTarget: (diff) => `${Math.floor(diff / 60)}m ${diff % 60}s under target`, resultOverTarget: (diff) => `${Math.floor(diff / 60)}m ${diff % 60}s over target`, tryAgain: 'Adjust Again', confirmApply: 'Apply', previousPlan: 'Prev Plan', nextPlan: 'Next Plan', planIndicator: (current, total) => `Plan ${current} / ${total}`, scheduleAlgorithm: 'Algorithm', algorithmAuto: 'Auto', algorithmGreedy: 'Greedy', algorithmExhaustive: 'Exhaustive', newPlan: 'New Plan', fixExistingSelected: 'Fix Selected Songs', fixExistingCount: (n) => `${n} fixed`, fixExistingDuration: (sec) => `${Math.floor(sec / 60)}:${String(Math.floor(sec % 60)).padStart(2, '0')}`, fixExistingRemaining: (totalMin, fixedSec) => { const sec = Math.max(0, Math.floor(totalMin * 60 - fixedSec)) return `${Math.floor(sec / 60)}m ${String(Math.floor(sec % 60)).padStart(2, '0')}s left` }, fixExistingNone: 'No scheduled songs yet', replaySong: 'Replay Song', replay: 'Replay', draft: 'Draft', replayApplicants: 'Replay applicants: ', applicant: 'Applicant:', andMoreApplicants: (count: number) => ` and ${count} more`, publishThisDraft: 'Publish this draft', moveDateTitle: 'Move Schedule Date', copyDateTitle: 'Copy Schedule Date', currentDate: (date: string) => `Current date: ${date}`, nextStep: 'Next', replayDetailTitle: (title: string) => `${title} - Replay Request Details`, noReplayDetails: 'No detailed request records', allGrades: 'All', allSemesters: 'All', monthLabel: (month: number) => `Month ${month}`, playlistName: (id: string | number) => `Playlist ${id}`, playTimeStart: (time: string) => `Starts at ${time}`, playTimeEnd: (time: string) => `Ends at ${time}`, allDay: 'All day', positionShort: 'POS', weekdays: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'], timeAgo: { justNow: 'Just now', minutes: (minutes: number) => `${minutes}m ago`, hours: (hours: number) => `${hours}h ago`, days: (days: number) => `${days}d ago` }, emptySearch: 'No matching songs found', emptySongs: 'No song records', pendingSongItemName: 'pending songs', preferredPlayTime: (name: string) => `Preferred: ${name}`, tabs: { normal: 'Normal', replay: 'Replay Requests', all: 'All', pool: 'Backup Pool' }, allPlayTimes: 'All Times', unspecifiedPlayTime: 'Unspecified', noPlayTimeAllDay: 'No play time selected (all day)', sortOptions: { newest: 'Newest', oldest: 'Oldest', hotDesc: 'Most Popular', hotAsc: 'Least Popular' }, confirmations: { leaveUnsaved: 'You have unsaved schedule changes. Leave anyway?', switchDateUnsaved: 'You have unsaved schedule changes. Switching dates will discard them. Continue?', switchPlayTimeUnsaved: 'You have unsaved schedule changes. Switching play times will discard them. Continue?', switchSemesterUnsaved: 'You have unsaved schedule changes. Switching semesters will discard them. Continue?', switchPageUnsaved: 'You have unsaved schedule changes. Switching pages will discard them. Continue?', rejectReplayTitle: 'Reject Replay Request', rejectReplayMessage: 'Reject this replay request?', markAllPlayedTitle: 'Mark All Played', markAllPlayedMessage: 'Mark every song in the list as played?', markAllPlayedConfirm: 'Mark Played', clearListTitle: 'Clear Playlist', clearListMessage: 'Clear the current play order? Unsaved changes will be lost.', clearListConfirm: 'Clear', moveDateMessage: (sourceDate: string, count: number, targetDate: string) => `Move all ${count} songs from ${sourceDate} to ${targetDate}? Order and content will be preserved.`, moveDateConfirm: 'Move', copyDateMessage: (fromStart: string, fromEnd: string, sourceDays: number, toStart: string, toEnd: string, targetDays: number) => `Copy ${sourceDays} days from ${fromStart} to ${fromEnd} into ${targetDays} days from ${toStart} to ${toEnd}? Source will cycle to fill.`, copyDateOverwriteMessage: (fromStart: string, fromEnd: string, sourceDays: number, toStart: string, toEnd: string, targetDays: number, draftDays: number) => `Copy ${sourceDays} days from ${fromStart} to ${fromEnd} into ${targetDays} days from ${toStart} to ${toEnd}? Drafts on ${draftDays} target date(s) will be overwritten. Source will cycle to fill.`, copyDateConfirm: 'Copy', copyDateSingleMessage: (sourceDate: string, targetDate: string) => `Copy the schedule from ${sourceDate} to ${targetDate}?`, copyDateSingleConfirm: 'Copy', copyDateOverwriteDraftConfirm: (draftCount: number) => `${draftCount} draft(s) on target date will be overwritten. Continue?`, deleteScheduleTitle: 'Delete Schedule', deleteScheduleMessage: 'Delete all schedules for this day? This cannot be recovered.', deleteScheduleConfirm: 'Delete', publishScheduleTitle: 'Publish Schedule', publishScheduleMessage: 'Publish the current schedule? It will be visible immediately and notifications will be sent.', publishDraftTitle: 'Publish Draft', publishDraftMessage: (title: string) => `Publish draft "${title}"? It will be visible immediately and notifications will be sent.` }, messages: { remarkVisibilityUpdated: 'Remark visibility updated', replayRejected: 'Replay request rejected', allMarkedPlayed: 'All songs marked as played', playlistCleared: 'Playlist cleared. Remember to save changes', saveBeforeMove: 'Save current unpublished changes before moving schedules', saveBeforeCopy: 'Save current unpublished changes before copying schedules', moveDateSuccess: (count: number, date: string) => `Moved ${count} songs to ${date}`, copyDateSuccess: (count: number, toStart: string, toEnd: string) => `Copied ${count} songs to ${toStart} ~ ${toEnd}`, draftSaved: 'Schedule draft saved!', allDraftsDeleted: 'All drafts deleted!', scheduleDeleted: 'Schedule deleted!', schedulePublished: 'Schedule published and notifications sent!', draftPublished: (title: string) => `Draft "${title}" published and notifications sent!`, durationUpdated: 'Duration updated', durationFailed: 'Failed to get duration', durationNoPlatform: 'Song missing platform or music ID, cannot get duration', durationInvalid: 'Invalid duration. Use M:SS or H:MM:SS (max 2 hours)', durationInputPlaceholder: 'M:SS', editDuration: 'Click to edit duration', allDurationsUpdated: (count: number) => `${count} song(s) duration refreshed`, allDurationsPartial: (ok: number, fail: number) => `Refresh done: ${ok} succeeded, ${fail} failed`, allDurationsFailed: 'Bulk duration refresh failed', allDurationsSkipped: 'No songs need duration refresh', allDurationsCapped: 'Too many candidates, refreshing the first 100 only', candidateDurationsUpdated: (count: number) => `Refreshed duration for ${count} candidate song(s)`, candidateDurationsPartial: (ok: number, fail: number) => `Refresh done: ${ok} succeeded, ${fail} failed`, candidateDurationsFailed: 'Candidate duration refresh failed', candidateDurationsSkipped: 'No candidate songs need duration refresh', confirmAutoScheduleApplied: (count, duration) => `Auto-scheduled ${count} song(s), total ${duration}`, allDurationsProgressTotal: '{0} song(s)', allDurationsProgressWithCount: '{0}/{1} ({2} ok, {3} fail)', candidateDurationsProgressTotal: '{0} song(s)', allDurationsProgress: '{0} / {1}', autoScheduleNoResult: 'No matching song combination found', autoScheduleNoMorePlans: 'No more unique plans available' }, playlistFilterModal: { title: 'Playlist Duplicate Filter', defaultPlaylists: 'Preset Charts', customPlaylists: 'Custom Playlist ID / Link', refreshTitle: 'Refetch all custom playlist information', refresh: 'Refresh Info', playlistPlaceholder: 'Enter playlist ID or link', addCustomPlaylist: 'Add Custom Playlist', clearFilter: 'Clear Filter', fetching: 'Fetching...', applyFilter: 'Apply Filter', defaultNames: { soaring: 'Soaring Chart', newSongs: 'New Songs Chart', original: 'Original Chart', hotSongs: 'Hot Songs Chart' } }, remarkDialog: { title: 'Request Remark', publicRemark: 'Public Remark', adminOnly: 'Admin Only', updating: 'Updating...', statusPending: 'Pending', statusRejected: 'Rejected', approveButton: 'Approve', rejectButton: 'Reject' }, errors: { remarkVisibilityUpdateFailed: 'Failed to update remark visibility', rejectReplayFailed: (message: string) => `Failed to reject request: ${message}`, operationFailed: 'Operation failed', invalidTargetDate: 'Invalid target date. Use YYYY-MM-DD and ensure the date is valid', sameTargetDate: 'Target date cannot be the same as the current date', noMovableSongs: 'No movable songs on the current date', moveDateFailed: (message: string) => `Move failed: ${message}`, noCopyableSongs: 'No copyable songs on the current date', invalidDateRange: 'Invalid date range — start must not be later than end.', targetDateConflicts: 'Target range overlaps with source range dates.', copyDateFailed: (message: string) => `Copy failed: ${message}`, saveDraftFailed: (message: string) => `Failed to save draft: ${message}`, publishScheduleFailed: (message: string) => `Failed to publish schedule: ${message}`, publishDraftFailed: (message: string) => `Failed to publish draft: ${message}` } }, backupManager: { title: 'Database Backup', desc: 'Export and import database backups', exportTitle: 'Export Backup', exportDesc: 'Create a database backup file', exporting: 'Exporting...', startExport: 'Start Export', importTitle: 'Import Backup', importDesc: 'Restore data from a backup file', importing: 'Importing...', chooseFile: 'Choose File', createTitle: 'Create Database Backup', backupType: 'Backup Type', fullBackup: 'Full Backup (Recommended)', fullBackupDesc: 'Back up all data tables', userDataOnly: 'User Data Only', userDataOnlyDesc: 'Back up user-related data only', includeSystemSettings: 'Include System Settings', includeSystemSettingsDesc: 'Include system configuration and settings data', cancel: 'Cancel', creating: 'Creating...', createBackup: 'Create Backup', importFileTitle: 'Import Backup File', uploadTitle: 'Choose or Drag File', uploadDesc: 'Supports .json backup files up to 100MB', restoreMode: 'Restore Mode', mergeMode: 'Merge Mode (Recommended)', mergeModeDesc: 'Update existing records and add new ones', replaceMode: 'Replace Mode', replaceModeDesc: 'Clear data first, then import the backup', confirmClear: 'I confirm clearing data', confirmClearDesc: 'This action cannot be undone. Proceed carefully.', overwriteSuperAdmin: 'Overwrite super admin from backup', overwriteSuperAdminDesc: 'When disabled, the current super admin, third-party bindings, and 2FA data are preserved', warningTitle: 'Warning', warningDesc: 'Importing a backup affects existing data. Make sure you understand the consequences.', startImport: 'Start Import', downloaded: (filename: string, size: string) => `Backup file downloaded: ${filename}${size}`, fileDownloadFailed: (message: string) => `File download failed: ${message}`, invalidResponse: 'Backup creation failed: invalid response format', serverResponseInvalid: 'Backup creation failed: invalid server response format', createFailed: (message: string) => `Failed to create backup: ${message}`, parseFailed: 'Unable to parse backup file. Please check the file format.', restoreSuccess: (tables: number, records: number) => `Backup restored successfully. Processed ${tables} tables and restored ${records} records.`, restoreWarning: (count: number) => `${count} errors occurred during restore`, restoreCompleteRedirect: 'Database restore completed. Returning home to sign in again in 3 seconds.', importFailed: (message: string) => `Backup import failed: ${message}`, unknownError: 'Unknown error' }, oauthConfig: { title: 'OAuth Third-Party Login Configuration', runtimeConfigNotice: 'OAuth runtime configuration is managed in the admin panel. Environment variables are retained only for legacy deployments and one-click import; imported values must still be saved here.', baseSettings: 'Basic Settings', importEnv: 'Import Env Config', allowRegistration: 'Allow Third-Party Registration', allowRegistrationDesc: 'When enabled, users can create new accounts through OAuth. When disabled, OAuth can only bind existing accounts.', oauthRegisterRequiresApproval: 'Require Admin Approval for Third-party Registration', oauthRegisterRequiresApprovalDesc: 'When enabled, accounts created through third-party login require approval before sign-in.', allowed: 'Allowed', notAllowed: 'Not Allowed', redirectUri: 'OAuth Redirect URI', redirectExample: 'Example:', stateSecret: 'OAuth State Secret', stateSecretDesc: 'Used to encrypt/decrypt OAuth state parameters. A strong random string is recommended.', stateSecretPlaceholder: 'Enter a strong random string', show: 'Show', hide: 'Hide', enabled: 'Enabled', disabled: 'Disabled', casdoorServerUrl: 'Casdoor Server URL', casdoorOrganizationName: 'Casdoor Organization Name', organizationPlaceholder: 'Enter organization name', customOAuth: 'Third-Party OAuth2', displayName: 'Button Display Name', displayNamePlaceholder: 'e.g. Campus SSO', authorizeUrl: 'Authorize URL', tokenUrl: 'Token URL', userInfoUrl: 'UserInfo URL', scope: 'Scope', clientId: 'Client ID', clientSecret: 'Client Secret', githubTitle: 'GitHub OAuth', githubClientId: 'GitHub Client ID', githubClientSecret: 'GitHub Client Secret', casdoorTitle: 'Casdoor OAuth', casdoorClientId: 'Casdoor Client ID', casdoorClientSecret: 'Casdoor Client Secret', googleTitle: 'Google OAuth', googleClientId: 'Google Client ID', googleClientSecret: 'Google Client Secret', clientIdPlaceholder: 'Enter Client ID', clientSecretPlaceholder: 'Enter Client Secret', githubClientIdPlaceholder: 'Enter GitHub Client ID', githubClientSecretPlaceholder: 'Enter GitHub Client Secret', googleClientIdPlaceholder: 'Enter Google Client ID (xxx.apps.googleusercontent.com)', googleClientSecretPlaceholder: 'Enter Google Client Secret', aggregateTitle: 'Aggregate OAuth', aggregateLoginTypeLabel: 'Login Methods', aggregateLoginTypePlaceholder: 'Select at least one login method', aggregateLoginTypeDesc: 'Select only the login methods that your aggregate login service has enabled. Enabling a platform the provider does not support will cause authorization failures. Each login method keeps its own account binding.', aggregateEndpointLabel: 'Endpoint URL', aggregateEndpointDesc: 'The connect.php endpoint for a compatible aggregate OAuth service. Use HTTPS on public networks; HTTP is acceptable only on trusted private networks.', aggregateClientIdPlaceholder: 'Enter aggregate OAuth AppID', aggregateClientSecretPlaceholder: 'Enter aggregate OAuth AppKey', aggregateDocLabel: 'View aggregate OAuth documentation', aggregateLoginTypes: { qq: 'QQ', wx: 'WeChat', alipay: 'Alipay', sina: 'Weibo', baidu: 'Baidu', douyin: 'Douyin', huawei: 'Huawei', xiaomi: 'Xiaomi', gitee: 'Gitee', gitea: 'Gitea', bilibili: 'Bilibili', kuaishou: 'Kuaishou' }, clearBindingsTitle: 'Clear All Aggregate Bindings', clearBindingsDesc: 'Use when changing provider. Clears all aggregate login bindings and requires users to rebind.', clearBindingsBtn: 'Clear Bindings', clearBindingsConfirmTitle: 'Clear all aggregate OAuth bindings?', clearBindingsConfirmMessage: 'This will permanently remove all aggregate OAuth bindings for all users.\nIf the provider has changed, old data is invalid and users will need to rebind before using aggregate login.\nThis action cannot be undone. Continue?', clearBindingsSuccess: 'Cleared {0} aggregate bindings ({1} users affected)', clearBindingsFailed: 'Failed to clear aggregate OAuth bindings', userIdField: 'User ID Field', usernameField: 'Username Field', nameField: 'Name Field', emailField: 'Email Field', avatarField: 'Avatar Field', brokerHintPrefix: 'When deploying multiple environments on platforms such as Vercel, providers like GitHub OAuth App usually support only one callback URL. To solve this, the project can use', brokerHintSuffix: 'as a unified callback entry. The broker can dynamically forward callback requests to the correct deployment environment according to the state parameter, allowing one OAuth App to support unlimited preview and production environments. See the project documentation for details.', importFailed: 'Failed to import environment configuration', logs: { fetchEnvFailed: 'Failed to fetch environment variables:', importEnvFailed: 'Failed to import environment configuration:' } }, musicSourceController: { title: 'Music Source Control', description: 'Manage music platform switches and search order. Changes take effect immediately.', saveConfig: 'Save Config', saving: 'Saving...', saved: 'Saved', reset: 'Reset', loading: 'Loading...', fetchFailed: 'Failed to fetch config', saveFailedRetry: 'Save failed, please retry', saveSuccess: 'Source config saved', switchTitle: 'Platform Switch', switchDesc: 'Enable or disable each music platform for search', orderTitle: 'Search Order', orderDesc: 'Drag to reorder platform buttons in the search area', resetOrder: 'Reset Order', mustKeepOne: 'At least one platform must stay enabled' }, playTimeManager: { title: 'Play Time Management', desc: 'Define regular broadcast windows. Users can request songs or view schedules within these periods.', enableSelection: 'Enable Time Selection', add: 'Add Play Time', aboutTitle: 'About Time Limits', aboutDesc: 'When global play time selection is enabled, requests are only open during configured periods. Empty start or end times are treated as unlimited boundaries.', loading: 'Loading...', fetchFailed: 'Failed to fetch data', emptyTitle: 'No Play Times', emptyDesc: 'Click “Add Play Time” to create the first play time', enabled: 'Enabled', disabled: 'Disabled', enable: 'Enable', disable: 'Disable', unlimited: 'Unlimited', noDescription: 'No detailed description...', editTitle: 'Edit Play Time', addTitle: 'Add Play Time', name: 'Time Name', namePlaceholder: 'e.g. Noon Broadcast', startTimeOptional: 'Start Time (Optional)', startTimeHint: 'Leave blank for no start-time limit', endTimeOptional: 'End Time (Optional)', endTimeHint: 'Leave blank for no end-time limit', descriptionOptional: 'Description (Optional)', descriptionPlaceholder: 'Enter play time description...', enableThisPlayTime: 'Enable this play time', cancel: 'Cancel', saving: 'Saving...', saveSettings: 'Save Settings', deleting: 'Deleting...', confirmDelete: 'Confirm Delete', deleteConfirmTitle: (name: string) => `Delete play time "${name}"?`, deleteConfirmDesc: 'This cannot be undone. Related song request and schedule play-time settings may be affected or cleared.', messages: { systemSettingsUpdated: 'System settings updated', playTimeEnabled: 'Play time enabled', playTimeDisabled: 'Play time disabled', playTimeDeleted: 'Play time deleted', playTimeUpdated: 'Play time updated', playTimeCreated: 'Play time created' }, errors: { adminOnly: 'Only admins can manage play times', fetchPlayTimesFailed: 'Failed to fetch play times', updateSystemSettingsFailed: 'Failed to update system settings', updatePlayTimeStatusFailed: 'Failed to update play time status', deletePlayTimeFailed: 'Failed to delete play time', startBeforeEnd: 'Start time must be earlier than end time', nameRequired: 'Time name is required', nameExists: 'A play time with this name already exists', savePlayTimeFailed: 'Failed to save play time' } }, requestTimeManager: { title: 'Request Window Management', desc: 'Manage request opening times, quantity limits, and global submission status', currentStatus: 'Current Status', open: 'Open', closed: 'Closed', enableRequest: 'Enable Requests', enableTimeLimit: 'Enable Time Limits', addTime: 'Add Request Window', addTimeDesc: 'Create a new request opening period', expired: 'Expired', enabled: 'Enabled', disabled: 'Disabled', none: 'None', enable: 'Enable', disable: 'Disable', unlimited: 'Unlimited', noDescription: 'No detailed description...', progress: 'Request Progress', editTitle: 'Edit Request Window', addTitle: 'Add Request Window', name: 'Window Name', namePlaceholder: 'e.g. 2025 Winter Regular Requests', startDateTime: 'Start Date & Time', endDateTime: 'End Date & Time', expectedCount: 'Planned Request Count', expectedCountShort: 'planned count', songUnit: 'songs', expectedHint: 'Set to 0 for no total request limit', enableThisRequestTime: 'Enable this request window', enableThisRequestTimeHint: 'Only enabled windows apply to time restrictions', descriptionOptional: 'Window Description (Optional)', descriptionPlaceholder: 'Enter special rules or notes for this request period...', cancel: 'Cancel', saving: 'Saving...', saveSettings: 'Save Settings', keepTime: 'Keep Window', deleting: 'Deleting...', confirmDelete: 'Confirm Delete', deleteConfirmTitle: (name: string) => `Remove "${name}" permanently?`, deleteConfirmPrefix: 'Deleting this window may invalidate request associations from this period. Consider', deleteConfirmSuffix: 'this window first.', priorityTitle: 'Logic Priority', globalSwitch: 'Global Switch', priorityGlobal: 'has the highest priority. When off, all requests are unavailable.', timeLimit: 'time limits', priorityTimeLimitPrefix: 'When', priorityTimeLimitMiddle: 'are enabled, the system checks whether the current time is within any', priorityTimeLimitAnd: 'window that has not reached its', priorityTimeLimitSuffix: '.', priorityNoLimit: 'If time limits are off, requests are open all day as long as the global switch is on.', noticeTitle: 'Notes', noticeOverlap: 'Overlapping windows are merged by the system.', noticeUnlimited: 'When planned request count is 0, the quantity limit is ignored.', noticeExpired: 'Expired windows are shown as “Expired” automatically and cannot be edited or re-enabled.', enableRestriction: 'Enable Duplicate Restriction', restrictionScope: 'Scope', restrictionScopeSelf: 'Self', restrictionScopeAll: 'Everyone', restrictionScopeDesc: 'Which users\' scheduled records are compared against', sameSongRestrictionHours: 'Same Song Window', sameSongRestrictionHoursDesc: 'Window during which the same song cannot be resubmitted after entering the schedule. Empty means no cooldown window. Max 720h (30 days)', sameArtistRestrictionHours: 'Same Artist Window', sameArtistRestrictionHoursDesc: 'Window during which the same artist cannot be resubmitted after entering the schedule. Empty means no cooldown window. Max 720h (30 days)', restrictionUnit: 'h', restrictionPanelTitle: 'Duplicate Restriction', restrictionPanelDesc: 'Set a cooldown window for songs or artists that have entered the schedule', restrictionPanelHint: 'Off removes all duplicate-submission limits, so users may resubmit scheduled or played songs. On: scope applies to both checks, and leaving both windows empty keeps the current-semester no-duplicate-song rule', restrictionSaveSuccess: 'Restriction settings saved', restrictionSaveFailed: 'Failed to save restriction settings', stats: { activeSlots: 'Active Windows', totalAccepted: 'Total Accepted', totalCapacity: 'Planned Capacity', remainingCapacity: 'Remaining Capacity' }, errors: { adminOnly: 'Only admins can manage request windows', fetchFailed: 'Failed to fetch request windows', fetchFailedWithStatus: (status: number) => `Failed to fetch request windows: ${status}`, updateSystemSettingsFailed: 'Failed to update system settings', updateSystemSettingsFailedWithStatus: (status: number) => `Failed to update system settings: ${status}`, updateStatusFailed: 'Failed to update request window status', updateStatusFailedWithStatus: (status: number) => `Failed to update request window status: ${status}`, deleteFailed: 'Failed to delete request window', deleteFailedWithStatus: (status: number) => `Failed to delete request window: ${status}`, startBeforeEnd: 'Start time must be earlier than end time', nameRequired: 'Window name is required', nameExists: 'A request window with this name already exists', saveFailed: 'Failed to save request window', saveFailedWithStatus: (isUpdate: boolean, status: number) => `${isUpdate ? 'Update' : 'Create'} request window failed: ${status}` } }, semesterManager: { title: 'Semester Management', desc: 'Define academic periods. Requests, schedules, and statistics are archived under the active semester.', add: 'Add Semester', currentActive: 'Current Active Semester', createdAt: (date: string) => `Created at ${date}`, inProgress: 'In Progress', noCurrent: 'No active semester set', createNow: 'Create Now', switchTitle: 'Semester Switching Notes', switchDesc: 'After switching semesters, the public request page clears old schedules. Historical data is archived under each semester and remains visible in the data center.', historyTitle: 'Semester History', historyCount: (count: number) => `${count} records`, loading: 'Loading...', currentSemester: 'Current Semester', setCurrent: 'Set Current', editSemester: 'Edit Semester', deleteSemester: 'Delete Semester', empty: 'No semester data', semesterName: 'Semester Name', namePlaceholder: 'e.g. 2025 Academic Year First Semester', setAsActive: 'Set as current active semester', setAsActiveHint: 'This semester becomes active immediately after creation, and the previous semester becomes historical.', namingTip: 'Tip: Use a consistent semester naming convention to make archived data easier to search and analyze.', cancel: 'Cancel', saving: 'Saving...', creating: 'Creating...', saveChanges: 'Save Changes', createSemester: 'Create Semester', confirmDelete: 'Confirm Delete', deleteDialogTitle: 'Confirm Semester Deletion', deleteMessage: (name: string) => `Delete "${name}" permanently? This also removes all song requests, schedules, and statistics under this semester. Deleted data cannot be recovered.`, terms: { first: 'First Semester', second: 'Second Semester' }, recommendedName: (academicYear: number, term: string) => `${academicYear} Academic Year ${term}`, messages: { activeSet: 'Active semester set successfully.', deleted: 'Semester deleted successfully.', nameUnchanged: 'Semester name did not change', updated: 'Semester updated successfully.', created: 'Semester created successfully.' }, errors: { setActiveFailedRetry: 'Failed to set active semester. Please try again later.', setFailed: 'Set failed', deleteFailed: 'Failed to delete semester', deleteFailedShort: 'Delete failed', updateFailed: 'Failed to update semester', createFailed: 'Failed to create semester' } }, smtpManager: { title: 'Mail Service Configuration', desc: 'Configure SMTP for verification codes, request notifications, and other important reminders', reload: 'Reload SMTP', reloading: 'Reloading...', save: 'Save Config', saving: 'Saving...', serviceConfig: 'Service Config', smtpHost: 'SMTP Server', port: 'Port', secure: 'Encryption', username: 'Sender Account', fromEmail: 'From Email', fromEmailHint: 'Usually the same as the sender account; some providers require them to match.', password: 'Auth Code / Password', fromName: 'Sender Name', serviceTest: 'Service Test', testEmail: 'Test Recipient Email', testEmailPlaceholder: 'Enter test email address', send: 'Send', sending: 'Sending', sent: 'Sent', secureOptions: { ssl: 'SSL/TLS', none: 'None' }, defaultFromName: 'Campus Radio', loadFailed: 'Failed to load configuration', incompleteConfig: 'Please complete the SMTP configuration', saveSuccess: 'SMTP configuration saved', saveFailed: 'Failed to save configuration', resetSuccess: 'Configuration reset', reloadSuccess: 'SMTP configuration reloaded', reloadFailed: 'Failed to reload SMTP configuration', serverRequired: 'Please configure the SMTP server first', testConnectionFailed: 'Connection test failed', testEmailRequired: 'Please enter a test email address', testEmailSuccess: 'Test email sent', testEmailFailed: 'Failed to send test email', serverMessages: { methodNotAllowed: { raw: '方法不被允许', text: 'Method not allowed' }, unauthorized: { raw: '未授权访问', text: 'Unauthorized access' }, adminReloadOnly: { raw: '只有管理员才能重载SMTP配置', text: 'Only administrators can reload SMTP configuration' }, adminTestConnectionOnly: { raw: '只有管理员才能测试SMTP连接', text: 'Only administrators can test SMTP connections' }, adminTestEmailOnly: { raw: '只有管理员才能发送测试邮件', text: 'Only administrators can send test emails' }, smtpDisabled: { raw: 'SMTP服务未启用', text: 'SMTP service is disabled' }, incompleteConfig: { raw: '请填写完整的SMTP配置信息', text: 'Please complete the SMTP configuration' }, testEmailRequired: { raw: '请输入测试邮箱地址', text: 'Please enter a test email address' }, reloadSuccess: { raw: 'SMTP配置已重载', text: 'SMTP configuration reloaded' }, reloadCleared: { raw: 'SMTP未启用或配置不完整,已清空当前SMTP实例', text: 'SMTP is disabled or incomplete; the current SMTP instance has been cleared' }, reloadFailed: { raw: 'SMTP配置重载失败', text: 'Failed to reload SMTP configuration' }, invalidOrDisabled: { raw: 'SMTP配置无效或未启用', text: 'SMTP configuration is invalid or disabled' }, initializeFailed: { raw: '初始化SMTP配置失败', text: 'Failed to initialize SMTP configuration' }, connectionSuccess: { raw: 'SMTP连接测试成功', text: 'SMTP connection test succeeded' }, connectionFailed: { raw: 'SMTP连接测试失败', text: 'SMTP connection test failed' }, testConnectionFailed: { raw: '测试连接失败', text: 'Connection test failed' }, testEmailSuccess: { raw: '测试邮件发送成功', text: 'Test email sent' }, testEmailSendFailed: { raw: '测试邮件发送失败', text: 'Failed to send test email' }, sendTestEmailFailed: { raw: '发送测试邮件失败', text: 'Failed to send test email' } } }, emailTemplateManager: { title: 'Email Template Management', desc: 'Customize system email subjects and content with variable substitution', refreshPreview: 'Refresh Preview', livePreview: 'Live Preview', saving: 'Saving...', updateTemplate: 'Update Template', searchPlaceholder: 'Search templates...', builtin: 'Built-in', custom: 'Custom', templateName: 'Template Name', emailSubject: 'Email Subject', supportsVariables: 'Supports Variables', subjectPlaceholder: 'Enter email subject...', availableVariables: 'Available Variables', htmlContent: 'HTML Content', restoreDefault: 'Restore Default', livePreviewLabel: 'Live Preview:', hidePreview: 'Hide Preview', emptySelectTemplate: 'Select an email template on the left', previewData: { name: 'Alex', title: 'System Notification', message: 'This is preview content\nLine breaks and links are supported' }, messages: { saved: 'Template saved', restored: 'Default template restored' }, errors: { saveFailed: 'Failed to save', restoreFailed: 'Failed to restore', previewFailed: 'Preview failed' } }, notificationSender: { title: 'Send Notifications', desc: 'Instantly push announcements, activity updates, or important reminders to selected groups', notificationTitle: 'Notification Title', titlePlaceholder: 'Enter notification title', notificationContent: 'Notification Content', contentPlaceholder: 'Enter notification details...', markdownHint: 'Markdown is supported and rendered safely in the preview.', importantToggleTitle: 'Important Pop-up', importantToggleDescription: 'Shown in a pop-up after sign-in and marked as read when closed', scope: 'Notification Scope', allUsersHint: 'This notification will be sent to all active registered users.', selectGrade: 'Select Grade', grade: 'Grade', className: 'Class', selectGradePlaceholder: 'Select grade', selectClassPlaceholder: 'Select class', add: 'Add', noClassSelected: 'No classes selected', userSearchPlaceholder: 'Search and select users (name, username, or ID)...', searchResults: (count: number) => `Search Results (${count})`, selected: 'Selected', select: 'Select', selectedUsers: (count: number) => `Selected Users (${count})`, clearAll: 'Clear All', noUsersSelected: 'No users selected', send: 'Send', sending: 'Sending...', previewTitle: 'Notification Preview', previewSender: 'Administrator', senderLabel: 'Sender', previewTitlePlaceholder: 'Notification title', previewContentPlaceholder: 'Notification content preview will appear here...', previewScope: (scope: string) => `Scope: ${scope}`, previewHint: 'The preview is for layout reference only. Actual rendering may vary by device language and screen size.', roles: { admin: 'Admin', teacher: 'Teacher', student: 'Student' }, scopeDescriptions: { all: 'All users', grade: (grade: string) => `Grade ${grade}`, class: (grade: string, className: string) => `Grade ${grade} Class ${className}`, multiClass: (count: number) => `${count} classes`, specificUsers: (count: number) => `${count} users selected`, selectGrade: 'Select a grade', selectClass: 'Select a class', selectUsers: 'Select users' }, messages: { sendSuccess: (count: number) => `Notification sent to ${count} users` }, errors: { adminOnly: 'Only admins can send system notifications', incomplete: 'Please complete all required fields', sendFailed: 'Failed to send notification', sendError: 'An error occurred while sending the notification' }, targets: { all: 'All Users', grade: 'By Grade', class: 'By Class', multiClass: 'Multiple Classes', specificUsers: 'Specific Users' }, history: { title: 'Sending History', description: 'Each send appears once; open its details to review recipient read status', refresh: 'Refresh', refreshing: 'Refreshing...', searchPlaceholder: 'Search notification title or content', typeFilter: 'Notification Type', allTypes: 'All Types', senderFilter: 'Sender', allSenders: 'All Senders', sentAtSort: 'Sort by Sent Time', newestFirst: 'Newest First', oldestFirst: 'Oldest First', clearFilters: 'Clear Filters', statusFilter: 'Filter by read status', all: 'All', read: 'Read', unread: 'Unread', notification: 'Notification', sender: 'Sender', systemSender: 'System', recipient: 'Recipient', recipientCountLabel: 'Recipients', recipientCount: (count: number) => `${count} recipients`, type: 'Type', status: 'Status', sentAt: 'Sent At', readAt: 'Read At', actions: 'Actions', viewDetails: 'View Details', view: 'View', edit: 'Edit', delete: 'Delete', detailsEyebrow: 'Recipient Read Status', close: 'Close', editEyebrow: 'Edit Sending Record', editNotification: 'Edit Notification', editTitle: 'Notification Title', editTitlePlaceholder: 'Enter a notification title', editContent: 'Notification Content', editContentPlaceholder: 'Enter Markdown notification content', editImportant: 'Important Notification Pop-up', editImportantDescription: 'Saving creates a new batch: all recipients (including those who read or deleted it) are reset to unread and see the pop-up again', preview: 'Markdown Preview', cancel: 'Cancel', save: 'Save Changes', saving: 'Saving...', deleteNotification: 'Delete Notification', deleteConfirmation: (title: string) => `Delete “${title}”? The notification will be removed for every recipient and cannot be recovered.`, confirmDelete: 'Delete', titleContentRequired: 'Notification title and content are required', titleTooLong: (max: number) => `Notification title cannot exceed ${max} characters`, contentTooLong: (max: number) => `Notification content cannot exceed ${max} characters`, updateSuccess: 'Notification updated', updateFailed: 'Failed to update notification. Please try again.', deleteSuccess: 'Notification deleted', deleteFailed: 'Failed to delete notification. Please try again.', important: 'Important', normal: 'Standard', untitled: 'Untitled notification', notRead: 'Not read yet', loading: 'Loading sending history...', detailsLoading: 'Loading recipient details...', empty: 'No sending history', emptyDescription: 'Each system notification send will appear here as one record.', filteredEmpty: 'No sending records match these filters', filteredEmptyDescription: 'Adjust the keyword or filters and try again.', noRecipients: 'No recipients match the current filter', loadFailed: 'Failed to load notification history. Please try again.', detailsLoadFailed: 'Failed to load recipient read details. Please try again.', retry: 'Retry', unknownTime: 'Unknown time', unknownUser: (id: number) => `User #${id}`, userId: (id: number) => `User ID: ${id}`, itemName: 'sending records', recipientItemName: 'recipients' } }, cardCodesManager: { title: 'Request Card Management', desc: 'Manage request card creation, filtering, bulk redemption, import/export, and notes', refresh: 'Refresh', exporting: 'Exporting...', searchPlaceholder: 'Search request cards or notes', status: 'Status', allStatus: 'All Status', clearFilters: 'Clear Filters', selectedItems: (count: number) => `${count} selected`, applyBulk: 'Apply Bulk', cancelSelection: 'Cancel Selection', loading: 'Loading request cards...', cardCode: 'Request Card', note: 'Note', time: 'Time', actions: 'Actions', copy: 'Copy request card', lockedAt: (time: string) => `Locked: ${time}`, redeemedAt: (time: string) => `Redeemed: ${time}`, saveNote: 'Save Note', createdAt: (time: string) => `Created: ${time}`, updatedAt: (time: string) => `Updated: ${time}`, available: 'Available', locked: 'Locked', redeemed: 'Redeemed', invalid: 'Invalid', lockedStatus: 'Locked', redeemedStatus: 'Redeemed', invalidStatus: 'Invalid', setAvailable: 'Set Available', setLocked: 'Set Locked', setRedeemed: 'Set Redeemed', setInvalid: 'Set Invalid', unknown: 'Unknown', empty: 'No matching request cards found', itemName: 'request cards', createTitle: 'Bulk Create / Import', createDesc: 'Paste separated codes manually or generate request cards automatically', manualImport: 'Manual Import', autoGenerate: 'Auto Generate', manualPlaceholder: 'Enter one request card, or separate multiple cards with commas, newlines, or spaces', generateCount: 'Count', prefix: 'Prefix', prefixPlaceholder: 'e.g. VH-', randomLength: 'Random Length', charset: 'Character Set', bulkNotePlaceholder: 'Optional: bulk note', createCardCode: 'Create Request Cards', fillDemo: 'Fill Demo', demoNote: 'Demo request cards', lastGeneratedTitle: (count: number) => `${count} request cards generated this time`, lastGeneratedDesc: 'Only the latest auto-generated result is kept so you can copy it before distribution.', copyAll: 'Copy All', quickTipsTitle: 'Quick Tips', quickTips: [ 'Filter by status and keyword; search covers request cards and notes.', 'Switch single or bulk statuses to lock, redeem, or invalidate cards.', 'Edit notes in place to record recipients, batches, or source details.', 'Export generates a complete CSV from current filters; selected cards are exported first.' ], redeemLogsTitle: 'Redeem Logs', redeemLogsDesc: 'Track which account redeemed each request card, when, from where, and related songs', refreshLogs: 'Refresh Logs', keyword: 'Keyword', logSearchPlaceholder: 'Request card / account / song / source', source: 'Source', allSources: 'All Sources', redeemer: 'Redeemer', redeemerPlaceholder: 'Username / display name', startDate: 'Start Date', endDate: 'End Date', queryLogs: 'Search Logs', clearConditions: 'Clear Conditions', loadingLogs: 'Loading redeem logs...', logId: 'Log ID', redeemedTime: 'Redeemed Time', relatedSong: 'Related Song', unknownUser: 'Unknown User', noRedeemLogs: 'No redeem logs', exportSelected: (count: number) => `Export ${count} selected`, exportFiltered: 'Export Filtered', sources: { adminManual: 'Manual Redeem', admin: 'Admin Action', scheduleAuto: 'Schedule Auto', scheduleRemove: 'Schedule Remove', schedule: 'Schedule', withdraw: 'Withdraw' }, messages: { fetchFailed: 'Failed to load request cards', fetchLogsFailed: 'Failed to load request card redeem logs', copied: 'Request card copied', copyFailed: 'Copy failed. Please copy manually', exportFailed: 'Failed to export request cards', exportSelectedSuccess: 'Selected request cards exported', exportFilteredSuccess: 'Filtered results exported', statusUpdated: 'Status updated', updateStatusFailed: 'Failed to update request card status', noteSaved: 'Note saved', saveNoteFailed: 'Failed to save note', inputRequired: 'Please enter request cards first', createSuccess: (count: number) => `Created ${count} request cards`, createDone: (inserted: number, skipped: number) => `Created ${inserted}; skipped ${skipped} duplicates`, createFailed: 'Failed to create request cards', noGeneratedToCopy: 'No generated request cards to copy', copiedGenerated: (count: number) => `Copied ${count} request cards` }, stats: { total: 'Total', all: 'All', available: 'Available', unused: 'Unused', locked: 'Locked', pendingRedeem: 'Pending', redeemed: 'Redeemed', completed: 'Completed' } }, databaseManager: { title: 'Database Operations', desc: 'Run low-level maintenance tasks, including backup, restore, and global data reset', highRisk: 'High Risk', executing: 'Running...', maintenanceTitle: 'Database Maintenance Advice', maintenanceDesc: 'Create a full backup weekly. Before restoring a backup or resetting the database, create a fresh backup to avoid irreversible data loss.', backupTitle: 'Create Database Backup', restoreTitle: 'Restore Database Backup', selectContent: 'Select Content', backupHint: 'The backup file will be generated as .json and downloaded automatically', cancel: 'Cancel', exporting: 'Exporting...', startExport: 'Start Export', selectFile: 'Click or drag a backup file here', fileHint: 'Only .json files exported by VoiceHub are supported', restoreMode: 'Restore Mode', restoreModes: { merge: { title: 'Incremental Mode', desc: 'Import only new non-duplicate records' }, replace: { title: 'Replace Mode', desc: 'Clear existing tables before full restore' } }, overwriteSuperAdmin: 'Overwrite super admin from backup', overwriteSuperAdminDesc: 'When disabled, the current super admin account plus third-party bindings and 2FA data are kept.', replaceWarning: 'Warning: replace mode permanently clears matching tables in the current database. Existing sessions will be interrupted.', restoring: 'Restoring...', confirmRestore: 'Confirm & Restore', resetSequenceTitle: 'Reset Table Sequences', selectTargetTable: 'Select Target Table', sequenceHelpTitle: 'What is sequence reset?', sequenceHelpDesc: 'If table IDs have gaps or stop auto-incrementing after manual database operations, resetting sequences updates the database counters to current max ID + 1. This resolves write failures caused by ID conflicts without modifying existing data.', resetting: 'Resetting...', executeReset: 'Run Reset', dangerResetTitle: 'Dangerous Operation: Reset Database', dangerResetHeading: 'You are performing an extremely dangerous operation!', dangerResetPrefix: 'The reset permanently deletes all', dangerResetScope: 'songs, submissions, schedules, notifications, logs, and user accounts except yours', confirmCodeLabel: 'Enter the following code to confirm', confirmCodePlaceholder: 'Enter the code above...', confirmResetDatabase: 'Confirm Reset', cards: { backup: { title: 'Create Backup', desc: 'Export all current database data to a file for backup or migration.', button: 'Create Backup File' }, restore: { title: 'Restore Backup', desc: 'Restore system data from a previously exported backup file.', button: 'Select Backup File' }, resetSeq: { title: 'Reset Sequences', desc: 'Fix table auto-increment ID sequences so new records start from the correct values.', button: 'Start Sequence Reset' }, resetDb: { title: 'Reset Database', desc: 'Clear all system data except administrator accounts. This cannot be undone.', button: 'Reset Database Now' }, autoBackup: { title: 'Auto Backup', desc: 'Configure multiple backup methods, triggered externally for scheduled auto backup', button: 'Configure Auto Backup' } }, backupOptions: { songs: { label: 'Songs & Schedules', desc: 'Includes song library, user submissions, and historical broadcast schedules' }, system: { label: 'System Configuration', desc: 'Includes site settings, blacklist, play times, and global parameters' }, users: { label: 'User Accounts', desc: 'Includes registered user permissions and preferences, excluding sensitive admin data' } }, tableOptions: [ { label: 'Reset All Tables (All)', value: 'all' }, { label: 'Song Table (Song)', value: 'Song' }, { label: 'User Table (User)', value: 'User' }, { label: 'User Identity Table (UserIdentity)', value: 'UserIdentity' }, { label: 'User Status Log Table (UserStatusLog)', value: 'UserStatusLog' }, { label: 'Vote Table (Vote)', value: 'Vote' }, { label: 'Schedule Table (Schedule)', value: 'Schedule' }, { label: 'Notification Table (Notification)', value: 'Notification' }, { label: 'Notification Settings Table (NotificationSettings)', value: 'NotificationSettings' }, { label: 'Play Time Table (PlayTime)', value: 'PlayTime' }, { label: 'Semester Table (Semester)', value: 'Semester' }, { label: 'System Settings Table (SystemSettings)', value: 'SystemSettings' }, { label: 'Song Blacklist Table (SongBlacklist)', value: 'SongBlacklist' }, { label: 'Song Replay Request Table (SongReplayRequest)', value: 'SongReplayRequest' }, { label: 'Request Time Table (RequestTime)', value: 'RequestTime' }, { label: 'Email Template Table (EmailTemplate)', value: 'EmailTemplate' } ], progress: { readingFile: 'Reading file...', clearingData: 'Clearing existing data...', restoringTable: (table: string, start: number, end: number, total: number, percent: number) => `Restoring ${table} (${start}-${end}/${total}) ${percent}%`, fixingSequence: 'Fixing table sequences...', reloadingSmtp: 'Reloading SMTP configuration...', finalizingAdmin: 'Finalizing admin replacement...' }, messages: { parseBackupFailed: 'Unable to parse backup file. Please check the file format.', invalidJsonFile: 'Please select a valid JSON backup file', backupDownloaded: 'Backup file downloaded', selectBackupFile: 'Please select a backup file', restoreSuccessRelogin: 'Database restored. Signing in again...', restoreSuccess: 'Database restored successfully', sequenceResetSuccess: 'Sequence reset successfully', databaseResetSuccess: 'Database reset successfully' }, logs: { parseBackupFailed: 'Failed to parse backup file:', createBackupFailed: 'Failed to create backup:', restoreBackupFailed: 'Failed to restore backup:', resetSequenceFailed: 'Failed to reset sequence:', resetDatabaseFailed: 'Failed to reset database:' }, errors: { noBackupContent: 'Select at least one backup item', downloadFailed: (status: number) => `Download failed: ${status}`, createBackupFailed: 'Failed to create backup', createBackupFailedWithMessage: (message: string) => `Failed to create backup: ${message}`, invalidBackupFormat: 'Unable to parse backup file. File format is invalid', missingBackupData: 'Backup file is missing the data section', clearDataFailed: 'Failed to clear data', restoreTableFailed: (table: string) => `Failed to restore table ${table}`, fixSequenceFailed: 'Failed to fix sequences', smtpReloadFailed: 'Failed to reload SMTP configuration', restoreBackupFailedWithMessage: (message: string) => `Failed to restore backup: ${message}`, resetFailed: 'Reset failed', resetSequenceFailedWithMessage: (message: string) => `Failed to reset sequence: ${message}`, resetDatabaseFailedWithMessage: (message: string) => `Failed to reset database: ${message}` }, autoBackup: { title: 'Auto Backup Settings', cancel: 'Cancel', saveAll: 'Save All', masterSwitch: { label: 'Enable Auto Backup', desc: 'When API is triggered, no backup runs if master switch is off; only enabled methods run when master switch is on', }, methods: { title: 'Backup Methods', s3: { name: 'S3 Compatible Storage', desc: 'Supports R2 / S3 / B2 / MinIO', endpoint: 'Endpoint', endpointPlaceholder: 'https://xxx.r2.cloudflarestorage.com', bucket: 'Bucket', bucketPlaceholder: 'my-backups', region: 'Region', regionPlaceholder: 'auto', pathPrefix: 'Path Prefix', pathPrefixPlaceholder: 'voicehub-backups/', accessKey: 'Access Key ID', accessKeyPlaceholder: 'Enter Access Key ID', secretKey: 'Secret Access Key', secretKeyPlaceholder: 'Enter Secret Access Key', testConnection: 'Test Connection', }, webdav: { name: 'WebDAV', desc: 'Supports Nextcloud / Synology / ownCloud', url: 'Server URL', urlPlaceholder: 'https://example.com/remote.php/dav/files/user/', username: 'Username', usernamePlaceholder: 'Enter username', password: 'Password', passwordPlaceholder: 'Enter password', path: 'Upload Path', pathPlaceholder: 'voicehub-backups/', testConnection: 'Test Connection', }, telegram: { name: 'Telegram Bot', desc: 'Send backup files to a chat via Bot', botToken: 'Bot Token', botTokenPlaceholder: '123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11', chatId: 'Chat ID', chatIdPlaceholder: '123456789', testSend: 'Test Send', }, email: { name: 'Email Attachment', desc: 'Send backup file to specified email', recipient: 'Recipient Email', recipientPlaceholder: 'admin@example.com', testSend: 'Test Send', smtpHint: 'Uses the site\'s configured email service. Only the recipient email is required.', }, }, endpoint: { title: 'API Trigger Endpoint', hint: 'First create an API key in "API Key Manager" with the "Auto Backup" permission, then call this endpoint from an external scheduler to trigger backups.', url: 'Backup Endpoint URL', methods: 'Trigger Method', curlTab: 'cURL', cronjobTab: 'cron-job.org', githubTab: 'GitHub Actions', cronTab: 'Linux Cron', curlHint: 'Replace YOUR_API_KEY with your API key and run in terminal to trigger a backup.', cronjobLink: 'Visit cron-job.org to create a free scheduled task', githubHint: 'Configure BACKUP_URL and API_KEY in GitHub repo Settings → Secrets, then save the above as .github/workflows/backup.yml.', cronHint: 'Replace YOUR_API_KEY with your API key, run crontab -e on your server, and add the above line.', }, history: { title: 'Backup History', empty: 'No backup records yet', emptyHint: 'Records will appear here after triggering a backup', clear: 'Clear History', }, messages: { copied: 'Copied to clipboard', copyFailed: 'Copy failed', testConnection: 'Testing connection...', testSend: 'Testing send...', allSaved: 'All settings saved', } } }, userManager: { title: 'User Management', subtitle: (total: number) => `${total} members in the system · Permissions and account management`, add: 'Add', import: 'Import', update: 'Update', searchPlaceholder: 'Search by name or student ID...', role: 'Role', allRoles: 'All Roles', status: 'Status', allStatus: 'All Status', sort: 'Sort', sortPlaceholder: 'Sort By', loading: 'Loading users...', emptySearch: 'No matching users found', empty: 'No user data', itemName: 'users', archivedUsers: 'Archived', archivedSubtitle: (total: number) => `${total} archived graduate/withdrawn accounts`, organization: { title: 'Organization', subtitle: (total: number) => `${total} members · Grouped by grade and class`, refresh: 'Refresh organization', currentScope: 'Current Scope', clear: 'Clear', loading: 'Loading organization...', empty: 'No users available for statistics', count: (total: number) => `(${total})`, unnamedUser: 'Unnamed user', unsetGrade: 'Grade not set', unsetClass: 'Class not set', stages: { junior: 'Junior High', senior: 'Senior High', university: 'University', staff: 'Faculty and Staff', other: 'Other', withdrawn: 'Former Students', graduate: 'Graduates' }, loadFailed: 'Failed to load organization' }, table: { userDetails: 'User Details', role: 'Role', status: 'Account Status', class: 'Class', lastActivity: 'Last Interaction', actions: 'Actions' }, roles: { user: 'User', songAdmin: 'Song Admin', admin: 'Admin', superAdmin: 'Super Admin' }, statuses: { active: 'Active', withdrawn: 'Withdrawn', graduate: 'Graduate', disabled: 'Disabled', pending: 'Pending Approval', activeAccess: 'Active Access', withdrawnAccess: 'Restricted (Withdrawn)', graduateAccess: 'Restricted (Graduate)', }, actions: { edit: 'Edit', editUser: 'Edit User', viewSongs: 'View Songs', records: 'Records', reset: 'Reset', resetPassword: 'Reset Password', deleteUser: 'Delete User', approve: 'Review', cancel: 'Cancel', close: 'Close' }, approval: { title: 'Registration Review', username: 'Username', name: 'Name', grade: 'Grade', class: 'Class', gradeNotSet: 'Not Set', registeredAt: 'Registered At', remark: 'Registration Remark', emptyRemark: 'Not provided', editHint: 'You can edit the registration info below when approving', approve: 'Approve', reject: 'Reject', rejectReason: 'Reject Reason', rejectReasonPlaceholder: 'Optional; recorded in the status log', confirmReject: 'Reject this registration request? The account will be deleted.', approveSuccess: 'Registration approved', rejectSuccess: 'Registration request rejected', approveFailed: 'Registration approval failed', rejectFailed: 'Registration approval failed' }, mobile: { accountStatus: 'Account Status', roleLevel: 'Role / Level', class: 'Class', recentActivity: 'Recent Activity', lastLoginIp: 'Last Login IP' }, form: { editTitle: 'Edit User Info', createTitle: 'Create New User', desc: 'Fill in the account details below to continue', name: 'Name', namePlaceholder: 'Enter real name', username: 'Username / Student ID', usernamePlaceholder: 'Unique login identifier', newPassword: 'New Password (blank = unchanged)', initialPassword: 'Initial Password', passwordPlaceholder: 'Set a secure password', role: 'Role', rolePlaceholder: 'Select role', status: 'Account Status', statusPlaceholder: 'Select status', grade: 'Grade', gradePlaceholder: 'e.g. 2024', class: 'Class', classPlaceholder: 'e.g. Class 1', cancel: 'Cancel', saving: 'Saving...', update: 'Update User', create: 'Create User' }, resetPasswordModal: { title: 'Reset Access Password', descPrefix: 'Changing login credentials for', descSuffix: '', newPassword: 'New Password', newPasswordPlaceholder: 'Set a strong new password', confirmPassword: 'Confirm Password', confirmPasswordPlaceholder: 'Enter again to confirm', cancel: 'Cancel', resetting: 'Resetting...', confirm: 'Reset Password' }, importModal: { title: 'Batch Import Users', desc: 'Upload an .xlsx file according to the template requirements', instructionsTitle: 'Import Instructions', instructionsLine1: 'Upload an Excel file (.xlsx); the system will parse it automatically.', instructionsLine2: 'Note: the first row may be a header row and will be skipped automatically. Role values must match system roles.', sampleHeaders: { name: 'Name', username: 'Username', password: 'Password', role: 'Role', grade: 'Grade', class: 'Class' }, sampleName: 'Alex', sampleGrade: 'Grade 10', sampleClass: 'Class 1', templateSheetName: 'User Import Template', templateFileName: 'user-import-template.xlsx', supportedRoles: 'Supported roles:', supportedRolesFull: 'USER (User), ADMIN (Admin), SONG_ADMIN (Song Admin), SUPER_ADMIN (Super Admin)', supportedRolesLimited: 'USER (User), SONG_ADMIN (Song Admin)', downloadTemplate: 'Download Template', chooseFile: 'Choose Data File', uploadTitle: 'Click or drag to upload an Excel file', uploadHint: '.xlsx only', previewData: (count: number) => `Preview Data (${count} records)`, account: 'Account', gradeClass: 'Grade / Class', moreRecords: (count: number) => `and ${count} more records...`, cancel: 'Cancel', importing: 'Importing...', confirm: 'Start Import' }, batchUpdateModal: { title: 'Batch Update Student Info', desc: 'Quickly adjust grades, classes, or batch-edit student info with Excel', updateTypes: { gradeOnly: { title: 'Grade Only', desc: 'Batch update selected students’ grades while keeping classes unchanged' }, excelBatch: { title: 'Excel Batch Update', desc: 'Precisely match and batch-edit student information via Excel files' }, statusBatch: { title: 'Set Account Status', desc: 'Batch set account status for selected students' }, songAdminBatch: { title: 'Song Admin', desc: 'Batch update grade, class, and account status for Song Admins' } }, fields: { username: 'Username', name: 'Name', realName: 'Real Name' }, studentFilter: { title: 'Student Scope Filter', currentGrade: 'Current Grade', currentClass: 'Current Class', allGrades: 'All Grades', allClasses: 'All Classes', selectUsers: (selected: number, total: number) => `Select Users (${selected}/${total})`, clearSelection: 'Clear Selection', selectAll: 'Select All Current', noMatchedUsers: 'No users match the filters' }, gradeSettings: { title: 'Target Settings', targetGrade: 'Target Grade', targetGradePlaceholder: 'e.g. 2025', keepClass: 'Keep original class unchanged' }, statusSettings: { sourceStatus: 'Current Account Status', sourceStatusPlaceholder: 'Select current status', title: 'Target Status Settings', targetStatus: 'Select Account Status', targetStatusPlaceholder: 'Select target status', reason: 'Reason for Change', reasonPlaceholder: 'e.g. Batch graduate class of 2025' }, songAdminSettings: { title: 'Song Admin Info Update', desc: 'Selected users will apply the changes below (blank fields keep original values)', scope: 'Song Admin Scope Filter', currentGrade: 'Current Grade', currentClass: 'Current Class', allGrades: 'All Grades', allClasses: 'All Classes', selectUsers: (selected: number, total: number) => `Select Admins (${selected}/${total})`, clearSelection: 'Clear Selection', selectAll: 'Select All Current', noMatchedUsers: 'No Song Admins match the filters', targetSettings: 'Update Target Settings', targetGrade: 'Target Grade', targetGradePlaceholder: 'e.g. 2025 (blank keeps original)', targetClass: 'Target Class', targetClassPlaceholder: 'e.g. Class 1 (blank keeps original)', targetStatus: 'Target Account Status', targetStatusPlaceholder: 'Select target status (blank keeps original)' }, statusOptions: { all: 'Any current status', active: 'Active Access', graduate: 'Restricted Access (Graduate)', withdrawn: 'Restricted Access (Withdrawn)' }, excelUpload: { dragTitle: 'Drag Excel file here', or: 'or', chooseFile: 'click to choose a file', supportedFormats: '(.xlsx / .xls supported)' }, matchType: { title: 'Match Method', byUsername: 'Match by Username', byName: 'Match by Name', usernamePrefix: 'Use ', usernameSuffix: ' to precisely locate existing accounts. Best for regular updates where student IDs do not change.', namePrefix: 'Use ', nameMiddle: ' to locate existing accounts. The ', nameSuffix: ' column will be treated as the new student ID, suitable for ID shifts after roster changes.' }, fileSpec: { title: 'File Format', headerLine: 'First row headers: Username, Name, Grade, Class, New Username', usernameColumn: ' column is used to precisely match existing accounts', nameColumn: ' column is used to precisely match existing accounts', blankKeepsOriginal: 'Blank fields keep original values and will not overwrite existing data', downloadTemplate: 'Get Template File' }, preview: { blockersTitle: (count: number) => `Users Requiring Action (${count})`, blockersDesc: 'These users are not in the current update list, but occupy target usernames. Mark them as withdrawn or disabled before updating.', dataPreview: (visible: number, total: number) => `Data Preview (${visible}/${total})`, pending: (count: number) => `Pending (${count})`, noChangeCount: (count: number) => `No Change (${count})`, errorCount: (count: number) => `Errors (${count})`, matchUser: 'Matched User', matchName: 'Matched Name', currentInfo: 'Current Info', afterUpdate: 'After Update', matchStatus: 'Match Status', noChange: 'No Change', ready: 'Ready', moreQueued: (count: number) => `${count} more records are queued` }, actions: { cancel: 'Cancel', submitting: 'Submitting updates...', confirm: 'Confirm & Update' }, template: { headers: { username: 'Username', name: 'Name', grade: 'Grade', class: 'Class', newUsername: 'New Username' }, sampleName1: 'Alex', sampleName2: 'Taylor', sampleClass1: 'Class 1', sampleClass2: 'Class 2', sheetName: 'Student Info', fileName: 'student-info-batch-update-template.xlsx' }, progress: { processing: (current: number, total: number) => `Updating: processing batch ${current} / ${total}...`, completed: (success: number, failed: number) => `Update complete: ${success} succeeded, ${failed} failed` }, messages: { partialGradeSuccess: (failed: number) => `Partially updated. ${failed} users were skipped due to permissions or status.`, gradeSuccess: (count: number) => `Successfully updated grades for ${count} users`, partialExcelSuccess: (success: number, failed: number) => `Partially updated: ${success} succeeded, ${failed} failed. Please check and retry.`, excelFailed: (failed: number) => `Batch update failed: ${failed} users could not be updated. Please check and retry.`, partialStatusSuccess: (failed: number) => `Partially updated. ${failed} users were skipped due to permissions or status.`, statusSuccess: 'Batch status update succeeded', songAdminSuccess: (count: number) => `Successfully updated ${count} Song Admins`, partialSongAdminSuccess: (failed: number) => `Partially updated. ${failed} admins were skipped due to permissions or status.`, songAdminNoChanges: 'No changes needed for the selected Song Admins' }, errors: { invalidExcelFormat: 'Invalid Excel format. Please check the file format.', readFileFailed: 'Failed to read file. Please try again.', unknownExcelError: 'Unknown error. Please check the file format or network connection.', processExcelFailed: (message: string) => `Failed to process Excel file: ${message}`, fieldRequired: (field: string) => `${field} is required`, duplicateNames: 'Multiple users have the same name. Please switch to username matching.', userNotFound: 'User not found', duplicateTarget: (username: string) => `Duplicate update target: multiple rows try to update to username ${username}`, usernameOccupied: (username: string, name: string) => `Username ${username} is occupied. Please handle ${name}’s account first.`, loadXlsxFailed: 'Failed to load Excel processing library. Please check your network connection.', loadXlsxRetry: 'Failed to load Excel processing library. Please refresh and try again.', batchUpdateFailed: (message: string) => `Batch update failed: ${message}`, unknownError: 'Unknown error', batchUpdateGeneric: 'Batch update failed', updateFailedWithEtc: (message: string) => `Update failed: ${message}, etc.`, noValidUpdates: 'No valid update data', batchRequestFailed: 'Batch update request failed', invalidBatchResponse: 'Batch update API returned an invalid response', statusUpdateFailed: 'Batch status update failed', songAdminUpdateFailed: 'Batch Song Admin update failed' } }, deleteDialog: { title: 'Confirm User Deletion', message: (name: string) => `Delete user "${name}"? This account will be permanently removed and cannot be restored.`, confirm: 'Delete' }, detail: { title: 'User Details', desc: 'View complete account profile and activity records', basicInfo: 'Basic Info', userId: 'User ID', name: 'Name', username: 'Username', role: 'Role', grade: 'Grade', class: 'Class', unset: 'Not Set', status: 'Account Status', passwordStatus: 'Password Status', passwordChanged: 'Password Changed', requirePasswordChange: 'Password Change Required', meowBinding: 'MeoW Account Binding', emailBinding: 'Email Binding', oauthBinding: 'OAuth Account Binding', bound: (value: string) => `Bound: ${value}`, unbound: 'Unbound', oauthBindingsTitle: 'OAuth Binding Details', oauthBindingsDesc: 'View third-party accounts bound to this user', oauthBindingsEmpty: 'No bound accounts', oauthProvider: 'Provider', oauthUsername: 'Username', oauthUserId: 'Platform User ID', oauthBoundAt: 'Bound At', oauthNoUsername: 'No username', oauthNoId: 'No ID', oauthUnknownDevice: 'Unknown device', oauthTotpLabel: 'Two-factor auth', oauthIdHidden: 'Hidden', oauthUnknownTime: 'Unknown time', verified: 'Verified', unverified: 'Unverified', loginInfo: 'Login Info', lastLogin: 'Last Login', loginIp: 'Login IP', unknown: 'Unknown', timeRecords: 'Time Records', createdAt: 'Created At', updatedAt: 'Updated At', statusLogs: 'Status Change Logs', loadingLogs: 'Loading records...', noLogs: 'No status change records', initial: 'Initial', reason: 'Reason:', operator: 'Operator:', system: 'System' }, sortOptions: { default: 'Default (ID)', nameAsc: 'Name (A-Z)', nameDesc: 'Name (Z-A)', lastLoginDesc: 'Recent Login', lastLoginAsc: 'Earliest Login', createdAtDesc: 'Recent Registration', createdAtAsc: 'Earliest Registration' }, time: { never: 'Never logged in', justNow: 'Just now', minutesAgo: (minutes: number) => `${minutes}m ago`, hoursAgo: (hours: number) => `${hours}h ago`, daysAgo: (days: number) => `${days}d ago`, unknown: 'Unknown time' }, notifications: { selfEditForbidden: 'You cannot modify your own account in User Management', selfResetForbidden: 'You cannot reset your own password in User Management', selfDeleteForbidden: 'You cannot delete your own account', deleteSuccess: 'User deleted successfully', deleteFailed: (message: string) => `Failed to delete user: ${message}`, updateSuccess: 'User updated successfully', createSuccess: 'User created successfully', resetSuccess: 'Password reset successfully', detailLoadFailed: (message: string) => `Failed to load user details: ${message}`, loadFailed: (message: string) => `Failed to load users: ${message}`, excelLoadFailed: 'Failed to load the Excel processing library. Refresh the page and try again.' }, errors: { requiredInfo: 'Please complete the required information', passwordRequired: 'Please enter a password', saveFailed: 'Failed to save', newPasswordRequired: 'Please enter a new password', passwordMismatch: 'The two passwords do not match', resetFailed: 'Failed to reset', xlsxLoadFailed: 'Unable to load the Excel processing library. Refresh and try again', noValidData: 'No valid data found', parseExcelFailed: (message: string) => `Failed to parse Excel file: ${message}`, readFileFailed: 'Failed to read file', processExcelFailed: (message: string) => `Failed to process Excel file: ${message}`, partialImportFailed: (errors: string) => `Some rows failed to import:\n${errors}`, importFailedWithReasons: (errors: string) => `Import failed. Check the data format and try again.\nReasons:\n${errors}`, importRuntimeFailed: (count: number, message: string) => `An error occurred during import (${count} imported successfully): ${message}`, statusLogsFailed: 'Failed to load status logs' }, importProgress: { processingBatch: (current: number, total: number) => `Importing: processing batch ${current} / ${total}...`, complete: (created: number, failed: number) => `Import complete: ${created} imported, ${failed} failed`, batchFailed: (batch: number, message: string) => `Batch ${batch} request failed: ${message}`, rowError: (row: string | number, name: string, reason: string) => `Row ${row}${name}: ${reason}` } }, apiKeys: { title: 'API Key Management', desc: 'Manage access keys for the open API and control third-party access', create: 'Create API Key', searchPlaceholder: 'Search API key name or description...', status: 'Status', allStatus: 'All Status', active: 'Active', inactive: 'Inactive', expired: 'Expired', creatorPlaceholder: 'Creator username', loading: 'Loading...', emptyTitle: 'No API Keys', emptyDesc: 'You have not created any access keys yet. Create one to safely integrate VoiceHub with third-party apps.', createFirst: 'Create your first key', noDescription: 'No description', unknown: 'Unknown', creator: 'Creator', createdAt: 'Created At', permissions: 'Permissions', itemName: 'API keys', edit: 'Edit API Key', name: 'Name *', namePlaceholder: 'Enter API key name', description: 'Description', descriptionPlaceholder: 'Enter API key description (optional)', expiresAt: 'Expiration', expiresOptions: { never: 'Never expires', keep: 'Keep unchanged', threeDays: 'Expires in 3 days', sevenDays: 'Expires in 7 days', thirtyDays: 'Expires in 30 days', sixtyDays: 'Expires in 60 days', ninetyDays: 'Expires in 90 days' }, permissionSettings: 'Permissions *', enabled: 'Enable this API key', cancel: 'Cancel', saving: 'Saving...', createKey: 'Create Key', saveChanges: 'Save Changes', createSuccessTitle: 'API Key Created', createSuccessHeading: 'API key created successfully!', createSuccessDesc: 'Store this API key safely. It will only be shown once.', fullKey: 'Full API Key', copyWarning: 'Copy and save this API key now. It cannot be viewed again after closing this window.', closeSaved: 'Saved, Close', details: 'API Key Details', basicInfo: 'Basic Info', nameField: 'Name:', statusField: 'Status:', descField: 'Description:', creatorField: 'Creator:', createdAtField: 'Created:', expiresAtField: 'Expires:', usageStats: 'Usage Stats', totalCalls: 'Total Calls', lastUsedAt: 'Last Used', neverChecked: 'Never checked', recentLogs: 'Recent Logs', loadingLogs: 'Loading logs...', noLogs: 'No call records', confirmDeleteTitle: 'Confirm Delete', delete: 'Delete', deleteMessage: (name: string) => `Delete API key "${name}"? This action cannot be undone.`, permissionOptions: { scheduleRead: { label: 'Schedule Read', description: 'View schedule list and details' }, songsRead: { label: 'Song Read', description: 'View song list and details' }, songsRequest: { label: 'Song Request', description: 'Submit song requests as the API key owner' }, songsWrite: { label: 'Song Management', description: 'Update song status' }, cardCodesRead: { label: 'Request Card Read', description: 'View request card list and statistics' }, cardCodesWrite: { label: 'Request Card Management', description: 'Create and update request cards' }, cardCodesDelete: { label: 'Request Card Delete', description: 'Delete request cards' }, backupExecute: { label: 'Auto Backup', description: 'Trigger automatic backup tasks' } }, loadFailed: 'Failed to load API keys', nameRequired: 'Enter key name', permissionRequired: 'Select at least one permission', updateSuccess: 'API key updated', updateFailed: 'Failed to update API key', createFailed: 'Failed to create API key', deleteSuccess: 'API key deleted', deleteFailed: 'Failed to delete API key', detailFailed: 'Failed to load API key details', logsFailed: 'Failed to load API usage logs', copied: 'Copied to clipboard', copyFailed: 'Copy failed', expiresAtText: (date: string) => `Expires: ${date}` } } as const // ==================== Theme ==================== export const theme = { select: 'Choose theme', System: 'Follow System', ClassicDark: 'Classic Dark', ClassicLight: 'Classic Light', ModernLight: 'Modern Light' } as const // Localized copy for server-side business error codes (keyed by SERVER_ERROR_CODES in server/config/constants.ts). // The client resolves these via useServerErrors().localize(err) using err.data.code, falling back to the server message. export const serverErrors = { CARD_CODE_AUTH_REQUIRED: 'Please sign in before validating a request card', CARD_CODE_REQUIRED: 'Please enter a request card first', CARD_CODE_DISABLED: 'Request card submissions are not enabled', CARD_CODE_INVALID_OR_USED: 'This request card is invalid or has already been used', CARD_CODE_LOCKED_OR_UNAVAILABLE: 'This request card is locked or unavailable', CARD_CODE_REQUIRED_FOR_SITE: 'This site requires a valid request card to submit songs', CARD_CODE_TOO_LONG: 'Request card code cannot exceed 100 characters', AUTH_CSRF_COOKIE_MISSING: 'CSRF validation failed: the cookie is missing. Please restart from the login page.', AUTH_CHALLENGE_EXPIRED: 'Challenge has expired', AUTH_CHALLENGE_INVALID: 'Challenge has expired or does not match', AUTH_STATE_INVALID: 'Invalid or expired state', AUTH_MISSING_CODE_OR_STATE: 'Missing code or state', AUTH_MISSING_PROVIDER: 'Missing provider', AUTH_OAUTH_PROVIDER_DISABLED: 'OAuth provider is disabled', AUTH_OAUTH_PROVIDER_STATE_MISMATCH: 'OAuth provider does not match state', AUTH_OAUTH_REDIRECT_ORIGIN_MISMATCH: 'The OAuth callback URL does not match the current request origin. Please set the OAuth redirect URI to the current site domain in the admin console.', AUTH_OAUTH_REDIRECT_INVALID: 'Invalid OAuth redirect URI configuration. Please check the settings in the admin console.', AUTH_WEBAUTHN_RP_ID_INVALID: 'Invalid WebAuthn RP ID configuration', AUTH_UNSUPPORTED_VERIFICATION_TYPE: 'Unsupported verification type', AUTH_CAPTCHA_FAILED_OR_EXPIRED: 'Human verification failed or expired. Please try again.', AUTH_CAPTCHA_SERVICE_UNAVAILABLE: 'The human verification service is temporarily unavailable', AUTH_SESSION_EXPIRED: 'Your session has expired. Please sign in again.', AUTH_CREDENTIAL_CORRUPTED: 'Credential data is corrupted', AUTH_CREDENTIAL_MISSING: 'Credential data is missing', AUTH_CREDENTIAL_MISSING_PUBLICKEY: 'Credential data is missing publicKey', AUTH_TOTP_CODE_INVALID: 'Invalid authenticator code', AUTH_TOTP_TOO_MANY_ATTEMPTS: 'Too many incorrect authenticator codes. Please try again in 5 minutes.', AUTH_INCOMPLETE_PARAMS: 'Incomplete parameters', COMMON_INVALID_PARAMS: 'Invalid parameters', THEME_INVALID_LIST: 'The enabled theme list is invalid', THEME_INVALID_DEFAULT: 'The default theme must be enabled and valid', THEME_SYSTEM_REQUIRES_CLASSIC: 'Follow System requires both Classic Dark and Classic Light', SETTINGS_GRADE_CLASS_OPTIONS_MISSING: 'Configure grade and class options before enabling this setting', BILIBILI_PLAYURL_FAILED: 'Failed to fetch Bilibili audio link', AUTH_NAME_USERNAME_PASSWORD_REQUIRED: 'Name, username, and password are required', AUTH_USERNAME_LENGTH_INVALID: 'Username must be between 3 and 30 characters', AUTH_USERNAME_PATTERN_INVALID: 'Username may only contain letters, numbers, underscores, and hyphens', AUTH_PASSWORD_TOO_SHORT: 'Password must be at least 6 characters', AUTH_PASSWORD_TOO_LONG: 'Password cannot exceed 128 characters', AUTH_PASSWORD_TOO_MANY_BYTES: 'Password cannot exceed 72 bytes', AUTH_PASSWORD_TOO_COMMON: 'This password is too common. Choose a more secure password.', AUTH_PASSWORD_COMPLEXITY_REQUIRED: 'Password must include at least three of: uppercase letters, lowercase letters, numbers, and special characters', AUTH_INITIAL_PASSWORD_COMPLEXITY_REQUIRED: 'Password must include at least two of: uppercase letters, lowercase letters, numbers, and special characters', AUTH_PASSWORD_NOT_SET: 'This account does not have a password yet. Use the initial password setup flow.', AUTH_INITIAL_PASSWORD_NOT_REQUIRED: 'This account does not need initial password setup. Use the change-password flow.', AUTH_PASSWORD_CHANGE_REQUIRED: 'Change your password before continuing', AUTH_PASSWORD_STATE_CHANGED: 'The password state changed. Verify again and retry.', AUTH_GRADE_CLASS_TOGETHER: 'Grade and class must be selected together, or both left empty', AUTH_GRADE_CLASS_REQUIRED: 'Grade and class are required to register', AUTH_UNSUPPORTED_OAUTH_PROVIDER: 'Only GitHub / Casdoor / Google / aggregated login / third-party OAuth2 are currently supported', AUTH_CURRENT_PASSWORD_INCORRECT: 'Current password is incorrect', AUTH_CURRENT_AND_NEW_PASSWORD_REQUIRED: 'Both the current password and the new password are required', AUTH_PASSWORD_ALREADY_SET: 'You have already set a password. Please use the change-password feature.', AUTH_IP_BLOCKED_RETRY_MINUTES: 'Your IP address is restricted. Please try again in {0} minutes.', AUTH_ACCOUNT_NAME_NOT_FOUND: 'Account name not found. Please check for typos.', AUTH_RATE_LIMITED_MINUTES: 'Too many requests. Please wait {0} minutes and try again.', AUTH_RATE_LIMITED_SECONDS: 'Too many requests. Please wait {0} seconds and try again.', AUTH_EMAIL_CODE_INVALID: 'Invalid or expired email verification code. Please request a new one.', AUTH_EMAIL_CODE_COOLDOWN: 'Verification code sent too frequently. Please wait 1 minute and try again.', AUTH_EMAIL_CODE_DAILY_LIMIT: 'Daily verification code limit reached. Please try again tomorrow.', AUTH_EMAIL_SERVICE_UNAVAILABLE: 'Email service is not configured or unavailable. Please contact the administrator.', AUTH_DATABASE_UNAVAILABLE: 'The database service is temporarily unavailable', AUTH_NEW_PASSWORD_SAME_AS_CURRENT: 'The new password cannot be the same as the current password', AUTH_NEW_PASSWORD_REQUIRED: 'The new password cannot be empty', AUTH_REGISTER_PASSWORD_MISMATCH: 'The two passwords do not match', AUTH_INVALID_REGISTER_TOKEN: 'Invalid registration token', AUTH_INVALID_BINDING_TOKEN: 'Invalid binding token', AUTH_INVALID_RESET_LINK: 'Invalid reset link', AUTH_UPDATE_FAILED: 'Update failed', AUTH_SERVER_CONFIG_ERROR: 'Server configuration error', AUTH_AGGREGATED_LOGIN_UNSUPPORTED: 'The aggregated login method is disabled or unsupported', AUTH_TOTP_NOT_ENABLED: 'TOTP verification is not enabled', AUTH_PASSKEY_ACCOUNT_NOT_FOUND: 'No account is associated with this passkey', AUTH_DEVICE_NOT_FOUND: 'Device not found', AUTH_UNAUTHORIZED: 'Unauthorized', AUTH_UNAUTHORIZED_ACCESS: 'Unauthorized access', AUTH_TOKEN_MISSING: 'No authentication token was provided', AUTH_MULTI_IP_PROTECTION: 'Multiple IP logins were detected for this account in a short period. This IP is restricted for {0} minutes and the account is protected for {1} minutes.', AUTH_REGISTER_SESSION_EXPIRED: 'The registration session has expired. Please restart via third-party login.', USER_NOT_FOUND: 'User not found', AUTH_USER_NOT_FOUND_OR_NO_EMAIL: 'The user does not exist or has no bound email', AUTH_USERNAME_PASSWORD_REQUIRED: 'Username and password are required', AUTH_USERNAME_TAKEN: 'Username already exists. Please choose another.', AUTH_INVALID_CREDENTIALS: 'Incorrect username or password', AUTH_LOGIN_UNKNOWN_ERROR: 'An unknown error occurred during login', AUTH_OAUTH_REGISTER_DISABLED: 'Third-party account registration is disabled', AUTH_OAUTH_REGISTER_DISABLED_BIND: 'Third-party account registration is disabled. Please sign in to an existing account to bind.', AUTH_REGISTER_DISABLED: 'Registration is not open', AUTH_USER_PENDING_APPROVAL: 'Your account is pending admin approval. Please wait.', AUTH_SYSTEM_ERROR: 'System error. Please try again later.', AUTH_BINDING_SESSION_INVALID: 'The binding session is invalid. Please restart binding.', AUTH_BINDING_SESSION_EXPIRED: 'The binding session has expired. Please restart via third-party login.', AUTH_MISSING_CREDENTIAL_ID: 'Missing credential ID', AUTH_MISSING_REQUIRED_PARAMS: 'Missing required parameters', AUTH_MISSING_PROVIDER_PARAM: 'Missing provider parameter', AUTH_MISSING_PRE_TOKEN: 'Missing pre-authentication token', AUTH_MISSING_PRE_TOKEN_RELOGIN: 'Missing pre-authentication token. Please sign in again.', AUTH_AGGREGATED_CALLBACK_STATE_MISMATCH: 'The aggregated login callback type does not match state', AUTH_AGGREGATED_METHOD_CHANGED: 'The aggregated login method is disabled or has changed', AUTH_AGGREGATED_STATE_INVALID: 'The aggregated login state is invalid or expired', AUTH_DEVICE_NAME_TOO_LONG: 'The device name is too long', AUTH_OAUTH_BOUND_OTHER_USER: 'This third-party account is already bound to another user', AUTH_OAUTH_ALREADY_BOUND: 'This third-party account is already bound. Please sign in directly or bind it to an existing account.', AUTH_ACCOUNT_UNAVAILABLE_OR_RESTRICTED: 'This account is unavailable or access is restricted', AUTH_ACCOUNT_GRADUATED: 'This account has graduated; access is restricted', AUTH_ACCOUNT_BANNED: 'This account has been banned', AUTH_ACCOUNT_WITHDRAWN: 'This account has withdrawn; access is restricted', AUTH_ACCOUNT_CURRENTLY_UNAVAILABLE: 'This account is currently unavailable', AUTH_NO_SECURITY_EMAIL: 'This account has no security email bound and cannot recover the password this way. Please contact an administrator.', AUTH_RESET_LINK_INVALIDATED: 'This reset link is no longer valid (the password has been changed)', AUTH_CAPTCHA_REQUIRED: 'Please complete the human verification', AUTH_IMAGE_CAPTCHA_REQUIRED: 'Please complete the image captcha', AUTH_ACCOUNT_NAME_REQUIRED: 'Please provide an account name', AUTH_GRADE_CLASS_MUST_EXIST: 'Please select a grade and class that already exist in the system', AUTH_ACCOUNT_DISABLED_OR_RESTRICTED: 'This account is disabled or access is restricted', AUTH_ACCOUNT_RISK_CONTROL: 'This account is in a risk-control period. Please try again in {0} minutes.', AUTH_ACCOUNT_LOCKED_MINUTES: 'This account is locked. Please try again in {0} minutes.', AUTH_ACCOUNT_LOCKED: 'This account is locked. Please try again later.', AUTH_EMAIL_MISMATCH: 'The email address does not match', AUTH_RESET_PASSWORD_TOO_MANY: 'Too many password reset attempts. Please wait {0} minutes and try again.', AUTH_RESET_LINK_EXPIRED: 'The reset link has expired. Please request a new one.', AUTH_USER_ID_MISMATCH: 'Illegal operation: user ID does not match', AUTH_VERIFICATION_FAILED: 'Verification failed', AUTH_TOO_MANY_VERIFY_ATTEMPTS: 'Too many verification attempts. Please request a new code.', AUTH_CODE_SEND_FAILED: 'Failed to send the verification code', AUTH_CODE_ALREADY_USED: 'This verification code has been used. Please request a new one.', AUTH_CODE_EXPIRED: 'The verification code has expired', AUTH_CODE_EXPIRED_OR_MISSING: 'The verification code has expired or does not exist', AUTH_CODE_SERVICE_CONFIG_ERROR: 'The verification code service is misconfigured. Please contact an administrator.', AUTH_CODE_INVALID_OR_EXPIRED: 'The verification code is incorrect or expired. Please re-enter it.', AUTH_CODE_WRONG_ATTEMPTS_LEFT: 'Incorrect verification code. Attempts remaining: {0}', SONG_INVALID_ID: 'Invalid song ID', HTTP_METHOD_NOT_ALLOWED: 'Method Not Allowed', SONG_ALREADY_EXISTS: 'Song already exists', SONG_TITLE_ARTIST_REQUIRED: 'Title and artist are required', SONG_CANNOT_SELF_VOTE: 'You cannot vote for your own submission', SONG_VOTE_NOT_FOUND: 'You have not voted for this song, so it cannot be canceled', SONG_ALREADY_VOTED: 'You have already voted for this song', SONG_REPLAY_CANCEL_FAILED: 'Failed to cancel the replay request. Please try again later.', SONG_VOTERS_ADMIN_ONLY: 'Only administrators can view the voter list', SONG_WITHDRAW_OWN_ONLY: 'You can only withdraw your own submission or leave a co-submission', SONG_REPLAY_CURRENT_SEMESTER_ONLY: 'You can only request a replay for songs from the current semester', SONG_INVITATION_FAILED: 'Failed to process the invitation', SONG_SCHEDULED_CANNOT_WITHDRAW: 'Scheduled songs cannot be withdrawn', SONG_PLAYED_CANNOT_WITHDRAW: 'Played songs cannot be withdrawn', SONG_NO_ACTIVE_SEMESTER_REPLAY: 'There is no active semester, so a replay cannot be requested', SONG_RESTRICTION_SAME_SONG: 'The song "{1}" cannot be resubmitted within {0} hour(s) after entering the schedule', SONG_RESTRICTION_SAME_ARTIST: 'Songs by "{1}" cannot be resubmitted within {0} hour(s) after entering the schedule', SONG_REPLAY_ALREADY_REQUESTED: 'You have already requested a replay for this song', SONG_REPLAY_COOLDOWN: 'Replay request is cooling down; please wait {0} more hour(s)', SONG_REPLAY_INVALID_REQUEST: 'The replay request is invalid or has already been processed', SONG_REPLAY_PLAY_TIME_DISABLED: 'Play time selection is not enabled', SONG_REPLAY_PLAY_TIME_INVALID: 'The selected play time does not exist or is not enabled', SONG_REQUESTER_NOT_FOUND: 'The submitting user does not exist', SONG_OPERATION_FAILED: 'Operation failed. Please try again later.', SONG_INVITATION_NOT_FOUND: 'No pending invitation was found', AUTH_LOGIN_REQUIRED: 'Please sign in first', SONG_NO_ACTIVE_SEMESTER_VOTE: 'No active semester is set, so voting is unavailable', COMMON_INSUFFICIENT_PERMISSION: 'Insufficient permissions', COMMON_TARGET_NOT_FOUND: 'Target not found', USER_NOT_PENDING: 'Only users pending approval can be reviewed', GRADE_CLASS_DUPLICATE: 'This grade/class combination already exists', SONG_ID_REQUIRED: 'Song ID is required', SONG_NOT_FOUND: 'Song not found', SONG_DURATION_PLATFORM_REQUIRED: 'Song missing platform or music ID, cannot retrieve duration', SONG_CARD_RELEASE_FAILED: 'Failed to release the request card; the withdrawal was aborted', SONG_NO_ACTIVE_SEMESTER_IMPORT: 'No active semester is set, so songs cannot be imported. Please ask an administrator to set an active semester first.', SONG_FETCH_VOTERS_FAILED: 'Failed to fetch the voter list', SONG_FETCH_STATUS_FAILED: 'Failed to fetch the submission status', SONG_FETCH_COUNT_FAILED: 'Failed to fetch the song count', SONG_TEMP_PROTECTED_SECONDS: 'This song is in a temporary protection period; {0} seconds remaining', SONG_NOT_PLAYED_NO_REPLAY: 'This song has not been played yet, so a replay cannot be requested', SONG_SCHEDULED_NO_VOTE: 'This song is scheduled, so voting is unavailable', SONG_PLAYED_NO_VOTE: 'This song has been played, so voting is unavailable', SONG_SELECT_TO_IMPORT: 'Please select the songs to import', SONG_REPLAY_NOT_FOUND_OR_FORBIDDEN: 'The replay request does not exist or you are not allowed to cancel it', SONG_REPLAY_DISABLED: 'The replay request feature is not enabled', SONG_LOGIN_REQUIRED_CANCEL_REPLAY: 'You must sign in to cancel a replay request', SONG_LOGIN_REQUIRED_VOTE: 'You must sign in to vote', SONG_LOGIN_REQUIRED_WITHDRAW: 'You must sign in to withdraw a song', SONG_LOGIN_REQUIRED_VIEW_VOTERS: 'You must sign in to view voters', SONG_LOGIN_REQUIRED_VIEW_STATUS: 'You must sign in to view the submission status', SONG_LOGIN_REQUIRED_REQUEST: 'You must sign in to request a song', SONG_LOGIN_REQUIRED_REPLAY: 'You must sign in to request a replay', SONG_INACTIVE_SEMESTER_VOTE: 'The semester is inactive, so voting is unavailable', USER_API_KEY_NOT_FOUND: 'Personal integration token not found', USER_API_KEY_ID_REQUIRED: 'Token ID is required', USER_API_KEY_ID_INVALID: 'Invalid token ID', USER_API_KEY_CREATE_FAILED: 'Failed to create the personal API key', USER_API_KEY_CREATE_FAILED_DETAIL: 'Failed to create the personal API key: {0}', USER_API_KEY_DELETE_FAILED_DETAIL: 'Failed to delete the personal integration token: {0}', USER_CODE_SEND_FAILED: 'Failed to send the verification code. Please try again later.', USER_PASSWORD_INCORRECT: 'Incorrect password', USER_2FA_ALREADY_ENABLED: 'Two-factor authentication is already enabled', USER_API_KEY_LIMIT_REACHED: 'Each user can create at most 5 personal integration tokens', USER_EMAIL_BIND_FAILED: 'Failed to bind the email', USER_MISSING_CODE_OR_SECRET: 'Missing verification code or secret', USER_API_KEY_FETCH_FAILED_DETAIL: 'Failed to fetch the personal integration token: {0}', USER_API_KEY_LOGS_FETCH_FAILED_DETAIL: 'Failed to fetch the personal integration token logs: {0}', USER_YEAR_REVIEW_FETCH_FAILED: 'Failed to fetch the year in review', USER_EMAIL_UNBIND_FAILED: 'Failed to unbind the email', USER_EMAIL_TAKEN: 'This email is already bound to another user', USER_SEND_CODE_FIRST: 'Please send a verification code first', USER_BIND_EMAIL_FIRST: 'Please bind an email first', USER_REQUEST_VALIDATION_FAILED: 'Request parameter validation failed: {0}', USER_PASSWORD_REQUIRED: 'Please enter your password', USER_INVALID_EMAIL: 'Please enter a valid email address', USER_EMAIL_AND_CODE_REQUIRED: 'Email and verification code are required', USER_EMAIL_CHANGED_RESEND: 'The email has changed. Please resend the verification code.', USER_EMAIL_ALREADY_VERIFIED: 'The email is already verified; no need to resend', USER_RESEND_EMAIL_FAILED: 'Failed to resend the verification email', USER_CODE_ALREADY_USED: 'This verification code has been used. Please resend.', USER_CODE_EXPIRED_RESEND: 'The verification code has expired. Please resend.', USER_CODE_INVALID: 'Incorrect verification code', USER_CODE_TOO_MANY_ATTEMPTS: 'Too many incorrect verification codes. Please resend.', BACKUP_DISABLED: 'Auto backup is disabled', MUSIC_SOURCE_PLATFORM_DISABLED: 'Platform "{0}" is disabled. Available platforms: {1}', QQ_COMMENT_FETCH_FAILED: 'Failed to load QQ Music comments. Please try again later.', BACKUP_NOT_CONFIGURED: 'Auto backup is not configured', NO_BACKUP_METHOD_ENABLED: 'No backup methods enabled', BACKUP_FAILED: 'Backup execution failed', SMTP_NOT_CONFIGURED: 'Email service is not configured', NOTIFICATION_AUTH_REQUIRED: 'Please sign in before working with notifications', NOTIFICATION_ADMIN_REQUIRED: 'Only administrators can send system notifications', NOTIFICATION_IMPORTANT_INVALID: 'The important notice flag must be a boolean', NOTIFICATION_TITLE_CONTENT_REQUIRED: 'Notification title and content are required', NOTIFICATION_TITLE_TOO_LONG: 'Notification title cannot exceed {0} characters', NOTIFICATION_CONTENT_TOO_LONG: 'Notification content cannot exceed {0} characters', NOTIFICATION_USER_ID_INVALID: 'Invalid user ID', NOTIFICATION_SCOPE_INVALID: 'Invalid notification scope', NOTIFICATION_GRADE_REQUIRED: 'Grade is required', NOTIFICATION_GRADE_CLASS_REQUIRED: 'Grade and class are required', NOTIFICATION_CLASSES_REQUIRED: 'Select at least one valid class', NOTIFICATION_USERS_REQUIRED: 'Select at least one user', NOTIFICATION_SEND_FAILED: 'Failed to send the notification', NOTIFICATION_FETCH_FAILED: 'Failed to fetch the important notice', NOTIFICATION_ID_INVALID: 'Invalid notification ID', NOTIFICATION_NOT_FOUND: 'Notification not found', NOTIFICATION_FORBIDDEN: 'You cannot operate on this notification', NOTIFICATION_MARK_READ_FAILED: 'Failed to mark the notification as read', NOTIFICATION_DELETE_FAILED: 'Failed to delete the notification', NOTIFICATION_CLEAR_FAILED: 'Failed to clear notifications', NOTIFICATION_HISTORY_STATUS_INVALID: 'Invalid notification history status filter', NOTIFICATION_HISTORY_FILTER_INVALID: 'Invalid notification history filter', NOTIFICATION_HISTORY_FETCH_FAILED: 'Failed to fetch notification history', NOTIFICATION_HISTORY_UPDATE_FAILED: 'Failed to update notification', NOTIFICATION_HISTORY_DELETE_FAILED: 'Failed to delete notification', } as const