diff --git a/content/docs/components/inputs/phone-input.mdx b/content/docs/components/inputs/phone-input.mdx index ea19e32..1678179 100644 --- a/content/docs/components/inputs/phone-input.mdx +++ b/content/docs/components/inputs/phone-input.mdx @@ -128,6 +128,8 @@ export function PhoneField() { | `side` | `"top" \| "bottom"` | No | Preferred menu side; flips when there is no room (default: `"bottom"`) | | `searchPlaceholder` | `string` | No | Placeholder for the country search (default: `"Search countries..."`) | | `emptyMessage` | `ReactNode` | No | Shown when the search matches nothing (default: `"No countries found."`) | +| `labels` | `Partial` | No | Override the built-in English UI text (see below) | +| `displayLocale` | `string` | No | Locale for country names, e.g. `"fr"` (default: English names) | | `maxHeight` | `number \| string` | No | Max height of the scrolling country list (default: `"15rem"`) | | `label` | `string` | No | Accessible label for the input | | `className` | `string` | No | Classes for the root wrapper | @@ -256,6 +258,50 @@ Every part takes its own classes through `classNames`, merged after the defaults /> ``` +### Translated UI text + +Pass `labels` to translate the country menu's text, and `displayLocale` to show +country names in another language. Names come from the browser's +[`Intl.DisplayNames`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DisplayNames), +so their exact wording can vary slightly between browsers. Searching matches the +translated name, while `onValueChange` still reports the country's English `name`. +The `searchPlaceholder` and `emptyMessage` props still work, and win over `labels` +when both are set. + +```tsx +"use client"; + +import { PhoneInput } from "@/components/ui/phone-input"; + +export default function App() { + return ( + + ); +} +``` + +| Key | Default | Used for | +| ------------------- | ----------------------- | ----------------------------- | +| `trigger` | `"Select country"` | Country trigger label | +| `searchPlaceholder` | `"Search countries..."` | Search field placeholder | +| `searchLabel` | `"Search countries"` | Search field accessible label | +| `clearSearch` | `"Clear search"` | Clear button accessible label | +| `emptyMessage` | `"No countries found."` | Shown when nothing matches | +| `listbox` | `"Select country"` | Country list accessible label | + ## Keyboard support The country menu follows the WAI-ARIA combobox pattern: focus stays in the search diff --git a/content/docs/components/language-switchers/language-switcher.mdx b/content/docs/components/language-switchers/language-switcher.mdx index 6d67bf0..58ada9b 100644 --- a/content/docs/components/language-switchers/language-switcher.mdx +++ b/content/docs/components/language-switchers/language-switcher.mdx @@ -130,6 +130,7 @@ export function LocaleSwitcher() { | `searchable` | `boolean` | No | Show the search field (default: `true`) | | `searchPlaceholder` | `string` | No | Search placeholder (default: `"Search languages..."`) | | `emptyMessage` | `ReactNode` | No | Shown when the search matches nothing (default: `"No languages found."`) | +| `labels` | `Partial` | No | Override the built-in English UI text (see below) | | `disabled` | `boolean` | No | Disable the trigger | | `renderOption` | `(locale, state) => ReactNode` | No | Replace each option row's content; `state` has `selected`, `active`, `label`, `flag` | | `classNames` | `LanguageSwitcherClassNames` | No | Per-part classes: `trigger`, `content`, `search`, `list`, `item`, `empty` | @@ -267,6 +268,46 @@ The switcher is styled with shadcn theme tokens, so it follows your theme. Use /> ``` +### Translated UI text + +The search placeholder, empty state and accessible 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 { LanguageSwitcher } from "@/components/ui/language-switcher"; + +export default function App() { + return ( + `Langue actuelle : ${name}`, + }} + /> + ); +} +``` + +| Key | Default | Used for | +| ------------------- | --------------------------------------- | ----------------------------- | +| `searchPlaceholder` | `"Search languages..."` | Search field placeholder | +| `searchLabel` | `"Search languages"` | Search field accessible label | +| `clearSearch` | `"Clear search"` | Clear button accessible label | +| `emptyMessage` | `"No languages found."` | Shown when nothing matches | +| `listbox` | `"Select language"` | Option list accessible label | +| `current` | `(name) => "Current language: " + name` | Trigger accessible label | + ## Keyboard support The popup follows the WAI-ARIA combobox pattern: focus stays in the search field, diff --git a/content/docs/components/navigation/navbar.mdx b/content/docs/components/navigation/navbar.mdx index ce8bfb1..f3a6a56 100644 --- a/content/docs/components/navigation/navbar.mdx +++ b/content/docs/components/navigation/navbar.mdx @@ -137,6 +137,7 @@ export function SiteNav() { | `showFlags` | `boolean` | No | Show flags in language selector (default: `true`) | | `maxHeight` | `number \| string` | No | Max height of the scrolling language menu (default: `"15rem"`) | | `linkComponent` | `React.ElementType` | No | Component used for internal links (default: `"a"`) | +| `labels` | `Partial` | No | Override the built-in English UI text (see below) | | `classNames` | `NavBarClassNames` | No | Per-part class overrides (see below) | | `className` | `string` | No | Additional CSS classes | @@ -361,6 +362,40 @@ export default function App() { } ``` +### Translated UI text + +The language menu, mobile menu button and GitHub link carry English accessible +labels by default. Pass `labels` to translate them — any key you leave out keeps +its English default. + +```tsx +"use client"; + +import { NavBar } from "@/components/ui/navbar"; + +export default function App() { + return ( + MyApp} + locales={[{ code: "en" }, { code: "fr" }]} + defaultValue="fr" + labels={{ + listbox: "Choisir la langue", + current: (name) => `Langue actuelle : ${name}`, + toggleMenu: "Ouvrir le menu", + }} + /> + ); +} +``` + +| Key | Default | Used for | +| ------------ | --------------------------------------- | --------------------------------- | +| `listbox` | `"Select language"` | Language menu accessible label | +| `current` | `(name) => "Current language: " + name` | Language trigger accessible label | +| `toggleMenu` | `"Toggle menu"` | Mobile menu button label | +| `github` | `"GitHub"` | GitHub icon link label | + ## Keyboard support The language menu follows the WAI-ARIA listbox pattern. With no text field in the diff --git a/content/docs/components/translation/translation-widget.mdx b/content/docs/components/translation/translation-widget.mdx index 21db86b..c676260 100644 --- a/content/docs/components/translation/translation-widget.mdx +++ b/content/docs/components/translation/translation-widget.mdx @@ -144,36 +144,37 @@ export function Translate() { ## Props -| Prop | Type | Required | Description | -| ---------------------- | ----------------------------- | -------- | ---------------------------------------------------------------------------------------- | -| `locales` | `Locale[]` | No | Locales for the per-pane language pickers; omit to show static chips | -| `sourceLocale` | `string` | No | BCP 47 code of the source locale (e.g. `"en"`); provide to control it | -| `defaultSourceLocale` | `string` | No | Initial source locale when uncontrolled (default: `locales[0]`, else `"en"`) | -| `onSourceLocaleChange` | `(code: string) => void` | No | Callback when the source locale changes | -| `targetLocale` | `string` | No | BCP 47 code of the target locale (e.g. `"fr"`, `"ar"`); provide to control it | -| `defaultTargetLocale` | `string` | No | Initial target locale when uncontrolled (default: `locales[1]`, else `"en"`) | -| `onTargetLocaleChange` | `(code: string) => void` | No | Callback when the target locale changes | -| `sourceText` | `string` | No | Source text — pass this to control the source pane | -| `defaultSourceText` | `string` | No | Initial source text when uncontrolled | -| `onSourceChange` | `(text: string) => void` | No | Callback when the source text changes; makes the pane editable | -| `targetText` | `string` | No | Target text — pass this to control the target pane (read-only without `onTargetChange`) | -| `defaultTargetText` | `string` | No | Initial target text when uncontrolled | -| `onTargetChange` | `(text: string) => void` | No | Callback when the target text changes | -| `onSwap` | `() => void` | No | Called after the swap button exchanges locales and texts | -| `sourcePlaceholder` | `string` | No | Placeholder for the source pane | -| `targetPlaceholder` | `string` | No | Placeholder for the target pane | -| `sourceLabel` | `string` | No | Accessible label for the source pane (default: `"Source text"`) | -| `targetLabel` | `string` | No | Accessible label for the target pane (default: `"Target text"`) | -| `readOnlySource` | `boolean` | No | Keep the source pane read-only even with `onSourceChange` | -| `showCount` | `boolean` | No | Show the source character count (default: `true`) | -| `maxLength` | `number` | No | Character limit for both panes; the count shows `n / max` | -| `showClear` | `boolean` | No | Show a clear button under an editable source (default: `true`) | -| `showCopy` | `boolean` | No | Show a copy-to-clipboard button under the target (default: `true`) | -| `onTargetCopy` | `(text: string) => void` | No | Called with the target text after it is copied | -| `loading` | `boolean` | No | Show a skeleton in the target pane and mark it `aria-busy` | -| `classNames` | `TranslationWidgetClassNames` | No | Per-part class overrides: `root`, `header`, `pane`, `textarea`, `footer`, `swap`, `chip` | -| `dir` | `"ltr" \| "rtl"` | No | Override the auto-detected layout direction | -| `className` | `string` | No | Additional CSS classes | +| Prop | Type | Required | Description | +| ---------------------- | ---------------------------------- | -------- | ---------------------------------------------------------------------------------------- | +| `locales` | `Locale[]` | No | Locales for the per-pane language pickers; omit to show static chips | +| `sourceLocale` | `string` | No | BCP 47 code of the source locale (e.g. `"en"`); provide to control it | +| `defaultSourceLocale` | `string` | No | Initial source locale when uncontrolled (default: `locales[0]`, else `"en"`) | +| `onSourceLocaleChange` | `(code: string) => void` | No | Callback when the source locale changes | +| `targetLocale` | `string` | No | BCP 47 code of the target locale (e.g. `"fr"`, `"ar"`); provide to control it | +| `defaultTargetLocale` | `string` | No | Initial target locale when uncontrolled (default: `locales[1]`, else `"en"`) | +| `onTargetLocaleChange` | `(code: string) => void` | No | Callback when the target locale changes | +| `sourceText` | `string` | No | Source text — pass this to control the source pane | +| `defaultSourceText` | `string` | No | Initial source text when uncontrolled | +| `onSourceChange` | `(text: string) => void` | No | Callback when the source text changes; makes the pane editable | +| `targetText` | `string` | No | Target text — pass this to control the target pane (read-only without `onTargetChange`) | +| `defaultTargetText` | `string` | No | Initial target text when uncontrolled | +| `onTargetChange` | `(text: string) => void` | No | Callback when the target text changes | +| `onSwap` | `() => void` | No | Called after the swap button exchanges locales and texts | +| `sourcePlaceholder` | `string` | No | Placeholder for the source pane | +| `targetPlaceholder` | `string` | No | Placeholder for the target pane | +| `sourceLabel` | `string` | No | Accessible label for the source pane (default: `"Source text"`) | +| `targetLabel` | `string` | No | Accessible label for the target pane (default: `"Target text"`) | +| `readOnlySource` | `boolean` | No | Keep the source pane read-only even with `onSourceChange` | +| `showCount` | `boolean` | No | Show the source character count (default: `true`) | +| `maxLength` | `number` | No | Character limit for both panes; the count shows `n / max` | +| `showClear` | `boolean` | No | Show a clear button under an editable source (default: `true`) | +| `showCopy` | `boolean` | No | Show a copy-to-clipboard button under the target (default: `true`) | +| `onTargetCopy` | `(text: string) => void` | No | Called with the target text after it is copied | +| `labels` | `Partial` | No | Override the built-in English UI text (see below) | +| `loading` | `boolean` | No | Show a skeleton in the target pane and mark it `aria-busy` | +| `classNames` | `TranslationWidgetClassNames` | No | Per-part class overrides: `root`, `header`, `pane`, `textarea`, `footer`, `swap`, `chip` | +| `dir` | `"ltr" \| "rtl"` | No | Override the auto-detected layout direction | +| `className` | `string` | No | Additional CSS classes | Any other prop — `id`, `data-*`, `aria-*` — is spread onto the root element, and the ref is forwarded to it. @@ -293,6 +294,47 @@ async function translate(text: string) { ; ``` +## Translated UI text + +The pane placeholders and labels have their own props (`sourcePlaceholder`, +`sourceLabel` and so on). The rest of the built-in text — toolbar buttons, the copy +announcement, the word read after the character count and the two language pickers — +comes from `labels`. Any key you leave out keeps its English default. + +```tsx + console.log(text)} + sourcePlaceholder="Texte source" + sourceLabel="Texte source" + labels={{ + swap: "Inverser les langues", + copy: "Copier la traduction", + copied: "Copié", + copiedStatus: "Traduction copiée dans le presse-papiers", + clearSource: "Effacer le texte source", + characters: (count) => (count === 1 ? "caractère" : "caractères"), + languageSwitcher: { + current: (name) => `Langue actuelle : ${name}`, + searchPlaceholder: "Rechercher des langues…", + emptyMessage: "Aucune langue trouvée.", + }, + }} +/> +``` + +| Key | Default | Used for | +| ------------------ | ----------------------------------- | --------------------------------------------------------- | +| `swap` | `"Swap languages"` | Swap button accessible label | +| `copy` | `"Copy translation"` | Copy button accessible label | +| `copied` | `"Copied"` | Copy button label right after a copy | +| `copiedStatus` | `"Translation copied to clipboard"` | Screen reader announcement after copy | +| `clearSource` | `"Clear source text"` | Clear button accessible label | +| `characters` | `(count) => "characters"` | Read after the character count; gets the count to plural | +| `languageSwitcher` | `LanguageSwitcher` defaults | Both language pickers (`Partial`) | + ## Customization Every part takes extra classes through `classNames`, merged after the defaults with diff --git a/content/docs/customization/interfaces.mdx b/content/docs/customization/interfaces.mdx index d7931d4..aa67cbe 100644 --- a/content/docs/customization/interfaces.mdx +++ b/content/docs/customization/interfaces.mdx @@ -65,11 +65,35 @@ export interface LanguageSwitcherProps { showFlags?: boolean; /** Display labels in native language or English (default: "english") */ label?: "native" | "english"; + /** Override the built-in English UI text — any key you omit keeps its default */ + labels?: Partial; /** Additional CSS classes */ className?: string; } ``` +### LanguageSwitcherLabels + +```ts +export interface LanguageSwitcherLabels { + /** Placeholder for the search field (default: "Search languages...") */ + searchPlaceholder: string; + /** Accessible label for the search field (default: "Search languages") */ + 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 languages found.") */ + emptyMessage: React.ReactNode; + /** Accessible label for the option list (default: "Select language") */ + listbox: string; + /** Accessible label for the trigger (default: "Current language: {name}") */ + current: (name: string) => string; +} +``` + +Pass any subset as `labels` — keys you omit keep their English default. The same +holds for every `*Labels` interface below. + ## Phone Input ### Country @@ -109,6 +133,29 @@ export interface PhoneInputProps { showFlags?: boolean; /** Accessible label for the input */ label?: string; + /** Override the built-in English UI text — any key you omit keeps its default */ + labels?: Partial; + /** BCP 47 locale used to display country names, e.g. "fr" (default: English names) */ + displayLocale?: string; +} +``` + +### PhoneInputLabels + +```ts +export interface PhoneInputLabels { + /** Accessible label for the country trigger (default: "Select country") */ + trigger: string; + /** Placeholder for the country search field (default: "Search countries...") */ + searchPlaceholder: string; + /** Accessible label for the country search field (default: "Search countries") */ + 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 countries found.") */ + emptyMessage: React.ReactNode; + /** Accessible label for the country list (default: "Select country") */ + listbox: string; } ``` @@ -159,11 +206,54 @@ export interface NavBarProps { sticky?: boolean; /** Show flags in language selector (default: true) */ showFlags?: boolean; + /** Override the built-in English UI text — any key you omit keeps its default */ + labels?: Partial; /** Additional CSS classes */ className?: string; } ``` +### NavBarLabels + +```ts +export interface NavBarLabels { + /** Accessible label for the language list (default: "Select language") */ + listbox: string; + /** Accessible label for the language menu triggers (default: "Current language: {name}") */ + current: (name: string) => string; + /** Accessible label for the mobile menu button (default: "Toggle menu") */ + toggleMenu: string; + /** Accessible label for the GitHub icon link (default: "GitHub") */ + github: string; +} +``` + +## Translation Widget + +### TranslationWidgetLabels + +```ts +export interface TranslationWidgetLabels { + /** Accessible label for the swap button (default: "Swap languages") */ + swap: string; + /** Accessible label for the copy button (default: "Copy translation") */ + copy: string; + /** Accessible label for the copy button right after a copy (default: "Copied") */ + copied: string; + /** Screen reader announcement after a copy (default: "Translation copied to clipboard") */ + copiedStatus: string; + /** Accessible label for the clear button (default: "Clear source text") */ + clearSource: string; + /** + * Screen reader word read after the character count (default: "characters"). + * Receives the count, so it can return the right plural form. + */ + characters: (count: number) => string; + /** UI text for both language pickers, passed through as their `labels` */ + languageSwitcher?: Partial; +} +``` + ## 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: diff --git a/src/registry/components/index.ts b/src/registry/components/index.ts index 30f91f2..d31a9de 100644 --- a/src/registry/components/index.ts +++ b/src/registry/components/index.ts @@ -6,6 +6,7 @@ export { LanguageSwitcher } from "./language-switcher"; export type { LanguageSwitcherProps, LanguageSwitcherClassNames, + LanguageSwitcherLabels, LanguageSwitcherOptionState, } from "./language-switcher"; @@ -16,10 +17,19 @@ export type { } from "./language-switcher-pill"; export { PhoneInput, COUNTRIES } from "./phone-input"; -export type { PhoneInputProps, PhoneInputClassNames, Country } from "./phone-input"; +export type { + PhoneInputProps, + PhoneInputClassNames, + PhoneInputLabels, + Country, +} from "./phone-input"; export { NavBar } from "./navbar"; -export type { NavBarProps, NavBarClassNames, NavLink } from "./navbar"; +export type { NavBarProps, NavBarClassNames, NavBarLabels, NavLink } from "./navbar"; export { TranslationWidget } from "./translation-widget"; -export type { TranslationWidgetProps, TranslationWidgetClassNames } from "./translation-widget"; +export type { + TranslationWidgetProps, + TranslationWidgetClassNames, + TranslationWidgetLabels, +} from "./translation-widget"; diff --git a/src/registry/components/language-switcher.tsx b/src/registry/components/language-switcher.tsx index 4bf68c2..70412a4 100644 --- a/src/registry/components/language-switcher.tsx +++ b/src/registry/components/language-switcher.tsx @@ -17,6 +17,31 @@ import { getFlag, getLanguageName, isRtl } from "./locale-data"; export type { Locale }; +/** Built-in UI text. Every key is optional in `labels`; omitted keys keep the English default. */ +export interface LanguageSwitcherLabels { + /** Placeholder for the search field (default: "Search languages...") */ + searchPlaceholder: string; + /** Accessible label for the search field (default: "Search languages") */ + 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 languages found.") */ + emptyMessage: React.ReactNode; + /** Accessible label for the option list (default: "Select language") */ + listbox: string; + /** Accessible label for the trigger (default: "Current language: {name}") */ + current: (name: string) => string; +} + +const DEFAULT_LABELS: LanguageSwitcherLabels = { + searchPlaceholder: "Search languages...", + searchLabel: "Search languages", + clearSearch: "Clear search", + emptyMessage: "No languages found.", + listbox: "Select language", + current: (name) => `Current language: ${name}`, +}; + /** Per-part class overrides, merged after the defaults with `cn`. */ export interface LanguageSwitcherClassNames { /** The button that opens the menu */ @@ -78,10 +103,12 @@ export interface LanguageSwitcherProps extends Omit< side?: "top" | "bottom"; /** Show the search field (default: true) */ searchable?: boolean; - /** Placeholder for the search field (default: "Search languages...") */ + /** Placeholder for the search field. Alias of `labels.searchPlaceholder`, wins over it */ searchPlaceholder?: string; - /** Content shown when the search matches nothing (default: "No languages found.") */ + /** Empty-search content. Alias of `labels.emptyMessage`, wins over it */ emptyMessage?: React.ReactNode; + /** Override the built-in English UI text — any key you omit keeps its default */ + labels?: Partial; /** Disable the trigger */ disabled?: boolean; /** Replace the content of each option row. The row keeps its button, role and state. */ @@ -141,8 +168,9 @@ export const LanguageSwitcher = React.forwardRef ref.current as HTMLDivElement); + const text: LanguageSwitcherLabels = { + searchPlaceholder: + searchPlaceholder ?? labels?.searchPlaceholder ?? DEFAULT_LABELS.searchPlaceholder, + searchLabel: labels?.searchLabel ?? DEFAULT_LABELS.searchLabel, + clearSearch: labels?.clearSearch ?? DEFAULT_LABELS.clearSearch, + // `null` is a valid empty message (it renders nothing), so only `undefined` falls through. + emptyMessage: + emptyMessage !== undefined + ? emptyMessage + : labels?.emptyMessage !== undefined + ? labels.emptyMessage + : DEFAULT_LABELS.emptyMessage, + listbox: labels?.listbox ?? DEFAULT_LABELS.listbox, + current: labels?.current ?? DEFAULT_LABELS.current, + }; const resolveLabel = (l: Locale) => l.label ?? getLanguageName(l.code, labelMode) ?? l.code; const activeLocale = locales.find((l) => l.code === locale); const activeLabel = activeLocale ? resolveLabel(activeLocale) : locale; @@ -259,7 +302,7 @@ export const LanguageSwitcher = React.forwardRef {showFlags ? ( @@ -300,8 +343,8 @@ export const LanguageSwitcher = React.forwardRef { setSearch(""); nav.resetActive(); @@ -335,7 +378,7 @@ export const LanguageSwitcher = React.forwardRef {filtered.map((l, index) => { @@ -396,7 +439,7 @@ export const LanguageSwitcher = React.forwardRef - {emptyMessage} + {text.emptyMessage} )} diff --git a/src/registry/components/navbar.tsx b/src/registry/components/navbar.tsx index e64f3d6..771eef0 100644 --- a/src/registry/components/navbar.tsx +++ b/src/registry/components/navbar.tsx @@ -24,6 +24,25 @@ export interface NavLink { href: string; } +/** Built-in UI text. Every key is optional in `labels`; omitted keys keep the English default. */ +export interface NavBarLabels { + /** Accessible label for the language list (default: "Select language") */ + listbox: string; + /** Accessible label for the language menu triggers (default: "Current language: {name}") */ + current: (name: string) => string; + /** Accessible label for the mobile menu button (default: "Toggle menu") */ + toggleMenu: string; + /** Accessible label for the GitHub icon link (default: "GitHub") */ + github: string; +} + +const DEFAULT_LABELS: NavBarLabels = { + listbox: "Select language", + current: (name) => `Current language: ${name}`, + toggleMenu: "Toggle menu", + github: "GitHub", +}; + /** Per-part class overrides, merged after the defaults with `cn`. */ export interface NavBarClassNames { /** The `