# Language Pair Selector

Language Pair Selector [#language-pair-selector]

The **Language Pair Selector** is a paired source/target language picker. The `pills` variant renders From/To pill toggles (flag + name) with a swap-languages button; the `compact` variant renders a grouped `{source} → {target}` pair of selects. It is fully controlled via `sourceCode` / `targetCode` and the `onSelectSource` / `onSelectTarget` / `onSwap` callbacks.

It is presentational and portable — feed the selected codes to any translation backend. With LocalMode you can (optionally) drive [`useTranslate()`](https://localmode.dev/docs/react) from `@localmode/react`, composed with [`useDetectLanguage()`](https://localmode.dev/docs/react) to auto-fill the source.

**When to use it:** any translation surface that lets the user choose a language pair and swap direction.

Preview [#preview]

```tsx
'use client';

import { useState } from 'react';
import { useTranslate } from '@localmode/react';
import { transformers } from '@localmode/transformers';
import {
  LanguagePairSelector,
  type LanguageOption,
} from '@/components/language-pair-selector';

const LANGUAGES: LanguageOption[] = [
  { code: 'en', name: 'English', flag: '🇬🇧' },
  { code: 'fr', name: 'French', flag: '🇫🇷' },
  { code: 'de', name: 'German', flag: '🇩🇪' },
  { code: 'es', name: 'Spanish', flag: '🇪🇸' },
];

/** Map a source/target code pair to an opus-mt model id (one model per pair). */
function modelIdFor(source: string, target: string) {
  return `Xenova/opus-mt-${source}-${target}`;
}

/**
 * Demo for LanguagePairSelector, used by the docs live preview. The selected
 * source/target codes pick an opus-mt model and drive a real `useTranslate`
 * call. The pair model downloads on the first run (Run-gated). Only pairs with
 * a published opus-mt model translate; others surface an error.
 */
export default function LanguagePairSelectorDemo() {
  const [source, setSource] = useState('en');
  const [target, setTarget] = useState('fr');
  const [text, setText] = useState('Local-first AI keeps your data on device.');

  const { data, isLoading, error, execute } = useTranslate({
    model: transformers.translator(modelIdFor(source, target)),
  });

  const swap = () => {
    setSource(target);
    setTarget(source);
  };

  return (
    <div className="flex w-full max-w-lg flex-col gap-3">
      <LanguagePairSelector
        languages={LANGUAGES}
        sourceCode={source}
        targetCode={target}
        onSelectSource={setSource}
        onSelectTarget={setTarget}
        onSwap={swap}
      />

      <textarea
        value={text}
        onChange={(e) => setText(e.target.value)}
        rows={2}
        className="w-full resize-none rounded-md border border-input bg-transparent px-3 py-2 text-sm outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50"
      />

      <button
        type="button"
        onClick={() => execute({ text, sourceLanguage: source, targetLanguage: target })}
        disabled={isLoading || source === target}
        className="inline-flex h-9 items-center justify-center rounded-md bg-primary px-4 text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/90 disabled:opacity-50"
      >
        {isLoading ? 'Translating…' : 'Translate'}
      </button>

      {error && <p className="text-sm text-destructive">{error.message}</p>}
      {data && !isLoading && (
        <p className="rounded-md border border-border bg-muted/30 px-3 py-2 text-sm">
          {data.translation}
        </p>
      )}
    </div>
  );
}
```

Installation [#installation]

```bash
npx shadcn@latest add @localmode/ui/input-controls/language-pair-selector
```

Dependencies [#dependencies]

* **Data source:** renders the selected codes you pass and emits `onSelectSource` / `onSelectTarget` / `onSwap` callbacks — works with any backend. Recommended LocalMode producer: `useTranslate` (optionally composed with `useDetectLanguage`).
* `lucide-react` — the swap icon

Files installed [#files-installed]

* `language-pair-selector.tsx` — the component
* `lib/utils.ts` — the `cn()` helper (if not already present)

Props [#props]

**LanguagePairSelector**

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `languages` | `array` | — | **Required.** All selectable languages, used for both source and target. |
| `sourceCode` | `string` | — | **Required.** Currently selected source code. |
| `targetCode` | `string` | — | **Required.** Currently selected target code. |
| `onSelectSource` | `function` | — | **Required.** Fired with the new source code. |
| `onSelectTarget` | `function` | — | **Required.** Fired with the new target code. |
| `onSwap` | `function` | — | **Required.** Fired when the swap button exchanges source and target. |
| `variant` | `"compact" \| "pills"` | `"pills"` | Layout variant. `pills` renders From/To pill toggles with a swap button; `compact` renders a grouped `{source} → {target}` pair of selects. |

Examples [#examples]

Drive a translation [#drive-a-translation]

```tsx
import { useState } from 'react';
import { useTranslate } from '@localmode/react';
import { transformers } from '@localmode/transformers';
import { LanguagePairSelector } from '@/components/language-pair-selector';

const LANGS = [
  { code: 'en', name: 'English', flag: '🇬🇧' },
  { code: 'fr', name: 'French', flag: '🇫🇷' },
];

export function Translator() {
  const [src, setSrc] = useState('en');
  const [tgt, setTgt] = useState('fr');
  const { execute, data } = useTranslate({
    model: transformers.translator(`Xenova/opus-mt-${src}-${tgt}`),
  });

  return (
    <LanguagePairSelector
      languages={LANGS}
      sourceCode={src}
      targetCode={tgt}
      onSelectSource={setSrc}
      onSelectTarget={setTgt}
      onSwap={() => { setSrc(tgt); setTgt(src); }}
    />
  );
}
```

Compact variant [#compact-variant]

```tsx
<LanguagePairSelector variant="compact" languages={LANGS} {...props} />
```

Customization [#customization]

`flag` is optional per language and renders before the name. The active pill uses `bg-primary/10 text-primary`; the compact variant uses native styled selects for a tight footprint. Everything is theme-driven via shadcn/ui CSS variables — edit the copied file to change the pill shape or wire the swap button to additional side effects.