1398 lines
70 KiB
TypeScript
1398 lines
70 KiB
TypeScript
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 файлов — в папке есть ещё.',
|
||
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: 'Все слои',
|
||
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.',
|
||
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: A–Z',
|
||
nameDesc: 'Name: Z–A',
|
||
},
|
||
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',
|
||
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 show’s 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
|