feat: persist desktop language preference
This commit is contained in:
parent
e412cfc45a
commit
15f1f413d0
|
|
@ -1621,6 +1621,7 @@ func (a *App) GetAppSettings() map[string]interface{} {
|
|||
"currentVaultPath": cfg.CurrentVaultPath,
|
||||
"recentVaults": cfg.RecentVaults,
|
||||
"theme": cfg.Theme,
|
||||
"language": cfg.Language,
|
||||
"devMode": cfg.DevMode,
|
||||
"userPluginsDir": cfg.UserPluginsDir,
|
||||
"lastOpenedAt": cfg.LastOpenedAt,
|
||||
|
|
@ -1634,19 +1635,34 @@ func (a *App) UpdateAppSettings(patch map[string]interface{}) string {
|
|||
}
|
||||
|
||||
cfg := &appsettings.Config{}
|
||||
hasConfigPatch := false
|
||||
if v, ok := patch["theme"].(string); ok && v != "" {
|
||||
cfg.Theme = v
|
||||
hasConfigPatch = true
|
||||
}
|
||||
if v, ok := patch["devMode"].(bool); ok {
|
||||
cfg.DevMode = v
|
||||
hasConfigPatch = true
|
||||
}
|
||||
if v, ok := patch["userPluginsDir"].(string); ok && v != "" {
|
||||
cfg.UserPluginsDir = v
|
||||
hasConfigPatch = true
|
||||
}
|
||||
|
||||
if hasConfigPatch {
|
||||
if err := a.appSettings.Update(cfg); err != nil {
|
||||
return err.Error()
|
||||
}
|
||||
}
|
||||
if value, exists := patch["language"]; exists {
|
||||
language, ok := value.(string)
|
||||
if !ok {
|
||||
return "language must be a string"
|
||||
}
|
||||
if err := a.appSettings.UpdateLanguage(language); err != nil {
|
||||
return err.Error()
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1294,6 +1294,34 @@ func assertSyncState(t *testing.T, service *syncsvc.Service, wantDeviceID, wantS
|
|||
}
|
||||
}
|
||||
|
||||
func TestAppSettingsLanguageAPI(t *testing.T) {
|
||||
settings := appsettings.NewManager(filepath.Join(t.TempDir(), "config.json"))
|
||||
if err := settings.Load(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := settings.Update(&appsettings.Config{Theme: "light", DevMode: true}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
app := &App{appSettings: settings}
|
||||
|
||||
if got := app.GetAppSettings()["language"]; got != "system" {
|
||||
t.Fatalf("initial language = %#v, want system", got)
|
||||
}
|
||||
if errStr := app.UpdateAppSettings(map[string]interface{}{"language": "ru"}); errStr != "" {
|
||||
t.Fatalf("UpdateAppSettings language: %s", errStr)
|
||||
}
|
||||
got := app.GetAppSettings()
|
||||
if got["language"] != "ru" || got["theme"] != "light" || got["devMode"] != true {
|
||||
t.Fatalf("settings after language update = %#v", got)
|
||||
}
|
||||
if errStr := app.UpdateAppSettings(map[string]interface{}{"language": "de"}); errStr == "" {
|
||||
t.Fatal("UpdateAppSettings accepted unsupported language")
|
||||
}
|
||||
if got := app.GetAppSettings()["language"]; got != "ru" {
|
||||
t.Fatalf("language after rejected update = %#v, want ru", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetCurrentVaultInitializesWorkspaceWhenMissingAtStartup(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
vaultParent := filepath.Join(tmpDir, "vault-parent")
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ type Config struct {
|
|||
CurrentVaultPath string `json:"currentVaultPath"`
|
||||
RecentVaults []string `json:"recentVaults"`
|
||||
Theme string `json:"theme"`
|
||||
Language string `json:"language"`
|
||||
DevMode bool `json:"devMode"`
|
||||
UserPluginsDir string `json:"userPluginsDir"`
|
||||
Workbench WorkbenchPreferences `json:"workbench,omitempty"`
|
||||
|
|
@ -30,6 +31,21 @@ type Config struct {
|
|||
LastOpenedAt string `json:"lastOpenedAt"`
|
||||
}
|
||||
|
||||
const (
|
||||
LanguageSystem = "system"
|
||||
LanguageEnglish = "en"
|
||||
LanguageRussian = "ru"
|
||||
)
|
||||
|
||||
func validLanguage(language string) bool {
|
||||
switch language {
|
||||
case LanguageSystem, LanguageEnglish, LanguageRussian:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
type WorkbenchPreferences struct {
|
||||
DefaultTextEditorProvider string `json:"defaultTextEditorProvider,omitempty"`
|
||||
DefaultMarkdownEditorProvider string `json:"defaultMarkdownEditorProvider,omitempty"`
|
||||
|
|
@ -121,6 +137,9 @@ func (m *Manager) Load() error {
|
|||
if cfg.Theme == "" {
|
||||
cfg.Theme = "dark"
|
||||
}
|
||||
if !validLanguage(cfg.Language) {
|
||||
cfg.Language = LanguageSystem
|
||||
}
|
||||
if cfg.RecentVaults == nil {
|
||||
cfg.RecentVaults = []string{}
|
||||
}
|
||||
|
|
@ -179,6 +198,9 @@ func (m *Manager) Update(patch *Config) error {
|
|||
if patch.Theme != "" {
|
||||
m.config.Theme = patch.Theme
|
||||
}
|
||||
if patch.Language != "" && validLanguage(patch.Language) {
|
||||
m.config.Language = patch.Language
|
||||
}
|
||||
if patch.UserPluginsDir != "" {
|
||||
m.config.UserPluginsDir = patch.UserPluginsDir
|
||||
}
|
||||
|
|
@ -200,6 +222,23 @@ func (m *Manager) Update(patch *Config) error {
|
|||
return m.saveLocked()
|
||||
}
|
||||
|
||||
// UpdateLanguage validates and persists the installation-local UI language.
|
||||
func (m *Manager) UpdateLanguage(language string) error {
|
||||
language = strings.TrimSpace(language)
|
||||
if !validLanguage(language) {
|
||||
return fmt.Errorf("unsupported language %q", language)
|
||||
}
|
||||
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
if m.config == nil {
|
||||
m.config = defaultConfig()
|
||||
}
|
||||
m.config.Language = language
|
||||
m.config.LastOpenedAt = time.Now().UTC().Format(time.RFC3339)
|
||||
return m.saveLocked()
|
||||
}
|
||||
|
||||
// UpdateSync replaces sync settings without changing unrelated app settings.
|
||||
func (m *Manager) UpdateSync(syncSettings SyncSettings) error {
|
||||
m.mu.Lock()
|
||||
|
|
@ -287,6 +326,7 @@ func defaultConfig() *Config {
|
|||
CurrentVaultPath: "",
|
||||
RecentVaults: []string{},
|
||||
Theme: "dark",
|
||||
Language: LanguageSystem,
|
||||
DevMode: false,
|
||||
UserPluginsDir: filepath.Join(os.Getenv("HOME"), ".config", "verstak", "plugins"),
|
||||
Workbench: WorkbenchPreferences{},
|
||||
|
|
@ -303,6 +343,7 @@ func copyConfig(c *Config) *Config {
|
|||
CurrentVaultPath: c.CurrentVaultPath,
|
||||
RecentVaults: recent,
|
||||
Theme: c.Theme,
|
||||
Language: c.Language,
|
||||
DevMode: c.DevMode,
|
||||
UserPluginsDir: c.UserPluginsDir,
|
||||
Workbench: c.Workbench,
|
||||
|
|
|
|||
|
|
@ -25,6 +25,53 @@ func TestLoad_DefaultCreation(t *testing.T) {
|
|||
if cfg.CurrentVaultPath != "" {
|
||||
t.Errorf("CurrentVaultPath: got %q, want empty", cfg.CurrentVaultPath)
|
||||
}
|
||||
if cfg.Language != "system" {
|
||||
t.Errorf("Language: got %q, want %q", cfg.Language, "system")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoad_MissingLanguageDefaultsToSystem(t *testing.T) {
|
||||
path := filepath.Join(t.TempDir(), "config.json")
|
||||
if err := os.WriteFile(path, []byte(`{"schemaVersion":1,"theme":"dark","devMode":true}`), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
m := NewManager(path)
|
||||
if err := m.Load(); err != nil {
|
||||
t.Fatalf("Load: %v", err)
|
||||
}
|
||||
if got := m.Get().Language; got != "system" {
|
||||
t.Fatalf("Language = %q, want system", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateLanguagePersistsAndPreservesUnrelatedSettings(t *testing.T) {
|
||||
path := filepath.Join(t.TempDir(), "config.json")
|
||||
m := NewManager(path)
|
||||
if err := m.Load(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := m.Update(&Config{Theme: "light", DevMode: true}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := m.UpdateLanguage("ru"); err != nil {
|
||||
t.Fatalf("UpdateLanguage: %v", err)
|
||||
}
|
||||
|
||||
reloaded := NewManager(path)
|
||||
if err := reloaded.Load(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cfg := reloaded.Get()
|
||||
if cfg.Language != "ru" || cfg.Theme != "light" || !cfg.DevMode {
|
||||
t.Fatalf("settings after language update = %+v", cfg)
|
||||
}
|
||||
if err := reloaded.UpdateLanguage("de"); err == nil {
|
||||
t.Fatal("UpdateLanguage(de) succeeded, want validation error")
|
||||
}
|
||||
if got := reloaded.Get().Language; got != "ru" {
|
||||
t.Fatalf("Language after rejected update = %q, want ru", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoad_CorruptConfig(t *testing.T) {
|
||||
|
|
|
|||
Loading…
Reference in New Issue