Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions content/docs/components/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,14 @@ Professional language switcher components designed for seamless locale selection
| **[Language Switcher](/docs/components/language-switchers/language-switcher)** | Full-featured dropdown with search, flags, and RTL support |
| **[Language Switcher Pill](/docs/components/language-switchers/language-switcher-pill)** | Animated pill-style selector with keyboard navigation and RTL support |

### Locale Pickers

Choose a region and read back the locale defaults that come with it — for settings pages and onboarding flows.

| Component | Description |
| ------------------------------------------------------------------ | --------------------------------------------------------------------------------- |
| **[Locale Picker](/docs/components/locale-pickers/locale-picker)** | Region picker with derived currency, date, number, and week defaults, plus search |

### Phone Input

International phone number input with country selector and dial code search.
Expand Down
243 changes: 243 additions & 0 deletions content/docs/components/locale-pickers/locale-picker.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,243 @@
---
title: Locale Picker
description: Region picker with locale-derived defaults — currency, date and number formats, and first day of week. Searchable, keyboard accessible, and RTL-aware.
---

import { PreviewComponents } from "@/components/docs/preview-components";
import { LocalePickerDemo } from "@/components/docs/locale-picker-demo";
import { Tabs, Tab } from "fumadocs-ui/components/tabs";

<PreviewComponents registryName="locale-picker">
<LocalePickerDemo />
</PreviewComponents>

## Installation

<Tabs
items={["shadcn", "Elements CLI"]}
defaultValue="shadcn"
className="bg-transparent border-none"
>
<Tab value="shadcn" className="border-none bg-transparent p-0 mt-3">
<Tabs items={["npx", "pnpm", "yarn", "bun"]} defaultValue="npx" groupId="installation-tabs">
<Tab value="npx">

```bash
npx shadcn@latest add @babelize/locale-picker
```

</Tab>
<Tab value="pnpm">

```bash
pnpm dlx shadcn@latest add @babelize/locale-picker
```

</Tab>
<Tab value="yarn">

```bash
yarn dlx shadcn@latest add @babelize/locale-picker
```

</Tab>
<Tab value="bun">

```bash
bunx shadcn@latest add @babelize/locale-picker
```

</Tab>
</Tabs>

</Tab>
<Tab value="Elements CLI" className="border-none bg-transparent p-0 mt-3">
<Tabs items={["npx", "pnpm", "yarn", "bun"]} defaultValue="npx" groupId="elements-cli-tabs">
<Tab value="npx">

```bash
npx @babelize/elements add locale-picker
```

</Tab>
<Tab value="pnpm">

```bash
pnpm dlx @babelize/elements add locale-picker
```

</Tab>
<Tab value="yarn">

```bash
yarn dlx @babelize/elements add locale-picker
```

</Tab>
<Tab value="bun">

```bash
bunx @babelize/elements add locale-picker
```

</Tab>
</Tabs>

</Tab>
</Tabs>

## Usage

```tsx
import { LocalePicker } from "@/components/ui/locale-picker";
```

```tsx
<LocalePicker
regions={["IN", "US", "GB", "DE", "JP"]} // your ISO 3166-1 alpha-2 regions
defaultValue="en-IN"
onValueChange={(locale) => console.log("Selected locale:", locale)}
label="english" // native, english
variant="outline" // outline, ghost, secondary
size="default" // sm, default, lg
/>
```

## Props

