Переглянути джерело

:globe_with_meridians: Internationalize MaterialFX

:rewind: Partially revert commit e5d8e31058564b44a367d182077bab3f55a1c490 as it breaks the Demo

Skins Package
:bug: MFXComboBoxSkin: ensure the caret position is at 0 if the combo box is not selectable
:recycle: MFXFilterPaneSkin: properly compute the minimum width
:recycle: MFXTableViewSkin: allow to drag the filter dialog
:bug: MFXTableViewSkin: ensure the dialog is on foreground

Signed-off-by: palexdev <alessandro.parisi406@gmail.com>
palexdev 3 роки тому
батько
коміт
ed1545c6d0
27 змінених файлів з 605 додано та 94 видалено
  1. 15 3
      CHANGELOG.md
  2. 0 10
      build.gradle
  3. 7 6
      materialfx/src/main/java/io/github/palexdev/materialfx/controls/MFXComboBox.java
  4. 2 1
      materialfx/src/main/java/io/github/palexdev/materialfx/controls/MFXFilterPane.java
  5. 16 15
      materialfx/src/main/java/io/github/palexdev/materialfx/controls/MFXNotificationCenter.java
  6. 8 7
      materialfx/src/main/java/io/github/palexdev/materialfx/controls/MFXTextField.java
  7. 4 2
      materialfx/src/main/java/io/github/palexdev/materialfx/enums/ChainMode.java
  8. 3 2
      materialfx/src/main/java/io/github/palexdev/materialfx/filter/BooleanFilter.java
  9. 7 6
      materialfx/src/main/java/io/github/palexdev/materialfx/filter/DoubleFilter.java
  10. 3 2
      materialfx/src/main/java/io/github/palexdev/materialfx/filter/EnumFilter.java
  11. 7 6
      materialfx/src/main/java/io/github/palexdev/materialfx/filter/FloatFilter.java
  12. 7 6
      materialfx/src/main/java/io/github/palexdev/materialfx/filter/IntegerFilter.java
  13. 7 6
      materialfx/src/main/java/io/github/palexdev/materialfx/filter/LongFilter.java
  14. 12 11
      materialfx/src/main/java/io/github/palexdev/materialfx/filter/StringFilter.java
  15. 205 0
      materialfx/src/main/java/io/github/palexdev/materialfx/i18n/I18N.java
  16. 50 0
      materialfx/src/main/java/io/github/palexdev/materialfx/i18n/Language.java
  17. 7 0
      materialfx/src/main/java/io/github/palexdev/materialfx/skins/MFXComboBoxSkin.java
  18. 2 1
      materialfx/src/main/java/io/github/palexdev/materialfx/skins/MFXFilterComboBoxSkin.java
  19. 9 3
      materialfx/src/main/java/io/github/palexdev/materialfx/skins/MFXFilterPaneSkin.java
  20. 2 1
      materialfx/src/main/java/io/github/palexdev/materialfx/skins/MFXNotificationCenterSkin.java
  21. 3 2
      materialfx/src/main/java/io/github/palexdev/materialfx/skins/MFXStepperSkin.java
  22. 2 1
      materialfx/src/main/java/io/github/palexdev/materialfx/skins/MFXStepperToggleSkin.java
  23. 2 0
      materialfx/src/main/java/io/github/palexdev/materialfx/skins/MFXTableViewSkin.java
  24. 5 3
      materialfx/src/main/java/io/github/palexdev/materialfx/utils/StringUtils.java
  25. 8 0
      materialfx/src/main/java/module-info.java
  26. 106 0
      materialfx/src/main/resources/io/github/palexdev/materialfx/i18n/mfxlang_en.properties
  27. 106 0
      materialfx/src/main/resources/io/github/palexdev/materialfx/i18n/mfxlang_it.properties

+ 15 - 3
CHANGELOG.md

@@ -14,24 +14,36 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 
 ## [Unreleased]
 ### Added
+
 - New control MFXMagnifierPane
 - ColorUtils: added some new methods to convert Colors to Strings
 - FunctionalStringConverter: added two new convenience methods
 - New utils class SwingFXUtils (copied from javafx.embed.swing)
 - Added fluent API builders for MaterialFX components and JavaFX Panes, as requested by #78
+- Added resource bundles and API for internationalization
 
 ### Changed
+
 - ColorUtils: changed some method to be null-safe
+- MFXFilterPaneSkin: properly compute the minimum width
+- MFXTableViewSkin: allow to drag the filter dialog
+
+### Fixed
+
+- MFXComboBoxSkin: ensure the caret position is at 0 if the combo box is not selectable
+- MFXTableViewSkin: ensure the dialog is on foreground
 
 ## [11.13.0] - 22-01-2022
-_This version won't follow the above scheme as the amount of changes and commits is simply too huge and there would be no
-way to correctly show all the changes without making mistakes (duplicates, "overlapping" changes...), for this reason
+
+_This version won't follow the above scheme as the amount of changes and commits is simply too huge and there would be
+no way to correctly show all the changes without making mistakes (duplicates, "overlapping" changes...), for this reason
 I'll try to sum up only the major changes below._
 
 - The demo has been completely remade
 - Added new beans and properties
 - Added new mechanisms for bindings
-- Added new collections, in particular an ObservableList that combines the capabilities of JavaFX's FilteredList and SortedList.
+- Added new collections, in particular an ObservableList that combines the capabilities of JavaFX's FilteredList and
+  SortedList.
 Also those two are read-only, but MaterialFX also offers a version that allows to directly make changes to the source list
 - ReactFX, Flowless removed in favor of my own Virtual Flow implementation, VirtualizedFX. As a result all controls having lists have been reworked.
 - The table view has been reworked as well to use a Virtual Flow (scrollable) so it's efficiency is now on a whole new level. There's also a paginated

+ 0 - 10
build.gradle

@@ -1,13 +1,3 @@
-buildscript {
-    repositories {
-        gradlePluginPortal()
-    }
-
-    dependencies {
-        classpath 'org.javamodularity:moduleplugin:1.8.10' // Workaround for broken javafxplugin
-    }
-}
-
 plugins {
     id 'java-library'
     id 'org.openjfx.javafxplugin' version '0.0.11' apply false

+ 7 - 6
materialfx/src/main/java/io/github/palexdev/materialfx/controls/MFXComboBox.java

@@ -29,6 +29,7 @@ import io.github.palexdev.materialfx.beans.properties.styleable.StyleableBoolean
 import io.github.palexdev.materialfx.controls.base.MFXCombo;
 import io.github.palexdev.materialfx.controls.cell.MFXComboBoxCell;
 import io.github.palexdev.materialfx.font.MFXFontIcon;
+import io.github.palexdev.materialfx.i18n.I18N;
 import io.github.palexdev.materialfx.selection.ComboBoxSelectionModel;
 import io.github.palexdev.materialfx.skins.MFXComboBoxSkin;
 import io.github.palexdev.materialfx.utils.ListChangeProcessor;
@@ -154,7 +155,7 @@ public class MFXComboBox<T> extends MFXTextField implements MFXCombo<T> {
 		});
 
 		// Default converter
-		setConverter(FunctionalStringConverter.to(Object::toString));
+		setConverter(FunctionalStringConverter.to(t -> t != null ? t.toString() : ""));
 
 		showing.addListener(invalidated -> pseudoClassStateChanged(POPUP_OPEN_PSEUDO_CLASS, showing.get()));
 
