docs(14-05): complete i18n rollout for tender-radar plan
This commit is contained in:
@@ -53,7 +53,7 @@
|
||||
|
||||
- [x] **CONFIG-01**: Das Modul ist im Marketplace registriert und pro Mandant aktivierbar (wie DKV-Fleet- und Cert-Manager-Modul).
|
||||
- [x] **CONFIG-02**: Admin verwaltet Quellen-Poll-Konfiguration sowie das E-Mail-Ingestion-Postfach pro Mandant (Zugangsdaten verschlüsselt gespeichert).
|
||||
- [ ] **CONFIG-03**: Die gesamte Modul-UI ist mehrsprachig (Deutsch + Englisch, i18n).
|
||||
- [x] **CONFIG-03**: Die gesamte Modul-UI ist mehrsprachig (Deutsch + Englisch, i18n).
|
||||
|
||||
## Future Requirements (deferred)
|
||||
|
||||
@@ -103,7 +103,7 @@
|
||||
| INGEST-04 | Phase 14 | Complete |
|
||||
| INGEST-05 | Phase 14 | Complete |
|
||||
| CONFIG-02 | Phase 14 | Complete |
|
||||
| CONFIG-03 | Phase 14 | Pending |
|
||||
| CONFIG-03 | Phase 14 | Complete |
|
||||
| UI-06 | Phase 14 | Complete |
|
||||
|
||||
**Coverage:** 29/29 v1.1 requirements mapped — no orphans.
|
||||
|
||||
@@ -464,7 +464,7 @@ Plans:
|
||||
4. vergabe24 and aumass are shown in the UI as "manually monitor" with a direct link, instead of appearing as a silent coverage gap
|
||||
5. The entire module UI (results list, filters, saved searches, settings) is fully usable in both German and English
|
||||
|
||||
**Plans**: 4/5 plans executed
|
||||
**Plans**: 5/5 plans executed
|
||||
|
||||
**Wave 1** *(parallel — disjoint files)*
|
||||
|
||||
@@ -481,7 +481,7 @@ Plans:
|
||||
|
||||
**Wave 4** *(blocked on 14-02 + 14-03 + 14-04)*
|
||||
|
||||
- [ ] 14-05-PLAN.md — i18n-Rollout: tenderRadar-Namensraum (DE/EN) + Komponenten auf useTranslations + Key-Parity-Test (CONFIG-03)
|
||||
- [x] 14-05-PLAN.md — i18n-Rollout: tenderRadar-Namensraum (DE/EN) + Komponenten auf useTranslations + Key-Parity-Test (CONFIG-03)
|
||||
|
||||
**UI hint**: yes
|
||||
|
||||
@@ -505,4 +505,4 @@ Phases execute in numeric order: 1 -> 2 -> 3 -> 4 -> 5 -> 6 -> 7 -> 8 -> 9 -> 10
|
||||
| 11. Filter Engine, Results UI & Saved Searches | 6/6 | In Progress| |
|
||||
| 12. Tender Notifications | 4/4 | In Progress| |
|
||||
| 13. Scraping Adapters & Cross-Source Deduplication | 6/6 | In Progress| |
|
||||
| 14. RSS, Email-Alert Ingestion & Module Rollout | 4/5 | In Progress| |
|
||||
| 14. RSS, Email-Alert Ingestion & Module Rollout | 5/5 | In Progress| |
|
||||
|
||||
+10
-8
@@ -5,15 +5,15 @@ milestone_name: Ausschreibungs-Radar
|
||||
current_phase: 14
|
||||
current_phase_name: rss-email-alert-ingestion-module-rollout
|
||||
status: executing
|
||||
stopped_at: Completed 14-04-PLAN.md
|
||||
last_updated: "2026-07-23T12:01:07.009Z"
|
||||
stopped_at: Completed 14-05-PLAN.md (last plan of Phase 14)
|
||||
last_updated: "2026-07-23T12:23:13.422Z"
|
||||
last_activity: 2026-07-23
|
||||
last_activity_desc: Completed 14-02-PLAN.md (RSS ingestion slice)
|
||||
progress:
|
||||
total_phases: 14
|
||||
completed_phases: 12
|
||||
completed_phases: 13
|
||||
total_plans: 67
|
||||
completed_plans: 65
|
||||
completed_plans: 66
|
||||
---
|
||||
|
||||
# Project State
|
||||
@@ -28,11 +28,11 @@ See: .planning/PROJECT.md (updated 2026-07-17)
|
||||
## Current Position
|
||||
|
||||
Phase: 14 (rss-email-alert-ingestion-module-rollout) — EXECUTING
|
||||
Plan: 4 of 5
|
||||
Plan: 5 of 5
|
||||
Status: Plan 14-02 complete
|
||||
Last activity: 2026-07-23 — Completed 14-02-PLAN.md (RSS ingestion slice)
|
||||
|
||||
Progress: [██████████] 97%
|
||||
Progress: [██████████] 99%
|
||||
|
||||
## Performance Metrics
|
||||
|
||||
@@ -99,6 +99,7 @@ Progress: [██████████] 97%
|
||||
| Phase 14 P01 | 13min | 2 tasks | 10 files |
|
||||
| Phase 14 P02 | 30min | 3 tasks | 21 files |
|
||||
| Phase 14 P04 | 25min | 2 tasks | 6 files |
|
||||
| Phase 14 P05 | 50min | 3 tasks | 19 files |
|
||||
|
||||
## Accumulated Context
|
||||
|
||||
@@ -221,6 +222,7 @@ Recent decisions affecting current work:
|
||||
- [Phase ?]: 14-03: email-alert TenderSourcePollConfig seeded isActive=false (no safe default mailbox, unlike RSS's service.bund.de) — framework-ready-activation-deferred
|
||||
- [Phase ?]: PORTAL_URLS typed as Record<(typeof DENYLISTED_PORTALS)[number], string> so the compiler enforces a URL for every denylisted portal (no re-declared set, no silent gap)
|
||||
- [Phase ?]: CoverageBanner's denylist block is independent of the onlyDoe coverage-note condition — component renders when either block has content, not gated behind the DOE-only check
|
||||
- [Phase ?]: 14-05: tenderRadar i18n namespace added; Bundesland/CPV filter option values stay canonical German for backend compatibility, only labels translated; portal display slugs left untranslated as proper nouns
|
||||
|
||||
### Pending Todos
|
||||
|
||||
@@ -260,7 +262,7 @@ Items acknowledged and carried forward from previous milestone close:
|
||||
|
||||
## Session Continuity
|
||||
|
||||
Last session: 2026-07-23T12:01:06.995Z
|
||||
Stopped at: Completed 14-04-PLAN.md
|
||||
Last session: 2026-07-23T12:23:13.408Z
|
||||
Stopped at: Completed 14-05-PLAN.md (last plan of Phase 14)
|
||||
Resume file: None
|
||||
Last activity: 2026-07-14 - Built LDAP per-user exclude/denylist filter (9d1323f), migration applied on live DB, verified via Playwright: sync deactivated 4 excluded service accounts (administrator/krbtgt/guest/dns-ldap), 2 real LDAP users stay active, 0 wrongly created
|
||||
|
||||
@@ -0,0 +1,188 @@
|
||||
---
|
||||
phase: 14-rss-email-alert-ingestion-module-rollout
|
||||
plan: 05
|
||||
subsystem: ui
|
||||
tags: [i18n, next-intl, react, tender-radar]
|
||||
|
||||
# Dependency graph
|
||||
requires:
|
||||
- phase: 14-rss-email-alert-ingestion-module-rollout
|
||||
provides: RssFeedListForm (14-02), EmailAlertConfigForm (14-03), CoverageBanner denylist block (14-04)
|
||||
provides:
|
||||
- tenderRadar i18n namespace (DE + EN) covering the entire tender-radar module UI
|
||||
- de/en key-parity guard test (tenderRadar-parity.spec.ts)
|
||||
- All tender-radar components/pages converted from hardcoded German to useTranslations('tenderRadar')
|
||||
affects: [any future tender-radar UI work, platform-wide i18n rollout for other modules]
|
||||
|
||||
# Tech tracking
|
||||
tech-stack:
|
||||
added: []
|
||||
patterns:
|
||||
- "Module i18n namespace: top-level {moduleName} key in de.json/en.json, grouped by component (results.*, filter.*, savedSearch.*, detail.*, coverage.*, settings.*, sourceConfig.*, rssFeeds.*, emailAlerts.*)"
|
||||
- "de/en key-parity spec: recursively flatten both locale JSON objects under the namespace and assert identical sorted key sets — catches any future one-sided edit"
|
||||
- "Filter option label translation while preserving the backend-matched data value: FilterPanel's Bundesland/CPV constants keep a stable `code` (NUTS-1 prefix / CPV division code) used only as the i18n lookup key; the actual filter `value` sent to the API stays the untranslated canonical string"
|
||||
- "Test mock convention: vi.mock('next-intl', () => ({ useTranslations: () => (key, params) => lookupTable[key] ?? key })) — mirrors apps/web's existing marketplace test convention (tenant-selector.test.tsx)"
|
||||
|
||||
key-files:
|
||||
created:
|
||||
- apps/web/src/messages/tenderRadar-parity.spec.ts
|
||||
modified:
|
||||
- apps/web/src/messages/de.json
|
||||
- apps/web/src/messages/en.json
|
||||
- apps/web/src/app/(portal)/modules/tender-radar/page.tsx
|
||||
- apps/web/src/app/(portal)/modules/tender-radar/components/ResultsList.tsx
|
||||
- apps/web/src/app/(portal)/modules/tender-radar/components/ResultsList.test.tsx
|
||||
- apps/web/src/app/(portal)/modules/tender-radar/components/FilterPanel.tsx
|
||||
- apps/web/src/app/(portal)/modules/tender-radar/components/SavedSearchBar.tsx
|
||||
- apps/web/src/app/(portal)/modules/tender-radar/components/SavedSearchBar.test.tsx
|
||||
- apps/web/src/app/(portal)/modules/tender-radar/components/TenderDetail.tsx
|
||||
- apps/web/src/app/(portal)/modules/tender-radar/components/TenderDetail.test.tsx
|
||||
- apps/web/src/app/(portal)/modules/tender-radar/components/CoverageBanner.tsx
|
||||
- apps/web/src/app/(portal)/modules/tender-radar/components/CoverageBanner.test.tsx
|
||||
- apps/web/src/app/(portal)/modules/tender-radar/settings/page.tsx
|
||||
- apps/web/src/app/(portal)/modules/tender-radar/settings/components/SourceConfigForm.tsx
|
||||
- apps/web/src/app/(portal)/modules/tender-radar/settings/components/SourceConfigForm.test.tsx
|
||||
- apps/web/src/app/(portal)/modules/tender-radar/settings/components/RssFeedListForm.tsx
|
||||
- apps/web/src/app/(portal)/modules/tender-radar/settings/components/RssFeedListForm.test.tsx
|
||||
- apps/web/src/app/(portal)/modules/tender-radar/settings/components/EmailAlertConfigForm.tsx
|
||||
- apps/web/src/app/(portal)/modules/tender-radar/settings/components/EmailAlertConfigForm.test.tsx
|
||||
|
||||
key-decisions:
|
||||
- "Proper-noun portal display labels (DÖE, DTVP, tender24, lhs-vpbw, vergabe.landbw) in TenderDetail.portalLabel() are left as hardcoded string constants, not routed through t() — they are brand/identifier abbreviations, not translatable prose, matching the same treatment protocol names (IMAP, STARTTLS, SSL-TLS) already receive"
|
||||
- "FilterPanel's Bundesland option *value* (the string sent to the backend filter query) stays the canonical German name unchanged — apps/api's nutsPrefixFor() matches against that exact string server-side and D-10 does not touch the API contract; only the rendered *label* is translated, keyed by the NUTS-1 prefix code (DE1..DEG)"
|
||||
- "CPV division labels (44 EU CPV vocabulary division descriptions) were moved into the tenderRadar namespace and translated with official EU CPV English division titles — these are genuine multi-word German prose that would otherwise fail the plan's grep-based German-literal audit even though they were originally embedded as 'constant data' rather than typical UI copy"
|
||||
- "Date formatting (toLocaleDateString('de-DE')) in ResultsList/TenderDetail is left unchanged regardless of locale — plan explicitly scoped this as a 'string-source swap only', not a full locale-aware formatting upgrade"
|
||||
|
||||
requirements-completed: [CONFIG-03]
|
||||
|
||||
coverage:
|
||||
- id: D1
|
||||
description: "tenderRadar i18n namespace added to de.json + en.json with EN translations authored by Claude, enforced by tenderRadar-parity.spec.ts"
|
||||
requirement: "CONFIG-03"
|
||||
verification:
|
||||
- kind: unit
|
||||
ref: "apps/web/src/messages/tenderRadar-parity.spec.ts#tenderRadar i18n namespace — de/en key parity"
|
||||
status: pass
|
||||
human_judgment: false
|
||||
- id: D2
|
||||
description: "Results-side components (page.tsx, ResultsList, FilterPanel, SavedSearchBar, TenderDetail, CoverageBanner) converted to useTranslations('tenderRadar'); no hardcoded user-facing German text remains"
|
||||
requirement: "CONFIG-03"
|
||||
verification:
|
||||
- kind: unit
|
||||
ref: "apps/web/src/app/(portal)/modules/tender-radar/components/ResultsList.test.tsx (8 tests)"
|
||||
status: pass
|
||||
- kind: unit
|
||||
ref: "apps/web/src/app/(portal)/modules/tender-radar/components/SavedSearchBar.test.tsx (9 tests)"
|
||||
status: pass
|
||||
- kind: unit
|
||||
ref: "apps/web/src/app/(portal)/modules/tender-radar/components/TenderDetail.test.tsx (5 tests)"
|
||||
status: pass
|
||||
- kind: unit
|
||||
ref: "apps/web/src/app/(portal)/modules/tender-radar/components/CoverageBanner.test.tsx (4 tests)"
|
||||
status: pass
|
||||
human_judgment: false
|
||||
- id: D3
|
||||
description: "Settings surface (settings/page.tsx, SourceConfigForm, RssFeedListForm, EmailAlertConfigForm) converted to useTranslations('tenderRadar')"
|
||||
requirement: "CONFIG-03"
|
||||
verification:
|
||||
- kind: unit
|
||||
ref: "apps/web/src/app/(portal)/modules/tender-radar/settings/components/SourceConfigForm.test.tsx (4 tests)"
|
||||
status: pass
|
||||
- kind: unit
|
||||
ref: "apps/web/src/app/(portal)/modules/tender-radar/settings/components/RssFeedListForm.test.tsx (6 tests)"
|
||||
status: pass
|
||||
- kind: unit
|
||||
ref: "apps/web/src/app/(portal)/modules/tender-radar/settings/components/EmailAlertConfigForm.test.tsx (5 tests)"
|
||||
status: pass
|
||||
human_judgment: false
|
||||
- id: D4
|
||||
description: "No language-switcher UI element introduced (D-11) — locale continues to follow the existing next-intl NEXT_LOCALE cookie mechanism, unchanged"
|
||||
verification: []
|
||||
human_judgment: true
|
||||
rationale: "Absence-of-a-feature is a structural/visual claim best confirmed by a human scan of the diff rather than an automated assertion; no new UI surface was added anywhere in this plan's file list."
|
||||
|
||||
# Metrics
|
||||
duration: ~50min
|
||||
completed: 2026-07-23
|
||||
status: complete
|
||||
---
|
||||
|
||||
# Phase 14 Plan 05: i18n Rollout for Tender Radar Summary
|
||||
|
||||
**New `tenderRadar` i18n namespace (DE+EN, ~140 keys) with a de/en key-parity guard test; all 10 tender-radar components/pages converted from hardcoded German to `useTranslations('tenderRadar')`.**
|
||||
|
||||
## Performance
|
||||
|
||||
- **Duration:** ~50 min
|
||||
- **Tasks:** 3
|
||||
- **Files modified:** 19 (2 new, 17 modified)
|
||||
|
||||
## Accomplishments
|
||||
- Added a `tenderRadar` top-level namespace to `apps/web/src/messages/de.json` and `en.json`, grouped by component (`page`, `results`, `filter`, `savedSearch`, `detail`, `coverage`, `settings`, `sourceConfig`, `rssFeeds`, `emailAlerts`) — roughly 140 leaf keys per locale including all 16 Bundesland labels and all 44 EU CPV division descriptions, both faithfully translated to English with consistent Vergabe-domain terminology (Ausschreibung→tender, Vergabestelle→contracting authority, Frist→deadline, Auftragswert→estimated value, Bundesland→federal state).
|
||||
- Created `tenderRadar-parity.spec.ts` — a structural test that recursively flattens both locale JSON objects under `tenderRadar` and asserts identical sorted key sets (plus a non-empty-string leaf-value check), failing loudly on any future one-sided edit.
|
||||
- Converted 10 components/pages (page.tsx, ResultsList, FilterPanel, SavedSearchBar, TenderDetail, CoverageBanner, settings/page.tsx, SourceConfigForm, RssFeedListForm, EmailAlertConfigForm) to `useTranslations('tenderRadar')` — string-source swap only, no behavior or markup changes.
|
||||
- Updated 7 affected component test files with a `next-intl` `useTranslations` mock (flat key → German-copy lookup table, with `{param}` interpolation support where needed) mirroring the existing `apps/web` marketplace test convention (`tenant-selector.test.tsx`).
|
||||
- No language switcher was added anywhere (D-11) — locale continues to follow the existing `NEXT_LOCALE` cookie mechanism in `apps/web/src/i18n/request.ts`, unchanged.
|
||||
|
||||
## Task Commits
|
||||
|
||||
1. **Task 1: tenderRadar namespace (DE + EN) + de/en key-parity spec** - `d73171b` (feat)
|
||||
2. **Task 2: Convert results-side components to useTranslations** - `2ea02a2` (feat)
|
||||
3. **Task 3: Convert settings + config forms to useTranslations** - `849aa9b` (feat)
|
||||
|
||||
**Plan metadata:** _pending — this commit_
|
||||
|
||||
## Files Created/Modified
|
||||
- `apps/web/src/messages/de.json` - Added `tenderRadar` namespace (German source-of-truth copy)
|
||||
- `apps/web/src/messages/en.json` - Added `tenderRadar` namespace (English translations)
|
||||
- `apps/web/src/messages/tenderRadar-parity.spec.ts` - de/en key-parity + non-empty-leaf-value guard test
|
||||
- `apps/web/src/app/(portal)/modules/tender-radar/page.tsx` - Page heading + Suspense fallback via `t('page.*')`
|
||||
- `apps/web/src/app/(portal)/modules/tender-radar/components/ResultsList.tsx` - Table headers, empty/error states, triage toggle labels, pagination via `t('results.*')`; fixed an unrelated `t` parameter name collision with the translations function
|
||||
- `apps/web/src/app/(portal)/modules/tender-radar/components/FilterPanel.tsx` - All filter labels/placeholders via `t('filter.*')`; Bundesland/CPV option labels translated by stable code while filter values stay canonical German strings for backend compatibility
|
||||
- `apps/web/src/app/(portal)/modules/tender-radar/components/SavedSearchBar.tsx` - Saved-search UI, rename/delete/instant-alert controls via `t('savedSearch.*')` with `{name}` interpolation
|
||||
- `apps/web/src/app/(portal)/modules/tender-radar/components/TenderDetail.tsx` - Detail drawer fields, source-link copy via `t('detail.*')` with `{portal}` interpolation
|
||||
- `apps/web/src/app/(portal)/modules/tender-radar/components/CoverageBanner.tsx` - Coverage note + denylist block via `t('coverage.*')`
|
||||
- `apps/web/src/app/(portal)/modules/tender-radar/settings/page.tsx` - Settings page headings, section bodies, digest interval selector via `t('settings.*')`
|
||||
- `apps/web/src/app/(portal)/modules/tender-radar/settings/components/SourceConfigForm.tsx` - Poll-interval config form via `t('sourceConfig.*')` with `{min}/{max}` interpolation
|
||||
- `apps/web/src/app/(portal)/modules/tender-radar/settings/components/RssFeedListForm.tsx` - RSS feed CRUD list via `t('rssFeeds.*')` with `{label}` interpolation
|
||||
- `apps/web/src/app/(portal)/modules/tender-radar/settings/components/EmailAlertConfigForm.tsx` - Per-tenant mailbox config form via `t('emailAlerts.*')`
|
||||
- 7 corresponding `*.test.tsx` files updated with `next-intl` mocks
|
||||
|
||||
## Decisions Made
|
||||
- Portal display slugs in `TenderDetail.portalLabel()` (DÖE, DTVP, tender24, etc.) are left untranslated as brand/identifier proper nouns, not UI copy.
|
||||
- Bundesland/CPV filter *values* sent to the API remain the canonical German strings the backend already matches against (`nutsPrefixFor()` in `apps/api`); only the rendered *labels* are translated, keyed by stable codes.
|
||||
- CPV division descriptions (44 entries) and Bundesland names (16 entries) were moved into the i18n namespace and fully translated — they are genuine multi-word prose embedded in the component source, not just internal identifiers, so leaving them hardcoded would have both violated the plan's literal "no German string literal remains" acceptance criterion and defeated the purpose of a bilingual filter panel.
|
||||
- Date formatting (`toLocaleDateString('de-DE')`) is intentionally left unchanged regardless of locale — out of scope per the plan's "string-source swap only" directive.
|
||||
|
||||
## Deviations from Plan
|
||||
|
||||
### Auto-fixed Issues
|
||||
|
||||
**1. [Rule 1 - Bug] Fixed a `t` parameter name shadowing the translations function in ResultsList**
|
||||
- **Found during:** Task 2 (ResultsList.tsx conversion)
|
||||
- **Issue:** `const ids = data.items.map((t) => t.id);` reused the identifier `t` as the map callback parameter, shadowing the outer `useTranslations()` result within that scope — harmless at runtime (the shadow was locally scoped and unused for translation lookups inside that callback) but a latent readability/maintenance hazard once `t` became a load-bearing translation function in this file.
|
||||
- **Fix:** Renamed the map parameter to `item`.
|
||||
- **Files modified:** `apps/web/src/app/(portal)/modules/tender-radar/components/ResultsList.tsx`
|
||||
- **Verification:** `npx vitest run` for ResultsList.test.tsx (8/8 pass), `npx tsc --noEmit` clean
|
||||
- **Committed in:** `2ea02a2` (Task 2 commit)
|
||||
|
||||
---
|
||||
|
||||
**Total deviations:** 1 auto-fixed (1 bug/hygiene)
|
||||
**Impact on plan:** No scope creep — purely a readability fix triggered directly by the i18n conversion introducing a load-bearing `t` in the same file.
|
||||
|
||||
## Issues Encountered
|
||||
- Initial `ResultsList` conversion added `t` to a `useCallback` dependency array; the real `next-intl` `useTranslations()` hook returns a stable reference, but the test's inline mock (`() => (key) => ...`) creates a new function identity on every render, causing an extra `listTenders()` invocation and one test failure (`toHaveBeenCalledTimes(1)` got `2`). Resolved by removing `t` from the dependency array (with an `eslint-disable-next-line react-hooks/exhaustive-deps` comment) since the callback only needs `t` for a static fallback-error string, not the effect's re-run condition — matches the codebase's existing convention of selectively suppressing exhaustive-deps for intentionally-once effects (already used elsewhere in this file and in SavedSearchBar/RssFeedListForm/SourceConfigForm/settings-page mount effects).
|
||||
|
||||
## Next Phase Readiness
|
||||
- This is the last plan of Phase 14. The entire tender-radar module (results, filters, saved searches, detail, coverage, settings, RSS/email admin forms) now honors the DE/EN locale via the existing `NEXT_LOCALE` cookie mechanism, with no visible language switcher (D-11, deferred to a future platform-wide i18n phase per 14-CONTEXT.md's Deferred Ideas).
|
||||
- Full `apps/web` test suite is green: 151/151 (148 pre-existing + 3 new parity tests). `tsc --noEmit` clean.
|
||||
- No blockers for phase closure. Phase-level verification (`gsd-verify-work` / phase audit) can proceed.
|
||||
|
||||
---
|
||||
*Phase: 14-rss-email-alert-ingestion-module-rollout*
|
||||
*Completed: 2026-07-23*
|
||||
|
||||
## Self-Check: PASSED
|
||||
|
||||
All 13 key created/modified files verified present on disk. All 3 task commit hashes (`d73171b`, `2ea02a2`, `849aa9b`) verified present in `git log --oneline --all`.
|
||||
Reference in New Issue
Block a user