/** Словарь локали «en». Плоская правка: ключи обеих локалей обязаны совпадать. */ export const en = { appName: 'TeleWave', nav: { dashboard: 'On Air', admin: 'Admin', settings: 'Settings', login: 'Log in', register: 'Sign up', logout: 'Log out', }, theme: { light: 'Light', dark: 'Dark', system: 'System' }, lang: { ru: 'RU', en: 'EN' }, common: { save: 'Save', cancel: 'Cancel', retry: 'Retry', close: 'Close', delete: 'Delete', create: 'Create', loading: 'Loading…', error: 'Something went wrong', search: 'Search', actions: 'Actions', prevPage: 'Previous page', nextPage: 'Next page', yes: 'Yes', no: 'No', }, home: { title: 'TELEWAVE', subtitle: 'BROADCAST CHANNEL GRID', tagline: 'Your channels. Your broadcast. Anytime.', cta: 'Go on air', ctaRegister: 'Create account', }, auth: { userName: 'Username', password: 'Password', loginTitle: 'Sign in', loginSubtitle: 'Enter your credentials to access the channel grid', registerTitle: 'New viewer', registerSubtitle: 'Create an account to set up your channel grid', submitLogin: 'Log in', submitRegister: 'Sign up', noAccount: "Don't have an account?", haveAccount: 'Already have an account?', registrationClosed: 'Registration is closed', registrationClosedHint: 'Public registration is disabled. An administrator can create an account for you.', invalidCredentials: 'Invalid username or password', userNameTaken: 'This username is already taken', blocked: 'Account blocked by an administrator', genericError: 'Could not sign in. Please try again', }, air: { now: 'Now', next: 'Up next', ad: 'Ad', bumper: 'Bumper', fallback: 'Emergency filler', signOff: 'Sign-off', episode: 'Episode', collectionPart: 'part {{part}} of {{total}}', volume: 'Volume', live: 'Live', noChannels: 'No channels available yet. Check back later.', offline: 'This channel is off the air', offlineHint: 'No schedule or content yet. Check back later.', retry: 'Retry', numbersHint: '↑ / ↓ — switch channels by number', iptv: 'IPTV playlist', iptvHint: 'An M3U with every channel and the programme guide — for VLC, TiviMate and the like. Access to the air is baked into the file itself: do not share it.', iptvDone: 'Playlist downloaded. The file grants access to the air — do not pass it on.', iptvFailed: 'Could not build the playlist', }, settings: { title: 'Account settings', changeUserName: 'Change username', newUserName: 'New username', changePassword: 'Change password', currentPassword: 'Current password', newPassword: 'New password', dangerZone: 'Danger zone', deleteAccount: 'Delete account', deleteAccountConfirm: 'The account and all its data will be permanently deleted. Continue?', saved: 'Saved', }, admin: { groups: { title: 'Groups', createTitle: 'Create a group', hint: 'A group is what may go on air. Grid slots reference it; the strategy picks an element from it.', name: 'Name', description: 'Description', items: 'Items', units: 'Units', duration: 'Runtime', hoursShort: 'h', minutesShort: 'min', hasFilter: 'has rule', composition: 'Composition', empty: 'Group is empty', orderHint: 'Drag to set the order — sequential strategies follow it.', showAdvanced: 'Advanced', hideAdvanced: 'Hide', find: 'Find matches', addFound: 'Add found', found: 'Found: {{total}}, new: {{fresh}}', added: 'Items added: {{count}}', alreadyIn: 'already in group', mode: { switchConfirm: 'Switch the mode? The item list will be rebuilt: in a static group it is the composition itself, in a dynamic one — only corrections to the rule.', label: 'Composition', Static: 'Explicit list', Dynamic: 'By rule', StaticHint: 'Items are added by hand; the rule only helps to find them.', DynamicHint: 'Composition is computed by the rule on every read — new library items go on air by themselves.', }, pinned: 'pinned', byRule: 'by rule', excluded: 'Excluded', excludeHint: 'Exclude from the composition — the rule will stop adding it back', restore: 'Restore', dynamicOrderHint: 'Order: pinned items first (drag to arrange), then rule matches alphabetically.', previewComposition: 'With this rule the composition would be: {{count}}', pendingFound: 'New matches for this rule: {{count}} —', pendingAdd: 'Add all ({{count}})', elementKinds: { Show: 'Show', Collection: 'Collection' }, suggestions: { title: 'Suggestions', hint: 'Built from the current library. The button creates a group with these items and saves the rule — later additions are then one click away.', stats: '{{shows}} shows · {{units}} units', exists: 'already exists', created: 'Group created', allCreated: 'Every suggestion has been created already.', showExisting: 'Show already created ({{count}})', hideExisting: 'Hide already created', kinds: { Genre: 'genre', ShowKind: 'kind', Audience: 'rating', BigSeries: 'big series', }, }, filter: { title: 'Selection rule', hint: 'In an explicit list the rule only finds candidates; in a rule-based group it is the composition.', elementKinds: 'What to search', showKinds: 'Show type', showKindsHint: 'Unchecked means films and series. Clips live in their own library and join the selection only when checked explicitly.', genres: 'Genres', genresHint: 'Any of the checked ones.', maxAudience: 'No stricter than', minAudience: 'No softer than', minAudienceHint: 'For late-night slots. Unlike the upper bound, unrated shows are excluded — otherwise unrated cartoons would slip into the adult group.', audienceHint: 'Ratings are ordered by strictness: G → PG → PG-13 → R → NC-17. Unrated shows stay in the selection.', year: 'Year', unitMinutes: 'Unit runtime, min', unitMinutesHint: 'Average episode or film length. Items without ready assets are not filtered out.', from: 'from', to: 'to', any: 'any', }, }, collections: { posters: 'Fill posters', postersHint: 'Takes the first part’s cover for collections that have no poster yet. A hand-picked one is left alone.', postersDone: 'Posters filled: {{count}}', title: 'Collections', createTitle: 'Create a collection', hint: 'A franchise is an ordered set of films played as a single unit.', name: 'Name', description: 'Description', parts: 'Parts', units: 'Units', addShow: 'Add show', pickShow: 'Pick a show', empty: 'Collection is empty', orderHint: 'Drag to set the order of the parts — that is the order they air in.', suggestions: { title: 'Franchise suggestions', hint: 'Built from movies not in any collection yet: what to assemble into a new franchise and what to add to an assembled one. Check the parts — the name-based guess sometimes misses.', parts: '{{count}} parts', exists: 'already exists', additionBadge: 'addition', add: 'Add', added: 'Added to the collection: {{count}}', created: 'Collection created', allCreated: 'Every suggestion has been created already.', showExisting: 'Show already created ({{count}})', hideExisting: 'Hide already created', sources: { Metadata: 'from metadata', NameGuess: 'by names' }, }, }, interstitials: { title: 'Clips', hint: 'Ads, promos and jingles. Drag clips into the block builder or a group on the right — a block is saved as a collection and airs as a whole.', upload: 'Upload clips', name: 'Name', duration: 'Duration', empty: 'No clips yet', noAsset: 'No file', blocks: 'Blocks', noBlocks: 'No blocks yet', clipsCount: '{{count}} clips', blockBuilder: 'Block builder', blockName: 'Block name', blockTotal: 'Block duration', dropHint: 'Drop clips here', groups: 'Clip groups', pickGroup: 'Pick a group', pickGroupFirst: 'Pick a group first', dropToGroup: 'Drop a clip or a block here', openGroup: 'Open group', newGroupName: 'Group name', createGroupTitle: 'Create a clip group', }, genres: { title: 'Genres', hint: 'Genre reference: content groups are built from it, provider metadata is mapped into it.', name: 'Name', slug: 'Key', slugHint: 'Latin letters, no spaces — the genre is matched by it when the reference updates.', aliases: 'Spellings', aliasesHint: 'Comma-separated. Maps provider genres onto yours: tmdb:28, action, боевик.', order: 'Order', usage: 'Shows', system: 'System', create: 'New genre', edit: 'Edit genre', }, roles: { title: 'Roles', name: 'Name', system: 'System', create: 'New role', rename: 'Rename', }, users: { title: 'Users', userName: 'Username', role: 'Role', status: 'Status', createdAt: 'Joined', blocked: 'Blocked', active: 'Active', block: 'Block', unblock: 'Unblock', resetPassword: 'Password', resetPasswordFor: 'Reset password: {{name}}', newPassword: 'New password', passwordReset: 'Password changed', filterAll: 'All roles', createTitle: 'Create user', password: 'Password', passwordHint: 'At least 8 characters, including a digit and an uppercase letter.', create: 'Create', created: 'User created', }, movies: { title: 'Movie import', hint: 'The file name is parsed into a title and a year, and the movie is looked up in the metadata source. Every file becomes its own show; fix the rows where the match is not exact.', sources: { manual: 'From manual folder', disk: 'From my disk' }, pickFiles: 'Pick files', scan: 'Parse ({{count}})', scanning: 'Searching the source…', nothingYet: 'Pick a source and press "Parse".', noProvider: 'No metadata source configured — there is nothing to search with.', file: 'File', parsed: 'Title and year', candidate: 'Candidate', toggleAll: 'Select all / clear all', year: 'Year', research: 'Search again', empty: 'nothing found', noCandidate: 'no metadata', import: 'Create ({{count}})', imported: 'Movies created: {{created}}, with metadata: {{enriched}}', uploadStarted: 'Files are uploading — movies appear as they finish', collectionsHint: 'Franchises turn into collections after the import — the suggestions wait on the Collections tab.', statuses: { Confident: 'exact match', Uncertain: 'check this', NotFound: 'not found — type the title', AlreadyInLibrary: 'already in the library', }, }, media: { title: 'Media', upload: 'Upload', preview: 'Preview', retry: 'Retry', retryAll: 'Retry failed', retryQueued: 'Requeued: {{count}}', manualButton: 'From manual folder', importButton: 'Import', importTitle: 'Media import', importHint: 'Three ways to fill the library: episodes from the manual folder into a show, movies in bulk, and uploading picked files into a show.', importTabs: { movies: 'Movies', manual: 'Episodes from manual', toShow: 'Picked files into a show', }, moviesButton: 'Movies', manualTitle: 'Manual pick from the manual folder', manualHint: 'Files in manual/ are not picked up by the scanner — select the ones you need and choose a show. Imported files leave the folder, just like from inbox.', manualSelectAll: 'Select all', manualSelected: 'Selected: {{count}}', manualEmpty: 'The manual folder is empty', regexPickHint: 'Click a number in the file name — a rule for all files is built from it:', regexPickTitle: 'This is the episode number', regexPresets: 'Ready-made:', regexPresetNames: { seriesWord: 'Серия N', episodeWord: 'Эпизод N', seasonEpisode: 'SxxEyy', productionCode: '2ACV01', afterDash: 'after a dash', firstNumber: 'first number', }, regexClear: 'clear', manualAlready: 'already in the library', manualRoot: 'manual/ root', manualRecognized: 'Recognized: {{count}} of {{total}}', manualCleanupHint: 'Files leave the folder; siblings (subtitles, nfo) and the emptied folder are removed.', manualTruncated: 'Showing the first 500 files — there are more in the folder.', manualShow: 'Show', manualDetected: 'Detected from the release name — check it and change if wrong.', manualPickShow: 'Pick a show', manualImport: 'Import into show', manualImported: 'Files imported: {{count}}', uploadToShow: 'Upload to show', toShowTitle: 'Upload and add to show', toShowPick: 'Pick files', toShowEmpty: 'No files picked', toShowPicked: 'Files picked: {{count}}', autoDetectHint: 'Each file is linked to the show whose original (or display) name appears in the release name, e.g. “The.Simpsons.S33E01…” → The Simpsons.', toShowLibrary: 'To library', toShowSeason: 'Season (manual)', toShowAuto: 'auto', toShowRegex: 'Episode regex', toShowRegexInvalid: 'invalid regex', toShowHint: String.raw`Season and regex are optional: numbers are usually detected automatically (see below). Regex: 1 group = episode, 2 groups = season and episode. Example: ^(\d+) for “01. Title.mkv”.`, toShowPreview: 'What we detect', toShowMatched: 'show detected for {{matched}} of {{total}}', applyToAll: 'Set for all…', toShowUnknown: '—', toShowConfirm: 'Upload and add', uploadedCount: 'Uploaded files: {{count}}', uploadingCount: 'Uploading {{done}}/{{total}}', cancelAll: 'Cancel all uploads', skippedDuplicates: 'Skipped duplicates: {{count}}', queueDrained: 'Processing finished — the queue is empty', filterActive: 'Active', filterAll: 'All', name: 'File', status: 'Status', duration: 'Duration', resolution: 'Resolution', processingTime: 'Processing time', empty: 'No uploaded files yet', statuses: { Pending: 'Queued', Processing: 'Processing', Ready: 'Ready', Failed: 'Failed', }, stats: { queued: 'Currently queued', queuedShort: 'Queued', processing: 'Currently processing', processingShort: 'Processing', average: 'Average processing time (recent)', averageShort: 'Avg time', eta: 'Estimated time until the queue drains: work left at the average rate, divided by the number of parallel transcodes', etaShort: 'Left', etaSoon: 'under a minute', hoursShort: 'h', minutesShort: 'min', }, }, gallery: { title: 'Gallery', upload: 'Upload', empty: 'No images in this category yet', pickHint: 'Pick an image or upload a new one', browseHint: 'All app images by category', sort: { newest: 'Newest first', oldest: 'Oldest first', nameAsc: 'Name: A–Z', nameDesc: 'Name: Z–A', }, categories: { Library: 'Library', ShowPoster: 'Show posters', EpisodeStill: 'Episode stills', BumperBackground: 'Bumper backgrounds', }, }, shows: { title: 'Shows', createTitle: 'Create a show', name: 'Name', originalName: 'Original name (eng)', kind: 'Kind', kinds: { Series: 'Series', Single: 'Movie', Interstitial: 'Clip' }, pickerPlaceholder: 'Pick a show', pickerSearch: 'Search by name…', pickerEmpty: 'Nothing found', pickerCreate: 'Create series “{{name}}”', pickerCreateSingleHint: 'Create as a movie', pickerCreated: 'Show created', audience: 'Rating', audienceUnset: 'Not rated', audiences: { G: 'G — general audiences', PG: 'PG — parental guidance', 'PG-13': 'PG-13 — over 13', R: 'R — under 17 with adult', 'NC-17': 'NC-17 — adults only', }, seasons: 'Seasons', loadedSeasons: 'Loaded seasons', genre: 'Genre', allGenres: 'All genres', allKinds: 'All kinds', deleteTitle: 'Delete the show?', deleteHint: '“{{name}}” will be removed from the library. This cannot be undone.', deleteWithMedia: 'Delete its media too: {{count}} file(s)', deleteWithMediaHint: 'Otherwise the files stay in the media list with no owner and keep taking space.', deleteWithSchedule: 'Cut it from the upcoming air too', deleteWithScheduleHint: 'Without this the deletion is refused while the show sits in the feed. Viewers see the filler where it was until the tail is rebuilt.', otherGenres: 'Other genres', bulk: { selected: 'Selected: {{count}}', selectPage: 'Select all on this page', noGenre: 'Keep genres', keepAudience: 'Keep rating', apply: 'Apply', tagged: 'Shows updated: {{count}}', clear: 'Clear selection', enrich: 'Metadata from {{provider}}', enrichHint: 'Search by name and apply — only for shows with no source linked. Ambiguous matches are skipped.', enriching: 'Searching…', enriched: 'Metadata applied: {{count}}', moreSkipped: 'Skipped as well: {{count}}', toCollection: 'To collection…', toCollectionHint: 'The selected shows go into a franchise. Parts are ordered by release year; drag to rearrange them in the collection card.', newCollection: 'New collection', addToCollection: 'Add ({{count}})', addedToCollection: '“{{name}}” updated: {{count}}', }, genresEmpty: 'No genres set', genresEdit: 'Genres', genresHint: 'Pick the genres of the show. The primary one is listed; all of them are used for selection.', genrePrimary: 'primary', inGroups: 'In groups', inChannels: 'Scheduled on', inCollections: 'Part of collections', episodes: 'Episodes', episode: 'Episode', noEpisodes: 'No episodes yet', addMedia: 'Add media', addMediaHint: 'Ready files not yet in this show. The selected ones are added as episodes.', filterAssets: 'Filter by name, e.g. Family.Guy.S16', selectAll: 'Select all', deselectAll: 'Clear', addSelected: 'Add selected', noMatches: 'No matching files', addedCount: 'Episodes added: {{count}}', candidatesTruncated: 'Not all files are shown (too many ready assets) — narrow the filter to see the rest.', }, channels: { number: 'Number', numberPlaceholder: 'not set', utcOffset: 'Time zone, h', utcOffsetHint: 'Whole hours: 3 is Moscow, 0 is UTC, −5 is New York. The whole grid and the schedule are expressed in this time.', dayStart: 'Broadcast day starts', dayStartHint: 'The night block before this time belongs to the previous day.', disabled: 'disabled', layers: 'Layers', layersHint: 'Higher priority wins. The background layer sits below all and fills gaps.', background: 'background', newLayerName: 'New layer', addSlotHere: 'Add slot', newSlot: 'New slot', /** Grid auto-build by profile: plan preview and creation. */ transfer: { action: 'Import / export', title: 'Grid configuration exchange', hint: 'The file references groups and junctions by name, so it moves between channels and installations. The AI request is not sent anywhere — you copy it into your own model and bring the answer back to the import tab.', tabs: { export: 'Export', import: 'Import', ai: 'AI request' }, exportHint: 'Exports layers, slots, rules and references to groups and junctions. Works both as a backup before an experiment and as a sample for a model.', exportAction: 'Download file', exported: 'File exported', importHint: 'Load a file or paste JSON — a model answer, for example. The file may bring missing groups and collections with it; shows are never created — those come from media uploads. Unknown references do not break the import: such slots are skipped and listed below.', pickFile: 'Pick a file', replace: 'Remove existing slots', pastePlaceholder: 'Paste the grid configuration JSON', importAction: 'Import', imported: 'Slots created: {{slots}}, skipped: {{skipped}}', importedLibrary: 'Groups created: {{groups}}, collections: {{collections}}', badJson: 'That does not look like JSON — check that the whole answer was copied.', warnings: 'What could not be resolved', aiHint: 'We build the request: channel settings, your groups and library, your references and notes, plus the answer schema. The model may define the groups it needs — the import creates them.', references: 'Reference channels', referencesPlaceholder: 'Cartoon Network, Paramount Comedy', referencesHint: 'Comma-separated. The model matches their rhythm — block length, prime, night.', notes: 'Notes', notesPlaceholder: 'For example: kids in the morning, films in the evening, adult block at night', buildPrompt: 'Build the request', copyPrompt: 'Copy', copied: 'Request copied', aiNext: 'Send this to your model and paste the JSON you get back on the "Import" tab.', }, generate: { action: 'Build grid', title: 'Grid auto-build', hint: 'A profile sets the rhythm of the day: block length, what airs in prime time and what fills the night. Groups are picked from the existing ones by rating, content type and how many units they hold.', modes: { FillGaps: 'Fill gaps', Rebuild: 'Rebuild the week', }, modeHints: { FillGaps: 'Existing slots are left alone — only uncovered time is filled.', Rebuild: 'All template slots are removed; the weekend is built as a separate layer on top.', }, summary: 'Slots to create: {{slots}} — that is {{percent}}% of the free week.', willRemove: 'Slots to remove: {{count}}.', fallback: 'The fallback group will be "{{name}}" — it covers pauses between slots.', nothing: 'Nothing to create: there is no free time in the grid.', create: 'Build', done: 'Created {{created}} slots, removed {{removed}}.', }, editSlot: 'Slot', slotTitle: 'Block title', slotStart: 'Start', slotDuration: 'Budget, min', weekday: 'Weekday', everyDay: 'every day', daypart: 'Daypart', slotKind: 'Slot type', group: 'Group', pickGroup: 'Pick a group', strategy: 'Strategy', cooldownDays: 'Cooldown, days', cooldownHint: 'Skip what already aired within this period.', blockMode: 'Block', blockValue: 'Value', overflow: 'If it does not fit', overflowHint: 'What to do with an element that exceeds the slot budget.', repeatDaysAgo: 'Days ago', repeatTime: 'Time', repeatDuration: 'Duration, min', anchor: 'Anchor', anchorHint: 'An anchor keeps the start hard: nothing that would cross it is started. Snapping is soft — it shifts the start to a round time if that fits the allowance.', maxDrift: 'Allowance, min', snap: 'Snap', snapOff: 'off', resizeSlot: 'Drag the edge to change duration', copyDay: 'Copy day', copyDayFrom: 'Copy {{day}} to:', copy: 'Copy', layerVisible: 'Show layer', layerName: 'Layer name', layerApplicability: 'When it applies', applicabilityHint: 'Sections are OR-ed: the layer applies when the date matches at least one condition. Nothing filled in — it always applies.', applicabilityWeekdays: 'Weekdays', applicabilityDateRanges: 'Date ranges', applicabilityAnnual: 'Yearly (month / day)', applicabilityDates: 'Specific dates', applicabilityNone: 'not set', showForDate: 'Grid for date', allDates: 'All layers', tabs: { grid: 'Grid', rules: 'Rules', junctions: 'Junctions', viewer: 'Viewer', settings: 'Settings', air: 'On air', }, noTemplate: 'The channel grid failed to load', createTemplate: 'Create the grid', createTemplateHint: 'An empty grid with a background layer appears — then add layers and slots.', rules: 'Candidate rules', rulesHint: 'Hard filters: they cut out what is not allowed before the draw. Like grid edits, they do not move the air — apply to take effect.', audienceWindows: 'Family hours', noAudienceWindows: 'No windows — the age is not limited.', audienceWindowsHint: 'Inside a window only content no stricter than the chosen category airs. A window may cross midnight. Content with no category is never dropped.', from: 'From', to: 'To', maxAudience: 'No stricter than', repeatLimit: 'Repeat cap', repeatWindowDays: 'Window, days', repeatMax: 'At most, times', repeatLimitHint: 'Counted against the already recorded tape. If every candidate hits the cap the slot is still filled: empty air is worse than an early repeat.', preview: 'Preview', previewHide: 'Hide preview', previewHint: 'A run against the current rules: nothing is written, slot cursors do not move.', previewDays_one: '{{count}} day', previewDays_other: '{{count}} days', previewTabs: { programme: 'Programme', tape: 'Tape', problems: 'Problems' }, noProblems: 'No problems', andMore: 'and {{count}} more', heatmap: 'Repeats: show × day', heatmapTotal: 'total', issues: 'Checks: {{errors}} errors, {{warnings}} warnings', goToSlot: 'to slot', issueKinds: { GroupEmpty: 'Empty group', GroupTooSmall: 'Too little content', GridGap: 'Gap in the grid', SlotOverlap: 'Slots overlap', CooldownUnreachable: 'Unreachable cooldown', AudienceConflict: 'Age conflict', GroupMissing: 'No group selected', }, viewer: 'How viewers see it', viewerHint: 'Overlays are drawn on the client on top of the picture — the video is not re-encoded. Everything is off by default.', logo: 'Logo', noLogo: 'none', pickLogo: 'Pick a logo', logoCorner: 'Corner', logoOpacity: 'Opacity', corners: { TopLeft: 'Top left', TopRight: 'Top right', BottomLeft: 'Bottom left', BottomRight: 'Bottom right', }, showClock: 'Show a clock', analogFilter: 'Analog filter', analogFilterHint: 'Strength 0..1. Zero is off; it is very easy to overdo.', whyHere: 'Why is this here', priority: 'priority', traceLayer: 'Layer', traceSlot: 'Slot', traceGroup: 'Group', traceCollection: 'Collection', traceStrategy: 'Strategy', traceJunction: 'Breaks', traceDrift: 'drift {{minutes}} min', traceSnapped: 'start snapped', traceCooldown: 'cooldown {{days}} d.', traceCandidates: 'candidates after cooldown: {{count}}', diffSummary: 'Affects {{total}} entries, {{changed}} will change', diffSoon: 'Entries changing within 24 hours: {{count}}', diffNoChanges: 'The air will not change', copyTemplate: 'Copy a grid from a channel', pickSourceChannel: 'Pick the source channel', copyHere: 'Copy here', copyTemplateHint: 'Layers, slots, junctions and rules of the chosen channel replace this channel grid. Groups are shared and not copied.', copyTemplateConfirm: 'This channel grid will be replaced with the chosen channel grid. Continue?', templateCopied: 'Copied: {{layers}} layers, {{slots}} slots', copyDroppedBumpers: 'Breaks left without a bumper block: {{count}} — set them up by hand', // Not «Reset»: that is already the revert-to-aired action (the restore key). reset: 'Clear grid', resetConfirm: 'The whole grid — every layer and slot — will be removed, and the future air built from it deleted. The past and the currently playing entry stay. Continue?', resetDone: 'Reset: {{layers}} layers, {{slots}} slots, {{entries}} air entries', resetHint: 'While the channel is on, the tape rebuilds itself from the empty grid — with the fallback group. To keep the air empty, clear it in the rules or turn the channel off.', fallbackGroup: 'Fallback group', fallbackGroupNone: 'None — channel filler only', fallbackGroupHint: 'What fills a pause when even the background layer is empty. An empty grid plays it for the whole air. Set by grid auto-build unless you pick your own.', postChecks: 'Post-checks', breakLimit: 'Breaks per hour cap, min', genreShare: 'Genre share per day cap, %', fallbackShare: 'Background share cap, %', postChecksHint: 'Post-checks run against the finished tape and only warn — nothing is replanned.', previewKinds: { Program: 'Programme', Fallback: 'Background', SignOff: 'Sign-off', Ad: 'Ad', Promo: 'Promo', Bumper: 'Bumper', }, previewLoad: 'Breaks per hour, peak — {{peak}} min', junctions: 'Junctions', junctionsHint: 'What plays between programmes: ads, promos, bumpers. A slot may pick its own junction, otherwise the default one is used.', defaultJunction: 'Default junction', noJunction: 'no junction', junctionBetween: 'Junction inside the slot', junctionAfter: 'Junction after the slot', junctionDefault: 'default', junctionsUsed: 'Used by this channel', junctionsNoneUsed: 'The channel references no junction — nothing plays between programmes.', openJunctionEditor: 'Junction editor', pendingChanges: 'Rules changed — the air still follows the old ones.', restore: 'Discard changes', restoreConfirm: 'Roll the grid back to what is on air? Everything changed since the last apply will be lost.', restored: 'The grid is back to the on-air state: {{count}} slots.', restoreDropped: 'References not restored: {{count}} — the group or junction was deleted after the apply.', apply: 'Apply', applied: 'Air rebuilt, entries: {{count}}', applyReport: 'Apply report', applyReportWarnings: 'The planner could not honour every rule — {{count}} warnings. The air is built, but these spots are worth a look.', weekdays: { 0: 'Sun', 1: 'Mon', 2: 'Tue', 3: 'Wed', 4: 'Thu', 5: 'Fri', 6: 'Sat', }, dayparts: { Morning: 'Morning', Day: 'Day', Prime: 'Prime', Night: 'Night' }, slotKinds: { Content: 'Content', Repeat: 'Repeat', SignOff: 'Sign-off' }, blockModes: { Count: 'Units', Duration: 'Minutes', FillSlot: 'Whole slot' }, overflows: { ContinueNext: 'Continue next time', ExtendSlot: 'Extend the slot', SkipIfNotFits: 'Do not start', }, strategies: { Sequential: 'In order', RandomWithCooldown: 'Random with cooldown', Fixed: 'Fixed element', }, warnings: { SlotEmpty: 'Slot produced no content', DriftExceeded: 'Start drifted past the allowance', CooldownExhausted: 'Cooldown ruled out every candidate', RepeatSourceEmpty: 'Nothing to repeat', FallbackEmpty: 'Nothing to fill pauses with', CandidatesFiltered: 'The age cap ruled out every candidate', BreakLimitExceeded: 'Breaks in an hour exceed the cap', GenreShareExceeded: 'Genre share above the norm', FallbackShareExceeded: 'Emergency filler share above the norm', }, title: 'Channels', createTitle: 'Create a channel', name: 'Name', slug: 'Slug', state: 'State', enabled: 'On air', enabledLabel: 'Channel on air', settings: 'Settings', filler: 'Filler', noFiller: 'No filler', noSchedule: 'Schedule not built yet', airToday: 'Today', airCount: 'Entries for the day: {{count}}', debugExport: 'Debug export', debugExportHint: 'An archive with a snapshot of the channel: grid, groups, slot cursors, tape and a dry run — to work out why the schedule came out the way it did.', airHidePast: 'Hide past entries', airHideBreaks: 'Hide breaks', }, junctions: { title: 'Junctions', hint: 'What plays between programmes: ads, promos, bumpers. A junction is shared across channels — a channel only picks which one a slot uses.', newName: 'New junction', empty0: 'No junctions yet.', empty: 'empty', from: 'end', to: 'start', fork: 'fork', addElement: '+ break', maxTotal: 'Cap, s', usedInChannels: 'in channels: {{count}}', cannotDeleteUsed: 'The junction is used by channels — drop the references first.', splitOut: 'Take out of the fork', mergeIntoFork: 'Merge with the previous one into a fork', element: 'Break', kind: 'Type', kinds: { Ad: 'Ad', Promo: 'Promo', Bumper: 'Bumper', Filler: 'Filler', }, group: 'Group', pickGroup: 'pick a group', bumperTemplate: 'Bumper block', pickBumper: 'pick a block', bumperVariant: 'Text variant', bumperVariantAuto: 'by trigger and weights', bumperVariantHint: 'Keep "by trigger and weights" so show changes and episode breaks get different texts.', amountMode: 'Measured in', amountModes: { Count: 'Units', Duration: 'Minutes' }, count: 'How many units', minutes: 'How many minutes', minutesShort: ' min', amountHint: 'For a mixed group (spots and ready-made blocks) count in minutes: one unit there is either a spot or a whole block.', required: 'Required', requiredHint: 'Required only decides what gets dropped when time runs short. It does not affect the order — that is the position in the chain.', choiceWeight: 'Weight in the fork', choiceWeightHint: 'The higher, the more often this break plays (0 — never picked).', conditions: 'Conditions', onlyOnChange: 'Only on show change', chance: 'Chance, %', chanceHint: 'The interval is per break. A fork rolls once for all its options: the picked variant rolls, and its chance is the chance of the whole fork. The roll comes from the generation seed, so rebuilds do not reshuffle breaks.', minInterval: 'No more often than once per, min', dayparts: 'Dayparts', daypartsHint: 'Nothing selected — the break plays in any daypart.', timeWindow: 'Channel time window', clearWindow: 'Clear', timeWindowHint: 'Empty — any time. The window may cross midnight.', nearHour: 'Near the round hour', nearHourUnit: 'min before and after :00', nearHourHint: '0 — no anchoring. Time signals and jingles work this way: the break only plays around :00, not somewhere within the hour.', badgeNearHour: '±{{minutes}} min off the hour', badgeOnChange: 'on change', badgeInterval: 'once per {{minutes}} min', }, bumpers: { title: 'Bumpers', hint: 'Bumper blocks are shared across channels: look and sound belong to the block, text to its variants. A bumper reaches air as a junction break.', empty: 'No bumper blocks yet.', newName: 'Block name', createTitle: 'Create a bumper block', newNamePlaceholder: 'Block name', newVariantName: 'New text', sampleChannel: 'Preview as channel', sampleChannelNone: 'no channel', sampleChannelHint: 'The block is shared, but sample values come from the selected channel.', name: 'Name', font: 'Font', fontSans: 'Sans', fontSerif: 'Serif', colorBg: 'Background (colour 1)', colorBg2: 'Background (colour 2)', colorAccent: 'Accent', colorText: 'Text', saveStyle: 'Save look', seconds: 's', defaultDuration: '≈8 s (jingle)', variantsCount: 'texts: {{count}}', usedInJunctions: 'in breaks: {{count}}', cannotDeleteUsed: 'The block is used by junction breaks — drop the references first.', audio: 'Sound', audioHint: 'The sound length sets the bumper duration; without it a jingle is synthesised.', background: 'Background', backgroundFieldHint: 'Block background image; otherwise a palette gradient or a show poster.', backgroundPick: 'Pick from gallery', backgrounds: { Template: 'Block background', NextPoster: 'Next show poster', NowPoster: 'Previous show poster', Clip: 'Existing clip', }, backgroundClip: 'Backing clip', pickClip: 'Pick a clip', backgroundClipHint: 'The text is drawn over the video. The clip sets the bumper length; the block’s audio is trimmed to it, and without audio the clip’s own track plays.', fileLoaded: 'loaded', fileDefault: 'default', upload: 'Upload', reset: 'Reset', variants: 'Text variants', variantsHint: 'Different text over the same music and look. Trigger and background are per variant.', addVariant: 'Add text', variantName: 'Name', trigger: 'Show on', triggers: { OnShowChange: 'Show change', BetweenEpisodes: 'Between episodes', Both: 'Both', }, weight: 'Weight', lineStyles: { Label: 'Label', Title: 'Title', Caption: 'Caption' }, lineColors: { Accent: 'Accent', Text: 'Text' }, addLine: 'Line', presets: 'Presets', preset_nowNext: 'Now / Next', preset_nextAt: 'Next at …', preset_channel: 'Channel ident', presetNow: 'NOW', presetNext: 'NEXT', presetNextAt: 'NEXT AT', framePreview: 'Frame', framePreviewHint: 'Sample substitution: this is how the line looks on air.', unknownPlaceholder: 'Unknown placeholder: {{tokens}}', volatileHint: 'every airing is unique — the render cache stops working', render: 'Render samples', rendering: 'Rendering…', renderHint: 'A real ffmpeg render of every variant with sound — takes a few seconds.', tokens: { channel: 'Channel name', 'channel.number': 'Channel number', 'now.title': 'Current show', 'next.title': 'Next show', 'now.episode': 'Current episode', 'next.episode': 'Next episode', 'next.year': 'Next show year', 'next.genre': 'Next show genre', 'next.time': 'Next start time', 'tonight.title': 'Tonight’s programme', 'tonight.time': 'Tonight’s start time', 'tomorrow.title': 'Tomorrow’s programme', 'tomorrow.time': 'Tomorrow’s start time', time: 'Bumper airing time', date: 'Date', weekday: 'Weekday', slot: 'Slot title', }, }, storage: { title: 'Storage', volume: 'Storage volume', volumeUnknown: 'The file system did not report the volume size — only the storage usage is shown.', totalVolume: 'Volume total', ours: 'Used by TeleWave', foreign: 'Other on the volume', foreignHint: 'database, system, foreign files', free: 'Free', lowSpace: 'Free space is below the threshold ({{threshold}}) — new uploads are rejected.', recount: 'Recount', computedAt: 'computed at {{time}}', breakdown: 'What takes the space', filesTotal: 'files: {{count}}', files: 'files: {{count}}', empty: 'The storage is empty.', areas: { Assets: 'Programme segments', BumperAssets: 'Bumper segments', Originals: 'Originals', Inbox: 'Inbox', ManualInbox: 'Manual inbox', Uploads: 'Unfinished uploads', BumperSources: 'Bumper sound and backgrounds', Images: 'Images', Other: 'Other under the root', }, areaHints: { Assets: 'HLS segments of the library — usually almost everything', BumperAssets: 'rendered bumpers; cleaned up along with the schedule', Originals: 'kept only when Storage:KeepOriginals is on', Inbox: 'not picked up by the scanner yet', ManualInbox: 'taken into shows by hand', Uploads: 'staging directory; leftovers mean an aborted upload', BumperSources: 'raw block files, not the segments', Images: 'posters, stills, logos', Other: 'files outside the known directories — usually zero', }, }, diagnostics: { title: 'Diagnostics', refresh: 'Refresh', serverTime: 'server time {{time}}', verdict: { ok: 'Links are built over HTTPS ({{origin}}) — forwarding works.', proxyNotTrusted: 'The proxy sends X-Forwarded-Proto: https, but the immediate peer {{ip}} is not trusted — the scheme stayed HTTP, so M3U/EPG links go out over http.', protoHeaderMissing: 'The scheme stayed HTTP and no X-Forwarded-Proto arrives from the proxy. Configure the proxy to send this header.', applyHint: 'Add to .env and restart the container:', }, copy: 'Copy', copied: 'Copied', forwarding: { title: 'Proxy forwarding', https: 'HTTPS', http: 'HTTP', remoteIp: 'Immediate peer', applied: 'Forwarding applied', appliedYes: 'yes — headers trusted', appliedNo: 'no — headers discarded', forwardLimit: 'Forward limit', flags: 'Processed headers', knownProxies: 'Known proxies', knownNetworks: 'Known networks', }, database: { title: 'Database', reachable: 'Reachable', unreachable: 'Unreachable', latency: 'response {{ms}} ms', }, storage: { title: 'Storage', exists: 'Directory present', missing: 'Directory missing', writable: 'Writable', readOnly: 'Read-only', lowSpace: 'Low space', root: 'Root', free: 'Free / total', }, tools: { available: 'Available', missing: 'Not found', }, host: { title: 'Process and runtime', appVersion: 'Build version', environment: 'Environment', framework: 'Framework', os: 'OS', }, }, telegram: { title: 'Telegram', enable: 'Bot enabled', enabled: 'Enabled', disabled: 'Disabled', saved: 'Saved, connection verified', token: 'Bot token', tokenKept: 'set — leave as is', tokenHint: 'Issued by @BotFather. Never returned: only the fact that it is set.', transport: 'Delivery', transports: { Polling: 'Polling (getUpdates)', Webhook: 'Webhook' }, transportHint: { Polling: 'The server pulls updates itself. Works without a public address and via a proxy.', Webhook: 'Telegram calls your HTTPS endpoint. Needs a public domain; a proxy will not help.', }, webhookUrl: 'Webhook URL', webhookHint: 'Full https URL down to /api/telegram/webhook. The secret is generated for you.', proxy: 'Proxy', proxies: { None: 'No proxy' }, proxyHint: 'How to reach api.telegram.org.', proxyHost: 'Host', proxyPort: 'Port', proxyUser: 'User', proxyPassword: 'Password', lastContact: 'Last contact', subscribers: 'Subscribers', noSubscribers: 'Nobody subscribed yet.', user: 'User', chat: 'Chat', channels: 'Channels', lastSeen: 'Last seen', stopped: 'stopped', openChat: 'Open bot chat', linkFailed: 'Bot is not configured', }, maintenance: { title: 'Maintenance', warning: 'These actions are irreversible — data and files are deleted permanently.', clearMedia: 'Clear all media', clearMediaHint: 'Deletes all uploaded files and their segments, plus channel schedules.', confirmClearMedia: 'Permanently delete ALL media files?', clearShowMedia: 'Delete show media', clearShowMediaHint: "Deletes files of the selected show's episodes and clears its episodes.", confirmClearShowMedia: 'Permanently delete all media of the selected show?', pickShow: 'Pick a show', deleteShows: 'Delete all shows', deleteShowsHint: 'Deletes all shows and their episodes. Media files stay in the library.', confirmDeleteShows: 'Permanently delete ALL shows?', doneCount: 'Deleted: {{count}}', }, settings: { title: 'Settings', registration: 'Registration', registrationHint: 'When off, new users cannot sign up themselves — only an administrator can create accounts.', registrationLabel: 'Allow public registration', channelNumbers: 'Switch channels by number', channelNumbersHint: 'Viewers switch channels with the arrow keys, like on a TV set. The channel grid stays available regardless.', preferredAudio: 'Preferred audio tracks', preferredAudioHint: 'Comma-separated language codes in priority order (e.g. "rus, eng"). If a file has a track in one of these languages, it is picked during processing (by order); otherwise ffmpeg default. Applies to new processing.', }, metadata: { title: 'Metadata', pickPoster: 'From gallery', name: 'Name', originalName: 'Original name (eng)', originalNamePlaceholder: 'e.g. Family Guy', originalNameHint: 'Metadata is looked up by this; screens still show the regular name.', sourceLabel: 'Source', searchBtn: 'Search', nothingFound: 'Nothing found', apply: 'Apply', applied: 'Metadata applied', overview: 'Overview', year: 'Year', clear: 'Clear', noPoster: 'No poster', refreshEpisodes: 'Refresh episodes', refreshing: 'Refreshing…', refreshedCount: 'Episodes updated: {{count}}', findMissing: 'Find missing episodes', missingTitle: 'Missing episodes', missingNoSeasons: 'No loaded episodes with recognized numbers.', seasonN: 'Season {{n}}', loadedOf: 'loaded {{loaded}} of {{total}}', missingUnknown: 'The source did not return the episode count for this season.', missingNone: 'All episodes present.', missingList: 'Missing', findMissingSeasons: 'Find missing seasons', missingSeasonsTitle: 'Missing seasons', missingSeasonsUnknownTotal: 'The source did not return the season list for this series.', missingSeasonsNone: 'All seasons present.', missingSeasonsList: 'Seasons with no loaded episodes', missingSeasonsUnknown: 'The source does not know these loaded seasons: {{list}}.', }, }, }