From 9114ea980089f5c0836f5b26e483683d9e5e980a Mon Sep 17 00:00:00 2001 From: Pit Friedrich Date: Sun, 19 Jul 2026 21:15:23 +0200 Subject: [PATCH] [FEATURE] i18n --- .gitea/workflows/ci.yml | 1 - AGENTS.md | 39 ++++++++ .../java/com/example/views/DashboardView.java | 42 +++++---- src/main/java/com/example/views/FormView.java | 74 ++++++++------- .../java/com/example/views/MainLayout.java | 39 ++++++-- .../java/com/example/views/TableView.java | 22 +++-- .../vaadin-i18n/translations.properties | 93 +++++++++++++++++++ .../vaadin-i18n/translations_en.properties | 93 +++++++++++++++++++ .../vaadin-i18n/translations_es.properties | 93 +++++++++++++++++++ 9 files changed, 431 insertions(+), 65 deletions(-) create mode 100644 AGENTS.md diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index 5db0678..c8546e7 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -16,7 +16,6 @@ jobs: with: distribution: temurin java-version: '25' - cache: maven - name: Build and Test run: | diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..6ba2eda --- /dev/null +++ b/AGENTS.md @@ -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. diff --git a/src/main/java/com/example/views/DashboardView.java b/src/main/java/com/example/views/DashboardView.java index 3bed727..5be787b 100644 --- a/src/main/java/com/example/views/DashboardView.java +++ b/src/main/java/com/example/views/DashboardView.java @@ -6,38 +6,42 @@ import com.example.components.LineChart; import com.example.components.PieChart; import com.vaadin.flow.component.notification.Notification; 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 java.util.List; @Route("") -@PageTitle("Dashboard") -public class DashboardView extends VerticalLayout { +public class DashboardView extends VerticalLayout implements HasDynamicTitle { public DashboardView() { addClassName("dialect-content"); + List 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.setWidthFull(); 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("Jan", "Feb", "Mär", "Apr", "Mai", "Jun")); + months); lineChart.addPointClickListener(e -> { - Notification.show("Serie %d, Punkt %d" - .formatted(e.getSeriesIndex(), e.getDataPointIndex())); + Notification.show(getTranslation("chart.pointClick", + e.getSeriesIndex(), e.getDataPointIndex())); }); BarChart barChart = new BarChart(); barChart.setWidthFull(); 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("Jan", "Feb", "Mär", "Apr", "Mai", "Jun")); + months); barChart.addPointClickListener(e -> { - Notification.show("Serie %d, Punkt %d" - .formatted(e.getSeriesIndex(), e.getDataPointIndex())); + Notification.show(getTranslation("chart.pointClick", + e.getSeriesIndex(), e.getDataPointIndex())); }); PieChart pieChart = new PieChart(); @@ -45,13 +49,19 @@ public class DashboardView extends VerticalLayout { pieChart.setHeight("400px"); pieChart.setData( 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 -> { - Notification.show("Slice %d".formatted(e.getDataPointIndex())); + Notification.show(getTranslation("chart.sliceClick", e.getDataPointIndex())); }); - add(new Card("Umsatz-Entwicklung", lineChart), - new Card("Umsatz nach Monat", barChart), - new Card("Umsatz nach Region", pieChart)); + add(new Card(getTranslation("card.revenueTrend"), lineChart), + new Card(getTranslation("card.revenueByMonth"), barChart), + new Card(getTranslation("card.revenueByRegion"), pieChart)); + } + + @Override + public String getPageTitle() { + return getTranslation("page.dashboard"); } } diff --git a/src/main/java/com/example/views/FormView.java b/src/main/java/com/example/views/FormView.java index 8e1462c..d1c8bbc 100644 --- a/src/main/java/com/example/views/FormView.java +++ b/src/main/java/com/example/views/FormView.java @@ -20,7 +20,7 @@ import com.vaadin.flow.component.timepicker.TimePicker; import com.vaadin.flow.data.binder.Binder; import com.vaadin.flow.data.validator.EmailValidator; 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 java.text.DateFormatSymbols; @@ -33,30 +33,34 @@ import java.util.Locale; * to {@link Registration} via {@link Binder}. */ @Route("formular") -@PageTitle("Formular") -public class FormView extends VerticalLayout { +public class FormView extends VerticalLayout implements HasDynamicTitle { private final Binder binder = new Binder<>(Registration.class); public FormView() { addClassName("dialect-content"); - add(new Card("Registrierung", buildForm())); + add(new Card(getTranslation("card.registration"), buildForm())); // addClassName("form-view-vertical-layout-1"); } + @Override + public String getPageTitle() { + return getTranslation("page.form"); + } + private Component buildForm() { - TextField vorname = new TextField("Vorname"); + TextField vorname = new TextField(getTranslation("form.firstName")); vorname.setAutofocus(true); - TextField nachname = new TextField("Nachname"); - EmailField email = new EmailField("E-Mail"); - PasswordField passwort = new PasswordField("Passwort"); + TextField nachname = new TextField(getTranslation("form.lastName")); + EmailField email = new EmailField(getTranslation("form.email")); + PasswordField passwort = new PasswordField(getTranslation("form.password")); // passwort.addClassName("form-view-password-field-1"); - IntegerField alter = new IntegerField("Alter"); + IntegerField alter = new IntegerField(getTranslation("form.age")); alter.setReadOnly(true); - BigDecimalField gehalt = new BigDecimalField("Gehalt (€)"); - DatePicker geburtsdatum = new DatePicker("Geburtsdatum"); + BigDecimalField gehalt = new BigDecimalField(getTranslation("form.salary")); + DatePicker geburtsdatum = new DatePicker(getTranslation("form.birthdate")); geburtsdatum.setLocale(Locale.GERMANY); DateFormatSymbols symbols = new DateFormatSymbols(Locale.GERMANY); @@ -69,44 +73,48 @@ vorname.setAutofocus(true); .setWeekdays(wochentage) .setWeekdaysShort(wochentageKurz) .setFirstDayOfWeek(1) - .setToday("Heute") - .setCancel("Abbrechen")); - TimePicker uhrzeit = new TimePicker("Bevorzugte Uhrzeit"); + .setToday(getTranslation("form.today")) + .setCancel(getTranslation("form.cancel"))); + TimePicker uhrzeit = new TimePicker(getTranslation("form.time")); - ComboBox land = new ComboBox<>("Land"); - land.setItems("Deutschland", "Österreich", "Schweiz", "Frankreich", "Niederlande"); + ComboBox land = new ComboBox<>(getTranslation("form.country")); + land.setItems(getTranslation("country.de"), getTranslation("country.at"), + getTranslation("country.ch"), getTranslation("country.fr"), getTranslation("country.nl")); Select abteilung = new Select<>(); - abteilung.setLabel("Abteilung"); - abteilung.setItems("Vertrieb", "Marketing", "Entwicklung", "Support", "Geschäftsführung"); + abteilung.setLabel(getTranslation("form.department")); + abteilung.setItems(getTranslation("dept.sales"), getTranslation("dept.marketing"), + getTranslation("dept.dev"), getTranslation("dept.support"), getTranslation("dept.management")); CheckboxGroup interessen = new CheckboxGroup<>(); - interessen.setLabel("Interessen"); - interessen.setItems("Newsletter-Themen", "Webinare", "Produkt-Updates", "Events"); + interessen.setLabel(getTranslation("form.interests")); + interessen.setItems(getTranslation("interest.newsletter"), getTranslation("interest.webinars"), + getTranslation("interest.updates"), getTranslation("interest.events")); RadioButtonGroup geschlecht = new RadioButtonGroup<>(); - geschlecht.setLabel("Anrede"); - geschlecht.setItems("Frau", "Herr", "Divers", "Keine Angabe"); + geschlecht.setLabel(getTranslation("form.salutation")); + 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); binder.forField(vorname) - .asRequired("Vorname wird benötigt") + .asRequired(getTranslation("valid.firstNameRequired")) .bind(Registration::getVorname, Registration::setVorname); binder.forField(nachname) - .asRequired("Nachname wird benötigt") + .asRequired(getTranslation("valid.lastNameRequired")) .bind(Registration::getNachname, Registration::setNachname); binder.forField(email) - .asRequired("E-Mail wird benötigt") - .withValidator(new EmailValidator("Ungültige E-Mail-Adresse")) + .asRequired(getTranslation("valid.emailRequired")) + .withValidator(new EmailValidator(getTranslation("valid.emailInvalid"))) .bind(Registration::getEmail, Registration::setEmail); binder.forField(passwort) .bind(Registration::getPasswort, Registration::setPasswort); 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); binder.forField(gehalt) .bind(Registration::getGehalt, Registration::setGehalt); @@ -143,20 +151,20 @@ vorname.setAutofocus(true); formLayout.setColspan(bemerkung, 2); formLayout.add(interessen, geschlecht, newsletter, bemerkung); - Button speichern = new Button("Speichern", e -> { + Button speichern = new Button(getTranslation("form.save"), e -> { Registration ziel = new Registration(); if (binder.writeBeanIfValid(ziel)) { Notification erfolg = Notification.show( - "Gespeichert: %s %s".formatted(ziel.getVorname(), ziel.getNachname())); + getTranslation("notify.saved", ziel.getVorname(), ziel.getNachname())); erfolg.addThemeVariants(NotificationVariant.LUMO_SUCCESS); } else { - Notification fehler = Notification.show("Bitte Eingaben prüfen"); + Notification fehler = Notification.show(getTranslation("notify.checkInput")); fehler.addThemeVariants(NotificationVariant.LUMO_ERROR); } }); speichern.addThemeVariants(ButtonVariant.LUMO_PRIMARY); - Button zuruecksetzen = new Button("Zurücksetzen", + Button zuruecksetzen = new Button(getTranslation("form.reset"), e -> binder.readBean(new Registration())); HorizontalLayout buttons = new HorizontalLayout(speichern, zuruecksetzen); diff --git a/src/main/java/com/example/views/MainLayout.java b/src/main/java/com/example/views/MainLayout.java index 10fb3b2..45c4157 100644 --- a/src/main/java/com/example/views/MainLayout.java +++ b/src/main/java/com/example/views/MainLayout.java @@ -1,15 +1,21 @@ package com.example.views; import com.example.js.CommonJS; +import com.vaadin.flow.component.UI; import com.vaadin.flow.component.applayout.AppLayout; import com.vaadin.flow.component.applayout.DrawerToggle; import com.vaadin.flow.component.button.Button; import com.vaadin.flow.component.button.ButtonVariant; import com.vaadin.flow.component.html.Span; 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.SideNavItem; import com.vaadin.flow.router.Layout; +import com.vaadin.flow.server.VaadinSession; + +import java.util.Locale; @Layout public class MainLayout extends AppLayout { @@ -19,32 +25,53 @@ public class MainLayout extends AppLayout { public MainLayout() { DrawerToggle toggle = new DrawerToggle(); - Span title = new Span("Chart App"); + Span title = new Span(getTranslation("app.title")); title.addClassName("dialect-title"); Span spacer = new Span(); spacer.getStyle().set("flex-grow", "1"); + MenuBar languageMenu = buildLanguageMenu(); + themeToggle.addThemeVariants(ButtonVariant.TERTIARY, ButtonVariant.LUMO_ICON); - themeToggle.setAriaLabel("Farbschema wechseln"); + themeToggle.setAriaLabel(getTranslation("action.themeToggle")); themeToggle.addClickListener(e -> 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); SideNav nav = new SideNav(); - nav.addItem(new SideNavItem("Dashboard", DashboardView.class, + nav.addItem(new SideNavItem(getTranslation("nav.dashboard"), DashboardView.class, VaadinIcon.DASHBOARD.create())); - nav.addItem(new SideNavItem("Formular", FormView.class, + nav.addItem(new SideNavItem(getTranslation("nav.form"), FormView.class, VaadinIcon.FORM.create())); - nav.addItem(new SideNavItem("Tabelle", TableView.class, + nav.addItem(new SideNavItem(getTranslation("nav.table"), TableView.class, VaadinIcon.TABLE.create())); 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) { themeToggle.setIcon("dark".equals(theme) ? VaadinIcon.SUN_O.create() : VaadinIcon.MOON.create()); } diff --git a/src/main/java/com/example/views/TableView.java b/src/main/java/com/example/views/TableView.java index 51399d0..ba76e2f 100644 --- a/src/main/java/com/example/views/TableView.java +++ b/src/main/java/com/example/views/TableView.java @@ -7,7 +7,7 @@ import com.vaadin.flow.component.icon.VaadinIcon; import com.vaadin.flow.component.orderedlayout.VerticalLayout; import com.vaadin.flow.component.textfield.TextField; 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 java.text.NumberFormat; @@ -19,8 +19,7 @@ import java.util.Locale; * live text filter across all columns. */ @Route("tabelle") -@PageTitle("Tabelle") -public class TableView extends VerticalLayout { +public class TableView extends VerticalLayout implements HasDynamicTitle { private record Mitarbeiter(String name, String abteilung, String stadt, double gehalt) { } @@ -45,7 +44,7 @@ public class TableView extends VerticalLayout { addClassName("dialect-content"); TextField suche = new TextField(); - suche.setPlaceholder("Suchen…"); + suche.setPlaceholder(getTranslation("table.search")); suche.setPrefixComponent(VaadinIcon.SEARCH.create()); suche.setClearButtonVisible(true); suche.setValueChangeMode(ValueChangeMode.EAGER); @@ -53,10 +52,10 @@ public class TableView extends VerticalLayout { Grid grid = new Grid<>(); grid.setWidthFull(); - grid.addColumn(Mitarbeiter::name).setHeader("Name").setSortable(true); - grid.addColumn(Mitarbeiter::abteilung).setHeader("Abteilung").setSortable(true); - grid.addColumn(Mitarbeiter::stadt).setHeader("Stadt").setSortable(true); - grid.addColumn(m -> GEHALT_FORMAT.format(m.gehalt())).setHeader("Gehalt (€)").setSortable(true); + grid.addColumn(Mitarbeiter::name).setHeader(getTranslation("col.name")).setSortable(true); + grid.addColumn(Mitarbeiter::abteilung).setHeader(getTranslation("col.department")).setSortable(true); + grid.addColumn(Mitarbeiter::stadt).setHeader(getTranslation("col.city")).setSortable(true); + grid.addColumn(m -> GEHALT_FORMAT.format(m.gehalt())).setHeader(getTranslation("col.salary")).setSortable(true); GridListDataView dataView = grid.setItems(MITARBEITER); suche.addValueChangeListener(e -> dataView.refreshAll()); @@ -66,7 +65,12 @@ public class TableView extends VerticalLayout { content.setPadding(false); 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) { diff --git a/src/main/resources/vaadin-i18n/translations.properties b/src/main/resources/vaadin-i18n/translations.properties index 5039b65..cdc8b1d 100644 --- a/src/main/resources/vaadin-i18n/translations.properties +++ b/src/main/resources/vaadin-i18n/translations.properties @@ -1 +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 (€) diff --git a/src/main/resources/vaadin-i18n/translations_en.properties b/src/main/resources/vaadin-i18n/translations_en.properties index 33806a3..13f9939 100644 --- a/src/main/resources/vaadin-i18n/translations_en.properties +++ b/src/main/resources/vaadin-i18n/translations_en.properties @@ -1 +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 (€) diff --git a/src/main/resources/vaadin-i18n/translations_es.properties b/src/main/resources/vaadin-i18n/translations_es.properties index 2d46bb2..5417c0b 100644 --- a/src/main/resources/vaadin-i18n/translations_es.properties +++ b/src/main/resources/vaadin-i18n/translations_es.properties @@ -1 +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 (€)