export type EditorLocale = 'ru' | 'en'; export const EDITOR_LOCALE_STORAGE_KEY = 'dnd_editor_locale'; function primaryLanguageTag(lang: string): string { const trimmed = lang.trim().toLowerCase(); if (!trimmed) return ''; const sep = trimmed.search(/[-_]/); return sep === -1 ? trimmed : trimmed.slice(0, sep); } /** * Выбор `ru` / `en` по языку ОС/браузера, если пользователь ещё не сохранил язык в `localStorage`. * В Electron совпадает с локалью системы (Chromium подставляет `navigator.languages`). */ export function inferEditorLocaleFromSystem(languages?: readonly string[]): EditorLocale { let list: string[]; if (languages !== undefined) { list = [...languages]; } else if (typeof navigator !== 'undefined') { list = [...navigator.languages]; if (navigator.language) { list.push(navigator.language); } list = list.filter((x) => x.trim() !== ''); } else { list = []; } for (const lang of list) { const tag = primaryLanguageTag(lang); if (tag === 'en') return 'en'; if (tag === 'ru') return 'ru'; } return 'ru'; } export function normalizeEditorLocale(raw: string | null | undefined): EditorLocale { if (raw == null) { return inferEditorLocaleFromSystem(); } const trimmed = raw.trim(); if (trimmed === '') { return inferEditorLocaleFromSystem(); } const s = trimmed.toLowerCase(); if (s === 'en') return 'en'; if (s === 'ru') return 'ru'; return inferEditorLocaleFromSystem(); } /** Flat message table; `{name}` placeholders supported in `translate`. */ export const EDITOR_MESSAGES: Record> = { ru: { 'common.close': 'Закрыть', 'common.cancel': 'Отмена', 'common.save': 'Сохранить', 'common.understood': 'Понятно', 'common.message': 'Сообщение', 'common.error': 'Ошибка', 'common.delete': 'Удалить', 'common.closeMenu': 'Закрыть меню', 'app.brandTitle': 'НРИ Плеер', 'notice.campaignAudioEmpty': 'Аудио не добавлено. Проверьте формат файла.', 'license.checkingTitle': 'Проверка лицензии…', 'license.checkingWait': 'Подождите.', 'license.requiredTitle': 'Требуется лицензия', 'license.requiredHint': 'Укажите ключ в меню «Настройки» → «Указать ключ». До активации доступно только меню «Настройки».', 'license.tokenTitle': 'Указать ключ', 'license.tokenKey': 'КЛЮЧ', 'license.tokenPlaceholder': 'Продуктовый ключ TTRPG-... или DND-...', 'license.tokenSaving': 'Сохранение…', 'license.eulaTitle': 'Лицензионное соглашение', 'license.eulaReject': 'Не принимаю', 'license.eulaAccept': 'Принимаю условия', 'license.eulaNoteEn': 'The binding legal text below is in Russian. If you need an English summary, contact support.', 'license.aboutTitle': 'О лицензии', 'license.aboutDevSkip': 'Режим разработки: проверка лицензии отключена (DND_SKIP_LICENSE).', 'license.aboutStatus': 'СТАТУС', 'license.aboutProduct': 'ПРОДУКТ', 'license.aboutLicenseId': 'ID ЛИЦЕНЗИИ', 'license.aboutExpiry': 'ОКОНЧАНИЕ', 'license.aboutDevice': 'УСТРОЙСТВО', 'license.aboutNoData': 'Нет данных лицензии.', 'license.reason.ok': 'Активна', 'license.reason.none': 'Ключ не указан', 'license.reason.expired': 'Срок действия истёк', 'license.reason.bad_signature': 'Недействительная подпись', 'license.reason.bad_payload': 'Неверный формат токена', 'license.reason.malformed': 'Повреждённый токен', 'license.reason.not_yet_valid': 'Ещё не действует', 'license.reason.wrong_device': 'Другой привязанный компьютер', 'license.reason.revoked_remote': 'Отозвана на сервере', 'presentation.overlay': 'Презентация запущена', 'presentation.title': 'Презентация запущена', 'presentation.body': 'Редактор заблокирован. Закройте окна «Презентация» и «Панель управления», чтобы продолжить.', 'zip.progress': 'Прогресс операции', 'zip.importTitle': 'Импорт проекта', 'zip.exportTitle': 'Экспорт проекта', 'top.settings': 'Настройки', 'top.project': 'Проект', 'top.file': 'Файл', 'top.backToProjects': 'К списку проектов', 'top.appVersion': 'Версия приложения', 'top.run': 'Запустить', 'top.afterLicense': 'Доступно после активации лицензии', 'top.setStartScene': 'Назначьте начальную сцену на графе (ПКМ по узлу)', 'menu.enterKey': 'Указать ключ', 'menu.aboutLicense': 'О лицензии', 'menu.checkUpdates': 'Проверить обновления', 'menu.language': 'Язык', 'menu.langRu': 'Русский', 'menu.langEn': 'English', 'menu.aboutProgram': 'О программе', 'menu.instructions': 'Инструкция', 'top.aboutApp': 'О приложении', 'app.about.title': 'О программе', 'app.about.tagline': 'Редактор кампаний и пульт мастера для настольных ролевых игр', 'app.about.description': 'TTRPG Player (НРИ Плеер) — настольное приложение для мастера: вы собираете кампанию как граф сцен с картами, музыкой и ветвлениями, а во время сессии управляете атмосферой с пульта. Игроки видят только презентацию на втором экране — без панелей редактора.', 'app.about.versionLabel': 'ВЕРСИЯ', 'app.about.developerLabel': 'РАЗРАБОТЧИК', 'app.about.developer': 'Независимая разработка. По вопросам лицензии, покупки и поддержки — электронная почта ниже.', 'app.about.supportLabel': 'ПОДДЕРЖКА', 'app.about.supportEmail': 'player.ttrpg@gmail.com', 'app.about.websiteLabel': 'САЙТ И ОБНОВЛЕНИЯ', 'app.about.websiteUrl': 'https://updates.mailib.ru/', 'help.title': 'Инструкция', 'help.navAria': 'Разделы инструкции', 'help.section.overview.title': 'Обзор приложения', 'help.section.overview.body': 'TTRPG Player объединяет три окна: «Редактор» (подготовка кампании), «Пульт управления» (ведение сессии) и «Презентация» (экран для игроков). Редактор открывается при запуске; пульт и презентация появляются после нажатия «Запустить» в шапке редактора.\n\nВ редакторе слева — список сцен, по центру — граф сцен, справа — свойства игры и выбранной сцены. Пока открыты окна показа, редактор блокируется: закройте презентацию и пульт, чтобы снова редактировать кампанию.\n\nИнтернет нужен только для активации лицензии и проверки обновлений. Сами проекты и медиафайлы хранятся локально на вашем компьютере.', 'help.section.license.title': 'Лицензия и первый запуск', 'help.section.license.body': 'При первом запуске откройте «Настройки» → «Указать ключ». Если лицензионное соглашение ещё не принято, сначала откроется окно EULA — примите условия, затем вставьте продуктовый ключ (формат TTRPG-… или DND-…) и нажмите «Сохранить».\n\nДо активации лицензии доступно только меню «Настройки». Создание и открытие проектов, редактирование сцен и запуск сессии станут доступны после успешной активации.\n\nСтатус лицензии, срок действия и привязка к устройству — в «Настройки» → «О лицензии». Ключ привязан к компьютеру; при смене ПК потребуется новая активация согласно условиям покупки.', 'help.section.projects.title': 'Проекты', 'help.section.projects.body': 'На начальном экране (когда проект не открыт) в левой колонке отображается список «Проекты». Введите название в поле и нажмите «Создать проект», либо кликните по существующему проекту, чтобы открыть его.\n\n«Проект» → «Начальный экран» — закрывает текущий проект и возвращает к списку. Клик по логотипу и названию приложения в шапке делает то же самое.\n\n«Проект» → «Импорт» — загрузите архив .ttrpg.zip с другого компьютера. «Проект» → «Экспорт» — сохраните копию выбранного проекта в указанную папку.\n\n«Файл» → «Переименовать проект» (когда проект открыт) — измените отображаемое имя и имя файла на диске. Минимум 3 символа; в имени файла нельзя использовать символы <>:"/\\|?*.\n\nВ списке проектов кнопка «⋯» у карточки открывает меню удаления. Удаление безвозвратно стирает файл проекта и кэш с диска.', 'help.section.scenes.title': 'Сцены', 'help.section.scenes.body': 'Сцена — единица кампании: локация, эпизод или кадр истории с превью, описанием и аудио. В левой колонке редактора нажмите «+ Новая сцена», чтобы создать пустую сцену.\n\nПоле «Поиск сцен…» фильтрует список по названию. Клик по карточке сцены выделяет её и открывает свойства справа; та же сцена подсвечивается на графе.\n\nПравый клик по карточке в списке — меню «Удалить». Удаление сцены убирает её из списка и с графа вместе со связями. Сцену можно перетащить мышью из списка на граф (см. раздел «Граф сцен»).', 'help.section.graph.title': 'Граф сцен', 'help.section.graph.body': 'Граф — визуальная карта кампании. Каждый узел на графе — экземпляр сцены; одна и та же сцена может встречаться на графе несколько раз (например, возврат в локацию).\n\nПеретащите сцену из левого списка на свободное место графа — появится карточка-узел. Перетаскивайте узлы, чтобы расставить схему. Соедините два узла: потяните от нижней точки (handle) одного узла к верхней точке другого — появится стрелка перехода. Одна карточка может иметь несколько исходящих связей (ветвление).\n\nНельзя провести вторую связь к тому же целевому узлу с той же карточки. Чтобы удалить связь, выделите стрелку и нажмите Delete или используйте контекстное меню React Flow.\n\nПравый клик по узлу: «Начальная сцена» — с какого узла стартует партия при «Запустить»; «Удалить» — убрать узел с графа (сама сцена в списке останется). На начальной сцене отображается метка «НАЧАЛО».\n\nПанель масштаба внизу графа: увеличение, уменьшение, «Показать всё». Колёсико мыши над графом тоже меняет масштаб.', 'help.section.sceneProps.title': 'Свойства сцены', 'help.section.sceneProps.body': 'Правая колонка «Свойства сцены» активна, когда выбрана сцена. «Название сцены» и «Описание» — текст для мастера; описание видно на пульте в блоке сюжетной линии.\n\n«Превью сцены» — главное изображение или видео для игроков. Нажмите «Загрузить» или «Изменить» и выберите файл (изображение: PNG, JPG, WebP, GIF и др.). Для изображений доступна кнопка «Повернуть» (шаг 90°). «Очистить» удаляет превью.\n\nДля видео-превью отметьте «Автостарт», если ролик должен начинаться сам на экране презентации. На видео-сценах эффекты кисти на пульте отключены (как на презентации).\n\n«Аудио сцены» — музыка и звуки, привязанные к этой сцене. «Загрузить» добавляет файлы. У каждого трека: «Авто» (автовоспроизведение при входе в сцену на пульте) и «Цикл» (зацикливание). Корзина убирает трек из сцены.\n\nБлок «Ветвления» напоминает: связи задаются перетаскиванием на графе, а не в инспекторе.', 'help.section.campaignAudio.title': 'Аудио игры', 'help.section.campaignAudio.body': 'В правой колонке блок «Свойства игры» → «Аудио игры» — музыка всей кампании (фон, тема, ambient), не привязанная к конкретной сцене.\n\nНажмите «Загрузить» и выберите аудиофайлы. Для каждого трека задайте «Авто» и «Цикл» так же, как у аудио сцены. Удаление — иконка корзины.\n\nНа пульте музыка сцены имеет приоритет над музыкой игры: пока играет сцена с аудио, кампанийные треки ставятся на паузу и могут возобновиться, когда сцена без своей музыки или после вашего управления с пульта.', 'help.section.session.title': 'Запуск сессии', 'help.section.session.body': 'Когда кампания готова, назначьте начальную сцену на графе (ПКМ по узлу → «Начальная сцена»). Кнопка «Запустить» в шапке редактора станет активной.\n\n«Запустить» открывает окно «Презентация» (для игроков) и «Пульт управления» (для мастера). Редактор показывает затемнение: «Презентация запущена» — это нормально, пока идёт сессия.\n\nЧтобы вернуться к редактированию, на пульте нажмите «Выключить демонстрацию» или «Завершить показ» (если нет дальнейших переходов). Оба окна должны закрыться — тогда блокировка редактора снимется.\n\nПеретащите окно презентации на второй монитор, проектор или ТВ и разверните на весь экран (F11 в окне или стандартные средства ОС). Игроки видят только карту/видео и эффекты, без интерфейса мастера.', 'help.section.controlPanel.title': 'Пульт управления', 'help.section.controlPanel.body': 'Пульт — главное рабочее место мастера во время игры. Слева — инструменты эффектов и сюжетная линия; справа — предпросмотр экрана игроков, варианты переходов и музыка.\n\n«Предпросмотр экрана» показывает то же, что видят игроки, в уменьшенном виде. Здесь же можно рисовать эффекты кистью (для сцен с изображением-превью).\n\n«Выключить демонстрацию» закрывает презентацию и пульт и разблокирует редактор.\n\nБлок «Сюжетная линия» — история посещённых узлов графа в текущей сессии. Текущая сцена помечена «ТЕКУЩАЯ СЦЕНА». Клик по пройденному шагу перематывает партию на ту точку графа без добавления нового шага в историю.', 'help.section.transitions.title': 'Переходы между сценами', 'help.section.transitions.body': 'Исходящие связи от текущего узла графа отображаются на пульте в блоке «Варианты ветвления» — карточки «ОПЦИЯ 1», «ОПЦИЯ 2» и т.д. с названием целевой сцены.\n\nНажмите «Переключить» на нужной опции — презентация и музыка перейдут к выбранной сцене; в сюжетной линии появится новый шаг.\n\nЕсли исходящих связей нет (конец ветки или тупик), отображается «Нет вариантов перехода» и кнопка «Завершить показ» — она закрывает окна показа.\n\nПереходы задаются только теми связями, которые вы провели на графе в редакторе от текущего узла к другим узлам.', 'help.section.music.title': 'Музыка на пульте', 'help.section.music.body': 'Раздел «Музыка» на пульте разделён на «Музыка сцены» и «Музыка игры» — те же треки, что вы настроили в редакторе.\n\nУ каждого трека отображаются режимы «Авто»/«Ручн.» и «Цикл»/«Один раз», а также статус воспроизведения. Кнопки ▶ / ⏸ / ⏹ управляют воспроизведением вручную.\n\nПолоска прогресса — клик по ней перематывает трек (если известна длительность). Стрелки ← → на полоске (при фокусе) перематывают на 5 секунд.\n\nЕсли браузер/Electron блокирует автозапуск, нажмите «Воспроизведение» вручную — после действия пользователя звук обычно разрешается. При ошибке формата проверьте файл (поддерживаются распространённые аудиоформаты).', 'help.section.effects.title': 'Эффекты поля и действий', 'help.section.effects.body': 'Эффекты доступны на пульте для сцен с изображением-превью (не для видео). Рисуйте прямо в области «Предпросмотр экрана» — эффекты синхронно появляются на презентации.\n\n«Инструменты»: ластик 🧹 — клик или проведение по эффекту удаляет его; «Очистить эффекты» — убрать все эффекты сразу.\n\n«Эффекты поля»: туман, дождь, огонь, вода — зажмите левую кнопку мыши и ведите кистью по карте.\n\n«Эффекты действий»: молния, луч света, заморозка, облако яда — короткий клик или штрих; у молнии и некоторых эффектов есть звук и ограниченная длительность анимации.\n\n«Радиус кисти» — ползунок под панелью эффектов; меняет размер области рисования. Чем больше значение, тем шире мазок.', 'help.section.presentation.title': 'Экран презентации', 'help.section.presentation.body': 'Окно «Презентация» показывает игрокам превью текущей сцены: изображение карты (с учётом поворота из редактора) или видео с автостартом/циклом по настройкам.\n\nПоверх карты отображаются нарисованные мастером эффекты. Интерфейса редактора и пульта на этом экране нет.\n\nПри смене сцены с пульта презентация обновляется автоматически. Разместите окно на экране, который видят игроки, и скройте панель задач при необходимости.\n\nЕсли превью не задано, экран может оставаться пустым или тёмным — задайте превью в свойствах сцены в редакторе.', 'help.section.importExport.title': 'Импорт и экспорт', 'help.section.importExport.body': 'Проект хранится как архив .ttrpg.zip в папке данных приложения. «Проект» → «Экспорт» создаёт копию архива: выберите проект в списке, нажмите «Сохранить как…» и укажите путь — удобно для резервной копии или переноса на другой ПК.\n\n«Проект» → «Импорт» открывает диалог выбора .ttrpg.zip. После импорта проект появится в списке на начальном экране. Импорт и экспорт показывают прогресс-бар при больших архивах.\n\nМедиафайлы (картинки, аудио, видео) упакованы внутрь проекта при загрузке в редактор — отдельно их переносить не нужно, если вы передаёте целый .ttrpg.zip.', 'help.section.settings.title': 'Настройки, язык и обновления', 'help.section.settings.body': '«Настройки» → «Указать ключ» — смена или первичный ввод лицензии. «О лицензии» — статус и срок. «Проверить обновления» (в установленной версии при активной лицензии) — поиск новой версии на сервере; при наличии обновления можно загрузить и перезапустить приложение.\n\n«Язык» → «Русский» или «English» — переключает интерфейс редактора, пульта и системных сообщений. Выбор сохраняется между запусками.\n\nВерсия приложения отображается в шапке справа от меню (например, v1.0.22). «О приложении» → «О программе» — описание продукта и контакты разработчика.', 'updates.dialogTitle': 'Обновления', 'updates.checking': 'Проверка наличия обновлений…', 'updates.available': 'Доступна новая версия {version}.', 'updates.current': 'У вас установлена актуальная версия ({version}).', 'updates.error': 'Не удалось проверить обновления: {message}', 'updates.notPackaged': 'Проверка доступна только в установленной версии приложения.', 'updates.noLicense': 'Нужна активная лицензия.', 'updates.download': 'Обновить', 'updates.downloading': 'Загрузка…', 'updates.stageLine': 'Этап: {stage}', 'updates.stage.checking': 'проверка обновлений', 'updates.stage.available': 'доступна версия {version}', 'updates.stage.not-available': 'актуальная версия', 'updates.stage.downloading': 'загрузка{percent}', 'updates.stage.installing': 'установка и перезапуск', 'updates.stage.error': 'ошибка', 'updates.stagePercent': ' ({percent}%)', 'projectMenu.home': 'Начальный экран', 'projectMenu.import': 'Импорт', 'projectMenu.export': 'Экспорт', 'projectMenu.noProjects': 'Нет сохранённых проектов', 'fileMenu.rename': 'Переименовать проект', 'scenes.search': 'Поиск сцен…', 'scenes.new': '+ Новая сцена', 'scenes.inspectorGame': 'Свойства игры', 'scenes.inspectorScene': 'Свойства сцены', 'scenes.selectHint': 'Выберите сцену слева, чтобы редактировать её свойства.', 'scenes.openProjectHint': 'Откройте проект, чтобы редактировать кампанию и сцены.', 'rename.title': 'Переименовать проект', 'rename.projectName': 'НАЗВАНИЕ ПРОЕКТА', 'rename.projectPlaceholder': 'Название проекта…', 'rename.projectMin': 'Минимум 3 символа.', 'rename.projectDup': 'Проект с таким названием уже существует.', 'rename.fileName': 'НАЗВАНИЕ ФАЙЛА ПРОЕКТА', 'rename.fileInvalid': 'Минимум 3 символа, без символов <>:"/\\|?*', 'rename.fileDup': 'Файл проекта с таким названием уже существует.', 'rename.saving': 'Сохранение…', 'export.title': 'Экспорт проекта', 'export.project': 'ПРОЕКТ', 'export.hint': 'Далее откроется окно сохранения: укажите имя и папку для файла .ttrpg.zip — будет создана копия архива проекта.', 'export.exporting': 'Экспорт…', 'export.saveAs': 'Сохранить как…', 'confirmDelete.title': 'Удаление проекта', 'confirmDelete.body': 'Удалить проект «{name}» безвозвратно? Файл и кэш будут стёрты с диска.', 'confirmDelete.failedTitle': 'Не удалось удалить', 'picker.title': 'Проекты', 'picker.newPlaceholder': 'Название нового проекта…', 'picker.create': 'Создать проект', 'picker.existing': 'СУЩЕСТВУЮЩИЕ', 'picker.lockedHint': 'Открытие и создание — после активации лицензии. Список показывает файлы в папке приложения.', 'picker.empty': 'Пока нет проектов.', 'picker.projectMenu': 'Меню проекта', 'picker.openDisabled': 'Открытие проекта — после активации лицензии', 'picker.defaultName': 'Моя кампания', 'campaign.label': 'АУДИО ИГРЫ', 'campaign.noFiles': 'Файлов пока нет. Добавьте аудио.', 'campaign.auto': 'Авто', 'campaign.loop': 'Цикл', 'campaign.removeTitle': 'Убрать из кампании', 'campaign.upload': 'Загрузить', 'scene.title': 'НАЗВАНИЕ СЦЕНЫ', 'scene.description': 'ОПИСАНИЕ', 'scene.preview': 'ПРЕВЬЮ СЦЕНЫ', 'scene.previewHint': 'Файл изображения (PNG, JPG, WebP, GIF и т.д.).', 'scene.previewEmpty': 'Превью не задано', 'scene.previewBusy': 'Загрузка и оптимизация изображения…', 'scene.change': 'Изменить', 'scene.clear': 'Очистить', 'scene.autostart': 'Автостарт', 'scene.rotate': 'Повернуть', 'scene.audio': 'АУДИО СЦЕНЫ', 'scene.removeTitle': 'Убрать из сцены', 'scene.branching': 'ВЕТВЛЕНИЯ', 'scene.branchingHint': 'Перетащите сцену из списка на граф. С одной карточки можно задать несколько вариантов — по одной связи на каждую целевую сцену. Повторно к той же сцене (включая вторую карточку той же сцены на графе) подключить нельзя.', 'sceneCard.current': 'ТЕКУЩАЯ', 'sceneCard.menu': 'Меню сцены', 'graph.badgeStart': 'НАЧАЛО', 'graph.untitled': 'Без названия', 'graph.videoBadge': 'Видео', 'graph.audioBadge': 'Аудио', 'graph.loop': 'Цикл', 'graph.autoplay': 'Автостарт', 'graph.previewAutostart': 'Авто превью', 'graph.videoLoop': 'Цикл видео', 'graph.zoomBar': 'Масштаб графа', 'graph.zoomIn': 'Увеличить', 'graph.zoomOut': 'Уменьшить', 'graph.fitAll': 'Показать всё', 'graph.startScene': 'Начальная сцена', 'graph.unsetStartScene': 'Снять метку «Начальная сцена»', 'control.remoteTitle': 'ПУЛЬТ УПРАВЛЕНИЯ', 'control.effects': 'ЭФФЕКТЫ', 'control.tools': 'Инструменты', 'control.fieldEffects': 'Эффекты поля', 'control.actionEffects': 'Эффекты действий', 'control.eraser': 'Ластик', 'control.clearEffects': 'Очистить эффекты', 'control.fog': 'Туман', 'control.rain': 'Дождь', 'control.fire': 'Огонь', 'control.water': 'Вода', 'control.lightning': 'Молния', 'control.sunbeam': 'Луч света', 'control.freeze': 'Заморозка', 'control.poisonCloud': 'Облако яда', 'control.brushRadius': 'Радиус кисти', 'control.storyLine': 'СЮЖЕТНАЯ ЛИНИЯ', 'control.gotoScene': 'Перейти к этой сцене', 'control.currentSceneBadge': 'ТЕКУЩАЯ СЦЕНА', 'control.passed': 'Пройдено', 'control.noActiveScene': 'Нет активной сцены.', 'control.screenPreview': 'Предпросмотр экрана', 'control.stopPresentation': 'Выключить демонстрацию', 'control.videoBrushHint': 'Видео-превью: кисть эффектов отключена (как на экране демонстрации — оверлей только для изображения).', 'control.branches': 'Варианты ветвления', 'control.option': 'ОПЦИЯ {n}', 'control.unnamed': 'Без названия', 'control.switchScene': 'Переключить', 'control.noBranches': 'Нет вариантов перехода.', 'control.endPresentation': 'Завершить показ', 'control.music': 'Музыка', 'control.sceneMusic': 'МУЗЫКА СЦЕНЫ', 'control.gameMusic': 'МУЗЫКА ИГРЫ', 'control.noSceneAudio': 'В текущей сцене нет аудио.', 'control.noGameAudio': 'В игре нет аудио.', 'control.modeAuto': 'Авто', 'control.modeManual': 'Ручн.', 'control.once': 'Один раз', 'control.loop': 'Цикл', 'control.scrubSeek': 'Клик — перемотка', 'control.durationUnknown': 'Длительность неизвестна', 'control.pauseSceneMusic': 'Пауза (сцена)', 'control.pauseSceneMusicTitle': 'В сцене есть музыка', 'control.pauseCampaignTitle': 'Пауза: в сцене есть музыка', 'control.playFailed': 'Не удалось запустить.', 'control.audioAutoplayBlocked': 'Автозапуск заблокирован (нужно действие пользователя) или ошибка воспроизведения.', 'control.audioNoUrl': 'URL не получен', 'control.audioNoUrlDetail': 'Не удалось получить dnd://asset URL для аудио.', 'control.audioBlocked': 'Ошибка/блок', 'control.audioError': 'Ошибка', 'control.audioMediaError': 'MediaError code={code} (1=ABORTED, 2=NETWORK, 3=DECODE, 4=SRC_NOT_SUPPORTED)', 'control.audioLoading': 'Загрузка…', 'control.audioPlaying': 'Играет', 'control.audioPaused': 'Пауза', 'control.audioStopped': 'Остановлено', 'control.previewTrackLabel': 'Превью без субтитров', 'control.transportPlay': 'Воспроизведение', 'control.transportPause': 'Пауза', 'control.transportStop': 'Стоп', }, en: { 'common.close': 'Close', 'common.cancel': 'Cancel', 'common.save': 'Save', 'common.understood': 'OK', 'common.message': 'Message', 'common.error': 'Error', 'common.delete': 'Delete', 'common.closeMenu': 'Close menu', 'app.brandTitle': 'TTRPG Player', 'notice.campaignAudioEmpty': 'No audio was added. Check the file format.', 'license.checkingTitle': 'Checking license…', 'license.checkingWait': 'Please wait.', 'license.requiredTitle': 'License required', 'license.requiredHint': 'Enter your key via Settings → Enter license key. Until activation, only Settings is available.', 'license.tokenTitle': 'Enter license key', 'license.tokenKey': 'KEY', 'license.tokenPlaceholder': 'TTRPG- or DND- product key…', 'license.tokenSaving': 'Saving…', 'license.eulaTitle': 'End User License Agreement', 'license.eulaReject': 'Decline', 'license.eulaAccept': 'I accept the terms', 'license.eulaNoteEn': 'The binding legal text below is in Russian. If you need an English summary, contact support.', 'license.aboutTitle': 'About license', 'license.aboutDevSkip': 'Development mode: license checks are disabled (DND_SKIP_LICENSE).', 'license.aboutStatus': 'STATUS', 'license.aboutProduct': 'PRODUCT', 'license.aboutLicenseId': 'LICENSE ID', 'license.aboutExpiry': 'EXPIRES', 'license.aboutDevice': 'DEVICE', 'license.aboutNoData': 'No license data.', 'license.reason.ok': 'Active', 'license.reason.none': 'No key provided', 'license.reason.expired': 'Expired', 'license.reason.bad_signature': 'Invalid signature', 'license.reason.bad_payload': 'Invalid token format', 'license.reason.malformed': 'Malformed token', 'license.reason.not_yet_valid': 'Not yet valid', 'license.reason.wrong_device': 'Wrong bound device', 'license.reason.revoked_remote': 'Revoked on server', 'presentation.overlay': 'Presentation running', 'presentation.title': 'Presentation running', 'presentation.body': 'The editor is locked. Close the Presentation and Control windows to continue.', 'zip.progress': 'Operation progress', 'zip.importTitle': 'Import project', 'zip.exportTitle': 'Export project', 'top.settings': 'Settings', 'top.project': 'Project', 'top.file': 'File', 'top.backToProjects': 'Back to projects', 'top.appVersion': 'App version', 'top.run': 'Run', 'top.afterLicense': 'Available after license activation', 'top.setStartScene': 'Set a start scene on the graph (right‑click a node)', 'menu.enterKey': 'Enter license key', 'menu.aboutLicense': 'About license', 'menu.checkUpdates': 'Check for updates', 'menu.language': 'Language', 'menu.langRu': 'Русский', 'menu.langEn': 'English', 'menu.aboutProgram': 'About', 'menu.instructions': 'Instructions', 'top.aboutApp': 'About', 'app.about.title': 'About', 'app.about.tagline': 'Campaign editor and GM control panel for tabletop RPGs', 'app.about.description': 'TTRPG Player is a desktop app for game masters: build your campaign as a scene graph with maps, music and branching paths, then run the session from the control panel. Players see only the presentation on a second screen — no editor panels.', 'app.about.versionLabel': 'VERSION', 'app.about.developerLabel': 'DEVELOPER', 'app.about.developer': 'Independent development. For licensing, purchase and support questions, use the email below.', 'app.about.supportLabel': 'SUPPORT', 'app.about.supportEmail': 'player.ttrpg@gmail.com', 'app.about.websiteLabel': 'WEBSITE & UPDATES', 'app.about.websiteUrl': 'https://updates.mailib.ru/', 'help.title': 'Instructions', 'help.navAria': 'Instruction sections', 'help.section.overview.title': 'App overview', 'help.section.overview.body': 'TTRPG Player uses three windows: Editor (campaign prep), Control panel (running the session) and Presentation (player screen). The editor opens at launch; the control panel and presentation appear after you click Run in the editor header.\n\nIn the editor, the left column lists scenes, the center shows the scene graph, and the right column holds game and scene properties. While presentation windows are open, the editor is locked — close presentation and control to edit again.\n\nInternet is only needed for license activation and update checks. Projects and media are stored locally on your computer.', 'help.section.license.title': 'License and first launch', 'help.section.license.body': 'On first launch, open Settings → Enter license key. If the EULA has not been accepted yet, you will see the agreement first — accept it, paste your product key (TTRPG-… or DND-… format) and click Save.\n\nUntil the license is active, only Settings is available. Creating and opening projects, editing scenes and running a session unlock after successful activation.\n\nLicense status, expiry and device binding are under Settings → About license. The key is bound to your computer; a new PC may require re-activation per your purchase terms.', 'help.section.projects.title': 'Projects', 'help.section.projects.body': 'On the home screen (no project open), the left column shows Projects. Enter a name and click Create project, or click an existing project to open it.\n\nProject → Home closes the current project and returns to the list. Clicking the app logo and title in the header does the same.\n\nProject → Import loads a .ttrpg.zip archive from another machine. Project → Export saves a copy of the selected project to a folder you choose.\n\nFile → Rename project (with a project open) changes the display name and on-disk file name. At least 3 characters; file names cannot contain <>:"/\\|?*.\n\nIn the project list, the ⋯ button on a card opens delete. Deletion permanently removes the project file and cache from disk.', 'help.section.scenes.title': 'Scenes', 'help.section.scenes.body': 'A scene is a campaign unit: a location, beat or story frame with preview, description and audio. In the editor left column, click + New scene to create an empty scene.\n\nThe Search scenes… field filters the list by title. Click a scene card to select it and edit properties on the right; the same scene is highlighted on the graph.\n\nRight-click a list card for Delete. Deleting a scene removes it from the list and graph including links. You can drag a scene from the list onto the graph (see Scene graph).', 'help.section.graph.title': 'Scene graph', 'help.section.graph.body': 'The graph is a visual map of your campaign. Each node is a scene instance; the same scene can appear on the graph more than once (e.g. returning to a location).\n\nDrag a scene from the left list onto empty graph space to create a node. Drag nodes to arrange the layout. Connect two nodes: pull from the bottom handle of one node to the top handle of another — a transition arrow appears. One card can have several outgoing links (branching).\n\nYou cannot add a second link to the same target from the same source. To remove a link, select the arrow and press Delete or use the flow context menu.\n\nRight-click a node: Start scene — where the party begins when you Run; Delete — remove the node from the graph (the scene stays in the list). The start node shows a START badge.\n\nThe zoom bar at the bottom: zoom in, zoom out, fit view. The mouse wheel over the graph also zooms.', 'help.section.sceneProps.title': 'Scene properties', 'help.section.sceneProps.body': 'The right column Scene properties is active when a scene is selected. Scene title and Description are for the GM; description appears on the control panel storyline block.\n\nScene preview is the main image or video for players. Click Upload or Change and pick a file (images: PNG, JPG, WebP, GIF, etc.). For images, Rotate turns the preview in 90° steps. Clear removes the preview.\n\nFor video preview, check Autostart if the clip should start on the presentation screen. On video scenes, brush effects on the control panel are disabled (same as on presentation).\n\nScene audio — music and sounds for this scene. Upload adds files. Per track: Auto (play when entering the scene on the control panel) and Loop. The trash icon removes a track from the scene.\n\nThe Branching block reminds you that links are drawn on the graph, not in the inspector.', 'help.section.campaignAudio.title': 'Game audio', 'help.section.campaignAudio.body': 'In the right column, Game properties → Game audio holds campaign-wide music (theme, ambient) not tied to a specific scene.\n\nClick Upload and choose audio files. Set Auto and Loop per track like scene audio. Remove with the trash icon.\n\nOn the control panel, scene music takes priority over game music: while a scene with audio is active, campaign tracks pause and may resume when you move to a scene without its own music or after manual control.', 'help.section.session.title': 'Starting a session', 'help.section.session.body': 'When the campaign is ready, set a start scene on the graph (right-click a node → Start scene). The Run button in the editor header becomes enabled.\n\nRun opens Presentation (for players) and Control panel (for the GM). The editor shows a dimmed overlay — Presentation running — which is expected during a session.\n\nTo edit again, click Stop presentation or End presentation on the control panel (when there are no further transitions). Both windows must close before the editor unlocks.\n\nMove the presentation window to a second monitor, projector or TV and fullscreen it (F11 or OS controls). Players see only the map/video and effects, not the GM UI.', 'help.section.controlPanel.title': 'Control panel', 'help.section.controlPanel.body': 'The control panel is the GM’s main workspace during play. On the left: effect tools and storyline; on the right: screen preview, branch options and music.\n\nScreen preview shows what players see, scaled down. You can paint effects here (for scenes with an image preview).\n\nStop presentation closes presentation and control and unlocks the editor.\n\nStoryline lists graph nodes visited in the current session. The current scene is marked CURRENT SCENE. Click a visited step to rewind the party to that graph point without adding a new history entry.', 'help.section.transitions.title': 'Scene transitions', 'help.section.transitions.body': 'Outgoing links from the current graph node appear on the control panel under Branch options — cards OPTION 1, OPTION 2, etc. with the target scene title.\n\nClick Switch on the desired option — presentation and music move to that scene; storyline gains a new step.\n\nIf there are no outgoing links (end of branch or dead end), you see No transitions available and End presentation — it closes the show windows.\n\nTransitions are only those links you drew on the graph in the editor from the current node to other nodes.', 'help.section.music.title': 'Music on the control panel', 'help.section.music.body': 'The Music section splits into Scene music and Game music — the same tracks configured in the editor.\n\nEach track shows Auto/Manual and Loop/Once modes plus playback status. ▶ / ⏸ / ⏹ control playback manually.\n\nThe progress bar — click to seek (when duration is known). ← → on the bar (when focused) seek by 5 seconds.\n\nIf autoplay is blocked, press Play manually — after a user gesture audio usually works. On format errors, check the file (common audio formats are supported).', 'help.section.effects.title': 'Field and action effects', 'help.section.effects.body': 'Effects are available on the control panel for scenes with an image preview (not video). Paint in the Screen preview area — effects sync to presentation.\n\nTools: eraser 🧹 — click or drag over an effect to remove it; Clear effects removes all effects at once.\n\nField effects: fog, rain, fire, water — hold the left mouse button and brush on the map.\n\nAction effects: lightning, sunbeam, freeze, poison cloud — click or short stroke; lightning and some effects include sound and limited animation duration.\n\nBrush radius — slider under the effects panel; changes stroke size. Higher values mean a wider brush.', 'help.section.presentation.title': 'Presentation screen', 'help.section.presentation.body': 'The Presentation window shows players the current scene preview: map image (with rotation from the editor) or video with autostart/loop per settings.\n\nPainted GM effects appear on top of the map. There is no editor or control UI on this screen.\n\nWhen you switch scenes from the control panel, presentation updates automatically. Place the window on the display players watch and hide the taskbar if needed.\n\nIf no preview is set, the screen may stay empty or dark — set preview in scene properties in the editor.', 'help.section.importExport.title': 'Import and export', 'help.section.importExport.body': 'A project is stored as a .ttrpg.zip archive in the app data folder. Project → Export creates a copy: pick a project, click Save as… and choose a path — useful for backup or moving to another PC.\n\nProject → Import opens a .ttrpg.zip picker. After import the project appears on the home screen. Import and export show a progress bar for large archives.\n\nMedia (images, audio, video) is packed into the project when you upload in the editor — you do not need to move files separately if you share the whole .ttrpg.zip.', 'help.section.settings.title': 'Settings, language and updates', 'help.section.settings.body': 'Settings → Enter license key — change or enter your license. About license — status and expiry. Check for updates (installed build with active license) — searches for a new version; you can download and restart when an update is available.\n\nLanguage → Русский or English switches the editor, control panel and system messages. The choice persists between launches.\n\nThe app version appears in the header to the right of the menus (e.g. v1.0.22). About → About opens product info and developer contacts.', 'updates.dialogTitle': 'Updates', 'updates.checking': 'Checking for updates…', 'updates.available': 'A new version is available: {version}.', 'updates.current': 'You have the latest version ({version}).', 'updates.error': 'Could not check for updates: {message}', 'updates.notPackaged': 'Updates can only be checked in the installed application.', 'updates.noLicense': 'An active license is required.', 'updates.download': 'Update', 'updates.downloading': 'Downloading…', 'updates.stageLine': 'Stage: {stage}', 'updates.stage.checking': 'checking for updates', 'updates.stage.available': 'version {version} available', 'updates.stage.not-available': 'up to date', 'updates.stage.downloading': 'downloading{percent}', 'updates.stage.installing': 'installing and restarting', 'updates.stage.error': 'error', 'updates.stagePercent': ' ({percent}%)', 'projectMenu.home': 'Home', 'projectMenu.import': 'Import', 'projectMenu.export': 'Export', 'projectMenu.noProjects': 'No saved projects', 'fileMenu.rename': 'Rename project', 'scenes.search': 'Search scenes…', 'scenes.new': '+ New scene', 'scenes.inspectorGame': 'Game properties', 'scenes.inspectorScene': 'Scene properties', 'scenes.selectHint': 'Select a scene on the left to edit its properties.', 'scenes.openProjectHint': 'Open a project to edit the campaign and scenes.', 'rename.title': 'Rename project', 'rename.projectName': 'PROJECT NAME', 'rename.projectPlaceholder': 'Project name…', 'rename.projectMin': 'At least 3 characters.', 'rename.projectDup': 'A project with this name already exists.', 'rename.fileName': 'PROJECT FILE NAME', 'rename.fileInvalid': 'At least 3 characters; forbidden characters <>:"/\\|?*', 'rename.fileDup': 'A project file with this name already exists.', 'rename.saving': 'Saving…', 'export.title': 'Export project', 'export.project': 'PROJECT', 'export.hint': 'A save dialog will open: choose a name and folder for the .ttrpg.zip file — a copy of the project archive will be created.', 'export.exporting': 'Exporting…', 'export.saveAs': 'Save as…', 'confirmDelete.title': 'Delete project', 'confirmDelete.body': 'Permanently delete project “{name}”? The file and cache will be removed from disk.', 'confirmDelete.failedTitle': 'Could not delete', 'picker.title': 'Projects', 'picker.newPlaceholder': 'New project name…', 'picker.create': 'Create project', 'picker.existing': 'EXISTING', 'picker.lockedHint': 'Opening and creating projects require an active license. The list still shows files in the app folder.', 'picker.empty': 'No projects yet.', 'picker.projectMenu': 'Project menu', 'picker.openDisabled': 'Open project — after license activation', 'picker.defaultName': 'My campaign', 'campaign.label': 'GAME AUDIO', 'campaign.noFiles': 'No files yet. Add audio.', 'campaign.auto': 'Auto', 'campaign.loop': 'Loop', 'campaign.removeTitle': 'Remove from campaign', 'campaign.upload': 'Upload', 'scene.title': 'SCENE TITLE', 'scene.description': 'DESCRIPTION', 'scene.preview': 'SCENE PREVIEW', 'scene.previewHint': 'Image file (PNG, JPG, WebP, GIF, etc.).', 'scene.previewEmpty': 'No preview', 'scene.previewBusy': 'Loading and optimizing image…', 'scene.change': 'Change', 'scene.clear': 'Clear', 'scene.autostart': 'Autostart', 'scene.rotate': 'Rotate', 'scene.audio': 'SCENE AUDIO', 'scene.removeTitle': 'Remove from scene', 'scene.branching': 'BRANCHING', 'scene.branchingHint': 'Drag a scene from the list onto the graph. One card can branch to several targets — one link per target scene. You cannot link twice to the same target (including a second card of the same scene).', 'sceneCard.current': 'CURRENT', 'sceneCard.menu': 'Scene menu', 'graph.badgeStart': 'START', 'graph.untitled': 'Untitled', 'graph.videoBadge': 'Video', 'graph.audioBadge': 'Audio', 'graph.loop': 'Loop', 'graph.autoplay': 'Autoplay', 'graph.previewAutostart': 'Preview autostart', 'graph.videoLoop': 'Video loop', 'graph.zoomBar': 'Graph zoom', 'graph.zoomIn': 'Zoom in', 'graph.zoomOut': 'Zoom out', 'graph.fitAll': 'Fit view', 'graph.startScene': 'Start scene', 'graph.unsetStartScene': 'Clear start scene mark', 'control.remoteTitle': 'CONTROL PANEL', 'control.effects': 'EFFECTS', 'control.tools': 'Tools', 'control.fieldEffects': 'Field effects', 'control.actionEffects': 'Action effects', 'control.eraser': 'Eraser', 'control.clearEffects': 'Clear effects', 'control.fog': 'Fog', 'control.rain': 'Rain', 'control.fire': 'Fire', 'control.water': 'Water', 'control.lightning': 'Lightning', 'control.sunbeam': 'Sunbeam', 'control.freeze': 'Freeze', 'control.poisonCloud': 'Poison cloud', 'control.brushRadius': 'Brush radius', 'control.storyLine': 'STORYLINE', 'control.gotoScene': 'Go to this scene', 'control.currentSceneBadge': 'CURRENT SCENE', 'control.passed': 'Visited', 'control.noActiveScene': 'No active scene.', 'control.screenPreview': 'Screen preview', 'control.stopPresentation': 'Stop presentation', 'control.videoBrushHint': 'Video preview: effect brush is disabled (like on the presentation screen — overlay is for images only).', 'control.branches': 'Branch options', 'control.option': 'OPTION {n}', 'control.unnamed': 'Untitled', 'control.switchScene': 'Switch', 'control.noBranches': 'No transitions available.', 'control.endPresentation': 'End presentation', 'control.music': 'Music', 'control.sceneMusic': 'SCENE MUSIC', 'control.gameMusic': 'GAME MUSIC', 'control.noSceneAudio': 'No audio in the current scene.', 'control.noGameAudio': 'No game audio.', 'control.modeAuto': 'Auto', 'control.modeManual': 'Manual', 'control.once': 'Once', 'control.loop': 'Loop', 'control.scrubSeek': 'Click to seek', 'control.durationUnknown': 'Duration unknown', 'control.pauseSceneMusic': 'Paused (scene)', 'control.pauseSceneMusicTitle': 'Scene has music', 'control.pauseCampaignTitle': 'Paused: scene has music', 'control.playFailed': 'Could not start playback.', 'control.audioAutoplayBlocked': 'Autoplay was blocked (user gesture required) or playback failed.', 'control.audioNoUrl': 'No URL', 'control.audioNoUrlDetail': 'Could not get dnd://asset URL for audio.', 'control.audioBlocked': 'Error / blocked', 'control.audioError': 'Error', 'control.audioMediaError': 'MediaError code={code} (1=ABORTED, 2=NETWORK, 3=DECODE, 4=SRC_NOT_SUPPORTED)', 'control.audioLoading': 'Loading…', 'control.audioPlaying': 'Playing', 'control.audioPaused': 'Paused', 'control.audioStopped': 'Stopped', 'control.previewTrackLabel': 'Preview (no captions)', 'control.transportPlay': 'Play', 'control.transportPause': 'Pause', 'control.transportStop': 'Stop', }, }; export function translateEditorMessage( locale: EditorLocale, key: string, vars?: Record, ): string { let s = EDITOR_MESSAGES[locale][key] ?? EDITOR_MESSAGES.ru[key] ?? key; if (vars) { for (const [k, v] of Object.entries(vars)) { s = s.split(`{${k}}`).join(String(v)); } } return s; }