7.6 KiB
Persian (fa) copy style guide
One page, binding for messages/fa.json. scripts/check-copy.mjs (npm run lint:copy, part of
npm run check) enforces the banned-variant rules below so these decisions cannot silently regress.
This file does not repeat en.json conventions beyond what's noted in §7 — the English catalog is
hand-written and reviewed for idiom, not linted.
1. Brand name
«بالینیار» — ZWNJ () between بالین and یار, always. Never a plain space («بالین یار»).
The brand name appears in money/trust copy (login, escrow, refunds) as often as anywhere else — an
unstable brand mark there is the worst place to be inconsistent.
2. تأیید — hamza, always
Write تأیید (with hamza) and its derived forms — تأییدشده, تأییدیه, تأیید کردن — every time, never تایید/تاییدشده/تاییدیه (hamza-less). This is the single most frequent word in a verification product; one spelling, no exceptions, in every namespace (booking, payment, auth, verification, admin, payouts, bnpl, refunds, legal — all of them).
3. جستجو — one form
Standard form: جستجو (no ZWNJ, one word). Not «جستوجو» / «جست و جو». Applies to the noun and any
compound (در جستجو, نتایج جستجو).
4. ZWNJ (نیمفاصله) rules
Use ZWNJ () — never a plain space or no separator — in:
- می + verb stem: میشود، میکند، میپردازید، میماند (never میشود/می شود).
- Plural ها: مراقبها-style compounds keep the ZWNJ before ها when the base ends in a consonant that
would otherwise misread (
شبهاnotشبها); a plain plural on a word already ending in a vowel/silent-h takes the ZWNJ too (بچهها). - Compound past-participle adjectives: تأییدشده، لغوشده، ردشده، منتشرشده، پرداختشده — the doer/state compound is one ZWNJ-joined word, not two spaced words («تایید شده») and not fused with no separator.
- Brand name itself (§1) is the other load-bearing ZWNJ case.
5. Punctuation & quotes
- Persian text uses «...» guillemets for quoted terms/labels in prose (as this document does), and
Persian «،» / «؛» for commas/semicolons inside translated sentences where the surrounding punctuation
is itself Persian prose (most UI strings use plain Latin
,/;today for simplicity in short labels — don't retrofit existing short strings, but prefer «،»/«؛» in new multi-clause sentences). - English (
en.json) uses straight apostrophes (don't,couldn't) throughout — never curly (’,‘). One admin-namespace holdout (curlydon't/couldn't) is fixed by this phase; don't reintroduce curly quotes when editing English copy.
6. Domain glossary
- بیمار — the care recipient, used consistently everywhere except one parenthetical. Do not adopt
«مددجو» — it appeared exactly once (
booking.patient_label) and has been dropped in favor of the 99%-majority «بیمار». - پرستار — the caregiver, always (never «مراقب» as a noun for the person — «مراقب» only survives as
an adjective/role qualifier, e.g.
booking.gender_label"جنسیت مراقب" meaning "the caregiver's gender"). - رزرو — a confirmed, paid booking. درخواست رزرو — a pre-payment request. Never conflate the two;
a
booking_requestis never called «رزرو» before it converts. - ویزیت — one scheduled visit/session within a booking.
- شبا — IBAN, always («شماره شبا» for the field label, «شبا» alone elsewhere).
7. Shell naming system
One metaphor per audience class, not four:
- End-user shells (family, nurse — the apps people book/work through day to day) → «اپلیکیشن»: «اپلیکیشن خانواده», «اپلیکیشن پرستار».
- Back-office shells (staff consoles — admin, partner-center) → «کنسول»: «کنسول مدیریت», «کنسول همکار».
- Never «نما» (view) or «پرتال» (portal) for a whole shell name — those read as one-off inconsistent
metaphors. (
booking.evv_nurse_view"نمای پرستار" is a different thing — a chip labeling whose perspective a shared booking-detail screen is rendered from, not a shell name; it correctly keeps «نما» in that narrower sense.)
8. Verification pipeline vs. the identity step
«تأیید صلاحیت» names the whole 7-step nurse trust pipeline (nav entry, the verification hub's title,
its start/progress/approved states, the admin queue). «احراز هویت» stays the name of the one KYC
step inside it (national-ID + civil-registry + liveness selfie) — both on the nurse side
(verification.step_identity_kyc) and the admin side (admin.step_identity_kyc). A nurse who passed the
KYC step but still sees a pipeline titled «احراز هویت» incomplete in the nav used to read as a
contradiction; they no longer share a name.
9. Status vocabulary — one nurse-facing form, one admin-facing form
For "this step/item was rejected/failed" style states that appear on both a nurse-facing screen and an admin-facing screen for the same underlying concept (a verification step's outcome):
- Nurse-facing: «رد شد» (
verification.status_failed) — a short declarative sentence-style status, matching the register of its siblingstatus_passed("تأییدشده")/status_in_review("در حال بررسی"). - Admin-facing: «ردشده» (
admin.step_failed,admin.agg_rejected,admin.rstatus_rejected,admin.mstatus_rejected) — the compound-adjective state form, matching the admin namespace's ownstep_passed/agg_approved/center_state_verified("تأییدشده") pattern.
This does not extend to unrelated money-failure vocabulary (payouts.pstatus_failed,
refunds.rstatus_failed, admin.batch_status_failed all legitimately use «ناموفق» — a transfer/payment
failing is a different concept from a document being rejected, and conflating them would blur a real
distinction).
10. Digits policy
Persian digits (۰۱۲۳۴۵۶۷۸۹) everywhere on /fa — both hard-coded literals ("۲۴ ساعت") and
interpolated numbers. For an interpolated {count}/{hours}/… inside an ICU message, use the ICU
number sub-format ({count, number}) or a plain # inside a plural block — next-intl formats both
through the active locale (fa → Persian digits) automatically. When a raw number is interpolated at a
call site instead of through ICU (e.g. built into a larger string in code, not a message placeholder),
route it through formatNumber (@/utils) — never template a raw JS number directly into Persian text.
11. Register
Formal شما throughout, with polite imperatives (کنید) for actions and instructions. Already consistent across the whole catalog — this codifies it so a future addition can't drift into informal تو/imperative stems (نکن, برو).
12. Policy numbers
Legally/financially sensitive numbers that the admin config panel can change (the dispute-window hours,
cancellation lead-time hours, refund ETA days) are never hard-coded into a message string. The
message key takes a parameter ({hours}, {minDays}/{maxDays}) and the call site interpolates from
client/src/constants/policy.ts (single-sourced, REQ-065 tracks the eventual public config-read that
replaces the constants file). A config edit must never again silently make the UI copy lie.