Skip to content

locale.t() needs a key-level fallback — WCAG 4.1.2 gap #196

@johnleider

Description

@johnleider

Problem

After 9823d09b (triage row B07), six components route aria strings through locale.t():

  • Dialog/DialogClose.vuelocale.t('Dialog.close')
  • AlertDialog/AlertDialogClose.vuelocale.t('AlertDialog.close')
  • Snackbar/SnackbarClose.vuelocale.t('Snackbar.close')
  • NumberField/NumberFieldIncrement.vuelocale.t('NumberField.increment')
  • NumberField/NumberFieldDecrement.vuelocale.t('NumberField.decrement')
  • Rating/RatingRoot.vuelocale.t('Rating.valueText', { value, size })

If a consumer hasn't registered locale messages (and v0 ships none by design), locale.t('Dialog.close') returns the literal string "Dialog.close" as the aria-label. That fails WCAG 4.1.2 Name, Role, Value — assistive tech announces the key identifier instead of a human label.

The same hole applies to createLocaleFallback() (the no-adapter path), which also returns the key verbatim.

History

Commit 245182f0 (2026-03-20, `feat(useLocale): change t() to variadic signature`) removed the `fallback?` parameter from `t()` to align with Vuetify's `t(key, ...params)` convention. The BREAKING CHANGE note advised consumers to use the fallback locale mechanism or ship default messages — but v0 itself ships no defaults and components have no way to contribute their own.

Rejected approaches (2026-04-20)

  • Re-add the fallback param (`t(key, params?, fallback?)`) — reverses the 2026-03-20 BREAKING CHANGE, re-introduces divergence from Vuetify's signature.
  • v0 ships minimal default English messages at plugin install — violates headless minimalism; v0 would own strings it currently doesn't.

Angles worth exploring

  • Component-level `ariaLabel` prop as the fallback boundary, so consumers supply English once without wiring up `useLocale`. Fallback chain becomes `prop ?? locale.t(key) ?? ???`.
  • Missing-key handler on the adapter — `createLocale({ onMissing: (key) => ... })`. Pushes the decision to app layer.
  • A `@vuetify/v0/messages/en` subpath export that consumers can `app.use(createLocalePlugin({ messages: { en } }))` — headless by default, but defaults are reachable without re-implementing them.

Tension

Three competing pulls, all load-bearing:

  1. Vuetify signature alignment (for direct adapter migration)
  2. Ergonomic safety for unconfigured consumers (WCAG 4.1.2)
  3. Headless minimalism (v0 ships no strings)

Today (1) and (3) are satisfied; (2) is the gap.

Acceptance

An approach that closes the WCAG gap without reverting (1) or violating (3). The two rejected options above are off the table.

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions