diff --git a/Cargo.lock b/Cargo.lock index 42e3bf6..65ed3c4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1768,6 +1768,51 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "fluent" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8137a6d5a2c50d6b0ebfcb9aaa91a28154e0a70605f112d30cb0cd4a78670477" +dependencies = [ + "fluent-bundle", + "unic-langid", +] + +[[package]] +name = "fluent-bundle" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01203cb8918f5711e73891b347816d932046f95f54207710bda99beaeb423bf4" +dependencies = [ + "fluent-langneg", + "fluent-syntax", + "intl-memoizer", + "intl_pluralrules", + "rustc-hash", + "self_cell", + "smallvec", + "unic-langid", +] + +[[package]] +name = "fluent-langneg" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7eebbe59450baee8282d71676f3bfed5689aeab00b27545e83e5f14b1195e8b0" +dependencies = [ + "unic-langid", +] + +[[package]] +name = "fluent-syntax" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54f0d287c53ffd184d04d8677f590f4ac5379785529e5e08b1c8083acdd5c198" +dependencies = [ + "memchr", + "thiserror 2.0.17", +] + [[package]] name = "fnv" version = "1.0.7" @@ -2968,6 +3013,25 @@ dependencies = [ "syn 2.0.113", ] +[[package]] +name = "intl-memoizer" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "310da2e345f5eb861e7a07ee182262e94975051db9e4223e909ba90f392f163f" +dependencies = [ + "type-map", + "unic-langid", +] + +[[package]] +name = "intl_pluralrules" +version = "7.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "078ea7b7c29a2b4df841a7f6ac8775ff6074020c6776d48491ce2268e068f972" +dependencies = [ + "unic-langid", +] + [[package]] name = "ipnet" version = "2.11.0" @@ -3059,6 +3123,8 @@ dependencies = [ name = "jarvis-core" version = "0.1.0" dependencies = [ + "fluent", + "fluent-bundle", "futures-util", "hound", "intent-classifier", @@ -3080,6 +3146,7 @@ dependencies = [ "tokio", "tokio-tungstenite", "toml 0.9.10+spec-1.1.0", + "unic-langid", "vosk", ] @@ -5338,6 +5405,12 @@ dependencies = [ "realfft", ] +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + [[package]] name = "rustc_version" version = "0.4.1" @@ -5526,6 +5599,12 @@ dependencies = [ "smallvec", ] +[[package]] +name = "self_cell" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b12e76d157a900eb52e81bc6e9f3069344290341720e9178cde2407113ac8d89" + [[package]] name = "semver" version = "1.0.27" @@ -6826,6 +6905,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" dependencies = [ "displaydoc", + "serde_core", "zerovec", ] @@ -7125,6 +7205,15 @@ dependencies = [ "utf-8", ] +[[package]] +name = "type-map" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb30dbbd9036155e74adad6812e9898d03ec374946234fbcebd5dfc7b9187b90" +dependencies = [ + "rustc-hash", +] + [[package]] name = "typeid" version = "1.0.3" @@ -7190,6 +7279,24 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc" +[[package]] +name = "unic-langid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28ba52c9b05311f4f6e62d5d9d46f094bd6e84cb8df7b3ef952748d752a7d05" +dependencies = [ + "unic-langid-impl", +] + +[[package]] +name = "unic-langid-impl" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dce1bf08044d4b7a94028c93786f8566047edc11110595914de93362559bc658" +dependencies = [ + "tinystr", +] + [[package]] name = "unic-ucd-ident" version = "0.9.0" @@ -8509,6 +8616,7 @@ version = "0.11.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" dependencies = [ + "serde", "yoke 0.8.1", "zerofrom", "zerovec-derive", diff --git a/Cargo.toml b/Cargo.toml index 1704188..4f6eb5c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,4 +36,7 @@ sha2 = "0.10" nnnoiseless = "0.5" sysinfo = "0.37.2" tokio-tungstenite = "0.28.0" -futures-util = "0.3" \ No newline at end of file +futures-util = "0.3" +fluent = "0.17.0" +fluent-bundle = "0.16.0" +unic-langid = "0.9" \ No newline at end of file diff --git a/crates/jarvis-app/src/main.rs b/crates/jarvis-app/src/main.rs index 06c388b..30d45c2 100644 --- a/crates/jarvis-app/src/main.rs +++ b/crates/jarvis-app/src/main.rs @@ -6,6 +6,7 @@ use std::sync::atomic::{AtomicBool, Ordering}; use jarvis_core::{ audio, audio_processing, commands, config, db, listener, recorder, stt, intent, ipc::{self, IpcAction}, + i18n, APP_CONFIG_DIR, APP_LOG_DIR, COMMANDS_LIST, DB, }; @@ -40,6 +41,9 @@ fn main() -> Result<(), String> { DB.set(Arc::new(RwLock::new(db::init_settings()))) .expect("DB already initialized"); + // init i18n + i18n::init(&DB.get().unwrap().read().language); + // initialize tray // @TODO. macOS currently not supported for tray functionality, // due to the separate thread in which tray processing works, diff --git a/crates/jarvis-app/src/tray.rs b/crates/jarvis-app/src/tray.rs index e2e50c5..3686477 100644 --- a/crates/jarvis-app/src/tray.rs +++ b/crates/jarvis-app/src/tray.rs @@ -10,7 +10,7 @@ use image; #[cfg(target_os="windows")] use winit::platform::windows::EventLoopBuilderExtWindows; -use jarvis_core::config; +use jarvis_core::{config, i18n}; const TRAY_ICON_BYTES: &[u8] = include_bytes!("../../../resources/icons/32x32.png"); @@ -21,22 +21,22 @@ pub fn init_blocking() { let icon = load_icon_from_bytes(TRAY_ICON_BYTES); // form tray menu - let tray_menu = Menu::with_items(&[ - &MenuItem::new("Перезапуск", true, None), - &MenuItem::new("Настройки", true, None), - &MenuItem::new("Выход", true, None), - ]) - .unwrap(); + // let tray_menu = Menu::with_items(&[ + // &MenuItem::new("Перезапуск", true, None), + // &MenuItem::new("Настройки", true, None), + // &MenuItem::new("Выход", true, None), + // ]) + // .unwrap(); let tray_menu = Menu::with_items(&[ - &MenuItem::with_id("restart", "Перезапуск", true, None), - &MenuItem::with_id("settings", "Настройки", true, None), - &MenuItem::with_id("exit", "Выход", true, None), + &MenuItem::with_id("restart", i18n::t("tray-restart"), true, None), + &MenuItem::with_id("settings", i18n::t("tray-settings"), true, None), + &MenuItem::with_id("exit", i18n::t("tray-exit"), true, None), ]).unwrap(); let _tray_icon = TrayIconBuilder::new() .with_menu(Box::new(tray_menu)) - .with_tooltip(config::TRAY_TOOLTIP) + .with_tooltip(i18n::t("tray-tooltip")) .with_icon(icon) .build() .unwrap(); diff --git a/crates/jarvis-core/Cargo.toml b/crates/jarvis-core/Cargo.toml index cdc1ec0..fa7469b 100644 --- a/crates/jarvis-core/Cargo.toml +++ b/crates/jarvis-core/Cargo.toml @@ -26,6 +26,9 @@ sha2.workspace = true nnnoiseless = { workspace = true, optional = true } tokio-tungstenite = { workspace = true, optional = true } futures-util = { workspace = true, optional = true } +fluent.workspace = true +fluent-bundle.workspace = true +unic-langid.workspace = true # pv_recorder = { workspace = true, optional = true } vosk = { version = "0.3.1", optional = true } diff --git a/crates/jarvis-core/src/config.rs b/crates/jarvis-core/src/config.rs index 566b2d8..12303d7 100644 --- a/crates/jarvis-core/src/config.rs +++ b/crates/jarvis-core/src/config.rs @@ -80,6 +80,8 @@ pub const AUTHOR_NAME: Option<&str> = option_env!("CARGO_PKG_AUTHORS"); pub const REPOSITORY_LINK: Option<&str> = option_env!("CARGO_PKG_REPOSITORY"); pub const TG_OFFICIAL_LINK: Option<&str> = Some("https://t.me/howdyho_official"); pub const FEEDBACK_LINK: Option<&str> = Some("https://t.me/jarvis_feedback_bot"); +pub const SUPPORT_BOOSTY_LINK: Option<&str> = Some("https://boosty.to/howdyho"); +pub const SUPPORT_PATREON_LINK: Option<&str> = Some("https://www.patreon.com/user?u=22843414"); /* Tray. diff --git a/crates/jarvis-core/src/db/structs.rs b/crates/jarvis-core/src/db/structs.rs index 58901e4..e7a1c8f 100644 --- a/crates/jarvis-core/src/db/structs.rs +++ b/crates/jarvis-core/src/db/structs.rs @@ -22,6 +22,8 @@ pub struct Settings { pub vad: VadBackend, pub gain_normalizer: bool, + pub language: String, + pub api_keys: ApiKeys, } @@ -41,6 +43,8 @@ impl Default for Settings { vad: config::DEFAULT_VAD, gain_normalizer: config::DEFAULT_GAIN_NORMALIZER, + language: String::from("ru"), + api_keys: ApiKeys { picovoice: String::from(""), openai: String::from(""), diff --git a/crates/jarvis-core/src/i18n.rs b/crates/jarvis-core/src/i18n.rs new file mode 100644 index 0000000..26de77b --- /dev/null +++ b/crates/jarvis-core/src/i18n.rs @@ -0,0 +1,176 @@ +use fluent_bundle::{FluentBundle, FluentResource, FluentArgs, FluentValue}; +use fluent_bundle::concurrent::FluentBundle as ConcurrentFluentBundle; +use once_cell::sync::OnceCell; +use parking_lot::RwLock; +use std::collections::HashMap; +use unic_langid::LanguageIdentifier; + +// locale files embedded at compile time +const LOCALE_RU: &str = include_str!("i18n/locales/ru.ftl"); +const LOCALE_EN: &str = include_str!("i18n/locales/en.ftl"); +const LOCALE_UA: &str = include_str!("i18n/locales/ua.ftl"); + +pub const SUPPORTED_LANGUAGES: &[&str] = &["ru", "en", "ua"]; +pub const DEFAULT_LANGUAGE: &str = "ru"; + +// use concurrent bundle (thread-safe) +type Bundle = ConcurrentFluentBundle; + +static BUNDLES: OnceCell> = OnceCell::new(); +static CURRENT_LANG: OnceCell> = OnceCell::new(); + +// Initialize i18n system +pub fn init(lang: &str) { + let bundles = create_bundles(); + BUNDLES.set(bundles).ok(); + + let lang = if SUPPORTED_LANGUAGES.contains(&lang) { lang } else { DEFAULT_LANGUAGE }; + CURRENT_LANG.set(RwLock::new(lang.to_string())).ok(); + + info!("i18n initialized with language: {}", lang); +} + +fn create_bundles() -> HashMap { + let mut bundles = HashMap::new(); + + bundles.insert("ru".to_string(), create_bundle("ru", LOCALE_RU)); + bundles.insert("en".to_string(), create_bundle("en", LOCALE_EN)); + bundles.insert("ua".to_string(), create_bundle("ua", LOCALE_UA)); + + bundles +} + +fn create_bundle(lang: &str, source: &str) -> Bundle { + let langid: LanguageIdentifier = lang.parse().expect("Invalid language identifier"); + let mut bundle = ConcurrentFluentBundle::new_concurrent(vec![langid]); + + let resource = FluentResource::try_new(source.to_string()) + .expect("Failed to parse FTL resource"); + + bundle.add_resource(resource).expect("Failed to add resource"); + bundle +} + +// Set current language +pub fn set_language(lang: &str) { + if let Some(current) = CURRENT_LANG.get() { + let lang = if SUPPORTED_LANGUAGES.contains(&lang) { lang } else { DEFAULT_LANGUAGE }; + *current.write() = lang.to_string(); + info!("Language changed to: {}", lang); + } +} + +// Get current language +pub fn get_language() -> String { + CURRENT_LANG.get() + .map(|l| l.read().clone()) + .unwrap_or_else(|| DEFAULT_LANGUAGE.to_string()) +} + +// Translate a key +pub fn t(key: &str) -> String { + t_with_args(key, None) +} + +// Translate a key with arguments +pub fn t_with_args(key: &str, args: Option<&FluentArgs>) -> String { + let lang = get_language(); + + let bundles = match BUNDLES.get() { + Some(b) => b, + None => return key.to_string(), + }; + + let bundle = match bundles.get(&lang) { + Some(b) => b, + None => bundles.get(DEFAULT_LANGUAGE).unwrap(), + }; + + let msg = match bundle.get_message(key) { + Some(m) => m, + None => return key.to_string(), + }; + + let pattern = match msg.value() { + Some(p) => p, + None => return key.to_string(), + }; + + let mut errors = vec![]; + let result = bundle.format_pattern(pattern, args, &mut errors); + + if !errors.is_empty() { + warn!("i18n errors for key '{}': {:?}", key, errors); + } + + result.to_string() +} + +// Translate with a single argument +pub fn t_arg(key: &str, arg_name: &str, arg_value: &str) -> String { + let mut args = FluentArgs::new(); + args.set(arg_name, FluentValue::from(arg_value)); + t_with_args(key, Some(&args)) +} + +// Translate with numeric argument +pub fn t_count(key: &str, count: i64) -> String { + let mut args = FluentArgs::new(); + args.set("count", FluentValue::from(count)); + t_with_args(key, Some(&args)) +} + +// Get all translations for current language (for frontend) +pub fn get_all_translations() -> HashMap { + let lang = get_language(); + get_translations_for(&lang) +} + +/// Get all translations for a specific language +pub fn get_translations_for(lang: &str) -> HashMap { + let mut result = HashMap::new(); + + let bundles = match BUNDLES.get() { + Some(b) => b, + None => return result, + }; + + let bundle = match bundles.get(lang) { + Some(b) => b, + None => match bundles.get(DEFAULT_LANGUAGE) { + Some(b) => b, + None => return result, + }, + }; + + // get source for this language and extract all keys + let source = match lang { + "ru" => LOCALE_RU, + "en" => LOCALE_EN, + "ua" => LOCALE_UA, + _ => LOCALE_RU, + }; + + // parse keys from FTL source (lines that have "=" and don't start with "#" or "-") + for line in source.lines() { + let line = line.trim(); + if line.is_empty() || line.starts_with('#') || line.starts_with('-') { + continue; + } + + if let Some(key) = line.split('=').next() { + let key = key.trim(); + if !key.is_empty() && !key.contains(' ') { + if let Some(msg) = bundle.get_message(key) { + if let Some(pattern) = msg.value() { + let mut errors = vec![]; + let value = bundle.format_pattern(pattern, None, &mut errors); + result.insert(key.to_string(), value.to_string()); + } + } + } + } + } + + result +} \ No newline at end of file diff --git a/crates/jarvis-core/src/i18n/locales/en.ftl b/crates/jarvis-core/src/i18n/locales/en.ftl new file mode 100644 index 0000000..10f293d --- /dev/null +++ b/crates/jarvis-core/src/i18n/locales/en.ftl @@ -0,0 +1,119 @@ +# ### APP INFO +app-name = JARVIS +app-description = Voice Assistant + +# ### TRAY MENU +tray-restart = Restart +tray-settings = Settings +tray-exit = Exit +tray-tooltip = JARVIS - Voice Assistant + +# ### HEADER +header-commands = COMMANDS +header-settings = SETTINGS + +# ### SEARCH +search-placeholder = Enter a command manually or say «Jarvis» ... + +# ### MAIN PAGE +assistant-not-running = ASSISTANT NOT RUNNING +assistant-offline-hint = You can configure it without starting. +btn-start = START +btn-starting = STARTING... + +# ### STATUS +status-disconnected = Disconnected +status-standby = Standby +status-listening = Listening... +status-processing = Processing... + +# ### STATS +stats-microphone = MICROPHONE +stats-neural-networks = NEURAL NETWORKS +stats-resources = RESOURCES +stats-system-default = System Default +stats-not-selected = Not selected +stats-loading = Loading... + +# ### FOOTER +footer-author = Project author +footer-telegram = Our Telegram channel +footer-github = Github repository +footer-support = Support the project on + +# ### SETTINGS +settings-title = Settings +settings-general = General +settings-devices = Devices +settings-neural-networks = Neural Networks +settings-audio = Audio +settings-recognition = Recognition +settings-about = About +settings-language = Language +settings-microphone = Microphone +settings-microphone-desc = The assistant will listen to this microphone. +settings-mic-default = Default (System) +settings-voice = Assistant voice +settings-voice-desc = Not all commands work with all sound packs. +settings-wake-word-engine = Wake word engine +settings-wake-word-desc = Choose the engine for wake word recognition. +settings-stt-engine = Speech recognition +settings-intent-engine = Intent recognition +settings-intent-engine-desc = Select neural network for command recognition. +settings-noise-suppression = Noise suppression +settings-noise-suppression-desc = Reduces background noise. +settings-vad = Voice detection (VAD) +settings-vad-desc = Skips silence, saves CPU resources. +settings-gain-normalizer = Gain normalizer +settings-gain-normalizer-desc = Automatically adjusts volume level. +settings-api-keys = API Keys +settings-save = Save +settings-cancel = Cancel +settings-back = Back +settings-enabled = Enabled +settings-disabled = Disabled + +# settings - beta notice +settings-beta-title = BETA version! +settings-beta-desc = Some features may not work correctly. +settings-beta-feedback = Report all bugs to +settings-beta-bot = our Telegram bot +settings-open-logs = Open logs folder + +# settings - picovoice +settings-attention = Attention! +settings-picovoice-warning = This neural network doesn't work for everyone! +settings-picovoice-waiting = We are waiting for an official patch from the developers. +settings-picovoice-key-desc = Enter your Picovoice key here. It is issued for free upon registration at +settings-picovoice-key = Picovoice Key + +# settings - vosk +settings-auto-detect = Auto-detect +settings-vosk-model = Speech recognition model (Vosk) +settings-vosk-model-desc = Select Vosk model for speech recognition. +settings-models-not-found = Models not found +settings-models-hint = Place Vosk models in resources/vosk folder + +# settings - openai +settings-openai-key = OpenAI Key +settings-openai-not-supported = ChatGPT is not currently supported. It will be added in future updates. + +# ### COMMANDS PAGE +commands-title = Commands +commands-search = Search commands... +commands-count = { $count } commands +commands-wip-title = [404] This section is under development! +commands-wip-desc = Here will be a list of commands + full-featured command editor. +commands-wip-follow = Follow updates in +commands-wip-channel = our Telegram channel + +# ### ERRORS +error-generic = An error occurred +error-connection = Connection error +error-not-found = Not found + +# ### NOTIFICATIONS +notification-saved = Settings saved! +notification-error = Error +notification-assistant-started = Assistant started +notification-assistant-stopped = Assistant stopped diff --git a/crates/jarvis-core/src/i18n/locales/ru.ftl b/crates/jarvis-core/src/i18n/locales/ru.ftl new file mode 100644 index 0000000..a883c66 --- /dev/null +++ b/crates/jarvis-core/src/i18n/locales/ru.ftl @@ -0,0 +1,119 @@ +# ### APP INFO +app-name = JARVIS +app-description = Голосовой ассистент + +# ### TRAY MENU +tray-restart = Перезапустить +tray-settings = Настройки +tray-exit = Выход +tray-tooltip = JARVIS - Голосовой ассистент + +# ### HEADER +header-commands = КОМАНДЫ +header-settings = НАСТРОЙКИ + +# ### SEARCH +search-placeholder = Введите команду вручную или произнесите «Джарвис» ... + +# ### MAIN PAGE +assistant-not-running = АССИСТЕНТ НЕ ЗАПУЩЕН +assistant-offline-hint = Настроить его можно не запуская. +btn-start = ЗАПУСТИТЬ +btn-starting = ЗАПУСК... + +# ### STATUS +status-disconnected = Отключен +status-standby = Ожидание +status-listening = Слушаю... +status-processing = Обработка... + +# ### STATS +stats-microphone = МИКРОФОН +stats-neural-networks = НЕЙРОСЕТИ +stats-resources = РЕСУРСЫ +stats-system-default = Системный +stats-not-selected = Не выбран +stats-loading = Загрузка... + +# ### FOOTER +footer-author = Автор проекта +footer-telegram = Наш телеграм канал +footer-github = Github репозиторий проекта +footer-support = Поддержать проект на + +# ### SETTINGS +settings-title = Настройки +settings-general = Основные +settings-devices = Устройства +settings-neural-networks = Нейросети +settings-audio = Аудио +settings-recognition = Распознавание +settings-about = О программе +settings-language = Язык +settings-microphone = Микрофон +settings-microphone-desc = Его будет слушать ассистент. +settings-mic-default = По умолчанию (Система) +settings-voice = Голос ассистента +settings-voice-desc = Не все команды работают со всеми звуковыми пакетами. +settings-wake-word-engine = Движок активации +settings-wake-word-desc = Выберите нейросеть для распознавания активационной фразы. +settings-stt-engine = Распознавание речи +settings-intent-engine = Определение намерения +settings-intent-engine-desc = Выберите нейросеть для распознавания команд. +settings-noise-suppression = Шумоподавление +settings-noise-suppression-desc = Уменьшает фоновый шум. +settings-vad = Определение голоса (VAD) +settings-vad-desc = Пропускает тишину, экономит ресурсы CPU. +settings-gain-normalizer = Нормализация громкости +settings-gain-normalizer-desc = Автоматически регулирует уровень громкости. +settings-api-keys = API Ключи +settings-save = Сохранить +settings-cancel = Отмена +settings-back = Назад +settings-enabled = Включено +settings-disabled = Отключено + +# settings - beta notice +settings-beta-title = БЕТА версия! +settings-beta-desc = Часть функций может работать некорректно. +settings-beta-feedback = Сообщайте обо всех найденных багах в +settings-beta-bot = наш телеграм бот +settings-open-logs = Открыть папку с логами + +# settings - picovoice +settings-attention = Внимание! +settings-picovoice-warning = Эта нейросеть работает не у всех! +settings-picovoice-waiting = Мы ждем официального патча от разработчиков. +settings-picovoice-key-desc = Введите сюда свой ключ Picovoice. Он выдается бесплатно при регистрации в +settings-picovoice-key = Ключ Picovoice + +# settings - vosk +settings-auto-detect = Авто-определение +settings-vosk-model = Модель распознавания речи (Vosk) +settings-vosk-model-desc = Выберите модель Vosk для распознавания речи. +settings-models-not-found = Модели не найдены +settings-models-hint = Поместите модели Vosk в папку resources/vosk + +# settings - openai +settings-openai-key = Ключ OpenAI +settings-openai-not-supported = В данный момент ChatGPT не поддерживается. Он будет добавлен в ближайших обновлениях. + +# ### COMMANDS PAGE +commands-title = Команды +commands-search = Поиск команд... +commands-count = { $count } команд +commands-wip-title = [404] Этот раздел еще находится в разработке! +commands-wip-desc = Тут будет список команд + полноценный редактор команд. +commands-wip-follow = Следите за обновлениями в +commands-wip-channel = нашем телеграм канале + +# ### ERRORS +error-generic = Произошла ошибка +error-connection = Ошибка подключения +error-not-found = Не найдено + +# ### NOTIFICATIONS +notification-saved = Настройки сохранены! +notification-error = Ошибка +notification-assistant-started = Ассистент запущен +notification-assistant-stopped = Ассистент остановлен diff --git a/crates/jarvis-core/src/i18n/locales/ua.ftl b/crates/jarvis-core/src/i18n/locales/ua.ftl new file mode 100644 index 0000000..3f39b2c --- /dev/null +++ b/crates/jarvis-core/src/i18n/locales/ua.ftl @@ -0,0 +1,119 @@ +# ### APP INFO +app-name = JARVIS +app-description = Голосовий асистент + +# ### TRAY MENU +tray-restart = Перезапустити +tray-settings = Налаштування +tray-exit = Вихід +tray-tooltip = JARVIS - Голосовий асистент + +# ### HEADER +header-commands = КОМАНДИ +header-settings = НАЛАШТУВАННЯ + +# ### SEARCH +search-placeholder = Введіть команду вручну або скажіть «Джарвіс» ... + +# ### MAIN PAGE +assistant-not-running = АСИСТЕНТ НЕ ЗАПУЩЕНО +assistant-offline-hint = Налаштувати його можна не запускаючи. +btn-start = ЗАПУСТИТИ +btn-starting = ЗАПУСК... + +# ### STATUS +status-disconnected = Відключено +status-standby = Очікування +status-listening = Слухаю... +status-processing = Обробка... + +# ### STATS +stats-microphone = МІКРОФОН +stats-neural-networks = НЕЙРОМЕРЕЖІ +stats-resources = РЕСУРСИ +stats-system-default = Системний +stats-not-selected = Не вибрано +stats-loading = Завантаження... + +# ### FOOTER +footer-author = Автор проєкту +footer-telegram = Наш телеграм канал +footer-github = Github репозиторій проєкту +footer-support = Підтримати проєкт на + +# ### SETTINGS +settings-title = Налаштування +settings-general = Основні +settings-devices = Пристрої +settings-neural-networks = Нейромережі +settings-audio = Аудіо +settings-recognition = Розпізнавання +settings-about = Про програму +settings-language = Мова +settings-microphone = Мікрофон +settings-microphone-desc = Його буде слухати асистент. +settings-mic-default = За замовчуванням (Система) +settings-voice = Голос асистента +settings-voice-desc = Не всі команди працюють з усіма звуковими пакетами. +settings-wake-word-engine = Рушій активації +settings-wake-word-desc = Виберіть нейромережу для розпізнавання активаційної фрази. +settings-stt-engine = Розпізнавання мовлення +settings-intent-engine = Визначення наміру +settings-intent-engine-desc = Виберіть нейромережу для розпізнавання команд. +settings-noise-suppression = Шумозаглушення +settings-noise-suppression-desc = Зменшує фоновий шум. +settings-vad = Визначення голосу (VAD) +settings-vad-desc = Пропускає тишу, економить ресурси CPU. +settings-gain-normalizer = Нормалізація гучності +settings-gain-normalizer-desc = Автоматично регулює рівень гучності. +settings-api-keys = API Ключі +settings-save = Зберегти +settings-cancel = Скасувати +settings-back = Назад +settings-enabled = Увімкнено +settings-disabled = Вимкнено + +# settings - beta notice +settings-beta-title = БЕТА версія! +settings-beta-desc = Частина функцій може працювати некоректно. +settings-beta-feedback = Повідомляйте про всі знайдені баги в +settings-beta-bot = наш телеграм бот +settings-open-logs = Відкрити папку з логами + +# settings - picovoice +settings-attention = Увага! +settings-picovoice-warning = Ця нейромережа працює не у всіх! +settings-picovoice-waiting = Ми чекаємо офіційного патча від розробників. +settings-picovoice-key-desc = Введіть сюди свій ключ Picovoice. Він видається безкоштовно при реєстрації в +settings-picovoice-key = Ключ Picovoice + +# settings - vosk +settings-auto-detect = Авто-визначення +settings-vosk-model = Модель розпізнавання мовлення (Vosk) +settings-vosk-model-desc = Виберіть модель Vosk для розпізнавання мовлення. +settings-models-not-found = Моделі не знайдено +settings-models-hint = Помістіть моделі Vosk в папку resources/vosk + +# settings - openai +settings-openai-key = Ключ OpenAI +settings-openai-not-supported = Наразі ChatGPT не підтримується. Він буде доданий у наступних оновленнях. + +# ### COMMANDS PAGE +commands-title = Команди +commands-search = Пошук команд... +commands-count = { $count } команд +commands-wip-title = [404] Цей розділ ще в розробці! +commands-wip-desc = Тут буде список команд + повноцінний редактор команд. +commands-wip-follow = Слідкуйте за оновленнями в +commands-wip-channel = нашому телеграм каналі + +# ### ERRORS +error-generic = Сталася помилка +error-connection = Помилка підключення +error-not-found = Не знайдено + +# ### NOTIFICATIONS +notification-saved = Налаштування збережено! +notification-error = Помилка +notification-assistant-started = Асистент запущено +notification-assistant-stopped = Асистент зупинено diff --git a/crates/jarvis-core/src/lib.rs b/crates/jarvis-core/src/lib.rs index d258e16..00cec74 100644 --- a/crates/jarvis-core/src/lib.rs +++ b/crates/jarvis-core/src/lib.rs @@ -11,6 +11,7 @@ pub mod audio; pub mod commands; pub mod config; pub mod db; +pub mod i18n; #[cfg(feature = "jarvis_app")] pub mod listener; diff --git a/crates/jarvis-gui/src/main.rs b/crates/jarvis-gui/src/main.rs index 9110c4c..f6655c2 100644 --- a/crates/jarvis-gui/src/main.rs +++ b/crates/jarvis-gui/src/main.rs @@ -1,7 +1,7 @@ // Prevents additional console window on Windows in release, DO NOT REMOVE!! #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] -use jarvis_core::{config, db, APP_CONFIG_DIR, APP_LOG_DIR, DB}; +use jarvis_core::{config, db, i18n, APP_CONFIG_DIR, APP_LOG_DIR, DB}; use parking_lot::RwLock; use std::sync::Arc; @@ -26,6 +26,11 @@ fn main() { // init db let settings = db::init_settings(); + + // init i18n + i18n::init(&settings.language); + + // set db DB.set(Arc::new(RwLock::new(settings))) .expect("DB already initialized"); let db_arc = DB.get().unwrap().clone(); @@ -67,6 +72,13 @@ fn main() { // vosk tauri_commands::list_vosk_models, + + // i18n + tauri_commands::get_translations, + tauri_commands::translate, + tauri_commands::get_current_language, + tauri_commands::set_language, + tauri_commands::get_supported_languages, ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); diff --git a/crates/jarvis-gui/src/tauri_commands.rs b/crates/jarvis-gui/src/tauri_commands.rs index 8afc8f8..dbc1faa 100644 --- a/crates/jarvis-gui/src/tauri_commands.rs +++ b/crates/jarvis-gui/src/tauri_commands.rs @@ -25,4 +25,8 @@ pub use sys::*; // import STT commands mod stt; -pub use stt::*; \ No newline at end of file +pub use stt::*; + +// import i18n commands +mod i18n; +pub use i18n::*; \ No newline at end of file diff --git a/crates/jarvis-gui/src/tauri_commands/db.rs b/crates/jarvis-gui/src/tauri_commands/db.rs index 73fddb8..35e864b 100644 --- a/crates/jarvis-gui/src/tauri_commands/db.rs +++ b/crates/jarvis-gui/src/tauri_commands/db.rs @@ -15,6 +15,7 @@ pub fn db_read(state: tauri::State<'_, AppState>, key: &str) -> String { "noise_suppression" => format!("{:?}", settings.noise_suppression), "vad" => format!("{:?}", settings.vad), "gain_normalizer" => settings.gain_normalizer.to_string(), + "language" => settings.language.to_string(), "api_key__picovoice" => settings.api_keys.picovoice.clone(), "api_key__openai" => settings.api_keys.openai.clone(), _ => String::new(), @@ -78,6 +79,9 @@ pub fn db_write(state: tauri::State<'_, AppState>, key: &str, val: &str) -> bool _ => return false, } } + "language" => { + settings.language = val.to_string(); + } "api_key__picovoice" => { settings.api_keys.picovoice = val.to_string(); } diff --git a/crates/jarvis-gui/src/tauri_commands/etc.rs b/crates/jarvis-gui/src/tauri_commands/etc.rs index d14b09e..4bddc54 100644 --- a/crates/jarvis-gui/src/tauri_commands/etc.rs +++ b/crates/jarvis-gui/src/tauri_commands/etc.rs @@ -38,6 +38,24 @@ pub fn get_tg_official_link() -> String { } } +#[tauri::command] +pub fn get_boosty_link() -> String { + if let Some(ver) = config::SUPPORT_BOOSTY_LINK { + ver.to_string() + } else { + String::from("error") + } +} + +#[tauri::command] +pub fn get_patreon_link() -> String { + if let Some(ver) = config::SUPPORT_PATREON_LINK { + ver.to_string() + } else { + String::from("error") + } +} + #[tauri::command] pub fn get_feedback_link() -> String { if let Some(res) = config::FEEDBACK_LINK { diff --git a/crates/jarvis-gui/src/tauri_commands/i18n.rs b/crates/jarvis-gui/src/tauri_commands/i18n.rs new file mode 100644 index 0000000..19042ae --- /dev/null +++ b/crates/jarvis-gui/src/tauri_commands/i18n.rs @@ -0,0 +1,49 @@ +use jarvis_core::i18n; +use std::collections::HashMap; +use crate::AppState; + +// Get all translations for frontend +#[tauri::command] +pub fn get_translations() -> HashMap { + i18n::get_all_translations() +} + +// Get single translation +#[tauri::command] +pub fn translate(key: &str) -> String { + i18n::t(key) +} + +// Get current language +#[tauri::command] +pub fn get_current_language() -> String { + i18n::get_language() +} + +// Set language and get new translations +#[tauri::command] +pub fn set_language(state: tauri::State<'_, AppState>, lang: &str) -> HashMap { + // update i18n + i18n::set_language(lang); + + // also save to db + { + let mut settings = state.db.write(); + settings.language = lang.to_string(); + } + + // save to disk + let snapshot = state.db.read().clone(); + if let Err(e) = jarvis_core::db::save_settings(&snapshot) { + log::error!("Failed to save settings: {}", e); + } + + // return new translations + i18n::get_all_translations() +} + +// Get supported languages +#[tauri::command] +pub fn get_supported_languages() -> Vec<&'static str> { + i18n::SUPPORTED_LANGUAGES.to_vec() +} \ No newline at end of file diff --git a/crates/jarvis-gui/tauri.conf.json b/crates/jarvis-gui/tauri.conf.json index cb8d9da..db12b8b 100644 --- a/crates/jarvis-gui/tauri.conf.json +++ b/crates/jarvis-gui/tauri.conf.json @@ -20,7 +20,7 @@ "resizable": false, "title": "Jarvis Voice Assistant", "width": 550, - "height": 700 + "height": 800 } ] }, diff --git a/frontend/public/media/128x128.png b/frontend/public/media/128x128.png new file mode 100644 index 0000000..6c36989 Binary files /dev/null and b/frontend/public/media/128x128.png differ diff --git a/frontend/public/media/bg.png b/frontend/public/media/bg.png new file mode 100644 index 0000000..c4bc17a Binary files /dev/null and b/frontend/public/media/bg.png differ diff --git a/frontend/public/media/flags/EN.png b/frontend/public/media/flags/EN.png new file mode 100644 index 0000000..6533717 Binary files /dev/null and b/frontend/public/media/flags/EN.png differ diff --git a/frontend/public/media/flags/RU.png b/frontend/public/media/flags/RU.png new file mode 100644 index 0000000..b34c6a2 Binary files /dev/null and b/frontend/public/media/flags/RU.png differ diff --git a/frontend/public/media/flags/UA.png b/frontend/public/media/flags/UA.png new file mode 100644 index 0000000..235ec80 Binary files /dev/null and b/frontend/public/media/flags/UA.png differ diff --git a/frontend/public/media/icons/boosty.webp b/frontend/public/media/icons/boosty.webp new file mode 100644 index 0000000..e63f4b8 Binary files /dev/null and b/frontend/public/media/icons/boosty.webp differ diff --git a/frontend/public/media/icons/patreon.png b/frontend/public/media/icons/patreon.png new file mode 100644 index 0000000..627d8a4 Binary files /dev/null and b/frontend/public/media/icons/patreon.png differ diff --git a/frontend/public/media/icons/telegram.webp b/frontend/public/media/icons/telegram.webp new file mode 100644 index 0000000..75cc30e Binary files /dev/null and b/frontend/public/media/icons/telegram.webp differ diff --git a/frontend/public/media/jarvis-hover.png b/frontend/public/media/jarvis-hover.png new file mode 100644 index 0000000..016e8fb Binary files /dev/null and b/frontend/public/media/jarvis-hover.png differ diff --git a/frontend/public/media/jarvis.png b/frontend/public/media/jarvis.png new file mode 100644 index 0000000..45b9a12 Binary files /dev/null and b/frontend/public/media/jarvis.png differ diff --git a/frontend/src/App.svelte b/frontend/src/App.svelte index a35a359..5dc5fff 100644 --- a/frontend/src/App.svelte +++ b/frontend/src/App.svelte @@ -11,7 +11,8 @@ startStatsPolling, stopStatsPolling, connectIpc, - disconnectIpc + disconnectIpc, + loadTranslations } from "@/stores" onMount(() => { @@ -24,6 +25,9 @@ // connect to IPC connectIpc() + + // load language + loadTranslations() }) onDestroy(() => { diff --git a/frontend/src/components/Footer.svelte b/frontend/src/components/Footer.svelte index 466b29e..7de6982 100644 --- a/frontend/src/components/Footer.svelte +++ b/frontend/src/components/Footer.svelte @@ -1,17 +1,23 @@ + + .lang-btn { + display: flex; + align-items: center; + gap: 0.35rem; + padding: 0.5rem 0.65rem; + background: transparent; + border: none; + border-radius: 6px; + color: #ffffff; + font-size: 0.7rem; + cursor: pointer; + + &:hover { + background: rgba(35, 50, 55, 0.7); + } + } + + .lang-flag { + font-size: 0.9rem; + line-height: 1; + } + + .lang-code { + font-weight: 600; + letter-spacing: 0.5px; + } + + .lang-arrow { + font-size: 0.8rem; + opacity: 0.6; + transition: transform 0.2s ease; + + &.open { + transform: rotate(180deg); + } + } + + .lang-dropdown { + position: absolute; + top: calc(100% + 0.35rem); + right: 0; + background: rgba(20, 30, 35, 0.98); + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 6px; + overflow: hidden; + z-index: 100; + min-width: 130px; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.4); + } + + .lang-option { + display: flex; + align-items: center; + gap: 0.5rem; + width: 100%; + padding: 0.6rem 0.85rem; + background: transparent; + border: none; + color: rgba(255, 255, 255, 0.75); + font-size: 0.75rem; + cursor: pointer; + transition: all 0.15s ease; + text-align: left; + + &:hover { + background: rgba(82, 254, 254, 0.1); + color: #ffffff; + } + + &.active { + background: rgba(82, 254, 254, 0.15); + color: #52fefe; + } + } + + .lang-name { + font-weight: 500; + } + \ No newline at end of file diff --git a/frontend/src/components/elements/ArcReactor.svelte b/frontend/src/components/elements/ArcReactor.svelte index 2bf2780..68963c4 100644 --- a/frontend/src/components/elements/ArcReactor.svelte +++ b/frontend/src/components/elements/ArcReactor.svelte @@ -1,7 +1,6 @@ - - - -
-
-
-
    - {#each Array(60) as _, i} -
  • - {/each} -
-
-
-
-
-
+
+
+
    + {#each Array(60) as _, i} +
  • + {/each} +
+
+
+
+
+
+
+
+
+
-
-
-
-
-
- {#each Array(8) as _, i} -
- {/each} -
- -
- - - {#if $jarvisState === "disconnected"} - Отключен - {:else if $jarvisState === "idle"} - Ожидание - {:else if $jarvisState === "listening"} - Слушаю - {:else if $jarvisState === "processing"} - Обработка - {/if} - +
+
+
+
+
+ {#each Array(8) as _, i} +
+ {/each}
\ No newline at end of file diff --git a/frontend/src/components/elements/SearchBar.svelte b/frontend/src/components/elements/SearchBar.svelte index 3fd11bc..0b57c03 100644 --- a/frontend/src/components/elements/SearchBar.svelte +++ b/frontend/src/components/elements/SearchBar.svelte @@ -1,4 +1,8 @@ @@ -8,12 +12,11 @@ bind:value={searchQuery} type="text" name="q" - placeholder="Введите команду или скажите «Джарвис» ..." + placeholder={t('search-placeholder')} autocomplete="off" minlength="3" maxlength="30" /> - Enter -
+
\ No newline at end of file diff --git a/frontend/src/components/elements/Stats.svelte b/frontend/src/components/elements/Stats.svelte index 4546b1c..d461dc1 100644 --- a/frontend/src/components/elements/Stats.svelte +++ b/frontend/src/components/elements/Stats.svelte @@ -1,346 +1,130 @@ -
-
-
-
- Микрофон - {microphoneLabel} +
+
+ +
+ {t('stats-microphone')} + {truncate(microphoneName, 18)}
- -
-
-
- Нейросети - {wakeWordEngine} + {sttEngine} + +
+ +
+ {t('stats-neural-networks')} + {wakeWordEngine} + {sttEngine} + {#if vadInfo != "None"}VAD: {vadInfo}{/if}
- -
-
-
- Ресурсы - {#if jarvisStats.running} - RAM: {jarvisStats.ram_mb} MB - - {:else} - - - {/if} + +
+ +
+ {t('stats-resources')} + {#if jarvisRamUsage }RAM {$jarvisRamUsage}mb{:else}...{/if}
+ + .stat-content { + display: flex; + flex-direction: column; + gap: 0.15rem; + } + + .stat-label { + font-size: 0.8rem; + font-weight: 600; + color: #ffffff; + text-transform: uppercase; + letter-spacing: 0.5px; + } + + .stat-value { + font-size: 0.7rem; + color: rgba(255, 255, 255, 0.42); + line-height: 1.35; + font-style: italic; + } + + .stat-value-sub { + font-size: 0.65rem; + color: rgba(255, 255, 255, 0.28); + } + \ No newline at end of file diff --git a/frontend/src/components/elements/__ArcReactor.svelte b/frontend/src/components/elements/__ArcReactor.svelte new file mode 100644 index 0000000..2bf2780 --- /dev/null +++ b/frontend/src/components/elements/__ArcReactor.svelte @@ -0,0 +1,411 @@ + + + + + +
+
+
+
    + {#each Array(60) as _, i} +
  • + {/each} +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {#each Array(8) as _, i} +
+ {/each} +
+
+ +
+ + + {#if $jarvisState === "disconnected"} + Отключен + {:else if $jarvisState === "idle"} + Ожидание + {:else if $jarvisState === "listening"} + Слушаю + {:else if $jarvisState === "processing"} + Обработка + {/if} + +
+
+ + \ No newline at end of file diff --git a/frontend/src/css/main.scss b/frontend/src/css/main.scss index f00538f..6161ca7 100644 --- a/frontend/src/css/main.scss +++ b/frontend/src/css/main.scss @@ -1,3 +1,37 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: 'Segoe UI', 'Inter', -apple-system, sans-serif; + background-color: #0a1214; + color: #ffffff; + overflow: hidden; + + background-color: darken($color: #090C10, $amount: 0.8)!important; + // background-image: url(../media/bg.png)!important; + background-repeat: no-repeat; + background-size: contain; + // background-attachment: fixed; + background-attachment: none; + +} + +::-webkit-scrollbar { + width: 6px; +} + +::-webkit-scrollbar-track { + background: rgba(255, 255, 255, 0.05); +} + +::-webkit-scrollbar-thumb { + background: rgba(82, 254, 254, 0.3); + border-radius: 3px; +} + // ### FONTS $prim-font: "Roboto", sans-serif; $sec-font: "Roboto Condensed", sans-serif; @@ -46,10 +80,9 @@ body { justify-content: space-between; .logo { - margin-top: 12px; & > a > img { - width: 57px; + width: 55px; display: inline-block; transition: 0.5s opacity ease-in; @@ -60,8 +93,6 @@ body { & > div { display: inline-block; - margin-left: 13px; - margin-top: 8px; vertical-align: top; } @@ -95,6 +126,147 @@ body { } } +.header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 1rem 1.5rem; + background: transparent; + } + + .header-left { + display: flex; + align-items: center; + } + + .logo { + display: flex; + align-items: center; + gap: 0.85rem; + } + + .logo-image { + width: 54px; + height: 54px; + border-radius: 12px; + object-fit: cover; + } + + .logo-text { + display: flex; + flex-direction: column; + } + + .logo-title { + font-size: 1.5rem; + font-weight: 700; + color: #ffffff; + letter-spacing: -1px; + margin-top: 10px; + display: block; + + & > #jarvis-logo { + display: block; + width: 100px; + height: 20px; + background: red; + background: url(../media/jarvis.png); + background-size: 80px; + background-repeat: no-repeat; + transition: all 0.3s ease; + + &:hover { + background: url(../media/jarvis-hover.png); + background-size: 80px; + background-repeat: no-repeat; + border: none!important; + text-decoration: none; + } + } + } + + .logo-version { + font-size: 0.7rem; + color: #aaa; + text-transform: uppercase; + font-style: italic; + letter-spacing: 1px; + font-weight: 500; + letter-spacing: 1px; + + .v-badge { + color: #8AC832; + opacity: 0.9; + letter-spacing: 0; + font-weight: 600; + } + } + + .header-right { + display: flex; + align-items: center; + gap: 0.6rem; + } + + .header-btn { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.55rem 0.85rem; + background: transparent; + border: none; + border-radius: 6px; + color: #ffffff; + font-size: 0.8rem; + text-transform: uppercase; + letter-spacing: 1.2px; + cursor: pointer; + font-weight: 600; + font-family: "Trebuchet MS", Arial, Helvetica, sans-serif; + opacity: 0.85; + + &:hover { + opacity: 1; + background: rgba(35, 50, 55, 0.95); + border-color: rgba(255, 255, 255, 0.12); + } + } + + .btn-icon { + opacity: 0.75; + } + + .btn-badge { + padding: 0.25rem 0.3rem; + border-radius: 2px; + font-size: 0.7rem; + font-weight: 700; + color: #ffffff; + letter-spacing: 0; + text-shadow: 0px 0px 13.92px rgba(55, 55, 55, 0.54); + + &.purple { + background-image: -moz-linear-gradient( -84deg, rgb(153,41,234) 0%, rgb(88,8,251) 100%); + background-image: -webkit-linear-gradient( -84deg, rgb(153,41,234) 0%, rgb(88,8,251) 100%); + background-image: -ms-linear-gradient( -84deg, rgb(153,41,234) 0%, rgb(88,8,251) 100%); + box-shadow: 0px 12px 14.56px 1.44px rgba(55, 55, 55, 0.15); + } + + &.blue { + background: #3b82f6; + } + + &.gray { + background: #333; + } + } + + .beta-badge { + color: #8AC832; + opacity: 0.9; + font-size: 13px; + } + .top-menu { vertical-align: top; margin-top: 18px; @@ -165,33 +337,67 @@ body { } } +.h-divider { + position: relative; + z-index: 1; +} + // ### SEARCH BAR .search { display: block; - margin: 20px 0; + margin: 0; text-align: center; + position: relative; + z-index: 1; + + & > .h-divider { + margin: 10px 0!important; + background-position: left!important; + } + + & > #search-form { + margin: 0; + position: relative; + } & > form { position: relative; & > input { - width: 380px; + width: 97%; height: 38px; + margin: 0 auto; box-shadow: inset 0 0 5px 1px rgba(0, 0, 0, 0.6); border: 1px solid rgba(6, 6, 6, 0.99); - background-color: #0f1012; outline: none; color: #d1d1d1; font-family: $sec-font; font-size: 17px; - font-weight: 600; + font-weight: 400; line-height: 70.58px; - padding-left: 20px; + padding-left: 15px; padding-right: 45px; + border-style: solid; + border-width: 3px; + border-color: rgba(6, 6, 6, 0.5); + background-color: rgba(25, 26, 28, 0.7); + opacity: 0.99; + box-shadow: inset 0px 0px 18.8px 1.2px rgba(0, 0, 0, 0.6); + &::placeholder { color: #676767; - font-weight: 400; + font-weight: 300; + font-style: italic; + font-family: "Roboto Condensed", "Trebuchet MS", Arial, Helvetica, sans-serif; + opacity: 0.7; + + font-size: 16px; + font-family: "Roboto Condensed"; + color: rgb(103, 103, 103); + font-style: italic; + line-height: 4.437; + text-align: left; } &:focus + button + small { @@ -237,17 +443,19 @@ body { font-family: $sec-font; font-size: 14px; font-weight: bold; - line-height: 1.7em; - top: 8px; - right: 75px; + line-height: 1.5em; + top: 9px; + right: 15px; background-color: #d1d1d1; - color: #080c0f; + color: #161B1F; padding: 0 7px; - padding-top: 2px; - border-radius: 4px; + padding-top: 0; + border-radius: 2px; opacity: 0; transition: opacity 0.3s; cursor: default; + border: none; + text-transform: uppercase; } } @@ -258,6 +466,109 @@ body { } } +// ### MAIN + +.search-section { + padding: 0.5rem 1.5rem; +} + +.main-content { + flex: 1; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + position: relative; + padding: 1rem; +} + +.reactor-section { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 1rem; + position: relative; +} + +.reactor-wrapper { + transition: opacity 0.5s ease, filter 0.5s ease; + + &.dimmed { + opacity: 0.15; + filter: grayscale(0.5); + } +} + +.offline-badge { + display: flex; + align-items: center; + gap: 0.75rem; + position: absolute; + top: 52%; + left: 50%; + transform: translate(-50%, -50%); + margin-top: -3rem; + + & > small { + display: block; + position: absolute; + top: 27px; + text-align: center; + width: 100%; + opacity: 0.7; + } +} + +.offline-icon { + font-size: 1.3rem; + color: #fbbf24; +} + +.offline-text { + color: #fbbf24; + font-size: 0.9rem; + font-weight: 500; + text-transform: uppercase; + letter-spacing: 3px; + white-space: nowrap; +} + +.start-button { + background: transparent; + border: 0 solid rgba(82, 254, 254, 0.4); + color: #52fefe; + padding: 0.75rem 2.5rem; + font-size: 0.8rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 2px; + cursor: pointer; + transition: all 0.3s ease; + border-radius: 4px; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + margin-top: 2rem; + + background: rgba(82, 254, 254, 0.1); + border-color: rgba(82, 254, 254, 0.7); + box-shadow: 0 0 20px rgba(82, 254, 254, 0.15); + + + &:hover:not(:disabled) { + background: rgba(82, 254, 254, 0.2); + border-color: rgba(82, 254, 254, 0.9); + box-shadow: 0 0 20px rgba(82, 254, 254, 0.25); + } + + &:disabled { + opacity: 0.5; + cursor: not-allowed; + } +} + // ### RESPONSIVE @media (max-width: 1364px) { #content > .inner > section.materials > header > h1 { diff --git a/frontend/src/lib/i18n.ts b/frontend/src/lib/i18n.ts new file mode 100644 index 0000000..2ee9159 --- /dev/null +++ b/frontend/src/lib/i18n.ts @@ -0,0 +1,55 @@ +import { writable, derived } from "svelte/store" +import { invoke } from "@tauri-apps/api/core" + +// stores +export const translations = writable>({}) +export const currentLanguage = writable("ru") + +// simple helper function (not a store) +export function translate(translations: Record, key: string, fallback?: string): string { + return translations[key] || fallback || key +} + +// load translations from backend +export async function loadTranslations() { + try { + const [trans, lang] = await Promise.all([ + invoke>("get_translations"), + invoke("get_current_language") + ]) + translations.set(trans) + currentLanguage.set(lang) + } catch (err) { + console.error("Failed to load translations:", err) + } +} + +// change language +export async function setLanguage(lang: string) { + try { + const newTranslations = await invoke>("set_language", { lang }) + translations.set(newTranslations) + currentLanguage.set(lang) + } catch (err) { + console.error("Failed to set language:", err) + } +} + +export async function loadLanguage() { + try { + const lang = await invoke("db_read", { key: "language" }) + if (lang) { + currentLanguage.set(lang) + } + } catch (err) { + console.error("Failed to load language:", err) + } +} + +export async function getSupportedLanguages(): Promise { + try { + return await invoke("get_supported_languages") + } catch { + return ["ru", "en", "ua"] + } +} \ No newline at end of file diff --git a/frontend/src/routes/commands/index.svelte b/frontend/src/routes/commands/index.svelte index 29e27e0..567be23 100644 --- a/frontend/src/routes/commands/index.svelte +++ b/frontend/src/routes/commands/index.svelte @@ -4,7 +4,9 @@ import HDivider from "@/components/elements/HDivider.svelte" import Footer from "@/components/Footer.svelte" - import { appInfo } from "@/stores" + import { appInfo, translations, translate } from "@/stores" + + $: t = (key: string) => translate($translations, key) let tgLink = "" appInfo.subscribe(info => { @@ -15,13 +17,13 @@ - Тут будет список команд + полноценный редактор команд.
- Следите за обновлениями в нашем телеграм канале! + {t('commands-wip-desc')}
+ {t('commands-wip-follow')} {t('commands-wip-channel')}!
@@ -36,4 +38,4 @@ text-align: center; margin-top: 25px; } - + \ No newline at end of file diff --git a/frontend/src/routes/index.svelte b/frontend/src/routes/index.svelte index f902808..1f1d474 100644 --- a/frontend/src/routes/index.svelte +++ b/frontend/src/routes/index.svelte @@ -1,46 +1,41 @@ - +
-{#if !processRunning} - - В данный момент ассистент не запущен.
- Но вы всё еще можете изменять его настройки.
-
+ - -
-{:else} - +
+
+ +
+ + {#if !processRunning} +
+ + {t('assistant-not-running')} + {t('assistant-offline-hint')} +
+ + {/if} +
- {#if !$ipcConnected} - - Устанавливается связь с ассистентом... - - {/if} -{/if} - - - -
\ No newline at end of file + + +
+
\ No newline at end of file diff --git a/frontend/src/routes/settings/index.svelte b/frontend/src/routes/settings/index.svelte index f167da7..79d9583 100644 --- a/frontend/src/routes/settings/index.svelte +++ b/frontend/src/routes/settings/index.svelte @@ -5,7 +5,7 @@ import { setTimeout } from "worker-timers" import { showInExplorer } from "@/functions" - import { appInfo, assistantVoice } from "@/stores" + import { appInfo, assistantVoice, translations, translate } from "@/stores" import HDivider from "@/components/elements/HDivider.svelte" import Footer from "@/components/Footer.svelte" @@ -33,6 +33,8 @@ CrossCircled } from "radix-icons-svelte" + $: t = (key: string) => translate($translations, key) + // ### STATE interface MicrophoneOption { label: string @@ -164,13 +166,13 @@ - Часть функций может работать некорректно.
- Сообщайте обо всех найденных багах в наш телеграм бот. + {t('settings-beta-desc')}
+ {t('settings-beta-feedback')} {t('settings-beta-bot')}.
@@ -187,7 +189,7 @@ {#if settingsSaved} { settingsSaved = false }} @@ -196,8 +198,7 @@ {/if} - - + - - + - - + {#if selectedWakeWordEngine === "picovoice"} - + - Мы ждем официального патча от разработчиков. + {t('settings-picovoice-waiting')} - Введите сюда свой ключ Picovoice.
- Он выдается бесплатно при регистрации в + {t('settings-picovoice-key-desc')} Picovoice Console.
@@ -284,9 +282,9 @@ {#if availableVoskModels.length === 0} - + - Поместите модели Vosk в папку resources/vosk + {t('settings-models-hint')} {/if} @@ -297,8 +295,8 @@ { label: "Intent Classifier", value: "IntentClassifier" }, { label: "Rasa", value: "Rasa" } ]} - label="Распознавание команд (Intent Recognition)" - description="Выберите, какая нейросеть будет отвечать за распознавание команд." + label={t('settings-intent-engine')} + description={t('settings-intent-engine-desc')} variant="filled" bind:value={selectedIntentRecognitionEngine} /> @@ -307,11 +305,11 @@ @@ -320,40 +318,39 @@ - + - Автоматически регулирует уровень громкости. + {t('settings-gain-normalizer-desc')} - + - В данный момент ChatGPT не поддерживается. - Он будет добавлен в ближайших обновлениях. + {t('settings-openai-not-supported')} - Сохранить + {t('settings-save')} @@ -388,7 +385,7 @@ fullSize on:click={() => $goto("/")} > - Назад + {t('settings-back')} diff --git a/frontend/src/stores.ts b/frontend/src/stores.ts index 4df7d29..746d9b9 100644 --- a/frontend/src/stores.ts +++ b/frontend/src/stores.ts @@ -17,6 +17,17 @@ export { reloadCommands } from "./lib/ipc" +// re-export i18n +export { + translations, + currentLanguage, + translate, + loadTranslations, + setLanguage, + loadLanguage, + getSupportedLanguages +} from "./lib/i18n" + // ### RUNNING STATE export const isJarvisRunning = writable(false) export const jarvisRamUsage = writable(0) @@ -30,6 +41,8 @@ export const appInfo = writable({ tgOfficialLink: "", feedbackLink: "", repositoryLink: "", + boostySupportLink: "", + patreonSupportLink: "", logFilePath: "" }) @@ -45,10 +58,12 @@ export async function loadVoiceSetting() { export async function loadAppInfo() { try { - const [tg, feedback, repo, logPath] = await Promise.all([ + const [tg, feedback, repo, boosty, patreon, logPath] = await Promise.all([ invoke("get_tg_official_link"), invoke("get_feedback_link"), invoke("get_repository_link"), + invoke("get_boosty_link"), + invoke("get_patreon_link"), invoke("get_log_file_path") ]) @@ -56,6 +71,8 @@ export async function loadAppInfo() { tgOfficialLink: tg, feedbackLink: feedback, repositoryLink: repo, + boostySupportLink: boosty, + patreonSupportLink: patreon, logFilePath: logPath }) } catch (err) {