What you see
Eleven accessible names in src/components/ui/ are hard-coded English string literals. On a localized storefront a screen-reader user hears them in English regardless of the active language, while a sighted user sees nothing wrong — every one of them belongs to an icon-only control.
Verified against v2026.9.0:
| File |
Line |
String |
dialog.tsx |
72 |
aria-label="Close" |
dialog.tsx |
76 |
<span className="sr-only">Close</span> |
sheet.tsx |
77 |
<span className="sr-only">Close</span> |
carousel.tsx |
200 |
<span className="sr-only">Previous slide</span> |
carousel.tsx |
230 |
<span className="sr-only">Next slide</span> |
pagination.tsx |
75 |
aria-label="Go to previous page" |
pagination.tsx |
92 |
aria-label="Go to next page" |
pagination.tsx |
115 |
<span className="sr-only">More pages</span> |
pagination.tsx |
15 |
aria-label="pagination" |
breadcrumb.tsx |
96 |
<span className="sr-only">More</span> |
breadcrumb.tsx |
8 |
aria-label="breadcrumb" |
No primitive in src/components/ui/ uses useTranslation at all — we checked every .tsx in the folder.
ELI5
The X that closes a dialog has no text in it, just an icon. So the only thing that tells a blind user what that button does is a hidden label. That label is written in English inside the component. A Spanish storefront translates its own copy fine, but the dialog still announces "Close" — and nobody testing visually will ever notice, because there is nothing to see.
Why a consumer cannot fix it
These elements are rendered inside the primitives and are not exposed:
DialogContent renders its own close button whenever showCloseButton is true (the default).
SheetContent always renders one.
CarouselPrevious / CarouselNext build the sr-only span themselves.
PaginationPrevious / PaginationNext set aria-label on the anchor they render.
There is no prop and no slot to override. A consumer's only options are to fork the file or to reach into the DOM after render. In our storefront that is 25 call sites for the dialog and sheet alone (20 DialogContent, 5 SheetContent), so patching call sites was never on the table either.
Impact
WCAG 2.2 SC 4.1.2 (Name, Role, Value) is satisfied — the name exists — but the name is in the wrong language for every non-English locale, which is 3.1.2 (Language of Parts) territory and, more practically, a control the user cannot identify. It affects every dialog, sheet, carousel, pagination and breadcrumb in a storefront, which in ours means cart, PDP, account, checkout and all Page Designer overlays.
Suggested fix
The cleanest is for the primitives to resolve their own labels, since they are the only ones that can:
const { t } = useTranslation("common")
// …
<span className="sr-only">{t("close")}</span>
with the keys added to src/locales/*/translations.json under common. That fixes every consumer at once and needs no call-site change — it is what we did in our own fork of dialog.tsx and sheet.tsx, and nothing else had to change. It does give src/components/ui/ its first dependency on the i18n provider, which may be a deliberate line the template does not want to cross.
If it is, the alternative that still fixes the default is an optional label prop whose fallback is the translation, not the literal:
function DialogContent({ closeLabel, ... }) {
const { t } = useTranslation("common")
// …
<span className="sr-only">{closeLabel ?? t("close")}</span>
What does not work is a prop with an English default: the bug then survives everywhere nobody remembers to pass it, which is the current situation with extra steps.
A small nit in the same place
dialog.tsx carries both aria-label="Close" (line 72) and <span className="sr-only">Close</span> (line 76) on the same button. aria-label wins the accessible name computation, so the span is dead text — it can never be announced. One of the two is redundant. (sheet.tsx has only the span, which is the shape upstream shadcn ships.)
Testing note, in case it is useful
Asserting the translated value is harder than it looks: t('common:close') resolves to "Close" in English, so such a test passes against a hard-coded literal, and i18next's fallback to the bare key means even a missing key matches a /close/i query. What worked for us was registering a sentinel bundle on a throwaway language and switching to it for the test — then putting the literal back and confirming the test actually fails.
What you see
Eleven accessible names in
src/components/ui/are hard-coded English string literals. On a localized storefront a screen-reader user hears them in English regardless of the active language, while a sighted user sees nothing wrong — every one of them belongs to an icon-only control.Verified against v2026.9.0:
dialog.tsxaria-label="Close"dialog.tsx<span className="sr-only">Close</span>sheet.tsx<span className="sr-only">Close</span>carousel.tsx<span className="sr-only">Previous slide</span>carousel.tsx<span className="sr-only">Next slide</span>pagination.tsxaria-label="Go to previous page"pagination.tsxaria-label="Go to next page"pagination.tsx<span className="sr-only">More pages</span>pagination.tsxaria-label="pagination"breadcrumb.tsx<span className="sr-only">More</span>breadcrumb.tsxaria-label="breadcrumb"No primitive in
src/components/ui/usesuseTranslationat all — we checked every.tsxin the folder.ELI5
The X that closes a dialog has no text in it, just an icon. So the only thing that tells a blind user what that button does is a hidden label. That label is written in English inside the component. A Spanish storefront translates its own copy fine, but the dialog still announces "Close" — and nobody testing visually will ever notice, because there is nothing to see.
Why a consumer cannot fix it
These elements are rendered inside the primitives and are not exposed:
DialogContentrenders its own close button whenevershowCloseButtonis true (the default).SheetContentalways renders one.CarouselPrevious/CarouselNextbuild thesr-onlyspan themselves.PaginationPrevious/PaginationNextsetaria-labelon the anchor they render.There is no prop and no slot to override. A consumer's only options are to fork the file or to reach into the DOM after render. In our storefront that is 25 call sites for the dialog and sheet alone (20
DialogContent, 5SheetContent), so patching call sites was never on the table either.Impact
WCAG 2.2 SC 4.1.2 (Name, Role, Value) is satisfied — the name exists — but the name is in the wrong language for every non-English locale, which is 3.1.2 (Language of Parts) territory and, more practically, a control the user cannot identify. It affects every dialog, sheet, carousel, pagination and breadcrumb in a storefront, which in ours means cart, PDP, account, checkout and all Page Designer overlays.
Suggested fix
The cleanest is for the primitives to resolve their own labels, since they are the only ones that can:
with the keys added to
src/locales/*/translations.jsonundercommon. That fixes every consumer at once and needs no call-site change — it is what we did in our own fork ofdialog.tsxandsheet.tsx, and nothing else had to change. It does givesrc/components/ui/its first dependency on the i18n provider, which may be a deliberate line the template does not want to cross.If it is, the alternative that still fixes the default is an optional label prop whose fallback is the translation, not the literal:
What does not work is a prop with an English default: the bug then survives everywhere nobody remembers to pass it, which is the current situation with extra steps.
A small nit in the same place
dialog.tsxcarries botharia-label="Close"(line 72) and<span className="sr-only">Close</span>(line 76) on the same button.aria-labelwins the accessible name computation, so the span is dead text — it can never be announced. One of the two is redundant. (sheet.tsxhas only the span, which is the shape upstream shadcn ships.)Testing note, in case it is useful
Asserting the translated value is harder than it looks:
t('common:close')resolves to"Close"in English, so such a test passes against a hard-coded literal, and i18next's fallback to the bare key means even a missing key matches a/close/iquery. What worked for us was registering a sentinel bundle on a throwaway language and switching to it for the test — then putting the literal back and confirming the test actually fails.