diff --git a/.env.example b/.env.example index de9f8a3..a2bf9c5 100644 --- a/.env.example +++ b/.env.example @@ -1,7 +1,9 @@ BOT_TOKEN=YOUR_TELEGRAM_BOT_TOKEN HTTP_PROXY= +HTTPS_PROXY= DB_PATH=db.sqlite3 BOT_WEBHOOK_URL= BOT_LISTEN=:8080 BOT_TLS_CERT= BOT_TLS_KEY= +BOT_ALLOWED_USERS= diff --git a/Dockerfile b/Dockerfile index 9572d41..610d4e6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -20,7 +20,7 @@ WORKDIR /data COPY --from=builder /usr/local/bin/worktime-bot /usr/local/bin/worktime-bot COPY --from=builder /src/db/schema.sql ./db/schema.sql -COPY --from=builder /src/pkg/i18n/ ./i18n/ +# i18n removed — strings are hardcoded in handlers VOLUME ["/data"] diff --git a/cmd/bot/main.go b/cmd/bot/main.go index 1a57fd7..eb89197 100644 --- a/cmd/bot/main.go +++ b/cmd/bot/main.go @@ -4,23 +4,23 @@ import ( "log" "net/http" "os" + "strconv" + "strings" + "time" tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" "github.com/joho/godotenv" "worktimeBot/internal/bot" "worktimeBot/internal/db" - "worktimeBot/pkg/i18n" ) func main() { - // Load .env file if it exists (ignored when running inside Docker with env vars) - _ = godotenv.Load() + godotenv.Load() token := os.Getenv("BOT_TOKEN") dbPath := getEnvDefault("DB_PATH", "db.sqlite3") - // Default HTTP client respects HTTP_PROXY env var automatically httpClient := http.DefaultClient botAPI, err := tgbotapi.NewBotAPIWithClient(token, tgbotapi.APIEndpoint, httpClient) @@ -33,21 +33,21 @@ func main() { log.Fatal(err) } - translator, err := i18n.NewTranslator("en") - if err != nil { - log.Fatal("i18n: ", err) - } + allowedUsers := parseAllowedUsers(os.Getenv("BOT_ALLOWED_USERS")) - handler := bot.NewHandler(botAPI, dbStore, translator) + handler := bot.NewHandler(botAPI, dbStore, allowedUsers) webhookURL := os.Getenv("BOT_WEBHOOK_URL") if webhookURL != "" { startWebhook(botAPI, handler, webhookURL) } else { - // Fallback to long‑polling — remove any stale webhook first if _, err := botAPI.Request(tgbotapi.DeleteWebhookConfig{}); err != nil { log.Printf("DeleteWebhook: %v", err) } + + // Daily report scheduler + go dailyReportScheduler(handler, dbStore) + u := tgbotapi.NewUpdate(0) u.Timeout = 30 updates := botAPI.GetUpdatesChan(u) @@ -62,6 +62,42 @@ func main() { } } +func parseAllowedUsers(raw string) map[int64]bool { + m := make(map[int64]bool) + for _, s := range strings.Split(raw, ",") { + s = strings.TrimSpace(s) + if s == "" { + continue + } + id, err := strconv.ParseInt(s, 10, 64) + if err == nil { + m[id] = true + } + } + return m +} + +func dailyReportScheduler(h *bot.Handler, store *db.Store) { + for { + now := time.Now() + next := time.Date(now.Year(), now.Month(), now.Day(), 23, 0, 0, 0, now.Location()) + if now.After(next) { + next = next.AddDate(0, 0, 1) + } + time.Sleep(time.Until(next)) + + chatIDStr, err := store.GetSetting("chat_id") + if err != nil { + continue + } + chatID, err := strconv.ParseInt(chatIDStr, 10, 64) + if err != nil { + continue + } + h.SendDailyReport(chatID) + } +} + func getEnvDefault(key, defaultValue string) string { if val := os.Getenv(key); val != "" { return val diff --git a/internal/bot/export.go b/internal/bot/export.go index 8777fcb..4502f7d 100644 --- a/internal/bot/export.go +++ b/internal/bot/export.go @@ -9,62 +9,112 @@ import ( "worktimeBot/internal/db" ) -// GenerateMonthlyReport creates an Excel spreadsheet for the given month. func GenerateMonthlyReport(store *db.Store, year int, month time.Month) ([]byte, error) { f := excelize.NewFile() - defer func() { _ = f.Close() }() + defer f.Close() sheet := "Report" - // Create a new sheet index, err := f.NewSheet(sheet) if err != nil { return nil, err } f.SetActiveSheet(index) + f.DeleteSheet("Sheet1") - // Header row - headers := []string{"Date", "Work Duration (h)", "Pairs"} + titleStyle, _ := f.NewStyle(&excelize.Style{ + Font: &excelize.Font{Bold: true, Size: 14, Color: "FFFFFF"}, + Fill: excelize.Fill{Type: "pattern", Pattern: 1, Color: []string{"4472C4"}}, + Alignment: &excelize.Alignment{Horizontal: "center", Vertical: "center"}, + }) + headerStyle, _ := f.NewStyle(&excelize.Style{ + Font: &excelize.Font{Bold: true, Color: "FFFFFF"}, + Fill: excelize.Fill{Type: "pattern", Pattern: 1, Color: []string{"4472C4"}}, + Border: []excelize.Border{ + {Type: "left", Color: "FFFFFF", Style: 1}, + {Type: "right", Color: "FFFFFF", Style: 1}, + {Type: "top", Color: "FFFFFF", Style: 1}, + {Type: "bottom", Color: "FFFFFF", Style: 1}, + }, + Alignment: &excelize.Alignment{Horizontal: "center", Vertical: "center"}, + }) + dataStyle, _ := f.NewStyle(&excelize.Style{ + Border: []excelize.Border{ + {Type: "left", Color: "D9D9D9", Style: 1}, + {Type: "right", Color: "D9D9D9", Style: 1}, + {Type: "top", Color: "D9D9D9", Style: 1}, + {Type: "bottom", Color: "D9D9D9", Style: 1}, + }, + Alignment: &excelize.Alignment{Horizontal: "center", Vertical: "center"}, + }) + + monthName := time.Date(year, month, 1, 0, 0, 0, 0, time.UTC).Format("January 2006") + + f.SetCellValue(sheet, "A1", fmt.Sprintf("Work Time Report — %s", monthName)) + f.MergeCell(sheet, "A1", "E1") + f.SetCellStyle(sheet, "A1", "E1", titleStyle) + f.SetRowHeight(sheet, 1, 30) + + headers := []string{"Date", "Clock In", "Clock Out", "Work (h)", "Break (m)"} for i, h := range headers { - cell, _ := excelize.CoordinatesToCellName(i+1, 1) - if err := f.SetCellValue(sheet, cell, h); err != nil { - return nil, err - } + cell, _ := excelize.CoordinatesToCellName(i+1, 2) + f.SetCellValue(sheet, cell, h) + f.SetCellStyle(sheet, cell, cell, headerStyle) } + f.SetRowHeight(sheet, 2, 22) - row := 2 + f.SetColWidth(sheet, "A", "A", 14) + f.SetColWidth(sheet, "B", "B", 10) + f.SetColWidth(sheet, "C", "C", 10) + f.SetColWidth(sheet, "D", "D", 10) + f.SetColWidth(sheet, "E", "E", 10) + + row := 3 loc := time.Now().Location() firstDay := time.Date(year, month, 1, 0, 0, 0, 0, loc) lastDay := firstDay.AddDate(0, 1, -1) - for d := firstDay; d.Before(lastDay.AddDate(0, 0, 1)); d = d.AddDate(0, 0, 1) { + for d := firstDay; !d.After(lastDay); d = d.AddDate(0, 0, 1) { dateStr := d.Format("2006-01-02") + isOff, err := store.IsDayOff(dateStr) if err != nil { return nil, err } if isOff { - continue // skip day‑offs + continue } + events, err := store.EventsForDay(dateStr) if err != nil { return nil, err } totals := ComputeDailyTotals(events) - hours := float64(totals.TotalSeconds) / 3600.0 cellDate, _ := excelize.CoordinatesToCellName(1, row) - cellHours, _ := excelize.CoordinatesToCellName(2, row) - cellPairs, _ := excelize.CoordinatesToCellName(3, row) + f.SetCellValue(sheet, cellDate, dateStr) + f.SetCellStyle(sheet, cellDate, cellDate, dataStyle) - if err := f.SetCellValue(sheet, cellDate, dateStr); err != nil { - return nil, err - } - if err := f.SetCellValue(sheet, cellHours, fmt.Sprintf("%.2f", hours)); err != nil { - return nil, err - } - if err := f.SetCellValue(sheet, cellPairs, totals.PairCount); err != nil { - return nil, err + if len(events) > 0 { + cellIn, _ := excelize.CoordinatesToCellName(2, row) + cellOut, _ := excelize.CoordinatesToCellName(3, row) + f.SetCellValue(sheet, cellIn, time.Unix(events[0].OccurredAt, 0).Format("15:04")) + f.SetCellStyle(sheet, cellIn, cellIn, dataStyle) + if len(events) > 1 && events[len(events)-1].EventType == "out" { + f.SetCellValue(sheet, cellOut, time.Unix(events[len(events)-1].OccurredAt, 0).Format("15:04")) + } + f.SetCellStyle(sheet, cellOut, cellOut, dataStyle) } + + cellWork, _ := excelize.CoordinatesToCellName(4, row) + hours := float64(totals.TotalSeconds) / 3600.0 + f.SetCellValue(sheet, cellWork, fmt.Sprintf("%.2f", hours)) + f.SetCellStyle(sheet, cellWork, cellWork, dataStyle) + + cellBreak, _ := excelize.CoordinatesToCellName(5, row) + breakMin := totals.BreakSeconds / 60 + f.SetCellValue(sheet, cellBreak, breakMin) + f.SetCellStyle(sheet, cellBreak, cellBreak, dataStyle) + row++ } diff --git a/internal/bot/handlers.go b/internal/bot/handlers.go index cfc1933..aa47998 100644 --- a/internal/bot/handlers.go +++ b/internal/bot/handlers.go @@ -9,27 +9,29 @@ import ( tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" "worktimeBot/internal/db" - "worktimeBot/pkg/i18n" ) type Handler struct { - Bot *tgbotapi.BotAPI - DB *db.Store - Translator *i18n.Translator + Bot *tgbotapi.BotAPI + DB *db.Store + AllowedUsers map[int64]bool } -func NewHandler(bot *tgbotapi.BotAPI, store *db.Store, tr *i18n.Translator) *Handler { - return &Handler{Bot: bot, DB: store, Translator: tr} +func NewHandler(bot *tgbotapi.BotAPI, store *db.Store, allowed map[int64]bool) *Handler { + return &Handler{Bot: bot, DB: store, AllowedUsers: allowed} } func (h *Handler) HandleMessage(update tgbotapi.Update) { msg := update.Message + if len(h.AllowedUsers) > 0 && !h.AllowedUsers[msg.Chat.ID] { + return + } switch msg.Text { case "/start": h.handleStart(msg) - case "/clockin", "Clock In", "ورود": + case "/clockin", "Clock In": h.handleClockInMsg(msg) - case "/clockout", "Clock Out", "خروج": + case "/clockout", "Clock Out": h.handleClockOutMsg(msg) case "/remote": h.toggleRemote(msg) @@ -40,12 +42,16 @@ func (h *Handler) HandleMessage(update tgbotapi.Update) { case "/dayoff": h.handleDayOff(msg) default: - h.replyWithText(msg.Chat.ID, "unknown_command") + h.sendText(msg.Chat.ID, "Unknown command") } } func (h *Handler) HandleCallback(update tgbotapi.Update) { cb := update.CallbackQuery + if len(h.AllowedUsers) > 0 && !h.AllowedUsers[cb.Message.Chat.ID] { + h.Bot.Request(tgbotapi.NewCallback(cb.ID, "Unauthorized")) + return + } chatID := cb.Message.Chat.ID msgID := cb.Message.MessageID switch cb.Data { @@ -66,18 +72,19 @@ func (h *Handler) HandleCallback(update tgbotapi.Update) { } } -// --- /start with inline keyboard --- +// --- /start --- func (h *Handler) handleStart(msg *tgbotapi.Message) { + h.DB.SetSetting("chat_id", fmt.Sprintf("%d", msg.Chat.ID)) text := "Welcome! Choose an action:" reply := tgbotapi.NewMessage(msg.Chat.ID, text) reply.ReplyMarkup = mainKeyboard() - _, _ = h.Bot.Send(reply) + h.Bot.Send(reply) kb := quickKeyboard() - quick := tgbotapi.NewMessage(msg.Chat.ID, "Quick buttons added below:") + quick := tgbotapi.NewMessage(msg.Chat.ID, "Use the buttons below to quickly clock in/out:") quick.ReplyMarkup = kb - _, _ = h.Bot.Send(quick) + h.Bot.Send(quick) } func quickKeyboard() tgbotapi.ReplyKeyboardMarkup { @@ -114,21 +121,21 @@ func backKeyboard() tgbotapi.InlineKeyboardMarkup { ) } -// --- Message-only handlers (legacy text commands) --- +// --- Text command handlers --- func (h *Handler) handleClockInMsg(msg *tgbotapi.Message) { if err := h.doClockIn(); err != nil { - h.replyWithError(msg.Chat.ID, err.Error()) + h.sendText(msg.Chat.ID, err.Error()) } else { - h.replyWithText(msg.Chat.ID, "clock_in") + h.sendSuccessWithMenu(msg.Chat.ID, "✅ Clocked in at "+time.Now().Format("15:04")) } } func (h *Handler) handleClockOutMsg(msg *tgbotapi.Message) { if err := h.doClockOut(); err != nil { - h.replyWithError(msg.Chat.ID, err.Error()) + h.sendText(msg.Chat.ID, err.Error()) } else { - h.replyWithText(msg.Chat.ID, "clock_out") + h.sendSuccessWithMenu(msg.Chat.ID, "✅ Clocked out at "+time.Now().Format("15:04")) } } @@ -138,37 +145,68 @@ func (h *Handler) toggleRemote(msg *tgbotapi.Message) { current = "onsite" } newMode := "remote" + text := "🌍 Remote mode enabled" if current == "remote" { newMode = "onsite" + text = "🏢 Onsite mode enabled" } if err := h.DB.SetSetting("remote_flag", newMode); err != nil { - h.replyWithError(msg.Chat.ID, "error") + h.sendText(msg.Chat.ID, "Error toggling mode") return } - key := "remote_switched_on" - if newMode == "onsite" { - key = "remote_switched_off" - } - h.replyWithText(msg.Chat.ID, key) + h.sendText(msg.Chat.ID, text) } func (h *Handler) handleReport(msg *tgbotapi.Message) { - today := time.Now().Format("2006-01-02") - events, err := h.DB.EventsForDay(today) + text := h.buildDailyReport(time.Now()) + reply := tgbotapi.NewMessage(msg.Chat.ID, text) + h.Bot.Send(reply) +} + +func (h *Handler) buildDailyReport(now time.Time) string { + dateStr := now.Format("2006-01-02") + events, err := h.DB.EventsForDay(dateStr) if err != nil { - h.replyWithError(msg.Chat.ID, "error") - return + return "Error fetching events." } + if len(events) == 0 { + isOff, _ := h.DB.IsDayOff(dateStr) + if isOff { + return fmt.Sprintf("📋 %s — Day Off 🎉", dateStr) + } + return fmt.Sprintf("📋 %s — No events recorded.", dateStr) + } + + remoteFlag := "Onsite" + current, err := h.DB.GetSetting("remote_flag") + if err == nil && current == "remote" { + remoteFlag = "Remote" + } + totals := ComputeDailyTotals(events) if totals.PairCount == 0 { - h.replyWithText(msg.Chat.ID, "total_work_time") - return + t := time.Unix(events[0].OccurredAt, 0).Format("15:04") + return fmt.Sprintf("📋 %s\n📍 Mode: %s\n⏰ Clocked in at %s\n\nStill working — no clock-out yet.", dateStr, remoteFlag, t) } + + lines := fmt.Sprintf("📋 Report for %s\n📍 Mode: %s\n\n", dateStr, remoteFlag) + + for _, e := range events { + t := time.Unix(e.OccurredAt, 0).Format("15:04") + icon := "⏰" + label := "Clock In" + if e.EventType == "out" { + icon = "🔴" + label = "Clock Out" + } + lines += fmt.Sprintf("%s %s — %s\n", icon, t, label) + } + workStr := formatDuration(totals.TotalSeconds) breakStr := formatDuration(totals.BreakSeconds) - text := fmt.Sprintf("Work: %s | Break: %s", workStr, breakStr) - reply := tgbotapi.NewMessage(msg.Chat.ID, text) - _, _ = h.Bot.Send(reply) + lines += fmt.Sprintf("\n📊 Summary:\n Work: %s\n Break: %s\n", workStr, breakStr) + + return lines } func (h *Handler) handleExport(msg *tgbotapi.Message) { @@ -176,7 +214,7 @@ func (h *Handler) handleExport(msg *tgbotapi.Message) { year, month := now.Year(), now.Month() data, err := GenerateMonthlyReport(h.DB, year, month) if err != nil { - h.replyWithError(msg.Chat.ID, "error") + h.sendText(msg.Chat.ID, "Error generating report") return } doc := tgbotapi.NewDocument(msg.Chat.ID, tgbotapi.FileBytes{ @@ -184,7 +222,7 @@ func (h *Handler) handleExport(msg *tgbotapi.Message) { Bytes: data, }) if _, err := h.Bot.Send(doc); err != nil { - h.replyWithError(msg.Chat.ID, "error") + h.sendText(msg.Chat.ID, "Error sending file") } } @@ -192,29 +230,29 @@ func (h *Handler) handleDayOff(msg *tgbotapi.Message) { today := time.Now().Format("2006-01-02") isOff, err := h.DB.IsDayOff(today) if err != nil { - h.replyWithError(msg.Chat.ID, "error") + h.sendText(msg.Chat.ID, "Error checking day off") return } if isOff { if err := h.DB.RemoveDayOff(today); err != nil { - h.replyWithError(msg.Chat.ID, "error") + h.sendText(msg.Chat.ID, "Error removing day off") return } - h.replyWithText(msg.Chat.ID, "dayoff_removed") + h.sendText(msg.Chat.ID, "✅ Day off removed") } else { if err := h.DB.SetDayOff(today, ""); err != nil { - h.replyWithError(msg.Chat.ID, "error") + h.sendText(msg.Chat.ID, "Error setting day off") return } - h.replyWithText(msg.Chat.ID, "dayoff_set") + h.sendText(msg.Chat.ID, "✅ Today marked as day off") } } -// --- Inline callback handlers --- +// --- Callback handlers --- func (h *Handler) clockInCallback(chatID int64, msgID int, callbackID string) { defer h.Bot.Request(tgbotapi.NewCallback(callbackID, "")) - text := h.Translator.Get("clock_in", "en") + text := "✅ Clocked in at " + time.Now().Format("15:04") if err := h.doClockIn(); err != nil { text = err.Error() } @@ -226,7 +264,7 @@ func (h *Handler) clockInCallback(chatID int64, msgID int, callbackID string) { func (h *Handler) clockOutCallback(chatID int64, msgID int, callbackID string) { defer h.Bot.Request(tgbotapi.NewCallback(callbackID, "")) - text := h.Translator.Get("clock_out", "en") + text := "✅ Clocked out at " + time.Now().Format("15:04") if err := h.doClockOut(); err != nil { text = err.Error() } @@ -243,17 +281,15 @@ func (h *Handler) toggleRemoteCallback(chatID int64, msgID int, callbackID strin current = "onsite" } newMode := "remote" + text := "🌍 Remote mode enabled" if current == "remote" { newMode = "onsite" + text = "🏢 Onsite mode enabled" } if err := h.DB.SetSetting("remote_flag", newMode); err != nil { return } - key := "remote_switched_on" - if newMode == "onsite" { - key = "remote_switched_off" - } - edit := tgbotapi.NewEditMessageText(chatID, msgID, h.Translator.Get(key, "en")) + edit := tgbotapi.NewEditMessageText(chatID, msgID, text) kb := backKeyboard() edit.ReplyMarkup = &kb h.Bot.Send(edit) @@ -261,18 +297,7 @@ func (h *Handler) toggleRemoteCallback(chatID int64, msgID int, callbackID strin func (h *Handler) reportCallback(chatID int64, msgID int, callbackID string) { defer h.Bot.Request(tgbotapi.NewCallback(callbackID, "")) - today := time.Now().Format("2006-01-02") - events, err := h.DB.EventsForDay(today) - if err != nil { - return - } - totals := ComputeDailyTotals(events) - text := "No events today" - if totals.PairCount > 0 { - workStr := formatDuration(totals.TotalSeconds) - breakStr := formatDuration(totals.BreakSeconds) - text = fmt.Sprintf("Work: %s | Break: %s", workStr, breakStr) - } + text := h.buildDailyReport(time.Now()) edit := tgbotapi.NewEditMessageText(chatID, msgID, text) kb := backKeyboard() edit.ReplyMarkup = &kb @@ -309,12 +334,10 @@ func (h *Handler) dayOffCallback(chatID int64, msgID int, callbackID string) { if err != nil { return } - text := h.Translator.Get("dayoff_set", "en") + text := "✅ Today marked as day off" if isOff { h.DB.RemoveDayOff(today) - text = h.Translator.Get("dayoff_removed", "en") - } else { - h.DB.SetDayOff(today, "") + text = "✅ Day off removed" } edit := tgbotapi.NewEditMessageText(chatID, msgID, text) kb := backKeyboard() @@ -330,7 +353,15 @@ func (h *Handler) backToMenu(chatID int64, msgID int, callbackID string) { h.Bot.Send(edit) } -// --- Pure logic (no chat I/O) --- +// --- Scheduled report --- + +func (h *Handler) SendDailyReport(chatID int64) { + text := h.buildDailyReport(time.Now()) + reply := tgbotapi.NewMessage(chatID, text) + h.Bot.Send(reply) +} + +// --- Pure logic --- func (h *Handler) doClockIn() error { last, err := h.DB.GetLastEvent() @@ -338,10 +369,10 @@ func (h *Handler) doClockIn() error { return err } if last != nil && last.EventType == "in" { - return errors.New(h.Translator.Get("already_clocked_in", "en")) + return errors.New("⚠️ You are already clocked in") } if _, err := h.DB.GetSetting("remote_flag"); err != nil { - _ = h.DB.SetSetting("remote_flag", "onsite") + h.DB.SetSetting("remote_flag", "onsite") } return h.DB.CreateEvent("in", time.Now().Unix(), "") } @@ -352,15 +383,15 @@ func (h *Handler) doClockOut() error { return err } if last == nil { - return errors.New(h.Translator.Get("error", "en")) + return errors.New("⚠️ No clock-in found. Start with /clockin first") } switch last.EventType { case "in": return h.DB.CreateEvent("out", time.Now().Unix(), "") case "out": - return errors.New(h.Translator.Get("already_clocked_out", "en")) + return errors.New("⚠️ You are already clocked out") default: - return errors.New(h.Translator.Get("error", "en")) + return errors.New("⚠️ Error processing request") } } @@ -375,14 +406,13 @@ func formatDuration(seconds int64) string { return fmt.Sprintf("%dm", mins) } -func (h *Handler) replyWithText(chatID int64, key string) { - text := h.Translator.Get(key, "en") +func (h *Handler) sendText(chatID int64, text string) { reply := tgbotapi.NewMessage(chatID, text) - _, _ = h.Bot.Send(reply) + h.Bot.Send(reply) } -func (h *Handler) replyWithError(chatID int64, key string) { - text := h.Translator.Get(key, "en") +func (h *Handler) sendSuccessWithMenu(chatID int64, text string) { reply := tgbotapi.NewMessage(chatID, text) - _, _ = h.Bot.Send(reply) + reply.ReplyMarkup = mainKeyboard() + h.Bot.Send(reply) } diff --git a/pkg/i18n/en.json b/pkg/i18n/en.json deleted file mode 100644 index 80c2505..0000000 --- a/pkg/i18n/en.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "unknown_command": "Unknown command", - "clock_in": "Clock In", - "clock_out": "Clock Out", - "error": "Error", - "no_permission": "You do not have permission", - "remote_switched_on": "Remote mode enabled", - "remote_switched_off": "Remote mode disabled", - "already_clocked_in": "You are already clocked in", - "already_clocked_out": "You are already clocked out", - "break_started": "Break started", - "remote_enabled": "Remote mode enabled", - "remote_disabled": "Remote mode disabled", - "dayoff_set": "Today marked as day off", - "dayoff_removed": "Day off removed" -} diff --git a/pkg/i18n/fa.json b/pkg/i18n/fa.json deleted file mode 100644 index 446a9af..0000000 --- a/pkg/i18n/fa.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "unknown_command": "دستور نامشخص", - "clock_in": "ورودی کاری", - "clock_out": "خروج کاری", - "error": "خطا", - "no_permission": "دسترسی ندارید", - "remote_switched_on": "وضعیت راه دور فعال شد", - "remote_switched_off": "وضعیت راه دور غیرفعال شد", - "already_clocked_in": "شما قبلاً ورود زده‌اید", - "already_clocked_out": "شما قبلاً خروج زده‌اید", - "break_started": "استراحت شروع شد", - "remote_enabled": "وضعیت راه دور فعال شد", - "remote_disabled": "وضعیت راه دور غیرفعال شد", - "dayoff_set": "امروز به عنوان تعطیل ثبت شد", - "dayoff_removed": "تعطیلی برداشته شد" -} diff --git a/pkg/i18n/translator.go b/pkg/i18n/translator.go deleted file mode 100644 index 5ad10db..0000000 --- a/pkg/i18n/translator.go +++ /dev/null @@ -1,67 +0,0 @@ -package i18n - -import ( - "encoding/json" - "fmt" - "io/ioutil" - "path/filepath" - "sync" -) - -// Translator handles translation loading and retrieval. -type Translator struct { - mu sync.RWMutex - messages map[string]map[string]string // lang -> key -> message -} - -// NewTranslator loads JSON locale files en.json and fa.json from the i18n directory. -// defaultLang sets the default fallback language (e.g., "en"). -func NewTranslator(defaultLang string) (*Translator, error) { - if defaultLang == "" { - defaultLang = "en" - } - t := &Translator{ - messages: make(map[string]map[string]string), - } - // Load known locale files: en.json and fa.json located in the same package directory. - for _, lang := range []string{"en", "fa"} { - filePath := filepath.Join("i18n", lang+".json") - data, err := ioutil.ReadFile(filePath) - if err != nil { - return nil, err - } - var m map[string]string - if err := json.Unmarshal(data, &m); err != nil { - return nil, err - } - t.messages[lang] = m - } - // Ensure default language exists. - if _, ok := t.messages[defaultLang]; !ok { - return nil, fmt.Errorf("default language %s not found", defaultLang) - } - return t, nil -} - -// Get returns the translated string for a given key and language. -// Falls back to English if the key/language is missing. -func (t *Translator) Get(key, lang string) string { - if lang == "" { - lang = "en" - } - t.mu.RLock() - defer t.mu.RUnlock() - if msgs, ok := t.messages[lang]; ok { - if v, ok := msgs[key]; ok { - return v - } - } - // fallback to English messages - if msgs, ok := t.messages["en"]; ok { - if v, ok := msgs[key]; ok { - return v - } - } - // If not found, return the key itself. - return key -}