PhoenixKitEcommerce.Web.Translations (PhoenixKitEcommerce v0.5.11)

Copy Markdown View Source

/admin/shop/translations — the management page design §4.5 calls the core of this whole initiative: "интерфейс — ядро задачи." Everything built in tasks 3–5 (fingerprints/staleness, the category adapter, the reconciliation sweep) is reachable ONLY from here — design §1's requirement that no capability exist the interface can't reach.

Why this page does NOT use phoenix_kit_ai's AITranslate components

FormGlue / Embed / AITranslate (design §4.5) are hard-wired to ONE resource: one ai_resource_uuid, one subscription, scalar ai_in_flight? / ai_progress / ai_modal_open, one <dialog id="ai-translation-modal">. A coverage table with many rows would collide on every one of those. This page is built directly on PhoenixKitAI.Translations.enqueue/1 / enqueue_all_missing/2 — both stateless, both taking identifiers as plain arguments — plus this package's own AITranslatable / CategoryAITranslatable adapters.

Data model: rows and columns

A row is a resource (product or category, per @type_filter — the two types are never mixed in one table because their field vocabularies differ). A column is a target language from TranslationSweepSettings.languages/0. A cell holds that resource's FOLDED state for that language (design §4.1's missing > stale > unknown > fresh), computed from TranslationFingerprint.field_state/3 over every translatable field.

States are computed in this process, not via TranslationFingerprint.select_candidates/2's hash-in-the-database query — that query deliberately returns ONLY missing/stale candidates (design §4.3: "наружу приезжают только uuid и список языков"), which is exactly wrong for a table that must also show fresh and unknown. At this catalog's measured size (665 products, 7 categories — design §2) loading the filtered set and folding states in Elixir is milliseconds, matching the design doc's own full-catalog hash measurements; this is an admin page, not a hot path.

Bulk verbs and the field/lang scope

Design §4.5: field/lang axes are smuggled into the data-bulk-action event NAME ("request_translate:de-DE:seo_title"), the same technique PhoenixKitEcommerce.Web.ShopifySync uses for its per-section actions — BulkSelectScope's data-bulk-action has no way to attach an extra phx-value-*. "all" in either segment means "every configured target language" / "every field of this resource type" — see bulk_event/3 and parse_scope/3.

Three verbs (design §4.4, §4.5):

  • translatemissing ∪ stale only, computed fresh per resource at confirm time (never trusts the request-time snapshot — see PhoenixKitEcommerce.Web.ShopifySync's moduledoc on why). The field scope narrows nothing here (design §4.4: "На состав задания он не влияет") — only the already-filtered row selection does that.
  • retranslate ("перевести заново") — resets the fingerprints for the selected langs × fields (reset_reference/3) and THEN enqueues for exactly that lang list, unconditionally: a freshly-reset field reads :unknown, which the ordinary missing∪stale computation would never pick up on its own.
  • stamp ("проштамповать") — stamp_reference/4, no model call.

Catalogue-wide stamp / reset (Fix D)

Every verb above reads its uuids off the bulk-select click payload, which BulkSelectScope can only ever populate from the rows currently in the DOM — one @per_page page (see PhoenixKitWeb.Components.Core.BulkSelect's own moduledoc: "purely client-side"). Design §1's requirement — no capability the interface can't reach — is violated by that alone for the §4.1 rollout: stamping 634 existing resources as reference 25 rows at a time is not a capability an admin will ever actually use; the only route that could do it in one shot was the CLI task, which no admin sees.

request_stamp_all / request_reset_all fix that by NOT depending on any client-side selection at all. matching_rows/1 re-queries every resource the CURRENT filter (type/category/lang/state/field/search) matches, across every page — the same function load_data/1 uses to build the table, so the confirmation modal's resource count can never drift from what the table itself would show if it had no pagination. They go through the identical request → confirm two-phase flow as every other verb here (open_pending_all/2 -> @pending -> <.confirm_modal> -> do_confirm/2).

Deliberately just these two verbs, not a catalogue-wide "translate": stamp_reference/4 and reset_reference/3 are metadata-only writes (no model call, no Oban job) — cheap and safe to run over the whole filtered set. A catalogue-wide TRANSLATE would enqueue a real TranslateWorker job per candidate language, and design §13 is explicit that a run of that size (1330 jobs at full catalogue scope, design §8) happens only by the owner's own separate decision, never as a button on this page.

What is deliberately NOT here

Per-field JOB scoping (design §12.2 rejected resource_scope for exactly this reason) — the field axis only narrows what gets WRITTEN (put_translation/4's write-narrowing) and, for retranslate/stamp, what gets touched in metadata. A "translate" job always asks the model for every non-empty field. Also not here: a catalogue-wide "translate" button — see the section above.