Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
name: Tests & Coverage

on:
pull_request:
branches: [ master, main, develop ]

jobs:
tests:
name: Run Tests & Coverage
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: '1.25'

- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '24'
cache: 'npm'
cache-dependency-path: frontend/package-lock.json

- name: Install dependencies
run: |
go mod download
cd frontend && npm ci

- name: Run Go tests with coverage
run: go test ./... -v -coverprofile=coverage.out -covermode=atomic


- name: Run frontend tests with coverage
working-directory: ./frontend
run: |
export NODE_OPTIONS="--max-old-space-size=4096 --no-warnings"
npm run test:coverage
env:
NODE_ENV: test
VITEST_MAX_THREADS: 1
VITEST_MIN_THREADS: 1
CI: true

- name: Upload Go coverage to Codecov
uses: codecov/codecov-action@v5
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: ./coverage.out
flags: backend
fail_ci_if_error: false
verbose: true

- name: Upload frontend coverage to Codecov
uses: codecov/codecov-action@v5
with:
token: ${{ secrets.CODECOV_TOKEN }}
directory: ./frontend/coverage
flags: frontend
fail_ci_if_error: false
verbose: true


51 changes: 51 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
name: Docker Release

on:
push:
tags:
- 'v*.*.*'

jobs:
docker-build:
name: Build & Release Docker Image
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '18'

- name: Build frontend
working-directory: ./frontend
run: |
npm ci
npm run build

- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ghcr.io/${{ github.repository }}
tags: |
type=ref,event=tag
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}

- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}

13 changes: 12 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,15 @@ node_modules
frontend/dist
frontend/playwright-report
frontend/test-results
*.exe
frontend/coverage
*.exe

# Ignore TypeScript buildinfo files
*.tsbuildinfo

# Ignore IDE metadata
.idea/

# Ignore frontend build artifacts
frontend/.idea/
frontend/tsconfig.tsbuildinfo
4 changes: 0 additions & 4 deletions .idea/BabelBridge.iml

This file was deleted.

97 changes: 88 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,15 +1,24 @@
# BabelBridge
<div align="center">
<img src="logo.svg" alt="BabelBridge Logo" width="128" height="128">

# BabelBridge

