Zum Inhalt springen
UI Kit
Accent

Sprachwahl

Sprach- und Regionswahl mit einem echten Link je Sprache. Sprachen mit ihrem eigenen Namen, nie mit einer Flagge.

Eine von 3 Components in Menüleisten und Umschalter

5 Variants

Variants

Dropdown

Ein ruhiger Knopf im Header. Das Menü zeigt jede Sprache im eigenen Namen und in der Seitensprache, Pfeiltasten führen hindurch.

Im Studio öffnen

Umschalter

Zwei oder drei kurze Codes, eine Markierung gleitet zur gewählten Sprache. Für Seiten mit wenigen Sprachen.

Liste im Fuß

Jede Sprache als Link in der letzten Zeile des Footers. Die Linie unter der aktuellen zeichnet sich beim Hovern.

Im Studio öffnen

Sprachhinweis

Bietet die Seite in der Sprache des Besuchers an, in dieser Sprache geschrieben. Bleiben faltet den Hinweis weg.

Im Studio öffnen

Regionswahl

Regionen als Tabs, Länder mit ihren Sprachen und eine Suche, die beim Tippen filtert.

Im Studio öffnen

Details

Was Sprachwahl mitbringt, aus Registry und Quellcode gelesen.

Bereich
Elements
Variants
5
Version
1.0.0 · seit 1.0.0
Datei
components/ui/language-switcher.tsx
Bringt mit
utilskitbuttoniconmotion
npm-Pakete
Keine
Sprachen
Englisch und Deutsch, 7 eigene Texte

Props

Die Schnittstelle LanguageSwitcherProps aus components/ui/language-switcher.tsx. Beschreibungen stammen aus den Kommentaren im Code und sind auf Englisch.

  • variantoptional
    LanguageSwitcherVariant

    dropdownsegmentedinline-listsuggestion-bannerregion-picker

  • languagesoptional
    Language[]
  • valueoptional
    string

    Code of the current language

  • onChangeoptional
    (code: string) => void

    Called with the code before the link is followed

  • defaultOpenoptional
    boolean

    dropdown: open on first render (for previews)

  • titleoptional
    string

    inline-list, region-picker: a heading

  • descriptionoptional
    string

    region-picker: a line under the heading

  • regionsoptional
    LanguageRegion[]

    region-picker: the regions

  • countryoptional
    string

    region-picker: id of the current country, e.g. Deutschland

  • suggestionoptional
    { code: string; message: string; action: string; stay: string }

    suggestion-banner: the offer, written in the suggested language

Eingebaute Texte

Diese Texte schreibt Sprachwahl selbst. Die Sprache folgt dem KitProvider, über die Prop labels ersetzt du einzelne.

Alle 7 Texte anzeigen
languageEnglisch LanguageDeutsch Sprache
chooseEnglisch Choose languageDeutsch Sprache wählen
currentEnglisch Current languageDeutsch Aktuelle Sprache
searchEnglisch Search country or languageDeutsch Land oder Sprache suchen
noneEnglisch No country or language matches. Try the name in English or in your language.Deutsch Kein Land und keine Sprache passt. Versuch den Namen auf Englisch oder in deiner Sprache.
allEnglisch All regionsDeutsch Alle Regionen
closeEnglisch CloseDeutsch Schließen

Barrierefreiheit

Aus dem Quellcode der Component abgeleitet.

  • Sprachwahl reagiert selbst auf die Tasten Escape, Pfeil hoch, Pfeil runter, Tab und Leertaste.
  • Setzt aria-checked, aria-controls, aria-current, aria-expanded, aria-haspopup, aria-label und aria-selected, damit Screenreader Zweck und Zustand ansagen.
  • Nutzt die Rollen menu, menuitemradio, region, status, tab und tablist.
  • Meldet Änderungen über eine Live-Region, ohne den Fokus zu verschieben.
  • Jedes Feld hat ein Label, sichtbar oder für Screenreader.
  • Ergänzt Text für Screenreader, wo Icons und Grafiken allein stehen.
  • Zeigt bei Bedienung mit der Tastatur einen sichtbaren Fokusring.
  • Der Server rendert den Startzustand vollständig, JavaScript ergänzt nur die Interaktion.

Bewegung

  • Erscheint beim ersten Sichtkontakt mit einem kurzen Auftritt (kit-rise, useInView).
  • Bringt eigene Animationen mit, ruhig getaktet und ohne Wackeln.
  • Bei reduzierter Bewegung (prefers-reduced-motion) bleibt sie ruhig. 4 Stellen im Code schalten Bewegung dann ab.

