Skip to content

Make component text localizable (stop baking in English)#211

Merged
lifeiscontent merged 13 commits into
mainfrom
fix/localizable-strings
Jul 1, 2026
Merged

Make component text localizable (stop baking in English)#211
lifeiscontent merged 13 commits into
mainfrom
fix/localizable-strings

Conversation

@lifeiscontent

@lifeiscontent lifeiscontent commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

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 / 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 down through ToastList and ToastProvider (was "Dismiss").
  • Banner — the ui BannerDismiss now requires aria-label; the ready-made pairs onDismiss with a required dismissLabel (required exactly when the dismiss button is shown).
  • Breadcrumb — the ui Breadcrumb nav landmark's aria-label is now required (was "Breadcrumb").
  • Badge — replaced the baked onDismiss/dismissLabel dismiss (outside the Figma spec, and the source of the "Remove" default) with a symmetric inlineEndNode matching inlineStartNode; both are decorative icon slots. BadgeDismiss stays a ui atom 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 check and the full vp test suite are green.

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.
@lifeiscontent lifeiscontent force-pushed the fix/localizable-strings branch from e0c3ada to cdfe3d5 Compare June 30, 2026 12:56
@github-actions

Copy link
Copy Markdown

📚 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.
Copilot AI review requested due to automatic review settings July 1, 2026 01:20

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 IconButton rather than bespoke button+icon slots.
  • Rename a few framing primitives (InputBoxInputGroup, TextAreaBoxTextAreaGroup) 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 (labelPillBoxlabelPillBase)
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 (inputBoxVariantsinputGroupVariants)
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.
Copilot AI review requested due to automatic review settings July 1, 2026 10:16

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 94 out of 94 changed files in this pull request and generated 5 comments.

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.
Copilot AI review requested due to automatic review settings July 1, 2026 11:11
@lifeiscontent lifeiscontent merged commit 8904cc4 into main Jul 1, 2026
3 checks passed
@lifeiscontent lifeiscontent deleted the fix/localizable-strings branch July 1, 2026 11:15

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 97 out of 97 changed files in this pull request and generated 5 comments.

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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants