Contract: AnalysesTranslationRepository + TagTranslationRepository + ArticleTranslationRepository
Feature: 004-translation | Date: 2026-05-29
AnalysesTranslationRepository (Domain Interface)
python
class AnalysesTranslationRepository(ABC):
@abstractmethod
def save(self, content: AnalysesContent) -> AnalysesContent:
"""Upsert: update if (analysis_id, language) exists, insert otherwise."""
@abstractmethod
def find_by_analysis_id_and_language(self, analysis_id: UUID, language: str) -> Optional[AnalysesContent]:
"""Return the translation for a specific analysis and language, or None."""
@abstractmethod
def exists(self, analysis_id: UUID, language: str) -> bool:
"""Return True if a translation exists for (analysis_id, language)."""
@abstractmethod
def find_analyses_without_translation(self, language: str, limit: int) -> list:
"""Return analyses that have no translation row for the given language,
up to `limit` results."""Behavioral Guarantees
save()is idempotent — calling it twice with the same(analysis_id, language)does not create duplicates.exists()is a lightweight check (COUNT query) — does not fetch content.find_analyses_without_translation()excludes analyses that already have a translation in the target language.
TagTranslationRepository (Domain Interface)
python
class TagTranslationRepository(ABC):
@abstractmethod
def save_tag_translation(self, tag_id: UUID, language: str, name: str) -> None:
"""Upsert: update if (tag_id, language) exists, insert otherwise."""
@abstractmethod
def find_tags_without_translation(self, language: str, limit: int) -> list:
"""Return tags that have no translation row for the given language,
up to `limit` results. Each element includes tag_id and name."""
@abstractmethod
def save_group_translation(self, group_id: UUID, language: str, display_name: str, description: Optional[str]) -> None:
"""Upsert: update if (group_id, language) exists, insert otherwise."""
@abstractmethod
def find_groups_without_translation(self, language: str, limit: int) -> list:
"""Return tag groups that have no translation row for the given language,
up to `limit` results. Each element includes group_id, display_name, and description."""Behavioral Guarantees
- Both
save_*methods are idempotent upserts. find_tags_without_translation()uses~Tag.translations.any(language == target)filter.find_groups_without_translation()uses~TagGroupDefinition.translations.any(language == target)filter.
ArticleTranslationRepository (Domain Interface)
python
class ArticleTranslationRepository(ABC):
@abstractmethod
def save(self, article_id: UUID, language: str, title: str, content: Optional[str]) -> None:
"""Upsert: update if (article_id, language) exists, insert otherwise."""
@abstractmethod
def find_by_article_id_and_language(self, article_id: UUID, language: str) -> Optional[ArticleTranslationContent]:
"""Return the translation for a specific article and language, or None."""
@abstractmethod
def exists(self, article_id: UUID, language: str) -> bool:
"""Return True if a translation exists for (article_id, language)."""
@abstractmethod
def find_articles_without_translation(self, language: str, limit: int) -> list:
"""Return articles that have no translation row for the given language,
up to `limit` results. Each element includes article_id, title, and content."""Behavioral Guarantees
save()is idempotent — calling it twice with the same(article_id, language)does not create duplicates.exists()is a lightweight check (COUNT query) — does not fetch content.find_articles_without_translation()excludes articles that already have a translation in the target language. Returns original English title and content as source for the translation call.find_by_article_id_and_language()returnsNonewhen no translation exists (not an error).