Quellcode

Die Datei, die aiw add in dein Projekt legt. Sie gehört danach dir.

Datei anzeigen, 447 Zeilen
components/ui/language-switcher.tsx
'use client';

import { useEffect, useId, useLayoutEffect, useMemo, useRef, useState } from 'react';
import { cn, useTexts } from '@/lib/kit';
import { Stagger, useScrollFade } from '@/components/ui/motion';
import { Button } from '@/components/ui/button';
import { Icon } from '@/components/ui/icon';

/*
 * Language and region choice. Every option is a real link to the page in that
 * language (hreflang, lang), so search engines and people without JavaScript
 * reach every version. Languages are shown by their own name, never by a
 * flag: a flag is a country, and Spanish is not Spain.
 *
 *   dropdown           a quiet button in the header, the menu lists the languages
 *   segmented          two or three short codes with a gliding marker
 *   inline-list        the languages as a row of links, for the footer
 *   suggestion-banner  offers the page in the visitor's language, in that language
 *   region-picker      regions, countries and their languages with search
 */

export type LanguageSwitcherVariant = 'dropdown' | 'segmented' | 'inline-list' | 'suggestion-banner' | 'region-picker';

export interface Language {
  /** BCP 47 code, e.g. de, en, fr-CH */
  code: string;
  /** The name in that language, e.g. Deutsch */
  label: string;
  /** The name in the language of the page, e.g. German */
  english?: string;
  /** Short code shown in compact places, default the code in capitals */
  short?: string;
  href: string;
}

export interface LanguageRegion {
  id: string;
  title: string;
  countries: { name: string; languages: Language[] }[];
}

export interface LanguageSwitcherProps {
  variant?: LanguageSwitcherVariant;
  languages?: Language[];
  /** Code of the current language */
  value?: string;
  /** Called with the code before the link is followed */
  onChange?: (code: string) => void;
  /** dropdown: open on first render (for previews) */
  defaultOpen?: boolean;
  /** inline-list, region-picker: a heading */
  title?: string;
  /** region-picker: a line under the heading */
  description?: string;
  /** region-picker: the regions */
  regions?: LanguageRegion[];
  /** region-picker: id of the current country, e.g. Deutschland */
  country?: string;
  /** suggestion-banner: the offer, written in the suggested language */
  suggestion?: { code: string; message: string; action: string; stay: string };
  labels?: Partial<Texts>;
  className?: string;
}

const TEXTS = {
  en: {
    language: 'Language',
    choose: 'Choose language',
    current: 'Current language',
    search: 'Search country or language',
    none: 'No country or language matches. Try the name in English or in your language.',
    all: 'All regions',
    close: 'Close',
  },
  de: {
    language: 'Sprache',
    choose: 'Sprache wählen',
    current: 'Aktuelle Sprache',
    search: 'Land oder Sprache suchen',
    none: 'Kein Land und keine Sprache passt. Versuch den Namen auf Englisch oder in deiner Sprache.',
    all: 'Alle Regionen',
    close: 'Schließen',
  },
};

type Texts = (typeof TEXTS)['en'];

const shortOf = (l: Language) => l.short ?? l.code.toUpperCase();

/** Keeps the choice in the component for previews, then follows the link */
function useChoice(p: LanguageSwitcherProps) {
  const [value, setValue] = useState(p.value ?? p.languages?.[0]?.code);
  useEffect(() => setValue(p.value ?? p.languages?.[0]?.code), [p.value]); // eslint-disable-line react-hooks/exhaustive-deps
  const pick = (code: string) => {
    setValue(code);
    p.onChange?.(code);
  };
  return [value, pick] as const;
}

const linkProps = (l: Language) => ({ href: l.href, hrefLang: l.code, lang: l.code });

/* ------------------------------------------------------------------ */
/* dropdown                                                            */
/* ------------------------------------------------------------------ */