@@ -169,31 +170,31 @@ public class MFXComboBox<T> extends MFXTextField implements MFXCombo<T> {
 	public void defaultContextMenu() {
 		MFXContextMenuItem selectFirst = MFXContextMenuItem.Builder.build()
 				.setIcon(new MFXFontIcon("mfx-first-page", 16))
-				.setText("Select First")
+				.setText(I18N.getOrDefault("comboBox.contextMenu.selectFirst"))
 				.setOnAction(event -> selectFirst())
 				.get();
 
 		MFXContextMenuItem selectNext = MFXContextMenuItem.Builder.build()
 				.setIcon(new MFXFontIcon("mfx-next", 18))
-				.setText("Select Next")
+				.setText(I18N.getOrDefault("comboBox.contextMenu.selectNext"))
 				.setOnAction(event -> selectNext())
 				.get();
 
 		MFXContextMenuItem selectPrevious = MFXContextMenuItem.Builder.build()
 				.setIcon(new MFXFontIcon("mfx-back", 18))
-				.setText("Select Previous")
+				.setText(I18N.getOrDefault("comboBox.contextMenu.selectPrevious"))
 				.setOnAction(event -> selectPrevious())
 				.get();
 
 		MFXContextMenuItem selectLast = MFXContextMenuItem.Builder.build()
 				.setIcon(new MFXFontIcon("mfx-last-page", 16))
-				.setText("Select Last")
+				.setText(I18N.getOrDefault("comboBox.contextMenu.selectLast"))
 				.setOnAction(event -> selectLast())
 				.get();
 
 		MFXContextMenuItem resetSelection = MFXContextMenuItem.Builder.build()
 				.setIcon(new MFXFontIcon("mfx-x", 16))
-				.setText("Clear Selection")
+				.setText(I18N.getOrDefault("comboBox.contextMenu.clearSelection"))
 				.setOnAction(event -> clearSelection())
 				.get();
 

+ 2 - 1
materialfx/src/main/java/io/github/palexdev/materialfx/controls/MFXFilterPane.java

@@ -22,6 +22,7 @@ import io.github.palexdev.materialfx.MFXResourcesLoader;
 import io.github.palexdev.materialfx.beans.FilterBean;
 import io.github.palexdev.materialfx.enums.ChainMode;
 import io.github.palexdev.materialfx.filter.base.AbstractFilter;
+import io.github.palexdev.materialfx.i18n.I18N;
 import io.github.palexdev.materialfx.skins.MFXFilterPaneSkin;
 import io.github.palexdev.materialfx.utils.PredicateUtils;
 import javafx.beans.property.SimpleStringProperty;
@@ -140,7 +141,7 @@ public class MFXFilterPane<T> extends Control {
 	//================================================================================
 	private final String STYLE_CLASS = "mfx-filter-pane";
 	private final String STYLESHEET = MFXResourcesLoader.load("css/MFXFilterPane.css");
-	private final StringProperty headerText = new SimpleStringProperty("Filters");
+	private final StringProperty headerText = new SimpleStringProperty(I18N.getOrDefault("filterPane.headerText"));
 	private final ObservableList<AbstractFilter<T, ?>> filters = FXCollections.observableArrayList();
 	private final ObservableList<FilterBean<T, ?>> activeFilters = FXCollections.observableArrayList();
 

+ 16 - 15
materialfx/src/main/java/io/github/palexdev/materialfx/controls/MFXNotificationCenter.java

@@ -24,6 +24,7 @@ import io.github.palexdev.materialfx.controls.base.MFXMenuControl;
 import io.github.palexdev.materialfx.controls.cell.MFXNotificationCell;
 import io.github.palexdev.materialfx.enums.NotificationCounterStyle;
 import io.github.palexdev.materialfx.enums.NotificationState;
+import io.github.palexdev.materialfx.i18n.I18N;
 import io.github.palexdev.materialfx.notifications.base.INotification;
 import io.github.palexdev.materialfx.selection.MultipleSelectionModel;
 import io.github.palexdev.materialfx.skins.MFXNotificationCenterSkin;
@@ -106,7 +107,7 @@ public class MFXNotificationCenter extends Control implements MFXMenuControl {
 	private final LongBinding unreadCountBinding;
 
 	private final ObjectProperty<NotificationCounterStyle> counterStyle = new SimpleObjectProperty<>(NUMBER);
-	private final StringProperty headerTextProperty = new SimpleStringProperty("Notifications");
+	private final StringProperty headerTextProperty = new SimpleStringProperty(I18N.getOrDefault("notificationCenter.header"));
 	private final BooleanProperty doNotDisturb = new SimpleBooleanProperty(false);
 	private final BooleanProperty showing = new SimpleBooleanProperty(false);
 
@@ -213,7 +214,7 @@ public class MFXNotificationCenter extends Control implements MFXMenuControl {
 	 */
 	protected void defaultContextMenu() {
 		MFXContextMenuItem selectAll = MFXContextMenuItem.Builder.build()
-				.setText("Select All")
+				.setText(I18N.getOrDefault("notificationCenter.contextMenu.selectAll"))
 				.setOnAction(event -> {
 					if (notifications.isEmpty()) return;
 					setSelectionMode(true);
@@ -222,7 +223,7 @@ public class MFXNotificationCenter extends Control implements MFXMenuControl {
 				}).get();
 
 		MFXContextMenuItem selectRead = MFXContextMenuItem.Builder.build()
-				.setText("Select Read")
+				.setText(I18N.getOrDefault("notificationCenter.contextMenu.selectRead"))
 				.setOnAction(event -> {
 					if (notifications.isEmpty()) return;
 
@@ -235,7 +236,7 @@ public class MFXNotificationCenter extends Control implements MFXMenuControl {
 				}).get();
 
 		MFXContextMenuItem selectUnread = MFXContextMenuItem.Builder.build()
-				.setText("Select Unread")
+				.setText(I18N.getOrDefault("notificationCenter.contextMenu.selectUnread"))
 				.setOnAction(event -> {
 					if (notifications.isEmpty()) return;
 					setSelectionMode(true);
@@ -247,22 +248,22 @@ public class MFXNotificationCenter extends Control implements MFXMenuControl {
 				}).get();
 
 		MFXContextMenuItem clearSelection = MFXContextMenuItem.Builder.build()
-				.setText("Clear Selection")
+				.setText(I18N.getOrDefault("notificationCenter.contextMenu.clearSelection"))
 				.setOnAction(event -> selectionModel.clearSelection())
 				.get();
 
 		MFXContextMenuItem sortByState = MFXContextMenuItem.Builder.build()
-				.setText("Sort By State")
+				.setText(I18N.getOrDefault("notificationCenter.contextMenu.sortState"))
 				.setOnAction(event -> notifications.setComparator(Comparator.comparing(INotification::getState)))
 				.get();
 
 		MFXContextMenuItem sortByTime = MFXContextMenuItem.Builder.build()
-				.setText("Sort By Time")
+				.setText(I18N.getOrDefault("notificationCenter.contextMenu.sortTime"))
 				.setOnAction(event -> notifications.setComparator(Comparator.comparing(INotification::getTime)))
 				.get();
 
 		MFXContextMenuItem reverseSort = MFXContextMenuItem.Builder.build()
-				.setText("Reverse Sort")
+				.setText(I18N.getOrDefault("notificationCenter.contextMenu.reverseSort"))
 				.setOnAction(event -> {
 					if (notifications.getComparator() == null) return;
 					Comparator<INotification> comparator = notifications.getComparator();
@@ -270,32 +271,32 @@ public class MFXNotificationCenter extends Control implements MFXMenuControl {
 				}).get();
 
 		MFXContextMenuItem filterRead = MFXContextMenuItem.Builder.build()
-				.setText("Filter By Read")
+				.setText(I18N.getOrDefault("notificationCenter.contextMenu.filterRead"))
 				.setOnAction(event -> notifications.setPredicate(notification -> notification.getState() == NotificationState.READ))
 				.get();
 
 		MFXContextMenuItem filterUnread = MFXContextMenuItem.Builder.build()
-				.setText("Filter By Unread")
+				.setText(I18N.getOrDefault("notificationCenter.contextMenu.filterUnread"))
 				.setOnAction(event -> notifications.setPredicate(notification -> notification.getState() == NotificationState.UNREAD))
 				.get();
 
 		MFXContextMenuItem clearFilter = MFXContextMenuItem.Builder.build()
-				.setText("Clear Filter")
+				.setText(I18N.getOrDefault("notificationCenter.contextMenu.clearFilter"))
 				.setOnAction(event -> notifications.setPredicate(null))
 				.get();
 
 		MFXContextMenuItem clearSort = MFXContextMenuItem.Builder.build()
-				.setText("Clear Sort")
+				.setText(I18N.getOrDefault("notificationCenter.contextMenu.clearSort"))
 				.setOnAction(event -> notifications.setComparator(null))
 				.get();
 
 
 		contextMenu = MFXContextMenu.Builder.build(virtualFlow)
-				.addSeparator(new Label("Selection"))
+				.addSeparator(new Label(I18N.getOrDefault("notificationCenter.contextMenu.selectionSeparator")))
 				.addItems(selectAll, selectRead, selectUnread, clearSelection)
-				.addSeparator(new Label("Sorting"))
+				.addSeparator(new Label(I18N.getOrDefault("notificationCenter.contextMenu.sortingSeparator")))
 				.addItems(sortByState, sortByTime, reverseSort, clearSort)
-				.addSeparator(new Label("Filtering"))
+				.addSeparator(new Label(I18N.getOrDefault("notificationCenter.contextMenu.filterSeparator")))
 				.addItems(filterRead, filterUnread, clearFilter)
 				.setPopupStyleableParent(this)
 				.installAndGet();

+ 8 - 7
materialfx/src/main/java/io/github/palexdev/materialfx/controls/MFXTextField.java

@@ -26,6 +26,7 @@ import io.github.palexdev.materialfx.beans.properties.styleable.StyleableObjectP
 import io.github.palexdev.materialfx.controls.base.MFXMenuControl;
 import io.github.palexdev.materialfx.enums.FloatMode;
 import io.github.palexdev.materialfx.font.MFXFontIcon;
+import io.github.palexdev.materialfx.i18n.I18N;
 import io.github.palexdev.materialfx.skins.MFXTextFieldSkin;
 import io.github.palexdev.materialfx.utils.StyleablePropertiesUtils;
 import io.github.palexdev.materialfx.validation.MFXValidator;
@@ -209,49 +210,49 @@ public class MFXTextField extends TextField implements Validated, MFXMenuControl
 	public void defaultContextMenu() {
 		MFXContextMenuItem copyItem = MFXContextMenuItem.Builder.build()
 				.setIcon(new MFXFontIcon("mfx-content-copy", 14))
-				.setText("Copy")
+				.setText(I18N.getOrDefault("textField.contextMenu.copy"))
 				.setAccelerator("Ctrl + C")
 				.setOnAction(event -> copy())
 				.get();
 
 		MFXContextMenuItem cutItem = MFXContextMenuItem.Builder.build()
 				.setIcon(new MFXFontIcon("mfx-content-cut", 14))
-				.setText("Cut")
+				.setText(I18N.getOrDefault("textField.contextMenu.cut"))
 				.setAccelerator("Ctrl + X")
 				.setOnAction(event -> cut())
 				.get();
 
 		MFXContextMenuItem pasteItem = MFXContextMenuItem.Builder.build()
 				.setIcon(new MFXFontIcon("mfx-content-paste", 14))
-				.setText("Paste")
+				.setText(I18N.getOrDefault("textField.contextMenu.paste"))
 				.setAccelerator("Ctrl + V")
 				.setOnAction(event -> paste())
 				.get();
 
 		MFXContextMenuItem deleteItem = MFXContextMenuItem.Builder.build()
 				.setIcon(new MFXFontIcon("mfx-delete-alt", 16))
-				.setText("Delete")
+				.setText(I18N.getOrDefault("textField.contextMenu.delete"))
 				.setAccelerator("Ctrl + D")
 				.setOnAction(event -> deleteText(getSelection()))
 				.get();
 
 		MFXContextMenuItem selectAllItem = MFXContextMenuItem.Builder.build()
 				.setIcon(new MFXFontIcon("mfx-select-all", 16))
-				.setText("Select All")
+				.setText(I18N.getOrDefault("textField.contextMenu.selectAll"))
 				.setAccelerator("Ctrl + A")
 				.setOnAction(event -> selectAll())
 				.get();
 
 		MFXContextMenuItem redoItem = MFXContextMenuItem.Builder.build()
 				.setIcon(new MFXFontIcon("mfx-redo", 12))
-				.setText("Redo")
+				.setText(I18N.getOrDefault("textField.contextMenu.redo"))
 				.setAccelerator("Ctrl + Y")
 				.setOnAction(event -> redo())
 				.get();
 
 		MFXContextMenuItem undoItem = MFXContextMenuItem.Builder.build()
 				.setIcon(new MFXFontIcon("mfx-undo", 12))
-				.setText("Undo")
+				.setText(I18N.getOrDefault("textField.contextMenu.undo"))
 				.setAccelerator("Ctrl + Z")
 				.setOnAction(event -> undo())
 				.get();

+ 4 - 2
materialfx/src/main/java/io/github/palexdev/materialfx/enums/ChainMode.java

@@ -18,13 +18,15 @@
 
 package io.github.palexdev.materialfx.enums;
 
+import io.github.palexdev.materialfx.i18n.I18N;
+
 /**
  * Enumeration to specify how two predicates should be chained.
  * Also specify how a ChainMode enumeration should be represented in UI.
  */
 public enum ChainMode {
 	AND("&"),
-	OR("or");
+	OR(I18N.getOrDefault("chainMode.or"));
 
 	public static boolean useAlternativeAnd = false;
 	private final String text;
@@ -34,7 +36,7 @@ public enum ChainMode {
 	}
 
 	public String text() {
-		return this == AND && useAlternativeAnd ? "and" : this.text;
+		return this == AND && useAlternativeAnd ? I18N.getOrDefault("chainMode.alternativeAnd") : this.text;
 	}
 
 	/**

+ 3 - 2
materialfx/src/main/java/io/github/palexdev/materialfx/filter/BooleanFilter.java

@@ -20,6 +20,7 @@ package io.github.palexdev.materialfx.filter;
 
 import io.github.palexdev.materialfx.beans.BiPredicateBean;
 import io.github.palexdev.materialfx.filter.base.AbstractFilter;
+import io.github.palexdev.materialfx.i18n.I18N;
 import io.github.palexdev.materialfx.utils.FXCollectors;
 import javafx.collections.ObservableList;
 import javafx.util.StringConverter;
@@ -55,8 +56,8 @@ public class BooleanFilter<T> extends AbstractFilter<T, Boolean> {
 	@Override
 	protected ObservableList<BiPredicateBean<Boolean, Boolean>> defaultPredicates() {
 		return Stream.<BiPredicateBean<Boolean, Boolean>>of(
-				new BiPredicateBean<>("is", Boolean::equals),
-				new BiPredicateBean<>("is not", (aBoolean, aBoolean2) -> !aBoolean.equals(aBoolean2))
+				new BiPredicateBean<>(I18N.getOrDefault("filter.is"), Boolean::equals),
+				new BiPredicateBean<>(I18N.getOrDefault("filter.isNot"), (aBoolean, aBoolean2) -> !aBoolean.equals(aBoolean2))
 		).collect(FXCollectors.toList());
 	}
 

+ 7 - 6
materialfx/src/main/java/io/github/palexdev/materialfx/filter/DoubleFilter.java

@@ -20,6 +20,7 @@ package io.github.palexdev.materialfx.filter;
 
 import io.github.palexdev.materialfx.beans.BiPredicateBean;
 import io.github.palexdev.materialfx.filter.base.NumberFilter;
+import io.github.palexdev.materialfx.i18n.I18N;
 import io.github.palexdev.materialfx.utils.FXCollectors;
 import javafx.collections.ObservableList;
 import javafx.util.StringConverter;
@@ -59,12 +60,12 @@ public class DoubleFilter<T> extends NumberFilter<T, Double> {
 	@Override
 	protected ObservableList<BiPredicateBean<Double, Double>> defaultPredicates() {
 		return Stream.<BiPredicateBean<Double, Double>>of(
-				new BiPredicateBean<>("is", Double::equals),
-				new BiPredicateBean<>("is not", (aDouble, aDouble2) -> !aDouble.equals(aDouble2)),
-				new BiPredicateBean<>("greater than", (aDouble, aDouble2) -> aDouble > aDouble2),
-				new BiPredicateBean<>("greater or equal to", (aDouble, aDouble2) -> aDouble >= aDouble2),
-				new BiPredicateBean<>("lesser than", (aDouble, aDouble2) -> aDouble < aDouble2),
-				new BiPredicateBean<>("lesser or equal to", (aDouble, aDouble2) -> aDouble <= aDouble2)
+				new BiPredicateBean<>(I18N.getOrDefault("filter.is"), Double::equals),
+				new BiPredicateBean<>(I18N.getOrDefault("filter.isNot"), (aDouble, aDouble2) -> !aDouble.equals(aDouble2)),
+				new BiPredicateBean<>(I18N.getOrDefault("filter.greater"), (aDouble, aDouble2) -> aDouble > aDouble2),
+				new BiPredicateBean<>(I18N.getOrDefault("filter.greaterEqual"), (aDouble, aDouble2) -> aDouble >= aDouble2),
+				new BiPredicateBean<>(I18N.getOrDefault("filter.lesser"), (aDouble, aDouble2) -> aDouble < aDouble2),
+				new BiPredicateBean<>(I18N.getOrDefault("filter.lesserEqual"), (aDouble, aDouble2) -> aDouble <= aDouble2)
 		).collect(FXCollectors.toList());
 	}
 

+ 3 - 2
materialfx/src/main/java/io/github/palexdev/materialfx/filter/EnumFilter.java

@@ -20,6 +20,7 @@ package io.github.palexdev.materialfx.filter;
 
 import io.github.palexdev.materialfx.beans.BiPredicateBean;
 import io.github.palexdev.materialfx.filter.base.AbstractFilter;
+import io.github.palexdev.materialfx.i18n.I18N;
 import io.github.palexdev.materialfx.utils.EnumStringConverter;
 import io.github.palexdev.materialfx.utils.FXCollectors;
 import javafx.collections.ObservableList;
@@ -70,8 +71,8 @@ public class EnumFilter<T, E extends Enum<E>> extends AbstractFilter<T, E> {
 	@Override
 	protected ObservableList<BiPredicateBean<E, E>> defaultPredicates() {
 		return Stream.<BiPredicateBean<E, E>>of(
-				new BiPredicateBean<>("is", Enum::equals),
-				new BiPredicateBean<>("is not", (anEnum, anEnum2) -> !anEnum.equals(anEnum2))
+				new BiPredicateBean<>(I18N.getOrDefault("filter.is"), Enum::equals),
+				new BiPredicateBean<>(I18N.getOrDefault("filter.isNot"), (anEnum, anEnum2) -> !anEnum.equals(anEnum2))
 		).collect(FXCollectors.toList());
 	}
 

+ 7 - 6
materialfx/src/main/java/io/github/palexdev/materialfx/filter/FloatFilter.java

@@ -20,6 +20,7 @@ package io.github.palexdev.materialfx.filter;
 
 import io.github.palexdev.materialfx.beans.BiPredicateBean;
 import io.github.palexdev.materialfx.filter.base.NumberFilter;
+import io.github.palexdev.materialfx.i18n.I18N;
 import io.github.palexdev.materialfx.utils.FXCollectors;
 import javafx.collections.ObservableList;
 import javafx.util.StringConverter;
@@ -59,12 +60,12 @@ public class FloatFilter<T> extends NumberFilter<T, Float> {
 	@Override
 	protected ObservableList<BiPredicateBean<Float, Float>> defaultPredicates() {
 		return Stream.<BiPredicateBean<Float, Float>>of(
-				new BiPredicateBean<>("is", Float::equals),
-				new BiPredicateBean<>("is not", (aFloat, aFloat2) -> !aFloat.equals(aFloat2)),
-				new BiPredicateBean<>("greater than", (aFloat, aFloat2) -> aFloat > aFloat2),
-				new BiPredicateBean<>("greater or equal to", (aFloat, aFloat2) -> aFloat >= aFloat2),
-				new BiPredicateBean<>("lesser than", (aFloat, aFloat2) -> aFloat < aFloat2),
-				new BiPredicateBean<>("lesser or equal to", (aFloat, aFloat2) -> aFloat <= aFloat2)
+				new BiPredicateBean<>(I18N.getOrDefault("filter.is"), Float::equals),
+				new BiPredicateBean<>(I18N.getOrDefault("filter.isNot"), (aFloat, aFloat2) -> !aFloat.equals(aFloat2)),
+				new BiPredicateBean<>(I18N.getOrDefault("filter.greater"), (aFloat, aFloat2) -> aFloat > aFloat2),
+				new BiPredicateBean<>(I18N.getOrDefault("filter.greaterEqual"), (aFloat, aFloat2) -> aFloat >= aFloat2),
+				new BiPredicateBean<>(I18N.getOrDefault("filter.lesser"), (aFloat, aFloat2) -> aFloat < aFloat2),
+				new BiPredicateBean<>(I18N.getOrDefault("filter.lesserEqual"), (aFloat, aFloat2) -> aFloat <= aFloat2)
 		).collect(FXCollectors.toList());
 	}
 

+ 7 - 6
materialfx/src/main/java/io/github/palexdev/materialfx/filter/IntegerFilter.java

@@ -20,6 +20,7 @@ package io.github.palexdev.materialfx.filter;
 
 import io.github.palexdev.materialfx.beans.BiPredicateBean;
 import io.github.palexdev.materialfx.filter.base.NumberFilter;
+import io.github.palexdev.materialfx.i18n.I18N;
 import io.github.palexdev.materialfx.utils.FXCollectors;
 import javafx.collections.ObservableList;
 import javafx.util.StringConverter;
@@ -59,12 +60,12 @@ public class IntegerFilter<T> extends NumberFilter<T, Integer> {
 	@Override
 	protected ObservableList<BiPredicateBean<Integer, Integer>> defaultPredicates() {
 		return Stream.<BiPredicateBean<Integer, Integer>>of(
-				new BiPredicateBean<>("is", Integer::equals),
-				new BiPredicateBean<>("is not", (anInteger, anInteger2) -> !anInteger.equals(anInteger2)),
-				new BiPredicateBean<>("greater than", (anInteger, anInteger2) -> anInteger > anInteger2),
-				new BiPredicateBean<>("greater or equal to", (anInteger, anInteger2) -> anInteger >= anInteger2),
-				new BiPredicateBean<>("lesser than", (anInteger, anInteger2) -> anInteger < anInteger2),
-				new BiPredicateBean<>("lesser or equal to", (anInteger, anInteger2) -> anInteger <= anInteger2)
+				new BiPredicateBean<>(I18N.getOrDefault("filter.is"), Integer::equals),
+				new BiPredicateBean<>(I18N.getOrDefault("filter.isNot"), (anInteger, anInteger2) -> !anInteger.equals(anInteger2)),
+				new BiPredicateBean<>(I18N.getOrDefault("filter.greater"), (anInteger, anInteger2) -> anInteger > anInteger2),
+				new BiPredicateBean<>(I18N.getOrDefault("filter.greaterEqual"), (anInteger, anInteger2) -> anInteger >= anInteger2),
+				new BiPredicateBean<>(I18N.getOrDefault("filter.lesser"), (anInteger, anInteger2) -> anInteger < anInteger2),
+				new BiPredicateBean<>(I18N.getOrDefault("filter.lesserEqual"), (anInteger, anInteger2) -> anInteger <= anInteger2)
 		).collect(FXCollectors.toList());
 	}
 

+ 7 - 6
materialfx/src/main/java/io/github/palexdev/materialfx/filter/LongFilter.java

@@ -20,6 +20,7 @@ package io.github.palexdev.materialfx.filter;
 
 import io.github.palexdev.materialfx.beans.BiPredicateBean;
 import io.github.palexdev.materialfx.filter.base.NumberFilter;
+import io.github.palexdev.materialfx.i18n.I18N;
 import io.github.palexdev.materialfx.utils.FXCollectors;
 import javafx.collections.ObservableList;
 import javafx.util.StringConverter;
@@ -59,12 +60,12 @@ public class LongFilter<T> extends NumberFilter<T, Long> {
 	@Override
 	protected ObservableList<BiPredicateBean<Long, Long>> defaultPredicates() {
 		return Stream.<BiPredicateBean<Long, Long>>of(
-				new BiPredicateBean<>("is", Long::equals),
-				new BiPredicateBean<>("is not", (aLong, aLong2) -> !aLong.equals(aLong2)),
-				new BiPredicateBean<>("greater than", (aLong, aLong2) -> aLong > aLong2),
-				new BiPredicateBean<>("greater or equal to", (aLong, aLong2) -> aLong >= aLong2),
-				new BiPredicateBean<>("lesser than", (aLong, aLong2) -> aLong < aLong2),
-				new BiPredicateBean<>("lesser or equal to", (aLong, aLong2) -> aLong <= aLong2)
+				new BiPredicateBean<>(I18N.getOrDefault("filter.is"), Long::equals),
+				new BiPredicateBean<>(I18N.getOrDefault("filter.isNot"), (aLong, aLong2) -> !aLong.equals(aLong2)),
+				new BiPredicateBean<>(I18N.getOrDefault("filter.greater"), (aLong, aLong2) -> aLong > aLong2),
+				new BiPredicateBean<>(I18N.getOrDefault("filter.greaterEqual"), (aLong, aLong2) -> aLong >= aLong2),
+				new BiPredicateBean<>(I18N.getOrDefault("filter.lesser"), (aLong, aLong2) -> aLong < aLong2),
+				new BiPredicateBean<>(I18N.getOrDefault("filter.lesserEqual"), (aLong, aLong2) -> aLong <= aLong2)
 		).collect(FXCollectors.toList());
 	}
 

+ 12 - 11
materialfx/src/main/java/io/github/palexdev/materialfx/filter/StringFilter.java

@@ -20,6 +20,7 @@ package io.github.palexdev.materialfx.filter;
 
 import io.github.palexdev.materialfx.beans.BiPredicateBean;
 import io.github.palexdev.materialfx.filter.base.AbstractFilter;
+import io.github.palexdev.materialfx.i18n.I18N;
 import io.github.palexdev.materialfx.utils.FXCollectors;
 import io.github.palexdev.materialfx.utils.StringUtils;
 import javafx.collections.ObservableList;
@@ -76,17 +77,17 @@ public class StringFilter<T> extends AbstractFilter<T, String> {
 	@Override
 	protected ObservableList<BiPredicateBean<String, String>> defaultPredicates() {
 		return Stream.<BiPredicateBean<String, String>>of(
-				new BiPredicateBean<>("contains", String::contains),
-				new BiPredicateBean<>("contains ignore case", StringUtils::containsIgnoreCase),
-				new BiPredicateBean<>("contains any", StringUtils::containsAny),
-				new BiPredicateBean<>("contains all", StringUtils::containsAll),
-				new BiPredicateBean<>("ends with", String::endsWith),
-				new BiPredicateBean<>("ends with ignore case", StringUtils::endsWithIgnoreCase),
-				new BiPredicateBean<>("equals", String::equals),
-				new BiPredicateBean<>("equals ignore case", String::equalsIgnoreCase),
-				new BiPredicateBean<>("is not equal to", (aString, aString2) -> !aString.equals(aString2)),
-				new BiPredicateBean<>("starts with", String::startsWith),
-				new BiPredicateBean<>("starts with ignore case", StringUtils::startsWithIgnoreCase)
+				new BiPredicateBean<>(I18N.getOrDefault("filter.contains"), String::contains),
+				new BiPredicateBean<>(I18N.getOrDefault("filter.containsIgnCase"), StringUtils::containsIgnoreCase),
+				new BiPredicateBean<>(I18N.getOrDefault("filter.containsAny"), StringUtils::containsAny),
+				new BiPredicateBean<>(I18N.getOrDefault("filter.containsAll"), StringUtils::containsAll),
+				new BiPredicateBean<>(I18N.getOrDefault("filter.endsWith"), String::endsWith),
+				new BiPredicateBean<>(I18N.getOrDefault("filter.endsWithIgnCase"), StringUtils::endsWithIgnoreCase),
+				new BiPredicateBean<>(I18N.getOrDefault("filter.equals"), String::equals),
+				new BiPredicateBean<>(I18N.getOrDefault("filter.equalsIgnCase"), String::equalsIgnoreCase),
+				new BiPredicateBean<>(I18N.getOrDefault("filter.notEqual"), (aString, aString2) -> !aString.equals(aString2)),
+				new BiPredicateBean<>(I18N.getOrDefault("filter.startsWith"), String::startsWith),
+				new BiPredicateBean<>(I18N.getOrDefault("filter.startsWithIgnCase"), StringUtils::startsWithIgnoreCase)
 		).collect(FXCollectors.toList());
 	}
 

+ 205 - 0
materialfx/src/main/java/io/github/palexdev/materialfx/i18n/I18N.java

@@ -0,0 +1,205 @@
+/*
+ * Copyright (C) 2022 Parisi Alessandro
+ * This file is part of MaterialFX (https://github.com/palexdev/MaterialFX).
+ *
+ * MaterialFX is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * MaterialFX is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with MaterialFX.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package io.github.palexdev.materialfx.i18n;
+
+import io.github.palexdev.materialfx.MFXResourcesLoader;
+import javafx.application.Application;
+import javafx.beans.binding.Bindings;
+import javafx.beans.binding.StringBinding;
+import javafx.beans.property.ObjectProperty;
+import javafx.beans.property.SimpleObjectProperty;
+import javafx.stage.Stage;
+
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.text.MessageFormat;
+import java.util.Locale;
+import java.util.ResourceBundle;
+import java.util.concurrent.Callable;
+
+/**
+ * Class to handle internationalization.
+ * <p>
+ * To change the project's language you should use {@link #setLanguage(Language)} before
+ * loading any node (for example at the top of the {@link Application#start(Stage)} method).
+ */
+public class I18N {
+	//================================================================================
+	// Properties
+	//================================================================================
+	private static final URLClassLoader loader;
+	private static final ObjectProperty<Locale> locale = new SimpleObjectProperty<>();
+
+	//================================================================================
+	// Static Block
+	//================================================================================
+	static {
+		URL[] urls = new URL[]{MFXResourcesLoader.loadURL("i18n")};
+		loader = new URLClassLoader(urls);
+
+		setLanguage(Language.defaultLanguage());
+		locale.addListener(invalidated -> Locale.setDefault(getLocale()));
+	}
+
+	//================================================================================
+	// Methods
+	//================================================================================
+
+	/**
+	 * @return the String associated with the given key.
+	 * The resource bundle used is loaded from the current specified locale, {@link #localeProperty()}
+	 */
+	public static String get(String key, Object... args) {
+		ResourceBundle bundle = getBundle(getLocale());
+		return MessageFormat.format(bundle.getString(key), args);
+	}
+
+	/**
+	 * @return the String associated with the given key.
+	 * The resource bundle used is loaded from the specified language parameter
+	 */
+	public static String get(Language language, String key, Object... args) {
+		ResourceBundle bundle = getBundle(language.getLocale());
+		return MessageFormat.format(bundle.getString(key), args);
+	}
+
+	/**
+	 * @return the String associated with the given key.
+	 * The resource bundle used is loaded from the current specified locale, {@link #localeProperty()}
+	 * If the bundle doesn't provide any value for the given key, returns the value from the
+	 * default language, {@link Language#defaultLanguage()}
+	 */
+	public static String getOrDefault(String key, Object... args) {
+		ResourceBundle bundle = getBundle(getLocale());
+		try {
+			String s = bundle.getString(key);
+			return MessageFormat.format(s, args);
+		} catch (Exception ex) {
+			return get(Language.defaultLanguage(), key, args);
+		}
+	}
+
+	/**
+	 * @return the String associated with the given key.
+	 * The resource bundle used is loaded from the specified language parameter.
+	 * If the bundle doesn't provide any value for the given key, returns the value from the
+	 * default language, {@link Language#defaultLanguage()}
+	 */
+	public static String getOrDefault(Language language, String key, Object... args) {
+		ResourceBundle bundle = getBundle(language.getLocale());
+		try {
+			String s = bundle.getString(key);
+			return MessageFormat.format(s, args);
+		} catch (Exception ex) {
+			return get(Language.defaultLanguage(), key, args);
+		}
+	}
+
+	/**
+	 * @return the String associated with the given key.
+	 * The resource bundle used is loaded from the current specified locale, {@link #localeProperty()}
+	 * If the bundle doesn't provide any value for the given key, returns the given def parameter
+	 */
+	public static String getOrDefault(String key, String def, Object... args) {
+		ResourceBundle bundle = getBundle(getLocale());
+		try {
+			String s = bundle.getString(key);
+			return MessageFormat.format(s, args);
+		} catch (Exception ex) {
+			return def;
+		}
+	}
+
+	/**
+	 * @return the String associated with the given key.
+	 * The resource bundle used is loaded from the specified language parameter.
+	 * If the bundle doesn't provide any value for the given key, returns the given def parameter
+	 */
+	public static String getOrDefault(Language language, String key, String def, Object... args) {
+		ResourceBundle bundle = getBundle(language.getLocale());
+		try {
+			String s = bundle.getString(key);
+			return MessageFormat.format(s, args);
+		} catch (Exception ex) {
+			return def;
+		}
+	}
+
+	/**
+	 * @return a {@link StringBinding} that updates whenever the {@link #localeProperty()} changes.
+	 * The localized String is loaded using {@link #getOrDefault(String, Object...)}
+	 */
+	public static StringBinding getBinding(String key, Object... args) {
+		return Bindings.createStringBinding(() -> getOrDefault(key, args), locale);
+	}
+
+	/**
+	 * @return a {@link StringBinding} that updates whenever the {@link #localeProperty()} changes.
+	 * The value is computed according to the given {@link Callable}
+	 */
+	public static StringBinding getBinding(Callable<String> callable) {
+		return Bindings.createStringBinding(callable, locale);
+	}
+
+	/**
+	 * Responsible for loading a {@link ResourceBundle} for the given Locale.
+	 * Uses {@link URLClassLoader} to load the resource.
+	 */
+	private static ResourceBundle getBundle(Locale locale) {
+		return ResourceBundle.getBundle(getBundleBaseName(), locale, loader);
+	}
+
+	//================================================================================
+	// Getters/Setters
+	//================================================================================
+	public static Locale getLocale() {
+		return locale.get();
+	}
+
+	/**
+	 * Specifies the current MaterialFX language.
+	 * <p></p>
+	 * <b>NOTE:</b> it is not recommended to set the Locale from this property, you
+	 * should use the given setter, {@link #setLanguage(Language)}, since MaterialFX may not
+	 * support all Locales.
+	 *
+	 * @see Language
+	 */
+	public static ObjectProperty<Locale> localeProperty() {
+		return locale;
+	}
+
+	public static void setLanguage(Language language) {
+		locale.set(language.getLocale());
+	}
+
+	/**
+	 * @return all the supported languages
+	 */
+	public static Language[] getSupportedLanguages() {
+		return Language.values();
+	}
+
+	/**
+	 * @return the {@link ResourceBundle}'s base name
+	 */
+	public static String getBundleBaseName() {
+		return "mfxlang";
+	}
+}

+ 50 - 0
materialfx/src/main/java/io/github/palexdev/materialfx/i18n/Language.java

@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2022 Parisi Alessandro
+ * This file is part of MaterialFX (https://github.com/palexdev/MaterialFX).
+ *
+ * MaterialFX is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * MaterialFX is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with MaterialFX.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package io.github.palexdev.materialfx.i18n;
+
+import java.util.Locale;
+
+/**
+ * Enumerator to list all the supported {@link Locale}s by MaterialFX.
+ * <p>
+ * Every {@code Language} enumeration is associated with a {@code Locale}.
+ * <p>
+ * The enumerator also specifies the project's default language, {@link #defaultLanguage()}.
+ */
+public enum Language {
+	ENGLISH(Locale.ENGLISH),
+	ITALIANO(Locale.ITALIAN);
+
+	private final Locale locale;
+
+	Language(Locale locale) {
+		this.locale = locale;
+	}
+
+	public Locale getLocale() {
+		return locale;
+	}
+
+	/**
+	 * @return the project's default language, {@link Language#ENGLISH}
+	 */
+	public static Language defaultLanguage() {
+		return ENGLISH;
+	}
+}

+ 7 - 0
materialfx/src/main/java/io/github/palexdev/materialfx/skins/MFXComboBoxSkin.java

@@ -119,6 +119,13 @@ public class MFXComboBoxSkin<T> extends MFXTextFieldSkin {
 			popup.hide();
 		});
 		comboBox.valueProperty().addListener(invalidated -> Event.fireEvent(comboBox, new ActionEvent()));
+
+		comboBox.delegateSelectionProperty().addListener((observable, oldValue, newValue) -> {
+			if (!comboBox.isAllowEdit() && !comboBox.isSelectable()) comboBox.selectRange(0, 0);
+		});
+		comboBox.focusedProperty().addListener((observable, oldValue, newValue) -> {
+			if (!newValue && !comboBox.isSelectable()) comboBox.selectRange(0, 0);
+		});
 	}
 
 	/**

+ 2 - 1
materialfx/src/main/java/io/github/palexdev/materialfx/skins/MFXFilterComboBoxSkin.java

@@ -23,6 +23,7 @@ import io.github.palexdev.materialfx.controls.BoundTextField;
 import io.github.palexdev.materialfx.controls.MFXFilterComboBox;
 import io.github.palexdev.materialfx.controls.MFXTextField;
 import io.github.palexdev.materialfx.controls.cell.MFXFilterComboBoxCell;
+import io.github.palexdev.materialfx.i18n.I18N;
 import io.github.palexdev.virtualizedfx.cell.Cell;
 import io.github.palexdev.virtualizedfx.flow.simple.SimpleVirtualFlow;
 import javafx.geometry.Orientation;
@@ -107,7 +108,7 @@ public class MFXFilterComboBoxSkin<T> extends MFXComboBoxSkin<T> {
 		MFXFilterComboBox<T> comboBox = getComboBox();
 		TransformableList<T> filterList = comboBox.getFilterList();
 
-		MFXTextField searchField = new MFXTextField("", "Search...");
+		MFXTextField searchField = new MFXTextField("", I18N.getOrDefault("filterCombo.search"));
 		searchField.getStyleClass().add("search-field");
 		searchField.textProperty().bindBidirectional(comboBox.searchTextProperty());
 		searchField.setMaxWidth(Double.MAX_VALUE);

+ 9 - 3
materialfx/src/main/java/io/github/palexdev/materialfx/skins/MFXFilterPaneSkin.java

@@ -35,6 +35,7 @@ import io.github.palexdev.materialfx.filter.EnumFilter;
 import io.github.palexdev.materialfx.filter.base.AbstractFilter;
 import io.github.palexdev.materialfx.filter.base.NumberFilter;
 import io.github.palexdev.materialfx.font.MFXFontIcon;
+import io.github.palexdev.materialfx.i18n.I18N;
 import io.github.palexdev.materialfx.utils.NodeUtils;
 import javafx.beans.InvalidationListener;
 import javafx.beans.property.ListProperty;
@@ -94,7 +95,7 @@ public class MFXFilterPaneSkin<T> extends SkinBase<MFXFilterPane<T>> {
 
 		Region header = buildHeader();
 
-		Label filtersLabel = new Label("Active filters");
+		Label filtersLabel = new Label(I18N.getOrDefault("filterPane.activeFilters"));
 		filtersLabel.getStyleClass().add("header-label");
 		VBox.setMargin(filtersLabel, InsetsFactory.top(15));
 
@@ -240,7 +241,7 @@ public class MFXFilterPaneSkin<T> extends SkinBase<MFXFilterPane<T>> {
 			}
 		};
 		searchField.setFloatMode(FloatMode.DISABLED);
-		searchField.setPromptText("Type in your filter value...");
+		searchField.setPromptText(I18N.getOrDefault("filterPane.searchField"));
 		searchField.textProperty().bindBidirectional(query);
 
 		MFXComboBox<Object> enumsCombo = new MFXComboBox<>() {
@@ -308,7 +309,7 @@ public class MFXFilterPaneSkin<T> extends SkinBase<MFXFilterPane<T>> {
 		});
 		filterCombo.selectFirst();
 
-		MFXButton addButton = new MFXButton("Add filter") {
+		MFXButton addButton = new MFXButton(I18N.getOrDefault("filterPane.addFilter")) {
 			@Override
 			public String getUserAgentStylesheet() {
 				return filterPane.getUserAgentStylesheet();
@@ -406,6 +407,11 @@ public class MFXFilterPaneSkin<T> extends SkinBase<MFXFilterPane<T>> {
 	//================================================================================
 	// Overridden Methods
 	//================================================================================
+	@Override
+	protected double computeMinWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
+		return leftInset + filterBuilder.prefWidth(-1) + rightInset;
+	}
+
 	@Override
 	protected double computeMaxWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
 		return getSkinnable().prefWidth(-1);

+ 2 - 1
materialfx/src/main/java/io/github/palexdev/materialfx/skins/MFXNotificationCenterSkin.java

@@ -25,6 +25,7 @@ import io.github.palexdev.materialfx.controls.cell.MFXNotificationCell;
 import io.github.palexdev.materialfx.enums.NotificationCounterStyle;
 import io.github.palexdev.materialfx.enums.NotificationState;
 import io.github.palexdev.materialfx.factories.InsetsFactory;
+import io.github.palexdev.materialfx.i18n.I18N;
 import io.github.palexdev.materialfx.notifications.base.INotification;
 import io.github.palexdev.materialfx.utils.NodeUtils;
 import io.github.palexdev.virtualizedfx.flow.simple.SimpleVirtualFlow;
@@ -75,7 +76,7 @@ public class MFXNotificationCenterSkin extends SkinBase<MFXNotificationCenter> {
 		headerLabel.setMaxWidth(Double.MAX_VALUE);
 		HBox.setHgrow(headerLabel, Priority.ALWAYS);
 
-		MFXToggleButton dndToggle = new MFXToggleButton("Do not disturb");
+		MFXToggleButton dndToggle = new MFXToggleButton(I18N.getOrDefault("notificationCenter.dnd"));
 		dndToggle.setContentDisplay(ContentDisplay.RIGHT);
 		dndToggle.setGraphicTextGap(15);
 		notificationCenter.doNotDisturbProperty().bindBidirectional(dndToggle.selectedProperty());

+ 3 - 2
materialfx/src/main/java/io/github/palexdev/materialfx/skins/MFXStepperSkin.java

@@ -26,6 +26,7 @@ import io.github.palexdev.materialfx.controls.MFXStepperToggle.MFXStepperToggleE
 import io.github.palexdev.materialfx.effects.ripple.RippleClipType;
 import io.github.palexdev.materialfx.factories.MFXAnimationFactory;
 import io.github.palexdev.materialfx.factories.RippleClipTypeFactory;
+import io.github.palexdev.materialfx.i18n.I18N;
 import io.github.palexdev.materialfx.utils.AnimationUtils;
 import io.github.palexdev.materialfx.utils.NodeUtils;
 import javafx.animation.*;
@@ -130,13 +131,13 @@ public class MFXStepperSkin extends SkinBase<MFXStepper> {
 				stepperBar.heightProperty()
 		));
 
-		nextButton = new MFXButton("Next");
+		nextButton = new MFXButton(I18N.getOrDefault("stepper.next"));
 		nextButton.setManaged(false);
 		nextButton.getRippleGenerator().setClipSupplier(() ->
 				new RippleClipTypeFactory(RippleClipType.ROUNDED_RECTANGLE, 34, 34).build(nextButton)
 		);
 
-		previousButton = new MFXButton("Previous");
+		previousButton = new MFXButton(I18N.getOrDefault("stepper.previous"));
 		previousButton.setManaged(false);
 		previousButton.getRippleGenerator().setClipSupplier(() ->
 				new RippleClipTypeFactory(RippleClipType.ROUNDED_RECTANGLE, 34, 34).build(previousButton)

+ 2 - 1
materialfx/src/main/java/io/github/palexdev/materialfx/skins/MFXStepperToggleSkin.java

@@ -24,6 +24,7 @@ import io.github.palexdev.materialfx.controls.MFXTextField;
 import io.github.palexdev.materialfx.dialogs.MFXDialogs;
 import io.github.palexdev.materialfx.enums.StepperToggleState;
 import io.github.palexdev.materialfx.enums.TextPosition;
+import io.github.palexdev.materialfx.i18n.I18N;
 import io.github.palexdev.materialfx.validation.MFXValidator;
 import javafx.scene.control.SkinBase;
 import javafx.scene.input.MouseEvent;
@@ -119,7 +120,7 @@ public class MFXStepperToggleSkin extends SkinBase<MFXStepperToggle> {
 		MFXDialogs.error()
 				.setShowAlwaysOnTop(false)
 				.setShowMinimize(false)
-				.setHeaderText("Invalid Fields...")
+				.setHeaderText(I18N.getOrDefault("stepperToggle.invalidFields"))
 				.makeScrollable(true)
 				.setContentText(validator.validateToString())
 				.toStageDialogBuilder()

+ 2 - 0
materialfx/src/main/java/io/github/palexdev/materialfx/skins/MFXTableViewSkin.java

@@ -93,11 +93,13 @@ public class MFXTableViewSkin<T> extends SkinBase<MFXTableView<T>> {
 		filterDialog = MFXDialogs.filter(filterPane)
 				.setShowMinimize(false)
 				.toStageDialogBuilder()
+				.setDraggable(true)
 				.setOwnerNode(container)
 				.setCenterInOwnerNode(true)
 				.initOwner(tableView.getScene().getWindow())
 				.initModality(Modality.APPLICATION_MODAL)
 				.get();
+		filterDialog.setOnShown(event -> filterDialog.toFront());
 
 		getChildren().setAll(container);
 		addListeners();

+ 5 - 3
materialfx/src/main/java/io/github/palexdev/materialfx/utils/StringUtils.java

@@ -18,6 +18,8 @@
 
 package io.github.palexdev.materialfx.utils;
 
+import io.github.palexdev.materialfx.i18n.I18N;
+
 import java.util.Arrays;
 import java.util.List;
 
@@ -178,14 +180,14 @@ public class StringUtils {
 	 */
 	public static String timeToHumanReadable(long elapsedSeconds) {
 		if (elapsedSeconds < 60) {
-			return "Just now";
+			return I18N.getOrDefault("stringUtil.now");
 		} else {
 			long minutes = elapsedSeconds / 60;
 			if (minutes < 60) {
-				return minutes + " min ago";
+				return minutes + I18N.getOrDefault("stringUtil.minutes");
 			} else {
 				long hours = minutes / 60;
-				return hours < 24 ? hours + " hours ago" : hours / 24 + " days ago";
+				return hours < 24 ? hours + I18N.getOrDefault("stringUtils.hours") : hours / 24 + I18N.getOrDefault("stringUtils.days");
 			}
 		}
 	}

+ 8 - 0
materialfx/src/main/java/module-info.java

@@ -21,6 +21,11 @@ module MaterialFX {
 	exports io.github.palexdev.materialfx.bindings;
 	exports io.github.palexdev.materialfx.bindings.base;
 
+	// Builders Package
+	exports io.github.palexdev.materialfx.builders.base;
+	exports io.github.palexdev.materialfx.builders.control;
+	exports io.github.palexdev.materialfx.builders.layout;
+
 	// Collections Package
 	exports io.github.palexdev.materialfx.collections;
 
@@ -54,6 +59,9 @@ module MaterialFX {
 	// Font Package
 	exports io.github.palexdev.materialfx.font;
 
+	// I18N Package
+	exports io.github.palexdev.materialfx.i18n;
+
 	// Notifications Package
 	exports io.github.palexdev.materialfx.notifications;
 	exports io.github.palexdev.materialfx.notifications.base;

+ 106 - 0
materialfx/src/main/resources/io/github/palexdev/materialfx/i18n/mfxlang_en.properties

@@ -0,0 +1,106 @@
+#
+# Copyright (C) 2022 Parisi Alessandro
+# This file is part of MaterialFX (https://github.com/palexdev/MaterialFX).
+#
+# MaterialFX is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# MaterialFX is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with MaterialFX.  If not, see <http://www.gnu.org/licenses/>.
+#
+
+##################################################
+# Controls
+##################################################
+
+# Combo Box
+comboBox.contextMenu.selectFirst = Select first
+comboBox.contextMenu.selectNext = Select next
+comboBox.contextMenu.selectPrevious = Select previous
+comboBox.contextMenu.selectLast = Select last
+comboBox.contextMenu.clearSelection = Clear selection
+
+# Filter Combo Box
+filterCombo.search = Search...
+
+# Filter Pane
+filterPane.headerText = Filters
+filterPane.activeFilters = Active Filters
+filterPane.searchField = Type in your filter value...
+filterPane.addFilter = Add Filter
+
+# Notification Center
+notificationCenter.contextMenu.selectAll = Select all
+notificationCenter.contextMenu.selectRead = Select read
+notificationCenter.contextMenu.selectUnread = Select unread
+notificationCenter.contextMenu.clearSelection = Clear selection
+notificationCenter.contextMenu.sortState = Sort by state
+notificationCenter.contextMenu.sortTime = Sort by time
+notificationCenter.contextMenu.reverseSort = Reverse sort
+notificationCenter.contextMenu.filterRead = Filter by read
+notificationCenter.contextMenu.filterUnread = Filter by unread
+notificationCenter.contextMenu.clearFilter = Clear filter
+notificationCenter.contextMenu.clearSort = Clear sort
+notificationCenter.contextMenu.selectionSeparator = Selection
+notificationCenter.contextMenu.sortingSeparator = Sorting
+notificationCenter.contextMenu.filterSeparator = Filtering
+notificationCenter.dnd = Do Not Disturb
+notificationCenter.header = Notifications
+
+# Stepper
+stepper.next = Next
+stepper.previous = Previous
+
+# Stepper Toggle
+stepperToggle.invalidFields = Invalid Fields...
+
+# Text Fields
+textField.contextMenu.copy = Copy
+textField.contextMenu.cut = Cut
+textField.contextMenu.paste = Paste
+textField.contextMenu.delete = Delete
+textField.contextMenu.selectAll = Select all
+textField.contextMenu.redo = Redo
+textField.contextMenu.undo = Undo
+
+##################################################
+# Enums
+##################################################
+chainMode.alternativeAnd = and
+chainMode.or = or
+
+##################################################
+# Filters
+##################################################
+filter.is = is
+filter.isNot = is not
+filter.greater = greater than
+filter.greaterEqual = greater or equal to
+filter.lesser = lesser than
+filter.lesserEqual = lesser or equal to
+filter.contains = contains
+filter.containsIgnCase = contains ignore case
+filter.containsAny = contains any
+filter.containsAll = contains all
+filter.endsWith = ends with
+filter.endsWithIgnCase = ends with ignore case
+filter.equals = equals
+filter.equalsIgnCase = equals ignore case
+filter.notEqual = is not equal to
+filter.startsWith = starts with
+filter.startsWithIgnCase = starts with ignore case
+
+##################################################
+# Utils
+##################################################
+stringUtil.now =  Just now
+stringUtil.minutes =  min ago
+stringUtils.hours =  hours ago
+stringUtils.days =  days ago

+ 106 - 0
materialfx/src/main/resources/io/github/palexdev/materialfx/i18n/mfxlang_it.properties

@@ -0,0 +1,106 @@
+#
+# Copyright (C) 2022 Parisi Alessandro
+# This file is part of MaterialFX (https://github.com/palexdev/MaterialFX).
+#
+# MaterialFX is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# MaterialFX is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with MaterialFX.  If not, see <http://www.gnu.org/licenses/>.
+#
+
+##################################################
+# Controls
+##################################################
+
+# Combo Box
+comboBox.contextMenu.selectFirst = Seleziona il primo
+comboBox.contextMenu.selectNext = Seleziona successivo
+comboBox.contextMenu.selectPrevious = Seleziona precedente
+comboBox.contextMenu.selectLast = Seleziona l'ultimo
+comboBox.contextMenu.clearSelection = Deseleziona tutti
+
+# Filter Combo Box
+filterCombo.search = Cerca...
+
+# Filter Pane
+filterPane.headerText = Filtri
+filterPane.activeFilters = Filtri Attivi
+filterPane.searchField = Valore filtro...
+filterPane.addFilter = Aggiungi Filtro
+
+# Notification Center
+notificationCenter.contextMenu.selectAll = Seleziona tutte
+notificationCenter.contextMenu.selectRead = Seleziona lette
+notificationCenter.contextMenu.selectUnread = Seleziona non lette
+notificationCenter.contextMenu.clearSelection = Deseleziona tutte
+notificationCenter.contextMenu.sortState = Ordina per stato
+notificationCenter.contextMenu.sortTime = Ordina per tempo
+notificationCenter.contextMenu.reverseSort = Inverti ordinamento
+notificationCenter.contextMenu.filterRead = Filtra lette
+notificationCenter.contextMenu.filterUnread = Filtra non lette
+notificationCenter.contextMenu.clearFilter = Rimuovi filtro
+notificationCenter.contextMenu.clearSort = Ripristina ordinamento
+notificationCenter.contextMenu.selectionSeparator = Selezione
+notificationCenter.contextMenu.sortingSeparator = Ordinamento
+notificationCenter.contextMenu.filterSeparator = Filtraggio
+notificationCenter.dnd = Non Disturbare
+notificationCenter.header = Notifiche
+
+# Stepper
+stepper.next = Avanti
+stepper.previous = Indietro
+
+# Stepper Toggle
+stepperToggle.invalidFields = Campi non validi...
+
+# Text Fields
+textField.contextMenu.copy = Copia
+textField.contextMenu.cut = Taglia
+textField.contextMenu.paste = Incolla
+textField.contextMenu.delete = Cancella
+textField.contextMenu.selectAll = Seleziona tutto
+textField.contextMenu.redo = Redo
+textField.contextMenu.undo = Undo
+
+##################################################
+# Enums
+##################################################
+chainMode.alternativeAnd = e
+chainMode.or = o
+
+##################################################
+# Filters
+##################################################
+filter.is = è
+filter.isNot = non è
+filter.greater = più grande di
+filter.greaterEqual = più grande o uguale a
+filter.lesser = più piccolo
+filter.lesserEqual = più piccolo o uguale a
+filter.contains = contiene
+filter.containsIgnCase = contiene (maiuscole/minuscole)
+filter.containsAny = contiene una fra le seguenti
+filter.containsAll = contiene tutte le seguenti
+filter.endsWith = termina con
+filter.endsWithIgnCase = termina con (maiuscole/minuscole)
+filter.equals = è uguale a
+filter.equalsIgnCase = è uguale a (maiuscole/minuscole)
+filter.notEqual = non è uguale a
+filter.startsWith = inizia con
+filter.startsWithIgnCase = inizia con (maiuscole/minuscole)
+
+##################################################
+# Utils
+##################################################
+stringUtil.now =  ora
+stringUtil.minutes =  min fa
+stringUtils.hours =  ore fa
+stringUtils.days =  giorni fa