The killer-feature of this pass is the live indicator: red pulsing
dot next to every streamer in the sidebar that is currently
broadcasting on Twitch. Suddenly the sidebar conveys real-time
state at a glance — you know who to click before clicking.
How it works:
- New live-status batch poller (main.ts) fires every 60s, packs
every streamer in the user's watch list into a single GQL query
using aliased user lookups (`u0:user(login:$l0){stream{type}} ...`),
chunked at 50 logins per request. One roundtrip for the whole
list — far cheaper than per-streamer polling.
- Updates a liveStatusByLogin Map on the main side, emits an IPC
`live-status-batch-update` event with only the entries that
flipped (plus a full snapshot for the renderer to keep in sync).
- Renderer subscribes once at boot via initLiveStatusSubscription,
keeps a parallel Map, and re-renders the streamer list on
change. Stamps a .streamer-live-dot before the name. Bold name
for live streamers so they pop in scannability.
- Restart triggers: app boot, streamer-list change (added/removed
via save-config) so a freshly added streamer gets their dot in
seconds without waiting for the next 60s tick.
Polish bundled in the same release:
- VOD card hover gets a more substantial lift: 12px shadow + faint
purple border-glow on hover. Subtle but enough to feel
"tactile". Border-color transitions alongside the shadow.
- Empty states get a floating animation and a bigger SVG icon
with accent-colored tint. "No VODs / select a streamer" now
feels intentional instead of an oversight.
- Streamer-name span dedicated class (.streamer-name +
.streamer-name.is-live) so a live streamer's name itself bolds,
not just gets a dot beside it.
Locale strings: liveNowTooltip ("Currently live on Twitch" / "Aktuell
live auf Twitch") for the dot's tooltip.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
490 lines
24 KiB
TypeScript
490 lines
24 KiB
TypeScript
const UI_TEXT_EN = {
|
|
appName: 'Twitch VOD Manager',
|
|
static: {
|
|
navVods: 'Twitch VODs',
|
|
navClips: 'Twitch Clips',
|
|
navCutter: 'Video Cutter',
|
|
navMerge: 'Merge Videos',
|
|
navSettings: 'Settings',
|
|
queueTitle: 'Queue',
|
|
retryFailed: 'Retry',
|
|
retryFailedHint: 'Retry failed downloads only',
|
|
healthUnknown: 'System: Unknown',
|
|
healthGood: 'System: Stable',
|
|
healthWarn: 'System: Warning',
|
|
healthBad: 'System: Problem',
|
|
clearQueue: 'Clear',
|
|
refresh: 'Refresh',
|
|
streamerPlaceholder: 'Add streamer...',
|
|
clipsHeading: 'Twitch Clip Download',
|
|
clipsInfoTitle: 'Info',
|
|
clipsInfoText: 'Supported formats:\n- https://clips.twitch.tv/ClipName\n- https://www.twitch.tv/streamer/clip/ClipName\n\nClips are saved in your download folder under "Clips/StreamerName/".',
|
|
cutterSelectTitle: 'Select video',
|
|
cutterBrowse: 'Browse',
|
|
mergeTitle: 'Merge videos',
|
|
mergeDesc: 'Select multiple videos to merge into one file. You can change the order before merging.',
|
|
mergeAdd: '+ Add videos',
|
|
designTitle: 'Design',
|
|
themeLabel: 'Theme',
|
|
themeLight: 'Light',
|
|
languageLabel: 'Language',
|
|
languageDe: 'German',
|
|
languageEn: 'English',
|
|
apiTitle: 'Twitch API',
|
|
clientIdLabel: 'Client ID',
|
|
clientSecretLabel: 'Client Secret',
|
|
saveSettings: 'Save & Connect',
|
|
downloadSettingsTitle: 'Download Settings',
|
|
storageLabel: 'Storage Path',
|
|
openFolder: 'Open',
|
|
modeLabel: 'Download Mode',
|
|
modeFull: 'Full VOD',
|
|
modeParts: 'Split into parts',
|
|
partMinutesLabel: 'Part Length (Minutes)',
|
|
parallelDownloadsLabel: 'Parallel Downloads',
|
|
parallelDownloads1: '1 (Default)',
|
|
parallelDownloads2: '2 (Parallel)',
|
|
performanceModeLabel: 'Performance Profile',
|
|
performanceModeStability: 'Max Stability',
|
|
performanceModeBalanced: 'Balanced',
|
|
performanceModeSpeed: 'Max Speed',
|
|
smartSchedulerLabel: 'Enable smart queue scheduler',
|
|
smartSchedulerHint: 'Prefers shorter VODs and older queue entries first so the queue throughput stays steady. Disable to drain in strict insertion order.',
|
|
streamerInvalid: 'Invalid Twitch username (4-25 chars, letters/digits/underscore).',
|
|
apiHelpIntro: 'You need a Client ID and Client Secret from Twitch.',
|
|
apiHelpLinkText: 'dev.twitch.tv/console/apps',
|
|
openDebugLogFile: 'Open log file',
|
|
storageCardTitle: 'Storage',
|
|
storageCardIntro: 'Per-streamer disk usage in the current download folder. Live recordings are surfaced separately.',
|
|
storageRefresh: 'Refresh',
|
|
storageEmpty: 'Download folder is empty or unreadable.',
|
|
storageScanning: 'Scanning...',
|
|
storageSummary: 'Total: {files} files, {size} — Free disk: {free}',
|
|
storageColumnFolder: 'Folder',
|
|
storageColumnFiles: 'Files',
|
|
storageColumnTotal: 'Total',
|
|
storageColumnLive: 'Live',
|
|
storageColumnChat: 'Chat',
|
|
storageOpen: 'Open',
|
|
storageOtherFolders: 'Other folders in download path',
|
|
cleanupTitle: 'Auto-cleanup',
|
|
cleanupIntro: 'Move recordings older than N days to an archive folder, or delete them outright. Sibling chat files (.chat.json/.chat.jsonl) travel with the video.',
|
|
cleanupEnabledLabel: 'Enable auto-cleanup',
|
|
cleanupDaysLabel: 'Age threshold (days)',
|
|
cleanupTargetLabel: 'Scope',
|
|
cleanupTargetLive: 'Live recordings only',
|
|
cleanupTargetAll: 'All recordings',
|
|
cleanupActionLabel: 'Action',
|
|
cleanupActionArchive: 'Move to archive folder',
|
|
cleanupActionDelete: 'Delete',
|
|
cleanupDryRun: 'Preview',
|
|
cleanupRunNow: 'Run now',
|
|
cleanupReportPreview: 'Would touch {count} files (~{size}). No files have been moved or deleted.',
|
|
cleanupReportDone: 'Processed {count} files, freed ~{size}.{failed}',
|
|
cleanupReportFailedSuffix: ' {failed} failed.',
|
|
cleanupReportEmpty: 'No recordings older than {days} days found.',
|
|
discordCardTitle: 'Discord webhook',
|
|
discordCardIntro: 'Send notifications to a Discord channel via webhook — handy for multi-device setups or a dedicated archive machine.',
|
|
discordWebhookUrlLabel: 'Webhook URL',
|
|
discordNotifyLiveStartLabel: 'Notify on live recording start',
|
|
discordNotifyLiveEndLabel: 'Notify on live recording end',
|
|
discordNotifyVodCompleteLabel: 'Notify on completed VOD download',
|
|
autoResumeLiveRecordingLabel: 'Auto-resume live recording if streamlink crashes (max 5 retries)',
|
|
autoMergeResumedPartsLabel: 'Auto-merge resumed-recording parts into one file (ffmpeg concat, no re-encode)',
|
|
deletePartsAfterMergeLabel: 'Delete individual parts after successful merge',
|
|
discordNotifyVodAutoQueuedLabel: 'Notify when a VOD gets auto-queued',
|
|
autoVodCardTitle: 'Auto-VOD download',
|
|
autoVodCardIntro: 'Streamers with the VOD toggle on are scanned for new Twitch VODs at the interval set here. New VODs within the age window are added to the download queue automatically.',
|
|
autoVodPollMinutesLabel: 'Poll interval (minutes)',
|
|
autoVodMaxAgeHoursLabel: 'Max age (hours)',
|
|
autoVodScanNow: 'Scan now',
|
|
autoRecordScanNow: 'Check live status',
|
|
statsTitle: 'Archive statistics',
|
|
statsIntro: 'Aggregated across the download folder. Live recordings live under {streamer}/live/, VOD downloads under {streamer}/. Scan time scales with file count.',
|
|
statsRefresh: 'Refresh',
|
|
statsScanning: 'Scanning...',
|
|
statsScannedAt: 'Last scan',
|
|
statsScannedAtNever: 'Not yet scanned',
|
|
statsSummaryTitle: 'Overview',
|
|
statsTopStreamersTitle: 'Top streamers (by size)',
|
|
statsActivityTitle: 'Activity (last 30 days)',
|
|
statsSizeBucketsTitle: 'Recording-size distribution',
|
|
statsTotalRecordings: 'Recordings total',
|
|
statsLiveRecordings: 'Live recordings',
|
|
statsVodRecordings: 'VOD downloads',
|
|
statsStreamers: 'Streamers',
|
|
statsAvgSize: 'Avg. recording size',
|
|
statsChatFiles: 'Chat files',
|
|
statsFiles: 'files',
|
|
statsActivityEmpty: 'No recordings in the last 30 days.',
|
|
statsActivitySummary: '{count} recordings - {size} in the last 30 days',
|
|
statsEmpty: 'No data.',
|
|
statsNoRoot: 'Download folder not found. Set a download path in Settings first.',
|
|
navStats: 'Statistics',
|
|
navArchive: 'Archive',
|
|
archiveTitle: 'Search archive',
|
|
archiveIntro: 'Search by filename, streamer, or date string. Hits show recordings (Live + VOD); related chat and events files appear as companion buttons.',
|
|
archiveAllTypes: 'All types',
|
|
archiveTypeLive: 'Live recordings',
|
|
archiveTypeVod: 'VOD downloads',
|
|
archiveAllStreamers: 'All streamers',
|
|
archiveSortDateDesc: 'Newest first',
|
|
archiveSortDateAsc: 'Oldest first',
|
|
archiveSortSizeDesc: 'Largest first',
|
|
archiveSortSizeAsc: 'Smallest first',
|
|
archiveSortNameAsc: 'Name (A-Z)',
|
|
archiveSearchBtn: 'Search',
|
|
archiveSearching: 'Scanning...',
|
|
archiveSummary: '{matchCount} matches (scanned {scanned} files)',
|
|
archiveSummaryTruncated: '{matchCount} matches (scanned {scanned} files, showing {shown} - tighten the query for more)',
|
|
archiveNoMatches: 'No matches.',
|
|
archiveNoRoot: 'Download folder not found. Set a download path in Settings first.',
|
|
archiveSearchPlaceholder: 'Search...',
|
|
archiveOpen: 'Open',
|
|
archiveShowInFolder: 'Folder',
|
|
archiveViewChat: 'Chat',
|
|
archiveViewEvents: 'Events',
|
|
backupCardTitle: 'Backup & Maintenance',
|
|
backupCardIntro: 'Back up your configuration, restore it on another machine, or reset the list of already-downloaded VODs.',
|
|
exportConfig: 'Export config',
|
|
importConfig: 'Import config',
|
|
resetDownloadedIds: 'Reset downloaded list',
|
|
configExported: 'Configuration exported.',
|
|
configExportFailed: 'Configuration export failed.',
|
|
configImported: 'Configuration imported. Some changes may need a restart.',
|
|
configImportFailed: 'Configuration import failed.',
|
|
resetDownloadedConfirm: 'Reset the downloaded-VODs list? Cards will lose the green check mark, but no files are deleted.',
|
|
resetDownloadedDone: 'Cleared {count} entries from the downloaded list.',
|
|
duplicatePreventionLabel: 'Prevent duplicate queue entries',
|
|
persistQueueLabel: 'Keep queue between app restarts',
|
|
autoResumeQueueLabel: 'Auto-resume the queue on startup',
|
|
autoResumeQueueHint: 'When enabled and the persisted queue has pending entries, downloads kick off ~5 seconds after the window opens. Disable to require an explicit Start click.',
|
|
notifyEachCompletionLabel: 'Notify on every completed download',
|
|
notifyEachCompletionHint: 'Off by default — long queues would otherwise spam the OS notifications panel. The end-of-queue summary notification fires either way.',
|
|
streamlinkDisableAdsLabel: 'Skip Twitch ads while downloading',
|
|
streamlinkDisableAdsHint: 'Passes --twitch-disable-ads to streamlink so mid-roll ads do not get embedded into the VOD output. Recommended on.',
|
|
downloadChatReplayLabel: 'Save chat replay alongside each VOD (.chat.json)',
|
|
downloadChatReplayHint: 'After a VOD download completes, fetches the public chat replay via Twitch GQL and saves it as JSON next to the video. Twitch keeps chat replay only as long as the VOD itself.',
|
|
captureLiveChatLabel: 'Capture live chat during recording (.chat.jsonl)',
|
|
captureLiveChatHint: 'Opens an anonymous IRC connection to Twitch chat during a live recording and appends every message to a sibling .chat.jsonl file (JSON Lines, one message per line) so a long capture can be killed mid-stream without corrupting earlier data.',
|
|
logStreamEventsLabel: 'Log stream events during live recording (.events.jsonl)',
|
|
logStreamEventsHint: 'Polls the streamer once a minute and writes title / game changes to a sibling .events.jsonl file. Useful for seeking inside long archived streams ("when did he switch to CS:GO?"). Cheap — one extra Helix/GQL hit per minute per active recording.',
|
|
streamlinkQualityLabel: 'Stream quality',
|
|
streamlinkQualityHint: 'Streamlink will try this quality first; if the VOD does not offer it, falls back to "best".',
|
|
streamlinkQualityBest: 'Best (default)',
|
|
streamlinkQualitySource: 'Source (original)',
|
|
streamlinkQualityAudio: 'Audio only',
|
|
downloadPathNotWritable: 'Download folder is not writable. Pick another folder or grant write permission.',
|
|
streamerSectionTitle: 'Streamer',
|
|
streamerListFilterPlaceholder: 'Filter...',
|
|
streamerBulkRemoveTitle: 'Remove all (or filtered)',
|
|
streamerBulkRemoveAll: 'Remove all {count} streamers from the list?',
|
|
streamerBulkRemoveFiltered: 'Remove the {count} matching streamer(s) from the list?',
|
|
cutterDropHint: 'Drop a video file here to load it.',
|
|
metadataCacheMinutesLabel: 'Metadata Cache (Minutes)',
|
|
filenameTemplatesTitle: 'Filename Templates',
|
|
vodTemplateLabel: 'VOD Template',
|
|
partsTemplateLabel: 'VOD Part Template',
|
|
defaultClipTemplateLabel: 'Clip Template',
|
|
filenameTemplateHint: 'Placeholders: {title} {id} {channel} {date} {part} {part_padded} {trim_start} {trim_end} {trim_length} {date_custom="yyyy-MM-dd"}',
|
|
vodTemplatePlaceholder: '{title}.mp4',
|
|
partsTemplatePlaceholder: '{date}_Part{part_padded}.mp4',
|
|
defaultClipTemplatePlaceholder: '{date}_{part}.mp4',
|
|
templateLintOk: 'Template check: OK',
|
|
templateLintWarn: 'Unknown placeholder(s)',
|
|
templateGuideButton: 'Template Guide',
|
|
templateGuideTitle: 'Filename Template Guide',
|
|
templateGuideIntro: 'Use placeholders for filenames and test your pattern with a live preview.',
|
|
templateGuideTemplateLabel: 'Template',
|
|
templateGuideOutputLabel: 'Live preview',
|
|
templateGuideVarsTitle: 'Available placeholders',
|
|
templateGuideVarCol: 'Placeholder',
|
|
templateGuideDescCol: 'Description',
|
|
templateGuideExampleCol: 'Example',
|
|
templateGuideUseVod: 'Use VOD template',
|
|
templateGuideUseParts: 'Use part template',
|
|
templateGuideUseClip: 'Use clip template',
|
|
templateGuideClose: 'Close',
|
|
templateGuideContextVod: 'Context: Sample full VOD download',
|
|
templateGuideContextParts: 'Context: Sample split VOD part',
|
|
templateGuideContextClip: 'Context: Sample clip trim',
|
|
templateGuideContextClipLive: 'Context: Current clip dialog selection',
|
|
runtimeMetricsTitle: 'Runtime Metrics',
|
|
runtimeMetricsRefresh: 'Refresh',
|
|
runtimeMetricsExport: 'Export JSON',
|
|
runtimeMetricsAutoRefresh: 'Auto refresh',
|
|
runtimeMetricsLoading: 'Loading metrics...',
|
|
runtimeMetricsError: 'Could not load runtime metrics.',
|
|
runtimeMetricsExportDone: 'Runtime metrics exported successfully.',
|
|
runtimeMetricsExportCancelled: 'Runtime metrics export cancelled.',
|
|
runtimeMetricsExportFailed: 'Runtime metrics export failed.',
|
|
runtimeMetricQueue: 'Queue',
|
|
runtimeMetricMode: 'Mode',
|
|
runtimeMetricRetries: 'Retries',
|
|
runtimeMetricIntegrity: 'Integrity failures',
|
|
runtimeMetricCache: 'Cache',
|
|
runtimeMetricBandwidth: 'Bandwidth',
|
|
runtimeMetricDownloads: 'Downloads',
|
|
runtimeMetricActive: 'Active item',
|
|
runtimeMetricLastError: 'Last error class',
|
|
runtimeMetricUpdated: 'Updated',
|
|
updateTitle: 'Updates',
|
|
checkUpdates: 'Check for updates',
|
|
preflightTitle: 'System Check',
|
|
preflightRun: 'Run check',
|
|
preflightFix: 'Auto-fix tools',
|
|
preflightEmpty: 'No checks run yet.',
|
|
preflightChecking: 'Checking...',
|
|
preflightFixing: 'Fixing...',
|
|
preflightReady: 'Everything is ready.',
|
|
preflightInternet: 'Internet',
|
|
preflightStreamlink: 'Streamlink',
|
|
preflightFfmpeg: 'FFmpeg',
|
|
preflightFfprobe: 'FFprobe',
|
|
preflightPath: 'Download path',
|
|
debugLogTitle: 'Live Debug Log',
|
|
refreshLog: 'Refresh',
|
|
autoRefresh: 'Auto refresh',
|
|
notConnected: 'Not connected'
|
|
},
|
|
status: {
|
|
noLogin: 'No login (public mode)',
|
|
connecting: 'Connecting...',
|
|
connected: 'Connected',
|
|
connectFailedPublic: 'Connection failed - public mode active'
|
|
},
|
|
tabs: {
|
|
vods: 'VODs',
|
|
clips: 'Clips',
|
|
cutter: 'Video Cutter',
|
|
merge: 'Merge Videos',
|
|
stats: 'Statistics',
|
|
archive: 'Archive',
|
|
settings: 'Settings'
|
|
},
|
|
queue: {
|
|
empty: 'No downloads in queue',
|
|
start: 'Start',
|
|
stop: 'Pause',
|
|
resume: 'Resume',
|
|
statusDone: 'Completed',
|
|
statusFailed: 'Failed',
|
|
statusRunning: 'Running',
|
|
statusPaused: 'Paused',
|
|
statusWaiting: 'Waiting',
|
|
progressError: 'Error',
|
|
progressReady: 'Ready',
|
|
progressLoading: 'Loading...',
|
|
readyToDownload: 'Ready to download',
|
|
started: 'Download started',
|
|
done: 'Done',
|
|
failed: 'Download failed',
|
|
speed: 'Speed',
|
|
eta: 'ETA',
|
|
part: 'Part',
|
|
emptyAlert: 'Queue is empty. Add a VOD or clip first.',
|
|
duplicateSkipped: 'This item is already active in the queue.',
|
|
openFile: 'Open file',
|
|
showInFolder: 'Show in folder',
|
|
openFileFailed: 'Could not open the file (it may have been moved or deleted).',
|
|
outputFilesLabel: '{count} output files',
|
|
retryItem: 'Retry this item',
|
|
viewChat: 'View chat',
|
|
viewChatLoading: 'Loading chat...',
|
|
viewChatFailed: 'Could not read chat file',
|
|
viewChatCount: '{count} messages',
|
|
viewChatTruncatedSuffix: ' (truncated)',
|
|
viewEvents: 'View events',
|
|
viewEventsCount: '{count} events',
|
|
viewEventsEmpty: 'No events recorded.',
|
|
eventStartedAs: 'Started as',
|
|
eventEndedAfter: 'Ended after',
|
|
eventTitleFromTo: 'Title: {from} -> {to}',
|
|
eventGameFromTo: 'Game: {from} -> {to}',
|
|
statusBarSummary: '{downloading} dl, {pending} queued',
|
|
ctxMoveTop: 'Move to top',
|
|
ctxMoveBottom: 'Move to bottom',
|
|
ctxCopyUrl: 'Copy URL',
|
|
ctxOpenOnTwitch: 'Open on Twitch',
|
|
ctxRemove: 'Remove from queue',
|
|
ctxCopiedUrl: 'URL copied to clipboard.',
|
|
liveRecordingTitle: 'Live recording — captures until the stream ends',
|
|
recordingHealth: {
|
|
ok: 'Healthy — bytes flowing',
|
|
stale: 'Stalled — no bytes recently (network blip or stream ending)',
|
|
unknown: 'Waiting for first segment'
|
|
},
|
|
eventRecordingResume: 'Recording resumed — starting part {part}'
|
|
},
|
|
profile: {
|
|
liveBadge: 'LIVE',
|
|
partner: 'Partner',
|
|
affiliate: 'Affiliate',
|
|
followers: 'Followers',
|
|
vods: 'VODs',
|
|
vodsTooltip: 'VODs visible via Twitch API for this channel',
|
|
lastStream: 'Last stream',
|
|
openTwitch: 'Open on Twitch',
|
|
openTwitchTooltip: 'Open this channel on twitch.tv',
|
|
liveCardTooltip: 'Click to start a live recording right now',
|
|
recordNow: 'Record now',
|
|
refresh: 'Refresh',
|
|
agoMinutes: '{n} min ago',
|
|
agoHours: '{n} h ago',
|
|
agoDays: '{n} d ago',
|
|
agoMonths: '{n} mo ago',
|
|
agoYears: '{n} y ago'
|
|
},
|
|
streamers: {
|
|
recordLiveTitle: 'Record this streamer live (captures until stream ends)',
|
|
liveRecordingStarted: 'Live recording started for {streamer}.',
|
|
liveRecordingOffline: '{streamer} is offline right now.',
|
|
liveRecordingAlreadyActive: 'Already recording {streamer}.',
|
|
liveRecordingFailed: 'Could not start live recording',
|
|
autoRecordTitle: 'Auto-record: when this streamer goes live the app records automatically',
|
|
autoRecordEnabled: 'Auto-record enabled for {streamer}. Polling for live state...',
|
|
autoRecordDisabled: 'Auto-record disabled for {streamer}.',
|
|
autoVodTitle: 'Auto-download new VODs (recently published) for this streamer',
|
|
autoVodEnabled: 'Auto-VOD enabled for {streamer}. Will pick up new VODs.',
|
|
autoVodDisabled: 'Auto-VOD disabled for {streamer}.',
|
|
autoVodScanQueued: '{count} new VOD(s) auto-queued.',
|
|
autoVodScanEmpty: 'No new VODs found.',
|
|
autoRecordScanTriggered: 'Manual scan: {count} live recording(s) started.',
|
|
autoRecordScanEmpty: 'Manual scan: no streamers currently live.',
|
|
liveNowTooltip: 'Currently live on Twitch'
|
|
},
|
|
vods: {
|
|
noneTitle: 'No VODs',
|
|
noneText: 'Select a streamer from the list.',
|
|
loading: 'Loading VODs...',
|
|
notFound: 'Streamer not found',
|
|
noResultsTitle: 'No VODs found',
|
|
noResultsText: 'This streamer has no VODs.',
|
|
untitled: 'Untitled VOD',
|
|
views: 'views',
|
|
addQueue: '+ Queue',
|
|
trimButton: 'Trim VOD',
|
|
filterPlaceholder: 'Filter by title... (Ctrl+F)',
|
|
filterClearTitle: 'Clear filter (Esc)',
|
|
filterNoMatchTitle: 'No matches',
|
|
filterNoMatchText: 'No VODs match the current filter.',
|
|
filterMatchCount: '{shown} of {total} VODs',
|
|
sortLabel: 'Sort:',
|
|
sortDateDesc: 'Newest first',
|
|
sortDateAsc: 'Oldest first',
|
|
sortViewsDesc: 'Most viewed',
|
|
sortDurationDesc: 'Longest first',
|
|
sortDurationAsc: 'Shortest first',
|
|
bulkSelectedCount: '{count} selected',
|
|
bulkAddToQueue: '+ Queue',
|
|
bulkAdding: 'Adding...',
|
|
bulkClear: 'Clear',
|
|
bulkAddedToQueue: 'Added {count} VODs to the queue.',
|
|
bulkAddSkipped: 'No VODs were added (already in queue or invalid).',
|
|
bulkMarkDownloaded: 'Mark as downloaded',
|
|
bulkUnmark: 'Unmark',
|
|
bulkMarkedDownloaded: 'Marked {count} VODs as downloaded.',
|
|
bulkUnmarkedDownloaded: 'Removed {count} VODs from the downloaded list.',
|
|
alreadyDownloaded: 'Already downloaded',
|
|
hideDownloaded: 'Hide downloaded',
|
|
hideDownloadedTitle: 'Hide VODs that are marked as already downloaded',
|
|
openOnTwitch: 'Open on Twitch',
|
|
ctxOpenOnTwitch: 'Open on Twitch',
|
|
ctxCopyUrl: 'Copy VOD URL',
|
|
ctxCopiedUrl: 'URL copied to clipboard.',
|
|
ctxMarkDownloaded: 'Mark as downloaded',
|
|
ctxUnmarkDownloaded: 'Unmark downloaded'
|
|
},
|
|
clips: {
|
|
dialogTitle: 'Trim VOD',
|
|
dialogStart: 'Start:',
|
|
dialogStartTime: 'Start time (HH:MM:SS):',
|
|
dialogEnd: 'End:',
|
|
dialogEndTime: 'End time (HH:MM:SS):',
|
|
dialogDuration: 'Duration: ',
|
|
dialogPartLabel: 'Start part number (optional, for continuation):',
|
|
dialogPartHint: 'Leave empty = part 1',
|
|
dialogFormatLabel: 'Filename format:',
|
|
dialogConfirm: 'Add to queue',
|
|
invalidDuration: 'Invalid!',
|
|
endBeforeStart: 'End time must be greater than start time!',
|
|
outOfRange: 'Time is outside VOD range!',
|
|
enterUrl: 'Please enter a URL',
|
|
loadingButton: 'Loading...',
|
|
loadingStatus: 'Downloading...',
|
|
downloadButton: 'Download clip',
|
|
success: 'Download successful!',
|
|
errorPrefix: 'Error: ',
|
|
unknownError: 'Unknown error',
|
|
formatSimple: '(default)',
|
|
formatTimestamp: '(with timestamp)',
|
|
formatParts: '(parts naming)',
|
|
formatTemplate: '(custom template)',
|
|
templateEmpty: 'Template cannot be empty in custom template mode.',
|
|
templatePlaceholder: '{date}_{part}.mp4',
|
|
templateHelp: 'Placeholders: {title} {id} {channel} {date} {part} {part_padded} {trim_start} {trim_end} {trim_length} {date_custom="yyyy-MM-dd"}'
|
|
},
|
|
cutter: {
|
|
videoInfoFailed: 'Could not read video info. Is FFprobe installed?',
|
|
previewLoading: 'Loading preview...',
|
|
previewUnavailable: 'Preview unavailable',
|
|
cutting: 'Cutting...',
|
|
cut: 'Cut',
|
|
cutSuccess: 'Video cut successfully!',
|
|
cutFailed: 'Failed to cut video.',
|
|
infoDuration: 'Duration',
|
|
infoResolution: 'Resolution',
|
|
infoFps: 'FPS',
|
|
infoSelection: 'Selection',
|
|
startLabel: 'Start:',
|
|
endLabel: 'End:'
|
|
},
|
|
merge: {
|
|
empty: 'No videos selected',
|
|
merging: 'Merging...',
|
|
merge: 'Merge',
|
|
success: 'Videos merged successfully!',
|
|
failed: 'Failed to merge videos.'
|
|
},
|
|
mergeGroup: {
|
|
btn: 'Merge & Split',
|
|
phaseDownloading: 'Downloading VOD',
|
|
phaseMerging: 'Merging...',
|
|
phaseSplitting: 'Splitting Part',
|
|
phaseCleanup: 'Cleaning up...',
|
|
needMinTwo: 'Select at least 2 VODs',
|
|
titleTwo: 'Merge: {title1} + {title2}',
|
|
titleMany: 'Merge: {title1} + {count} more',
|
|
metaLabel: '{count} VODs',
|
|
},
|
|
updates: {
|
|
bannerDefault: 'New version available!',
|
|
latest: 'You are on the latest version!',
|
|
checking: 'Checking for updates...',
|
|
checkInProgress: 'Update check is already running.',
|
|
readyToInstall: 'Update is ready to install.',
|
|
checkFailed: 'Update check failed.',
|
|
downloading: 'Downloading...',
|
|
downloadInProgress: 'Update download is already running.',
|
|
downloadFailed: 'Update download failed.',
|
|
available: 'available!',
|
|
downloadNow: 'Download now',
|
|
downloadLabel: 'Download',
|
|
ready: 'ready to install!',
|
|
installNow: 'Install now & restart',
|
|
modalAvailableTitle: 'Update available',
|
|
modalAvailableMessage: 'Version {version} is available. Download it now?',
|
|
modalReadyTitle: 'Update ready',
|
|
modalReadyMessage: 'Version {version} has been downloaded. Install and restart now?',
|
|
modalDismiss: 'No',
|
|
modalDownloadConfirm: 'Yes, download',
|
|
modalInstallConfirm: 'Yes, install',
|
|
modalSkipVersion: 'Skip this version',
|
|
changelogLabel: 'Changelog',
|
|
showChangelog: 'Show changelog',
|
|
hideChangelog: 'Hide changelog',
|
|
noChangelog: 'No changelog available.',
|
|
releasedLabel: 'Release'
|
|
}
|
|
} as const;
|