Compare commits

...

3 Commits

Author SHA1 Message Date
pitfriedrich 38d3cbe38a Merge pull request 'Feature/i18n' (#5) from feature/i18n into main
Reviewed-on: #5
2026-07-19 19:17:08 +00:00
Pit Friedrich 9114ea9800 [FEATURE] i18n
CI / build-and-test (pull_request) Successful in 1m14s
2026-07-19 21:15:23 +02:00
Pit Friedrich 9316b83f82 chore(i18n): scaffold translation bundles 2026-07-19 20:48:03 +02:00
10 changed files with 435 additions and 65 deletions
-1
View File
@@ -16,7 +16,6 @@ jobs:
with: with:
distribution: temurin distribution: temurin
java-version: '25' java-version: '25'
cache: maven
- name: Build and Test - name: Build and Test
run: | run: |
+1
View File
@@ -0,0 +1 @@
key de en es status
1 key de en es status
+39
View File
@@ -0,0 +1,39 @@
# AGENTS.md
This file provides guidance to Codex (Codex.ai/code) when working with code in this repository.
## Commands
```bash
./mvnw spring-boot:run # run app (default goal) — http://localhost:8080, ~30s first start
./mvnw compile # compile only
./mvnw package # production build → target/*.jar
java -jar target/*.jar # run production jar
```
No test sources exist yet (`src/test` is empty) — `spring-boot-starter-test` and `browserless-test-spring` are on the classpath but unused.
**Java 25 toolchain required** (`pom.xml` sets `java.version=25`). If the default JDK on PATH is older (check `java -version`), point `JAVA_HOME` at a Java 25 install for the Maven build, e.g.:
```bash
JAVA_HOME="/path/to/jdk-25" ./mvnw compile
```
Port 8080 conflicts: a prior `spring-boot:run` left running in the background is the usual cause (Vaadin dev mode keeps a second process/thread alive under a different PID than the launching Maven process — killing the Maven process alone may not free the port). Find and stop the actual listener before restarting.
## Architecture
Vaadin Flow (server-side Java UI, no hand-written HTML/JS for views) on Spring Boot 4.1, using the **Aura** theme (not Lumo — no `@Theme` annotation; Aura is wired via `@StyleSheet(Aura.STYLESHEET)` in `Application.java`).
**Charts are ApexCharts, not Vaadin Charts.** The bridge lives in `components/`:
- `ApexChart` (abstract, `@Tag("apex-chart")`) — owns the JS module (`frontend/components/apex-chart.ts`, a Lit element rendering into light DOM), serializes an options `Map` to JSON via Jackson and calls `renderChart` client-side, and exposes point-click events back to the server via `@ClientCallable`.
- `AxisChart` (abstract) — builds `series`/`xaxis.categories` options for line/bar.
- `LineChart`, `BarChart` extend `AxisChart`; `PieChart` extends `ApexChart` directly (`series`/`labels` instead of axis-based).
- `DialectTheme` — single source of chart styling (categorical color palette, grid/legend/stroke option fragments). Every chart's `setData(...)` starts from `DialectTheme.baseOptions(chartType)` and merges in its data. Add new chart types here, not by duplicating option maps.
Since `apex-chart.ts` renders into light DOM (`createRenderRoot()` returns `this`), global CSS can reach into the chart markup — but series/legend/grid colors are driven entirely by the options JSON, not CSS, because ApexCharts renders its own SVG/canvas.
**Views** (`views/`): `MainLayout` (`@Layout`, applies to all routes) is the `AppLayout` shell — navbar + `SideNav` drawer, with one `SideNavItem` per route. Routes: `DashboardView` (`@Route("")`) wraps each chart in a `Card` (`components/Card.java`); `FormView` (`@Route("formular")`) demonstrates form controls bound via `Binder`; `TableView` (`@Route("tabelle")`) demonstrates a `Grid` over dummy data with a live text filter (`GridListDataView.addFilter`, `TextField` in `ValueChangeMode.EAGER`). New views should reuse `Card` to wrap their content rather than adding components directly, and get a matching `SideNavItem` in `MainLayout`.
**Styling**: `src/main/resources/META-INF/resources/styles.css` is the one project-level stylesheet (loaded via `@StyleSheet("styles.css")` in `Application.java`). It defines `--dialect-*` design tokens (Dialect design system: primary orange `#E86C00`, cool-gray background, card radius/shadow) and aliases them onto Aura's own CSS custom properties (`--aura-accent-color-*`, `--aura-background-color-*`, `--aura-orange`, `--aura-yellow`) rather than fighting the theme. Aura tokens use OKLCH + relative-color syntax and accept plain hex overrides. When restyling, prefer extending this alias layer over hardcoding new colors in components.
UI copy/data (chart labels, notifications) is in German.
@@ -6,38 +6,42 @@ import com.example.components.LineChart;
import com.example.components.PieChart; import com.example.components.PieChart;
import com.vaadin.flow.component.notification.Notification; import com.vaadin.flow.component.notification.Notification;
import com.vaadin.flow.component.orderedlayout.VerticalLayout; import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.router.PageTitle; import com.vaadin.flow.router.HasDynamicTitle;
import com.vaadin.flow.router.Route; import com.vaadin.flow.router.Route;
import java.util.List; import java.util.List;
@Route("") @Route("")
@PageTitle("Dashboard") public class DashboardView extends VerticalLayout implements HasDynamicTitle {
public class DashboardView extends VerticalLayout {
public DashboardView() { public DashboardView() {
addClassName("dialect-content"); addClassName("dialect-content");
List<String> months = List.of(
getTranslation("month.jan"), getTranslation("month.feb"),
getTranslation("month.mar"), getTranslation("month.apr"),
getTranslation("month.may"), getTranslation("month.jun"));
LineChart lineChart = new LineChart(); LineChart lineChart = new LineChart();
lineChart.setWidthFull(); lineChart.setWidthFull();
lineChart.setHeight("400px"); lineChart.setHeight("400px");
lineChart.setData("Umsatz 2026", lineChart.setData(getTranslation("chart.revenueSeries"),
List.of(30.0, 40.0, 35.0, 50.0, 49.0, 60.0), List.of(30.0, 40.0, 35.0, 50.0, 49.0, 60.0),
List.of("Jan", "Feb", "Mär", "Apr", "Mai", "Jun")); months);
lineChart.addPointClickListener(e -> { lineChart.addPointClickListener(e -> {
Notification.show("Serie %d, Punkt %d" Notification.show(getTranslation("chart.pointClick",
.formatted(e.getSeriesIndex(), e.getDataPointIndex())); e.getSeriesIndex(), e.getDataPointIndex()));
}); });
BarChart barChart = new BarChart(); BarChart barChart = new BarChart();
barChart.setWidthFull(); barChart.setWidthFull();
barChart.setHeight("400px"); barChart.setHeight("400px");
barChart.setData("Umsatz 2026", barChart.setData(getTranslation("chart.revenueSeries"),
List.of(30.0, 40.0, 35.0, 50.0, 49.0, 60.0), List.of(30.0, 40.0, 35.0, 50.0, 49.0, 60.0),
List.of("Jan", "Feb", "Mär", "Apr", "Mai", "Jun")); months);
barChart.addPointClickListener(e -> { barChart.addPointClickListener(e -> {
Notification.show("Serie %d, Punkt %d" Notification.show(getTranslation("chart.pointClick",
.formatted(e.getSeriesIndex(), e.getDataPointIndex())); e.getSeriesIndex(), e.getDataPointIndex()));
}); });
PieChart pieChart = new PieChart(); PieChart pieChart = new PieChart();
@@ -45,13 +49,19 @@ public class DashboardView extends VerticalLayout {
pieChart.setHeight("400px"); pieChart.setHeight("400px");
pieChart.setData( pieChart.setData(
List.of(30.0, 40.0, 35.0, 50.0), List.of(30.0, 40.0, 35.0, 50.0),
List.of("Nord", "Süd", "Ost", "West")); List.of(getTranslation("region.north"), getTranslation("region.south"),
getTranslation("region.east"), getTranslation("region.west")));
pieChart.addPointClickListener(e -> { pieChart.addPointClickListener(e -> {
Notification.show("Slice %d".formatted(e.getDataPointIndex())); Notification.show(getTranslation("chart.sliceClick", e.getDataPointIndex()));
}); });
add(new Card("Umsatz-Entwicklung", lineChart), add(new Card(getTranslation("card.revenueTrend"), lineChart),
new Card("Umsatz nach Monat", barChart), new Card(getTranslation("card.revenueByMonth"), barChart),
new Card("Umsatz nach Region", pieChart)); new Card(getTranslation("card.revenueByRegion"), pieChart));
}
@Override
public String getPageTitle() {
return getTranslation("page.dashboard");
} }
} }
+41 -33
View File
@@ -20,7 +20,7 @@ import com.vaadin.flow.component.timepicker.TimePicker;
import com.vaadin.flow.data.binder.Binder; import com.vaadin.flow.data.binder.Binder;
import com.vaadin.flow.data.validator.EmailValidator; import com.vaadin.flow.data.validator.EmailValidator;
import com.vaadin.flow.data.validator.IntegerRangeValidator; import com.vaadin.flow.data.validator.IntegerRangeValidator;
import com.vaadin.flow.router.PageTitle; import com.vaadin.flow.router.HasDynamicTitle;
import com.vaadin.flow.router.Route; import com.vaadin.flow.router.Route;
import java.text.DateFormatSymbols; import java.text.DateFormatSymbols;
@@ -33,30 +33,34 @@ import java.util.Locale;
* to {@link Registration} via {@link Binder}. * to {@link Registration} via {@link Binder}.
*/ */
@Route("formular") @Route("formular")
@PageTitle("Formular") public class FormView extends VerticalLayout implements HasDynamicTitle {
public class FormView extends VerticalLayout {
private final Binder<Registration> binder = new Binder<>(Registration.class); private final Binder<Registration> binder = new Binder<>(Registration.class);
public FormView() { public FormView() {
addClassName("dialect-content"); addClassName("dialect-content");
add(new Card("Registrierung", buildForm())); add(new Card(getTranslation("card.registration"), buildForm()));
//<theme-editor-local-classname> //<theme-editor-local-classname>
addClassName("form-view-vertical-layout-1"); addClassName("form-view-vertical-layout-1");
} }
@Override
public String getPageTitle() {
return getTranslation("page.form");
}
private Component buildForm() { private Component buildForm() {
TextField vorname = new TextField("Vorname"); TextField vorname = new TextField(getTranslation("form.firstName"));
vorname.setAutofocus(true); vorname.setAutofocus(true);
TextField nachname = new TextField("Nachname"); TextField nachname = new TextField(getTranslation("form.lastName"));
EmailField email = new EmailField("E-Mail"); EmailField email = new EmailField(getTranslation("form.email"));
PasswordField passwort = new PasswordField("Passwort"); PasswordField passwort = new PasswordField(getTranslation("form.password"));
//<theme-editor-local-classname> //<theme-editor-local-classname>
passwort.addClassName("form-view-password-field-1"); passwort.addClassName("form-view-password-field-1");
IntegerField alter = new IntegerField("Alter"); IntegerField alter = new IntegerField(getTranslation("form.age"));
alter.setReadOnly(true); alter.setReadOnly(true);
BigDecimalField gehalt = new BigDecimalField("Gehalt (€)"); BigDecimalField gehalt = new BigDecimalField(getTranslation("form.salary"));
DatePicker geburtsdatum = new DatePicker("Geburtsdatum"); DatePicker geburtsdatum = new DatePicker(getTranslation("form.birthdate"));
geburtsdatum.setLocale(Locale.GERMANY); geburtsdatum.setLocale(Locale.GERMANY);
DateFormatSymbols symbols = new DateFormatSymbols(Locale.GERMANY); DateFormatSymbols symbols = new DateFormatSymbols(Locale.GERMANY);
@@ -69,44 +73,48 @@ vorname.setAutofocus(true);
.setWeekdays(wochentage) .setWeekdays(wochentage)
.setWeekdaysShort(wochentageKurz) .setWeekdaysShort(wochentageKurz)
.setFirstDayOfWeek(1) .setFirstDayOfWeek(1)
.setToday("Heute") .setToday(getTranslation("form.today"))
.setCancel("Abbrechen")); .setCancel(getTranslation("form.cancel")));
TimePicker uhrzeit = new TimePicker("Bevorzugte Uhrzeit"); TimePicker uhrzeit = new TimePicker(getTranslation("form.time"));
ComboBox<String> land = new ComboBox<>("Land"); ComboBox<String> land = new ComboBox<>(getTranslation("form.country"));
land.setItems("Deutschland", "Österreich", "Schweiz", "Frankreich", "Niederlande"); land.setItems(getTranslation("country.de"), getTranslation("country.at"),
getTranslation("country.ch"), getTranslation("country.fr"), getTranslation("country.nl"));
Select<String> abteilung = new Select<>(); Select<String> abteilung = new Select<>();
abteilung.setLabel("Abteilung"); abteilung.setLabel(getTranslation("form.department"));
abteilung.setItems("Vertrieb", "Marketing", "Entwicklung", "Support", "Geschäftsführung"); abteilung.setItems(getTranslation("dept.sales"), getTranslation("dept.marketing"),
getTranslation("dept.dev"), getTranslation("dept.support"), getTranslation("dept.management"));
CheckboxGroup<String> interessen = new CheckboxGroup<>(); CheckboxGroup<String> interessen = new CheckboxGroup<>();
interessen.setLabel("Interessen"); interessen.setLabel(getTranslation("form.interests"));
interessen.setItems("Newsletter-Themen", "Webinare", "Produkt-Updates", "Events"); interessen.setItems(getTranslation("interest.newsletter"), getTranslation("interest.webinars"),
getTranslation("interest.updates"), getTranslation("interest.events"));
RadioButtonGroup<String> geschlecht = new RadioButtonGroup<>(); RadioButtonGroup<String> geschlecht = new RadioButtonGroup<>();
geschlecht.setLabel("Anrede"); geschlecht.setLabel(getTranslation("form.salutation"));
geschlecht.setItems("Frau", "Herr", "Divers", "Keine Angabe"); geschlecht.setItems(getTranslation("salutation.mrs"), getTranslation("salutation.mr"),
getTranslation("salutation.diverse"), getTranslation("salutation.none"));
Checkbox newsletter = new Checkbox("Newsletter abonnieren"); Checkbox newsletter = new Checkbox(getTranslation("form.newsletter"));
TextArea bemerkung = new TextArea("Bemerkung"); TextArea bemerkung = new TextArea(getTranslation("form.remark"));
bemerkung.setMaxLength(500); bemerkung.setMaxLength(500);
binder.forField(vorname) binder.forField(vorname)
.asRequired("Vorname wird benötigt") .asRequired(getTranslation("valid.firstNameRequired"))
.bind(Registration::getVorname, Registration::setVorname); .bind(Registration::getVorname, Registration::setVorname);
binder.forField(nachname) binder.forField(nachname)
.asRequired("Nachname wird benötigt") .asRequired(getTranslation("valid.lastNameRequired"))
.bind(Registration::getNachname, Registration::setNachname); .bind(Registration::getNachname, Registration::setNachname);
binder.forField(email) binder.forField(email)
.asRequired("E-Mail wird benötigt") .asRequired(getTranslation("valid.emailRequired"))
.withValidator(new EmailValidator("Ungültige E-Mail-Adresse")) .withValidator(new EmailValidator(getTranslation("valid.emailInvalid")))
.bind(Registration::getEmail, Registration::setEmail); .bind(Registration::getEmail, Registration::setEmail);
binder.forField(passwort) binder.forField(passwort)
.bind(Registration::getPasswort, Registration::setPasswort); .bind(Registration::getPasswort, Registration::setPasswort);
binder.forField(alter) binder.forField(alter)
.withValidator(new IntegerRangeValidator("Alter muss zwischen 0 und 120 liegen", 0, 120)) .withValidator(new IntegerRangeValidator(getTranslation("valid.ageRange"), 0, 120))
.bind(Registration::getAlter, Registration::setAlter); .bind(Registration::getAlter, Registration::setAlter);
binder.forField(gehalt) binder.forField(gehalt)
.bind(Registration::getGehalt, Registration::setGehalt); .bind(Registration::getGehalt, Registration::setGehalt);
@@ -143,20 +151,20 @@ vorname.setAutofocus(true);
formLayout.setColspan(bemerkung, 2); formLayout.setColspan(bemerkung, 2);
formLayout.add(interessen, geschlecht, newsletter, bemerkung); formLayout.add(interessen, geschlecht, newsletter, bemerkung);
Button speichern = new Button("Speichern", e -> { Button speichern = new Button(getTranslation("form.save"), e -> {
Registration ziel = new Registration(); Registration ziel = new Registration();
if (binder.writeBeanIfValid(ziel)) { if (binder.writeBeanIfValid(ziel)) {
Notification erfolg = Notification.show( Notification erfolg = Notification.show(
"Gespeichert: %s %s".formatted(ziel.getVorname(), ziel.getNachname())); getTranslation("notify.saved", ziel.getVorname(), ziel.getNachname()));
erfolg.addThemeVariants(NotificationVariant.LUMO_SUCCESS); erfolg.addThemeVariants(NotificationVariant.LUMO_SUCCESS);
} else { } else {
Notification fehler = Notification.show("Bitte Eingaben prüfen"); Notification fehler = Notification.show(getTranslation("notify.checkInput"));
fehler.addThemeVariants(NotificationVariant.LUMO_ERROR); fehler.addThemeVariants(NotificationVariant.LUMO_ERROR);
} }
}); });
speichern.addThemeVariants(ButtonVariant.LUMO_PRIMARY); speichern.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
Button zuruecksetzen = new Button("Zurücksetzen", Button zuruecksetzen = new Button(getTranslation("form.reset"),
e -> binder.readBean(new Registration())); e -> binder.readBean(new Registration()));
HorizontalLayout buttons = new HorizontalLayout(speichern, zuruecksetzen); HorizontalLayout buttons = new HorizontalLayout(speichern, zuruecksetzen);
@@ -1,15 +1,21 @@
package com.example.views; package com.example.views;
import com.example.js.CommonJS; import com.example.js.CommonJS;
import com.vaadin.flow.component.UI;
import com.vaadin.flow.component.applayout.AppLayout; import com.vaadin.flow.component.applayout.AppLayout;
import com.vaadin.flow.component.applayout.DrawerToggle; import com.vaadin.flow.component.applayout.DrawerToggle;
import com.vaadin.flow.component.button.Button; import com.vaadin.flow.component.button.Button;
import com.vaadin.flow.component.button.ButtonVariant; import com.vaadin.flow.component.button.ButtonVariant;
import com.vaadin.flow.component.html.Span; import com.vaadin.flow.component.html.Span;
import com.vaadin.flow.component.icon.VaadinIcon; import com.vaadin.flow.component.icon.VaadinIcon;
import com.vaadin.flow.component.menubar.MenuBar;
import com.vaadin.flow.component.menubar.MenuBarVariant;
import com.vaadin.flow.component.sidenav.SideNav; import com.vaadin.flow.component.sidenav.SideNav;
import com.vaadin.flow.component.sidenav.SideNavItem; import com.vaadin.flow.component.sidenav.SideNavItem;
import com.vaadin.flow.router.Layout; import com.vaadin.flow.router.Layout;
import com.vaadin.flow.server.VaadinSession;
import java.util.Locale;
@Layout @Layout
public class MainLayout extends AppLayout { public class MainLayout extends AppLayout {
@@ -19,32 +25,53 @@ public class MainLayout extends AppLayout {
public MainLayout() { public MainLayout() {
DrawerToggle toggle = new DrawerToggle(); DrawerToggle toggle = new DrawerToggle();
Span title = new Span("Chart App"); Span title = new Span(getTranslation("app.title"));
title.addClassName("dialect-title"); title.addClassName("dialect-title");
Span spacer = new Span(); Span spacer = new Span();
spacer.getStyle().set("flex-grow", "1"); spacer.getStyle().set("flex-grow", "1");
MenuBar languageMenu = buildLanguageMenu();
themeToggle.addThemeVariants(ButtonVariant.TERTIARY, ButtonVariant.LUMO_ICON); themeToggle.addThemeVariants(ButtonVariant.TERTIARY, ButtonVariant.LUMO_ICON);
themeToggle.setAriaLabel("Farbschema wechseln"); themeToggle.setAriaLabel(getTranslation("action.themeToggle"));
themeToggle.addClickListener(e -> themeToggle.addClickListener(e ->
getElement().executeJs(CommonJS.TOGGLE_THEME_JS).then(String.class, this::applyIconFor)); getElement().executeJs(CommonJS.TOGGLE_THEME_JS).then(String.class, this::applyIconFor));
addToNavbar(toggle, title, spacer, themeToggle); addToNavbar(toggle, title, spacer, languageMenu, themeToggle);
getElement().executeJs(CommonJS.INIT_THEME_JS).then(String.class, this::applyIconFor); getElement().executeJs(CommonJS.INIT_THEME_JS).then(String.class, this::applyIconFor);
SideNav nav = new SideNav(); SideNav nav = new SideNav();
nav.addItem(new SideNavItem("Dashboard", DashboardView.class, nav.addItem(new SideNavItem(getTranslation("nav.dashboard"), DashboardView.class,
VaadinIcon.DASHBOARD.create())); VaadinIcon.DASHBOARD.create()));
nav.addItem(new SideNavItem("Formular", FormView.class, nav.addItem(new SideNavItem(getTranslation("nav.form"), FormView.class,
VaadinIcon.FORM.create())); VaadinIcon.FORM.create()));
nav.addItem(new SideNavItem("Tabelle", TableView.class, nav.addItem(new SideNavItem(getTranslation("nav.table"), TableView.class,
VaadinIcon.TABLE.create())); VaadinIcon.TABLE.create()));
addToDrawer(nav); addToDrawer(nav);
} }
private MenuBar buildLanguageMenu() {
MenuBar menuBar = new MenuBar();
menuBar.addThemeVariants(MenuBarVariant.LUMO_TERTIARY_INLINE);
var root = menuBar.addItem(VaadinIcon.GLOBE_WIRE.create());
root.setAriaLabel(getTranslation("action.language"));
root.getSubMenu().addItem(getTranslation("lang.de"), e -> switchLanguage(Locale.GERMAN));
root.getSubMenu().addItem(getTranslation("lang.en"), e -> switchLanguage(Locale.ENGLISH));
root.getSubMenu().addItem(getTranslation("lang.es"), e -> switchLanguage(Locale.of("es")));
return menuBar;
}
private void switchLanguage(Locale locale) {
VaadinSession.getCurrent().setLocale(locale);
UI.getCurrent().getPage().reload();
}
private void applyIconFor(String theme) { private void applyIconFor(String theme) {
themeToggle.setIcon("dark".equals(theme) ? VaadinIcon.SUN_O.create() : VaadinIcon.MOON.create()); themeToggle.setIcon("dark".equals(theme) ? VaadinIcon.SUN_O.create() : VaadinIcon.MOON.create());
} }
+13 -9
View File
@@ -7,7 +7,7 @@ import com.vaadin.flow.component.icon.VaadinIcon;
import com.vaadin.flow.component.orderedlayout.VerticalLayout; import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.component.textfield.TextField; import com.vaadin.flow.component.textfield.TextField;
import com.vaadin.flow.data.value.ValueChangeMode; import com.vaadin.flow.data.value.ValueChangeMode;
import com.vaadin.flow.router.PageTitle; import com.vaadin.flow.router.HasDynamicTitle;
import com.vaadin.flow.router.Route; import com.vaadin.flow.router.Route;
import java.text.NumberFormat; import java.text.NumberFormat;
@@ -19,8 +19,7 @@ import java.util.Locale;
* live text filter across all columns. * live text filter across all columns.
*/ */
@Route("tabelle") @Route("tabelle")
@PageTitle("Tabelle") public class TableView extends VerticalLayout implements HasDynamicTitle {
public class TableView extends VerticalLayout {
private record Mitarbeiter(String name, String abteilung, String stadt, double gehalt) { private record Mitarbeiter(String name, String abteilung, String stadt, double gehalt) {
} }
@@ -45,7 +44,7 @@ public class TableView extends VerticalLayout {
addClassName("dialect-content"); addClassName("dialect-content");
TextField suche = new TextField(); TextField suche = new TextField();
suche.setPlaceholder("Suchen…"); suche.setPlaceholder(getTranslation("table.search"));
suche.setPrefixComponent(VaadinIcon.SEARCH.create()); suche.setPrefixComponent(VaadinIcon.SEARCH.create());
suche.setClearButtonVisible(true); suche.setClearButtonVisible(true);
suche.setValueChangeMode(ValueChangeMode.EAGER); suche.setValueChangeMode(ValueChangeMode.EAGER);
@@ -53,10 +52,10 @@ public class TableView extends VerticalLayout {
Grid<Mitarbeiter> grid = new Grid<>(); Grid<Mitarbeiter> grid = new Grid<>();
grid.setWidthFull(); grid.setWidthFull();
grid.addColumn(Mitarbeiter::name).setHeader("Name").setSortable(true); grid.addColumn(Mitarbeiter::name).setHeader(getTranslation("col.name")).setSortable(true);
grid.addColumn(Mitarbeiter::abteilung).setHeader("Abteilung").setSortable(true); grid.addColumn(Mitarbeiter::abteilung).setHeader(getTranslation("col.department")).setSortable(true);
grid.addColumn(Mitarbeiter::stadt).setHeader("Stadt").setSortable(true); grid.addColumn(Mitarbeiter::stadt).setHeader(getTranslation("col.city")).setSortable(true);
grid.addColumn(m -> GEHALT_FORMAT.format(m.gehalt())).setHeader("Gehalt (€)").setSortable(true); grid.addColumn(m -> GEHALT_FORMAT.format(m.gehalt())).setHeader(getTranslation("col.salary")).setSortable(true);
GridListDataView<Mitarbeiter> dataView = grid.setItems(MITARBEITER); GridListDataView<Mitarbeiter> dataView = grid.setItems(MITARBEITER);
suche.addValueChangeListener(e -> dataView.refreshAll()); suche.addValueChangeListener(e -> dataView.refreshAll());
@@ -66,7 +65,12 @@ public class TableView extends VerticalLayout {
content.setPadding(false); content.setPadding(false);
content.setSpacing(true); content.setSpacing(true);
add(new Card("Mitarbeiter", content)); add(new Card(getTranslation("card.employees"), content));
}
@Override
public String getPageTitle() {
return getTranslation("page.table");
} }
private static boolean matches(Mitarbeiter mitarbeiter, String filter) { private static boolean matches(Mitarbeiter mitarbeiter, String filter) {
@@ -0,0 +1,94 @@
# German (source of truth / DefaultI18NProvider fallback)
app.title=Chart App
action.themeToggle=Farbschema wechseln
action.language=Sprache wechseln
lang.de=Deutsch
lang.en=English
lang.es=Español
nav.dashboard=Dashboard
nav.form=Formular
nav.table=Tabelle
page.dashboard=Dashboard
page.form=Formular
page.table=Tabelle
card.revenueTrend=Umsatz-Entwicklung
card.revenueByMonth=Umsatz nach Monat
card.revenueByRegion=Umsatz nach Region
card.registration=Registrierung
card.employees=Mitarbeiter
chart.revenueSeries=Umsatz 2026
chart.pointClick=Serie {0}, Punkt {1}
chart.sliceClick=Slice {0}
month.jan=Jan
month.feb=Feb
month.mar=Mär
month.apr=Apr
month.may=Mai
month.jun=Jun
region.north=Nord
region.south=Süd
region.east=Ost
region.west=West
form.firstName=Vorname
form.lastName=Nachname
form.email=E-Mail
form.password=Passwort
form.age=Alter
form.salary=Gehalt (€)
form.birthdate=Geburtsdatum
form.time=Bevorzugte Uhrzeit
form.country=Land
form.department=Abteilung
form.interests=Interessen
form.salutation=Anrede
form.newsletter=Newsletter abonnieren
form.remark=Bemerkung
form.save=Speichern
form.reset=Zurücksetzen
form.today=Heute
form.cancel=Abbrechen
country.de=Deutschland
country.at=Österreich
country.ch=Schweiz
country.fr=Frankreich
country.nl=Niederlande
dept.sales=Vertrieb
dept.marketing=Marketing
dept.dev=Entwicklung
dept.support=Support
dept.management=Geschäftsführung
interest.newsletter=Newsletter-Themen
interest.webinars=Webinare
interest.updates=Produkt-Updates
interest.events=Events
salutation.mrs=Frau
salutation.mr=Herr
salutation.diverse=Divers
salutation.none=Keine Angabe
valid.firstNameRequired=Vorname wird benötigt
valid.lastNameRequired=Nachname wird benötigt
valid.emailRequired=E-Mail wird benötigt
valid.emailInvalid=Ungültige E-Mail-Adresse
valid.ageRange=Alter muss zwischen 0 und 120 liegen
notify.saved=Gespeichert: {0} {1}
notify.checkInput=Bitte Eingaben prüfen
table.search=Suchen…
col.name=Name
col.department=Abteilung
col.city=Stadt
col.salary=Gehalt (€)
@@ -0,0 +1,94 @@
# English
app.title=Chart App
action.themeToggle=Switch color scheme
action.language=Switch language
lang.de=Deutsch
lang.en=English
lang.es=Español
nav.dashboard=Dashboard
nav.form=Form
nav.table=Table
page.dashboard=Dashboard
page.form=Form
page.table=Table
card.revenueTrend=Revenue Trend
card.revenueByMonth=Revenue by Month
card.revenueByRegion=Revenue by Region
card.registration=Registration
card.employees=Employees
chart.revenueSeries=Revenue 2026
chart.pointClick=Series {0}, point {1}
chart.sliceClick=Slice {0}
month.jan=Jan
month.feb=Feb
month.mar=Mar
month.apr=Apr
month.may=May
month.jun=Jun
region.north=North
region.south=South
region.east=East
region.west=West
form.firstName=First name
form.lastName=Last name
form.email=Email
form.password=Password
form.age=Age
form.salary=Salary (€)
form.birthdate=Date of birth
form.time=Preferred time
form.country=Country
form.department=Department
form.interests=Interests
form.salutation=Salutation
form.newsletter=Subscribe to newsletter
form.remark=Remark
form.save=Save
form.reset=Reset
form.today=Today
form.cancel=Cancel
country.de=Germany
country.at=Austria
country.ch=Switzerland
country.fr=France
country.nl=Netherlands
dept.sales=Sales
dept.marketing=Marketing
dept.dev=Development
dept.support=Support
dept.management=Management
interest.newsletter=Newsletter topics
interest.webinars=Webinars
interest.updates=Product updates
interest.events=Events
salutation.mrs=Ms.
salutation.mr=Mr.
salutation.diverse=Diverse
salutation.none=Not specified
valid.firstNameRequired=First name is required
valid.lastNameRequired=Last name is required
valid.emailRequired=Email is required
valid.emailInvalid=Invalid email address
valid.ageRange=Age must be between 0 and 120
notify.saved=Saved: {0} {1}
notify.checkInput=Please check your entries
table.search=Search…
col.name=Name
col.department=Department
col.city=City
col.salary=Salary (€)
@@ -0,0 +1,94 @@
# Spanish
app.title=Chart App
action.themeToggle=Cambiar esquema de color
action.language=Cambiar idioma
lang.de=Deutsch
lang.en=English
lang.es=Español
nav.dashboard=Panel
nav.form=Formulario
nav.table=Tabla
page.dashboard=Panel
page.form=Formulario
page.table=Tabla
card.revenueTrend=Evolución de ingresos
card.revenueByMonth=Ingresos por mes
card.revenueByRegion=Ingresos por región
card.registration=Registro
card.employees=Empleados
chart.revenueSeries=Ingresos 2026
chart.pointClick=Serie {0}, punto {1}
chart.sliceClick=Sector {0}
month.jan=Ene
month.feb=Feb
month.mar=Mar
month.apr=Abr
month.may=May
month.jun=Jun
region.north=Norte
region.south=Sur
region.east=Este
region.west=Oeste
form.firstName=Nombre
form.lastName=Apellido
form.email=Correo electrónico
form.password=Contraseña
form.age=Edad
form.salary=Salario (€)
form.birthdate=Fecha de nacimiento
form.time=Hora preferida
form.country=País
form.department=Departamento
form.interests=Intereses
form.salutation=Tratamiento
form.newsletter=Suscribirse al boletín
form.remark=Observación
form.save=Guardar
form.reset=Restablecer
form.today=Hoy
form.cancel=Cancelar
country.de=Alemania
country.at=Austria
country.ch=Suiza
country.fr=Francia
country.nl=Países Bajos
dept.sales=Ventas
dept.marketing=Marketing
dept.dev=Desarrollo
dept.support=Soporte
dept.management=Dirección
interest.newsletter=Temas del boletín
interest.webinars=Seminarios web
interest.updates=Actualizaciones de producto
interest.events=Eventos
salutation.mrs=Sra.
salutation.mr=Sr.
salutation.diverse=Diverso
salutation.none=Sin especificar
valid.firstNameRequired=El nombre es obligatorio
valid.lastNameRequired=El apellido es obligatorio
valid.emailRequired=El correo electrónico es obligatorio
valid.emailInvalid=Dirección de correo no válida
valid.ageRange=La edad debe estar entre 0 y 120
notify.saved=Guardado: {0} {1}
notify.checkInput=Por favor revise los datos ingresados
table.search=Buscar…
col.name=Nombre
col.department=Departamento
col.city=Ciudad
col.salary=Salario (€)