Twenty of letter-canvas's twenty-eight input()s are $localize labels that no caller ever binds. Inline nineteen of them as template i18n, using the same ids and source text so messages.en.xlf does not change. recipientText stays an input() because its message embeds a literal \n, which as template text becomes a different source string to Angular's extractor. Extract letter-line.component.ts for the #line template plus the label/auto/state/sample helpers it needs, replacing letter-canvas's three ngTemplateOutlet incantations with one tag each. Its helpers are exported pure functions with a spec, no TestBed. The file stays over the 250-line budget (77 lines of CSS plus one letter's markup), so the eslint-disable max-lines directive stays too, with its reason rewritten to say so plainly — the only such disable left in the repo. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
8.0 KiB
RD-26 — letter-canvas: inline the labels, extract letter-line, keep the disable
Status: done Source: PLAN.md 3d, order step 8
Why
letter-canvas.component.ts is 418 effective lines against a limit of 250. Unlike the other
six offenders, it is not badly structured — 77 lines are CSS and the rest is one letter.
Splitting it into letterhead, body, signature and footer would make "what does the letter look
like" a five-file question and buy no behavioural seam.
Two things do pad it without earning their place:
- 20 of its 28
input()s are pure$localizelabels, and no caller binds a single one. Verified against all three call sites:behandel-scherm,letter-composerandorg-template-editorbind only data (brief,orgTemplate,logoUrl,editableRegions,diagnostics,blockDiffs,showDiff). - Six lines of
ngTemplateOutletceremony around one#linetemplate.
The CLAUDE.md rule those labels were built for — "Shared/English components must not hardcode
Dutch — expose copy as input()s" — governs libs/shared. This is a Dutch domain component in
brief/ui/. The rule does not apply here.
Read first
letter-canvas.component.ts:1— the disable, whose reason this ticket rewrites.letter-canvas.component.ts:140— the#linetemplate, and lines 255-276, its threengTemplateOutletuses.letter-canvas.component.ts:345-372— the inputs: 8 data, 20 labels.- PLAN.md 3d and 3e.
Decisions (pre-made, don't relitigate)
-
Inline 19 of the 20 label inputs as
i18nin the template. Same id, same source text, so neithermessages.en.xlfchanges. A label read as{{ foo() }}becomes its literal text plusi18n="@@id"; one feeding an attribute becomesi18n-<attr>="@@id"(logoAltis thealtcase).The template is inline in this same file, so the 20
@@brief.canvas.*ids stay in the file — they move from a TS declaration into ani18nattribute. The id count does not change. -
recipientTextstays in TS. It is the one exception, and it is not the parameterised case RD-25 hit. Its message embeds a newline escape:$localize`:@@brief.canvas.recipient:Adres van de geadresseerde\n(wordt ingevuld bij verzending)`;In TS that
\nis one character of the message. Written as template text it becomes a source line break, which Angular's extractor treats differently — so "same source text", the property that makes decision 1 free, does not hold for this one. Leave it as aninput()and put a one-line comment on it saying why. Verified: no other label interpolates or escapes anything. -
Do not collapse the labels into a config object or an injection token.
HEADER_NAV_ITEMSandDEBUG_PANELexist because two apps genuinely differ. Here nothing differs, so a token would add a provider and an indirection to solve a problem nobody has. -
Extract
letter-line.component.tsbeside the canvas, with a spec. It takes the#linetemplate plus the sample and diff helpers it needs, and replaces the threengTemplateOutletincantations with three one-line tags. It is the only part of this file with logic worth testing, so it gets a*.spec.ts— a pure spec over the helpers, no TestBed.Drop the
NgTemplateOutletimport from the canvas once the last outlet is gone. -
Keep
/* eslint-disable max-lines */, and rewrite its reason. This is the one ticket in the arc that keeps a disable. After RD-21 through RD-25 removed theirs, this is the only one left in the repository — verified. The new reason must state the honest fact rather than promising a future removal:/* eslint-disable max-lines */ // 77 lines of CSS + one letter's markup; splitting it into // letterhead/body/signature/footer makes "what does the letter look like" a five-file // question for no behavioural seam. Deliberate, not deferred.The README requires a disable to name the ticket that removes it. This one names no ticket because none will, and the reason says so in those words.
reportUnusedDisableDirectivesstill keeps it honest: if the file ever drops under 250, lint fails on the unused directive. -
No new stories.
letter-canvas.stories.tsrenders the canvas, andletter-lineis exercised through it.
Files
apps/ssp/src/app/brief/ui/letter-canvas/letter-line.component.ts(new)apps/ssp/src/app/brief/ui/letter-canvas/letter-line.spec.ts(new)apps/ssp/src/app/brief/ui/letter-canvas/letter-canvas.component.ts
Steps
- Extract
letter-line.component.tsand its spec (decision 4). - Replace the three
ngTemplateOutletuses and drop theNgTemplateOutletimport. - Inline the 19 labels (decision 1), leaving
recipientTextalone (decision 2). - Rewrite the disable's reason (decision 5). Do not delete the directive.
- Run
npm run gen:behaviour-spec— the new spec adds titles. git add -A, then run the acceptance commands.- Update this ticket's
Status:todoneand the README's RD-26 row todone. - Commit all of it together.
Acceptance criteria
Measured against the tree before handover. Run after git add -A.
P=apps/ssp/src/app/brief/ui/letter-canvas/letter-canvas.component.ts
git grep -c "= input" -- $P # is 28 -> MUST be 9 (8 data + recipientText)
grep -c 'localize' $P # is 20 -> MUST be 1 (recipientText only)
The ids stayed in the file and the translation seam did not move (decision 1):
grep -o "@@[a-zA-Z0-9_.]*" $P | sort -u | wc -l # is 20 -> MUST still be 20
git status --short -- '*.xlf' | wc -l # MUST be 0
The outlet ceremony is gone and the child exists (decision 4):
D=apps/ssp/src/app/brief/ui/letter-canvas
git grep -c "ngTemplateOutlet" -- $P # is 6 -> MUST be 0
git grep -c "NgTemplateOutlet" -- $P # is 2 -> MUST be 0
git ls-files $D/letter-line.component.ts $D/letter-line.spec.ts | wc -l # is 0 -> MUST be 2
The disable survives, with a new reason, and is the only one in the repository (decision 5):
git grep -c "eslint-disable max-lines" -- $P # is 1 -> MUST still be 1
git grep -c "eslint-disable max-lines" -- apps libs | wc -l # MUST be 1 (one file matches)
git grep -c "RD-26 rewrites this reason" -- $P # is 1 -> MUST be 0
npm run ci --full # exits 0
Verification
npm run lint is what proves decision 5 both ways. If the extraction takes the file under
250 lines, the kept directive becomes unused and lint fails — which would mean the disable
should go after all. Report that rather than deleting it silently: it changes the arc's
conclusion that one file legitimately stays over budget.
ng build --localize inside the gate is the check on decisions 1 and 2. If you find yourself
editing a .xlf, you have changed an id or a source string — undo it instead.
--full is required (the Order table says so): letter-canvas.stories.ts renders this
component, and the axe pass over it is what proves the inlined i18n markup kept its alt
text and its labels.
Out of scope
- Splitting the letter into region components. PLAN 3d rejects it explicitly.
- The 77 lines of CSS.
recipientText(decision 2).- A config object or token for the labels (decision 3).
Risks
recipientText's\nis the trap (decision 2). Inlining it is the one change here that can silently alter an extracted source string, and the.xlffiles are hand-maintained.- Keep the directive (decision 5). Every other ticket in Phase 3 deleted one; this ticket is
the exception, and deleting it here would fail
max-linesinstead. - A label bound by no caller is still a public input. Removing it is safe only because all three call sites were checked. Do not extend the same reasoning to the 8 data inputs.
logoAltfeeds an attribute, so it needsi18n-alt, noti18n. Ani18nattribute on the element localises its content, not itsalt, and the a11y check will not catch the difference because the text is still present.