f462e65581
Co-authored-by: Cursor <cursoragent@cursor.com>
501 lines
23 KiB
TypeScript
501 lines
23 KiB
TypeScript
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<EditorLocale, Record<string, string>> = {
|
||
ru: {
|
||
'common.close': 'Закрыть',
|
||
'common.cancel': 'Отмена',
|
||
'common.save': 'Сохранить',
|
||
'common.understood': 'Понятно',
|
||
'common.message': 'Сообщение',
|
||
'common.error': 'Ошибка',
|
||
'common.delete': 'Удалить',
|
||
'common.closeMenu': 'Закрыть меню',
|
||
|
||
'notice.campaignAudioEmpty': 'Аудио не добавлено. Проверьте формат файла.',
|
||
|
||
'license.checkingTitle': 'Проверка лицензии…',
|
||
'license.checkingWait': 'Подождите.',
|
||
'license.requiredTitle': 'Требуется лицензия',
|
||
'license.requiredHint':
|
||
'Укажите ключ в меню «Настройки» → «Указать ключ». До активации доступно только меню «Настройки».',
|
||
'license.tokenTitle': 'Указать ключ',
|
||
'license.tokenKey': 'КЛЮЧ',
|
||
'license.tokenPlaceholder': 'Продуктовый ключ 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.language': 'Язык',
|
||
'menu.langRu': 'Русский',
|
||
'menu.langEn': 'English',
|
||
|
||
'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':
|
||
'Далее откроется окно сохранения: укажите имя и папку для файла .dnd.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',
|
||
|
||
'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': '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.language': 'Language',
|
||
'menu.langRu': 'Русский',
|
||
'menu.langEn': 'English',
|
||
|
||
'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 .dnd.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, string | number>,
|
||
): 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;
|
||
}
|