Twitch-VOD-Manager/src/renderer-locale-en.ts
xRangerDE 3c73efbad7 feat: banner background + live preview card + VOD hover storyboard + sticky header
Four interlocking visual upgrades that push the profile area from
"works" to "looks like a real Twitch app". Single release because
all four share data plumbing and need to land coherently.

1) Banner background — getStreamerProfile now also pulls
   bannerImageURL via public GQL, fetches the bytes server-side as a
   data URL (same path as the avatar fix in 4.6.18-4.6.19), and the
   renderer puts it behind the header content with blur(18px) +
   saturate(1.2) + a 0.55 opacity overlay. Result: per-streamer
   colour identity at a glance, like twitch.tv's channel page.

2) Live preview card — when isLive, the public-GQL stream block also
   carries previewImageURL(640x360), viewersCount, title, game{name}.
   A second card slides in below the main profile row showing the
   current frame at 240×135, eye-icon viewer count, big bold title,
   game, and a red "Jetzt aufnehmen" CTA. Click anywhere on the card
   OR on the button triggers triggerLiveRecording — same path as
   the sidebar REC dot, so the recording reaches the queue with
   identical settings.

3) VOD hover storyboard — Twitch ships a seekPreviewsURL per VOD
   pointing at a JSON manifest of sprite-sheet images, each a grid
   of preview thumbnails spanning the recording. New IPC
   get-vod-storyboard fetches the manifest, picks the high-quality
   first sprite, fetches its bytes as a data URL, and returns the
   grid metadata. Renderer (new renderer-vod-hover.ts) hooks
   delegated mouseover on #vodGrid: 220ms debounce, then on
   activation overlays a div positioned over the thumbnail with
   background-image=sprite + a setInterval cycling
   background-position through 4 evenly-spaced cells at 600ms each.
   Per-VOD result cached client-side so repeated hovers don't
   re-fetch. Negative results (private VODs, expired) are also
   cached so we don't re-query a known-empty manifest.

4) Sticky header — position:sticky;top:0;z-index:20 plus a
   backdrop-filter:blur(6px) so the VOD grid scrolling underneath
   reads through the banner subtly. Header stays anchored to the top
   of .content as the user scrolls hundreds of VODs.

GQL refresher: the public schema rejects `broadcasterType` but
accepts `roles{isPartner isAffiliate}`, plus the same query now
includes bannerImageURL and stream{previewImageURL viewersCount
title game{name}}. One single roundtrip pulls everything we need
for the header AND the live card. The old separate-follower-count
roundtrip (fetchOnlyFollowerCount) is now redundant but kept around
for back-compat in case other call sites grow into it.

Also: profile layout switched from one big flex row to a relative
container with two children (.streamer-profile-row for the meta,
.streamer-profile-live-card for the live block). The .live-card
only renders when isLive — offline streamers get the same compact
header they had before.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 00:55:17 +02:00

489 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.'
},
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;