07 - Voice & Copywriting
Microcopy is code. A bad button label costs more than a bad variable name.
This chapter codifies every string the user sees. Follow Ant’s copy chapter (ant.design/docs/spec/copywriting) closely, layer Linear’s plain-language ethos on top.
Rule of thumb: say what the user can do, not what the system can do. “Send invitation” beats “Invitation will be sent”.
Tone by surface
Same tokens, different voice. Keep them distinct on purpose.
| Surface | Tone | Example |
|---|---|---|
| Admin | Concise, direct, technical | ”Save changes”, “Export failed: quota exceeded” |
| Chat (end-user) | Warm, conversational | ”Hi — what would you like to know?” |
| Error / exception | Objective, actionable | ”Unable to save. Name must be unique.” |
| Success | Calm, brief | ”Saved” |
| Empty state | Inviting, specific | ”No agents yet. Create your first one to get started.” |
| Onboarding tooltip | Helpful, light | ”Tip: press ⌘K to search” |
Admin is a tool. Chat is a conversation partner. Don’t confuse them.
Global copy rules
Punctuation
- Labels, buttons, titles, tooltips, table headers, menu items: no trailing period.
- Sentences in body text / descriptions: period is fine.
- Exclamation marks: greetings (“Welcome!”) and congratulations (“Import complete!”) only. Never in errors, never in neutral text.
- Em dashes: avoid. Use commas, periods, or ”…”.
- Curly quotes: use straight quotes in UI strings (simpler to diff/translate).
Capitalization
- Sentence case for everything — labels, buttons, titles, table headers, empty-state messages.
- Title Case only for product names (“Nexus Agentic”, “Nexus Admin”) and proper nouns.
- Never ALL CAPS for emphasis. Use
font-semiboldif you need emphasis.
Sentence case examples:
- “Create agent” ✓
- “Create Agent” ✗
- “CREATE AGENT” ✗
Numbers
Always Arabic numerals. 1,234 not “one thousand two hundred thirty-four”. Transmit faster, translate cleanly.
- Dates → see 05-patterns.md → Data Format.
- Counts in labels: “3 items” not “Three items”.
- Range: “1-10” with hyphen, not “1 to 10”.
- File sizes: binary units with a space before the suffix —
12 KB,3.4 MB,1.2 GB. Round to one decimal from MB and up; integers under 1 MB. Use theformatByteshelper in@na/utils, notIntl.NumberFormat(it doesn’t cover binary sizes).
Keyboard shortcuts — display format
Shortcut hints appear inside tooltips, menu items, and <kbd>-styled inline labels. The format adapts to the user’s platform:
| Key | macOS | Windows / Linux |
|---|---|---|
| Command | ⌘ | Ctrl |
| Option | ⌥ | Alt |
| Shift | ⇧ | Shift |
| Enter | ↵ | Enter |
| Delete | ⌫ | Backspace |
Display examples: ⌘K on macOS, Ctrl+K on others. Combine with + (no space) on non-Mac. Detect via navigator.platform.startsWith('Mac') in a useIsMac() hook; never ship a single string for both platforms. For chord shortcuts, join with space: g t (go to tools). Always wrap in <kbd> for semantic meaning; style defaults to font-mono text-xs border px-1 rounded.
<code> — when raw mono style is allowed
Monospace font appears in three cases only:
- Identifiers: agent IDs, doc IDs, tool names (
send_email), env var names. - File paths and URLs:
/api/v1/agents,~/.gstack/projects. - Code snippets in help text, onboarding, or error messages.
Never use mono for user-authored content (agent names, document titles) — that’s a typography mismatch that looks like a bug. Wrap in <code> (or font-mono text-sm) and keep it inline; multi-line code uses a <pre> block.
Vocabulary: pick one name per concept (C3)
Canonical names, with rejected synonyms. Enforced in UI strings, identifiers, docs.
| Canonical | Use for | Rejected synonyms |
|---|---|---|
| Agent | The AI configuration (admin side) | Bot, Assistant (user-facing in chat only — see below), Persona, Model |
| Assistant | End-user-facing name for an Agent (chat UI label only) | Bot, Companion, Buddy |
| Tool | A function an agent can invoke | Skill, Plugin, Extension, Action |
| Knowledge base | Collection of documents for RAG | KB, Wiki, Corpus, Doc store |
| Thread | LangGraph-side conversation session | Session, Chat |
| Conversation | App-level record (User + Agent + Thread) | Chat, Dialog, Session |
| Workflow | Agent’s graph of nodes (1:1 with agent) | Graph, Flow, Pipeline, Process |
| Channel | External integration (Slack, Zalo, web) | Integration, Connector |
| Widget | Embeddable chat UI for external sites | Embed, iframe, Popup |
| Variable | Named input binding for agent configuration | Param, Parameter, Context item |
| API key | Credential for programmatic access | Token, Secret (except when we mean the JWT) |
| Member | Human user of the admin app | User (except for low-level tech contexts) |
| Role | Permission grouping | Group, Access level |
When you must use a rejected synonym (external API name, legacy URL), leave a comment explaining why. Never let the rejected term spread into new code.
Authoritative source: .claude/rules/agent-terminology.md.
Product names
| Name | When |
|---|---|
| Nexus Agentic | Product umbrella |
| Nexus Admin | Admin app (internal use only; users just see “Admin”) |
| Nexus Chat | Chat app (end-user sees “Chat”) |
Title Case for these. Never hyphenated. Never all-caps.
Button labels — the most important strings
Structure
Verb + noun. Specific. Matches what happens.
| Good | Bad |
|---|---|
| ”Save" | "OK" |
| "Save changes" | "Submit" |
| "Create agent" | "Create” (when in multi-entity context) |
| “Delete agent" | "Delete” (destructive = be explicit) |
| “Send invitation" | "Send" |
| "Add tool" | "New" |
| "Cancel" | "No” (confusing in a dialog) |
| “Discard changes" | "Yes” (confusing in a dialog) |
| “Sign in" | "Login” (one word, not a verb) |
| “Sign out" | "Log out” (inconsistent with “Sign in”) |
Benefit-labeled CTAs (Ant)
When a button can be named by the user benefit, use that.
- “Start free trial” > “Submit”
- “Get my report” > “Generate”
- “Save and keep editing” > “Save”
No-op labels to avoid
- “OK”, “Confirm”, “Submit”, “Go”, “Apply” (rare) — all too generic.
- “Click here” — inline link labels should describe the destination.
- “More” — say what more of.
Destructive button copy
Always the verb. Always explicit. Never “OK” or “Confirm” in a delete dialog.
<ConfirmDialog confirmLabel="Delete agent" // ✓ verb + noun variant="destructive" .../>
// NOT:confirmLabel="OK" // ✗confirmLabel="Confirm" // ✗confirmLabel="Delete" // ok, but "Delete agent" is clearerconfirmLabel="Yes" // ✗Cancel vs discard vs close
- Cancel — undo a pending action (dialog cancel, form cancel, cancel a mutation).
- Discard changes — throw away unsaved edits (unsaved-changes dialog).
- Close — dismiss a non-destructive panel (detail pane, info dialog).
- Back — go up a level (back to list).
Form labels
Sentence case, no colons, no periods
<Label>Display name</Label> // ✓<Label>Display Name:</Label> // ✗ (Title case + colon)<Label>Display name.</Label> // ✗ (period)Required marker
One asterisk suffix. aria-required="true" on the input.
<Label>Email <span aria-hidden="true">*</span></Label><Input aria-required="true" />Optional marker
Don’t mark optional fields if most are required. If most are optional, mark the required ones. Never mark both.
Help text
Use <p className="text-muted-foreground text-xs"> below the input. Always visible.
- Good: “Shown on your public profile.”
- Bad:
<Input placeholder="This is shown on your profile...">(placeholder disappears)
Error messages
Objective. Actionable. No blame. Error text is the point where voice most often fails.
Template
<What went wrong>. <What to do next>.
| Bad | Good |
|---|---|
| ”Error: 500 Internal Server Error" | "Unable to save. Try again in a moment." |
| "You entered a bad value" | "Email must be a valid address" |
| "Failed to load agents" | "Couldn’t load agents. Check your connection and try again." |
| "Something went wrong" | "Upload failed. File must be under 5 MB." |
| "Operation not permitted" | "You don’t have permission to delete this agent." |
| "Name cannot be empty" | "Name is required" |
| "Not found" | "Agent not found. It may have been deleted.” |
Don’t blame the user
- “You entered…” → “The input must be…”
- “Your file is too big” → “File must be under 5 MB”
- “Incorrect password” → “Email or password is incorrect” (don’t leak which one)
Don’t expose stack traces
Strip server stack traces before display. If the user needs to contact support, show an opaque ID they can share, not the raw error.
// extractErrorMessage() in @na/utils parses ApiError JSON and returns the user-safe message.Don’t apologize excessively
- “We’re sorry, something went wrong” → “Something went wrong. Try again.”
- Sorry is fine once per error, not three times per line.
Empty state copy
Four parts: title (fact), description (what to do next), primary action (the verb), optional secondary (alternative path).
| Part | Rule | Example |
|---|---|---|
| Title | State the fact, don’t dramatize | ”No agents yet” |
| Description | Explain the value + next step, not what’s missing | ”Create your first agent to get started.” |
| Primary CTA | Verb + noun | ”Create agent” |
| Secondary | Optional alternative | ”Import from template” |
Bad:
- Title: ”😔 It’s empty here!” (don’t dramatize)
- Title: “0 Agents” (useless)
- Description: “You haven’t created any agents” (blame + stating the obvious)
- CTA: “Get started” (vague)
Good:
- Title: “No agents yet”
- Description: “Agents are the AI assistants you connect to your business data.”
- CTA: “Create agent”
Empty state variants (copy)
| Variant | Title | Description |
|---|---|---|
| Blank slate | ”No agents yet" | "Agents are AI assistants connected to your data. Create your first one.” |
| Filtered-to-zero | ”No agents match" | "Try adjusting your search or filters. [Clear filters]“ |
| Permission-blocked | ”You don’t have access" | "Ask your admin to grant Agent Manager to your account.” |
| Network / error | ”Couldn’t load agents" | "Check your connection and try again. [Retry]“ |
Confirmation dialogs
Title
A question that states the consequence.
| Bad | Good |
|---|---|
| ”Are you sure?" | "Delete this agent?" |
| "Please confirm" | "Sign out?" |
| "Warning" | "Remove this tool from the agent?” |
Description
Explain the consequence. What changes, what’s lost, whether it’s reversible.
| Example |
|---|
| ”This permanently deletes the agent, all threads, and all associated data. This cannot be undone." |
| "The tool will stop responding until you add it back." |
| "Any unsaved changes will be discarded.” |
Buttons
[Cancel] + action verb. Never [Cancel][OK].
[Cancel] [Delete agent][Cancel] [Sign out][Keep editing] [Discard changes]Success messages
Short. Stop dancing. Users already know they saved.
| Bad | Good |
|---|---|
| ”Congratulations! Saved!" | "Saved" |
| "Successfully deleted!" | "Deleted" |
| "Operation completed.” | (ActionButton success tick is enough) |
Loading copy
| Context | Copy |
|---|---|
| Mutation pending | pendingText="Saving…" |
| Background job | "Importing 234 of 1,000 rows" |
| Initial page | Skeleton (no copy) |
| Large file | "Uploading your file…" + progress |
Use the single ellipsis character (…) not three periods (...). It’s one character and one character’s worth of spacing.
Tooltip copy
One line. Provides extra context, not a restated label.
| Bad (restates) | Good (adds context) |
|---|---|
tooltip="Save" | tooltip="Save without reloading (Cmd+S)" |
tooltip="Delete" | tooltip="Delete this agent and its data" |
tooltip="Edit" | tooltip="Edit this row" (OK for icon-only buttons) |
If the explanation is > one line, use an inline helper text under the element instead of cramming into a tooltip.
Keyboard shortcut copy
In tooltips
Show shortcut at the end in <kbd> style:
tooltip="Save (Cmd+S)"tooltip="Open command palette (Cmd+K)"Use Cmd on Mac-style docs; the OS-aware display can swap to Ctrl at render time.
In shortcut overlays (? key)
Grouped by category, aligned:
Navigation Cmd+K Open command palette / Focus search j / k Move down / up
Editing Cmd+S Save Esc Canceli18n awareness (previewed here, full rules in 09)
Our admin app is English-first but supports translated strings via react-i18next. Copy rules that matter for i18n:
- Don’t concatenate strings.
"Found " + count + " results"breaks in many languages. Use ICU:"Found {count, plural, one {# result} other {# results}}". - Avoid idioms that don’t translate (“the ball is in your court”).
- Keep labels short — translations often grow 20-40% (German famous for this).
- Don’t hardcode plurals — use ICU
pluralor equivalent.
Full i18n conventions in 09-i18n-testing.md.
Copy audit checklist
Before shipping any new screen:
- Every button label is a verb + noun (or a verb-only if unambiguous).
- No “OK” / “Confirm” / “Submit” in dialogs.
- No periods at the end of labels, buttons, titles, table headers, tooltips.
- Sentence case everywhere (except product names).
- No ALL CAPS for emphasis.
- No exclamation marks outside greetings / congratulations.
- No blame in error messages (“File must be under 5 MB”, not “You uploaded a huge file”).
- Empty state has icon + title (fact) + description (next step) + primary action.
- Canonical terms used (Agent, Tool, Workflow, Channel).
- No raw server error text exposed to user.
- Tooltips add context, don’t restate the label.
- Arabic numerals everywhere.
- No em dashes.
- No curly quotes.
- Single ellipsis character (
…) for loading states.
Next: 08-accessibility.md.