Deft
Ask AI

Search

Search skills, authors and topics

jakubkrehel / skills

better-writing

Improve UX writing for interface labels, errors, empty states, settings, notifications, and product voice consistency.

Install

npx skills add https://github.com/jakubkrehel/skills --skill better-writing

Copy patterns

Templates for destructive flows and status copy, and recipes for plurals and locale formatting.

Destructive flows

SituationCopy
Reversible, frequent actionToast: "Project archived" with an Undo action
Irreversible, one itemTitle: "Delete 'Q3 report'?" Body: "This permanently deletes the report and its 4 comments." Buttons: Delete report, Cancel
Irreversible, many itemsTitle: "Delete 12 files?" Body: "You can't undo this." Buttons: Delete 12 files, Cancel
Affects other peopleBody names who: "The 8 members of Design lose access."
Account, workspace or repositoryField label: "Type 'acme-web' to confirm". The button stays disabled until it matches

An undo toast carries an action, so how long it stays on screen is better-accessibility's.

Status copy

StatePatternExample
In progressVerb in -ing plus the object"Saving changes…", "Loading invoices…"
DoneObject plus past-tense verb, no "successfully""Changes saved", "Invite sent to sam@example.com"
FailedWhat didn't happen, then the next step"Changes not saved. Check your connection and try again."
UnavailableWhy, and what unlocks it, beside the control"Add a payment method to publish"
PartialCounts for both outcomes"9 of 12 files uploaded. Retry 3 failed files"

Keep the in-progress and done strings parallel, so "Saving changes…" resolves to "Changes saved".

Plurals and placeholders

Write one message per sentence in ICU MessageFormat, which most i18n libraries read. The variable sits inside the sentence and the translator moves it:

text
inbox.count = {count, plural,  =0 {No new messages}  one {# new message}  other {# new messages}}invite.sent = {name} invited you to {project}

=0 matches the number zero exactly. one and other are plural categories, and a translator adds few or many where the language needs them. Without a library, pick the category with new Intl.PluralRules(locale).select(count).

Locale formatting

Never hand-build a number, date or list. Pass the user's locale to Intl:

ValueCallen-US output
Currencynew Intl.NumberFormat(locale, { style: "currency", currency: "EUR" })€4,320.00
Unitnew Intl.NumberFormat(locale, { style: "unit", unit: "megabyte" })12 MB
Datenew Intl.DateTimeFormat(locale, { dateStyle: "medium" })Jun 30, 2026
Relative timenew Intl.RelativeTimeFormat(locale, { numeric: "auto" }).format(-1, "day")yesterday
Listnew Intl.ListFormat(locale, { type: "conjunction" })Ana, Ben, and Cy

Intl.ListFormat follows each locale's own comma and conjunction rules, so never join names with ", " and " and " by hand.