CrudLocalization.java
package com.vaadin.demo.component.crud;
import com.vaadin.demo.domain.Person;
import com.vaadin.flow.component.crud.BinderCrudEditor;
import com.vaadin.flow.component.crud.Crud;
import com.vaadin.flow.component.crud.CrudEditor;
import com.vaadin.flow.component.crud.CrudI18n;
import com.vaadin.flow.component.formlayout.FormLayout;
import com.vaadin.flow.component.grid.Grid;
import com.vaadin.flow.component.html.Div;
import com.vaadin.flow.component.textfield.EmailField;
import com.vaadin.flow.component.textfield.TextField;
import com.vaadin.flow.data.binder.Binder;
import com.vaadin.flow.router.Route;
import java.util.Arrays;
import java.util.List;
@Route("crud-localization")
public class CrudLocalization extends Div {
    private Crud<Person> crud;
    private String FIRST_NAME = "firstName";
    private String LAST_NAME = "lastName";
    private String EMAIL = "email";
    private String PROFESSION = "profession";
    private String EDIT_COLUMN = "vaadin-crud-edit-column";
    public CrudLocalization() {
        crud = new Crud<>(Person.class, createEditor());
        setupGrid();
        setupDataProvider();
        setupI18n();
        add(crud);
    }
    private CrudEditor<Person> createEditor() {
        TextField firstName = new TextField("First name");
        TextField lastName = new TextField("Last name");
        EmailField email = new EmailField("Email");
        TextField profession = new TextField("Profession");
        FormLayout form = new FormLayout(firstName, lastName, email,
                profession);
        Binder<Person> binder = new Binder<>(Person.class);
        binder.forField(firstName).asRequired().bind(Person::getFirstName,
                Person::setFirstName);
        binder.forField(lastName).asRequired().bind(Person::getLastName,
                Person::setLastName);
        binder.forField(email).asRequired().bind(Person::getEmail,
                Person::setEmail);
        binder.forField(profession).asRequired().bind(Person::getProfession,
                Person::setProfession);
        return new BinderCrudEditor<>(binder, form);
    }
    private void setupGrid() {
        Grid<Person> grid = crud.getGrid();
        // Only show these columns (all columns shown by default):
        List<String> visibleColumns = Arrays.asList(FIRST_NAME, LAST_NAME,
                EMAIL, PROFESSION, EDIT_COLUMN);
        grid.getColumns().forEach(column -> {
            String key = column.getKey();
            if (!visibleColumns.contains(key)) {
                grid.removeColumn(column);
            }
        });
        // Reorder the columns (alphabetical by default)
        grid.setColumnOrder(grid.getColumnByKey(FIRST_NAME),
                grid.getColumnByKey(LAST_NAME), grid.getColumnByKey(EMAIL),
                grid.getColumnByKey(PROFESSION),
                grid.getColumnByKey(EDIT_COLUMN));
        // Translate headers
        grid.getColumnByKey(FIRST_NAME).setHeader("Etunimi");
        grid.getColumnByKey(LAST_NAME).setHeader("Sukunimi");
        grid.getColumnByKey(EMAIL).setHeader("Sähköposti");
        grid.getColumnByKey(PROFESSION).setHeader("Ammatti");
    }
    private void setupDataProvider() {
        PersonDataProvider dataProvider = new PersonDataProvider();
        crud.setDataProvider(dataProvider);
        crud.addDeleteListener(
                deleteEvent -> dataProvider.delete(deleteEvent.getItem()));
        crud.addSaveListener(
                saveEvent -> dataProvider.persist(saveEvent.getItem()));
    }
    private void setupI18n() {
        // tag::snippet[]
        CrudI18n i18n = CrudI18n.createDefault();
        i18n.setNewItem("Luo uusi");
        i18n.setEditItem("Muuta tietoja");
        i18n.setSaveItem("Tallenna");
        i18n.setCancel("Peruuta");
        i18n.setDeleteItem("Poista...");
        i18n.setEditLabel("Muokkaa");
        CrudI18n.Confirmations.Confirmation delete = i18n.getConfirm()
                .getDelete();
        delete.setTitle("Poista kohde");
        delete.setContent(
                "Haluatko varmasti poistaa tämän kohteen? Poistoa ei voi perua.");
        delete.getButton().setConfirm("Poista");
        delete.getButton().setDismiss("Peruuta");
        CrudI18n.Confirmations.Confirmation cancel = i18n.getConfirm()
                .getCancel();
        cancel.setTitle("Hylkää muutokset");
        cancel.setContent("Kohteessa on tallentamattomia muutoksia.");
        cancel.getButton().setConfirm("Hylkää");
        cancel.getButton().setDismiss("Peruuta");
        crud.setI18n(i18n);
        // end::snippet[]
    }
}
PersonDataProvider.java
package com.vaadin.demo.component.crud;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Optional;
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.stream.Stream;
import com.vaadin.demo.domain.DataService;
import com.vaadin.demo.domain.Person;
import com.vaadin.flow.component.crud.CrudFilter;
import com.vaadin.flow.data.provider.AbstractBackEndDataProvider;
import com.vaadin.flow.data.provider.Query;
import com.vaadin.flow.data.provider.SortDirection;
import static java.util.Comparator.naturalOrder;
// Person data provider
public class PersonDataProvider
        extends AbstractBackEndDataProvider<Person, CrudFilter> {
    // A real app should hook up something like JPA
    final List<Person> DATABASE = new ArrayList<>(DataService.getPeople());
    private Consumer<Long> sizeChangeListener;
    @Override
    protected Stream<Person> fetchFromBackEnd(Query<Person, CrudFilter> query) {
        int offset = query.getOffset();
        int limit = query.getLimit();
        Stream<Person> stream = DATABASE.stream();
        if (query.getFilter().isPresent()) {
            stream = stream.filter(predicate(query.getFilter().get()))
                    .sorted(comparator(query.getFilter().get()));
        }
        return stream.skip(offset).limit(limit);
    }
    @Override
    protected int sizeInBackEnd(Query<Person, CrudFilter> query) {
        // For RDBMS just execute a SELECT COUNT(*) ... WHERE query
        long count = fetchFromBackEnd(query).count();
        if (sizeChangeListener != null) {
            sizeChangeListener.accept(count);
        }
        return (int) count;
    }
    void setSizeChangeListener(Consumer<Long> listener) {
        sizeChangeListener = listener;
    }
    private static Predicate<Person> predicate(CrudFilter filter) {
        // For RDBMS just generate a WHERE clause
        return filter.getConstraints().entrySet().stream()
                .map(constraint -> (Predicate<Person>) person -> {
                    try {
                        Object value = valueOf(constraint.getKey(), person);
                        return value != null && value.toString().toLowerCase()
                                .contains(constraint.getValue().toLowerCase());
                    } catch (Exception e) {
                        e.printStackTrace();
                        return false;
                    }
                }).reduce(Predicate::and).orElse(e -> true);
    }
    private static Comparator<Person> comparator(CrudFilter filter) {
        // For RDBMS just generate an ORDER BY clause
        return filter.getSortOrders().entrySet().stream().map(sortClause -> {
            try {
                Comparator<Person> comparator = Comparator.comparing(
                        person -> (Comparable) valueOf(sortClause.getKey(),
                                person));
                if (sortClause.getValue() == SortDirection.DESCENDING) {
                    comparator = comparator.reversed();
                }
                return comparator;
            } catch (Exception ex) {
                return (Comparator<Person>) (o1, o2) -> 0;
            }
        }).reduce(Comparator::thenComparing).orElse((o1, o2) -> 0);
    }
    private static Object valueOf(String fieldName, Person person) {
        try {
            Field field = Person.class.getDeclaredField(fieldName);
            field.setAccessible(true);
            return field.get(person);
        } catch (Exception ex) {
            throw new RuntimeException(ex);
        }
    }
    void persist(Person item) {
        if (item.getId() == null) {
            item.setId(DATABASE.stream().map(Person::getId).max(naturalOrder())
                    .orElse(0) + 1);
        }
        final Optional<Person> existingItem = find(item.getId());
        if (existingItem.isPresent()) {
            int position = DATABASE.indexOf(existingItem.get());
            DATABASE.remove(existingItem.get());
            DATABASE.add(position, item);
        } else {
            DATABASE.add(item);
        }
    }
    Optional<Person> find(Integer id) {
        return DATABASE.stream().filter(entity -> entity.getId().equals(id))
                .findFirst();
    }
    void delete(Person item) {
        DATABASE.removeIf(entity -> entity.getId().equals(item.getId()));
    }
}
PersonDataProvider.java
crud-localization.tsx
import React, { useEffect } from 'react';
import { useSignal } from '@vaadin/hilla-react-signals';
import { ComboBox } from '@vaadin/react-components/ComboBox.js';
import { EmailField } from '@vaadin/react-components/EmailField.js';
import { Grid } from '@vaadin/react-components/Grid.js';
import { GridColumn } from '@vaadin/react-components/GridColumn.js';
import { TextField } from '@vaadin/react-components/TextField.js';
import { Crud, crudPath } from '@vaadin/react-components-pro/Crud.js';
import { CrudEditColumn } from '@vaadin/react-components-pro/CrudEditColumn.js';
import { getPeople } from 'Frontend/demo/domain/DataService';
import type Person from 'Frontend/generated/com/vaadin/demo/domain/Person';
function Example() {
  const items = useSignal<Person[]>([]);
  useEffect(() => {
    getPeople().then(({ people }) => {
      items.value = people;
    });
  }, []);
  // tag::snippet[]
  const i18n = {
    newItem: 'Luo uusi',
    editItem: 'Muuta tietoja',
    saveItem: 'Tallenna',
    cancel: 'Peruuta',
    deleteItem: 'Poista...',
    editLabel: 'Muokkaa',
    confirm: {
      delete: {
        title: 'Poista kohde',
        content: 'Haluatko varmasti poistaa tämän kohteen? Poistoa ei voi perua.',
        button: {
          confirm: 'Poista',
          dismiss: 'Peruuta',
        },
      },
      cancel: {
        title: 'Hylkää muutokset',
        content: 'Kohteessa on tallentamattomia muutoksia',
        button: {
          confirm: 'Hylkää',
          dismiss: 'Peruuta',
        },
      },
    },
  };
  return (
    <Crud
      editorPosition="aside"
      include="firstName, lastName, email, profession"
      items={items.value}
      i18n={i18n}
    >
      <Grid slot="grid">
        <GridColumn path="firstName" header="Etunimi" />
        <GridColumn path="lastName" header="Sukunimi" />
        <GridColumn path="email" header="Sähköposti" />
        <GridColumn path="profession" header="Ammatti" />
        <CrudEditColumn />
      </Grid>
      <div slot="form">
        <TextField {...crudPath('firstName')} label="Etunimi" required />
        <TextField {...crudPath('lastName')} label="Sukunimi" required />
        <EmailField {...crudPath('email')} label="Sähköposti" required />
        <ComboBox
          {...crudPath('profession')}
          label="Ammatti"
          items={[...new Set(items.value.map((i) => i.profession))]}
        />
      </div>
    </Crud>
  );
  // end::snippet[]
}
crud-localization.ts
import '@vaadin/crud';
import { html, LitElement } from 'lit';
import { customElement, query, state } from 'lit/decorators.js';
import type { Crud } from '@vaadin/crud';
import { getPeople } from 'Frontend/demo/domain/DataService';
import type Person from 'Frontend/generated/com/vaadin/demo/domain/Person';
import { applyTheme } from 'Frontend/demo/theme';
@customElement('crud-localization')
export class Example extends LitElement {
  protected override createRenderRoot() {
    const root = super.createRenderRoot();
    applyTheme(root);
    return root;
  }
  @state()
  private items: Person[] = [];
  @query('vaadin-crud')
  private crud!: Crud<Person>;
  protected override async firstUpdated() {
    const { people } = await getPeople();
    this.items = people;
    this.crud.i18n = {
      newItem: 'Luo uusi',
      editItem: 'Muuta tietoja',
      saveItem: 'Tallenna',
      cancel: 'Peruuta',
      deleteItem: 'Poista...',
      editLabel: 'Muokkaa',
      confirm: {
        delete: {
          title: 'Poista kohde',
          content: 'Haluatko varmasti poistaa tämän kohteen? Poistoa ei voi perua.',
          button: {
            confirm: 'Poista',
            dismiss: 'Peruuta',
          },
        },
        cancel: {
          title: 'Hylkää muutokset',
          content: 'Kohteessa on tallentamattomia muutoksia',
          button: {
            confirm: 'Hylkää',
            dismiss: 'Peruuta',
          },
        },
      },
    };
  }
  protected override render() {
    return html`
      <!-- tag::snippethtml[] -->
      <vaadin-crud
        editor-position="aside"
        include="firstName, lastName, email, profession"
        .items="${this.items}"
      >
        <vaadin-grid slot="grid">
          <vaadin-grid-column path="firstName" header="Etunimi"></vaadin-grid-column>
          <vaadin-grid-column path="lastName" header="Sukunimi"></vaadin-grid-column>
          <vaadin-grid-column path="email" header="Sähköposti"></vaadin-grid-column>
          <vaadin-grid-column path="profession" header="Ammatti"></vaadin-grid-column>
          <vaadin-crud-edit-column></vaadin-crud-edit-column>
        </vaadin-grid>
        <vaadin-form-layout slot="form">
          <vaadin-text-field path="firstName" label="Etunimi" required></vaadin-text-field>
          <vaadin-text-field path="lastName" label="Sukunimi" required></vaadin-text-field>
          <vaadin-email-field path="email" label="Sähköposti" required></vaadin-email-field>
          <vaadin-combo-box
            path="profession"
            label="Ammatti"
            .items="${[...new Set(this.items.map((i) => i.profession))]}"
          ></vaadin-combo-box>
        </vaadin-form-layout>
      </vaadin-crud>
      <!-- end::snippethtml[] -->
    `;
  }
}