diff --git a/internal/infrastructure/i18n/constants.go b/internal/infrastructure/i18n/constants.go index 78d6b99..26655e5 100644 --- a/internal/infrastructure/i18n/constants.go +++ b/internal/infrastructure/i18n/constants.go @@ -87,6 +87,7 @@ const ( SettingsLanguageEnKey = "settings.language.en" SettingsLanguageRuKey = "settings.language.ru" + SettingsLanguageJaKey = "settings.language.ja" SettingsConfirmSaveKey = "settings.confirm.save" diff --git a/internal/presentation/tui/model.go b/internal/presentation/tui/model.go index 2a33d9c..2584723 100644 --- a/internal/presentation/tui/model.go +++ b/internal/presentation/tui/model.go @@ -118,7 +118,10 @@ func (m *Model) navigate(to state.ScreenType, params interface{}) tea.Cmd { m.current = screens.NewStatsScreen(m.service, m.translator) case state.ScreenSettings: - m.current = screens.NewSettingsScreen(m.translator, m.settingsRepo) + m.current = screens.NewSettingsScreen(m.translator) + + case state.ScreenLanguageSettings: + m.current = screens.NewLanguageSettingsScreen(m.translator, m.settingsRepo) } return m.current.Init() diff --git a/internal/presentation/tui/screens/language_settings_screen.go b/internal/presentation/tui/screens/language_settings_screen.go new file mode 100644 index 0000000..893b26a --- /dev/null +++ b/internal/presentation/tui/screens/language_settings_screen.go @@ -0,0 +1,234 @@ +package screens + +import ( + "context" + "fmt" + "strings" + "time" + + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" + + "github.com/ignavan39/mood-diary/internal/domain/entity" + "github.com/ignavan39/mood-diary/internal/domain/repository" + "github.com/ignavan39/mood-diary/internal/infrastructure/i18n" + "github.com/ignavan39/mood-diary/internal/presentation/styles" + "github.com/ignavan39/mood-diary/internal/presentation/tui/components" + "github.com/ignavan39/mood-diary/internal/presentation/tui/constants" + "github.com/ignavan39/mood-diary/internal/presentation/tui/state" +) + +type LanguageSettingsScreen struct { + state.BaseState + + translator i18n.Translator + settingsRepo repository.SettingsRepository + + cursor int + locales []string + currentLocale string + saved bool +} + +func NewLanguageSettingsScreen(translator i18n.Translator, settingsRepo repository.SettingsRepository) *LanguageSettingsScreen { + supported := i18n.SupportedLocales() + locales := make([]string, len(supported)) + for i, l := range supported { + locales[i] = string(l) + } + return &LanguageSettingsScreen{ + translator: translator, + settingsRepo: settingsRepo, + locales: locales, + } +} + +func (s *LanguageSettingsScreen) t(key string, args ...interface{}) string { + if s.translator == nil { + return key + } + return s.translator.T(key, args...) +} + +func (s *LanguageSettingsScreen) Init() tea.Cmd { + s.SetLoading(true) + return s.loadSettings() +} + +func (s *LanguageSettingsScreen) Update(msg tea.Msg) (state.Screen, tea.Cmd) { + switch msg := msg.(type) { + case tea.WindowSizeMsg: + s.SetSize(msg.Width, msg.Height) + + case tea.KeyMsg: + return s.handleKeyMsg(msg) + + case languageSettingsLoadedMsg: + s.currentLocale = msg.locale + s.SetLoading(false) + return s, nil + + case languageSettingsSavedMsg: + + s.saved = true + s.SetLoading(false) + + return s, tea.Tick(1200*time.Millisecond, func(t time.Time) tea.Msg { + return state.NavigateMsg{To: state.ScreenSettings} + }) + + case state.ErrorMsg: + s.SetError(msg.Error) + s.SetLoading(false) + return s, nil + } + + return s, nil +} + +func (s *LanguageSettingsScreen) handleKeyMsg(msg tea.KeyMsg) (state.Screen, tea.Cmd) { + switch msg.String() { + case "up", "k": + if s.cursor > 0 { + s.cursor-- + } + + case "down", "j": + if s.cursor < len(s.locales)-1 { + s.cursor++ + } + + case "enter", " ": + + selectedLocale := s.locales[s.cursor] + if selectedLocale != s.currentLocale { + s.SetLoading(true) + return s, s.saveLocale(selectedLocale) + } + + case "esc", "q": + return s, state.Navigate(state.ScreenSettings, nil) + } + + return s, nil +} + +type languageSettingsLoadedMsg struct { + locale string +} + +type languageSettingsSavedMsg struct{} + +func (s *LanguageSettingsScreen) loadSettings() tea.Cmd { + return func() tea.Msg { + ctx := context.Background() + settings, err := s.settingsRepo.Get(ctx, entity.SettingsKeyLanguage) + if err != nil { + return state.ErrorMsg{Error: err} + } + + return languageSettingsLoadedMsg{ + locale: settings.Value, + } + } +} + +func (s *LanguageSettingsScreen) saveLocale(locale string) tea.Cmd { + return func() tea.Msg { + ctx := context.Background() + + settings, err := s.settingsRepo.Get(ctx, entity.SettingsKeyLanguage) + if err != nil { + return state.ErrorMsg{Error: err} + } + + settings.Value = locale + err = s.settingsRepo.Upsert(ctx, settings) + if err != nil { + return state.ErrorMsg{Error: err} + } + + newLocale := i18n.Locale(locale) + _ = s.translator.SetLocale(newLocale) + + s.currentLocale = locale + + return languageSettingsSavedMsg{} + } +} + +func (s *LanguageSettingsScreen) View() string { + var b strings.Builder + + header := styles.HeaderStyle.Render(s.t(i18n.SettingsOptionLanguageKey)) + b.WriteString(header) + b.WriteString("\n\n") + + if s.Error != nil { + b.WriteString(styles.ErrorStyle.Render(s.t(i18n.CommonErrorPrefixKey) + s.Error.Error())) + b.WriteString("\n\n") + } + + if s.saved { + b.WriteString(styles.SuccessStyle.Render(s.t(i18n.SettingsSuccessEditKey))) + b.WriteString("\n\n") + b.WriteString(styles.HelpStyle.Render(s.t(i18n.CommonReturningKey))) + return lipgloss.NewStyle().Padding(2, 4).Render(b.String()) + } + + if s.Loading { + loading := components.NewLoading(s.t(i18n.CommonLoaderMessageKey)) + b.WriteString(styles.InfoStyle.Render(s.t(i18n.CommonLoaderMessageKey))) + b.WriteString("\n") + b.WriteString(loading.View()) + return lipgloss.NewStyle().Padding(2, 4).Render(b.String()) + } + + b.WriteString(s.renderLanguageSelection()) + b.WriteString("\n") + + help := styles.HelpStyle.Render(s.t(i18n.HelpNavigationSettingsKey)) + b.WriteString(help) + + return lipgloss.NewStyle().Padding(2, 4).Render(b.String()) +} + +func (s *LanguageSettingsScreen) renderLanguageSelection() string { + var b strings.Builder + + b.WriteString(styles.SubtitleStyle.Render(s.t(i18n.SettingsOptionLanguageKey))) + b.WriteString("\n\n") + + for i, locale := range s.locales { + label := s.getLocaleLabel(locale) + + if i == s.cursor { + b.WriteString(constants.ArrowRight + " ") + b.WriteString(styles.SelectedListItemStyle.Render(fmt.Sprintf("%s %s", constants.FilledDot, label))) + if locale == s.currentLocale { + b.WriteString(constants.Checkmark) + } + } else { + b.WriteString(" ") + style := styles.ListItemStyle + if locale == s.currentLocale { + label = label + " " + constants.Checkmark + } + b.WriteString(style.Render(fmt.Sprintf("%s %s", constants.EmptyDot, label))) + } + b.WriteString("\n") + } + + b.WriteString("\n") + + return b.String() +} + +func (s *LanguageSettingsScreen) getLocaleLabel(locale string) string { + key := "settings.language." + locale + name := s.t(key) + if name == key { + return locale + } + return fmt.Sprintf("[%s] - %s", strings.ToUpper(locale), name) +} diff --git a/internal/presentation/tui/screens/settings_screen.go b/internal/presentation/tui/screens/settings_screen.go index 33e0eae..6d0b32b 100644 --- a/internal/presentation/tui/screens/settings_screen.go +++ b/internal/presentation/tui/screens/settings_screen.go @@ -1,41 +1,42 @@ package screens import ( - "context" "fmt" "strings" - "time" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" - "github.com/ignavan39/mood-diary/internal/domain/entity" - "github.com/ignavan39/mood-diary/internal/domain/repository" "github.com/ignavan39/mood-diary/internal/infrastructure/i18n" "github.com/ignavan39/mood-diary/internal/presentation/styles" - "github.com/ignavan39/mood-diary/internal/presentation/tui/components" "github.com/ignavan39/mood-diary/internal/presentation/tui/constants" "github.com/ignavan39/mood-diary/internal/presentation/tui/state" ) +type settingsChoice struct { + label string + screen state.ScreenType +} + type SettingsScreen struct { state.BaseState - translator i18n.Translator - settingsRepo repository.SettingsRepository - - cursor int - locales []string - currentLocale string - saved bool + translator i18n.Translator + choices []settingsChoice + cursor int } -func NewSettingsScreen(translator i18n.Translator, settingsRepo repository.SettingsRepository) *SettingsScreen { - return &SettingsScreen{ - translator: translator, - settingsRepo: settingsRepo, - locales: []string{"en", "ru", "ja"}, +func NewSettingsScreen(translator i18n.Translator) *SettingsScreen { + s := &SettingsScreen{ + translator: translator, + } + s.choices = []settingsChoice{ + { + label: s.t(i18n.SettingsOptionLanguageKey), + screen: state.ScreenLanguageSettings, + }, } + return s } func (s *SettingsScreen) t(key string, args ...interface{}) string { @@ -46,8 +47,7 @@ func (s *SettingsScreen) t(key string, args ...interface{}) string { } func (s *SettingsScreen) Init() tea.Cmd { - s.SetLoading(true) - return s.loadSettings() + return nil } func (s *SettingsScreen) Update(msg tea.Msg) (state.Screen, tea.Cmd) { @@ -57,25 +57,6 @@ func (s *SettingsScreen) Update(msg tea.Msg) (state.Screen, tea.Cmd) { case tea.KeyMsg: return s.handleKeyMsg(msg) - - case settingsLoadedMsg: - s.currentLocale = msg.locale - s.SetLoading(false) - return s, nil - - case settingsSavedMsg: - - s.saved = true - s.SetLoading(false) - - return s, tea.Tick(1200*time.Millisecond, func(t time.Time) tea.Msg { - return state.NavigateMsg{To: state.ScreenMenu} - }) - - case state.ErrorMsg: - s.SetError(msg.Error) - s.SetLoading(false) - return s, nil } return s, nil @@ -89,17 +70,12 @@ func (s *SettingsScreen) handleKeyMsg(msg tea.KeyMsg) (state.Screen, tea.Cmd) { } case "down", "j": - if s.cursor < len(s.locales)-1 { + if s.cursor < len(s.choices)-1 { s.cursor++ } case "enter", " ": - - selectedLocale := s.locales[s.cursor] - if selectedLocale != s.currentLocale { - s.SetLoading(true) - return s, s.saveLocale(selectedLocale) - } + return s, state.Navigate(s.choices[s.cursor].screen, nil) case "esc", "q": return s, state.Navigate(state.ScreenMenu, nil) @@ -108,50 +84,6 @@ func (s *SettingsScreen) handleKeyMsg(msg tea.KeyMsg) (state.Screen, tea.Cmd) { return s, nil } -type settingsLoadedMsg struct { - locale string -} - -type settingsSavedMsg struct{} - -func (s *SettingsScreen) loadSettings() tea.Cmd { - return func() tea.Msg { - ctx := context.Background() - settings, err := s.settingsRepo.Get(ctx, entity.SettingsKeyLanguage) - if err != nil { - return state.ErrorMsg{Error: err} - } - - return settingsLoadedMsg{ - locale: settings.Value, - } - } -} - -func (s *SettingsScreen) saveLocale(locale string) tea.Cmd { - return func() tea.Msg { - ctx := context.Background() - - settings, err := s.settingsRepo.Get(ctx, entity.SettingsKeyLanguage) - if err != nil { - return state.ErrorMsg{Error: err} - } - - settings.Value = locale - err = s.settingsRepo.Upsert(ctx, settings) - if err != nil { - return state.ErrorMsg{Error: err} - } - - newLocale := i18n.Locale(locale) - _ = s.translator.SetLocale(newLocale) - - s.currentLocale = locale - - return settingsSavedMsg{} - } -} - func (s *SettingsScreen) View() string { var b strings.Builder @@ -159,75 +91,20 @@ func (s *SettingsScreen) View() string { b.WriteString(header) b.WriteString("\n\n") - if s.Error != nil { - b.WriteString(styles.ErrorStyle.Render(s.t(i18n.CommonErrorPrefixKey) + s.Error.Error())) - b.WriteString("\n\n") - } - - if s.saved { - b.WriteString(styles.SuccessStyle.Render(s.t(i18n.SettingsSuccessEditKey))) - b.WriteString("\n\n") - b.WriteString(styles.HelpStyle.Render(s.t(i18n.CommonReturningKey))) - return lipgloss.NewStyle().Padding(2, 4).Render(b.String()) - } - - if s.Loading { - loading := components.NewLoading(s.t(i18n.CommonLoaderMessageKey)) - b.WriteString(styles.InfoStyle.Render(s.t(i18n.CommonLoaderMessageKey))) - b.WriteString("\n") - b.WriteString(loading.View()) - return lipgloss.NewStyle().Padding(2, 4).Render(b.String()) - } - - b.WriteString(s.renderLanguageSelection()) - b.WriteString("\n") - - help := styles.HelpStyle.Render(s.t(i18n.HelpNavigationSettingsKey)) - b.WriteString(help) - - return lipgloss.NewStyle().Padding(2, 4).Render(b.String()) -} - -func (s *SettingsScreen) renderLanguageSelection() string { - var b strings.Builder - - b.WriteString(styles.SubtitleStyle.Render(s.t(i18n.SettingsOptionLanguageKey))) - b.WriteString("\n\n") - - for i, locale := range s.locales { - label := s.getLocaleLabel(locale) - + for i, choice := range s.choices { if i == s.cursor { - b.WriteString(constants.ArrowRight + " ") - b.WriteString(styles.SelectedListItemStyle.Render(fmt.Sprintf("%s %s", constants.FilledDot, label))) - if locale == s.currentLocale { - b.WriteString(constants.Checkmark) - } + b.WriteString(fmt.Sprintf("%s ", constants.ArrowRight)) + b.WriteString(styles.SelectedListItemStyle.Render(choice.label)) } else { b.WriteString(" ") - style := styles.ListItemStyle - if locale == s.currentLocale { - label = label + " " + constants.Checkmark - } - b.WriteString(style.Render(fmt.Sprintf("%s %s", constants.EmptyDot, label))) + b.WriteString(styles.ListItemStyle.Render(choice.label)) } b.WriteString("\n") } b.WriteString("\n") + help := styles.HelpStyle.Render(s.t(i18n.HelpNavigationSettingsKey)) + b.WriteString(help) - return b.String() -} - -func (s *SettingsScreen) getLocaleLabel(locale string) string { - labels := map[string]string{ - "en": "[En] - English", - "ru": "[Ru] - Русский", - "ja": "[Ja] - 日本語", - } - - if label, ok := labels[locale]; ok { - return label - } - return locale + return lipgloss.NewStyle().Padding(2, 4).Render(b.String()) } diff --git a/internal/presentation/tui/state/messages.go b/internal/presentation/tui/state/messages.go index d3ec90d..abeed79 100644 --- a/internal/presentation/tui/state/messages.go +++ b/internal/presentation/tui/state/messages.go @@ -17,6 +17,7 @@ const ( ScreenHistory ScreenStats ScreenSettings + ScreenLanguageSettings ) type NavigateMsg struct { @@ -121,3 +122,7 @@ func NavigateToStats(period string) tea.Cmd { func NavigateToSettings() tea.Cmd { return Navigate(ScreenSettings, nil) } + +func NavigateToLanguageSettings() tea.Cmd { + return Navigate(ScreenLanguageSettings, nil) +} diff --git a/locales/en.toml b/locales/en.toml index 64a13e3..32b15f4 100644 --- a/locales/en.toml +++ b/locales/en.toml @@ -94,6 +94,7 @@ success_edit = "✓ Language changed successfully!" [settings.language] en = "English" ru = "Russian" +ja = "Japanese" [settings.confirm] save = "Settings saved!" diff --git a/locales/ru.toml b/locales/ru.toml index 4f83c3d..3981898 100644 --- a/locales/ru.toml +++ b/locales/ru.toml @@ -91,6 +91,7 @@ success_edit = "✓ Язык успешно обновлен!" [settings.language] en = "Английский" ru = "Русский" +ja = "Японский" [mood.level] 0 = "Ужасно"