Make component text localizable (stop baking in English)#211
Merged
Conversation
Plane localizes into many languages, but several components hardcoded English in the markup — accessible labels and placeholders a consumer can't replace. Each baked string is now consumer-provided: - Search / ExpandableSearch: drop the "Search" placeholder default and the "Search" aria-label fallback; add a required clearLabel for the clear button. - MenuSearch: drop the "Search" placeholder default. - NumberField: add required decrementLabel / incrementLabel (were "Decrease" / "Increase"). - Toast: add a required closeLabel, threaded through ToastList and ToastProvider (was "Dismiss"). - Banner: BannerDismiss now requires aria-label; the ready-made pairs onDismiss with a required dismissLabel. - Breadcrumb: the nav landmark aria-label is now required (was "Breadcrumb"). - Badge: onDismiss pairs with a required dismissLabel (was defaulting to "Remove"). Stories pass the English demo text themselves.
e0c3ada to
cdfe3d5
Compare
|
📚 Storybook preview: https://pr-211-propel-storybook.vamsi-906.workers.dev |
…miss) The ready-made Badge baked an onDismiss/dismissLabel dismiss button, which is outside the spec — the badge has a leading and a trailing node slot. Replaced onDismiss/dismissLabel with a symmetric inlineEndNode (matching inlineStartNode); both are decorative icon slots wrapped in BadgeIcon. This also removes the last baked English (the "Remove" dismiss default). BadgeDismiss stays a ui atom for hand-composed removable badges (demoed in the UI/Badge story).
BannerDismiss and SearchClear were just ghost IconButtons. Removed both (ui parts + cvas + exports) and render IconButton instead. The Banner no longer has an onDismiss/dismissLabel API — a dismiss is just an IconButton the consumer puts in the actions slot. Added docs/icon-button-dedup-review.md listing the remaining candidates to review (ToastClose, AutocompleteClear, ComboboxClear, BadgeDismiss) and the keep-list (the pure DialogClose-style behavior wrappers).
…managed a11y)
AutocompleteClear/AutocompleteTrigger re-implemented a ghost icon button (shared
autocompleteButtonVariants). Destyled both to pure Base UI behavior wrappers and render them via
IconButton. Used the behavior-outer direction — <AutocompleteClear render={<IconButton/>}> not
<IconButton render={<AutocompleteClear/>}> — because Base UI Autocomplete.Clear manages
aria-hidden/focusability (hidden when empty); IconButton-outer forces a focusable aria-hidden button
(axe aria-hidden-focus). Deleted autocompleteButtonVariants + the obsolete components-tier wrappers.
Documented the two render directions in the review doc.
Same treatment as autocomplete. ComboboxClear/ComboboxTrigger and NumberFieldIncrement/Decrement re-implemented a ghost icon button (comboboxButtonVariants / numberFieldButtonVariants). Destyled all to pure Base UI behavior wrappers and render them behavior-outer via IconButton. Deleted both cvas, the NumberFieldButtonIcon slot, and the obsolete components-tier combobox wrappers. Updated the review doc; toast still pending (its close needs a positioned wrapper for the 4px corner offset).
ToastClose re-implemented a ghost icon button plus an absolute 4px-corner position. Destyled it to a pure Base UI behavior wrapper; the close is now an IconButton (ghost/neutral/sm, IconButton-outer). The absolute inset-e-1 top-1 offset can't live on the IconButton (no className), so it moved to a new ToastCloseSlot ui part — position is toast layout, chrome is the IconButton. toastCloseVariants deleted. This completes the dismiss/clear/trigger/close/stepper → IconButton sweep; only BadgeDismiss stays (too small for IconButton's 20px floor — design call).
The badge spec is two symmetric node slots (inlineStartNode/inlineEndNode), not a dismiss button. Removed the BadgeDismiss ui part, badgeDismissVariants (+ the now-unused cx import), the ui + components index exports, and the UI/Badge WithDismiss story; updated the doc comments and the icon-button review doc.
"Box" is a non-Base-UI synonym; Base UI names the bordered control-frame "Group"/"InputGroup" (NumberField.Group, Autocomplete.InputGroup), which the rest of propel already follows (NumberFieldGroup, AutocompleteInputGroup, ComboboxInputGroup). Renamed the two off-convention parts plus their cvas, types, files, index exports, stories, and the field compositions to match.
- resolvedAriaLabel was a no-op pass-through (const resolvedAriaLabel = ariaLabel;) left after the i18n change removed the "Search" fallback — inlined ariaLabel in Search + ExpandableSearch. - SearchBox (the "Search as SearchBox" alias for the ui Search frame) renamed to SearchElement, matching the rule-5 ...Element convention (BadgeElement/BannerElement/IconButtonElement). - Cleared the last stray Box identifiers: labelPillBox to labelPillBase (private pill cva fragment), restingBox/invalidBox to restingGroup/invalidGroup (field-story test locals), and a stale comment naming InputFieldBox/TextAreaFieldBox to InputGroup/TextAreaGroup.
There was a problem hiding this comment.
Pull request overview
This PR removes baked-in English accessible labels/placeholders from several Propel primitives and shifts responsibility to consumers (or stories) to provide localized strings. It also consolidates multiple “icon-only control” implementations by composing Base UI behavior wrappers under the canonical IconButton, reducing duplicated styling logic.
Changes:
- Remove default English labels/placeholders (e.g., search placeholder / aria-label fallbacks; toast close label; number-field stepper labels) and require consumer-provided labels instead.
- Refactor “dismiss / clear / trigger” controls across Toast, Search, NumberField, Autocomplete, and Combobox to compose
IconButtonrather than bespoke button+icon slots. - Rename a few framing primitives (
InputBox→InputGroup,TextAreaBox→TextAreaGroup) and delete non-canonical badge/banner dismiss atoms.
Reviewed changes
Copilot reviewed 88 out of 88 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/propel/src/ui/toast/variants.ts | Replace toast close styling with a minimal positioned close slot variant |
| packages/propel/src/ui/toast/toast.stories.tsx | Update UI toast story to compose close via IconButton + ToastCloseSlot |
| packages/propel/src/ui/toast/toast-close.tsx | Destyle ToastClose into a behavior-only wrapper |
| packages/propel/src/ui/toast/toast-close-slot.tsx | Add new positioned slot component for toast close placement |
| packages/propel/src/ui/toast/index.tsx | Export the new ToastCloseSlot part |
| packages/propel/src/ui/text-area/variants.ts | Rename textarea frame variants to “group” naming |
| packages/propel/src/ui/text-area/text-area.stories.tsx | Update UI textarea story to use TextAreaGroup |
| packages/propel/src/ui/text-area/text-area-group.tsx | Rename TextAreaBox API to TextAreaGroup |
| packages/propel/src/ui/text-area/index.tsx | Re-export text-area-group instead of text-area-box |
| packages/propel/src/ui/search/variants.ts | Remove SearchClear styling variants |
| packages/propel/src/ui/search/search.tsx | Update docs to refer to IconButton clear control composition |
| packages/propel/src/ui/search/search.stories.tsx | Replace SearchClear usage with IconButton composition in UI stories |
| packages/propel/src/ui/search/search-expandable.tsx | Update docs to refer to IconButton clear control composition |
| packages/propel/src/ui/search/search-clear.tsx | Delete SearchClear atom |
| packages/propel/src/ui/search/index.tsx | Stop exporting SearchClear |
| packages/propel/src/ui/pill/variants.ts | Minor internal naming cleanup (labelPillBox→labelPillBase) |
| packages/propel/src/ui/number-field/variants.ts | Remove bespoke stepper button/icon variants; keep shared magnitude type |
| packages/propel/src/ui/number-field/number-field.stories.tsx | Update UI number field story to render steppers via IconButton composition |
| packages/propel/src/ui/number-field/number-field-increment.tsx | Destyle increment into a behavior-only wrapper |
| packages/propel/src/ui/number-field/number-field-decrement.tsx | Destyle decrement into a behavior-only wrapper |
| packages/propel/src/ui/number-field/number-field-button-icon.tsx | Delete NumberFieldButtonIcon slot |
| packages/propel/src/ui/number-field/index.tsx | Stop exporting NumberFieldButtonIcon |
| packages/propel/src/ui/input/variants.ts | Rename input frame variants (inputBoxVariants→inputGroupVariants) |
| packages/propel/src/ui/input/input.stories.tsx | Update UI input story to use InputGroup |
| packages/propel/src/ui/input/input-group.tsx | Add InputGroup frame component |
| packages/propel/src/ui/input/input-box.tsx | Delete InputBox frame component |
| packages/propel/src/ui/input/index.tsx | Export InputGroup instead of InputBox |
| packages/propel/src/ui/combobox/variants.ts | Remove combobox button variants in favor of composing IconButton |
| packages/propel/src/ui/combobox/combobox.stories.tsx | Update UI combobox story to render clear/trigger via IconButton |
| packages/propel/src/ui/combobox/combobox-trigger.tsx | Destyle trigger into a behavior-only wrapper with composition docs |
| packages/propel/src/ui/combobox/combobox-clear.tsx | Destyle clear into a behavior-only wrapper with composition docs |
| packages/propel/src/ui/breadcrumb/breadcrumb.tsx | Require consumer-provided aria-label (no English default) |
| packages/propel/src/ui/breadcrumb/breadcrumb.stories.tsx | Update UI breadcrumb stories to pass required aria-label |
| packages/propel/src/ui/banner/variants.ts | Remove banner-dismiss styling variants (dismiss no longer an atom) |
| packages/propel/src/ui/banner/index.tsx | Stop exporting BannerDismiss |
| packages/propel/src/ui/banner/banner.tsx | Update docs to describe optional dismiss IconButton composition |
| packages/propel/src/ui/banner/banner.stories.tsx | Update UI banner stories to use IconButton as dismiss control |
| packages/propel/src/ui/banner/banner-dismiss.tsx | Delete BannerDismiss atom (and its baked label) |
| packages/propel/src/ui/badge/variants.ts | Remove badge dismiss variants; clarify label composition docs |
| packages/propel/src/ui/badge/index.tsx | Stop exporting BadgeDismiss |
| packages/propel/src/ui/badge/badge.tsx | Update badge docs to remove BadgeDismiss references |
| packages/propel/src/ui/badge/badge.stories.tsx | Remove UI “WithDismiss” story and related imports |
| packages/propel/src/ui/badge/badge-label.tsx | Update label docs to describe symmetric icon slots (no dismiss) |
| packages/propel/src/ui/badge/badge-dismiss.tsx | Delete BadgeDismiss atom |
| packages/propel/src/ui/autocomplete/variants.ts | Remove autocomplete button variants in favor of composing IconButton |
| packages/propel/src/ui/autocomplete/autocomplete.stories.tsx | Update UI autocomplete story to render clear/trigger via IconButton |
| packages/propel/src/ui/autocomplete/autocomplete-trigger.tsx | Destyle trigger into a behavior-only wrapper with composition docs |
| packages/propel/src/ui/autocomplete/autocomplete-clear.tsx | Destyle clear into a behavior-only wrapper with composition docs |
| packages/propel/src/components/toast/toast.tsx | Require closeLabel and compose close via ToastCloseSlot + IconButton |
| packages/propel/src/components/toast/toast.stories.tsx | Provide closeLabel in stories and provider usage |
| packages/propel/src/components/toast/toast-provider.tsx | Require closeLabel and thread it into the toast list |
| packages/propel/src/components/toast/toast-list.tsx | Accept closeLabel and pass it to each toast instance |
| packages/propel/src/components/toast/index.tsx | Re-export ToastCloseSlot from components entrypoint |
| packages/propel/src/components/text-area/text-area.stories.tsx | Update components textarea stories to use TextAreaGroup |
| packages/propel/src/components/text-area/index.tsx | Update docs to reference TextAreaGroup name |
| packages/propel/src/components/text-area-field/text-area-field.tsx | Use TextAreaGroup in field composition |
| packages/propel/src/components/text-area-field/text-area-field.stories.tsx | Update story/test wording and variable names for “group” |
| packages/propel/src/components/search/search.tsx | Remove default placeholder/label fallbacks; require clearLabel |
| packages/propel/src/components/search/search.stories.tsx | Update components search stories to pass required labels/placeholder |
| packages/propel/src/components/search/index.tsx | Stop re-exporting SearchClear from components |
| packages/propel/src/components/search/expandable-search.tsx | Remove default placeholder/label fallbacks; require clearLabel |
| packages/propel/src/components/number-field/number-field.tsx | Require localized increment/decrement labels; compose steppers via IconButton |
| packages/propel/src/components/number-field/number-field.stories.tsx | Provide required increment/decrement labels in story args |
| packages/propel/src/components/number-field/index.tsx | Stop exporting NumberFieldButtonIcon from components |
| packages/propel/src/components/menu/menu.stories.tsx | Update menu stories to pass MenuSearch placeholder explicitly |
| packages/propel/src/components/menu/menu-search.tsx | Remove default “Search” placeholder fallback |
| packages/propel/src/components/input/input.stories.tsx | Update components input story to use InputGroup |
| packages/propel/src/components/input/index.tsx | Update docs to reference InputGroup naming |
| packages/propel/src/components/input-field/input-field.tsx | Use InputGroup for field framing |
| packages/propel/src/components/input-field/input-field.stories.tsx | Update story/test wording and variable names for “group” |
| packages/propel/src/components/combobox/index.tsx | Re-export UI behavior-only clear/trigger parts; remove components wrappers |
| packages/propel/src/components/combobox/combobox.stories.tsx | Update components combobox stories to compose IconButton via render |
| packages/propel/src/components/combobox/combobox-trigger.tsx | Delete components-tier trigger wrapper with default glyph |
| packages/propel/src/components/combobox/combobox-clear.tsx | Delete components-tier clear wrapper with default glyph |
| packages/propel/src/components/combobox-field/combobox-field.tsx | Compose UI behavior-only clear/trigger under IconButton |
| packages/propel/src/components/breadcrumb/breadcrumb.stories.tsx | Update components breadcrumb stories to pass required aria-label |
| packages/propel/src/components/banner/index.tsx | Stop re-exporting BannerDismiss from components |
| packages/propel/src/components/banner/banner.tsx | Remove onDismiss API; treat dismiss as consumer-provided action node |
| packages/propel/src/components/banner/banner.stories.tsx | Update banner stories to render dismiss as an IconButton action |
| packages/propel/src/components/badge/index.tsx | Stop re-exporting BadgeDismiss from components |
| packages/propel/src/components/badge/badge.tsx | Replace dismiss API with symmetric inlineEndNode icon slot |
| packages/propel/src/components/badge/badge.stories.tsx | Update stories to demonstrate trailing icon slot vs dismiss action |
| packages/propel/src/components/autocomplete/index.tsx | Re-export UI behavior-only clear/trigger parts; remove components wrappers |
| packages/propel/src/components/autocomplete/autocomplete.stories.tsx | Update components autocomplete stories to compose IconButton via render |
| packages/propel/src/components/autocomplete/autocomplete-trigger.tsx | Delete components-tier trigger wrapper with default glyph |
| packages/propel/src/components/autocomplete/autocomplete-clear.tsx | Delete components-tier clear wrapper with default glyph |
| packages/propel/src/components/autocomplete-field/autocomplete-field.tsx | Compose UI behavior-only clear/trigger under IconButton |
| packages/propel/docs/icon-button-dedup-review.md | Add review doc explaining icon-button dedup and composition rules |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| import { expect, fn } from "storybook/test"; | ||
|
|
||
| import { Button } from "../button/index"; | ||
| import { IconButton } from "../icon-button"; |
Comment on lines
+134
to
+136
| > | ||
| <X /> | ||
| </IconButton> |
Comment on lines
+164
to
+166
| > | ||
| <X /> | ||
| </IconButton> |
Comment on lines
83
to
87
| (Story) => ( | ||
| <ToastProvider> | ||
| <ToastProvider closeLabel="Dismiss"> | ||
| <Story /> | ||
| </ToastProvider> | ||
| ), |
Comment on lines
30
to
34
| /** | ||
| * Accessible name for the field. The consumer must supply this (or `aria-labelledby`) to name the | ||
| * field. | ||
| */ | ||
| "aria-label"?: string; |
Comment on lines
+35
to
40
| /** | ||
| * Trailing actions, placed after the message — e.g. buttons, or a dismiss `IconButton` | ||
| * (`<IconButton aria-label="Dismiss" onClick={…}><X /></IconButton>`). | ||
| */ | ||
| actions?: React.ReactNode; | ||
| /** When provided, shows a dismiss button that calls this on click. */ | ||
| onDismiss?: () => void; | ||
| }; |
…d-lg) First step of folding Search into Autocomplete. Added AutocompleteInputIcon (a decorative leading magnifier slot, aria-hidden, mirrors SearchIcon); a magnitude axis (sm/md/lg = 28/32/36px + node sizes, matching Search) plus rounded-lg + w-full on the input group; and a text-size axis on the input. A new Search story renders the autocomplete pixel-identical to today's Search (verified visually). AutocompleteField pins its control to lg (the old fixed 36px box; the field's md/lg/xl label scale is separate). Verified Autocomplete.Icon is Base UI's chevron indicator, so the magnifier is correctly a propel decorative part. Expandable + the Search removal are the follow-up tasks.
Following the clear/trigger pattern, the ready-mades no longer bake icon-buttons + label strings — they take node props the consumer supplies with their own localized aria-label: - AutocompleteField/ComboboxField: controlLabel → clear + trigger node props; emptyLabel → empty. - NumberField: decrementLabel/incrementLabel → decrement/increment node props. - Toast: closeLabel → a close node prop, threaded through ToastProvider → ToastList → Toast. This drops the last baked English (the 'Clear'/'Open' prefixes) and the baked glyphs (X/±/chevron). Search's clearLabel is left as-is — it's being folded into Autocomplete.
Comment on lines
30
to
34
| /** | ||
| * Accessible name for the field. The consumer must supply this (or `aria-labelledby`) to name the | ||
| * field. | ||
| */ | ||
| "aria-label"?: string; |
| import { expect, fn } from "storybook/test"; | ||
|
|
||
| import { Button } from "../button/index"; | ||
| import { IconButton } from "../icon-button"; |
Comment on lines
+134
to
+136
| > | ||
| <X /> | ||
| </IconButton> |
Comment on lines
+164
to
+166
| > | ||
| <X /> | ||
| </IconButton> |
Comment on lines
+35
to
39
| /** | ||
| * Trailing actions, placed after the message — e.g. buttons, or a dismiss `IconButton` | ||
| * (`<IconButton aria-label="Dismiss" onClick={…}><X /></IconButton>`). | ||
| */ | ||
| actions?: React.ReactNode; |
AutocompleteInputIcon hand-rolled a useRender <span aria-hidden> — which is exactly what Base UI's Autocomplete.Icon (= Combobox.Icon) already is: a <span aria-hidden> icon slot with a default indicator glyph you override via children. Renamed to AutocompleteIcon and wrapped BaseAutocomplete.Icon (rule 6a — mirror Base UI's anatomy), styled via the cva. Magnifier renders identically; verified visually + the autocomplete axe stories pass.
Deleted components/search (Search, ExpandableSearch) and ui/search (the Search primitives + the collapse/expand chrome). Search's role is now the search-styled Autocomplete (leading magnifier via Autocomplete.Icon + rounded-lg + magnitude). Nothing imported them — the MenuSearch* parts are a separate thing in ui/menu — and the package's wildcard exports (./components/*, ./ui/*) drop the subpaths automatically. The SearchExpandable/Viewport expand chrome lives in git history for when the expandable Autocomplete wrapper is built.
Comment on lines
5
to
7
| import { Button } from "../button/index"; | ||
| import { IconButton } from "../icon-button"; | ||
| import { |
| aria-label="Dismiss" | ||
| onClick={fn()} | ||
| > | ||
| <X /> |
| aria-label="Dismiss" | ||
| onClick={dismissSpy} | ||
| > | ||
| <X /> |
Comment on lines
+24
to
+28
| - **`BannerDismiss`** — removed. The `Banner` has no dismiss API (no `onDismiss`/`dismissLabel`); a | ||
| dismiss is just a ghost `IconButton` the consumer renders in the `actions` slot. | ||
| - **`SearchClear`** — removed; `Search` / `ExpandableSearch` now render `IconButton` (ghost/neutral, | ||
| sm → md by search size) for the clear action. | ||
| - **`AutocompleteClear` / `AutocompleteTrigger`** — destyled to pure behavior wrappers; the field + |
Comment on lines
+33
to
+38
| - **`NumberFieldIncrement` / `NumberFieldDecrement`** — destyled to behavior wrappers; the stepper | ||
| renders `IconButton` behavior-outer (magnitude passed through, `−`/`+` glyphs). `numberFieldButtonVariants` | ||
| and the `NumberFieldButtonIcon` slot deleted. | ||
| - **`ToastClose`** — destyled to a behavior wrapper; the close is now an `IconButton` (ghost/neutral/sm, | ||
| IconButton-outer). Its `absolute inset-e-1 top-1` **4px corner offset** moved to a new `ToastCloseSlot` | ||
| ui part (position is toast layout; chrome is the IconButton). `toastCloseVariants` deleted. |
lifeiscontent
added a commit
that referenced
this pull request
Jul 1, 2026
- ui/banner story: wrap the ui-tier IconButton's glyph in IconButtonIcon, which applies the --node-size sizing and aria-hidden the ui IconButton doesn't apply itself. The bare <X/> was the only ui story missing it (autocomplete/combobox/number-field/toast already wrap). - toast story: point the ToastProvider decorator at context.args.close instead of hard-coding closeButton, so the close arg actually drives the rendered output.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Plane supports many languages, but a handful of propel components baked English strings into the markup — accessible labels and placeholders a consumer can't localize. This makes each of those consumer-provided.
Changes
"Search"placeholder default and the"Search"aria-label fallback; add a requiredclearLabelfor the clear button."Search"placeholder default.decrementLabel/incrementLabel(were"Decrease"/"Increase").closeLabel, threaded down throughToastListandToastProvider(was"Dismiss").uiBannerDismissnow requiresaria-label; the ready-made pairsonDismisswith a requireddismissLabel(required exactly when the dismiss button is shown).uiBreadcrumbnav landmark'saria-labelis now required (was"Breadcrumb").onDismiss/dismissLabeldismiss (outside the Figma spec, and the source of the"Remove"default) with a symmetricinlineEndNodematchinginlineStartNode; both are decorative icon slots.BadgeDismissstays auiatom for hand-composed removable badges (see the UI/Badge story).Note: these are intentionally breaking — the affected components now require the consumer to pass the (localized) label, rather than falling back to English. Stories pass the English demo text themselves.
vp checkand the fullvp testsuite are green.