Tasks: Translation
Input: Design documents from /specs/004-translation/
Prerequisites: plan.md (required), spec.md (required for user stories), data-model.md, contracts/
Tests: Every tasks.md MUST include at least one dedicated test phase. Tests are NOT optional — omitting test tasks violates the project constitution (§III).
Organization: Tasks are grouped by user story to enable independent implementation and testing of each story. The feature is mixed brownfield/greenfield:
- Brownfield (analysis/tag/group translation): already implemented — tasks are test verification only
- Greenfield (article title/content translation): new production code + tests across all DDD layers
Format: [ID] [P?] [Story] Description
- [P]: Can run in parallel (different files, no dependencies)
- [Story]: Which user story this task belongs to (e.g., US1, US2, US3)
- Include exact file paths in descriptions
Phase 1: Setup (Shared Infrastructure)
Purpose: Create test infrastructure shared across all phases
- [ ] T001 Create test directory structure:
src/tests/unit/modules/intelligence/application/,src/tests/unit/modules/intelligence/domain/,src/tests/integration/intelligence/ - [ ] T002 [P] Create shared unit test fixtures in
src/tests/unit/modules/intelligence/application/conftest.py— mockLLMService,AnalysesTranslationRepository,TagTranslationRepository,ArticleTranslationRepository, prompt factories - [X] T003 [P] Create integration test conftest in
src/tests/integration/intelligence/conftest.py— extend base integration conftest withanalyses_translation,tags_translation,tag_group_definitions_translation,articles_translationtable setup and per-test rollback
Checkpoint: Test infrastructure ready — all phases can begin
Phase 2: Foundational (Blocking Prerequisites)
Purpose: Unit tests for domain value objects that all phases depend on
⚠️ CRITICAL: No user story test phase can begin until this phase is complete
- [ ] T004 [P] Test
ArticleTranslationPrompt.render()substitutes all placeholders (__SUMMARY__,__PAIN_POINTS__,__INSIGHTS__,__INNOVATIONS__,__TARGET_LANGUAGE__) and maps language codes to display names insrc/tests/unit/modules/intelligence/domain/test_translation_prompt.py - [X] T005 [P] Test
ArticleBodyTranslationPrompt.render()substitutes__TARGET_LANGUAGE__,__TITLE__,__CONTENT__andparse_response()splits LLM output into Title/Content sections correctly insrc/tests/unit/modules/intelligence/domain/test_translation_prompt.py - [ ] T006 [P] Test
TagTranslationPrompt.render()substitutes__TARGET_LANGUAGE__and__TAGS__insrc/tests/unit/modules/intelligence/domain/test_translation_prompt.py - [ ] T007 [P] Test
GroupTranslationPrompt.render()substitutes__TARGET_LANGUAGE__and__GROUPS__, andformat_group()formats "display_name | description" insrc/tests/unit/modules/intelligence/domain/test_translation_prompt.py - [ ] T008 [P] Test
LANGUAGE_NAMESmapping returns display name for known codes and raw code for unknown codes insrc/tests/unit/modules/intelligence/domain/test_translation_prompt.py
Checkpoint: Domain value object tests pass — user story phases can begin
Phase 3: Greenfield — Article Body Translation (Production Code)
Purpose: Implement all new production code for article title/content translation
⚠️ CRITICAL: Phase 4 US1 unit tests, Phase 5 US2 implementation, and Phase 6 US3 integration tests all depend on this phase completing first
Data Layer
- [X] T009 Create
ArticleTranslationORM model inmodels/article_translation.py— columns: id (UUID PK), article_id (UUID FK → articles.id, CASCADE DELETE), language (VARCHAR 10), title (TEXT NOT NULL), content (TEXT nullable), created_at, updated_at; unique constraint on (article_id, language) - [X] T010 Create Alembic migration
alembic/versions/20_add_article_translation.py— createarticles_translationtable with all columns and constraints from data-model.md; runmake migratelocally to verify
Domain Layer
- [X] T011 [P] Create
ArticleTranslationdomain entity insrc/modules/intelligence/domain/entities/article_translation.py— fields: id, article_id, language, title, content, created_at, updated_at - [X] T012 [P] Add
ArticleBodyTranslationContentdataclass (title: Optional[str], content: Optional[str]) andArticleBodyTranslationResultdataclass (article_id, language, content, success) tosrc/modules/intelligence/domain/value_objects/analyses_translation_content.py - [X] T013 [P] Add
ArticleBodyTranslationPrompttosrc/modules/intelligence/domain/value_objects/translation_prompt.py— placeholders:__TARGET_LANGUAGE__,__TITLE__,__CONTENT__;render(target_language, title, content)returns new instance;parse_response()splits LLM output by "Title:" / "Content:" section headers - [X] T014 Create
ArticleTranslationRepositoryABC insrc/modules/intelligence/domain/repositories/article_translation_repository.py— methods:save(article_id, language, title, content),find_by_article_id_and_language(article_id, language) -> Optional[ArticleBodyTranslationContent],exists(article_id, language) -> bool,find_articles_without_translation(language, limit) -> list
Application Layer
- [X] T015 Create
TranslateArticleBodyUseCaseinsrc/modules/intelligence/application/use_cases/translate_article_body.py—execute(article_id, title, content, target_language): checkrepository.exists()for dedup; substitute empty content with "(empty)"; renderArticleBodyTranslationPrompt; callLLMService.translate(); parse response into title + content sections; callrepository.save(); returnArticleBodyTranslationResult - [X] T016 Extend
TagNormalizationCompletedEventinsrc/modules/intelligence/application/events/tag_normalization_completed.py— addarticle_title: strandarticle_content: strfields (with empty-string defaults for backward compatibility) - [X] T017 Update
TagNormalizationHandlerinsrc/modules/intelligence/application/event_handlers/tag_normalization_handler.py— inject a read-only article query (via SQLAlchemyArticlemodel or a minimalArticleReadRepositoryinterface) to fetcharticle.titleandarticle.contentbyevent.article_id; populate both fields inTagNormalizationCompletedEventbefore publishing - [X] T018 Inject
TranslateArticleBodyUseCaseintoAnalysisCompletedHandlerinsrc/modules/intelligence/application/event_handlers/analysis_completed_handler.py— calltranslate_body_uc.execute(article_id=event.article_id, title=event.article_title, content=event.article_content, target_language=lang)per language inside the existing per-language loop; publishTranslationFailedEventon failure (task_type="translate_article_body")
Infrastructure Layer
- [X] T019 Create
SqlAlchemyArticleTranslationRepositoryinsrc/infrastructure/persistence/intelligence/article_translation_repo_impl.py— implementsArticleTranslationRepositoryABC;save()uses upsert on(article_id, language);find_by_article_id_and_language()maps ORM row toArticleBodyTranslationContent;find_articles_without_translation()left-joinsArticlewithArticleTranslationfiltered by language and limit
Wiring & Backend
- [X] T020 Update
src/bootstrap.py— wireSqlAlchemyArticleTranslationRepository, instantiateTranslateArticleBodyUseCasewith LLM service and repo, inject intoAnalysisCompletedHandler; also inject article query capability intoTagNormalizationHandler - [X] T021 [P] Add
translated_title: Optional[str]andtranslated_content: Optional[str]toArticleDetailOutinbackend/schemas/article.py - [X] T022 [P] Update articles detail endpoint in
backend/routers/articles.py— after fetching the article, queryArticleTranslationby (article_id, lang) where lang comes from the request'slangquery param orAccept-Languageheader; populatetranslated_titleandtranslated_contentif a row exists
Checkpoint: All greenfield production code complete — article body translation is functional end-to-end
Phase 4: User Story 1 — Auto-translation after analysis (Priority: P1) 🎯 MVP
Goal: Verify that AnalysisCompletedHandler triggers article analysis, article body, and tag/group translation for each configured language, and that failures produce TranslationFailedEvent.
Independent Test: Mock TagNormalizationCompletedEvent with article_title and article_content, invoke handler, assert all three use cases are called per language, and TranslationFailedEvent published on failure.
Unit Tests for User Story 1
- [ ] T023 [P] [US1] Test
TranslateArticleUseCase.execute()returns existing translation without calling LLM whenrepository.exists()is True insrc/tests/unit/modules/intelligence/application/test_translate_article_use_case.py - [ ] T024 [P] [US1] Test
TranslateArticleUseCase.execute()calls LLM, parses response into 4 sections (Summary/Pain Points/Insights/Innovations), and persists result insrc/tests/unit/modules/intelligence/application/test_translate_article_use_case.py - [ ] T025 [P] [US1] Test
TranslateArticleUseCase.execute()returnssuccess=Falsewith empty content when LLM returns None insrc/tests/unit/modules/intelligence/application/test_translate_article_use_case.py - [ ] T026 [P] [US1] Test
TranslateArticleUseCase.execute()substitutes empty source fields with "(empty)" in prompt render call insrc/tests/unit/modules/intelligence/application/test_translate_article_use_case.py - [ ] T027 [P] [US1] Test
TranslateArticleUseCase._parse_sections()handles: full response, missing sections, full-width colons, case-insensitive headers insrc/tests/unit/modules/intelligence/application/test_translate_article_use_case.py - [X] T028 [P] [US1] Test
TranslateArticleBodyUseCase.execute()returns existing translation without calling LLM whenrepository.exists()is True insrc/tests/unit/modules/intelligence/application/test_translate_article_body_use_case.py - [X] T029 [P] [US1] Test
TranslateArticleBodyUseCase.execute()calls LLM, parses response into Title + Content sections, and persists result insrc/tests/unit/modules/intelligence/application/test_translate_article_body_use_case.py - [X] T030 [P] [US1] Test
TranslateArticleBodyUseCase.execute()returnssuccess=Falsewith empty content when LLM returns None insrc/tests/unit/modules/intelligence/application/test_translate_article_body_use_case.py - [X] T031 [P] [US1] Test
TranslateArticleBodyUseCase.execute()substitutes empty content with "(empty)" in prompt render call insrc/tests/unit/modules/intelligence/application/test_translate_article_body_use_case.py - [ ] T032 [P] [US1] Test
TranslateTagsUseCase.translate_tags()finds untranslated tags, calls LLM, saves positional matches, returns{total, success, failed}insrc/tests/unit/modules/intelligence/application/test_translate_tags_use_case.py - [ ] T033 [P] [US1] Test
TranslateTagsUseCase.translate_tags()counts unmatched lines (fewer LLM lines than tags) as failures insrc/tests/unit/modules/intelligence/application/test_translate_tags_use_case.py - [ ] T034 [P] [US1] Test
TranslateTagsUseCase.translate_groups()parses pipe-delimited lines into display_name + description, handles missing description insrc/tests/unit/modules/intelligence/application/test_translate_tags_use_case.py - [X] T035 [US1] Test
AnalysisCompletedHandler.handle()callstranslate_article_uc.execute(),translate_body_uc.execute(),translate_tags_uc.translate_tags(), andtranslate_groups()for each language insrc/tests/unit/modules/intelligence/application/test_analysis_completed_handler.py - [X] T036 [US1] Test
AnalysisCompletedHandler.handle()skips analysis translation and logs warning when English content row is missing, but still callstranslate_body_uc.execute()insrc/tests/unit/modules/intelligence/application/test_analysis_completed_handler.py - [X] T037 [US1] Test
AnalysisCompletedHandler.handle()publishesTranslationFailedEventwith task_type="translate_article" when analysis translation returnssuccess=Falseinsrc/tests/unit/modules/intelligence/application/test_analysis_completed_handler.py - [X] T038 [US1] Test
AnalysisCompletedHandler.handle()publishesTranslationFailedEventwith task_type="translate_article_body" when body translation returnssuccess=Falseinsrc/tests/unit/modules/intelligence/application/test_analysis_completed_handler.py
Checkpoint: User Story 1 unit tests pass — auto-translation behavior verified
Phase 5: User Story 2 — Manual batch translation via CLI (Priority: P2)
Goal: Verify that the CLI translate command handles article body batch translation, validates language, and calls all four translation paths (analysis, body, tags, groups).
Independent Test: Run CLI with mocked pipeline, verify article body use case is called per untranslated article, verify backend returns translated_title/translated_content when lang matches.
CLI + Backend Changes
- [X] T039 [US2] Update
src/entrypoints/cli/translate.py— after existing article/tag/group batch loops, add article body batch: callarticle_translation_repo.find_articles_without_translation(language, limit)andtranslate_body_uc.execute()for each result
Unit Tests for User Story 2
- [ ] T040 [P] [US2] Test
build_translation_pipeline()insrc/bootstrap.pyreturns dict withtranslate_body_uc(TranslateArticleBodyUseCase) andarticle_translation_repositoryinsrc/tests/unit/test_bootstrap.py - [ ] T041 [P] [US2] Test CLI
translate.pyvalidates language againstLANGUAGE_NAMESkeys, exits with error for unsupported code insrc/tests/unit/entrypoints/cli/test_translate_cli.py - [ ] T042 [P] [US2] Test CLI
translate.pycallstranslate_body_uc.execute()for each article returned byfind_articles_without_translation(language, limit)insrc/tests/unit/entrypoints/cli/test_translate_cli.py - [ ] T043 [P] [US2] Test CLI
translate.pyfetches untranslated analyses and callstranslate_use_case.execute()for each insrc/tests/unit/entrypoints/cli/test_translate_cli.py - [ ] T044 [P] [US2] Test CLI
translate.pycallstag_translate_use_case.translate_tags()andtranslate_groups()with same language and limit insrc/tests/unit/entrypoints/cli/test_translate_cli.py - [ ] T045 [P] [US2] Test
ArticleDetailOutincludestranslated_titleandtranslated_contentfields, and backend articles detail endpoint returns them populated when a matchingArticleTranslationrow exists inbackend/tests/test_articles_router.py
Checkpoint: User Story 2 unit tests pass — CLI and API behavior verified
Phase 6: User Story 3 — Translation deduplication (Priority: P3)
Goal: Verify that all four repositories apply deduplication correctly and that upsert semantics prevent duplicates.
Independent Test: Save a translation twice with the same key; verify one row exists, content is updated, and no LLM call is made on second execute() invocation.
Integration Tests for User Story 3
- [ ] T046 [P] [US3] Test
SqlAlchemyAnalysesTranslationRepository.exists()returns False for new pair, True aftersave()insrc/tests/integration/intelligence/test_translate_article_integration.py - [ ] T047 [P] [US3] Test
SqlAlchemyAnalysesTranslationRepository.save()upserts — second save with same (analysis_id, language) updates content, no duplicate row insrc/tests/integration/intelligence/test_translate_article_integration.py - [ ] T048 [P] [US3] Test
SqlAlchemyAnalysesTranslationRepository.find_analyses_without_translation()excludes analyses that already have a translation in the target language insrc/tests/integration/intelligence/test_translate_article_integration.py - [X] T049 [P] [US3] Test
SqlAlchemyArticleTranslationRepository.exists()returns False for new pair, True aftersave()insrc/tests/integration/intelligence/test_translate_article_body_integration.py - [X] T050 [P] [US3] Test
SqlAlchemyArticleTranslationRepository.save()upserts — second save with same (article_id, language) updates title + content, no duplicate row insrc/tests/integration/intelligence/test_translate_article_body_integration.py - [X] T051 [P] [US3] Test
SqlAlchemyArticleTranslationRepository.find_articles_without_translation()excludes articles that already have a translation in the target language insrc/tests/integration/intelligence/test_translate_article_body_integration.py - [ ] T052 [P] [US3] Test
SqlAlchemyTagTranslationRepository.save_tag_translation()upserts, andfind_tags_without_translation()excludes already-translated tags insrc/tests/integration/intelligence/test_translate_tags_integration.py - [ ] T053 [P] [US3] Test
SqlAlchemyTagTranslationRepository.save_group_translation()upserts, andfind_groups_without_translation()excludes already-translated groups insrc/tests/integration/intelligence/test_translate_tags_integration.py
Checkpoint: User Story 3 integration tests pass — deduplication and upsert behavior verified
Phase 7: Polish & Cross-Cutting Concerns
Purpose: End-to-end integration coverage and Docker validation
- [ ] T054 Integration test: end-to-end
TranslateArticleUseCase.execute()with real DB — create analysis + English translation, translate to zh-TW, verify row inanalyses_translationinsrc/tests/integration/intelligence/test_translate_article_integration.py - [X] T055 Integration test: end-to-end
TranslateArticleBodyUseCase.execute()with real DB — create article, call execute() with mocked LLM, verify row inarticles_translationinsrc/tests/integration/intelligence/test_translate_article_body_integration.py - [ ] T056 Integration test: end-to-end
TranslateTagsUseCase.translate_tags()+translate_groups()with real DB — verify rows intags_translationandtag_group_definitions_translationinsrc/tests/integration/intelligence/test_translate_tags_integration.py - [ ] T057 Run
make testand verify all translation unit tests pass via Docker - [ ] T058 Run
make test-integrationand verify all translation integration tests pass via Docker - [ ] T059 Run
make migratelocally and verifyarticles_translationtable is created with correct schema - [ ] T060 Validate
make translate LANG=zh-TWtranslates article bodies and reports count in quickstart.md output
Dependencies & Execution Order
Phase Dependencies
- Setup (Phase 1): No dependencies — start immediately
- Foundational (Phase 2): Depends on Phase 1
- Greenfield Production (Phase 3): No test dependencies — can start immediately in parallel with Phase 2; T011–T014 can start in parallel with T009–T010
- US1 (Phase 4): Depends on Phase 2 (prompt tests) AND Phase 3 (production code)
- US2 (Phase 5): Depends on Phase 3 (production code for CLI + backend changes)
- US3 (Phase 6): Depends on Phase 1 (integration conftest) AND Phase 3 (ArticleTranslationRepository impl)
- Polish (Phase 7): Depends on all user story phases complete
User Story Dependencies
- US1 (P1): Depends on Foundational (Phase 2) + Greenfield (Phase 3)
- US2 (P2): Depends on Greenfield (Phase 3) — can run in parallel with US1
- US3 (P3): Depends on Phase 1 + Greenfield (Phase 3) — can run in parallel with US1 and US2
Parallel Opportunities
- T002, T003 can run in parallel (different conftest files)
- T004–T008 can all run in parallel (same file, different test functions)
- T009, T010 can run in parallel (ORM model and migration are independent authoring steps)
- T011–T014 can all run in parallel after T009 (different domain files)
- T015–T018 can start after T014 (use case and event modifications are independent files)
- T019 after T014 (repo impl depends on ABC)
- T020 after T015 + T019 (bootstrap wiring depends on both)
- T021, T022 can run in parallel (schema and router changes in different files)
- T023–T034 can all run in parallel (different test files and functions)
- T040–T045 can all run in parallel (different test files)
- T046–T053 can all run in parallel (different integration test files)
Implementation Strategy
Greenfield First (Phase 3 Before Tests)
- Complete Phase 1: Setup
- Complete Phase 2: Foundational prompt tests — validates new
ArticleBodyTranslationPromptbefore use case depends on it - Complete Phase 3: All greenfield production code (T009–T022) — MVP pipeline is functional
- STOP and VALIDATE: Run
make migrate→make test→ confirm pipeline runs end-to-end - Add Phase 4 US1 tests → verify full auto-translation behavior
- Add Phase 5 US2 tests + backend changes → verify CLI and API delivery
- Add Phase 6 US3 integration tests → verify deduplication
- Polish
MVP Scope
Complete Phases 1–4 only. This delivers:
- Article body translation in the auto-pipeline
- Unit test coverage for all translation use cases
- Verified
make testpassing
Notes
- All integration tests MUST use
@pytest.mark.integrationwith isolated schema and per-test rollback (Constitution III) - Unit tests MUST NOT require a running database — mock all repo/LLM dependencies
- All test runs MUST execute inside Docker via
make test/make test-integration(Constitution III) TagNormalizationHandlermodification (T017) is the only change outside theintelligencemodule — it requires fetchingArticle.titleandArticle.contentbyarticle_id; use a direct SQLAlchemy query or a minimal read-only interface; do not couple the intelligence domain to the collection domain- New
TranslationFailedEventfor body translation failures uses task_type="translate_article_body" to distinguish from analysis failures (task_type="translate_article") - Migration T010 MUST be verified locally with
make migratebefore the Polish phase - [P] tasks = different files, no dependencies
- [Story] label maps task to specific user story for traceability