# 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 1. **AC1: API Integration** - Given `DEEPL_API_KEY` is configured, when `DeepLProvider.translate_text()` is called, then text is translated using DeepL API 2. **AC2: Auto-Detection Free/Pro** - Provider automatically detects Free vs Pro API endpoint based on API key format (Free keys end with `:fx`) 3. **AC3: Graceful Error Handling** - All API errors (quota exceeded, invalid key, network timeout) return structured errors with code and message (never HTTP 500) 4. **AC4: Health Check** - Provider `is_available()` returns `True` when API key is configured and API is reachable, `False` otherwise 5. **AC5: Registry Integration** - Provider is registered in `ProviderRegistry` and appears in fallback chain 6. **AC6: Unit Tests** - Tests verify all error scenarios, Free/Pro detection, and mock API responses ## Tasks / Subtasks - [x] **Task 1: Create DeepL Provider Implementation** (AC: 1, 2) - [x] 1.1 Create `services/providers/deepl_provider.py` - [x] 1.2 Implement `DeepLProvider` class extending `TranslationProvider` - [x] 1.3 Implement `_detect_api_type()` to auto-detect Free vs Pro from API key - [x] 1.4 Implement `_get_api_url()` to return correct endpoint based on type - [x] 1.5 Use `deep_translator` library for DeepL integration (same pattern as Google) - [x] **Task 2: Implement Error Handling** (AC: 3) - [x] 2.1 Define error codes: `DEEPL_QUOTA_EXCEEDED`, `DEEPL_INVALID_KEY`, `DEEPL_NETWORK_ERROR`, `DEEPL_UNSUPPORTED_LANGUAGE`, `DEEPL_TEXT_TOO_LONG` - [x] 2.2 Implement `DeepLProviderError` exception class - [x] 2.3 Map DeepL API errors to structured error responses - [x] 2.4 Add retry logic with exponential backoff for transient errors - [x] 2.5 Add timeout configuration (default 30s) - [x] 2.6 Ensure all errors return JSON: `{error, message, details?}` format - [x] **Task 3: Implement Health Check** (AC: 4) - [x] 3.1 Implement `is_available()` to check API key presence - [x] 3.2 Add `health_check()` with caching (TTL 60s) matching Google provider pattern - [x] 3.3 Return `ProviderHealthStatus` with availability and latency - [x] **Task 4: Registry Integration** (AC: 5) - [x] 4.1 Add `register_deepl_provider()` function - [x] 4.2 Add `get_deepl_provider()` singleton function - [x] 4.3 Update `services/providers/__init__.py` to auto-register DeepL when enabled - [x] 4.4 Verify provider appears in fallback chain when configured - [x] **Task 5: Configuration Updates** (AC: 1, 2) - [x] 5.1 Verify `DEEPL_API_KEY` and `DEEPL_ENABLED` in `config.py` (already present) - [x] 5.2 Add DeepL-specific configuration options to `.env.example`: - `DEEPL_TIMEOUT=30` - `DEEPL_MAX_RETRIES=3` - `DEEPL_RETRY_DELAY=1` - [x] **Task 6: Create Unit Tests** (AC: 6) - [x] 6.1 Create `tests/test_providers/test_deepl_provider.py` - [x] 6.2 Test Free vs Pro API key detection - [x] 6.3 Test all error scenarios (quota, invalid key, timeout, unsupported language) - [x] 6.4 Test retry logic - [x] 6.5 Test health check functionality - [x] 6.6 Test registry integration - [x] **Task 7: Update Documentation** (AC: 1-6) - [x] 7.1 Update `services/providers/README.md` with DeepL section - [x] 7.2 Document Free vs Pro API key differences - [x] 7.3 Document supported languages (fewer than Google but higher quality) ## 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:** ```python 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 `None` or 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:** ```json { "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_translator` library 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:** ```python # 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`: ```python 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 implementation - `tests/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`):** ```bash # 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_TIMEOUT` - `DEEPL_MAX_RETRIES` - `DEEPL_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_KEY` in environment: real API calls - Without API key: skip integration tests - Use pytest markers: `@pytest.mark.integration` **Test Commands:** ```bash # 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 ```python 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 class - `services/providers/registry.py` - ProviderRegistry - `services/providers/config.py` - Configuration - `services/providers/schemas.py` - TranslationRequest/Response models **External:** - `deep_translator` - DeepL integration (already installed for Google) - `structlog` or standard `logging` - 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 `:fx` suffix) **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_error` to bridge the gap ### Completion Notes List - ✅ Implemented `DeepLTranslationProvider` class with all required features - ✅ Auto-detection of Free/Pro API endpoints based on key format (`:fx` suffix) - ✅ All 5 error codes implemented with French messages - ✅ Retry logic with exponential backoff for `DEEPL_NETWORK_ERROR` and `DEEPL_QUOTA_EXCEEDED` - ✅ Health check with 60s TTL caching - ✅ Registry integration with auto-registration when `DEEPL_ENABLED=true` and `DEEPL_API_KEY` is 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 implementation - `tests/test_providers/test_deepl_provider.py` - Unit tests (incl. timeout/error_details) **Files Modified:** - `services/providers/__init__.py` - Added DeepL auto-registration - `services/providers/config.py` - Added DEEPL_TIMEOUT, DEEPL_MAX_RETRIES, DEEPL_RETRY_DELAY - `services/providers/README.md` - Added comprehensive DeepL documentation - `.env.example` - Added DeepL-specific configuration options - `main.py` - Use `get_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