function Dropdown(p: LanguageSwitcherProps & { t: Texts }) {
  const { t } = p;
  const languages = p.languages ?? [];
  const [value, pick] = useChoice(p);
  const [open, setOpen] = useState(!!p.defaultOpen);
  const id = useId();
  const rootRef = useRef<HTMLDivElement>(null);
  const buttonRef = useRef<HTMLButtonElement>(null);
  const current = languages.find((l) => l.code === value) ?? languages[0];

  useEffect(() => {
    if (!open) return;
    const onDown = (e: PointerEvent) => {
      if (!rootRef.current?.contains(e.target as Node)) setOpen(false);
    };
    document.addEventListener('pointerdown', onDown);
    return () => document.removeEventListener('pointerdown', onDown);
  }, [open]);

  const items = () => [...(rootRef.current?.querySelectorAll<HTMLElement>('[role="menuitemradio"]') ?? [])];
  const onKey = (e: React.KeyboardEvent) => {
    const list = items();
    const i = list.indexOf(document.activeElement as HTMLElement);
    if (e.key === 'Escape') {
      setOpen(false);
      buttonRef.current?.focus();
    } else if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
      e.preventDefault();
      if (!open) setOpen(true);
      const next = e.key === 'ArrowDown' ? (i + 1) % list.length : (i - 1 + list.length) % list.length;
      requestAnimationFrame(() => (items()[i < 0 ? 0 : next] ?? items()[0])?.focus());
    } else if (e.key === 'Tab' && open) setOpen(false);
  };

  if (!current) return null;
  return (
    <div ref={rootRef} onKeyDown={onKey} className={cn('relative inline-block', p.className)}>
      <button
        ref={buttonRef}
        type="button"
        aria-haspopup="menu"
        aria-expanded={open}
        aria-controls={id}
        aria-label={`${t.choose}, ${t.current} ${current.label}`}
        onClick={() => setOpen((v) => !v)}
        className="group/lang inline-flex h-10 items-center gap-2 rounded-lg border border-border bg-background px-3 text-[14px] font-medium text-foreground shadow-[var(--shadow-sm)] transition-colors hover:bg-accent aria-expanded:bg-accent max-sm:h-11"
      >
        <Icon name="globe" size={16} className="text-muted-foreground transition-transform duration-500 ease-[var(--ease-kit)] group-hover/lang:rotate-[24deg] group-aria-expanded/lang:rotate-[24deg]" />
        <span className="relative grid overflow-hidden">
          <span key={current.code} className="kit-rise [--kit-delay:0ms]">
            {shortOf(current)}
          </span>
        </span>
        <Icon name="chevron-down" size={14} className={cn('text-muted-foreground transition-transform duration-300 ease-[var(--ease-kit)]', open && 'rotate-180')} />
      </button>
      <div
        id={id}
        role="menu"
        aria-label={t.language}
        inert={!open}
        className={cn(
          'absolute right-0 top-[calc(100%+8px)] z-30 w-64 origin-top-right rounded-xl border border-border bg-popover p-1.5 shadow-[var(--highlight),var(--shadow-lg)] transition-[transform,opacity] duration-300 ease-[var(--ease-kit)] motion-reduce:transition-none',
          open ? 'translate-y-0 scale-100 opacity-100' : 'pointer-events-none -translate-y-1 scale-[0.97] opacity-0',
        )}
      >
        {languages.map((l, i) => {
          const on = l.code === value;
          return (
            <a
              key={l.code}
              {...linkProps(l)}
              role="menuitemradio"
              aria-checked={on}
              onClick={() => {
                pick(l.code);
                setOpen(false);
              }}
              className={cn('flex min-h-11 items-center gap-3 rounded-lg px-2.5 py-1.5 outline-none transition-[background-color,transform,opacity] duration-300 ease-[var(--ease-kit)] hover:bg-accent focus-visible:bg-accent', open ? 'translate-y-0 opacity-100' : '-translate-y-1 opacity-0')}
              style={{ transitionDelay: open ? `${40 + i * 30}ms` : '0ms' }}
            >
              <span className="grid h-7 w-9 shrink-0 place-items-center rounded-md bg-muted text-[11.5px] font-semibold text-muted-foreground">{shortOf(l)}</span>
              <span className="min-w-0 flex-1">
                <span className="block text-[14px] font-medium text-foreground">{l.label}</span>
                {l.english && l.english !== l.label && <span className="block text-[12.5px] text-muted-foreground">{l.english}</span>}
              </span>
              <Icon name="check" size={16} className={cn('text-primary transition-[opacity,transform] duration-300', on ? 'scale-100 opacity-100' : 'scale-50 opacity-0')} />
            </a>
          );
        })}
      </div>
    </div>
  );
}

/* ------------------------------------------------------------------ */
/* segmented                                                           */
/* ------------------------------------------------------------------ */

