Skip to content

Commit 3e14f2d

Browse files
feat: separate language settings into dedicated screen (#13)
* feat: separate language settings into dedicated screen * fix: consolidate locale labels into single translation source
1 parent a250ad8 commit 3e14f2d

7 files changed

Lines changed: 274 additions & 152 deletions

File tree

internal/infrastructure/i18n/constants.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ const (
8787

8888
SettingsLanguageEnKey = "settings.language.en"
8989
SettingsLanguageRuKey = "settings.language.ru"
90+
SettingsLanguageJaKey = "settings.language.ja"
9091

9192
SettingsConfirmSaveKey = "settings.confirm.save"
9293

internal/presentation/tui/model.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,10 @@ func (m *Model) navigate(to state.ScreenType, params interface{}) tea.Cmd {
118118
m.current = screens.NewStatsScreen(m.service, m.translator)
119119

120120
case state.ScreenSettings:
121-
m.current = screens.NewSettingsScreen(m.translator, m.settingsRepo)
121+
m.current = screens.NewSettingsScreen(m.translator)
122+
123+
case state.ScreenLanguageSettings:
124+
m.current = screens.NewLanguageSettingsScreen(m.translator, m.settingsRepo)
122125
}
123126

124127
return m.current.Init()
Lines changed: 234 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
1+
package screens
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"strings"
7+
"time"
8+
9+
tea "github.com/charmbracelet/bubbletea"
10+
"github.com/charmbracelet/lipgloss"
11+
12+
"github.com/ignavan39/mood-diary/internal/domain/entity"
13+
"github.com/ignavan39/mood-diary/internal/domain/repository"
14+
"github.com/ignavan39/mood-diary/internal/infrastructure/i18n"
15+
"github.com/ignavan39/mood-diary/internal/presentation/styles"
16+
"github.com/ignavan39/mood-diary/internal/presentation/tui/components"
17+
"github.com/ignavan39/mood-diary/internal/presentation/tui/constants"
18+
"github.com/ignavan39/mood-diary/internal/presentation/tui/state"
19+
)
20+
21+
type LanguageSettingsScreen struct {
22+
state.BaseState
23+
24+
translator i18n.Translator
25+
settingsRepo repository.SettingsRepository
26+
27+
cursor int
28+
locales []string
29+
currentLocale string
30+
saved bool
31+
}
32+
33+
func NewLanguageSettingsScreen(translator i18n.Translator, settingsRepo repository.SettingsRepository) *LanguageSettingsScreen {
34+
supported := i18n.SupportedLocales()
35+
locales := make([]string, len(supported))
36+
for i, l := range supported {
37+
locales[i] = string(l)
38+
}
39+
return &LanguageSettingsScreen{
40+
translator: translator,
41+
settingsRepo: settingsRepo,
42+
locales: locales,
43+
}
44+
}
45+
46+
func (s *LanguageSettingsScreen) t(key string, args ...interface{}) string {
47+
if s.translator == nil {
48+
return key
49+
}
50+
return s.translator.T(key, args...)
51+
}
52+
53+
func (s *LanguageSettingsScreen) Init() tea.Cmd {
54+
s.SetLoading(true)
55+
return s.loadSettings()
56+
}
57+
58+
func (s *LanguageSettingsScreen) Update(msg tea.Msg) (state.Screen, tea.Cmd) {
59+
switch msg := msg.(type) {
60+
case tea.WindowSizeMsg:
61+
s.SetSize(msg.Width, msg.Height)
62+
63+
case tea.KeyMsg:
64+
return s.handleKeyMsg(msg)
65+
66+
case languageSettingsLoadedMsg:
67+
s.currentLocale = msg.locale
68+
s.SetLoading(false)
69+
return s, nil
70+
71+
case languageSettingsSavedMsg:
72+
73+
s.saved = true
74+
s.SetLoading(false)
75+
76+
return s, tea.Tick(1200*time.Millisecond, func(t time.Time) tea.Msg {
77+
return state.NavigateMsg{To: state.ScreenSettings}
78+
})
79+
80+
case state.ErrorMsg:
81+
s.SetError(msg.Error)
82+
s.SetLoading(false)
83+
return s, nil
84+
}
85+
86+
return s, nil
87+
}
88+
89+
func (s *LanguageSettingsScreen) handleKeyMsg(msg tea.KeyMsg) (state.Screen, tea.Cmd) {
90+
switch msg.String() {
91+
case "up", "k":
92+
if s.cursor > 0 {
93+
s.cursor--
94+
}
95+
96+
case "down", "j":
97+
if s.cursor < len(s.locales)-1 {
98+
s.cursor++
99+
}
100+
101+
case "enter", " ":
102+
103+
selectedLocale := s.locales[s.cursor]
104+
if selectedLocale != s.currentLocale {
105+
s.SetLoading(true)
106+
return s, s.saveLocale(selectedLocale)
107+
}
108+
109+
case "esc", "q":
110+
return s, state.Navigate(state.ScreenSettings, nil)
111+
}
112+
113+
return s, nil
114+
}
115+
116+
type languageSettingsLoadedMsg struct {
117+
locale string
118+
}
119+
120+
type languageSettingsSavedMsg struct{}
121+
122+
func (s *LanguageSettingsScreen) loadSettings() tea.Cmd {
123+
return func() tea.Msg {
124+
ctx := context.Background()
125+
settings, err := s.settingsRepo.Get(ctx, entity.SettingsKeyLanguage)
126+
if err != nil {
127+
return state.ErrorMsg{Error: err}
128+
}
129+
130+
return languageSettingsLoadedMsg{
131+
locale: settings.Value,
132+
}
133+
}
134+
}
135+
136+
func (s *LanguageSettingsScreen) saveLocale(locale string) tea.Cmd {
137+
return func() tea.Msg {
138+
ctx := context.Background()
139+
140+
settings, err := s.settingsRepo.Get(ctx, entity.SettingsKeyLanguage)
141+
if err != nil {
142+
return state.ErrorMsg{Error: err}
143+
}
144+
145+
settings.Value = locale
146+
err = s.settingsRepo.Upsert(ctx, settings)
147+
if err != nil {
148+
return state.ErrorMsg{Error: err}
149+
}
150+
151+
newLocale := i18n.Locale(locale)
152+
_ = s.translator.SetLocale(newLocale)
153+
154+
s.currentLocale = locale
155+
156+
return languageSettingsSavedMsg{}
157+
}
158+
}
159+
160+
func (s *LanguageSettingsScreen) View() string {
161+
var b strings.Builder
162+
163+
header := styles.HeaderStyle.Render(s.t(i18n.SettingsOptionLanguageKey))
164+
b.WriteString(header)
165+
b.WriteString("\n\n")
166+
167+
if s.Error != nil {
168+
b.WriteString(styles.ErrorStyle.Render(s.t(i18n.CommonErrorPrefixKey) + s.Error.Error()))
169+
b.WriteString("\n\n")
170+
}
171+
172+
if s.saved {
173+
b.WriteString(styles.SuccessStyle.Render(s.t(i18n.SettingsSuccessEditKey)))
174+
b.WriteString("\n\n")
175+
b.WriteString(styles.HelpStyle.Render(s.t(i18n.CommonReturningKey)))
176+
return lipgloss.NewStyle().Padding(2, 4).Render(b.String())
177+
}
178+
179+
if s.Loading {
180+
loading := components.NewLoading(s.t(i18n.CommonLoaderMessageKey))
181+
b.WriteString(styles.InfoStyle.Render(s.t(i18n.CommonLoaderMessageKey)))
182+
b.WriteString("\n")
183+
b.WriteString(loading.View())
184+
return lipgloss.NewStyle().Padding(2, 4).Render(b.String())
185+
}
186+
187+
b.WriteString(s.renderLanguageSelection())
188+
b.WriteString("\n")
189+
190+
help := styles.HelpStyle.Render(s.t(i18n.HelpNavigationSettingsKey))
191+
b.WriteString(help)
192+
193+
return lipgloss.NewStyle().Padding(2, 4).Render(b.String())
194+
}
195+
196+
func (s *LanguageSettingsScreen) renderLanguageSelection() string {
197+
var b strings.Builder
198+
199+
b.WriteString(styles.SubtitleStyle.Render(s.t(i18n.SettingsOptionLanguageKey)))
200+
b.WriteString("\n\n")
201+
202+
for i, locale := range s.locales {
203+
label := s.getLocaleLabel(locale)
204+
205+
if i == s.cursor {
206+
b.WriteString(constants.ArrowRight + " ")
207+
b.WriteString(styles.SelectedListItemStyle.Render(fmt.Sprintf("%s %s", constants.FilledDot, label)))
208+
if locale == s.currentLocale {
209+
b.WriteString(constants.Checkmark)
210+
}
211+
} else {
212+
b.WriteString(" ")
213+
style := styles.ListItemStyle
214+
if locale == s.currentLocale {
215+
label = label + " " + constants.Checkmark
216+
}
217+
b.WriteString(style.Render(fmt.Sprintf("%s %s", constants.EmptyDot, label)))
218+
}
219+
b.WriteString("\n")
220+
}
221+
222+
b.WriteString("\n")
223+
224+
return b.String()
225+
}
226+
227+
func (s *LanguageSettingsScreen) getLocaleLabel(locale string) string {
228+
key := "settings.language." + locale
229+
name := s.t(key)
230+
if name == key {
231+
return locale
232+
}
233+
return fmt.Sprintf("[%s] - %s", strings.ToUpper(locale), name)
234+
}

0 commit comments

Comments
 (0)