Problem
After 9823d09b (triage row B07), six components route aria strings through locale.t():
Dialog/DialogClose.vue — locale.t('Dialog.close')
AlertDialog/AlertDialogClose.vue — locale.t('AlertDialog.close')
Snackbar/SnackbarClose.vue — locale.t('Snackbar.close')
NumberField/NumberFieldIncrement.vue — locale.t('NumberField.increment')
NumberField/NumberFieldDecrement.vue — locale.t('NumberField.decrement')
Rating/RatingRoot.vue — locale.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:
- Vuetify signature alignment (for direct adapter migration)
- Ergonomic safety for unconfigured consumers (WCAG 4.1.2)
- 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.
Problem
After 9823d09b (triage row B07), six components route aria strings through
locale.t():Dialog/DialogClose.vue—locale.t('Dialog.close')AlertDialog/AlertDialogClose.vue—locale.t('AlertDialog.close')Snackbar/SnackbarClose.vue—locale.t('Snackbar.close')NumberField/NumberFieldIncrement.vue—locale.t('NumberField.increment')NumberField/NumberFieldDecrement.vue—locale.t('NumberField.decrement')Rating/RatingRoot.vue—locale.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)
Angles worth exploring
Tension
Three competing pulls, all load-bearing:
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.