function Segmented(p: LanguageSwitcherProps & { t: Texts }) {
  const { t } = p;
  const languages = p.languages ?? [];
  const [value, pick] = useChoice(p);
  const listRef = useRef<HTMLDivElement>(null);
  const [mark, setMark] = useState<{ x: number; w: number } | null>(null);
  useLayoutEffect(() => {
    const a = listRef.current?.querySelector<HTMLElement>('[aria-current="true"]');
    if (a) setMark({ x: a.offsetLeft, w: a.offsetWidth });
  }, [value, languages.length]);
  return (
    <nav aria-label={t.language} className={cn('inline-block', p.className)}>
      <div ref={listRef} className="relative inline-flex h-10 items-center rounded-lg border border-border bg-surface p-1 shadow-[var(--highlight)] max-sm:h-11">
        {mark && <span aria-hidden className="absolute inset-y-1 left-0 rounded-md bg-card shadow-[var(--highlight),var(--shadow-sm)] transition-[transform,width] duration-500 ease-[var(--ease-kit)] motion-reduce:transition-none" style={{ width: mark.w, transform: `translateX(${mark.x}px)` }} />}
        {languages.map((l) => {
          const on = l.code === value;
          return (
            <a
              key={l.code}
              {...linkProps(l)}
              aria-current={on ? 'true' : undefined}
              aria-label={l.label}
              title={l.label}
              onClick={() => pick(l.code)}
              className={cn('relative z-10 grid h-full min-w-11 place-items-center rounded-md px-3 text-[13px] font-semibold tracking-[0.02em] transition-colors duration-300', on ? 'text-foreground' : 'text-muted-foreground hover:text-foreground', !mark && on && 'bg-card shadow-[var(--shadow-sm)]')}
            >
              {shortOf(l)}
            </a>
          );
        })}
      </div>
    </nav>
  );
}

/* ------------------------------------------------------------------ */
/* inline-list                                                         */
/* ------------------------------------------------------------------ */

function InlineList(p: LanguageSwitcherProps & { t: Texts }) {
  const { t } = p;
  const languages = p.languages ?? [];
  const [value, pick] = useChoice(p);
  return (
    <nav aria-label={p.title ?? t.language} className={cn('flex flex-col gap-3 sm:flex-row sm:items-center sm:gap-5', p.className)}>
      <span className="flex items-center gap-2 text-[13px] font-medium text-foreground">
        <Icon name="languages" size={16} className="text-muted-foreground" />
        {p.title ?? t.language}
      </span>
      <Stagger as="ul" step={50} className="-ml-2.5 flex flex-wrap gap-x-1 gap-y-1 sm:ml-0">
        {languages.map((l) => {
          const on = l.code === value;
          return (
            <li key={l.code}>
              <a
                {...linkProps(l)}
                aria-current={on ? 'true' : undefined}
                onClick={() => pick(l.code)}
                className={cn(
                  'relative inline-flex min-h-11 items-center rounded-md px-2.5 text-[14px] transition-colors sm:min-h-9',
                  'after:absolute after:inset-x-2.5 after:bottom-1.5 after:h-px after:origin-left after:bg-current after:transition-transform after:duration-300 after:ease-[var(--ease-kit)] after:content-[""] sm:after:bottom-1',
                  on ? 'font-medium text-foreground after:scale-x-100' : 'text-muted-foreground after:scale-x-0 hover:text-foreground hover:after:scale-x-100',
                )}
              >
                {l.label}
              </a>
            </li>
          );
        })}
      </Stagger>
    </nav>
  );
}

/* ------------------------------------------------------------------ */
/* suggestion-banner                                                   */
/* ------------------------------------------------------------------ */

function SuggestionBanner(p: LanguageSwitcherProps & { t: Texts }) {
  const { t } = p;
  const s = p.suggestion;
  const target = p.languages?.find((l) => l.code === s?.code);
  const [closed, setClosed] = useState(false);
  if (!s || !target) return null;
  return (
    <div
      role="region"
      aria-label={t.language}
      lang={target.code}
      inert={closed}
      className={cn('grid transition-[grid-template-rows,opacity] duration-500 ease-[var(--ease-kit)] motion-reduce:transition-none', closed ? 'grid-rows-[0fr] opacity-0' : 'grid-rows-[1fr] opacity-100', p.className)}
    >
      <div className="min-h-0 overflow-hidden">
        <div className="kit-rise flex flex-col gap-3 rounded-xl border border-border bg-card p-3 pl-4 shadow-[var(--highlight),var(--shadow-md)] sm:flex-row sm:items-center sm:gap-4">
          <span className="flex min-w-0 flex-1 items-start gap-3 sm:items-center">
            <span className="grid size-8 shrink-0 place-items-center rounded-lg bg-[color-mix(in_oklab,var(--primary)_12%,var(--card))] text-primary">
              <Icon name="languages" size={16} />
            </span>
            <span className="text-pretty pt-1 text-[14px] leading-snug text-foreground sm:pt-0">{s.message}</span>
          </span>
          <span className="flex shrink-0 items-center gap-2">
            <Button size="sm" {...linkProps(target)} onClick={() => p.onChange?.(target.code)} className="max-sm:flex-1">
              {s.action}
            </Button>
            <Button size="sm" variant="ghost" onClick={() => setClosed(true)} className="max-sm:flex-1">
              {s.stay}
            </Button>
          </span>
        </div>
      </div>
    </div>
  );
}

