Major changes across backend, frontend, infrastructure: - Provider system with model selection (Google, DeepL, OpenAI, Ollama, Google Cloud) - Admin panel: user management, pricing, settings - Glossary system with CSV import/export - Subscription and tier quota management - Security hardening (rate limiting, API key auth, path traversal fixes) - Docker compose for dev, prod, and IONOS deployment - Alembic migrations for new tables - Frontend: dashboard, pricing page, landing page, i18n (en/fr) - Test suite and verification scripts Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
14 KiB
Story 2.3: Provider DeepL
Status: done
Story
As a system, I want to integrate DeepL API as a production-ready provider with automatic Free/Pro endpoint detection, so that users can translate documents with higher quality (especially for European languages).
Acceptance Criteria
- AC1: API Integration - Given
DEEPL_API_KEYis configured, whenDeepLProvider.translate_text()is called, then text is translated using DeepL API - AC2: Auto-Detection Free/Pro - Provider automatically detects Free vs Pro API endpoint based on API key format (Free keys end with
:fx) - AC3: Graceful Error Handling - All API errors (quota exceeded, invalid key, network timeout) return structured errors with code and message (never HTTP 500)
- AC4: Health Check - Provider
is_available()returnsTruewhen API key is configured and API is reachable,Falseotherwise - AC5: Registry Integration - Provider is registered in
ProviderRegistryand appears in fallback chain - AC6: Unit Tests - Tests verify all error scenarios, Free/Pro detection, and mock API responses
Tasks / Subtasks
-
Task 1: Create DeepL Provider Implementation (AC: 1, 2)
- 1.1 Create
services/providers/deepl_provider.py - 1.2 Implement
DeepLProviderclass extendingTranslationProvider - 1.3 Implement
_detect_api_type()to auto-detect Free vs Pro from API key - 1.4 Implement
_get_api_url()to return correct endpoint based on type - 1.5 Use
deep_translatorlibrary for DeepL integration (same pattern as Google)
- 1.1 Create
-
Task 2: Implement Error Handling (AC: 3)
- 2.1 Define error codes:
DEEPL_QUOTA_EXCEEDED,DEEPL_INVALID_KEY,DEEPL_NETWORK_ERROR,DEEPL_UNSUPPORTED_LANGUAGE,DEEPL_TEXT_TOO_LONG - 2.2 Implement
DeepLProviderErrorexception class - 2.3 Map DeepL API errors to structured error responses
- 2.4 Add retry logic with exponential backoff for transient errors
- 2.5 Add timeout configuration (default 30s)
- 2.6 Ensure all errors return JSON:
{error, message, details?}format
- 2.1 Define error codes:
-
Task 3: Implement Health Check (AC: 4)
- 3.1 Implement
is_available()to check API key presence - 3.2 Add
health_check()with caching (TTL 60s) matching Google provider pattern - 3.3 Return
ProviderHealthStatuswith availability and latency
- 3.1 Implement
-
Task 4: Registry Integration (AC: 5)
- 4.1 Add
register_deepl_provider()function - 4.2 Add
get_deepl_provider()singleton function - 4.3 Update
services/providers/__init__.pyto auto-register DeepL when enabled - 4.4 Verify provider appears in fallback chain when configured
- 4.1 Add
-
Task 5: Configuration Updates (AC: 1, 2)
- 5.1 Verify
DEEPL_API_KEYandDEEPL_ENABLEDinconfig.py(already present) - 5.2 Add DeepL-specific configuration options to
.env.example:DEEPL_TIMEOUT=30DEEPL_MAX_RETRIES=3DEEPL_RETRY_DELAY=1
- 5.1 Verify
-
Task 6: Create Unit Tests (AC: 6)
- 6.1 Create
tests/test_providers/test_deepl_provider.py - 6.2 Test Free vs Pro API key detection
- 6.3 Test all error scenarios (quota, invalid key, timeout, unsupported language)
- 6.4 Test retry logic
- 6.5 Test health check functionality
- 6.6 Test registry integration
- 6.1 Create
-
Task 7: Update Documentation (AC: 1-6)
- 7.1 Update
services/providers/README.mdwith DeepL section - 7.2 Document Free vs Pro API key differences
- 7.3 Document supported languages (fewer than Google but higher quality)
- 7.1 Update
Dev Notes
🔬 DeepL API Specifics
API Endpoints:
| Type | Endpoint | Key Format |
|---|---|---|
| Free | https://api-free.deepl.com/v2/translate |
Ends with :fx |
| Pro | https://api.deepl.com/v2/translate |
Does NOT end with :fx |
Auto-Detection Logic:
def _detect_api_type(self, api_key: str) -> str:
"""Detect if API key is Free or Pro based on suffix."""
if api_key.endswith(":fx"):
return "free"
return "pro"
def _get_api_url(self) -> str:
"""Get correct API URL based on key type."""
if self._api_type == "free":
return "https://api-free.deepl.com/v2/translate"
return "https://api.deepl.com/v2/translate"
Free Tier Limits:
- 500,000 characters/month
- Rate limit: ~5 requests/second
Pro Tier:
- Pay per character (~€25 per million characters)
- Higher rate limits
- Priority support
Supported Languages (DeepL)
DeepL supports fewer languages than Google but with higher quality for European languages:
Supported (as of 2024):
- BG (Bulgarian), CS (Czech), DA (Danish), DE (German), EL (Greek)
- EN-GB/EN-US (English), ES (Spanish), ET (Estonian), FI (Finnish)
- FR (French), HU (Hungarian), ID (Indonesian), IT (Italian), JA (Japanese)
- KO (Korean), LT (Lithuanian), LV (Latvian), NB (Norwegian Bokmål)
- NL (Dutch), PL (Polish), PT-BR/PT-PT (Portuguese), RO (Romanian)
- RU (Russian), SK (Slovak), SL (Slovenian), SV (Swedish), TR (Turkish)
- UK (Ukrainian), ZH (Chinese)
Key Differences from Google:
- No auto-detect for source language code "auto" - use
Noneor omit - Language codes are case-sensitive (uppercase)
- English has two variants: EN-GB, EN-US
- Portuguese has two variants: PT-BR, PT-PT
Architecture Compliance
Per _bmad-output/planning-artifacts/architecture.md:
Error Format:
{
"error": "DEEPL_QUOTA_EXCEEDED",
"message": "Quota DeepL dépassé. Réessayez demain.",
"details": {
"provider": "deepl",
"api_type": "free",
"reset_at": "2024-01-16T00:00:00Z"
}
}
Never return HTTP 500 - All errors must be 4xx or 502 (upstream error).
Naming Conventions:
- File:
deepl_provider.py(snake_case) - Class:
DeepLTranslationProvider(PascalCase) - Error codes:
DEEPL_*(UPPER_SNAKE_CASE) - JSON fields: snake_case
Previous Story Intelligence (Story 2.2 - Google Translate)
What Worked Well:
deep_translatorlibrary integration (no API key management for Google)- Thread-safe translator instances per thread
- Error codes with
to_dict()method - Retry logic with exponential backoff
- Health check with 60s TTL caching
Patterns to Reuse:
# Error codes pattern
DEEPL_QUOTA_EXCEEDED = "DEEPL_QUOTA_EXCEEDED"
DEEPL_INVALID_KEY = "DEEPL_INVALID_KEY"
DEEPL_NETWORK_ERROR = "DEEPL_NETWORK_ERROR"
DEEPL_UNSUPPORTED_LANGUAGE = "DEEPL_UNSUPPORTED_LANGUAGE"
DEEPL_TEXT_TOO_LONG = "DEEPL_TEXT_TOO_LONG"
_RETRYABLE_ERRORS = {DEEPL_NETWORK_ERROR, DEEPL_QUOTA_EXCEEDED}
# Exception class pattern
class DeepLProviderError(Exception):
def __init__(self, code: str, message: str, details: Optional[Dict[str, Any]] = None):
self.code = code
self.message = message
self.details = details or {}
super().__init__(message)
def to_dict(self) -> Dict[str, Any]:
result = {"error": self.code, "message": self.message}
if self.details:
result["details"] = self.details
return result
Key Difference for DeepL:
Unlike Google (which uses deep_translator without API key), DeepL requires an API key passed to deep_translator.DeepLTranslator:
from deep_translator import DeepLTranslator
# Free tier
translator = DeepLTranslator(api_key="your-key:fx", source="en", target="fr")
# Pro tier (same library, different endpoint internally detected)
translator = DeepLTranslator(api_key="your-pro-key", source="en", target="fr")
Note: deep_translator handles Free vs Pro endpoint detection internally based on API key format!
File Structure
Files to Create:
services/providers/deepl_provider.py- Main provider implementationtests/test_providers/test_deepl_provider.py- Unit tests
Files to Modify:
services/providers/__init__.py- Add DeepL auto-registration.env.example- Add DeepL-specific config (if not present)services/providers/README.md- Add DeepL documentation
Error Codes to Implement
| Code | HTTP | Scenario | Message Template |
|---|---|---|---|
DEEPL_QUOTA_EXCEEDED |
429 | Character quota exceeded | "Quota DeepL dépassé. Réessayez demain." |
DEEPL_INVALID_KEY |
401 | Invalid API key | "Clé API DeepL invalide. Contactez l'administrateur." |
DEEPL_NETWORK_ERROR |
502 | Network/timeout error | "Service DeepL indisponible. Réessayez." |
DEEPL_UNSUPPORTED_LANGUAGE |
400 | Language not supported | "Langue '{lang}' non supportée par DeepL." |
DEEPL_TEXT_TOO_LONG |
413 | Text exceeds limit | "Texte trop long (max 128KB par requête)." |
Configuration
Environment Variables (.env.example):
# DeepL Provider
DEEPL_ENABLED=true
DEEPL_API_KEY=your_deepl_api_key_here # Free keys end with :fx
DEEPL_TIMEOUT=30
DEEPL_MAX_RETRIES=3
DEEPL_RETRY_DELAY=1
Provider Config (services/providers/config.py):
Already has basic config. May need to add:
DEEPL_TIMEOUTDEEPL_MAX_RETRIESDEEPL_RETRY_DELAY
Testing Strategy
Unit Tests (Mocked):
- Free vs Pro API key detection
- All error scenarios (quota, invalid key, timeout)
- Health check logic
- Retry logic
- Caching behavior
- Registry integration
Integration Tests (Optional):
- With
DEEPL_API_KEYin environment: real API calls - Without API key: skip integration tests
- Use pytest markers:
@pytest.mark.integration
Test Commands:
# Unit tests only
pytest tests/test_providers/test_deepl_provider.py -v
# All provider tests
pytest tests/test_providers/ -v
# With coverage
pytest tests/test_providers/ --cov=services/providers -v
Logging Pattern
import logging
logger = logging.getLogger(__name__)
# Good - metadata only (NO document content)
logger.info(
f"deepl_translation_success chars={len(text)} "
f"source_lang={source_language} target_lang={target_language} "
f"api_type={self._api_type}"
)
logger.error(
f"deepl_translation_failed error_code={error.code} "
f"text_length={len(text)} source_lang={source_language} "
f"target_lang={target_language}"
)
Dependencies
Internal:
services/providers/base.py- TranslationProvider abstract classservices/providers/registry.py- ProviderRegistryservices/providers/config.py- Configurationservices/providers/schemas.py- TranslationRequest/Response models
External:
deep_translator- DeepL integration (already installed for Google)structlogor standardlogging- Structured logging
References
- [Source: _bmad-output/planning-artifacts/architecture.md#Error Handling]
- [Source: _bmad-output/planning-artifacts/architecture.md#API Response Formats]
- [Source: _bmad-output/planning-artifacts/epics.md#Story 2.3]
- [Source: _bmad-output/planning-artifacts/prd.md#FR6 Google/DeepL providers]
- [Source: _bmad-output/planning-artifacts/prd.md#NFR12 Zero HTTP 500 errors]
- [Source: _bmad-output/planning-artifacts/prd.md#NFR13 Provider fallback]
- [Source: _bmad-output/implementation-artifacts/2-2-provider-google-translate.md]
- [Source: services/providers/google_provider.py - Implementation pattern]
- [Source: services/providers/registry.py - Registration pattern]
Security Considerations
API Key Protection:
- Never log API key
- Load from environment variable only
- Validate key format on startup (basic check for
:fxsuffix)
Data Privacy:
- Never log document content (NFR11)
- Only log metadata: text length, languages, timestamps
- Clear cache entries after TTL (security + privacy)
Dev Agent Record
Agent Model Used
Claude (GLM-5) via opencode
Debug Log References
- Fixed logging compatibility issue: standard logging doesn't support keyword arguments like structlog
- Created helper functions
_log_info,_log_warning,_log_errorto bridge the gap
Completion Notes List
- ✅ Implemented
DeepLTranslationProviderclass with all required features - ✅ Auto-detection of Free/Pro API endpoints based on key format (
:fxsuffix) - ✅ All 5 error codes implemented with French messages
- ✅ Retry logic with exponential backoff for
DEEPL_NETWORK_ERRORandDEEPL_QUOTA_EXCEEDED - ✅ Health check with 60s TTL caching
- ✅ Registry integration with auto-registration when
DEEPL_ENABLED=trueandDEEPL_API_KEYis set - ✅ Language code normalization (uppercase, EN→EN-US, PT→PT-BR)
- ✅ Unit tests created (incl. timeout→DEEPL_NETWORK_ERROR, error_details for TEXT_TOO_LONG)
- ✅ Documentation updated in README.md with DeepL section
- ✅ [Code review] main.py uses
get_legacy_deepl_adapter()so API uses new provider;is_available()performs minimal translate to verify API reachable;get_deepl_provider()thread-safe; DeepL error codes mapped in utils/exceptions.py
File List
Files Created:
services/providers/deepl_provider.py- Main DeepL provider implementationtests/test_providers/test_deepl_provider.py- Unit tests (incl. timeout/error_details)
Files Modified:
services/providers/__init__.py- Added DeepL auto-registrationservices/providers/config.py- Added DEEPL_TIMEOUT, DEEPL_MAX_RETRIES, DEEPL_RETRY_DELAYservices/providers/README.md- Added comprehensive DeepL documentation.env.example- Added DeepL-specific configuration optionsmain.py- Useget_legacy_deepl_adapter()for DeepL (code review)utils/exceptions.py- Added DEEPL_* error codes to HTTP status mapping (code review)
Change Log
- 2026-02-21: Story 2.3 implementation complete - DeepL provider with Free/Pro detection, error handling, and 35 passing tests
- 2026-02-21: Code review fixes – main.py uses legacy DeepL adapter; is_available() does minimal API check; thread-safe singleton; timeout tests; DeepL HTTP status mapping in utils/exceptions.py