filters-internationalization

PreviousNext
Docs
reuicomponent

Preview

Loading preview…
registry/default/components/filters/internationalization.tsx
'use client';

import { useCallback, useMemo, useState } from 'react';
import { Button } from '@/registry/default/ui/button';
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuTrigger,
} from '@/registry/default/ui/dropdown-menu';
import {
  createFilter,
  Filters,
  type Filter,
  type FilterFieldConfig,
  type FilterI18nConfig,
} from '@/registry/default/ui/filters';
import { Building, Calendar, CheckCircle, ChevronDown, DollarSign, Mail, MapPin, Star, User } from 'lucide-react';

// Internationalization configurations
const i18nConfigs: Record<string, FilterI18nConfig> = {
  en: {
    // UI Labels
    addFilter: 'Add filter',
    searchFields: 'Search fields...',
    noFieldsFound: 'No fields found.',
    noResultsFound: 'No results found.',
    select: 'Select...',
    true: 'True',
    false: 'False',
    min: 'Min',
    max: 'Max',
    to: 'to',
    typeAndPressEnter: 'Type and press Enter to add tag',
    selected: 'selected',
    selectedCount: 'selected',
    percent: '%',
    defaultCurrency: '$',
    defaultColor: '#000000',
    addFilterTitle: 'Add filter',

    // Operators
    operators: {
      is: 'is',
      isNot: 'is not',
      isAnyOf: 'is any of',
      isNotAnyOf: 'is not any of',
      includesAll: 'includes all',
      excludesAll: 'excludes all',
      before: 'before',
      after: 'after',
      between: 'between',
      notBetween: 'not between',
      contains: 'contains',
      notContains: 'does not contain',
      startsWith: 'starts with',
      endsWith: 'ends with',
      isExactly: 'is exactly',
      equals: 'equals',
      notEquals: 'not equals',
      greaterThan: 'greater than',
      lessThan: 'less than',
      overlaps: 'overlaps',
      includes: 'includes',
      excludes: 'excludes',
      includesAllOf: 'includes all of',
      includesAnyOf: 'includes any of',
      empty: 'is empty',
      notEmpty: 'is not empty',
    },

    // Placeholders
    placeholders: {
      enterField: (fieldType: string) => `Enter ${fieldType}...`,
      selectField: 'Select...',
      searchField: (fieldName: string) => `Search ${fieldName.toLowerCase()}...`,
      enterKey: 'Enter key...',
      enterValue: 'Enter value...',
    },

    // Helper functions
    helpers: {
      formatOperator: (operator: string) => operator.replace(/_/g, ' '),
    },

    // Validation
    validation: {
      invalidEmail: 'Invalid email format',
      invalidUrl: 'Invalid URL format',
      invalidTel: 'Invalid phone format',
      invalid: 'Invalid input format',
    },
  },
  es: {
    // UI Labels
    addFilter: 'Agregar filtro',
    searchFields: 'Buscar campos...',
    noFieldsFound: 'No se encontraron campos.',
    noResultsFound: 'No se encontraron resultados.',
    select: 'Seleccionar...',
    true: 'Verdadero',
    false: 'Falso',
    min: 'Mín',
    max: 'Máx',
    to: 'a',
    typeAndPressEnter: 'Escriba y presione Enter para agregar etiqueta',
    selected: 'seleccionado',
    selectedCount: 'seleccionados',
    percent: '%',
    defaultCurrency: '€',
    defaultColor: '#000000',
    addFilterTitle: 'Agregar filtro',

    // Operators
    operators: {
      is: 'es',
      isNot: 'no es',
      isAnyOf: 'es cualquiera de',
      isNotAnyOf: 'no es cualquiera de',
      includesAll: 'incluye todos',
      excludesAll: 'excluye todos',
      before: 'antes de',
      after: 'después de',
      between: 'entre',
      notBetween: 'no entre',
      contains: 'contiene',
      notContains: 'no contiene',
      startsWith: 'comienza con',
      endsWith: 'termina con',
      isExactly: 'es exactamente',
      equals: 'igual a',
      notEquals: 'no igual a',
      greaterThan: 'mayor que',
      lessThan: 'menor que',
      overlaps: 'se superpone',
      includes: 'incluye',
      excludes: 'excluye',
      includesAllOf: 'incluye todos de',
      includesAnyOf: 'incluye cualquiera de',
      empty: 'está vacío',
      notEmpty: 'no está vacío',
    },

    // Placeholders
    placeholders: {
      enterField: (fieldType: string) => `Ingrese ${fieldType}...`,
      selectField: 'Seleccionar...',
      searchField: (fieldName: string) => `Buscar ${fieldName.toLowerCase()}...`,
      enterKey: 'Ingrese clave...',
      enterValue: 'Ingrese valor...',
    },

    // Helper functions
    helpers: {
      formatOperator: (operator: string) => operator.replace(/_/g, ' '),
    },

    // Validation
    validation: {
      invalidEmail: 'Formato de email inválido',
      invalidUrl: 'Formato de URL inválido',
      invalidTel: 'Formato de teléfono inválido',
      invalid: 'Formato de entrada inválido',
    },
  },
  fr: {
    // UI Labels
    addFilter: 'Ajouter un filtre',
    searchFields: 'Rechercher des champs...',
    noFieldsFound: 'Aucun champ trouvé.',
    noResultsFound: 'Aucun résultat trouvé.',
    select: 'Sélectionner...',
    true: 'Vrai',
    false: 'Faux',
    min: 'Min',
    max: 'Max',
    to: 'à',
    typeAndPressEnter: 'Tapez et appuyez sur Entrée pour ajouter une étiquette',
    selected: 'sélectionné',
    selectedCount: 'sélectionnés',
    percent: '%',
    defaultCurrency: '€',
    defaultColor: '#000000',
    addFilterTitle: 'Ajouter un filtre',

    // Operators
    operators: {
      is: 'est',
      isNot: "n'est pas",
      isAnyOf: "est l'un de",
      isNotAnyOf: "n'est pas l'un de",
      includesAll: 'inclut tous',
      excludesAll: 'exclut tous',
      before: 'avant',
      after: 'après',
      between: 'entre',
      notBetween: 'pas entre',
      contains: 'contient',
      notContains: 'ne contient pas',
      startsWith: 'commence par',
      endsWith: 'se termine par',
      isExactly: 'est exactement',
      equals: 'égal à',
      notEquals: 'pas égal à',
      greaterThan: 'supérieur à',
      lessThan: 'inférieur à',
      overlaps: 'se chevauche',
      includes: 'inclut',
      excludes: 'exclut',
      includesAllOf: 'inclut tous de',
      includesAnyOf: "inclut l'un de",
      empty: 'est vide',
      notEmpty: "n'est pas vide",
    },

    // Placeholders
    placeholders: {
      enterField: (fieldType: string) => `Entrez ${fieldType}...`,
      selectField: 'Sélectionner...',
      searchField: (fieldName: string) => `Rechercher ${fieldName.toLowerCase()}...`,
      enterKey: 'Entrez la clé...',
      enterValue: 'Entrez la valeur...',
    },

    // Helper functions
    helpers: {
      formatOperator: (operator: string) => operator.replace(/_/g, ' '),
    },

    // Validation
    validation: {
      invalidEmail: "Format d'email invalide",
      invalidUrl: "Format d'URL invalide",
      invalidTel: 'Format de téléphone invalide',
      invalid: 'Format de saisie invalide',
    },
  },
  de: {
    // UI Labels
    addFilter: 'Filter hinzufügen',
    searchFields: 'Felder suchen...',
    noFieldsFound: 'Keine Felder gefunden.',
    noResultsFound: 'Keine Ergebnisse gefunden.',
    select: 'Auswählen...',
    true: 'Wahr',
    false: 'Falsch',
    min: 'Min',
    max: 'Max',
    to: 'bis',
    typeAndPressEnter: 'Tippen und Enter drücken, um Tag hinzuzufügen',
    selected: 'ausgewählt',
    selectedCount: 'ausgewählt',
    percent: '%',
    defaultCurrency: '€',
    defaultColor: '#000000',
    addFilterTitle: 'Filter hinzufügen',

    // Operators
    operators: {
      is: 'ist',
      isNot: 'ist nicht',
      isAnyOf: 'ist eines von',
      isNotAnyOf: 'ist nicht eines von',
      includesAll: 'enthält alle',
      excludesAll: 'schließt alle aus',
      before: 'vor',
      after: 'nach',
      between: 'zwischen',
      notBetween: 'nicht zwischen',
      contains: 'enthält',
      notContains: 'enthält nicht',
      startsWith: 'beginnt mit',
      endsWith: 'endet mit',
      isExactly: 'ist genau',
      equals: 'gleich',
      notEquals: 'nicht gleich',
      greaterThan: 'größer als',
      lessThan: 'kleiner als',
      overlaps: 'überschneidet sich',
      includes: 'enthält',
      excludes: 'schließt aus',
      includesAllOf: 'enthält alle von',
      includesAnyOf: 'enthält eines von',
      empty: 'ist leer',
      notEmpty: 'ist nicht leer',
    },

    // Placeholders
    placeholders: {
      enterField: (fieldType: string) => `${fieldType} eingeben...`,
      selectField: 'Auswählen...',
      searchField: (fieldName: string) => `${fieldName.toLowerCase()} suchen...`,
      enterKey: 'Schlüssel eingeben...',
      enterValue: 'Wert eingeben...',
    },

    // Helper functions
    helpers: {
      formatOperator: (operator: string) => operator.replace(/_/g, ' '),
    },

    // Validation
    validation: {
      invalidEmail: 'Ungültiges E-Mail-Format',
      invalidUrl: 'Ungültiges URL-Format',
      invalidTel: 'Ungültiges Telefonformat',
      invalid: 'Ungültiges Format',
    },
  },
  ja: {
    // UI Labels
    addFilter: 'フィルターを追加',
    searchFields: 'フィールドを検索...',
    noFieldsFound: 'フィールドが見つかりません。',
    noResultsFound: '結果が見つかりません。',
    select: '選択...',
    true: '真',
    false: '偽',
    min: '最小',
    max: '最大',
    to: 'から',
    typeAndPressEnter: '入力してEnterキーを押してタグを追加',
    selected: '選択済み',
    selectedCount: '選択済み',
    percent: '%',
    defaultCurrency: '¥',
    defaultColor: '#000000',
    addFilterTitle: 'フィルターを追加',

    // Operators
    operators: {
      is: 'は',
      isNot: 'ではない',
      isAnyOf: 'のいずれか',
      isNotAnyOf: 'のいずれでもない',
      includesAll: 'すべて含む',
      excludesAll: 'すべて除外',
      before: 'より前',
      after: 'より後',
      between: 'の間',
      notBetween: 'の間ではない',
      contains: '含む',
      notContains: '含まない',
      startsWith: 'で始まる',
      endsWith: 'で終わる',
      isExactly: '正確に',
      equals: '等しい',
      notEquals: '等しくない',
      greaterThan: 'より大きい',
      lessThan: 'より小さい',
      overlaps: '重複する',
      includes: '含む',
      excludes: '除外',
      includesAllOf: 'すべて含む',
      includesAnyOf: 'いずれか含む',
      empty: '空',
      notEmpty: '空でない',
    },

    // Placeholders
    placeholders: {
      enterField: (fieldType: string) => `${fieldType}を入力...`,
      selectField: '選択...',
      searchField: (fieldName: string) => `${fieldName.toLowerCase()}を検索...`,
      enterKey: 'キーを入力...',
      enterValue: '値を入力...',
    },

    // Helper functions
    helpers: {
      formatOperator: (operator: string) => operator.replace(/_/g, ' '),
    },

    // Validation
    validation: {
      invalidEmail: '無効なメール形式',
      invalidUrl: '無効なURL形式',
      invalidTel: '無効な電話番号形式',
      invalid: '無効な形式',
    },
  },
};

