Files
office_translator/_bmad-output/implementation-artifacts/2-1-abstraction-provider-base-registry.md
Sepehr Ramezani 26bd096a06 feat: production deployment - full update with providers, admin, glossaries, pricing, tests
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>
2026-04-25 15:01:47 +02:00

14 KiB

Story 2.1: Abstraction Provider (Base + Registry)

Status: done

Story

As a Developer, I want to create an abstract TranslationProvider base class with a ProviderRegistry, so that multiple translation providers can be plugged in with fallback support and clean architecture compliance.

Acceptance Criteria

  1. AC1: Abstract Base Class - TranslationProvider defines abstract methods: translate_text(), get_name(), is_available()
  2. AC2: ProviderRegistry - Registry can register, retrieve, and list providers by name
  3. AC3: Environment Configuration - Providers configured via environment variables (API keys, URLs)
  4. AC4: Health Check - Each provider has is_available() method returning True/False
  5. AC5: Unit Tests - Tests verify provider interface and registry functionality

Tasks / Subtasks

  • Task 1: Create Provider Base Class (AC: 1, 4)

    • 1.1 Create services/providers/__init__.py
    • 1.2 Create services/providers/base.py with abstract TranslationProvider
    • 1.3 Define abstract methods: translate_text(), get_name(), is_available()
    • 1.4 Add optional methods: translate_batch(), health_check()
    • 1.5 Add Pydantic models for request/response in services/providers/schemas.py
  • Task 2: Create Provider Registry (AC: 2)

    • 2.1 Create services/providers/registry.py with ProviderRegistry class
    • 2.2 Implement register(name, provider_class) method
    • 2.3 Implement get(name) -> TranslationProvider method
    • 2.4 Implement list_available() -> List[str] method
    • 2.5 Implement get_first_available(names: List[str]) -> TranslationProvider for fallback
    • 2.6 Add singleton pattern for global registry
  • Task 3: Migrate Existing Google Provider (AC: 1, 3, 4)

    • 3.1 Create services/providers/google_provider.py
    • 3.2 Extend TranslationProvider base class
    • 3.3 Implement is_available() using a simple ping/test
    • 3.4 Migrate caching logic from existing GoogleTranslationProvider
    • 3.5 Register in registry on import
  • Task 4: Environment Configuration (AC: 3)

    • 4.1 Add provider settings to config.py (or create services/providers/config.py)
    • 4.2 Load API keys from environment: GOOGLE_API_KEY, DEEPL_API_KEY, OPENAI_API_KEY, OLLAMA_BASE_URL
    • 4.3 Add provider enable/disable flags
    • 4.4 Update .env.example with new variables
  • Task 5: Unit Tests (AC: 5)

    • 5.1 Create tests/test_providers/test_base.py
    • 5.2 Create tests/test_providers/test_registry.py
    • 5.3 Create tests/test_providers/test_google_provider.py
    • 5.4 Test abstract class cannot be instantiated directly
    • 5.5 Test registry registration and retrieval
    • 5.6 Test fallback chain logic

Dev Notes

🚨 BROWNFIELD CONTEXT - CRITICAL

This is a refactoring story. The project already has provider implementations in services/translation_service.py:

  • TranslationProvider (ABC) - lines 116-157
  • GoogleTranslationProvider - lines 159-282
  • DeepLTranslationProvider - lines 285-340
  • OllamaTranslationProvider - lines 397-517
  • OpenAITranslationProvider - lines 667-767
  • OpenRouterTranslationProvider - lines 520-654

Strategy: Extract and reorganize into clean module structure, keeping backward compatibility.

Target Module Structure (from Architecture)

services/
├── providers/
│   ├── __init__.py          # Exports + registry setup
│   ├── base.py              # Abstract TranslationProvider
│   ├── registry.py          # ProviderRegistry singleton
│   ├── schemas.py           # Pydantic request/response models
│   ├── config.py            # Provider settings from env
│   └── google_provider.py   # Google implementation (migrated)
├── translation_service.py   # Keep for now, refactor later
└── ...

Existing Code to Preserve

From services/translation_service.py:

Component Lines Action
TranslationCache 53-113 Keep in services/translation_service.py (used by all providers)
retry_with_backoff 27-50 Keep as utility
TranslationProvider (ABC) 116-157 Refactor to providers/base.py
GoogleTranslationProvider 159-282 Migrate to providers/google_provider.py
Other providers Various Migrate in subsequent stories (2.2-2.5)

