add migration for transcription.language to general.language

This commit is contained in:
leonardotrapani
2026-02-01 13:44:12 +01:00
parent e46d8abe0d
commit 13b1de4e04
7 changed files with 241 additions and 14 deletions
+202
View File
@@ -2136,3 +2136,205 @@ func TestConfig_Validate_GeneralLanguage(t *testing.T) {
}
})
}
func TestConfig_MigrateLanguageToGeneral(t *testing.T) {
t.Run("old config with transcription.language migrates to general.language", func(t *testing.T) {
tempDir := t.TempDir()
configPath := filepath.Join(tempDir, "hyprvoice", "config.toml")
err := os.MkdirAll(filepath.Dir(configPath), 0755)
if err != nil {
t.Fatalf("Failed to create config directory: %v", err)
}
// Old config with language in transcription section
oldConfig := `[recording]
sample_rate = 16000
channels = 1
format = "s16"
buffer_size = 8192
channel_buffer_size = 30
timeout = "5m"
[transcription]
provider = "openai"
api_key = "test-key"
model = "whisper-1"
language = "es"
[injection]
backends = ["clipboard"]
ydotool_timeout = "5s"
wtype_timeout = "5s"
clipboard_timeout = "3s"
[notifications]
type = "log"`
err = os.WriteFile(configPath, []byte(oldConfig), 0644)
if err != nil {
t.Fatalf("Failed to create config file: %v", err)
}
originalConfigDir := os.Getenv("XDG_CONFIG_HOME")
os.Setenv("XDG_CONFIG_HOME", tempDir)
defer func() {
if originalConfigDir == "" {
os.Unsetenv("XDG_CONFIG_HOME")
} else {
os.Setenv("XDG_CONFIG_HOME", originalConfigDir)
}
}()
config, err := Load()
if err != nil {
t.Errorf("Load() error = %v", err)
return
}
// Should have migrated to general.language
if config.General.Language != "es" {
t.Errorf("Expected general.language='es' after migration, got %q", config.General.Language)
}
// Effective language should be 'es'
transcriberConfig := config.ToTranscriberConfig()
if transcriberConfig.Language != "es" {
t.Errorf("Expected effective language 'es', got %q", transcriberConfig.Language)
}
})
t.Run("migration does not run when general.language already set", func(t *testing.T) {
tempDir := t.TempDir()
configPath := filepath.Join(tempDir, "hyprvoice", "config.toml")
err := os.MkdirAll(filepath.Dir(configPath), 0755)
if err != nil {
t.Fatalf("Failed to create config directory: %v", err)
}
// Config with both general.language and transcription.language set
configContent := `[general]
language = "fr"
[recording]
sample_rate = 16000
channels = 1
format = "s16"
buffer_size = 8192
channel_buffer_size = 30
timeout = "5m"
[transcription]
provider = "openai"
api_key = "test-key"
model = "whisper-1"
language = "es"
[injection]
backends = ["clipboard"]
ydotool_timeout = "5s"
wtype_timeout = "5s"
clipboard_timeout = "3s"
[notifications]
type = "log"`
err = os.WriteFile(configPath, []byte(configContent), 0644)
if err != nil {
t.Fatalf("Failed to create config file: %v", err)
}
originalConfigDir := os.Getenv("XDG_CONFIG_HOME")
os.Setenv("XDG_CONFIG_HOME", tempDir)
defer func() {
if originalConfigDir == "" {
os.Unsetenv("XDG_CONFIG_HOME")
} else {
os.Setenv("XDG_CONFIG_HOME", originalConfigDir)
}
}()
config, err := Load()
if err != nil {
t.Errorf("Load() error = %v", err)
return
}
// general.language should remain 'fr', not overwritten by migration
if config.General.Language != "fr" {
t.Errorf("Expected general.language='fr' (not migrated), got %q", config.General.Language)
}
// transcription.language should still override
transcriberConfig := config.ToTranscriberConfig()
if transcriberConfig.Language != "es" {
t.Errorf("Expected effective language 'es' (transcription override), got %q", transcriberConfig.Language)
}
})
t.Run("original file not modified until explicit save", func(t *testing.T) {
tempDir := t.TempDir()
configPath := filepath.Join(tempDir, "hyprvoice", "config.toml")
err := os.MkdirAll(filepath.Dir(configPath), 0755)
if err != nil {
t.Fatalf("Failed to create config directory: %v", err)
}
oldConfig := `[recording]
sample_rate = 16000
channels = 1
format = "s16"
buffer_size = 8192
channel_buffer_size = 30
timeout = "5m"
[transcription]
provider = "openai"
api_key = "test-key"
model = "whisper-1"
language = "de"
[injection]
backends = ["clipboard"]
ydotool_timeout = "5s"
wtype_timeout = "5s"
clipboard_timeout = "3s"
[notifications]
type = "log"`
err = os.WriteFile(configPath, []byte(oldConfig), 0644)
if err != nil {
t.Fatalf("Failed to create config file: %v", err)
}
originalConfigDir := os.Getenv("XDG_CONFIG_HOME")
os.Setenv("XDG_CONFIG_HOME", tempDir)
defer func() {
if originalConfigDir == "" {
os.Unsetenv("XDG_CONFIG_HOME")
} else {
os.Setenv("XDG_CONFIG_HOME", originalConfigDir)
}
}()
_, err = Load()
if err != nil {
t.Errorf("Load() error = %v", err)
return
}
// Read the file again - should still have old format
content, err := os.ReadFile(configPath)
if err != nil {
t.Fatalf("Failed to read config file: %v", err)
}
// File should NOT have [general] section (migration is in-memory only)
if strings.Contains(string(content), "[general]") {
t.Error("Original file should not be modified by migration - [general] section found")
}
})
}
+9
View File
@@ -78,6 +78,7 @@ func Load() (*Config, error) {
config.applyLLMDefaults()
config.applyThreadsDefault()
config.migrateLanguageToGeneral()
log.Printf("Config: configuration loaded successfully")
return &config, nil
@@ -132,6 +133,14 @@ func (c *Config) applyLLMDefaults() {
}
}
// migrateLanguageToGeneral migrates old transcription.language to general.language
func (c *Config) migrateLanguageToGeneral() {
if c.Transcription.Language != "" && c.General.Language == "" {
c.General.Language = c.Transcription.Language
log.Printf("Config: migrated language setting to [general] section")
}
}
// migrateInjectionMode converts old mode field to new backends array
func (c *Config) migrateInjectionMode(mode string) {
switch mode {
+2 -8
View File
@@ -6,7 +6,6 @@ import (
"github.com/charmbracelet/huh"
"github.com/leonardotrapani/hyprvoice/internal/config"
"github.com/leonardotrapani/hyprvoice/internal/language"
)
// formatProvidersLabel formats the providers menu option
@@ -14,14 +13,9 @@ func formatProvidersLabel(cfg *config.Config) string {
return "Providers"
}
// formatLanguageMenuLabel formats the language menu option showing current setting
// formatLanguageMenuLabel formats the language menu option
func formatLanguageMenuLabel(cfg *config.Config) string {
langCode := cfg.General.Language
if langCode == "" {
return "Language (Auto-detect)"
}
lang := language.FromCode(langCode)
return fmt.Sprintf("Language (%s)", lang.Name)
return "Language"
}
// formatTranscriptionLabel formats the transcription menu option
+1 -1
View File
@@ -12,7 +12,7 @@ import (
// editLanguage allows the user to select the global transcription language
func editLanguage(cfg *config.Config) error {
// no model-specific warnings for global language selection
languageOptions := getLanguageOptions(nil)
languageOptions := getLanguageOptions(nil, cfg.General.Language)
selectedLanguage := cfg.General.Language
+13 -3
View File
@@ -10,16 +10,26 @@ import (
// getLanguageOptions returns language options for the dropdown
// if currentModel is provided, languages unsupported by that model will be marked
func getLanguageOptions(currentModel *provider.Model) []huh.Option[string] {
// currentLang is the currently selected language code (empty string for auto-detect)
func getLanguageOptions(currentModel *provider.Model, currentLang string) []huh.Option[string] {
var options []huh.Option[string]
// auto-detect is always first and recommended
options = append(options, huh.NewOption("Auto-detect (Recommended)", ""))
// auto-detect is always first
autoLabel := "Auto-detect"
if currentLang == "" {
autoLabel += " (current)"
}
options = append(options, huh.NewOption(autoLabel, ""))
// add all languages
for _, lang := range language.List() {
label := formatLanguageLabel(lang)
// mark current selection
if lang.Code == currentLang {
label += " (current)"
}
// add warning if model doesn't support this language
if currentModel != nil && !currentModel.SupportsLanguage(lang.Code) {
label += " (not supported by current model)"