OpenVerba is a language-learning application that helps you learn new languages through text translation, word-by-word breakdowns and audio pronunciation.
- 👤 User Accounts: Secure signup and login with JWT authentication
- 📝 Text Translation: Translate texts between multiple languages
- 🔤 Word-by-Word Breakdown: See individual word translations and their meanings
- 🤖 AI Text Generation: Generate new texts based on your known vocabulary
- 🔊 Audio Pronunciation: Listen to sentences and words with natural-sounding voices (generated asynchronously in the background)
- 📊 Word Tracking: Track word frequency and usage across all your texts, with separate counts for writing and speaking practice
- ✏️ Cloze Completion Practice: Practice vocabulary with interactive cloze exercises where you complete words by typing missing letters
- 🎤 Pronunciation Practice: Practice speaking with real-time speech recognition and accuracy scoring
- 🔥 Streak Tracking: Track your daily practice streak to stay motivated with live updates (click the streak counter to view detailed statistics)
- 📈 Practice Statistics: View completion trends with a visual graph showing your practice activity over the last 30 days
- 💾 Persistent Storage: All translations and audio files are saved locally
- English (en)
- Spanish (es)
- French (fr)
- German (de)
- Italian (it)
- Portuguese (pt)
- Polish (pl)
- Dutch (nl)
- Docker and Docker Compose (for Docker setup)
- Node.js 20+ (for local development)
- OpenAI API key
- AWS credentials (for Polly text-to-speech)
The application requires environment variables to be configured:
Configure these in backend/.env:
| Variable | Description | Example |
|---|---|---|
OPENAI_API_KEY |
Your OpenAI API key for translation services | sk-... |
AWS_REGION |
AWS region for Polly service | us-east-1 |
AWS_ACCESS_KEY_ID |
AWS access key ID for Polly | AKIA... |
AWS_SECRET_ACCESS_KEY |
AWS secret access key for Polly | ... |
COOKIE_SECRET |
Secret key for JWT cookie encryption | random-secret-key-change |
Configure these in frontend/.env:
| Variable | Description | Required | Example |
|---|---|---|---|
VITE_API_URL |
Backend API URL | Yes | http://localhost:3000 |
Note: The frontend needs to know where the backend API is running. Set this to match your backend URL.
OpenAI API Key:
- Visit OpenAI Platform
- Sign in or create an account
- Navigate to API Keys section
- Create a new API key
- Copy the key (you won't be able to see it again)
AWS Credentials:
- Visit AWS IAM Console
- Create a new IAM user or use existing one
- Attach the
AmazonPollyFullAccesspolicy - Create access keys for the user
- Copy the Access Key ID and Secret Access Key
git clone <repository-url>
cd OpenVerba# Copy the example environment file
cp backend/.env.example backend/.env
# Edit the .env file with your actual credentials
nano backend/.env# Build and start all services
docker-compose up -d
# View logs
docker-compose logs -f
# Stop the application
docker-compose downThe application will be available at:
- Frontend: http://localhost:5173
- Backend API: http://localhost:3000
Docker volumes are configured to persist:
- Database:
backend/database.db- All translations and word data - Audio Files:
backend/public/audio/- Generated pronunciation files
These files will persist even if you restart or recreate the Docker containers.
cd backend
# Install dependencies
yarn install
# Copy environment file
cp .env.example .env
# Edit .env with your credentials
# Run in development mode (with hot reload)
yarn dev
# Build for production
yarn build
# Run production build
yarn startcd frontend
# Install dependencies
yarn install
# Copy environment file
cp .env.example .env
# (Default value http://localhost:3000 should work for local development)
# Run in development mode
yarn dev
# Build for production
yarn build
# Preview production build
yarn previewBoth the frontend and backend include test suites using Vitest.
The frontend includes specs for React components and utilities.
cd frontend
# Run all tests once
yarn test --run
# Run tests in watch mode (re-runs on file changes)
yarn test
# Run a specific test file
yarn test --run Words.spec.tsx
# Run tests with coverage
yarn test --run --coverageTest Files Location: frontend/src/**/*.spec.tsx
Available Specs:
Submit.spec.tsx- Submit form component testsText.spec.tsx- Text display component testsWords.spec.tsx- Words list component testsClozeWord.spec.tsx- Cloze word completion component testsStreakCounter.spec.tsx- Streak counter component testsCompletionGraph.spec.tsx- Completion statistics graph component tests
The backend includes specs for core library functions, services, and controllers. Tests run against an isolated in-memory SQLite database.
cd backend
# Run all tests once
yarn test --run
# Run tests in watch mode
yarn test
# Run a specific test file
yarn test --run translate.spec.ts
# Run tests with coverage
yarn test --run --coverageTest Files Location: backend/**/*.spec.ts
Available Specs:
lib/translate.spec.ts- Translation logic testslib/generate.spec.ts- AI text generation testslib/audio.spec.ts- Audio generation testsservices/wordService.spec.ts- Word service testsservices/completionService.spec.ts- Completion service testscontrollers/completionController.spec.ts- Completion controller testscontrollers/wordController.spec.ts- Word controller testscontrollers/textController.spec.ts- Text controller tests
Both projects use:
- Test Framework: Vitest
- Test Environment: jsdom (frontend), node (backend)
- Database (Backend): In-memory SQLite (
better-sqlite3) for isolated integration tests - Mocking: Vitest's built-in
vimocking utilities
When adding new features, follow these patterns:
Frontend Component Tests:
import { describe, it, expect, vi } from 'vitest';
import { render, screen, waitFor } from '@testing-library/react';
describe('YourComponent', () => {
it('renders correctly', () => {
render(<YourComponent />);
expect(screen.getByText('Expected Text')).toBeInTheDocument();
});
});Backend Function Tests:
import { describe, it, expect, vi } from 'vitest';
describe('yourFunction', () => {
it('returns expected result', () => {
const result = yourFunction();
expect(result).toBe(expectedValue);
});
});OpenVerba/
├── backend/
│ ├── controllers/ # Request handlers
│ ├── services/ # Business logic
│ ├── lib/ # Shared utilities (translation, audio)
│ ├── public/ # Static assets (audio files)
│ ├── index.ts # Main server entry point
│ ├── database.db # SQLite database
│ ├── Dockerfile
│ └── .env # Environment variables
├── frontend/
│ ├── src/
│ │ ├── components/ # Reusable UI components
│ │ ├── pages/ # Route components
│ │ ├── context/ # React Context providers
│ │ ├── config/ # Configuration files
│ │ ├── utils/ # Utility functions
│ │ ├── App.tsx # Main App component
│ │ └── main.tsx # Entry point
│ ├── Dockerfile
│ └── nginx.conf
└── docker-compose.yml
Each text follows a structured 6-step learning process:
- Read Translation: Read the text in your known language (the translation)
- Listen: Listen to the audio pronunciation while reading the translation
- Dual: View both languages side-by-side with word-by-word alignment
- Read Original: Read the text in the language you're learning
- Write: Complete cloze exercises where you type the missing letters of each word (only the first letter is shown)
- Speak: Practice pronunciation with real-time speech recognition and accuracy scoring
The Write step (step 5) uses cloze completion exercises:
- Each word displays only its first letter
- You type the remaining letters to complete the word
- Correct completions trigger a celebration animation
- Completions in this step are tracked as "writing" practice
- The streak counter (🔥) in the navigation bar updates live and shows your consecutive days of practice
- Click the streak counter to view detailed statistics and completion trends
The Speak step (step 6) uses speech recognition for pronunciation practice:
- Click the record button to start recording your pronunciation
- Speak the sentence shown on screen
- The app uses the Web Speech API to recognize your speech
- You'll receive an accuracy score comparing your pronunciation to the expected text
- Correctly pronounced words are automatically tracked as "speaking" practice
- Scores above 80% are considered excellent, 50-80% are good, and below 50% need improvement
Statistics Page:
- Accessible by clicking the streak counter in the navigation bar
- Displays a completion trends graph showing your practice activity over the last 30 days
- Shows total completions and maximum completions per day
Create a new user account
Request Body:
{
"email": "user@example.com",
"password": "securepassword"
}Response:
{
"message": "User created successfully"
}Note: Sets an HTTP-only cookie with JWT token for authentication.
Login to an existing account
Request Body:
{
"email": "user@example.com",
"password": "securepassword"
}Response:
{
"message": "Logged in successfully"
}Note: Sets an HTTP-only cookie with JWT token for authentication.
Logout from the current session
Response:
{
"message": "Logged out successfully"
}Note: Clears the authentication cookie.
Get current user information (requires authentication)
Response:
{
"id": 1,
"email": "user@example.com"
}Note: All text endpoints require authentication.
Create a new translation
Request Body:
{
"text": "¿Hola, cómo estás?",
"source_language": "en",
"target_language": "es"
}Note:
textshould be in the language you want to learn (target_language)source_languageis the language you know (for translations)target_languageis the language you're learning
Response:
{
"id": 1,
"translation": { ... },
"usage": { ... },
"audio_status": "processing",
"message": "Text saved successfully"
}Note: Audio generation happens asynchronously in the background. The audio_status field indicates the current status:
pending: Waiting to startprocessing: Currently generating audio filescompleted: All audio files readyfailed: Audio generation encountered an error
Get all translations
Get a specific translation by ID
Check the audio generation status for a specific text
Response:
{
"audio_status": "completed"
}Use this endpoint to poll for completion status after creating a new text.
Delete a translation
Generate a new text based on known words
Request Body:
{
"source_language": "es",
"new_words_percentage": 20,
"number_of_sentences": 4
}Note:
source_languageis the language to generate text in (the language you're learning)new_words_percentagecontrols how many new words to introduce (0-100)number_of_sentencesis optional, defaults to 4
Get all words grouped by language (the language you're learning)
Response:
{
"es": [
{
"id": 1,
"source_word": "hola",
"target_word": "hello",
"target_language": "es",
"occurrence_count": 5,
"writing_count": 2,
"speaking_count": 1,
"audio_url": "/audio/hola.mp3"
}
]
}Note:
- Words are grouped by
target_language(the language you're learning) source_wordcontains the word in the language you're learningtarget_wordcontains the translation in your known language- Audio is generated for
source_word(learning language)
Record a word completion (used when user successfully completes a cloze exercise or pronunciation practice)
Request Body:
{
"word_id": 123,
"method": "writing" // or "speaking"
}Response:
{
"success": true
}Get the current practice streak (consecutive days with completions)
Response:
{
"streak": 5
}Get completion statistics grouped by date
Response:
{
"stats": [
{
"date": "2025-11-30",
"count": 10
},
{
"date": "2025-11-29",
"count": 5
}
]
}The application uses SQLite with the following tables:
-
texts: Stores original texts and metadata
source_language: The language you know (for translations)target_language: The language you're learningtext: The original text in the target languageaudio_status: Tracks async audio generation ('pending', 'processing', 'completed', or 'failed')
-
sentences: Stores sentence translations
source_sentence: Sentence in the target language (learning language)target_sentence: Translation in the source language (known language)- Note: Audio is generated for
source_sentence
-
words: Stores unique word translations
source_word: Word in the target language (learning language)target_word: Translation in the source language (known language)target_language: The language you're learning (used for grouping)- Note: Audio is generated for
source_word
-
sentence_words: Links words to sentences
-
completions: Tracks word completions for practice streaks (stores
word_id,method('writing' or 'speaking'), andcompleted_attimestamp)
Important: The field naming can be confusing - source_* fields actually contain the learning language content, while target_* fields contain the known language content. The language code fields (source_language and target_language) follow the correct semantic naming.
The database includes optimized indexes for query performance:
Foreign Key Indexes (High Priority):
idx_sentences_text_id- Optimizes sentence lookups by textidx_sentence_words_sentence_id- Optimizes word lookups by sentenceidx_sentence_words_word_id- Optimizes word occurrence countsidx_completions_word_id- Optimizes completion counts
Ordering Indexes (Medium Priority):
idx_completions_completed_at- Optimizes streak calculationsidx_texts_created_at- Optimizes text list ordering
Additional Indexes (Low Priority):
idx_words_source_language- Legacy index (should be updated toidx_words_target_languagefor optimal performance)idx_sentences_order_in_text- Optimizes sentence orderingidx_sentence_words_order- Optimizes word ordering within sentences
Note: Words are now grouped by target_language (the language you're learning), so consider adding an index on words(target_language) for better performance.
These indexes significantly improve query performance, especially for:
- Loading text lists (50-200x faster with many texts)
- Retrieving individual texts with all sentences and words
- Calculating word occurrence counts
- Computing practice streaks
Index Maintenance: Indexes are automatically created on startup using the CREATE INDEX IF NOT EXISTS pattern for safe schema evolution.
- Fastify: Fast and low overhead web framework
- TypeScript: Type-safe JavaScript
- better-sqlite3: SQLite database
- OpenAI API: GPT-4 for translations
- AWS Polly: Neural text-to-speech
- React 19: UI framework
- TypeScript: Type-safe JavaScript
- Vite: Build tool
- TailwindCSS v4: Utility-first CSS
- React Router: Client-side routing
- Lucide React: Icon set
Containers won't start:
# Check logs
docker-compose logs
# Rebuild containers
docker-compose up --buildDatabase or audio files not persisting:
- Ensure the volume mounts in
docker-compose.ymlare correct - Check file permissions on the host machine
Translation fails:
- Verify
OPENAI_API_KEYis set correctly - Check OpenAI account has credits
- Review backend logs:
docker-compose logs backend
Audio generation fails:
- Verify AWS credentials are correct
- Ensure IAM user has Polly permissions
- Check AWS region is supported
- Review backend logs for specific errors
Port already in use:
# Change ports in docker-compose.yml or stop conflicting services
lsof -ti:3000 | xargs kill -9 # Kill process on port 3000
lsof -ti:80 | xargs kill -9 # Kill process on port 80This project is licensed under the MIT License - see the LICENSE file for details.
Contributions are welcome! Please feel free to submit a Pull Request.