Abstract Base Class Design

# services/providers/base.py
from abc import ABC, abstractmethod
from typing import Optional, List
from pydantic import BaseModel

class TranslationRequest(BaseModel):
    text: str
    target_language: str
    source_language: str = "auto"

class TranslationResponse(BaseModel):
    translated_text: str
    provider_name: str
    from_cache: bool = False

class TranslationProvider(ABC):
    """Abstract base class for translation providers."""
    
    @abstractmethod
    def translate_text(self, request: TranslationRequest) -> TranslationResponse:
        """Translate a single text string."""
        pass
    
    @abstractmethod
    def get_name(self) -> str:
        """Return provider name for logging and registry."""
        pass
    
    @abstractmethod
    def is_available(self) -> bool:
        """Check if provider is configured and reachable."""
        pass
    
    def translate_batch(self, requests: List[TranslationRequest]) -> List[TranslationResponse]:
        """Default batch implementation using individual calls."""
        return [self.translate_text(req) for req in requests]
    
    def health_check(self) -> dict:
        """Return health status details."""
        return {
            "name": self.get_name(),
            "available": self.is_available()
        }

Provider Registry Design

# services/providers/registry.py
from typing import Dict, List, Optional, Type
from .base import TranslationProvider

class ProviderRegistry:
    """Singleton registry for translation providers."""
    
    _instance: Optional['ProviderRegistry'] = None
    
    def __new__(cls) -> 'ProviderRegistry':
        if cls._instance is None:
            cls._instance = super().__new__(cls)
            cls._instance._providers: Dict[str, TranslationProvider] = {}
        return cls._instance
    
    def register(self, name: str, provider: TranslationProvider) -> None:
        self._providers[name] = provider
    
    def get(self, name: str) -> Optional[TranslationProvider]:
        return self._providers.get(name)
    
    def list_available(self) -> List[str]:
        return [name for name, p in self._providers.items() if p.is_available()]
    
    def get_first_available(self, names: List[str]) -> Optional[TranslationProvider]:
        """Get first available provider from a list of names (fallback chain)."""
        for name in names:
            provider = self.get(name)
            if provider and provider.is_available():
                return provider
        return None

# Global registry instance
registry = ProviderRegistry()

Environment Variables

# .env.example additions
GOOGLE_TRANSLATE_ENABLED=true
DEEPL_ENABLED=false
DEEPL_API_KEY=
OPENAI_ENABLED=false
OPENAI_API_KEY=
OLLAMA_ENABLED=false
OLLAMA_BASE_URL=http://localhost:11434
OPENROUTER_ENABLED=false
OPENROUTER_API_KEY=

# Provider fallback chain (comma-separated)
PROVIDER_FALLBACK_CHAIN=google,deepl,ollama,openrouter

Backward Compatibility

During transition, keep existing TranslationService in services/translation_service.py working:

# services/translation_service.py (modified)
from services.providers.registry import registry
from services.providers.google_provider import GoogleTranslationProvider

# For backward compatibility, existing code continues to work
# New code uses registry.get("google").translate_text(...)

Files to Create

File Action Description
services/providers/__init__.py CREATE Package init, exports
services/providers/base.py CREATE Abstract TranslationProvider
services/providers/registry.py CREATE ProviderRegistry singleton
services/providers/schemas.py CREATE Pydantic models
services/providers/config.py CREATE Provider settings from env
services/providers/google_provider.py CREATE Google provider (migrated)
tests/test_providers/__init__.py CREATE Test package
tests/test_providers/test_base.py CREATE Base class tests
tests/test_providers/test_registry.py CREATE Registry tests
tests/test_providers/test_google_provider.py CREATE Google provider tests

