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