| Prop | Type | Required | Default | Description |
| ------------------- | ------------------------------------- | -------- | --------------------- | ------------------------------------------------------------------------------------ |
| `regions` | `string[]` | Yes | — | Regions to choose from, ISO 3166-1 alpha-2 codes |
| `value` | `string` | No | — | Selected locale (BCP 47) — pass this to control the component |
| `defaultValue` | `string` | No | `en-<first region>` | Initial selected locale when uncontrolled |
| `onValueChange` | `(locale: string) => void` | No | — | Callback when the region changes — receives the new BCP 47 tag |
| `label` | `"native" \| "english"` | No | `"english"` | Region names in English, or localized to the selected locale |
| `showFlags` | `boolean` | No | `true` | Show the region's flag emoji next to its name |
| `maxHeight` | `number \| string` | No | `"15rem"` | Max height of the scrolling option list, before it scrolls |
| `variant` | `"outline" \| "ghost" \| "secondary"` | No | `"outline"` | Trigger style |
| `size` | `"sm" \| "default" \| "lg"` | No | `"default"` | Trigger size |
| `align` | `"start" \| "end"` | No | `"start"` | Edge of the trigger the popup lines up with |
| `side` | `"top" \| "bottom"` | No | `"bottom"` | Preferred popup side — flips when the viewport has no room |
| `searchable` | `boolean` | No | `true` | Show the search field |
| `searchPlaceholder` | `string` | No | `"Search regions..."` | Placeholder for the search field |
| `emptyMessage` | `React.ReactNode` | No | `"No results"` | Content shown when the search matches nothing |
| `disabled` | `boolean` | No | `false` | Disable the trigger |
| `showSummary` | `boolean` | No | `true` | Show the summary card of derived locale defaults |
| `labels` | `Partial<LocalePickerLabels>` | No | — | Override the built-in English UI text (see below) |
| `classNames` | `LocalePickerClassNames` | No | — | Per-part classes: `trigger`, `content`, `search`, `list`, `item`, `empty`, `summary` |
| `className` | `string` | No | — | Additional CSS classes |

The selected value is a full BCP 47 tag: its **region** subtag is the picked
region, and its **language** subtag survives region changes. Picking Germany
from `en-IN` yields `en-DE`; picking it from `fr-JP` yields `fr-DE`.

## Derived defaults

The summary card derives everything from the selected locale through `Intl` —
no data bundled beyond an ISO 4217 fallback table for the currency code:

| Row | Source |
| -------------- | -------------------------------------------------------------------------------- |
| Currency | Region → ISO 4217 code, previewed with `Intl.NumberFormat` (`style: "currency"`) |
| Date format | `Intl.DateTimeFormat` (`dateStyle: "medium"`) on a fixed sample date |
| Number format | `Intl.NumberFormat` on a fixed sample amount |
| Week starts on | `Intl.Locale#getWeekInfo()` where available, with a regional fallback |

Regions without a currency mapping show an em dash rather than a guess.

## Keyboard support

The popup follows the WAI-ARIA combobox pattern: focus stays in the search
field, which is exposed as a `role="combobox"`, and the highlighted option is
announced through `aria-activedescendant`.

| Key | Action |
| -------------------------------- | ------------------------------------------------------------- |
| `Enter`, `Space`, or click | Open the popup from the trigger |
| `ArrowDown` / `ArrowUp` (closed) | Open with the first / last region highlighted |
| `ArrowDown` / `ArrowUp` | Move the highlight, wrapping at both ends |
| `Home` / `End` | Highlight the first / last region |
| `Enter` | Select the highlighted region and return focus to the trigger |
| `Escape` | Close the popup and return focus to the trigger |
| Typing | Filter by name or code; the highlight restarts at the top |

## Features

- **Derived defaults** — currency, date format, number format, and first day of week, resolved live through `Intl`
- **Dropdown with search** — filter regions by English name, localized name, or region code, with a clear button
- **Motion** — the popup fades and slides via `motion/react`, honouring `prefers-reduced-motion`, and leaves the accessibility tree as it closes
- **RTL-aware** — flips direction for RTL locales (ar, he, fa, ur) via `dir` on the root
- **Keyboard accessible** — arrow keys, Home / End, Enter, and Escape navigation
- **ARIA labels** — `role="combobox"` search field with `aria-activedescendant`, plus `aria-expanded`, `aria-haspopup`, and `aria-selected`
- **Themed with shadcn tokens** — `bg-popover`, `border-input`, `bg-accent` and friends, so it inherits your theme; dark mode comes from the tokens
- **Customisable** — `variant` / `size` triggers plus per-part `classNames` (`LocalePickerClassNames` is exported from the barrel), with `lucide-react` icons throughout
- **Controlled or uncontrolled** — `value` / `defaultValue` / `onValueChange`

## Examples

### Controlled

```tsx
<LocalePicker regions={["IN", "US", "GB", "DE", "JP"]} value={locale} onValueChange={setLocale} />
```

### Native region names

