stored settings for language, and also overlay is now transcribed (#465)
This commit is contained in:
parent
57629f1374
commit
ffd38f4d89
18 changed files with 90 additions and 27 deletions
|
|
@ -261,6 +261,7 @@ pub fn run() {
|
|||
shortcut::resume_binding,
|
||||
shortcut::change_mute_while_recording_setting,
|
||||
shortcut::change_append_trailing_space_setting,
|
||||
shortcut::change_app_language_setting,
|
||||
shortcut::change_update_checks_setting,
|
||||
trigger_update_check,
|
||||
commands::cancel_operation,
|
||||
|
|
|
|||
|
|
@ -291,6 +291,8 @@ pub struct AppSettings {
|
|||
pub mute_while_recording: bool,
|
||||
#[serde(default)]
|
||||
pub append_trailing_space: bool,
|
||||
#[serde(default = "default_app_language")]
|
||||
pub app_language: String,
|
||||
}
|
||||
|
||||
fn default_model() -> String {
|
||||
|
|
@ -360,6 +362,10 @@ fn default_post_process_enabled() -> bool {
|
|||
false
|
||||
}
|
||||
|
||||
fn default_app_language() -> String {
|
||||
"en".to_string()
|
||||
}
|
||||
|
||||
fn default_post_process_provider_id() -> String {
|
||||
"openai".to_string()
|
||||
}
|
||||
|
|
@ -554,6 +560,7 @@ pub fn get_default_settings() -> AppSettings {
|
|||
post_process_selected_prompt_id: None,
|
||||
mute_while_recording: false,
|
||||
append_trailing_space: false,
|
||||
app_language: default_app_language(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -717,6 +717,16 @@ pub fn change_append_trailing_space_setting(app: AppHandle, enabled: bool) -> Re
|
|||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub fn change_app_language_setting(app: AppHandle, language: String) -> Result<(), String> {
|
||||
let mut settings = settings::get_settings(&app);
|
||||
settings.app_language = language;
|
||||
settings::write_settings(&app, settings);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Determine whether a shortcut string contains at least one non-modifier key.
|
||||
/// We allow single non-modifier keys (e.g. "f5" or "space") but disallow
|
||||
/// modifier-only combos (e.g. "ctrl" or "ctrl+shift").
|
||||
|
|
|
|||
|
|
@ -252,6 +252,14 @@ async changeAppendTrailingSpaceSetting(enabled: boolean) : Promise<Result<null,
|
|||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async changeAppLanguageSetting(language: string) : Promise<Result<null, string>> {
|
||||
try {
|
||||
return { status: "ok", data: await TAURI_INVOKE("change_app_language_setting", { language }) };
|
||||
} catch (e) {
|
||||
if(e instanceof Error) throw e;
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async changeUpdateChecksSetting(enabled: boolean) : Promise<Result<null, string>> {
|
||||
try {
|
||||
return { status: "ok", data: await TAURI_INVOKE("change_update_checks_setting", { enabled }) };
|
||||
|
|
@ -613,7 +621,7 @@ async isLaptop() : Promise<Result<boolean, string>> {
|
|||
|
||||
/** user-defined types **/
|
||||
|
||||
export type AppSettings = { bindings: Partial<{ [key in string]: ShortcutBinding }>; push_to_talk: boolean; audio_feedback: boolean; audio_feedback_volume?: number; sound_theme?: SoundTheme; start_hidden?: boolean; autostart_enabled?: boolean; update_checks_enabled?: boolean; selected_model?: string; always_on_microphone?: boolean; selected_microphone?: string | null; clamshell_microphone?: string | null; selected_output_device?: string | null; translate_to_english?: boolean; selected_language?: string; overlay_position?: OverlayPosition; debug_mode?: boolean; log_level?: LogLevel; custom_words?: string[]; model_unload_timeout?: ModelUnloadTimeout; word_correction_threshold?: number; history_limit?: string; recording_retention_period?: RecordingRetentionPeriod; paste_method?: PasteMethod; clipboard_handling?: ClipboardHandling; post_process_enabled?: boolean; post_process_provider_id?: string; post_process_providers?: PostProcessProvider[]; post_process_api_keys?: Partial<{ [key in string]: string }>; post_process_models?: Partial<{ [key in string]: string }>; post_process_prompts?: LLMPrompt[]; post_process_selected_prompt_id?: string | null; mute_while_recording?: boolean; append_trailing_space?: boolean }
|
||||
export type AppSettings = { bindings: Partial<{ [key in string]: ShortcutBinding }>; push_to_talk: boolean; audio_feedback: boolean; audio_feedback_volume?: number; sound_theme?: SoundTheme; start_hidden?: boolean; autostart_enabled?: boolean; update_checks_enabled?: boolean; selected_model?: string; always_on_microphone?: boolean; selected_microphone?: string | null; clamshell_microphone?: string | null; selected_output_device?: string | null; translate_to_english?: boolean; selected_language?: string; overlay_position?: OverlayPosition; debug_mode?: boolean; log_level?: LogLevel; custom_words?: string[]; model_unload_timeout?: ModelUnloadTimeout; word_correction_threshold?: number; history_limit?: string; recording_retention_period?: RecordingRetentionPeriod; paste_method?: PasteMethod; clipboard_handling?: ClipboardHandling; post_process_enabled?: boolean; post_process_provider_id?: string; post_process_providers?: PostProcessProvider[]; post_process_api_keys?: Partial<{ [key in string]: string }>; post_process_models?: Partial<{ [key in string]: string }>; post_process_prompts?: LLMPrompt[]; post_process_selected_prompt_id?: string | null; mute_while_recording?: boolean; append_trailing_space?: boolean; app_language?: string }
|
||||
export type AudioDevice = { index: string; name: string; is_default: boolean }
|
||||
export type BindingResponse = { success: boolean; binding: ShortcutBinding | null; error: string | null }
|
||||
export type ClipboardHandling = "dont_modify" | "copy_to_clipboard"
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { useTranslation } from "react-i18next";
|
|||
import { Dropdown } from "../ui/Dropdown";
|
||||
import { SettingContainer } from "../ui/SettingContainer";
|
||||
import { SUPPORTED_LANGUAGES, type SupportedLanguageCode } from "../../i18n";
|
||||
import { useSettings } from "@/hooks/useSettings";
|
||||
|
||||
interface AppLanguageSelectorProps {
|
||||
descriptionMode?: "inline" | "tooltip";
|
||||
|
|
@ -12,8 +13,10 @@ interface AppLanguageSelectorProps {
|
|||
export const AppLanguageSelector: React.FC<AppLanguageSelectorProps> =
|
||||
React.memo(({ descriptionMode = "tooltip", grouped = false }) => {
|
||||
const { t, i18n } = useTranslation();
|
||||
const { settings, updateSetting } = useSettings();
|
||||
|
||||
const currentLanguage = i18n.language as SupportedLanguageCode;
|
||||
const currentLanguage = (settings?.app_language ||
|
||||
i18n.language) as SupportedLanguageCode;
|
||||
|
||||
const languageOptions = SUPPORTED_LANGUAGES.map((lang) => ({
|
||||
value: lang.code,
|
||||
|
|
@ -22,8 +25,7 @@ export const AppLanguageSelector: React.FC<AppLanguageSelectorProps> =
|
|||
|
||||
const handleLanguageChange = (langCode: string) => {
|
||||
i18n.changeLanguage(langCode);
|
||||
// Persist to localStorage for next session
|
||||
localStorage.setItem("handy-app-language", langCode);
|
||||
updateSetting("app_language", langCode);
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import i18n from "i18next";
|
|||
import { initReactI18next } from "react-i18next";
|
||||
import { locale } from "@tauri-apps/plugin-os";
|
||||
import { LANGUAGE_METADATA } from "./languages";
|
||||
import { commands } from "@/bindings";
|
||||
|
||||
// Auto-discover translation files using Vite's glob import
|
||||
const localeModules = import.meta.glob<{ default: Record<string, unknown> }>(
|
||||
|
|
@ -55,17 +56,11 @@ const getSupportedLanguage = (
|
|||
return supported ? supported.code : null;
|
||||
};
|
||||
|
||||
// Get saved language from localStorage
|
||||
const getSavedLanguage = (): SupportedLanguageCode | null => {
|
||||
const savedLang = localStorage.getItem("handy-app-language");
|
||||
return getSupportedLanguage(savedLang);
|
||||
};
|
||||
|
||||
// Initialize i18n with saved language or English as default
|
||||
// System locale detection happens async after init
|
||||
// Initialize i18n with English as default
|
||||
// Language will be synced from settings after init
|
||||
i18n.use(initReactI18next).init({
|
||||
resources,
|
||||
lng: getSavedLanguage() || "en",
|
||||
lng: "en",
|
||||
fallbackLng: "en",
|
||||
interpolation: {
|
||||
escapeValue: false, // React already escapes values
|
||||
|
|
@ -75,23 +70,29 @@ i18n.use(initReactI18next).init({
|
|||
},
|
||||
});
|
||||
|
||||
// After init, check system locale if no saved preference
|
||||
const initSystemLocale = async () => {
|
||||
// Skip if user has explicitly set a language
|
||||
if (getSavedLanguage()) return;
|
||||
|
||||
// Sync language from app settings
|
||||
export const syncLanguageFromSettings = async () => {
|
||||
try {
|
||||
const systemLocale = await locale();
|
||||
const supported = getSupportedLanguage(systemLocale);
|
||||
if (supported && supported !== i18n.language) {
|
||||
await i18n.changeLanguage(supported);
|
||||
const result = await commands.getAppSettings();
|
||||
if (result.status === "ok" && result.data.app_language) {
|
||||
const supported = getSupportedLanguage(result.data.app_language);
|
||||
if (supported && supported !== i18n.language) {
|
||||
await i18n.changeLanguage(supported);
|
||||
}
|
||||
} else {
|
||||
// Fall back to system locale detection if no saved preference
|
||||
const systemLocale = await locale();
|
||||
const supported = getSupportedLanguage(systemLocale);
|
||||
if (supported && supported !== i18n.language) {
|
||||
await i18n.changeLanguage(supported);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("Failed to get system locale:", e);
|
||||
console.warn("Failed to sync language from settings:", e);
|
||||
}
|
||||
};
|
||||
|
||||
// Run async locale detection
|
||||
initSystemLocale();
|
||||
// Run language sync on init
|
||||
syncLanguageFromSettings();
|
||||
|
||||
export default i18n;
|
||||
|
|
|
|||
|
|
@ -406,5 +406,8 @@
|
|||
"appLanguage": {
|
||||
"title": "Anwendungssprache",
|
||||
"description": "Sprache der Handy-Oberfläche ändern"
|
||||
},
|
||||
"overlay": {
|
||||
"transcribing": "Transkribiere..."
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -406,5 +406,8 @@
|
|||
"appLanguage": {
|
||||
"title": "Application Language",
|
||||
"description": "Change the language of the Handy interface"
|
||||
},
|
||||
"overlay": {
|
||||
"transcribing": "Transcribing..."
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -399,5 +399,8 @@
|
|||
"appLanguage": {
|
||||
"title": "Idioma de la aplicación",
|
||||
"description": "Cambia el idioma de la interfaz de Handy"
|
||||
},
|
||||
"overlay": {
|
||||
"transcribing": "Transcribiendo..."
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -400,5 +400,8 @@
|
|||
"appLanguage": {
|
||||
"title": "Langue de l'application",
|
||||
"description": "Changer la langue de l'interface de Handy"
|
||||
},
|
||||
"overlay": {
|
||||
"transcribing": "Transcription..."
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -406,5 +406,8 @@
|
|||
"appLanguage": {
|
||||
"title": "Lingua Applicazione",
|
||||
"description": "Cambia la lingua dell'interfaccia di Handy"
|
||||
},
|
||||
"overlay": {
|
||||
"transcribing": "Trascrizione..."
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -406,5 +406,8 @@
|
|||
"appLanguage": {
|
||||
"title": "アプリケーション言語",
|
||||
"description": "Handyインターフェースの言語を変更"
|
||||
},
|
||||
"overlay": {
|
||||
"transcribing": "文字起こし中..."
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -406,5 +406,8 @@
|
|||
"appLanguage": {
|
||||
"title": "Język aplikacji",
|
||||
"description": "Zmień język interfejsu Handy"
|
||||
},
|
||||
"overlay": {
|
||||
"transcribing": "Transkrypcja..."
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -400,5 +400,8 @@
|
|||
"appLanguage": {
|
||||
"title": "Ngôn ngữ ứng dụng",
|
||||
"description": "Thay đổi ngôn ngữ giao diện của Handy"
|
||||
},
|
||||
"overlay": {
|
||||
"transcribing": "Đang chuyển đổi..."
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -406,5 +406,8 @@
|
|||
"appLanguage": {
|
||||
"title": "应用语言",
|
||||
"description": "更改 Handy 界面的语言"
|
||||
},
|
||||
"overlay": {
|
||||
"transcribing": "正在转录..."
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { listen } from "@tauri-apps/api/event";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
MicrophoneIcon,
|
||||
TranscriptionIcon,
|
||||
|
|
@ -7,10 +8,12 @@ import {
|
|||
} from "../components/icons";
|
||||
import "./RecordingOverlay.css";
|
||||
import { commands } from "@/bindings";
|
||||
import { syncLanguageFromSettings } from "@/i18n";
|
||||
|
||||
type OverlayState = "recording" | "transcribing";
|
||||
|
||||
const RecordingOverlay: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
const [state, setState] = useState<OverlayState>("recording");
|
||||
const [levels, setLevels] = useState<number[]>(Array(16).fill(0));
|
||||
|
|
@ -19,7 +22,9 @@ const RecordingOverlay: React.FC = () => {
|
|||
useEffect(() => {
|
||||
const setupEventListeners = async () => {
|
||||
// Listen for show-overlay event from Rust
|
||||
const unlistenShow = await listen("show-overlay", (event) => {
|
||||
const unlistenShow = await listen("show-overlay", async (event) => {
|
||||
// Sync language from settings each time overlay is shown
|
||||
await syncLanguageFromSettings();
|
||||
const overlayState = event.payload as OverlayState;
|
||||
setState(overlayState);
|
||||
setIsVisible(true);
|
||||
|
|
@ -84,7 +89,7 @@ const RecordingOverlay: React.FC = () => {
|
|||
</div>
|
||||
)}
|
||||
{state === "transcribing" && (
|
||||
<div className="transcribing-text">Transcribing...</div>
|
||||
<div className="transcribing-text">{t("overlay.transcribing")}</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import React from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import RecordingOverlay from "./RecordingOverlay";
|
||||
import "@/i18n";
|
||||
|
||||
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
|
||||
<React.StrictMode>
|
||||
|
|
|
|||
|
|
@ -124,6 +124,7 @@ const settingUpdaters: {
|
|||
append_trailing_space: (value) =>
|
||||
commands.changeAppendTrailingSpaceSetting(value as boolean),
|
||||
log_level: (value) => commands.setLogLevel(value as any),
|
||||
app_language: (value) => commands.changeAppLanguageSetting(value as string),
|
||||
};
|
||||
|
||||
export const useSettingsStore = create<SettingsStore>()(
|
||||
|
|
|
|||
Loading…
Reference in a new issue