Handy/src/i18n/locales/uk/translation.json
Hoss a4999164ae
feat(models): add opt-in custom Whisper model discovery (#622)
* feat(models): auto-discover custom Whisper models in models directory

Enable automatic discovery of custom GGML-format Whisper models (.bin files)
placed in the models directory, so users don't need to modify source code
to use their own fine-tuned models.

Backend changes:
- Add discover_custom_whisper_models() to scan for .bin files
- Generate display names from filenames (e.g., "my-model" → "My Model")
- Skip predefined model filenames to avoid duplicates
- Add 3 unit tests for discovery logic

Frontend changes:
- Split model dropdown into 3 sections: Custom, Downloaded, Downloadable
- Add collapsible section for downloadable models to reduce clutter
- Add max-height with scroll for long model lists
- Add "Custom" badge for user-provided models

* feat(models): make custom Whisper model discovery opt-in

[why] Address maintainer concern about support burden from community
models. Custom models should be a power-user feature, not enabled by
default.

[how] Add custom_models_enabled setting (default: false) in Debug
settings. Discovery only runs when enabled. Models show "Not officially
supported" messaging. Documentation updated with enable steps.

* feat(models): add is_custom field and integrate custom models with new UI

[why]
PR review requested an explicit is_custom field instead of inferring
custom status from url === null. Custom models also need proper
integration with the new models settings page from PR #478.

[how]
- Add is_custom: bool to ModelInfo struct, set true on discovered models
- Change discover_custom_whisper_models signature from &PathBuf to &Path
- Use 0.0 sentinel scores so UI hides score bars for custom models
- Add "Custom Models" section to ModelsSettings with 3-way model split
- Show "Custom" badge in ModelCard and ModelDropdown
- Hide language/translation tags when supported_languages is empty
- Remove custom models from available_models on delete instead of
  just marking as not downloaded (they have no re-download URL)
- Update tests with new fields and assertions

* chore(i18n): add custom model translation keys to all locales

[why]
Custom model feature introduces 5 new translation keys that need
to be present in all 15 non-English locales for CI to pass.

[how]
Add English placeholder values for: customModelDescription,
modelSelector.custom, settings.models.customModels,
settings.debug.customModels.label, settings.debug.customModels.description

* fix(models): keep language filter visible when no models match

[why]
The language filter was inside a conditionally rendered section
that disappeared when no downloaded models matched the selected
language, leaving the user stuck with no way to change the filter.

[how]
Always render the "Your Models" header row with the language filter,
only conditionally render the model cards below it.

* docs: fix custom models section reference in README

Model selector → Models settings page.

* fix(models): apply custom models toggle immediately without restart

[why]
Two bugs reported in PR review: (1) app breaks on restart when a
custom model was selected and the toggle is disabled — selected_model
still points to the missing model, causing "Model not found" error.
(2) Custom models remain visible in the UI after toggle-off because
the in-memory model list is never updated and no event is emitted.

[how]
- Add remove_custom_models() and add_custom_models() to ModelManager
  for runtime mutation of the available_models mutex
- On disable: reset selected_model to empty if it's a custom model,
  then remove custom models from the in-memory list
- On enable: run discover_custom_whisper_models against the mutex
- Emit model-state-changed event so the frontend refreshes immediately

* chore(i18n): remove "restart required" from custom model descriptions

Toggle now takes effect immediately, so the restart sentence
is inaccurate. Updated all 16 locales and README instructions.

* fix(models): clear stale model selection on startup

[why]
If a custom model file is deleted from disk while it's the selected
model, the app gets stuck on "Loading..." forever on next launch
because the model ID is not in available_models but
auto_select_model_if_needed only checked for empty string.

[how]
Validate that selected_model exists in available_models before
accepting it. If not found, clear the selection so auto-select
picks a valid downloaded model.

* remove toggle and clean up

* format

---------

Co-authored-by: CJ Pais <cj@cjpais.com>
2026-02-09 10:15:45 +08:00

502 lines
26 KiB
JSON
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{
"tray": {
"settings": "Налаштування...",
"checkUpdates": "Перевірити оновлення...",
"copyLastTranscript": "Скопіювати останню транскрипцію",
"quit": "Вийти",
"cancel": "Скасувати"
},
"sidebar": {
"general": "Загальні",
"models": "Моделі",
"advanced": "Розширені",
"postProcessing": "Постобробка",
"history": "Історія",
"debug": "Дебаг",
"about": "Інфо"
},
"onboarding": {
"subtitle": "Для початку оберіть модель транскрипції",
"recommended": "Рекомендовано",
"download": "Завантажити",
"downloading": "Завантаження...",
"customModelDescription": "Офіційно не підтримується",
"downloadFailed": "Завантаження не вдалося. Будь ласка, спробуйте ще раз.",
"modelCard": {
"accuracy": "точність",
"speed": "швидкість"
},
"models": {
"small": {
"name": "Whisper Small",
"description": "Швидка та досить точна"
},
"medium": {
"name": "Whisper Medium",
"description": "Хороша точність, середня швидкість"
},
"turbo": {
"name": "Whisper Turbo",
"description": "Збалансована точність та швидкість"
},
"large": {
"name": "Whisper Large",
"description": "Хороша точність, але повільна"
},
"parakeet-tdt-0.6b-v2": {
"name": "Parakeet V2",
"description": "Лише англійська. Найкраща модель для англомовних"
},
"parakeet-tdt-0.6b-v3": {
"name": "Parakeet V3",
"description": "Швидка та точна"
},
"moonshine-base": {
"name": "Moonshine Base",
"description": "Very fast, English only. Handles accents well."
}
},
"errors": {
"loadModels": "Не вдалося завантажити доступні моделі",
"downloadModel": "Не вдалося завантажити модель: {{error}}"
},
"permissions": {
"title": "Потрібні дозволи",
"description": "Handy потребує деяких дозволів для коректної роботи.",
"microphone": {
"title": "Доступ до мікрофона",
"description": "Потрібен для прослуховування вашого голосу для транскрипції."
},
"accessibility": {
"title": "Доступ до доступності",
"description": "Потрібен для введення транскрибованого тексту у ваші додатки."
},
"grant": "Надати дозвіл",
"granted": "Надано",
"waiting": "Очікування...",
"allGranted": "Все готово!",
"errors": {
"checkFailed": "Не вдалося перевірити дозволи. Будь ласка, спробуйте ще раз.",
"requestFailed": "Не вдалося запросити дозвіл. Будь ласка, спробуйте ще раз."
}
}
},
"modelSelector": {
"custom": "Користувацька",
"active": "Активна",
"noModelsAvailable": "Немає доступних моделей",
"extracting": "Розпакування {{modelName}}...",
"extractingMultiple": "Розпакування {{count}} моделей...",
"extractingGeneric": "Розпакування...",
"downloading": "Завантаження {{percentage}}%",
"downloadingMultiple": "Завантаження {{count}} моделей...",
"modelReady": "Модель готова",
"loading": "Завантаження {{modelName}}...",
"loadingGeneric": "Завантаження...",
"modelError": "Помилка моделі",
"modelUnloaded": "Модель вивантажена",
"noModelDownloadRequired": "Немає моделі - потрібно завантажити",
"deleteModel": "Видалити {{modelName}}",
"switching": "Перемикання...",
"downloadSpeed": "{{speed}} MB/s",
"capabilities": {
"languageSelection": "Підтримує кілька мов введення",
"multiLanguage": "Багатомовна",
"translation": "Може перекладати на англійську",
"translate": "Перекласти на англійську",
"singleLanguage": "Підтримує лише цю мову",
"languageOnly": "Лише {{language}}"
},
"cancel": "Скасувати",
"cancelDownload": "Скасувати завантаження"
},
"settings": {
"modelSettings": {
"title": "Налаштування {{model}}",
"noSettingsNeeded": "Ця модель працює автоматично без потреби в налаштуванні."
},
"general": {
"title": "Загальні",
"shortcut": {
"title": "Комбінації клавіш",
"description": "Налаштуйте клавіатурні скорочення для запуску запису мовлення в текст",
"loading": "Завантаження скорочень...",
"none": "Скорочення не налаштовані",
"notFound": "Скорочення не знайдено",
"pressKeys": "Натисніть клавіші...",
"bindings": {
"transcribe": {
"name": "Гаряча клавіша транскрипції",
"description": "Комбінація клавіш для запису та транскрибування вашого голосу."
},
"cancel": {
"name": "Гаряча клавіша скасування",
"description": "Комбінація клавіш для скасування поточного запису."
},
"transcribe_with_post_process": {
"name": "Гаряча клавіша постобробки",
"description": "Необов'язково: Спеціальна гаряча клавіша, яка завжди застосовує AI-постобробку до вашої транскрипції."
}
},
"errors": {
"restore": "Не вдалося відновити початкове скорочення",
"set": "Не вдалося встановити скорочення: {{error}}",
"reset": "Не вдалося скинути скорочення до початкового значення"
}
},
"language": {
"title": "Мова",
"description": "Оберіть мову для розпізнавання мовлення. Режим «Авто» визначить мову автоматично, а вибір конкретної мови може покращити точність.",
"descriptionUnsupported": "Модель Parakeet автоматично визначає мову. Ручний вибір не потрібен.",
"searchPlaceholder": "Пошук мов...",
"noResults": "Мов не знайдено",
"auto": "Авто"
},
"pushToTalk": {
"label": "Утримувати для запису (Push To Talk)",
"description": "Утримуйте для запису, відпустіть для зупинки"
}
},
"models": {
"title": "Моделі транскрипції",
"description": "Виберіть модель транскрипції або завантажте додаткові моделі. Різні моделі пропонують різні рівні точності та швидкості.",
"downloaded": "Завантажено",
"available": "Доступні для завантаження",
"deleteConfirm": "Ви впевнені, що хочете видалити {{modelName}}? Вам потрібно буде завантажити її знову, щоб використовувати.",
"deleteActiveConfirm": "{{modelName}} — ваша активна модель. Видалення зупинить транскрипцію, доки ви не оберете нову модель. Ви впевнені?",
"deleteTitle": "Видалити модель",
"filters": {
"all": "Усі",
"multiLanguage": "Багатомовні",
"translation": "Переклад",
"allLanguages": "Усі мови"
},
"noModelsMatch": "Жодна модель не відповідає цьому фільтру.",
"yourModels": "Завантажені моделі",
"availableModels": "Доступні для завантаження"
},
"sound": {
"title": "Звук",
"microphone": {
"title": "Мікрофон",
"description": "Оберіть бажаний мікрофон",
"placeholder": "Оберіть мікрофон...",
"loading": "Завантаження..."
},
"audioFeedback": {
"label": "Звукове сповіщення",
"description": "Відтворювати звук при початку та зупинці запису"
},
"outputDevice": {
"title": "Пристрій виводу",
"description": "Оберіть бажаний пристрій виводу звуку для звукових сповіщень",
"placeholder": "Оберіть пристрій виводу...",
"loading": "Завантаження..."
},
"volume": {
"title": "Гучність",
"description": "Налаштуйте гучність звукових сповіщень"
}
},
"advanced": {
"title": "Розширені",
"groups": {
"app": "Додаток",
"output": "Вивід",
"transcription": "Транскрипція",
"history": "Історія",
"experimental": "Експериментальне"
},
"experimentalToggle": {
"label": "Експериментальні функції",
"description": "Увімкнути експериментальні функції, які ще в розробці."
},
"startHidden": {
"label": "Запуск у фоні",
"description": "Запускати в системному треї без відкриття вікна"
},
"autostart": {
"label": "Запуск при старті системи",
"description": "Автоматично запускати Handy при вході в систему"
},
"overlay": {
"title": "Позиція оверлею",
"description": "Показувати візуальний оверлей під час запису та транскрипції. На Linux рекомендовано «Немає»",
"options": {
"none": "Немає",
"bottom": "Внизу",
"top": "Вгорі"
}
},
"pasteMethod": {
"title": "Метод вставки",
"description": "Оберіть спосіб вставки тексту. Прямий: емулює набір тексту. Немає: пропускає вставку, оновлює лише історію/буфер обміну.",
"options": {
"clipboard": "Буфер обміну ({{modifier}}+V)",
"clipboardCtrlShiftV": "Буфер обміну (Ctrl+Shift+V)",
"clipboardShiftInsert": "Буфер обміну (Shift+Insert)",
"direct": "Прямий",
"none": "Немає"
}
},
"clipboardHandling": {
"title": "Робота з буфером обміну",
"description": "Не змінювати буфер обміну зберігає поточний вміст буфера після транскрипції. Копіювати в буфер обміну залишає результат транскрипції в буфері після вставки.",
"options": {
"dontModify": "Не змінювати буфер обміну",
"copyToClipboard": "Копіювати в буфер обміну"
}
},
"translateToEnglish": {
"label": "Перекласти на англійську",
"description": "Автоматично перекладати мовлення з інших мов англійською під час транскрипції.",
"descriptionUnsupported": "Переклад не підтримується моделлю {{model}}."
},
"modelUnload": {
"title": "Вивантаження моделі",
"description": "Автоматично звільняти пам'ять GPU/CPU, коли модель не використовується протягом вказаного часу",
"options": {
"never": "Ніколи",
"immediately": "Негайно",
"min2": "Через 2 хвилини",
"min5": "Через 5 хвилин",
"min10": "Через 10 хвилин",
"min15": "Через 15 хвилин",
"hour1": "Через 1 годину",
"sec5": "Через 5 секунд (Дебаг)"
}
},
"customWords": {
"title": "Власні слова",
"description": "Додайте слова, які часто неправильно розпізнаються під час транскрипції. Система автоматично виправлятиме схожі за звучанням слова відповідно до вашого списку.",
"placeholder": "Додати слово",
"add": "Додати",
"remove": "Видалити {{word}}"
}
},
"postProcessing": {
"title": "Постобробка",
"hotkey": {
"title": "Гаряча клавіша"
},
"api": {
"title": "API (сумісний з OpenAI)",
"provider": {
"title": "Провайдер",
"description": "Оберіть OpenAI-сумісного провайдера"
},
"appleIntelligence": {
"title": "Apple Intelligence",
"description": "Працює повністю на пристрої. API-ключ або доступ до мережі не потрібні.",
"requirements": "Потрібен Mac з Apple Silicon під управлінням macOS Tahoe (26.0) або новіше. Apple Intelligence має бути увімкнений у Системних налаштуваннях.",
"unavailable": "Apple Intelligence недоступний на цьому пристрої. Потрібен Mac з Apple Silicon під управлінням macOS Tahoe (26.0) або новіше з увімкненим Apple Intelligence в Системних налаштуваннях."
},
"baseUrl": {
"title": "Базова URL-адреса",
"description": "Базова URL-адреса API для обраного провайдера. Лише власний провайдер можна редагувати.",
"placeholder": "https://api.openai.com/v1"
},
"apiKey": {
"title": "API-ключ",
"description": "API-ключ для обраного провайдера",
"placeholder": "sk-..."
},
"model": {
"title": "Модель",
"descriptionApple": "Вкажіть необов'язковий числовий ліміт токенів або залиште стандартний пресет на пристрої.",
"descriptionCustom": "Вкажіть ідентифікатор моделі, який очікує ваш ендпоінт.",
"descriptionDefault": "Оберіть модель, надану обраним провайдером",
"placeholderApple": "Apple Intelligence",
"placeholderWithOptions": "Шукайте або оберіть модель",
"placeholderNoOptions": "Введіть назву моделі",
"refreshModels": "Оновити моделі"
}
},
"prompts": {
"title": "Промпт",
"selectedPrompt": {
"title": "Обраний промпт",
"description": "Оберіть шаблон для покращення транскрипцій або створіть новий. Використовуйте ${output} у тексті промпта для посилання на захоплену транскрипцію."
},
"noPrompts": "Немає доступних промптів",
"selectPrompt": "Оберіть промпт",
"createNew": "Створити новий промпт",
"promptLabel": "Назва промпта",
"promptLabelPlaceholder": "Введіть назву промпта",
"promptInstructions": "Інструкції промпта",
"promptInstructionsPlaceholder": "Напишіть інструкції для виконання після транскрипції. Приклад: Покращити граматику та ясність для наступного тексту: ${output}",
"promptTip": "Порада: Використовуйте <code>${output}</code> для вставки транскрибованого тексту у ваш промпт.",
"updatePrompt": "Оновити промпт",
"deletePrompt": "Видалити промпт",
"createPrompt": "Створити промпт",
"cancel": "Скасувати",
"selectToEdit": "Оберіть промпт вище для перегляду та редагування його деталей.",
"createFirst": "Натисніть «Створити новий промпт» вище, щоб створити ваш перший промпт постобробки."
}
},
"history": {
"title": "Історія",
"openFolder": "Відкрити папку записів",
"loading": "Завантаження історії...",
"empty": "Транскрипцій поки немає. Почніть запис, щоб створити історію!",
"copyToClipboard": "Копіювати транскрипцію в буфер обміну",
"save": "Зберегти транскрипцію",
"unsave": "Видалити зі збережених",
"delete": "Видалити запис",
"deleteError": "Не вдалося видалити запис. Спробуйте ще раз."
},
"debug": {
"title": "Дебаг",
"logDirectory": {
"title": "Папка логів",
"description": "Розташування файлів логів"
},
"logLevel": {
"title": "Рівень логування",
"description": "Встановіть детальність логування"
},
"updateChecks": {
"label": "Перевірка оновлень",
"description": "Автоматично перевіряти наявність нових версій Handy"
},
"soundTheme": {
"label": "Звукова тема",
"description": "Оберіть звукову тему для сповіщень про початок і зупинку запису"
},
"wordCorrectionThreshold": {
"title": "Поріг корекції слів",
"description": "Чутливість для корекції власних слів"
},
"historyLimit": {
"title": "Ліміт історії",
"description": "Максимальна кількість записів в історії",
"entries": "записів"
},
"recordingRetention": {
"title": "Автовидалення записів",
"description": "Автоматично видаляти старі записи для економії місця",
"never": "Ніколи",
"preserveLimit": "Зберігати останні {{count}}",
"days3": "Через 3 дні",
"weeks2": "Через 2 тижні",
"months3": "Через 3 місяці",
"placeholder": "Оберіть період зберігання..."
},
"alwaysOnMicrophone": {
"label": "Постійно активний мікрофон",
"description": "Тримати мікрофон активним для швидшого відгуку"
},
"clamshellMicrophone": {
"title": "Мікрофон у закритому режимі",
"description": "Мікрофон для використання при закритій кришці ноутбука"
},
"postProcessingToggle": {
"label": "Постобробка",
"description": "Увімкнути покращення тексту за допомогою AI після транскрипції"
},
"muteWhileRecording": {
"label": "Вимкнути звук під час запису",
"description": "Вимикати системний звук під час запису"
},
"appendTrailingSpace": {
"label": "Додавати пробіл в кінці",
"description": "Додавати пробіл після вставленої транскрипції"
},
"keyboardImplementation": {
"title": "Keyboard Implementation",
"description": "Choose the keyboard shortcut backend.",
"bindingsReset": "Keyboard shortcuts were incompatible and reset to defaults"
},
"paths": {
"appData": "Дані програми:",
"models": "Моделі:",
"settings": "Налаштування:"
},
"pasteDelay": {
"title": "Затримка вставки",
"description": "Затримка перед надсиланням натискання клавіші вставки (у мілісекундах). Збільшіть, якщо вставляється неправильний текст."
}
},
"about": {
"title": "Інфо",
"version": {
"title": "Версія",
"description": "Поточна версія Handy"
},
"appDataDirectory": {
"title": "Папка даних програми",
"description": "Розташування даних Handy"
},
"sourceCode": {
"title": "Вихідний код",
"description": "Переглянути вихідний код та зробити внесок",
"button": "Переглянути на GitHub"
},
"supportDevelopment": {
"title": "Підтримати розробку",
"description": "Допоможіть нам продовжувати розвивати Handy",
"button": "Підтримати"
},
"acknowledgments": {
"title": "Подяки",
"whisper": {
"title": "Whisper.cpp",
"description": "Високопродуктивний інференс моделі автоматичного розпізнавання мовлення Whisper від OpenAI",
"details": "Handy використовує Whisper.cpp для швидкої локальної обробки мовлення в текст. Дякуємо за чудову роботу Георгію Герганову та контриб'юторам."
}
}
}
},
"footer": {
"downloadingModel": "Завантаження {{model}}...",
"checkingUpdates": "Перевірка оновлень...",
"updateAvailable": "Доступне оновлення: {{version}}",
"updateAvailableShort": "Доступне оновлення",
"upToDate": "Актуальна версія",
"downloadUpdate": "Завантажити оновлення",
"restart": "Перезапустити",
"updateCheckingDisabled": "Перевірка оновлень вимкнена",
"downloading": "Завантаження... {{progress}}%",
"installing": "Встановлення...",
"preparing": "Підготовка...",
"checkForUpdates": "Перевірити оновлення"
},
"common": {
"loading": "Завантаження...",
"save": "Зберегти",
"cancel": "Скасувати",
"reset": "Скинути",
"add": "Додати",
"remove": "Видалити",
"delete": "Видалити",
"edit": "Редагувати",
"create": "Створити",
"update": "Оновити",
"close": "Закрити",
"open": "Відкрити",
"default": "За замовчуванням",
"enabled": "Увімкнено",
"disabled": "Вимкнено",
"on": "Увімк.",
"off": "Вимк.",
"yes": "Так",
"no": "Ні",
"noOptionsFound": "Опцій не знайдено"
},
"accessibility": {
"permissionsRequired": "Потрібні права доступу",
"permissionsDescription": "Handy потребує дозволи доступності для введення транскрибованого тексту.",
"openSettings": "Відкрити Системні налаштування",
"dismiss": "Закрити"
},
"errors": {
"loadDirectory": "Помилка завантаження папки: {{error}}"
},
"appLanguage": {
"title": "Мова інтерфейсу",
"description": "Змінити мову інтерфейсу Handy"
},
"overlay": {
"transcribing": "Обробка..."
}
}