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.applyLLMDefaults()
config.applyThreadsDefault() config.applyThreadsDefault()
config.migrateLanguageToGeneral()
log.Printf("Config: configuration loaded successfully") log.Printf("Config: configuration loaded successfully")
return &config, nil 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 // migrateInjectionMode converts old mode field to new backends array
func (c *Config) migrateInjectionMode(mode string) { func (c *Config) migrateInjectionMode(mode string) {
switch mode { switch mode {
+2 -8
View File
@@ -6,7 +6,6 @@ import (
"github.com/charmbracelet/huh" "github.com/charmbracelet/huh"
"github.com/leonardotrapani/hyprvoice/internal/config" "github.com/leonardotrapani/hyprvoice/internal/config"
"github.com/leonardotrapani/hyprvoice/internal/language"
) )
// formatProvidersLabel formats the providers menu option // formatProvidersLabel formats the providers menu option
@@ -14,14 +13,9 @@ func formatProvidersLabel(cfg *config.Config) string {
return "Providers" return "Providers"
} }
// formatLanguageMenuLabel formats the language menu option showing current setting // formatLanguageMenuLabel formats the language menu option
func formatLanguageMenuLabel(cfg *config.Config) string { func formatLanguageMenuLabel(cfg *config.Config) string {
langCode := cfg.General.Language return "Language"
if langCode == "" {
return "Language (Auto-detect)"
}
lang := language.FromCode(langCode)
return fmt.Sprintf("Language (%s)", lang.Name)
} }
// formatTranscriptionLabel formats the transcription menu option // 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 // editLanguage allows the user to select the global transcription language
func editLanguage(cfg *config.Config) error { func editLanguage(cfg *config.Config) error {
// no model-specific warnings for global language selection // no model-specific warnings for global language selection
languageOptions := getLanguageOptions(nil) languageOptions := getLanguageOptions(nil, cfg.General.Language)
selectedLanguage := cfg.General.Language selectedLanguage := cfg.General.Language
+13 -3
View File
@@ -10,16 +10,26 @@ import (
// getLanguageOptions returns language options for the dropdown // getLanguageOptions returns language options for the dropdown
// if currentModel is provided, languages unsupported by that model will be marked // 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] var options []huh.Option[string]
// auto-detect is always first and recommended // auto-detect is always first
options = append(options, huh.NewOption("Auto-detect (Recommended)", "")) autoLabel := "Auto-detect"
if currentLang == "" {
autoLabel += " (current)"
}
options = append(options, huh.NewOption(autoLabel, ""))
// add all languages // add all languages
for _, lang := range language.List() { for _, lang := range language.List() {
label := formatLanguageLabel(lang) label := formatLanguageLabel(lang)
// mark current selection
if lang.Code == currentLang {
label += " (current)"
}
// add warning if model doesn't support this language // add warning if model doesn't support this language
if currentModel != nil && !currentModel.SupportsLanguage(lang.Code) { if currentModel != nil && !currentModel.SupportsLanguage(lang.Code) {
label += " (not supported by current model)" label += " (not supported by current model)"
+13 -1
View File
@@ -624,4 +624,16 @@ Started: Sun Feb 1 12:22:47 AM CET 2026
- Updated all Example Configurations with `[general]` section - Updated all Example Configurations with `[general]` section
- Added "Multilingual Setup with Specific Language" example - Added "Multilingual Setup with Specific Language" example
- Added Language Migration section explaining the change from transcription.language - Added Language Migration section explaining the change from transcription.language
- Typecheck passes - Typecheck passes
### Task 13: Add migration for existing configs
- Added `migrateLanguageToGeneral()` method to Config in internal/config/load.go
- Logic: if transcription.language is set but general.language is empty, copies to general.language
- Logs "Config: migrated language setting to [general] section" when migration occurs
- Called in Load() after applyThreadsDefault()
- Migration is in-memory only - original file not modified until explicit save
- Added 3 comprehensive tests:
- old config with transcription.language='es' migrates to general.language='es'
- migration does not run when general.language already set
- original file not modified until explicit save
- All tests passing, typecheck passes
+1 -1
View File
@@ -216,7 +216,7 @@
"Original file not modified until explicit save", "Original file not modified until explicit save",
"Typecheck passes" "Typecheck passes"
], ],
"passes": false "passes": true
} }
] ]
} }