```tsx
<LocalePicker regions={["IN", "US", "GB", "DE", "JP"]} defaultValue="de-DE" label="native" />
```

With `de-DE` selected, the trigger reads "Deutschland" and the options read
"Vereinigte Staaten", "Vereinigtes Königreich", and so on.

### Translated UI text

The search placeholder, empty state, accessible labels and summary row labels
default to English. Pass `labels` to translate them — any key you leave out
keeps its English default. The `searchPlaceholder` and `emptyMessage` props
still work, and win over `labels` when both are set.

```tsx
"use client";

import { LocalePicker } from "@/components/ui/locale-picker";

export default function App() {
return (
<LocalePicker
regions={["IN", "US", "GB", "DE", "JP"]}
defaultValue="en-IN"
labels={{
searchPlaceholder: "Rechercher une région…",
searchLabel: "Rechercher une région",
clearSearch: "Effacer la recherche",
emptyMessage: "Aucune région trouvée.",
listbox: "Choisir la région",
current: (name) => `Région actuelle : ${name}`,
currency: "Devise",
dateFormat: "Format de date",
numberFormat: "Format de nombre",
weekStart: "La semaine commence le",
}}
/>
);
}
```

## Dependencies

This component uses:

- `clsx` and `tailwind-merge` (bundled as dependencies)
- `lucide-react` (icons)
- `motion` (popup animation)

Peer dependencies:

- `react` >= 18
- `react-dom` >= 18
- `tailwindcss` >= 3

## Credits

Created by [ujwalbajaj](https://github.com/ujwalbajaj).
6 changes: 6 additions & 0 deletions content/docs/components/locale-pickers/meta.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"title": "Locale Pickers",
"icon": "MapPin",
"pages": ["locale-picker"],
"collapsible": false
}
2 changes: 1 addition & 1 deletion content/docs/components/meta.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{
"title": "Components",
"pages": ["index", "language-switchers", "navigation", "inputs", "translation"]
"pages": ["index", "language-switchers", "locale-pickers", "navigation", "inputs", "translation"]
}
29 changes: 29 additions & 0 deletions content/docs/customization/interfaces.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,35 @@ export interface TranslationWidgetLabels {
}
```

## Locale Picker

### LocalePickerLabels

```ts
export interface LocalePickerLabels {
/** Placeholder for the search field (default: "Search regions...") */
searchPlaceholder: string;
/** Accessible label for the search field (default: "Search regions") */
searchLabel: string;
/** Accessible label for the button that clears the search (default: "Clear search") */
clearSearch: string;
/** Content shown when the search matches nothing (default: "No results") */
emptyMessage: React.ReactNode;
/** Accessible label for the option list (default: "Select region") */
listbox: string;
/** Accessible label for the trigger (default: "Current region: {name}") */
current: (name: string) => string;
/** Summary row label for the currency preview (default: "Currency") */
currency: string;
/** Summary row label for the date preview (default: "Date format") */
dateFormat: string;
/** Summary row label for the number preview (default: "Number format") */
numberFormat: string;
/** Summary row label for the first day of week (default: "Week starts on") */
weekStart: string;
}
```

## Extending Interfaces

Because Babelize Elements components are fully owned by you once installed, you can extend these interfaces directly in the component files. For example, to add a custom `meta` field to each locale, add it to the `Locale` interface in your local copy:
Expand Down
16 changes: 16 additions & 0 deletions src/components/docs/locale-picker-demo.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
"use client";

import { LocalePicker } from "@/registry/components/locale-picker";

export function LocalePickerDemo() {
return (
<LocalePicker
regions={["IN", "US", "GB", "DE", "JP"]} // your ISO 3166-1 alpha-2 regions
defaultValue="en-IN"
onValueChange={(locale) => console.log("Selected locale:", locale)}
label="english" // native, english
variant="outline" // outline, ghost, secondary
size="default" // sm, default, lg
/>
);
}
7 changes: 7 additions & 0 deletions src/registry/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,10 @@ export type {
TranslationWidgetClassNames,
TranslationWidgetLabels,
} from "./translation-widget";

export { LocalePicker } from "./locale-picker";
export type {
LocalePickerProps,
LocalePickerClassNames,
LocalePickerLabels,
} from "./locale-picker";
Loading
Loading