Files
TeleWave/frontend/src/shared/lib/i18n.ts
T
Leonid Pershin 9c865e1faa
build / backend (push) Successful in 3m23s
build / frontend (push) Successful in 34s
tests / backend-tests (push) Successful in 1m38s
Add CreateTemplate endpoint and related functionality for channels without grids
Implement a new API endpoint to create a template for channels lacking a grid, enhancing the template management capabilities. Update frontend components to support the creation of templates directly from the channel detail view, including user feedback and translations for improved clarity. Add integration tests to ensure the new functionality works as expected.
2026-07-26 20:10:47 +03:00

1428 lines
71 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.
import i18n from 'i18next'
import { initReactI18next } from 'react-i18next'
const resources = {
ru: {
translation: {
appName: 'TeleWave',
nav: {
dashboard: 'Эфир',
admin: 'Админка',
settings: 'Настройки',
login: 'Войти',
register: 'Регистрация',
logout: 'Выйти',
},
theme: { light: 'Светлая', dark: 'Тёмная', system: 'Системная' },
lang: { ru: 'RU', en: 'EN' },
common: {
save: 'Сохранить',
cancel: 'Отмена',
retry: 'Повторить',
delete: 'Удалить',
create: 'Создать',
loading: 'Загрузка…',
error: 'Что-то пошло не так',
search: 'Поиск',
actions: 'Действия',
prevPage: 'Предыдущая страница',
nextPage: 'Следующая страница',
yes: 'Да',
no: 'Нет',
},
home: {
title: 'TELEWAVE',
subtitle: 'ЭФИРНАЯ СЕТКА КАНАЛОВ',
tagline: 'Твои каналы. Твой эфир. В любое время.',
cta: 'Войти в эфир',
ctaRegister: 'Создать аккаунт',
},
auth: {
userName: 'Имя пользователя',
password: 'Пароль',
loginTitle: 'Вход в эфир',
loginSubtitle: 'Введите учётные данные для доступа к сетке каналов',
registerTitle: 'Новый зритель',
registerSubtitle: 'Создайте аккаунт, чтобы настроить свою сетку каналов',
submitLogin: 'Войти',
submitRegister: 'Зарегистрироваться',
noAccount: 'Нет аккаунта?',
haveAccount: 'Уже есть аккаунт?',
registrationClosed: 'Регистрация закрыта',
registrationClosedHint: 'Открытая регистрация отключена. Учётную запись может завести администратор.',
invalidCredentials: 'Неверное имя пользователя или пароль',
userNameTaken: 'Это имя пользователя уже занято',
blocked: 'Аккаунт заблокирован администратором',
genericError: 'Не удалось выполнить вход. Попробуйте ещё раз',
},
air: {
now: 'Сейчас',
next: 'Далее',
ad: 'Реклама',
bumper: 'Заставка',
episode: 'Серия',
live: 'В эфире',
volume: 'Громкость',
noChannels: 'Пока нет доступных каналов. Загляните позже.',
offline: 'Канал сейчас не в эфире',
offlineHint: 'Нет расписания или контента. Загляните позже.',
numbersHint: '↑ / ↓ — переключение каналов по номерам',
retry: 'Повторить',
},
settings: {
title: 'Настройки аккаунта',
changeUserName: 'Смена имени пользователя',
newUserName: 'Новое имя пользователя',
changePassword: 'Смена пароля',
currentPassword: 'Текущий пароль',
newPassword: 'Новый пароль',
dangerZone: 'Опасная зона',
deleteAccount: 'Удалить аккаунт',
deleteAccountConfirm: 'Аккаунт и все данные будут удалены безвозвратно. Продолжить?',
saved: 'Сохранено',
},
admin: {
groups: {
title: 'Группы',
hint: 'Группа — что может попасть в эфир. На неё ссылается слот сетки, из неё стратегия выбирает элемент.',
name: 'Название',
description: 'Описание',
items: 'Позиций',
units: 'Единиц',
duration: 'Объём',
hoursShort: 'ч',
minutesShort: 'мин',
hasFilter: 'с правилом',
composition: 'Состав',
empty: 'Группа пуста',
orderHint: 'Порядок задаётся перетаскиванием — по нему идут последовательные стратегии.',
showAdvanced: 'Дополнительно',
hideAdvanced: 'Скрыть',
find: 'Подобрать',
addFound: 'Добавить найденное',
found: 'Найдено: {{total}}, новых: {{fresh}}',
added: 'Добавлено позиций: {{count}}',
alreadyIn: 'уже в группе',
elementKinds: { Show: 'Шоу', Collection: 'Коллекция' },
filter: {
title: 'Правило набора',
hint: 'Правило только ищет кандидатов — состав группы остаётся явным списком.',
elementKinds: 'Что искать',
showKinds: 'Тип шоу',
genres: 'Жанры',
genresHint: 'Любой из отмеченных.',
maxAudience: 'Возраст не строже',
audienceHint: 'Категории упорядочены по строгости: детское → семейное → … → взрослое.',
year: 'Год',
unitMinutes: 'Длительность единицы, мин',
unitMinutesHint:
'Средняя длина серии или фильма. Позиции без готовых ассетов фильтр не отбрасывает.',
from: 'от',
to: 'до',
any: 'любой',
},
},
collections: {
title: 'Коллекции',
hint: 'Франшиза — упорядоченный набор фильмов, который играется как одно целое.',
name: 'Название',
description: 'Описание',
parts: 'Части',
units: 'Единиц',
addShow: 'Добавить шоу',
empty: 'Коллекция пуста',
orderHint: 'Порядок частей задаётся перетаскиванием — в нём они и пойдут в эфир.',
},
interstitials: {
title: 'Ролики',
hint: 'Реклама, промо и джинглы. Перетащите ролики в сборку блока или в группу справа — блок сохранится коллекцией и пойдёт в эфир целиком.',
upload: 'Загрузить ролики',
name: 'Название',
duration: 'Длительность',
empty: 'Роликов пока нет',
noAsset: 'Без файла',
blocks: 'Блоки',
noBlocks: 'Блоков пока нет',
clipsCount: '{{count}} рол.',
blockBuilder: 'Сборка блока',
blockName: 'Название блока',
blockTotal: 'Длительность блока',
dropHint: 'Перетащите сюда ролики',
groups: 'Группы роликов',
pickGroup: 'Выберите группу',
pickGroupFirst: 'Сначала выберите группу',
dropToGroup: 'Перетащите сюда ролик или блок',
openGroup: 'Открыть группу',
newGroupName: 'Новая группа',
},
genres: {
title: 'Жанры',
hint: 'Справочник жанров: по нему собираются группы контента, в него сводятся жанры из метаданных.',
name: 'Название',
slug: 'Ключ',
slugHint: 'Латиницей, без пробелов — по нему жанр опознаётся при обновлении справочника.',
aliases: 'Варианты написания',
aliasesHint:
'Через запятую. Так жанры провайдеров сводятся к вашему: tmdb:28, action, боевик.',
order: 'Порядок',
usage: 'Шоу',
system: 'Системный',
create: 'Новый жанр',
edit: 'Изменить жанр',
},
roles: {
title: 'Роли',
name: 'Название',
system: 'Системная',
create: 'Новая роль',
rename: 'Переименовать',
},
users: {
title: 'Пользователи',
userName: 'Имя пользователя',
role: 'Роль',
status: 'Статус',
createdAt: 'Регистрация',
blocked: 'Заблокирован',
active: 'Активен',
block: 'Заблокировать',
unblock: 'Разблокировать',
resetPassword: 'Пароль',
resetPasswordFor: 'Сменить пароль: {{name}}',
newPassword: 'Новый пароль',
passwordReset: 'Пароль изменён',
filterAll: 'Все роли',
createTitle: 'Создать пользователя',
password: 'Пароль',
passwordHint: 'Минимум 8 символов, хотя бы одна цифра и заглавная буква.',
create: 'Создать',
created: 'Пользователь создан',
},
media: {
title: 'Медиа',
upload: 'Загрузить',
manualButton: 'Из папки manual',
manualTitle: 'Ручной разбор папки manual',
manualHint:
'Файлы из manual/ не разбираются сканером — выберите нужные и укажите шоу. Импортированные файлы уходят из папки, как и из inbox.',
manualSelectAll: 'Выбрать все',
manualSelected: 'Выбрано: {{count}}',
manualEmpty: 'В папке manual пусто',
regexPickHint: 'Кликните число в имени файла — по нему соберётся правило для всех файлов:',
regexPickTitle: 'Это номер серии',
regexPresets: 'Готовые:',
regexPresetNames: {
seriesWord: 'Серия N',
episodeWord: 'Эпизод N',
seasonEpisode: 'SxxEyy',
afterDash: 'после тире',
firstNumber: 'первое число',
},
regexClear: 'сбросить',
manualAlready: 'уже в библиотеке',
manualRoot: 'корень manual/',
manualRecognized: 'Распознано: {{count}} из {{total}}',
manualCleanupHint:
'Файлы уйдут из папки, спутники (субтитры, nfo) и опустевший каталог будут удалены.',
manualTruncated: 'Показаны первые 500 файлов — в папке есть ещё.',
manualShow: 'Шоу',
manualDetected: 'Определено по имени релиза — проверьте и поправьте, если не то.',
manualPickShow: 'Выберите шоу',
manualImport: 'Забрать в шоу',
manualImported: 'Импортировано файлов: {{count}}',
uploadToShow: 'Загрузить в шоу',
toShowTitle: 'Загрузить и добавить в шоу',
autoDetectHint:
'Каждый файл привяжется к шоу, чьё оригинальное (или отображаемое) название есть в имени релиза, напр. «The.Simpsons.S33E01…» → The Simpsons.',
toShowLibrary: 'В библиотеку',
toShowSeason: 'Сезон (вручную)',
toShowAuto: 'авто',
toShowRegex: 'Regex серии',
toShowRegexInvalid: 'некорректный regex',
toShowHint:
'Сезон и regex — необязательны: обычно номера распознаются сами (см. ниже). Regex: 1 группа = серия, 2 группы = сезон и серия. Пример: ^(\\d+) для «01. Название.mkv».',
toShowPreview: 'Что распознаем',
toShowMatched: 'шоу распознано у {{matched}} из {{total}}',
applyToAll: 'Задать всем…',
toShowUnknown: '—',
toShowConfirm: 'Загрузить и добавить',
uploadedCount: 'Загружено файлов: {{count}}',
uploadingCount: 'Загрузка {{done}}/{{total}}',
cancelAll: 'Отменить все загрузки',
skippedDuplicates: 'Пропущено дубликатов: {{count}}',
filterActive: 'Активные',
filterAll: 'Все',
name: 'Файл',
status: 'Статус',
duration: 'Длительность',
resolution: 'Разрешение',
processingTime: 'Время обработки',
empty: 'Пока нет загруженных файлов',
statuses: {
Pending: 'В очереди',
Processing: 'Обработка',
Ready: 'Готов',
Failed: 'Ошибка',
},
stats: {
queued: 'Сейчас в очереди',
queuedShort: 'В очереди',
processing: 'Сейчас в обработке',
processingShort: 'В обработке',
average: 'Среднее время обработки (по недавним)',
averageShort: 'Ср. время',
},
},
gallery: {
title: 'Галерея',
upload: 'Загрузить',
empty: 'В этой категории пока нет изображений',
pickHint: 'Выберите изображение или загрузите новое',
browseHint: 'Все изображения приложения по категориям',
sort: {
newest: 'Сначала новые',
oldest: 'Сначала старые',
nameAsc: 'Имя: А–Я',
nameDesc: 'Имя: Я–А',
},
categories: {
Library: 'Библиотека',
ShowPoster: 'Постеры шоу',
EpisodeStill: 'Кадры серий',
BumperBackground: 'Фоны заставок',
},
},
shows: {
title: 'Шоу',
name: 'Название',
originalName: 'Оригинальное название (eng)',
kind: 'Тип',
kinds: { Series: 'Сериал', Single: 'Полнометражка', Interstitial: 'Ролик' },
audience: 'Категория',
audiences: {
Kids: 'Детское',
Family: 'Семейное',
Teen: 'Подростковое',
General: 'Общее',
Adult: 'Взрослое',
},
seasons: 'Сезоны',
loadedSeasons: 'Загружены сезоны',
genre: 'Жанр',
allGenres: 'Все жанры',
genresEmpty: 'Жанры не проставлены',
genresEdit: 'Жанры',
genresHint:
'Отметьте жанры шоу. Основной показывается в списке; в отборе контента участвуют все.',
genrePrimary: 'основной',
inCollections: 'Входит в коллекции',
episodes: 'Серии',
episode: 'Серия',
noEpisodes: 'Серий пока нет',
filterAssets: 'Фильтр по имени, напр. Family.Guy.S16',
selectAll: 'Выбрать все',
deselectAll: 'Снять все',
addSelected: 'Добавить выбранные',
noMatches: 'Нет подходящих файлов',
addedCount: 'Добавлено серий: {{count}}',
candidatesTruncated:
'Показаны не все файлы (слишком много готовых ассетов) — уточните фильтр, чтобы увидеть остальные.',
},
channels: {
number: 'Номер',
numberPlaceholder: 'не задан',
utcOffset: 'Часовой пояс, ч',
utcOffsetHint:
'Целыми часами: 3 — Москва, 0 — UTC, −5 — Нью-Йорк. В этом времени задаётся вся сетка и показывается расписание.',
dayStart: 'Начало вещательных суток',
dayStartHint: 'Ночной блок до этого времени относится к предыдущему дню.',
disabled: 'выключен',
grid: 'Сетка',
layers: 'Слои',
layersHint: 'Побеждает слой с большим приоритетом. Фоновый лежит под всеми и закрывает дыры.',
background: 'фон',
newLayerName: 'Новый слой',
addSlotHere: 'Добавить слот',
newSlot: 'Новый слот',
editSlot: 'Слот',
slotTitle: 'Название блока',
slotStart: 'Начало',
slotDuration: 'Бюджет, мин',
weekday: 'День недели',
everyDay: 'каждый день',
daypart: 'Дейпарт',
slotKind: 'Тип слота',
group: 'Группа',
pickGroup: 'выберите группу',
strategy: 'Стратегия',
cooldownDays: 'Остывание, дней',
cooldownHint: 'Не брать то, что уже выходило за этот срок.',
blockMode: 'Блок',
blockValue: 'Значение',
overflow: 'Если не помещается',
overflowHint: 'Как поступить с элементом, который не влезает в бюджет слота.',
repeatDaysAgo: 'Суток назад',
repeatTime: 'Время',
repeatDuration: 'Длительность, мин',
anchor: 'Якорь',
anchorHint:
'Якорь держит старт жёстко: через него не переносят то, что не влезает. Округление мягкое — сдвигает старт до круглого времени, если это укладывается в допуск.',
maxDrift: 'Допуск, мин',
snap: 'Округление',
snapOff: 'нет',
bumperConditionsHint:
'Как часто ставить заставку и на каких переходах — условия элемента стыка, а не настройка канала.',
resizeSlot: 'Потянуть за край — длительность',
copyDay: 'Копировать день',
copyDayFrom: 'Копировать {{day}} в:',
copy: 'Копировать',
layerVisible: 'Показывать слой',
layerName: 'Название слоя',
layerApplicability: 'Когда действует',
applicabilityHint:
'Разделы объединяются по ИЛИ: слой действует, если дата подходит хотя бы под одно условие. Ничего не заполнено — действует всегда.',
applicabilityWeekdays: 'Дни недели',
applicabilityDateRanges: 'Диапазоны дат',
applicabilityAnnual: 'Ежегодно (месяц / день)',
applicabilityDates: 'Конкретные даты',
applicabilityNone: 'не задано',
showForDate: 'Сетка на дату',
allDates: 'Все слои',
tabs: {
grid: 'Сетка',
rules: 'Правила',
junctions: 'Стыки',
bumpers: 'Заставки',
viewer: 'Зритель',
settings: 'Настройки',
air: 'Эфир',
},
noTemplate: 'Сетка канала не загрузилась',
createTemplate: 'Создать сетку',
createTemplateHint:
'Появится пустая сетка с фоновым слоем — дальше добавляйте слои и слоты.',
rules: 'Правила отбора',
rulesHint:
'Жёсткие фильтры: отсекают неподходящее до жребия. Как и правка сетки, эфир не двигают — нужно применить.',
audienceWindows: 'Детское время',
noAudienceWindows: 'Окон нет — возраст ничем не ограничен.',
audienceWindowsHint:
'В окне в эфир идёт контент не строже выбранной категории. Окно может переходить через полночь. Контент без категории не отсекается.',
from: 'С',
to: 'До',
maxAudience: 'Не строже',
repeatLimit: 'Потолок повторов',
repeatWindowDays: 'Окно, суток',
repeatMax: 'Не чаще, раз',
repeatLimitHint:
'Считается по уже записанной ленте. Если потолка достигли все кандидаты, слот всё равно заполняется: пустой эфир хуже раннего повтора.',
preview: 'Предпросмотр',
previewHide: 'Свернуть предпросмотр',
previewHint: 'Прогон по текущим правилам: ничего не пишется, курсоры слотов не двигаются.',
previewDays_one: '{{count}} сутки',
previewDays_few: '{{count}} суток',
previewDays_many: '{{count}} суток',
previewTabs: { programme: 'Программа', tape: 'Лента', problems: 'Проблемы' },
noProblems: 'Проблем нет',
andMore: 'и ещё {{count}}',
heatmap: 'Повторы: шоу × сутки',
heatmapTotal: 'всего',
issues: 'Проверки: ошибок {{errors}}, предупреждений {{warnings}}',
goToSlot: 'к слоту',
issueKinds: {
GroupEmpty: 'Пустая группа',
GroupTooSmall: 'Мало контента',
GridGap: 'Дыра в сетке',
SlotOverlap: 'Слоты пересекаются',
CooldownUnreachable: 'Недостижимое остывание',
AudienceConflict: 'Возрастной конфликт',
GroupMissing: 'Группа не выбрана',
},
viewer: 'Как выглядит у зрителя',
viewerHint:
'Оверлеи рисуются поверх картинки на клиенте — видео не перекодируется. Всё по умолчанию выключено.',
logo: 'Логотип',
noLogo: 'нет',
pickLogo: 'Выбрать логотип',
logoCorner: 'Угол',
logoOpacity: 'Прозрачность',
corners: {
TopLeft: 'Слева вверху',
TopRight: 'Справа вверху',
BottomLeft: 'Слева внизу',
BottomRight: 'Справа внизу',
},
showClock: 'Показывать часы',
analogFilter: 'Аналоговый фильтр',
analogFilterHint: 'Сила 0..1. Ноль — выключен; переборщить очень легко.',
whyHere: 'Почему это здесь',
priority: 'приоритет',
traceLayer: 'Слой',
traceSlot: 'Слот',
traceGroup: 'Группа',
traceCollection: 'Коллекция',
traceStrategy: 'Стратегия',
traceJunction: 'Врезки',
traceDrift: 'дрейф {{minutes}} мин',
traceSnapped: 'старт округлён',
traceCooldown: 'остывание {{days}} дн.',
traceCandidates: 'кандидатов после остывания: {{count}}',
diffSummary: 'Затронет {{total}} записей, изменятся {{changed}}',
diffSoon: 'В ближайшие сутки изменится записей: {{count}}',
diffNoChanges: 'Эфир не изменится',
copyTemplate: 'Копировать сетку',
pickTargetChannel: 'Выберите канал',
copyTemplateHint:
'Слои, слоты, стыки и правила уедут на выбранный канал, его прежняя сетка заменится. Группы общие и не копируются.',
templateCopied: 'Скопировано: слоёв {{layers}}, слотов {{slots}}',
copyDroppedBumpers: 'Врезок без блока заставки: {{count}} — донастройте руками',
postChecks: 'Пост-проверки',
breakLimit: 'Потолок врезок в час, мин',
genreShare: 'Потолок доли жанра за сутки, %',
fallbackShare: 'Потолок доли фона, %',
postChecksHint:
'Пост-проверки считаются по готовой ленте и только предупреждают — ничего не переигрывается.',
previewKinds: {
Program: 'Программа',
Fallback: 'Фон',
SignOff: 'Конец вещания',
Ad: 'Реклама',
Promo: 'Анонс',
Bumper: 'Заставка',
},
previewLoad: 'Врезки по часам, пик — {{peak}} мин',
junctions: 'Стыки',
junctionsHint:
'Что играет между программами: реклама, анонсы, заставки. Слот может взять свой стык, иначе берётся стык по умолчанию.',
defaultJunction: 'Стык по умолчанию',
noJunction: 'без стыка',
newJunctionName: 'Новый стык',
addJunctionElement: '+ врезка',
junctionEmpty: 'пусто',
junctionFrom: 'конец',
junctionTo: 'начало',
junctionElement: 'Врезка',
junctionKind: 'Тип',
junctionKinds: {
Ad: 'Реклама',
Promo: 'Анонс',
Bumper: 'Заставка',
Filler: 'Заполнитель',
},
junctionAmountMode: 'Чем меряется',
junctionAmountModes: { Count: 'Единиц', Duration: 'Минут' },
junctionCount: 'Сколько единиц',
junctionMinutes: 'Сколько минут',
junctionAmountHint:
'В смешанной группе (ролики и готовые блоки) считайте минутами: одна «единица» там — то ли ролик, то ли блок.',
junctionRequired: 'Обязательная — не выбрасывать при нехватке времени',
junctionOnlyOnChange: 'Только при смене шоу',
junctionMinInterval: 'Не чаще, чем раз в, мин',
junctionMinIntervalHint: '0 — без ограничения.',
junctionBetween: 'Стык внутри слота',
junctionAfter: 'Стык после слота',
junctionDefault: 'по умолчанию',
bumperTemplate: 'Блок заставки',
pickBumperTemplate: 'выберите блок',
minutesShort: ' мин',
pendingChanges: 'Правила изменены — эфир идёт по старым.',
apply: 'Применить',
applied: 'Эфир пересобран, записей: {{count}}',
weekdays: {
0: 'Вс',
1: 'Пн',
2: 'Вт',
3: 'Ср',
4: 'Чт',
5: 'Пт',
6: 'Сб',
},
dayparts: { Morning: 'Утро', Day: 'День', Prime: 'Прайм', Night: 'Ночь' },
slotKinds: { Content: 'Контент', Repeat: 'Повтор', SignOff: 'Конец вещания' },
blockModes: { Count: 'Единиц', Duration: 'Минут', FillSlot: 'Весь слот' },
overflows: {
ContinueNext: 'Доиграть в следующий раз',
ExtendSlot: 'Растянуть слот',
SkipIfNotFits: 'Не начинать',
},
strategies: {
Sequential: 'По порядку',
RandomWithCooldown: 'Случайно с остыванием',
Fixed: 'Фиксированный элемент',
},
warnings: {
SlotEmpty: 'Слот не дал контента',
DriftExceeded: 'Старт ушёл за допуск',
CooldownExhausted: 'Остывание отсекло всех',
RepeatSourceEmpty: 'Нечего повторять',
FallbackEmpty: 'Нечем закрыть паузы',
CandidatesFiltered: 'Возрастной потолок отсёк всех',
BreakLimitExceeded: 'Врезок в часе больше потолка',
GenreShareExceeded: 'Доля жанра выше нормы',
FallbackShareExceeded: 'Фона в эфире больше нормы',
},
title: 'Каналы',
name: 'Название',
slug: 'Slug',
state: 'Состояние',
enabled: 'В эфире',
enabledLabel: 'Канал в эфире',
regenerate: 'Пересобрать',
regenerated: 'Расписание пересобрано',
settings: 'Настройки',
adPolicy: 'Реклама',
betweenBlocks: 'Между блоками',
betweenEpisodes: 'Между сериями',
adsPerBreak: 'Роликов подряд',
bumpers: 'ТВ-заставки',
bumpersLabel: 'Заставки на переходах',
bumpersHint: 'Короткая заставка «Сейчас / Далее» между разными шоу',
bumperSelection: 'Выбор блока',
bumperSelectionRandom: 'Случайно',
bumperSelectionWeighted: 'Случайно взвешенный',
bumperSelectionAlwaysFirst: 'Всегда первый',
bumperFont: 'Шрифт',
bumperFontSans: 'Гротеск',
bumperFontSerif: 'Антиква',
bumperNowLabel: 'Подпись «Сейчас»',
bumperNextLabel: 'Подпись «Далее»',
bumperBg: 'Фон (цвет 1)',
bumperBg2: 'Фон (цвет 2)',
bumperAccent: 'Акцент',
bumperText: 'Текст',
bumperTemplates: 'Блоки заставок',
bumperTemplatesHint:
'Каждый блок — свой звук и оформление. Первый блок дефолтный, его нельзя удалить. Длительность заставки — по длине звука.',
bumperAddTemplate: 'Добавить блок',
bumperTemplateName: 'Название',
bumperVariants: 'Подблоки (текст)',
bumperVariantsHint:
'Разный текст на одной музыке и оформлении блока. Правило показа — у каждого подблока своё.',
bumperAddVariant: 'Добавить текст',
bumperVariantName: 'Название',
bumperTextKind: 'Режим текста',
bumperKindNowNext: 'Сейчас / Далее',
bumperKindFree: 'Свободный текст',
bumperLine1: 'Строка 1',
bumperLine2: 'Строка 2',
bumperTrigger: 'Показывать',
bumperTriggerOnShowChange: 'При смене шоу',
bumperTriggerBetweenEpisodes: 'Между сериями',
bumperTriggerBoth: 'Оба',
bumperVariantWeight: 'Вес',
bumperVariantWeightHint: 'Для стратегии «случайно взвешенный»: чем больше — тем чаще (0 — не выбирается)',
bumperDefault: 'по умолчанию',
bumperSeconds: 'с',
bumperDefaultDuration: '≈8 с (джингл)',
bumperAudio: 'Звук',
bumperAudioHint: 'Звук заставки; иначе — синтезированный джингл',
bumperPreview: 'Отрендерить примеры',
bumperPreviewRendering: 'Рендерим…',
bumperPreviewHint: 'Примеры всех подблоков со звуком и анимацией (примерные названия шоу). Использует сохранённые настройки.',
bumperBackground: 'Фон-картинка',
bumperBackgroundHint: 'Картинка фона; иначе — постер шоу или градиент',
bumperBackgroundPick: 'Выбрать из галереи',
bumperFileLoaded: 'загружено',
bumperFileDefault: 'по умолчанию',
bumperUpload: 'Загрузить',
bumperReset: 'Сбросить',
filler: 'Заглушка',
noFiller: 'Без заглушки',
shows: 'Шоу канала',
show: 'Шоу',
weight: 'Вес',
block: 'Блок',
on: 'Вкл',
noShows: 'Шоу не добавлены',
pickShow: 'Выберите шоу',
blockCount: 'По сериям',
blockDuration: 'По времени',
episodes: 'серий',
minutes: 'минут',
preferredHours: 'Часы',
preferredMultiplier: 'Множитель веса',
preferredHoursHint:
'В выбранные часы вес шоу умножается — оно чаще попадает в эфир. Время в UTC.',
preferredNone: 'Окна не заданы — предпочтений по времени нет.',
preferredAddWindow: 'Добавить окно',
preferredBadRange: 'начало ≥ конца',
noSchedule: 'Расписание ещё не построено',
},
maintenance: {
title: 'Обслуживание',
warning: 'Операции необратимы — удаляют данные и файлы навсегда.',
clearMedia: 'Очистить все медиа',
clearMediaHint: 'Удалит все загруженные файлы и их нарезку, а также расписание каналов.',
confirmClearMedia: 'Удалить ВСЕ медиа-файлы безвозвратно?',
clearShowMedia: 'Удалить медиа шоу',
clearShowMediaHint: 'Удалит файлы всех серий выбранного шоу и очистит его серии.',
confirmClearShowMedia: 'Удалить все медиа выбранного шоу безвозвратно?',
pickShow: 'Выберите шоу',
deleteShows: 'Удалить все шоу',
deleteShowsHint: 'Удалит все шоу и их серии. Медиа-файлы останутся в библиотеке.',
confirmDeleteShows: 'Удалить ВСЕ шоу безвозвратно?',
doneCount: 'Удалено: {{count}}',
},
settings: {
title: 'Настройки',
registration: 'Регистрация',
registrationHint:
'Когда выключено — новые пользователи не могут регистрироваться сами, учётки заводит только администратор.',
registrationLabel: 'Разрешить регистрацию на сайте',
preferredAudio: 'Предпочитаемые озвучки',
channelNumbers: 'Переключение каналов по номерам',
channelNumbersHint:
'Зритель переключает каналы стрелками, как на телевизоре. Сетка каналов остаётся всегда.',
preferredAudioHint:
'Коды языков через запятую в порядке приоритета (напр. «rus, eng»). Если в файле есть дорожка с таким языком — при обработке выбирается она (по порядку); иначе — выбор ffmpeg по умолчанию. Применяется к новым обработкам.',
},
metadata: {
title: 'Метаданные',
pickPoster: 'Из галереи',
name: 'Название',
originalName: 'Оригинальное название (eng)',
originalNamePlaceholder: 'Например: Family Guy',
originalNameHint: 'По нему ищутся метаданные; на экранах показывается обычное название.',
sourceLabel: 'Источник',
searchBtn: 'Искать',
nothingFound: 'Ничего не найдено',
apply: 'Применить',
applied: 'Метаданные применены',
overview: 'Описание',
year: 'Год',
clear: 'Очистить',
noPoster: 'Нет постера',
refreshEpisodes: 'Обновить серии',
refreshing: 'Обновляем…',
refreshedCount: 'Обновлено серий: {{count}}',
findMissing: 'Найти отсутствующие серии',
missingTitle: 'Отсутствующие серии',
missingNoSeasons: 'В шоу нет загруженных серий с распознанными номерами.',
seasonN: 'Сезон {{n}}',
loadedOf: 'загружено {{loaded}} из {{total}}',
missingUnknown: 'Источник не отдал число серий этого сезона.',
missingNone: 'Все серии на месте.',
missingList: 'Не хватает',
},
},
},
},
en: {
translation: {
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',
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',
episode: 'Episode',
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',
},
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',
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',
elementKinds: { Show: 'Show', Collection: 'Collection' },
filter: {
title: 'Selection rule',
hint: 'The rule only finds candidates — the group composition stays an explicit list.',
elementKinds: 'What to search',
showKinds: 'Show type',
genres: 'Genres',
genresHint: 'Any of the checked ones.',
maxAudience: 'No stricter than',
audienceHint: 'Categories are ordered by strictness: kids → family → … → adult.',
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: {
title: 'Collections',
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',
empty: 'Collection is empty',
orderHint: 'Drag to set the order of the parts — that is the order they air in.',
},
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: 'New 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',
},
media: {
title: 'Media',
upload: 'Upload',
manualButton: 'From manual folder',
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',
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',
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:
'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}}',
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',
},
},
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',
name: 'Name',
originalName: 'Original name (eng)',
kind: 'Kind',
kinds: { Series: 'Series', Single: 'Movie', Interstitial: 'Clip' },
audience: 'Category',
audiences: {
Kids: 'Kids',
Family: 'Family',
Teen: 'Teen',
General: 'General',
Adult: 'Adult',
},
seasons: 'Seasons',
loadedSeasons: 'Loaded seasons',
genre: 'Genre',
allGenres: 'All genres',
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',
inCollections: 'Part of collections',
episodes: 'Episodes',
episode: 'Episode',
noEpisodes: 'No episodes yet',
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',
grid: 'Grid',
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',
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',
bumperConditionsHint:
'How often a bumper is inserted and on which transitions is a junction-element condition, not a channel setting.',
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',
bumpers: 'Bumpers',
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 grid',
pickTargetChannel: 'Pick a channel',
copyTemplateHint:
'Layers, slots, junctions and rules move to the chosen channel, replacing its grid. Groups are shared and not copied.',
templateCopied: 'Copied: {{layers}} layers, {{slots}} slots',
copyDroppedBumpers: 'Breaks left without a bumper block: {{count}} — set them up by hand',
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',
newJunctionName: 'New junction',
addJunctionElement: '+ break',
junctionEmpty: 'empty',
junctionFrom: 'end',
junctionTo: 'start',
junctionElement: 'Break',
junctionKind: 'Kind',
junctionKinds: { Ad: 'Ad', Promo: 'Promo', Bumper: 'Bumper', Filler: 'Filler' },
junctionAmountMode: 'Measured in',
junctionAmountModes: { Count: 'Units', Duration: 'Minutes' },
junctionCount: 'How many units',
junctionMinutes: 'How many minutes',
junctionAmountHint:
'For a mixed group (clips and ready-made blocks) count in minutes: one "unit" there is either a clip or a whole block.',
junctionRequired: 'Required — never dropped when time runs short',
junctionOnlyOnChange: 'Only when the show changes',
junctionMinInterval: 'No more often than once per, min',
junctionMinIntervalHint: '0 — no limit.',
junctionBetween: 'Junction inside the slot',
junctionAfter: 'Junction after the slot',
junctionDefault: 'default',
bumperTemplate: 'Bumper block',
pickBumperTemplate: 'pick a block',
minutesShort: ' min',
pendingChanges: 'Rules changed — the air still follows the old ones.',
apply: 'Apply',
applied: 'Air rebuilt, entries: {{count}}',
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: 'Background share above the norm',
},
title: 'Channels',
name: 'Name',
slug: 'Slug',
state: 'State',
enabled: 'On air',
enabledLabel: 'Channel on air',
regenerate: 'Rebuild',
regenerated: 'Schedule rebuilt',
settings: 'Settings',
adPolicy: 'Ads',
betweenBlocks: 'Between blocks',
betweenEpisodes: 'Between episodes',
adsPerBreak: 'Ads per break',
bumpers: 'TV bumpers',
bumpersLabel: 'Transition bumpers',
bumpersHint: 'Short “Now / Next” bumper between different shows',
bumperSelection: 'Block selection',
bumperSelectionRandom: 'Random',
bumperSelectionWeighted: 'Weighted random',
bumperSelectionAlwaysFirst: 'Always first',
bumperFont: 'Font',
bumperFontSans: 'Sans',
bumperFontSerif: 'Serif',
bumperNowLabel: '“Now” label',
bumperNextLabel: '“Next” label',
bumperBg: 'Background (color 1)',
bumperBg2: 'Background (color 2)',
bumperAccent: 'Accent',
bumperText: 'Text',
bumperTemplates: 'Bumper blocks',
bumperTemplatesHint:
'Each block has its own sound and style. The first block is the default and cannot be removed. Bumper length follows the sound length.',
bumperAddTemplate: 'Add block',
bumperTemplateName: 'Name',
bumperVariants: 'Sub-blocks (text)',
bumperVariantsHint:
'Different text over the same music and style. Each sub-block has its own show rule.',
bumperAddVariant: 'Add text',
bumperVariantName: 'Name',
bumperTextKind: 'Text mode',
bumperKindNowNext: 'Now / Next',
bumperKindFree: 'Free text',
bumperLine1: 'Line 1',
bumperLine2: 'Line 2',
bumperTrigger: 'Show on',
bumperTriggerOnShowChange: 'Show change',
bumperTriggerBetweenEpisodes: 'Between episodes',
bumperTriggerBoth: 'Both',
bumperVariantWeight: 'Weight',
bumperVariantWeightHint: 'For the “weighted random” strategy: higher = more often (0 — never picked)',
bumperDefault: 'default',
bumperSeconds: 's',
bumperDefaultDuration: '≈8 s (jingle)',
bumperAudio: 'Sound',
bumperAudioHint: 'Bumper sound; otherwise a synthesized jingle',
bumperPreview: 'Render samples',
bumperPreviewRendering: 'Rendering…',
bumperPreviewHint: 'Samples of all sub-blocks with sound and animation (example show names). Uses saved settings.',
bumperBackground: 'Background image',
bumperBackgroundHint: 'Background image; otherwise the show poster or a gradient',
bumperBackgroundPick: 'Pick from gallery',
bumperFileLoaded: 'loaded',
bumperFileDefault: 'default',
bumperUpload: 'Upload',
bumperReset: 'Reset',
filler: 'Filler',
noFiller: 'No filler',
shows: 'Channel shows',
show: 'Show',
weight: 'Weight',
block: 'Block',
on: 'On',
noShows: 'No shows added',
pickShow: 'Pick a show',
blockCount: 'By episodes',
blockDuration: 'By time',
episodes: 'episodes',
minutes: 'minutes',
preferredHours: 'Hours',
preferredMultiplier: 'Weight multiplier',
preferredHoursHint:
'During the selected hours the shows weight is multiplied, so it airs more often. Times are UTC.',
preferredNone: 'No windows set — no time preference.',
preferredAddWindow: 'Add window',
preferredBadRange: 'start ≥ end',
noSchedule: 'Schedule not built yet',
},
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',
},
},
},
},
}
const STORAGE_KEY = 'tw-lang'
const stored = typeof localStorage !== 'undefined' ? localStorage.getItem(STORAGE_KEY) : null
void i18n.use(initReactI18next).init({
resources,
lng: stored ?? 'ru',
fallbackLng: 'ru',
interpolation: { escapeValue: false },
})
export function setLanguage(lng: string) {
localStorage.setItem(STORAGE_KEY, lng)
void i18n.changeLanguage(lng)
}
export default i18n