23 KiB
UI Phase 12 — Copy, motion & final polish
Mission: the closing sweep of the UI chain — make the product read and move as one: a checked-in Persian style guide enforced across both catalogs, the verified copy defects on trust-critical strings fixed, a restrained motion language behind a single reduced-motion gate, an accessibility pass, and a final four-axes QA of everything the chain touched. Copy is the cheapest trust lever Balinyaar has — and today the brand name is spelled two ways, a nurse-facing EVV error reads as "there is no game-entrance", and one BNPL sentence states the inverse of the intended risk allocation.
Track: frontend · Depends on: all prior phases — 0 → 11 (run this last) · Unlocks: ships the chain — one voice, one motion language, verified on all axes. Before you start, read ../../phases/_shared/agent-operating-rules.md and invoke the frontend-designer skill — both are mandatory.
1. Context — where this sits
Balinyaar is a trust-first product in a market where written register carries weight: families decide whether
to let a stranger into a parent's home partly on whether the product sounds careful. The catalogs are
already hand-written — the weaknesses are craft, not tone. Verified in client/messages/fa.json / en.json
(line numbers spot-checked 2026-07-16; earlier phases may shift them — grep the key, not the line):
- The brand name is spelled two ways: «بالین یار» (plain space) in 5 keys —
common.brand(53),auth.customer_title(625),auth.select_role_title(645),auth.account_error_body(654),verification.start_body(663) — vs «بالینیار» (ZWNJ) in 17: login and escrow copy disagree on the name. - تأیید — the most frequent word in a verification product — appears 38× without hamza and 26× with, no
pattern (
bank.status_verified_titlevsadmin.ver_pass); ~64 occurrences to sweep. - Two grammar bugs read as nonsense:
booking.evv_no_open_check_in(542) «ورود بازی برای ثبت خروج وجود ندارد» andadmin.alert_empty(1360) «هشدار بازی وجود ندارد» — the indefinite ی attached to the wrong word. And a typo sits on a trust chip:bank.status_verified_chip(180) reads «تاییدشد» (missing final ه) — the chip a nurse stares at while IBAN ownership verification runs. - One en arrow points the wrong way:
booking.continue_payment(en 445) is "Continue to payment ←" while its siblingpayment.cta_pay(577) is "…→". Five keys per catalog embed ←/→ literally (445, 577, 632, 633, 1337) — directional glyphs inside translatable strings are how this bug happens. - fa lacks ICU plurals where en has them:
search.cta_view_results(fa 350) is «مشاهده {count} پرستار» — the primary search CTA can render «مشاهده ۰ پرستار»; en has ICU but its=0case renders "View no nurses". - BNPL copy is banker's language with an ambiguous pronoun:
bnpl.ownership_note(857) «…ریسک نکول مشتری کاملاً با اوست…» — «نکول» is credit-desk jargon and «با اوست» leaves the reader unsure whether the customer carries the default risk (the intended meaning: the provider does). - Policy numbers are hard-coded into legally-sensitive copy the admin config panel can change: the ۷۲-hour
dispute window (
payouts.explainer_point_2, 958), the ۲۴-hour cancellation tiers (refunds.lead_gt_24h/ lead_lt_24h, 789–790), the ۷–۱۰-day refund ETA (refunds.eta_business_days, 850). One config edit makes the UI lie — and no public config read exists (platform_config/*inservices/admin/is admin-only). - Motion and live-region a11y are absent, app-wide: zero
prefers-reduced-motionhandling and zeroaria-liveanywhere inclient/src;AppIconButtonnames icon-only actions solely via Tooltiptitle, dropped entirely when disabled (AppIconButton.tsx:89–95).
What already exists (do not rebuild):
- Phase 0's token extension (motion/focus/elevation tokens in
theme/tokens.css) and single-weight icon registry with a mirrored chevron — this phase applies motion tokens, it does not invent them. - Phase 1's shared primitives:
utils/number.tsformatNumber(replacing the 25+ inlinefa-IRternaries), the state-view kit,CountdownTimerv2, route-levelloading.tsx/error.tsx. - Full key parity between
fa.jsonanden.json(28 namespaces) with complete loading/empty/error/confirm/ toast coverage per namespace, and the trust copy that is already right (keep-list, §5):payment.escrow_notice,payouts.explainer_point_1–3, calming refund-status vocabulary, two-stage disclosure copy, the specific verification-failure reasons. - Phases 4/6/8/9 may already have fixed items this phase lists conditionally (3.2 address hint, 3.5 pipeline naming, 3.7 cities, 3.10 desktop layouts) — read their reports first and skip what is done.
2. Required reading (do this first)
- audit/microcopy.md — the 16 copy problems + 7 opportunities this phase executes; its keep-list is binding. Then audit/cross-cutting-ux.md — motion/a11y/responsive findings (AppIconButton, motion absence, desktop phone-column) and its keep-list.
- Prior phase reports in
../../shared-working-context/reports/(ui-phase-0…ui-phase-11) — to learn which conditional items are already done and what naming phase 8 chose for the verification journey. client/messages/fa.json+en.json— read the namespaces you will sweep (booking, verification, payment, auth, common, bnpl, search, refunds, payouts, admin, shell, tickets, bank, address).- Code (under
client/src/):components/common/AppIconButton/AppIconButton.tsx,components/CountdownTimer/,components/EscrowNotice/EscrowNotice.tsx,theme/tokens.css(phase-0 motion/focus tokens),utils/number.ts(phase-1formatNumber), and for 3.10app/[locale]/(private-routes)/(customer)/bookings/checkout/+…/(customer)/search/results/page.tsx. - Product: ../../../product/overview/platform-summary.md §"Glossary" — consult it before deciding پرستار/مددجو/بیمار; the cancellation/refund docs under ../../../product/business/ before touching policy-number copy.
- The design contract: ../../../.claude/skills/frontend-designer/SKILL.md
(§7 non-negotiables) and
client/CLAUDE.md"Golden rules". REQ tracker: ../../shared-working-context/frontend/requests/for-backend.md — find the tail (REQ-038 at authoring time; earlier UI phases may have appended) and number onward.
3. Scope — build this
3.1 The Persian style guide — client/messages/STYLE.md + enforcement
Write a one-page style guide the catalogs are then swept against, fixing these decisions:
- Brand = «بالینیار» (ZWNJ), always. Sweep the 5 plain-space keys.
- Hamza form «تأیید» everywhere (~64 occurrences to normalize — the 38 hamza-less ones change).
- One form of جستجو. Standardize on «جستجو» (the 9-key majority; fold in the 2 «جستوجو» keys).
- ZWNJ rules: می + verb, plural ها, compound adjectives («تأییدشده»); codify with examples.
- Punctuation & quotes: Persian «،» / «؛» / guillemets; en uses straight apostrophes (the admin namespace's curly 'don’t' normalizes to the file's straight-quote majority).
- Domain glossary: پرستار / بیمار vs مددجو — decide against the product glossary; «مددجو» appears exactly
once (
booking.patient_label), so adopt it consistently or drop the one-off. One status vocabulary shared by nurse-facing and admin-facing keys (see 3.5). - Shell naming system: one metaphor per audience class instead of today's four (
shell.*, fa 56–61: اپلیکیشن/نما/کنسول/پرتال). Recommended: end-user shells = «اپلیکیشن» (خانواده، پرستار), back-office shells = «کنسول» (مدیریت، همکار). - Register: formal شما with polite imperatives (کنید) — already consistent; codify so it cannot drift.
- Digits policy: Persian digits in fa for both literals and interpolations (mechanics in 3.3).
Enforcement: add a ~30-line Node script (client/scripts/check-copy.mjs) that greps fa.json for the
banned variants (space-brand, hamza-less تایید, the «بازی» word-boundary trap, جستوجو, میگردد) and exits
non-zero; wire it as npm run lint:copy and into npm run check. 60+ scattered findings become a one-time
fix that cannot regress.
3.2 Verified copy-bug fixes (each independently shippable)
booking.evv_no_open_check_in→ «ورودِ ثبتشدهای برای این ویزیت وجود ندارد؛ ابتدا ورود را ثبت کنید.» andadmin.alert_empty→ «هشداری برای رسیدگی نیست.»address.line_hint«…برای یافتن در نیاز دارد» → «هر جزئیاتی که پرستار برای پیدا کردن منزل شما لازم دارد.» — only if phase 9 didn't. Andbank.status_verified_chip«تاییدشد» → «تأییدشده».- Archaic passive «میگردد» → «میشود»:
refunds.confirm_restate(814),admin.mod_confirm_publish(1314),admin.cfg_save_confirm_body(1333) — three occurrences, not one as the audit counted. tickets.thread_empty_bodycomma splice → «هنوز پیامی نیست. هماهنگی را شروع کنید.»- en normalization:
auth.nurse_subtitle"licence" → "license" (American throughout — the file already uses "center" 25×); straighten the admin namespace's curly apostrophes. - Introduce EVV in Persian once, then abbreviate: a first-occurrence key «ثبت حضور الکترونیکی (EVV) — ورود و
خروج شما ثبت میشود تا ویزیت بدون اختلاف تأیید شود» on the nurse's visits surface (
booking.evv_*, fa 528–535), short chips («ثبت ورود») thereafter — EVV is why the nurse gets paid without arguments; sell it.
3.3 ICU plurals + designed zero cases
Every {count} key gets an ICU plural form in both catalogs with a designed =0 case: fa
search.cta_view_results (350), results_count (352), reviews_count (364), booking.session_count (515)
plus a grep sweep for the rest. Zero states are copy, not numerals — «پرستاری یافت نشد» / "No nurses found";
also fix en's awkward =0 label ("View no nurses"). Digits policy: fa interpolations render Persian digits —
use {count, number} / ICU # (locale-formatted by next-intl) in message keys, and phase 1's formatNumber
where a raw value is interpolated at a call site; verify one of each renders «۳» not "3".
3.4 Arrows out of strings
Remove the five literal ←/→ glyphs per catalog (booking.continue_payment, payment.cta_pay,
auth.nurse_switch, auth.customer_switch, admin.cfg_history_change — fa/en lines 445/577/632/633/1337)
and fix en 445's wrong-direction arrow by construction: direction moves into auto-mirroring endIcon slots
on the buttons involved (phase 0's chevron via the AppIcon registry; the RTL Emotion cache mirrors it).
admin.cfg_history_change «{old} ← {new}» becomes two interpolations joined by a component-rendered mirrored
arrow icon. Touch each call site; the strings become text-only.
3.5 Trust-moments copy pass
- OTP screen reassurance:
auth.customer_subtitle(626) is just «با شماره موبایل خود وارد شوید» — add one warm trust line (verified nurses + escrow, e.g. «پرستاران تأییدشده، پرداخت امن نزد بالینیار»). - Checkout «چرا امن است»: one line near the pay CTA linking escrow to the dispute process —
supplementing, never replacing,
payment.escrow_notice(§5). And post-payment what-happens-next: the confirmation states the next steps in order (nurse notified → coordination ticket → visit-day EVV). - BNPL de-jargon: rewrite
bnpl.ownership_note(857) reader-first, plainly allocating risk to the provider — e.g. «قسطها را مستقیماً به {provider} میپردازید؛ بالینیار مبلغ کامل را همان ابتدا دریافت میکند و اگر قسطی پرداخت نشود، مسئولیت آن با ارائهدهنده است، نه شما و نه پرستار.» «نکول» disappears from the catalog. - Disambiguate the verification names: «احراز هویت» names both the 7-step pipeline (
nav.verification13,verification.title658) and the KYC step inside it (step_identity_kyc676, admin 1470). Rename the pipeline «تأیید صلاحیت»; the KYC step keeps «احراز هویت». Coordinate with phase 8's journey naming — if its report already renamed, adopt its term and only sweep stragglers. - One rejected-status vocabulary: unify رد شد / ناموفق / ردشده into one nurse-facing and one
admin-facing form (decide in STYLE.md; sweep
verification.*andadmin.*together — they cross-reference).
3.6 Config-served policy numbers (REQ + client single-sourcing)
Parameterize the hard-coded policy numbers: payouts.explainer_point_2 takes {hours} (72),
refunds.lead_gt_24h/lead_lt_24h take {hours} (24), refunds.eta_business_days takes {minDays}/
{maxDays} (7–10). File a REQ (next free number) for a public/authenticated policy-config read
(dispute-window hours, cancellation tiers + percentages, refund ETA) — verify first that no existing endpoint
serves it (at authoring time only admin-scoped platform_config/* exists). Until served, single-source the
constants in client/src/constants/policy.ts with a comment linking the REQ, and feed the interpolations
from there. One config edit must never again silently make copy lie.
3.7 Honest search empty state
Replace search.empty_suggest_city (361, both catalogs) — «شهر نزدیک دیگری مانند مشهد، اصفهان یا شیراز را
امتحان کنید» is nonsense advice for a Tehran-launch user — with honest relaxations already next to it (clear
the district, widen filters) or interpolated actually-covered cities. Only if phase 4 didn't.
3.8 Motion pass — one restrained language
Apply phase 0's motion tokens app-wide; introduce nothing springy — this is a calm clinical product:
- Page/content transitions: 150–200ms fade + small slide on route-group content; skeleton → content crossfade (the phase-1 skeleton twins).
- Dialog/bottom-sheet enter/exit via
theme.componentsdefaults (one place, not per-dialog); list-entrance restraint — at most a subtle stagger on first paint, never re-animating on refetch. prefers-reduced-motionis respected via a single media-query gate in one place (a token/theme-level switch that zeroes durations) — today the app has zero handling; do not scatter per-component checks.
3.9 A11y sweep
AppIconButton: add anaria-labelpassthrough that defaults fromtitleand survivesdisabled(today the Tooltip — the only name source — is dropped when disabled;AppIconButton.tsx:89–95). Update its test.aria-liveon the moments that change without focus:CountdownTimer(polite), async error/retry regions, payment-status polling. Todayaria-liveappears nowhere inclient/src.- Disclosure semantics (
aria-expanded+aria-controls) on collapsibles — the earningsExplainerCardpattern and any phase-built equivalents. - Form label association audit (MUI does this when
labelis used — audit the hand-rolled ones); focus-visible coverage check (phase 0 tokenized the ring — verify interactive elements show it); contrast re-check on remaining terracotta text usages in both schemes.
3.10 Desktop-aware layouts — the two worst phone-column offenders (scope-boxed)
Only if phases 4/6 didn't already: above ~900px, (a) checkout ((customer)/bookings/checkout/) becomes
two-column with a sticky order summary; (b) search results ((customer)/search/results/page.tsx) uses the
width (wider cards or list + detail). Nothing else — a full responsive pass is (DEFERRED → post-chain).
3.11 Final QA — the closing walkthrough
Write a per-shell walkthrough checklist (customer funnel, nurse day-of + verification, admin desks, partner
portal, auth/first-run) × /fa+/en × light+dark × mobile+desktop. Run it end-to-end; fix what it finds
(anything non-trivial becomes a report note, not silent scope expansion). Attach the completed checklist to
the phase report — it is the chain's shipping evidence.
4. Mocks & seams in this phase
None introduced. UI stays mock-tolerant behind the existing services/{domain} seams. The one backend gap is
3.6's policy-config read: a REQ appended to
for-backend.md (REQ-001…038 taken at
authoring time — check the tail for numbers earlier UI phases claimed, and number onward). Until delivered,
the client single-sources the numbers in constants/policy.ts; no mock flag changes.
5. Critical rules you must not get wrong
payment.escrow_noticeand product-mandated strings change only with a flagged product note. The escrow sentence,payouts.explainer_point_1–3(especially point 3's BNPL-fee-never-deducted guarantee), the two-stage disclosure copy (booking.notes_hint/booking.disclosure_note), and the verification-failure reasons are model trust writing — sweeps may normalize their spelling, never their meaning. If a rewrite seems needed, flag it in the report instead.- Don't regress the keep-lists (both audit files): calming money-status vocabulary («در راه»، «نیازمند
بررسی»), culturally-tuned gender copy, formal شما, complete per-namespace state strings, admin
confirm-dialog operational candor, token discipline, RTL
dir="ltr"islands, Shamsi-first formatting. - en stays hand-written-idiomatic — no machine-translation tone ("Queue clear — nothing to review" is the bar). Key parity fa/en stays absolute: every add/rename lands in both catalogs in the same change.
- Don't rename stable message keys casually. A key rename must grep and update every
t('…')usage; if a key's meaning is unchanged, keep its name and change only the value. - Design-contract non-negotiables that bite here: tokens not hexes (motion durations/easings are tokens,
not inline magic numbers); RTL logical props (arrows mirror by construction); dark mode on every touched
surface; MUI v9 API only; co-located tests for touched shared components (
AppIconButton,CountdownTimer,EscrowNoticeall have.test.tsx); fetch/cookies rules untouched — no data-layer changes. - Reduced motion is a hard gate, not a nice-to-have — every animation collapses to no-motion under
prefers-reduced-motion: reducevia the single gate (3.8). Frontend lane only: the policy-config gap is a REQ, never aserver/edit.
6. Definition of Done
On top of the shared definition-of-done.md:
npm run checkgreen (now includinglint:copy);npm run test:cigreen for touched shared components (AppIconButton,CountdownTimer, any collapsible/state component touched).client/messages/STYLE.mdexists;scripts/check-copy.mjspasses with zero banned variants — no plain-space brand, no hamza-less تایید, no «بازی» trap, no میگردد, one جستجو form.en.json/fa.jsonin key parity; every{count}key has ICU plural + designed=0in both catalogs; no literal ←/→ remains in either catalog (grep proves it).bnpl.ownership_noteplainly allocates default risk to the provider; «نکول» absent from the catalog.- Policy numbers interpolate from
constants/policy.ts; the policy-config REQ is filed with a number. prefers-reduced-motion: reducedisables all chain-added motion; icon-only buttons have accessible names including when disabled; countdown/async-status regions havearia-live; touched collapsibles exposearia-expanded.- Visual verification on the four axes —
/fa+/en× light + dark — and mobile + desktop for 3.8/3.10 surfaces; the completed 3.11 walkthrough checklist is attached to the phase report.
7. How to test (what a human can verify after this phase)
cd client && npm run check— passes, including the new copy lint. Temporarily add «بالین یار» to any fa value →lint:copyfails; revert.- Log in as a customer (
/fa): login, checkout escrow copy, and refund screens all spell «بالینیار» identically; the OTP screen shows the new reassurance line. - Search with filters that match nothing → «پرستاری یافت نشد» (not «۰ پرستار»; not "View no nurses" on
/en); counts render Persian digits on/fa. - As a nurse, attempt EVV check-out without a check-in → the corrected sentence (no «ورود بازی»); the bank screen's verified chip reads «تأییدشده»; the visits screen introduces «ثبت حضور الکترونیکی (EVV)» once.
- On
/encheckout, "Continue to payment" carries a mirrored end-icon pointing forward (→ in LTR, mirrored on/fa); grep both catalogs for←|→→ zero hits. - Open the BNPL comparison → the ownership note reads plainly, no «نکول», risk clearly on the provider.
Grep the catalogs for «۷۲ ساعته» → none; the keys take
{hours}fromconstants/policy.ts; the REQ exists in the tracker. - Navigate between routes → 150–200ms calm fade/slide, skeletons crossfade into content; enable reduced motion in DevTools → everything appears instantly.
- Inspect the a11y tree on a screen with icon-only buttons: each has a name, including a disabled one; a
running countdown announces politely (
aria-live). - Run the 3.11 checklist across all shells on the four axes + both widths; every row checked or fixed.
8. Hand off & document (close the phase)
- Update
client/CLAUDE.md: mentionmessages/STYLE.md+lint:copyin the i18n/Golden-rules sections and the reduced-motion gate location; update "Project Structure" ifscripts/orconstants/policy.tsare new. - Write the frontend report at
../../shared-working-context/reports/ui-phase-12-report.md: the style-guide decisions taken (hamza form, جستجو form, shell naming, status vocabulary, digits policy), swept-key counts, conditional items skipped because an earlier phase did them, the completed QA checklist, and anything the walkthrough found but deferred. List the REQ(s) filed with their numbers. - Save a memory note per operating-rules §8: the chain is complete — style guide + copy lint now enforce Persian orthography; motion and a11y gates exist; record the STYLE.md decisions so future copy follows them.