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