From 0bb5db46d08a436602925184e2f8b2f3c1191599 Mon Sep 17 00:00:00 2001 From: db123 Date: Tue, 23 Jun 2026 20:23:04 +0330 Subject: [PATCH] Dockerfile, docker-compose, Gitea CI, Makefile, i18n (en/fa) --- .env.example | 7 +++++ .gitea/workflows/ci.yml | 27 +++++++++++++++++ Dockerfile | 20 ++++++++++++ Makefile | 32 ++++++++++++++++++++ docker-compose.yml | 15 +++++++++ pkg/i18n/en.json | 16 ++++++++++ pkg/i18n/fa.json | 16 ++++++++++ pkg/i18n/translator.go | 67 +++++++++++++++++++++++++++++++++++++++++ 8 files changed, 200 insertions(+) create mode 100644 .env.example create mode 100644 .gitea/workflows/ci.yml create mode 100644 Dockerfile create mode 100644 Makefile create mode 100644 docker-compose.yml create mode 100644 pkg/i18n/en.json create mode 100644 pkg/i18n/fa.json create mode 100644 pkg/i18n/translator.go diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..de9f8a3 --- /dev/null +++ b/.env.example @@ -0,0 +1,7 @@ +BOT_TOKEN=YOUR_TELEGRAM_BOT_TOKEN +HTTP_PROXY= +DB_PATH=db.sqlite3 +BOT_WEBHOOK_URL= +BOT_LISTEN=:8080 +BOT_TLS_CERT= +BOT_TLS_KEY= diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml new file mode 100644 index 0000000..dee4aef --- /dev/null +++ b/.gitea/workflows/ci.yml @@ -0,0 +1,27 @@ +name: CI + +on: + push: + branches: [main, master] + pull_request: + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version: "1.22" + + - name: Check formatting + run: test -z "$(gofmt -d .)" || exit 1 + + - name: Vet + run: go vet ./... + + - name: Build + run: go build ./... + + - name: Test + run: go test ./... diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..44a732f --- /dev/null +++ b/Dockerfile @@ -0,0 +1,20 @@ +FROM golang:1.22-alpine AS builder + +WORKDIR /src +COPY go.mod go.sum ./ +RUN go mod download +COPY . . +RUN go build -o /usr/local/bin/worktime-bot ./cmd/bot/ + +FROM alpine:3.18 + +RUN apk add --no-cache ca-certificates tzdata + +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 + +VOLUME ["/data"] + +CMD ["worktime-bot"] diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..50754b7 --- /dev/null +++ b/Makefile @@ -0,0 +1,32 @@ +.PHONY: build run test clean docker docker-run lint vet + +BINARY=worktime-bot +BUILD_DIR=.build + +build: $(BUILD_DIR)/$(BINARY) + +$(BUILD_DIR)/$(BINARY): cmd/bot/*.go internal/**/*.go pkg/**/*.go go.mod go.sum + mkdir -p $(BUILD_DIR) + go build -o $(BUILD_DIR)/$(BINARY) ./cmd/bot/ + +run: build + $(BUILD_DIR)/$(BINARY) + +test: + go test ./... + +lint: vet +vet: + go vet ./... + +clean: + rm -rf $(BUILD_DIR) + +docker: + docker compose build + +docker-run: docker + docker compose up -d + +.PHONY: all +all: build test lint diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..a40f804 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,15 @@ +services: + worktime-bot: + build: . + env_file: + - .env + environment: + - BOT_TOKEN=${BOT_TOKEN} + - HTTP_PROXY=${HTTP_PROXY-} + - DB_PATH=/data/db.sqlite3 + volumes: + - worktime_data:/data + +volumes: + worktime_data: + driver: local diff --git a/pkg/i18n/en.json b/pkg/i18n/en.json new file mode 100644 index 0000000..80c2505 --- /dev/null +++ b/pkg/i18n/en.json @@ -0,0 +1,16 @@ +{ + "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 new file mode 100644 index 0000000..446a9af --- /dev/null +++ b/pkg/i18n/fa.json @@ -0,0 +1,16 @@ +{ + "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 new file mode 100644 index 0000000..5ad10db --- /dev/null +++ b/pkg/i18n/translator.go @@ -0,0 +1,67 @@ +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 +}