Admin page for catalogue AI-translation freshness (block-6 plan, Task 5):
/admin/catalogue/translations.
Lists (resource, target language) rows across the four catalogue
translatable types (item / category / set_label / set_value),
filterable by type, language, freshness state and a name search, with a
per-state count in the header. Row actions enqueue a single translation
(Translate) or record the current source as the reference without
calling the AI (Stamp fresh, TranslationStatus.stamp_fresh/2); bulk
actions do the same across every row matching the current type/language/
search scope for one target state ("Translate all missing" /
"Retranslate all stale").
Only rendered when PhoenixKitAI is configured with a working endpoint —
otherwise a notice points at the AI admin section. Availability and the
endpoint/prompt resolution are the SAME check the sweep tick makes
(TranslationSweepWorker.endpoint_and_prompts/0), so a manual click here
and an automatic sweep enqueue can never disagree about which endpoint
or prompt a resource type uses.
In-flight tracking
PhoenixKitAI.Translations.job_in_flight?/1 (the private check enqueue/1
itself uses for dedup) isn't part of the module's public API, so rather
than re-querying oban_jobs directly this page tracks in-flight rows the
same way phoenix_kit_ai's own single-resource AITranslate component
does: an enqueue/1 call's conflict?/:ok result marks a row, and the
global {:ai_translation, event, payload} broadcast
(Translations.subscribe/0) clears it again on completion/failure — no
private function, no direct DB query.
Gettext
Uses the local gettext/1,2 macros (use Gettext, backend: PhoenixKitCatalogue.Gettext), not the qualified Gettext.gettext(Backend, ...) runtime call some older catalogue modules still carry — the
qualified form is a plain function call the extractor never sees (see
commit "Fix strings the gettext extractor cannot see").