Configuration

vale.ini and the custom styles

The enforceable half: configuration and rule definitions.

The enforceable half of the style guide. Each rule carries the defect it prevents in its message, so the writer who trips it reads the reason rather than only the correction.

Rationale for every rule, and for the rules deliberately not written, is in the ruleset and why. Two checks in that list are not Vale rules, because Vale lints the text of one file at a time: link checking and required front matter run as separate steps in docs-quality.yml.

Written for Vale 3.

.vale.ini

StylesPath = .vale/styles
MinAlertLevel = warning

# Product names and approved terms, so they are never flagged as spelling
# errors. Vale 3 reads them from .vale/styles/config/vocabularies/Product/.
Vocab = Product

Packages = Google

[*.md]
BasedOnStyles = Vale, Google, Docs

# Google's rules, adjusted where they conflict with a decision recorded in
# the style guide.

# Passive voice is often correct in technical writing. Rejected outright.
Google.Passive = NO

# Docs.FutureTense covers the same ground with a narrower pattern. Running
# both reports every hit twice.
Google.Will = NO

# The help center speaks as the company, and "we" is correct there.
Google.We = NO

Google.FirstPerson = warning
Google.Headings = warning

[docs/reference/**.md]
# Generated from the OpenAPI specification. Only Vale's spelling check runs
# here: a typo in a schema description surfaces on this page first. Everything
# else about this content is governed by the Redocly ruleset that gates the
# specification itself.
BasedOnStyles = Vale

[docs/changelog.md]
# Deprecation notices state future behavior on purpose. "Will be removed" is
# a commitment; the present tense would describe it as already true.
Docs.FutureTense = NO

.vale/styles/config/vocabularies/Product/accept.txt

Orchestrator
Ledger
Sift
[Ii]dempotenc(y|e)
[Ww]ebhook
OpenAPI
Redocly
Vale
[Rr]unbook

.vale/styles/Docs/AmbiguousPronoun.yml

extends: existence
message: >-
  "%s" opens this sentence. If the previous sentence names more than one thing
  it could refer to, name the noun. A reader who has to guess will guess
  wrong about half the time, and in a procedure a wrong guess is a wrong
  action.
level: warning
scope: paragraph
nonword: true
tokens:
  - '(?<=[.!?]\s)(?:It|This|That|They)\b'

.vale/styles/Docs/DirectionalWords.yml

extends: existence
message: >-
  "%s" describes a position on the page, not a target. The reference breaks
  on a narrow screen, in a single-topic export, and for a screen reader.
  Name what you are pointing at instead.
level: error
ignorecase: true
tokens:
  - 'see (above|below)'
  - 'the (table|list|diagram|figure) (above|below)'
  - 'as shown (on the (right|left)|above|below)'
  - 'click here'
  - 'see here'

.vale/styles/Docs/ConditionAfterAction.yml

extends: existence
message: >-
  This step states its condition after its action. The reader performs the
  action before learning it was conditional. Move the condition to the
  front: "If the check fails, restart the service."
level: error
scope: list
nonword: true
tokens:
  - '^(?!(?:If|When|Unless)\b)[A-Z][a-z]+\b[^.]*?\s(?:if|when|unless)\s'

.vale/styles/Docs/StepHasOneAction.yml

extends: existence
message: >-
  This step contains more than one action. Split it. The second action is
  skipped when the reader is interrupted between them, and interruption
  during a procedure is the normal case.
level: error
scope: list
nonword: true
tokens:
  - '^(?:Enter|Select|Run|Open|Copy|Install|Restart|Delete|Add)\b[^.]*\b(?:and then|, then|and)\s+(?:enter|select|run|open|copy|install|restart|delete|add)\b'

.vale/styles/Docs/StepLength.yml

extends: occurrence
message: >-
  This list item runs to more than 20 words. A reader performing a procedure
  holds the whole step in working memory before acting. Split it.
level: warning
scope: list
max: 20
token: '\b\w+\b'

.vale/styles/Docs/PlaceholderLeft.yml

extends: existence
message: '"%s" is a placeholder, and this page is about to ship with it.'
level: error
tokens:
  - 'TODO'
  - 'TKTK'
  - 'FIXME'
  - 'XXX'
  - '[Ll]orem ipsum'
  - '\[[Ii]nsert [^\]]{0,30}\]'

.vale/styles/Docs/LinkText.yml

extends: existence
message: >-
  The link text carries no meaning out of context: "%s". Screen-reader users
  navigate by a list of links with the surrounding prose stripped away.
  Describe the destination.
level: error
ignorecase: true
scope: raw
nonword: true
tokens:
  - '\[(?:here|this|link|more|read more|learn more|click here)\]\('
  - '\[https?://[^\]]*\]\('
  - '<https?://[^>]+>'

.vale/styles/Docs/AltText.yml

extends: existence
message: >-
  This image has no usable alt text. Where a diagram carries information the
  prose does not, a screen-reader user loses the page.
level: error
scope: raw
nonword: true
tokens:
  - '!\[\s*\]\('
  - '!\[.{1,4}\]\('
  - '<img(?![^>]*\balt\s*=\s*"[^"]{5,})[^>]*>'

.vale/styles/Docs/TermSubstitutions.yml

extends: substitution
message: >-
  Use "%s" rather than "%s". A reader searching for the word on the screen
  finds nothing when three articles use three words for one action.
level: warning
ignorecase: true
swap:
  'sign in|signin|log on': log in
  'sign out|signout|log off': log out
  'remove the app': uninstall
  'hit (?:the )?enter': press Enter
  'tick the box': select the checkbox
  'gr[ae]yed[ -]out': unavailable

.vale/styles/Docs/Wordiness.yml

extends: substitution
message: >-
  Use "%s" rather than "%s". The longer form adds reading time and no
  meaning.
level: warning
ignorecase: true
# "in order to" is not listed: Google.WordListCase already reports it.
swap:
  at this point in time: now
  is able to: can
  prior to: before
  make use of: use
  for the purpose of: for

.vale/styles/Docs/FutureTense.yml

extends: existence
message: >-
  "%s" states system behavior as a prediction. Use the present tense: "the
  service starts", not "the service will start". The reader is deciding
  whether to depend on this behavior.
level: warning
tokens:
  - '\bwill (?:be |then )?(?:start|stop|return|send|create|display|show|appear|remove)\w*'

.vale/styles/Docs/VersionedCommands.yml.tmpl

Vale rules cannot read another file, so this one is generated. The template is committed; CI substitutes the current release from docs/_data/versions.yml before Vale runs, and the generated VersionedCommands.yml is listed in .gitignore.

extends: existence
message: >-
  Version %s does not match the current release, __CURRENT__. Version numbers
  in prose rot silently: nothing fails until a reader runs the command.
level: error
scope: raw
nonword: true
tokens:
  - '(?<=[@=v])(?!__CURRENT_RE__\b)\d+\.\d+\.\d+\b'

scope: raw is deliberate. Version numbers live in code blocks, and Vale skips code blocks in every other scope.

Suppressing a rule

A rule that is wrong for one specific passage is suppressed inline, with the reason in a comment beside it:

<!-- The label is quoted exactly as the product shows it. -->
<!-- vale Docs.TermSubstitutions = NO -->
Select **Sign in with SSO**.
<!-- vale Docs.TermSubstitutions = YES -->

Suppressions are reviewed quarterly. A cluster around one rule means the rule has false positives, and the fix is the rule, not the suppressions.

← Back to Portfolio