add migration for transcription.language to general.language
This commit is contained in:
@@ -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")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user