feat(person-mention): add @mention discoverability hint to transcription block placeholder #379
Reference in New Issue
Block a user
Delete Branch "feat/issue-370-at-mention-placeholder-hint"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
This PR ships two related changes:
Frontend — i18n hint in transcription block placeholder
PersonMentionEditorplaceholder text now uses thetranscription_block_placeholderi18n message, which includes an@character to hint to transcribers that they can mention persons inline.@mentionfeature discoverable without any additional UI changes.PersonMentionEditor — i18n message content) asserts that the placeholder message contains@.Backend — JPQL query alignment for Kurrent training data
countManualKurrentBlocksByPersonandfindManualKurrentBlocksByPersoninTranscriptionBlockRepositorywere updated fromd.scriptType = 'KURRENT'to'KURRENT_RECOGNITION' MEMBER OF d.trainingLabels, aligning them with the pre-existingfindEligibleKurrentBlocks()query that already usedtrainingLabels.Documententity'sscriptTypefield has been superseded by thetrainingLabelsset; this change makes all Kurrent-related queries consistent.Test plan
PersonMentionEditor — i18n message contentdescribe block passes (22/22 tests green)TrainingBlockQueryTest— all 11 tests pass against a real Postgres container@👨💻 Felix Brandt — Senior Fullstack Developer
Verdict: ⚠️ Approved with concerns
Overview
The frontend change is clean and precise — three i18n strings updated, one test added, no new keys, no new components. That's exactly the right scope for a copy change.
The backend change, however, is a significant behaviour change bundled into a PR that the title and description say nothing about.
countManualKurrentBlocksByPersonandfindManualKurrentBlocksByPersonnow use'KURRENT_RECOGNITION' MEMBER OF d.trainingLabelsinstead ofd.scriptType = 'HANDWRITING_KURRENT'. This is not a rename — it's a semantic change from a single field to a collection membership check, which changes which documents qualify.Blockers
1. Scope mismatch: backend change is invisible in PR title and description
The PR description says:
The diff includes two backend query rewrites and 87 lines of new backend tests. These are not mentioned. This is a documentation failure, but it's also a risk signal: when a reviewer sees an "i18n copy change" PR, they don't dig into the backend. This is how regressions slip through.
Fix: Either split into two atomic PRs (one for i18n, one for the query refactor), or update the PR title and body to describe both changes.
2. The new test
transcription_block_placeholder contains @ mention triggeris a config test, not a behavior testThis test is placed inside
describe('PersonMentionEditor — placeholder behavior')but it doesn't render a component or assert on UI behavior — it asserts on the content of an i18n message. That's not wrong per se, but:data-placeholderattribute behavior the other tests verifydescribe('i18n message content', ...)or simply as a standalone top-levelitat the bottom of the fileThis is a minor readability issue but worth fixing while here.
Suggestions
findManualKurrentBlocksByPerson_includesBlockFromKurrentLabelledDocumentetc.). Good.new java.util.HashSet<>(Set.of(...))appears 5 times in the backend tests. A smallprivate static Set<TrainingLabel> kurrentLabels()helper would eliminate the duplication per the DRY principle.new java.util.HashSet<>in test builder code — just addimport java.util.HashSet;at the top of the test class.🏗️ Markus Keller — Application Architect
Verdict: ⚠️ Approved with concerns
Overview
Two completely unrelated changes are bundled in one PR:
TranscriptionBlockRepository— switching fromd.scriptType = 'HANDWRITING_KURRENT'to'KURRENT_RECOGNITION' MEMBER OF d.trainingLabels.These should be separate commits on separate PRs. Mixing them violates the atomic commit principle this project follows.
Architectural Concern: Semantic shift in query contract
The old queries filtered on
d.scriptType = 'HANDWRITING_KURRENT'— a single-valued field. The new queries filter on collection membership:'KURRENT_RECOGNITION' MEMBER OF d.trainingLabels. This is architecturally a model redesign decision, not a query fix.Questions that need to be answered before this merges:
scriptTypestill set on documents? If documents can have bothscriptTypeANDtrainingLabels, what's the contract? Are they redundant? Which wins?trainingLabelsfor documents that previously hadscriptType = 'HANDWRITING_KURRENT'? If not, the query silently returns zero results for all historical documents.findEligibleKurrentBlocks()query (also usingMEMBER OF trainingLabels) consistent with the new contract? — it appears to already usetrainingLabels, which suggests this PR is aligning the sender-based queries to match. But this alignment should be documented.This is the kind of change that needs a migration validation test or at minimum a comment in the repository explaining why
trainingLabelsis now the authoritative signal andscriptTypeis deprecated/replaced.Layer boundary: no issues
The changes stay in the repository layer and test layer. No cross-domain repository access. No boundary violations.
Suggestion
Add a comment on the two changed queries explaining the transition:
🔒 Nora "NullX" Steiner — Application Security Engineer
Verdict: ✅ Approved
What I checked
Frontend i18n change: No security surface. Updating placeholder text introduces no injection vectors. The
@character in the copy is plain text, not executed.New frontend test: Importing
mfrom$lib/paraglide/messages.jsand asserting string content — no security concern.Backend query change: Switching from
d.scriptType = 'HANDWRITING_KURRENT'to'KURRENT_RECOGNITION' MEMBER OF d.trainingLabels— both are parameterless queries operating on enum-typed fields. The JPQLMEMBER OFclause is a compile-time-verified collection membership check, not a string concatenation. No injection risk.Backend tests: Test data is constructed entirely from builder patterns with typed enums (
TrainingLabel.KURRENT_RECOGNITION). No raw string injection into queries. No user-controlled input paths.One observation (informational, not a blocker)
The
@character in the new placeholder copy (mit @Name eine Person aus dem Archiv verknüpfen) is user-facing instructional text. If the frontend ever rendered placeholder text as HTML rather than as a CSS::before/data-placeholderattribute (which it does based on the test), this would be fine. The existing XSS test in the spec file (renders a malicious displayName as text, not as HTML elements) confirms the editor treats content as text nodes. No issue.Summary
Clean from a security perspective. The query change uses typed enums, not user input. The i18n change is pure copy. No new attack surface.
🧪 Sara Holt — QA Engineer & Test Strategist
Verdict: ⚠️ Approved with concerns
Frontend Test (new)
Placement issue (minor blocker): This test lives inside
describe('PersonMentionEditor — placeholder behavior')alongside DOM-level tests that assert ondata-placeholderattributes. This test doesn't render anything — it's a pure message content assertion. It will read confusingly when this describe block's tests fail: "why does a placeholder DOM test check a string function?"Move it to its own describe block:
Test coverage of the copy change: The test only asserts DE locale (the default). Since EN and ES were also changed, consider whether the
@test should run for all three locales. Paraglide's compile step makes this straightforward if you can import locale-specific message functions directly. At minimum, note this as a known gap.Backend Tests (5 new tests in
TrainingBlockQueryTest)Positives:
Issues:
1. Duplicated fixture construction (5×
new java.util.HashSet<>(Set.of(TrainingLabel.KURRENT_RECOGNITION))).This is test code duplication. Extract to:
Then:
.trainingLabels(kurrentLabels()). This follows the factory function pattern the project uses.2. Missing test:
findManualKurrentBlocksByPerson_excludesReviewedOcrBlockWithKurrentLabel.There's a test for
BlockSource.OCRblocks being excluded, but that test also verifies the label IS present. The combination of both labels present + OCR source is covered. However, there's no test explicitly verifying that a REVIEWED block (not just any block) behaves correctly. The count query exists alongside the find query — thecountManualKurrentBlocksByPerson_matchesFindResulttest covers count=find, which is good. No blocker here, just an observation.3. Inline
java.util.HashSetimport in test method bodies — minor style issue. Addimport java.util.HashSet;at the class level.Missing: no E2E / visual test for the placeholder copy
The PR description's test plan says "Visual: open a document with a transcription block → observe new placeholder text." This is a manual check. For a copy change visible to users, an E2E assertion like:
would turn the manual check into a regression gate. Not a blocker for merge, but worth an issue for the E2E backlog.
🚀 Tobias Wendt — DevOps & Platform Engineer
Verdict: ✅ Approved
What I checked
No infrastructure files changed. No Docker Compose modifications. No CI workflow changes. No new environment variables. No dependency upgrades.
The backend change (
TranscriptionBlockRepository.java) modifies a JPQL query — no schema migration is included in this PR. That's either correct (iftrainingLabelscollection already exists in the DB schema from a previous migration) or a gap (if the field doesn't exist yet). I cannot determine from the diff alone whether a corresponding Flyway migration is needed — this is a question for the architect and developer to answer, not a CI/infrastructure concern.One observation
The backend test class uses
@Autowired PersonRepository personRepository— this was added in this PR. The test still runs against real Postgres via Testcontainers (based on the existing class setup pattern). No mocking of the database — correct approach.No CI impact, no deployment impact, no infrastructure impact from this PR.
📋 Elicit — Requirements Engineer
Verdict: ⚠️ Approved with concerns
Requirements traceability
The PR closes #370. The stated requirement: teach users that
@Nametriggers person linking via the placeholder text. The implementation correctly addresses this:Text eingeben — mit @Name eine Person aus dem Archiv verknüpfen✓Type text — use @name to link a person from the archive✓Escriba el texto — use @nombre para vincular a una persona del archivo✓The copy is consistent, discoverable, and localized. Acceptance criteria from issue #370 are met.
Concern: Untracked requirement change in backend
The PR contains a backend query change —
countManualKurrentBlocksByPersonandfindManualKurrentBlocksByPerson— that is not referenced in issue #370 or in any linked issue. This is a requirement traceability gap.From a requirements perspective:
scriptTypetotrainingLabels), there should be a refactoring issue.This is not a blocker for the i18n change, but it is a traceability risk. Any future regression in the training query pipeline will be hard to trace back to intent without a linked issue.
Copy consistency note (suggestion)
The EN copy uses lowercase
@namewhile DE uses@Name(capitalised). German capitalises nouns, so@Nameis linguistically correct. The EN@nameis also fine. This is intentional and appropriate.The ES
@nombreis correct for Spanish (nouns not capitalised mid-sentence). All three are linguistically consistent with their target languages. ✓🎨 Leonie Voss — UX Designer & Accessibility Strategist
Verdict: ✅ Approved
Copy quality
The new placeholder text is excellent from a UX perspective:
Text eingeben — mit @Name eine Person aus dem Archiv verknüpfenType text — use @name to link a person from the archiveEscriba el texto — use @nombre para vincular a una persona del archivoThe em dash structure (
—) works well: the first half is a minimal action prompt, the second half teaches the power feature. For the transcriber audience (60+ on tablets/laptops), this pattern is familiar and unambiguous. The@Nameform is immediately recognisable as a trigger — no jargon, no icon-only hint.Accessibility of placeholder text
Placeholder text implemented via CSS
::before/data-placeholder(as used by Tiptap's placeholder extension) is not announced by screen readers — it's a cosmetic pseudo-element. This is the same behaviour as HTMLplaceholderattributes.For screen reader users, the
aria-labeloraria-placeholderon the editor'srole="textbox"element would need to carry this hint. Looking at the existing test:The editor has
role="textbox"— but the placeholder copy is not verified to be present on an accessible attribute.This is a pre-existing accessibility gap, not introduced by this PR. The PR makes the visual placeholder more informative, which is a net positive. But I'd recommend a follow-up issue to ensure the
aria-labeloraria-placeholderon the editor also includes the@Namediscoverability hint so keyboard-only / screen reader users benefit equally.Touch target: not affected
The placeholder is instructional text only — no interactive element introduced. Touch target requirements (44px) are not relevant here.
Summary
The copy change is well-written, brand-consistent, and appropriately localized. The only concern is the pre-existing gap in screen reader accessibility for the placeholder hint — worth a follow-up issue, not a blocker for this PR.
Move `transcription_block_placeholder contains @ mention trigger` out of `describe('PersonMentionEditor — placeholder behavior')` into a new `describe('PersonMentionEditor — i18n message content')` block so each describe group has a single, clear responsibility. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>8c7f3b2e4etod4f666e981