/* ------------------------------------------------------------------ */
/* region-picker                                                       */
/* ------------------------------------------------------------------ */

const norm = (s: string) => s.toLocaleLowerCase().normalize('NFD').replace(/[̀-ͯ]/g, '');

function RegionPicker(p: LanguageSwitcherProps & { t: Texts }) {
  const { t } = p;
  const regions = p.regions ?? [];
  const [tab, setTab] = useState('all');
  const [query, setQuery] = useState('');
  const [current, setCurrent] = useState({ country: p.country, code: p.value });
  const [tabsRef, fade] = useScrollFade<HTMLDivElement>();
  const [mark, setMark] = useState<{ x: number; w: number } | null>(null);
  useLayoutEffect(() => {
    const a = tabsRef.current?.querySelector<HTMLElement>('[aria-selected="true"]');
    if (a) setMark({ x: a.offsetLeft, w: a.offsetWidth });
  }, [tab, regions.length]);

  const q = norm(query.trim());
  const shown = useMemo(
    () =>
      regions
        .filter((r) => tab === 'all' || r.id === tab)
        .flatMap((r) => r.countries.map((c) => ({ ...c, region: r.title })))
        .filter((c) => !q || norm(c.name).includes(q) || c.languages.some((l) => norm(l.label).includes(q) || norm(l.english ?? '').includes(q))),
    [regions, tab, q],
  );
  const tabs = [{ id: 'all', title: t.all }, ...regions.map((r) => ({ id: r.id, title: r.title }))];

  return (
    <div className={cn('rounded-2xl border border-border bg-card shadow-[var(--highlight),var(--shadow-md)]', p.className)}>
      <div className="flex flex-col gap-4 border-b border-border p-5 sm:p-6 lg:flex-row lg:items-end lg:justify-between">
        <div>
          {p.title && <h2 className="text-balance text-h3 font-semibold text-foreground">{p.title}</h2>}
          {p.description && <p className="mt-1.5 max-w-[34rem] text-pretty text-[14.5px] text-muted-foreground">{p.description}</p>}
        </div>
        <label className="relative block lg:w-72">
          <span className="sr-only">{t.search}</span>
          <Icon name="search" size={16} className="pointer-events-none absolute left-3.5 top-1/2 -translate-y-1/2 text-muted-foreground" />
          <input
            type="search"
            value={query}
            onChange={(e) => setQuery(e.target.value)}
            placeholder={t.search}
            className="h-11 w-full rounded-lg border border-input bg-background pl-10 pr-3 text-[14.5px] text-foreground shadow-[var(--shadow-sm)] placeholder:text-subtle-foreground transition-[border-color,box-shadow] focus-visible:border-[color-mix(in_oklab,var(--primary)_50%,var(--border))] focus-visible:shadow-[0_0_0_4px_var(--glow)] focus-visible:outline-none"
          />
        </label>
      </div>
      <div ref={tabsRef} role="tablist" aria-label={t.all} className={cn('kit-no-scrollbar relative flex overflow-x-auto border-b border-border px-3 sm:px-4', fade)}>
        {mark && <span aria-hidden className="absolute bottom-0 left-0 h-0.5 rounded-full bg-primary transition-[transform,width] duration-500 ease-[var(--ease-kit)] motion-reduce:transition-none" style={{ width: mark.w, transform: `translateX(${mark.x}px)` }} />}
        {tabs.map((r) => (
          <button
            key={r.id}
            type="button"
            role="tab"
            aria-selected={tab === r.id}
            onClick={() => setTab(r.id)}
            className={cn('relative h-12 shrink-0 whitespace-nowrap px-3 text-[14px] font-medium transition-colors', tab === r.id ? 'text-foreground' : 'text-muted-foreground hover:text-foreground')}
          >
            {r.title}
          </button>
        ))}
      </div>
      <div className="p-3 sm:p-4">
        {shown.length === 0 ? (
          <p role="status" className="kit-rise px-3 py-10 text-center text-[14.5px] text-muted-foreground">
            {t.none}
          </p>
        ) : (
          <Stagger key={`${tab}|${q}`} as="ul" step={35} className="grid grid-cols-2 gap-1 lg:grid-cols-3">
            {shown.map((c) => {
              const here = current.country === c.name;
              return (
                <li key={`${c.region}-${c.name}`} className={cn('rounded-xl p-3 transition-colors', here ? 'bg-[color-mix(in_oklab,var(--primary)_7%,var(--card))] ring-1 ring-[color-mix(in_oklab,var(--primary)_30%,var(--border))]' : 'hover:bg-surface')}>
                  <p className="flex items-center justify-between gap-2 text-[14.5px] font-medium text-foreground">
                    {c.name}
                    {here && <Icon name="check" size={15} className="text-primary" />}
                  </p>
                  <div className="mt-1 flex flex-wrap gap-x-3">
                    {c.languages.map((l) => {
                      const on = here && current.code === l.code;
                      return (
                        <a
                          key={l.code}
                          {...linkProps(l)}
                          aria-current={on ? 'true' : undefined}
                          onClick={() => {
                            setCurrent({ country: c.name, code: l.code });
                            p.onChange?.(l.code);
                          }}
                          className={cn('inline-flex min-h-9 items-center text-[13.5px] underline-offset-4 transition-colors hover:underline max-sm:min-h-11', on ? 'font-medium text-primary' : 'text-muted-foreground hover:text-foreground')}
                        >
                          {l.label}
                        </a>
                      );
                    })}
                  </div>
                </li>
              );
            })}
          </Stagger>
        )}
      </div>
    </div>
  );
}

