From 13b1de4e044b4d1804f923ea64b410c6df34f1fb Mon Sep 17 00:00:00 2001 From: leonardotrapani Date: Sun, 1 Feb 2026 13:41:08 +0100 Subject: [PATCH] add migration for transcription.language to general.language --- internal/config/config_test.go | 202 +++++++++++++++++++++++++++++ internal/config/load.go | 9 ++ internal/tui/configure_helpers.go | 10 +- internal/tui/configure_language.go | 2 +- internal/tui/languages.go | 16 ++- progress.txt | 14 +- tasks/prd.jsonc | 2 +- 7 files changed, 241 insertions(+), 14 deletions(-) diff --git a/internal/config/config_test.go b/internal/config/config_test.go index e73712a..82ca45a 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -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") + } + }) +} diff --git a/internal/config/load.go b/internal/config/load.go index 0740a46..00e430e 100644 --- a/internal/config/load.go +++ b/internal/config/load.go @@ -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 { diff --git a/internal/tui/configure_helpers.go b/internal/tui/configure_helpers.go index cf10adb..16e68d7 100644 --- a/internal/tui/configure_helpers.go +++ b/internal/tui/configure_helpers.go @@ -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 diff --git a/internal/tui/configure_language.go b/internal/tui/configure_language.go index a9e40c9..5d910d2 100644 --- a/internal/tui/configure_language.go +++ b/internal/tui/configure_language.go @@ -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 diff --git a/internal/tui/languages.go b/internal/tui/languages.go index 7fdb069..5f59491 100644 --- a/internal/tui/languages.go +++ b/internal/tui/languages.go @@ -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)" diff --git a/progress.txt b/progress.txt index 47129ca..789ada8 100644 --- a/progress.txt +++ b/progress.txt @@ -624,4 +624,16 @@ Started: Sun Feb 1 12:22:47 AM CET 2026 - Updated all Example Configurations with `[general]` section - Added "Multilingual Setup with Specific Language" example - Added Language Migration section explaining the change from transcription.language -- Typecheck passes \ No newline at end of file +- 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 \ No newline at end of file diff --git a/tasks/prd.jsonc b/tasks/prd.jsonc index ec2e557..29fcc94 100644 --- a/tasks/prd.jsonc +++ b/tasks/prd.jsonc @@ -216,7 +216,7 @@ "Original file not modified until explicit save", "Typecheck passes" ], - "passes": false + "passes": true } ] }