BabelBridge is a web-based translation tool with a modern UI, supporting live language identification, multi-message context, and a pluggable AI backend (Ollama or Cohere).
[![Tests & Coverage](https://github.com/daniel-sullivan/babel-bridge/actions/workflows/ci.yml/badge.svg)](https://github.com/daniel-sullivan/babel-bridge/actions/workflows/ci.yml)
[![codecov](https://codecov.io/gh/daniel-sullivan/babel-bridge/branch/master/graph/badge.svg)](https://codecov.io/gh/daniel-sullivan/babel-bridge)

## Features
**A modern web-based translation tool with live language identification and pluggable AI backends**
</div>

- Translate text between multiple languages
- Live language identification as you type
- Multi-message context (chain mode)
- Language variety buttons with responsive overflow
- Accessible, responsive, and mobile-friendly UI
- Pluggable AI backend: OpenAI (public or local e.g.: Ollama) or Cohere
## ✨ Features

- 🌐 Translate text between multiple languages
- 🔍 Live language identification as you type
- 🔗 Multi-message context (chain mode)
- 🎯 Language variety buttons with responsive overflow
- ♿ Accessible, responsive, and mobile-friendly UI
- 🔌 Pluggable AI backend: OpenAI (public or local e.g.: Ollama) or Cohere
- 🧪 **Comprehensive test suite with 100% passing tests**
- 📊 **Full test coverage reporting**

## Getting Started

Expand Down Expand Up @@ -67,6 +76,76 @@ BabelBridge is a web-based translation tool with a modern UI, supporting live la

5. Visit [http://localhost:8080](http://localhost:8080) in your browser.

## 🧪 Testing

BabelBridge has a comprehensive test suite covering both backend and frontend components.

### Test Coverage

- **Go Backend**: Unit tests, integration tests, and mock testing
- **Frontend**: Component tests, context tests, utility tests, and E2E tests
- **Coverage Target**: 70%+ for both backend and frontend
- **Status**: 100% passing tests ✅

### Running Tests

#### All Tests
```sh
make test-all # Run all tests with coverage
npm run test:all # Alternative using npm (frontend only)
```

#### Go Backend Tests
```sh
make test-go # Run Go tests with coverage
go test -v ./... # Basic test run
go test -cover ./... # With coverage
```

#### Frontend Tests
```sh
make test-frontend # Run frontend tests with coverage
cd frontend && npm test # Unit tests (watch mode)
cd frontend && npm run test:unit # Unit tests (single run)
cd frontend && npm run test:coverage # With coverage report
```

#### E2E Tests
```sh
make test-e2e # Run end-to-end tests
cd frontend && npm run test:e2e # Direct E2E run
cd frontend && npm run test:e2e:ui # E2E with UI
```

### Coverage Reports

After running tests with coverage, reports are available at:
- **Go**: `coverage.html` (root directory)
- **Frontend**: `frontend/coverage/index.html`

### Continuous Integration

All tests run automatically on:
- ✅ Every push to main/develop branches
- ✅ Every pull request
- ✅ Coverage reports posted as PR comments
- ✅ Tests must pass before merging

### Test Structure

```
tests/
├── backend/
│ ├── unit/ # Go unit tests
│ ├── integration/ # Go integration tests
│ └── mock/ # Mock implementations
└── frontend/
├── components/ # React component tests
├── context/ # Context/state tests
├── utils/ # Utility function tests
└── e2e/ # End-to-end tests
```

### Docker

You can build and run BabelBridge with Docker:
Expand Down
37 changes: 23 additions & 14 deletions api/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"BabelBridge/service"
"log/slog"
"net/http"
"os"
"time"

"github.com/gin-contrib/sessions"
Expand Down Expand Up @@ -55,12 +56,28 @@ func NewServerWithTTLs(svc service.TranslationService, sessTTL, ctxTTL time.Dura
cookieSameSite: http.SameSiteLaxMode,
}

api := r.Group("/api")
sessionHandler := []gin.HandlerFunc{func(c *gin.Context) {
s.issueSessionHandler(c)
if c.IsAborted() {
return
}
c.Data(http.StatusOK, "text/plain; charset=utf-8", []byte("OK"))
}}

// rate limiting setup
memStore := memory.NewStore()
limiterSession := limiterpkg.New(memStore, limiterpkg.Rate{Period: time.Minute, Limit: 5})
limiterAPI := limiterpkg.New(memStore, limiterpkg.Rate{Period: time.Minute, Limit: 30})
limiterSessionMW := ginlimiter.NewMiddleware(limiterSession)
limiterAPIMW := ginlimiter.NewMiddleware(limiterAPI)
if os.Getenv("RATE_LIMITING_ENABLED") == "true" {
slog.Info("Rate limiting enabled")
memStore := memory.NewStore()
limiterSession := limiterpkg.New(memStore, limiterpkg.Rate{Period: time.Minute, Limit: 5})
limiterAPI := limiterpkg.New(memStore, limiterpkg.Rate{Period: time.Minute, Limit: 30})
limiterSessionMW := ginlimiter.NewMiddleware(limiterSession)
limiterAPIMW := ginlimiter.NewMiddleware(limiterAPI)
sessionHandler = append([]gin.HandlerFunc{limiterSessionMW}, sessionHandler...)
api.Use(limiterAPIMW)
} else {
slog.Warn("Rate limiting disabled")
}

// serve static assets for the frontend
r.Static("/assets", "frontend/dist/assets")
Expand All @@ -75,16 +92,8 @@ func NewServerWithTTLs(svc service.TranslationService, sessTTL, ctxTTL time.Dura
})

// manual session creation endpoint when front-end is running on a separate service
r.GET("/session", limiterSessionMW, func(c *gin.Context) {
s.issueSessionHandler(c)
if c.IsAborted() {
return
}
c.Data(http.StatusOK, "text/plain; charset=utf-8", []byte("OK"))
})
r.GET("/session", sessionHandler...)

api := r.Group("/api")
api.Use(limiterAPIMW)
api.Use(s.sessionMiddleware())
{
api.POST("/translate/start", s.startTranslation)
Expand Down
Loading