refactor(gui): разделить app.go на binding-файлы по доменам, вынести sync apply

- app.go (1810→280 строк): только App struct, startup, DTOs, helpers
- bindings_{nodes,files,notes,actions,worklog,activity,sync,settings}.go
- sync_apply.go: все applyRemote* методы
- i18n: internal/i18n (Go, embed JSON) + frontend/src/lib/i18n (JS)
- core/sync/safe_path.go: SafeVaultPath
- scripts/check-i18n.sh: проверка хардкода кириллицы и bidi-символов
- build.sh: NVM loading, set -e

Все сборки (CLI, server, gui, frontend), go vet, go test проходят.
This commit is contained in:
2026-06-02 10:47:38 +08:00
parent 390d451977
commit 3089d777a8
20 changed files with 2658 additions and 1603 deletions
+32
View File
@@ -0,0 +1,32 @@
import ru from './locales/ru.js'
import en from './locales/en.js'
const catalogs = { ru, en }
let currentLocale = 'ru'
export function t(key, params) {
const catalog = catalogs[currentLocale]
let msg = catalog?.[key]
if (msg == null && currentLocale !== 'ru') {
msg = catalogs.ru?.[key]
}
if (msg == null) {
msg = key
}
if (params != null) {
for (const [k, v] of Object.entries(params)) {
msg = msg.replace(`{${k}}`, String(v))
}
}
return msg
}
export function setLocale(locale) {
if (catalogs[locale]) {
currentLocale = locale
}
}
export function getLocale() {
return currentLocale
}
+80
View File
@@ -0,0 +1,80 @@
export default {
'nav.today': 'Today',
'nav.inbox': 'Inbox',
'nav.activity': 'Activity',
'nav.clients': 'Clients',
'nav.projects': 'Projects',
'nav.recipes': 'Recipes',
'nav.documents': 'Documents',
'nav.archive': 'Archive',
'tab.overview': 'Overview',
'tab.notes': 'Notes',
'tab.files': 'Files',
'tab.actions': 'Actions',
'tab.worklog': 'Work Log',
'tab.activity': 'Activity',
'common.save': 'Save',
'common.cancel': 'Cancel',
'common.delete': 'Delete',
'common.rename': 'Rename',
'common.close': 'Close',
'common.create': 'Create',
'common.confirm': 'Confirm',
'common.back': '← Back',
'common.loading': 'Loading...',
'common.error': 'Error:',
'common.yes': 'Yes',
'common.ok': 'OK',
'common.run': 'Run',
'common.name': 'Name',
'common.settings': 'Settings',
'welcome.title': 'Verstak',
'welcome.selectSection': 'Select a section in the sidebar.',
'welcome.addCase': 'Add case',
'event.noteCreated': 'Note created',
'event.noteUpdated': 'Note updated',
'event.fileAdded': 'File added',
'event.fileDeleted': 'File deleted',
'event.fileRenamed': 'File renamed',
'event.fileCopied': 'File copied',
'event.fileMoved': 'File moved',
'event.caseCreated': 'Case created',
'action.openUrl': 'Open URL',
'action.openFile': 'Open file',
'action.openFolder': 'Open folder',
'action.runCommand': 'Run command',
'action.runScript': 'Run script',
'action.openTerminal': 'Open terminal',
'action.launchApp': 'Launch app',
'note.add': '+ Add note',
'note.noNotes': 'No notes',
'note.title': 'Note title',
'note.placeholder': 'Start writing...',
'file.addFile': '+ Add file',
'file.addFolder': '+ Add folder',
'file.preview': 'Preview',
'file.openExternal': 'Open in external program',
'file.openFolder': 'Open folder',
'file.delete': 'Delete',
'file.pickSingle': 'Select file',
'file.pickDirectory': 'Select folder',
'sync.title': 'Sync',
'sync.settings': 'Sync settings',
'sync.status': 'Status',
'sync.server': 'Server',
'sync.device': 'Device',
'sync.connected': 'Connected',
'sync.notConnected': 'Not connected',
'sync.disabled': 'Disabled',
'error.generic': 'An error occurred',
'error.invalidCredentials': 'Invalid username or password',
}
+254
View File
@@ -0,0 +1,254 @@
export default {
'nav.today': 'Сегодня',
'nav.inbox': 'Неразобранное',
'nav.activity': 'Активность',
'nav.clients': 'Клиенты',
'nav.projects': 'Проекты',
'nav.recipes': 'Рецепты',
'nav.documents': 'Документы',
'nav.archive': 'Архив',
'nav.sections': 'Разделы',
'nav.cases': 'Дела',
'nav.noCases': 'Нет дел',
'nav.sync': 'Синхронизация',
'nav.syncSettings': 'Настройки синхронизации',
'nav.syncNow': 'Синхронизировать',
'nav.selectPrompt': 'Выберите раздел или дело',
'nav.brand': 'Верстак',
'tab.overview': 'Обзор',
'tab.notes': 'Заметки',
'tab.files': 'Файлы',
'tab.actions': 'Действия',
'tab.worklog': 'Журнал',
'tab.activity': 'Активность',
'common.save': 'Сохранить',
'common.cancel': 'Отмена',
'common.delete': 'Удалить',
'common.rename': 'Переименовать',
'common.close': 'Закрыть',
'common.create': 'Создать',
'common.confirm': 'Подтверждение',
'common.back': '← Назад',
'common.loading': 'Загрузка...',
'common.error': 'Ошибка:',
'common.yes': 'Да',
'common.ok': 'OK',
'common.copy': 'Копировать',
'common.cut': 'Вырезать',
'common.paste': 'Вставить',
'common.duplicate': 'Дублировать',
'common.run': 'Запустить',
'common.test': 'Test',
'common.testAgain': 'Проверить',
'common.connect': 'Подключиться',
'common.disconnect': 'Отключиться',
'common.settings': 'Настройки',
'common.name': 'Название',
'common.type': 'Тип',
'common.section': 'Раздел',
'common.created': 'Создано',
'common.empty': 'Нет',
'common.newName': 'Новое имя',
'welcome.title': 'Верстак',
'welcome.selectSection': 'Выберите раздел в боковой панели.',
'welcome.createCase': 'Или создайте новое дело кнопкой «+».',
'welcome.addCase': 'Добавить дело',
'event.noteCreated': 'Заметка создана',
'event.noteUpdated': 'Заметка изменена',
'event.fileAdded': 'Файл добавлен',
'event.fileDeleted': 'Файл удалён',
'event.fileRenamed': 'Файл переименован',
'event.fileCopied': 'Файл скопирован',
'event.fileMoved': 'Файл перемещён',
'event.folderAdded': 'Папка добавлена',
'event.folderDeleted': 'Папка удалена',
'event.folderRenamed': 'Папка переименована',
'event.caseCreated': 'Дело создано',
'event.caseUpdated': 'Дело изменено',
'kind.project': 'Проект',
'kind.client': 'Клиент',
'kind.document': 'Документ',
'kind.recipe': 'Рецепт',
'kind.archive': 'Архив',
'kind.case': 'Дело',
'action.openUrl': 'Открыть URL',
'action.openFile': 'Открыть файл',
'action.openFolder': 'Открыть папку',
'action.runCommand': 'Запустить команду',
'action.runScript': 'Запустить скрипт',
'action.openTerminal': 'Открыть терминал',
'action.launchApp': 'Запустить приложение',
'action.addAction': '+ Добавить действие',
'action.newAction': 'Новое действие',
'action.noActions': 'Действий пока нет',
'action.run': 'Запустить',
'action.dataUrl': 'URL',
'action.dataPath': 'Путь',
'action.dataCommand': 'Команда',
'action.urlPlaceholder': 'https://example.com',
'action.pathPlaceholder': '/path/to/file',
'action.commandPlaceholder': 'команда',
'action.namePlaceholder': 'Например: Открыть сайт',
'note.add': '+ Добавить заметку',
'note.new': 'Новая заметка',
'note.title': 'Название заметки',
'note.noNotes': 'Нет заметок',
'note.createFirst': 'Создайте первую заметку для этого дела.',
'note.placeholder': 'Начните писать...',
'note.unsavedTitle': 'Несохранённые изменения',
'note.unsavedMessage': 'Закрыть редактор? Все несохранённые изменения будут потеряны.',
'note.unsavedClose': 'Закрыть',
'file.addFile': '+ Добавить файл',
'file.addFolder': '+ Добавить папку',
'file.newFile': '+ Новый файл',
'file.addFileSimple': 'Добавить файл',
'file.addFolderSimple': 'Добавить папку',
'file.noFiles': 'В этой папке пока нет файлов',
'file.noFilesCase': 'В этом проекте пока нет файлов',
'file.hint': 'Добавьте файл или папку, чтобы сохранить материалы проекта.',
'file.root': 'Файлы',
'file.preview': 'Предпросмотр',
'file.openExternal': 'Открыть во внешней программе',
'file.openFolder': 'Открыть папку',
'file.showInExplorer': 'Показать в проводнике',
'file.more': 'Ещё',
'file.delete': 'Удалить',
'file.ariaFolder': 'Папка',
'file.ariaFile': 'Файл',
'file.scanning': 'Сканирование...',
'file.pickSingle': 'Выберите файл',
'file.pickMultiple': 'Выберите файлы',
'file.pickDirectory': 'Выберите папку',
'file.importTitle': 'Добавить в',
'file.importFiles': 'Файлов:',
'file.importFolders': 'Папок:',
'file.importSize': 'Размер:',
'file.importCopy': 'Скопировать',
'file.importLink': 'Привязать',
'file.selectCaseFirst': 'Сначала выберите дело для добавления файлов',
'worklog.title': 'Журнал',
'worklog.whatDone': 'Что сделано',
'worklog.minutes': 'Мин',
'worklog.min': 'мин',
'worklog.log': 'Записать',
'worklog.empty': 'Записей работы пока нет',
'sync.title': 'Синхронизация',
'sync.settings': 'Настройки синхронизации',
'sync.status': 'Статус',
'sync.server': 'Сервер',
'sync.device': 'Устройство',
'sync.deviceId': 'ID устройства',
'sync.unpushed': 'Неотправлено',
'sync.lastSync': 'Последняя синх.',
'sync.revoked': 'Отозвано',
'sync.connected': 'Подключено',
'sync.notConnected': 'Не подключено',
'sync.disabled': 'Отключена',
'sync.serverUrl': 'URL сервера',
'sync.serverUrlPlaceholder': 'https://example.com:47732',
'sync.username': 'Логин',
'sync.usernamePlaceholder': 'username',
'sync.password': 'Пароль',
'sync.passwordPlaceholder': 'password',
'sync.autoSync': 'Автосинхронизация (мин, 0 = отключено)',
'sync.saveInterval': 'Сохранить интервал',
'sync.syncNow': 'Синхронизировать',
'sync.disconnect': 'Отключиться',
'sync.connect': 'Подключиться',
'sync.test': 'Проверить',
'sync.settingsSaved': 'интервал сохранён',
'today.title': 'Сегодня',
'today.changedCases': 'Изменён сегодня',
'today.timeline': 'Лента за сегодня',
'today.empty': 'Сегодня пока тихо',
'today.emptyHint': 'Здесь появятся дела, заметки, файлы и действия, с которыми вы работали сегодня.',
'today.plural.case_one': 'дело',
'today.plural.case_few': 'дела',
'today.plural.case_many': 'дел',
'today.plural.note_one': 'заметка',
'today.plural.note_few': 'заметки',
'today.plural.note_many': 'заметок',
'today.plural.file_one': 'файл',
'today.plural.file_few': 'файла',
'today.plural.file_many': 'файлов',
'today.plural.event_one': 'событие',
'today.plural.event_few': 'события',
'today.plural.event_many': 'событий',
'activity.title': 'Активность',
'activity.empty': 'Активность пока не зафиксирована',
'activity.perCaseEmpty': 'Активность пока не зафиксирована',
'overview.type': 'Тип',
'overview.section': 'Раздел',
'overview.created': 'Создано',
'overview.newNote': 'Новая заметка',
'overview.addFile': 'Добавить файл',
'overview.addAction': 'Добавить действие',
'overview.logTime': 'Записать время',
'overview.recentNotes': 'Последние заметки',
'overview.recentEntries': 'Последние записи',
'rename.title': 'Переименовать',
'rename.emptyError': 'Имя не может быть пустым',
'rename.invalidError': 'Недопустимое имя',
'delete.confirmTitle': 'Удаление',
'delete.confirmMessage': 'Удалить',
'delete.folder': 'папку',
'delete.file': 'файл',
'template.optionNone': 'Без шаблона',
'template.optional': 'Шаблон (опционально)',
'mime.jpeg': 'Изображение JPEG',
'mime.png': 'Изображение PNG',
'mime.gif': 'Изображение GIF',
'mime.webp': 'Изображение WebP',
'mime.svg': 'Изображение SVG',
'mime.bmp': 'Изображение BMP',
'mime.tiff': 'Изображение TIFF',
'mime.avif': 'Изображение AVIF',
'mime.pdf': 'PDF документ',
'mime.word': 'Документ Word',
'mime.excel': 'Таблица Excel',
'mime.ppt': 'Презентация PowerPoint',
'mime.zip': 'ZIP архив',
'mime.gzip': 'GZIP архив',
'mime.tar': 'TAR архив',
'mime.sevenz': '7z архив',
'mime.rar': 'RAR архив',
'mime.text': 'Текстовый файл',
'mime.html': 'HTML файл',
'mime.css': 'CSS файл',
'mime.js': 'JavaScript файл',
'mime.json': 'JSON файл',
'mime.xml': 'XML файл',
'mime.yaml': 'YAML файл',
'mime.binary': 'Бинарный файл',
'mime.executable': 'Исполняемый файл',
'mime.folder': 'Папка',
'mime.unknown': 'Неизвестно',
'mime.file': 'Файл',
'error.nameEmpty': 'Имя не может быть пустым',
'error.nameInvalid': 'Недопустимое имя',
'error.selectCaseFirst': 'Сначала выберите дело',
'error.generic': 'Произошла ошибка',
'error.invalidCredentials': 'Неверный логин или пароль',
'error.accountBlocked': 'Аккаунт заблокирован',
'error.emailNotConfirmed': 'Email не подтверждён',
'error.tokenInvalid': 'Неверный или просроченный токен',
'error.tokenExpired': 'Срок действия токена истёк',
}