/* ------------------------------------------------------------------ */
/* LanguageSwitcher                                                    */
/* ------------------------------------------------------------------ */

const VARIANTS: Record<LanguageSwitcherVariant, (p: LanguageSwitcherProps & { t: Texts }) => React.ReactNode> = {
  dropdown: Dropdown,
  segmented: Segmented,
  'inline-list': InlineList,
  'suggestion-banner': SuggestionBanner,
  'region-picker': RegionPicker,
};

/**
 * Language and region choice with real links per language.
 *   <LanguageSwitcher variant="dropdown" value="de" languages={[{ code: 'de', label: 'Deutsch', href: '/de/' }, …]} />
 */
export function LanguageSwitcher({ variant = 'dropdown', labels, ...props }: LanguageSwitcherProps) {
  const t = useTexts(TEXTS, labels);
  const View = VARIANTS[variant] ?? Dropdown;
  return <View {...props} t={t} />;
}

Häufige Fragen

Wie installiere ich Sprachwahl?

Mit aiw add @ai-website/language-switcher. Der Befehl legt die Datei components/ui/language-switcher.tsx in dein Projekt, der Code gehört danach dir. Mit dabei sind button, icon und motion. Weitere npm-Pakete braucht sie nicht.

Welche Varianten hat Sprachwahl?

5 Varianten, Dropdown, Umschalter, Liste im Fuß, Sprachhinweis und Regionswahl. Du wählst sie mit der Prop variant, zum Beispiel variant="segmented".

Kann ich die Texte von Sprachwahl übersetzen?

Ja. Sprachwahl bringt 7 eigene Texte auf Deutsch und Englisch mit, etwa „Sprache“, „Sprache wählen“ und „Aktuelle Sprache“. Die Sprache folgt dem KitProvider, einzelne Texte ersetzt du über die Prop labels.

Kann ein KI-Assistent Sprachwahl einbauen?

Ja. Über den MCP-Server der aiw CLI finden Claude, Cursor, GitHub Copilot, Windsurf, Codex und Gemini Sprachwahl mit aiw_components und installieren sie mit aiw_add @ai-website/language-switcher. Nenn im Prompt die Variante, etwa variant="dropdown", dann schreibt der Assistent deine Inhalte direkt in die Props.

Kann ich Sprachwahl ohne Code anpassen?

Ja, im Studio. 4 von 5 Varianten lassen sich dort bearbeiten, Texte, Listen und Schalter. Das Studio schreibt den passenden LanguageSwitcher-Code zum Kopieren.

Oft gesucht

Direkt zu

In die Zwischenablage kopiert