Files
TeleWave/frontend/src/shared/lib/locales/en.ts
T
Leonid Pershin bed6d4fcfc
ci / build-backend (push) Successful in 1m18s
ci / build-frontend (push) Successful in 38s
ci / tests (push) Successful in 1m21s
ci / sonar (push) Successful in 3m46s
Add diagnostics endpoints and types for improved system monitoring
Implemented new diagnostics endpoints in the API and added corresponding route configurations in the frontend. Introduced types for diagnostics data, including host, forwarding, and system diagnostics, to enhance monitoring capabilities. Updated localization files to support diagnostics UI elements in both English and Russian, ensuring a comprehensive user experience.
2026-08-02 13:49:48 +03:00

1147 lines
47 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/** Словарь локали «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 parts 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: AZ',
nameDesc: 'Name: ZA',
},
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 blocks audio is trimmed to it, and without audio the clips 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': 'Tonights programme',
'tonight.time': 'Tonights start time',
'tomorrow.title': 'Tomorrows programme',
'tomorrow.time': 'Tomorrows 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}}.',
},
},
}