// Language options for the selector
const languageOptions = [
  { value: 'en', label: 'English', flag: '🇺🇸' },
  { value: 'es', label: 'Español', flag: '🇪🇸' },
  { value: 'fr', label: 'Français', flag: '🇫🇷' },
  { value: 'de', label: 'Deutsch', flag: '🇩🇪' },
  { value: 'ja', label: '日本語', flag: '🇯🇵' },
];

export default function InternationalizationDemo() {
  const [currentLanguage, setCurrentLanguage] = useState<string>('es');
  const [filters, setFilters] = useState<Filter[]>([createFilter('status', 'is', ['active'])]);

  // Get current i18n configuration
  const currentI18n = useMemo(() => i18nConfigs[currentLanguage], [currentLanguage]);

  // Filter field configurations with localized labels
  const fields: FilterFieldConfig[] = useMemo(() => {
    const fieldLabels = {
      en: {
        name: 'Name',
        email: 'Email',
        company: 'Company',
        role: 'Role',
        status: 'Status',
        location: 'Location',
        joined: 'Joined Date',
        balance: 'Balance',
        rating: 'Rating',
      },
      es: {
        name: 'Nombre',
        email: 'Correo electrónico',
        company: 'Empresa',
        role: 'Rol',
        status: 'Estado',
        location: 'Ubicación',
        joined: 'Fecha de ingreso',
        balance: 'Saldo',
        rating: 'Calificación',
      },
      fr: {
        name: 'Nom',
        email: 'E-mail',
        company: 'Entreprise',
        role: 'Rôle',
        status: 'Statut',
        location: 'Localisation',
        joined: "Date d'adhésion",
        balance: 'Solde',
        rating: 'Note',
      },
      de: {
        name: 'Name',
        email: 'E-Mail',
        company: 'Unternehmen',
        role: 'Rolle',
        status: 'Status',
        location: 'Standort',
        joined: 'Beitrittsdatum',
        balance: 'Guthaben',
        rating: 'Bewertung',
      },
      ja: {
        name: '名前',
        email: 'メール',
        company: '会社',
        role: '役割',
        status: 'ステータス',
        location: '場所',
        joined: '参加日',
        balance: '残高',
        rating: '評価',
      },
    };

    const labels = fieldLabels[currentLanguage as keyof typeof fieldLabels] || fieldLabels.en;

    return [
      {
        key: 'name',
        label: labels.name,
        icon: <User className="size-3.5" />,
        type: 'text',
        className: 'w-40',
        placeholder:
          currentLanguage === 'en'
            ? 'Search names...'
            : currentLanguage === 'es'
              ? 'Buscar nombres...'
              : currentLanguage === 'fr'
                ? 'Rechercher des noms...'
                : currentLanguage === 'de'
                  ? 'Namen suchen...'
                  : '名前を検索...',
      },
      {
        key: 'email',
        label: labels.email,
        icon: <Mail className="size-3.5" />,
        type: 'email',
        className: 'w-48',
        placeholder: 'user@example.com',
      },
      {
        key: 'company',
        label: labels.company,
        icon: <Building className="size-3.5" />,
        type: 'select',
        searchable: true,
        className: 'w-[180px]',
        options: [
          { value: 'TechCorp', label: 'TechCorp' },
          { value: 'StartupCo', label: 'StartupCo' },
          { value: 'BigCorp', label: 'BigCorp' },
          { value: 'InnovateTech', label: 'InnovateTech' },
          { value: 'GlobalNet', label: 'GlobalNet' },
        ],
      },
      {
        key: 'role',
        label: labels.role,
        icon: <User className="size-3.5" />,
        type: 'select',
        searchable: true,
        className: 'w-[160px]',
        options: [
          { value: 'Developer', label: 'Developer' },
          { value: 'Designer', label: 'Designer' },
          { value: 'Manager', label: 'Manager' },
          { value: 'Product Manager', label: 'Product Manager' },
          { value: 'Sales Rep', label: 'Sales Rep' },
        ],
      },
      {
        key: 'status',
        label: labels.status,
        icon: <CheckCircle className="size-3.5" />,
        type: 'select',
        searchable: false,
        className: 'w-[140px]',
        options: [
          {
            value: 'active',
            label:
              currentLanguage === 'en'
                ? 'Active'
                : currentLanguage === 'es'
                  ? 'Activo'
                  : currentLanguage === 'fr'
                    ? 'Actif'
                    : currentLanguage === 'de'
                      ? 'Aktiv'
                      : 'アクティブ',
          },
          {
            value: 'inactive',
            label:
              currentLanguage === 'en'
                ? 'Inactive'
                : currentLanguage === 'es'
                  ? 'Inactivo'
                  : currentLanguage === 'fr'
                    ? 'Inactif'
                    : currentLanguage === 'de'
                      ? 'Inaktiv'
                      : '非アクティブ',
          },
        ],
      },
      {
        key: 'location',
        label: labels.location,
        icon: <MapPin className="size-3.5" />,
        type: 'text',
        className: 'w-40',
        placeholder:
          currentLanguage === 'en'
            ? 'Search locations...'
            : currentLanguage === 'es'
              ? 'Buscar ubicaciones...'
              : currentLanguage === 'fr'
                ? 'Rechercher des lieux...'
                : currentLanguage === 'de'
                  ? 'Standorte suchen...'
                  : '場所を検索...',
      },
      {
        key: 'joined',
        label: labels.joined,
        icon: <Calendar className="size-3.5" />,
        type: 'date',
        className: 'w-36',
      },
      {
        key: 'balance',
        label: labels.balance,
        icon: <DollarSign className="size-3.5" />,
        type: 'number',
        min: 0,
        max: 10000,
        step: 100,
        className: 'w-32',
      },
      {
        key: 'rating',
        label: labels.rating,
        icon: <Star className="size-3.5" />,
        type: 'number',
        min: 0,
        max: 5,
        step: 0.1,
        className: 'w-32',
      },
    ];
  }, [currentLanguage]);

  const handleFiltersChange = useCallback((newFilters: Filter[]) => {
    console.log('I18n filters updated:', newFilters);
    setFilters(newFilters);
  }, []);

  return (
    <div className="flex items-start grow space-y-6 self-start content-start">
      <div className="space-y-5 grow">
        {/* Filters Section */}
        <div className="flex items-start gap-2.5">
          <div className="flex-1">
            <Filters filters={filters} fields={fields} onChange={handleFiltersChange} size="sm" i18n={currentI18n} />
          </div>

          <div className="flex items-center gap-2">
            {/* Language selection */}
            <DropdownMenu>
              <DropdownMenuTrigger asChild>
                <Button variant="outline" size="sm" className="flex items-center gap-2">
                  <span>{languageOptions.find((lang) => lang.value === currentLanguage)?.flag}</span>
                  <span>{languageOptions.find((lang) => lang.value === currentLanguage)?.label}</span>
                  <ChevronDown className="size-4" />
                </Button>
              </DropdownMenuTrigger>
              <DropdownMenuContent align="start">
                {languageOptions.map((lang) => (
                  <DropdownMenuItem
                    key={lang.value}
                    onClick={() => setCurrentLanguage(lang.value)}
                    className="flex items-center gap-2"
                  >
                    <span>{lang.flag}</span>
                    <span>{lang.label}</span>
                  </DropdownMenuItem>
                ))}
              </DropdownMenuContent>
            </DropdownMenu>
          </div>
        </div>
      </div>
    </div>
  );
}

Installation

npx shadcn@latest add @reui/filters-internationalization

Usage

import { FiltersInternationalization } from "@/components/filters-internationalization"
<FiltersInternationalization />