Files NOT to Modify (Yet)

  • services/translation_service.py - Keep existing classes, we'll deprecate later
  • translators/*.py - File processors, different concern
  • routes/*.py - Will be updated in subsequent stories
  • main.py - No changes in this story

Testing Commands

# Run tests
pytest tests/test_providers/ -v

# Test specific file
pytest tests/test_providers/test_registry.py -v

# With coverage
pytest tests/test_providers/ --cov=services/providers

Project Structure Notes

  • Python files: snake_case (e.g., google_provider.py)
  • Classes: PascalCase (e.g., TranslationProvider, ProviderRegistry)
  • Variables/functions: snake_case
  • Follow existing patterns in services/ folder

Architecture Compliance

Per _bmad-output/planning-artifacts/architecture.md:

  • Uses Clean Architecture pattern (providers as pluggable adapters)
  • Follows naming conventions (snake_case files, PascalCase classes)
  • Configuration via environment variables
  • Async support preparation (future story will add async)

References

  • [Source: _bmad-output/planning-artifacts/architecture.md#Data Architecture]
  • [Source: _bmad-output/planning-artifacts/architecture.md#Implementation Patterns]
  • [Source: _bmad-output/planning-artifacts/epics.md#Story 2.1]
  • [Source: _bmad-output/planning-artifacts/prd.md#FR6-FR7 Translation Providers]
  • [Source: _bmad-output/planning-artifacts/prd.md#NFR12-NFR13 Provider Fallback]

Previous Story Learnings (Epic 1)

From Story 1.1:

  • Use pytest with pytest-asyncio for async tests
  • Create conftest.py with shared fixtures
  • Add __init__.py to test directories
  • Keep backward compatibility during refactoring

Dev Agent Record

Agent Model Used

claude-3-5-sonnet (claude-sonnet-4-20250514)

Debug Log References

None - implementation completed without issues.

Completion Notes List

  • Task 1 Complete: Created services/providers/ module with abstract base class, schemas, and Pydantic models for requests/responses
  • Task 2 Complete: Implemented thread-safe singleton ProviderRegistry with register, get, list_available, and get_first_available methods
  • Task 3 Complete: Migrated Google provider to new architecture, preserving caching support via existing _translation_cache
  • Task 4 Complete: Created services/providers/config.py with environment-based configuration for all providers; updated .env.example
  • Task 5 Complete: 45 unit tests covering base class, registry, and Google provider functionality
  • All 141 tests pass including new provider tests and existing regression tests
  • Backward compatibility maintained: Existing services/translation_service.py unchanged

File List

New Files Created:

  • services/providers/__init__.py
  • services/providers/base.py
  • services/providers/registry.py
  • services/providers/schemas.py
  • services/providers/config.py
  • services/providers/google_provider.py
  • tests/test_providers/__init__.py
  • tests/test_providers/test_base.py
  • tests/test_providers/test_registry.py
  • tests/test_providers/test_google_provider.py

Modified Files:

  • .env.example - Added provider enable flags and fallback chain configuration

Change Log

  • 2026-02-20: Story 2.1 completed - Provider abstraction layer with registry, base class, schemas, config, and Google provider migration. All 45 new tests pass. Total test suite: 141 tests passing.
  • 2026-02-20: Code Review (AI) - Fixed 3 HIGH + 4 MEDIUM issues:
    • [FIXED] Auto-registration of Google provider in __init__.py
    • [FIXED] Added error field to TranslationResponse for silent failure detection
    • [FIXED] Added success property to TranslationResponse
    • [FIXED] Language code validation in TranslationRequest (ISO 639-1 format)
    • [FIXED] Improved dotenv loading in config.py
    • [ADDED] 3 new tests for error handling and validation
    • Total tests: 47 passing

Senior Developer Review (AI)

Review Date: 2026-02-20
Reviewer: AI Code Review (adversarial mode)
Outcome: APPROVED (after fixes)

Issues Found and Resolved

Severity Issue File:Line Status
HIGH Google provider not auto-registered __init__.py:42-50 Fixed
HIGH Silent failure returns untranslated text google_provider.py:135-141 Fixed
HIGH No error indication on translation failure schemas.py:21-36 Fixed
MEDIUM No language code validation schemas.py:9-22 Fixed
MEDIUM load_dotenv side effect at module level config.py:12-19 Fixed
MEDIUM Circular import risk (deferred) google_provider.py:51 ⚠️ Acceptable
MEDIUM No integration tests tests/ ⚠️ Deferred
LOW Class-level lock pattern registry.py:24-25 ⚠️ Acceptable
LOW Two provider instance patterns google_provider.py:178-186 ⚠️ Acceptable

Git vs Story Discrepancy Note

9 modified files not in File List (from Story 1.7 in-progress):

  • alembic/env.py, database/, main.py, models/, requirements.txt, routes/, services/auth_service.py

Test Coverage

  • 47 unit tests for providers (all passing)
  • Coverage: base class, registry, Google provider, schemas, error handling