Merge branch 'dev-internationalization' into 'dev'

feat(i18n): ajout de l'internationalisation sur l'application graphique

See merge request iut_rt/but2/sae302-applicom/bouclyma!11
This commit is contained in:
Emi Boucly 2025-01-29 22:33:53 +01:00
commit 0752d7af0a
8 changed files with 118 additions and 40 deletions

View file

@ -8,7 +8,9 @@ import javafx.stage.Stage;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.util.Locale;
import java.util.Objects; import java.util.Objects;
import java.util.ResourceBundle;
import java.util.logging.Level; import java.util.logging.Level;
import java.util.logging.LogManager; import java.util.logging.LogManager;
import java.util.logging.Logger; import java.util.logging.Logger;
@ -28,7 +30,9 @@ public class ChatApplication extends Application {
@Override @Override
public void start(Stage stage) throws IOException { public void start(Stage stage) throws IOException {
FXMLLoader fxmlLoader = new FXMLLoader(ChatApplication.class.getResource("chat-view.fxml")); ResourceBundle i18nBundle = ResourceBundle.getBundle("rtgre.chat.i18nBundle",
Locale.getDefault());
FXMLLoader fxmlLoader = new FXMLLoader(ChatApplication.class.getResource("chat-view.fxml"), i18nBundle);
Scene scene = new Scene(fxmlLoader.load(), 600, 400); Scene scene = new Scene(fxmlLoader.load(), 600, 400);
stage.setTitle("Chat @BOUCLY_Emi (B2GA)"); stage.setTitle("Chat @BOUCLY_Emi (B2GA)");

View file

@ -77,6 +77,7 @@ public class ChatController implements Initializable {
private PostVector postVector; private PostVector postVector;
private RoomMap roomMap = new RoomMap(); private RoomMap roomMap = new RoomMap();
private ObservableList<Room> roomObservableList = FXCollections.observableArrayList(); private ObservableList<Room> roomObservableList = FXCollections.observableArrayList();
private ResourceBundle i18nBundle;
@Override @Override
@ -84,6 +85,8 @@ public class ChatController implements Initializable {
LOGGER.info("Initialisation de l'interface graphique"); LOGGER.info("Initialisation de l'interface graphique");
Image image = new Image(Objects.requireNonNull(ChatController.class.getResourceAsStream("anonymous.png"))); Image image = new Image(Objects.requireNonNull(ChatController.class.getResourceAsStream("anonymous.png")));
this.avatarImageView.setImage(image); this.avatarImageView.setImage(image);
this.i18nBundle = resourceBundle;
Thread dateTimeLoop = new Thread(this::dateTimeLoop); Thread dateTimeLoop = new Thread(this::dateTimeLoop);
dateTimeLoop.setDaemon(true); dateTimeLoop.setDaemon(true);
@ -94,7 +97,7 @@ public class ChatController implements Initializable {
hostComboBox.setValue("localhost:2024"); hostComboBox.setValue("localhost:2024");
hostComboBox.setOnAction(this::statusNameUpdate); hostComboBox.setOnAction(this::statusNameUpdate);
statusLabel.setText("Disconnected"); statusLabel.setText(i18nBundle.getString("disconnected"));
connectionButton.disableProperty().bind(validatorLogin.containsErrorsProperty()); connectionButton.disableProperty().bind(validatorLogin.containsErrorsProperty());
connectionButton.selectedProperty().addListener(this::handleConnection); connectionButton.selectedProperty().addListener(this::handleConnection);
@ -134,7 +137,7 @@ public class ChatController implements Initializable {
private void handleHostAdd(ActionEvent actionEvent) { private void handleHostAdd(ActionEvent actionEvent) {
try { try {
ChatHostAddController controller = showNewStage("Add host", "chathostadd-view.fxml"); ChatHostAddController controller = showNewStage(i18nBundle.getString("addHost"), "chathostadd-view.fxml");
if (controller.isOk()) { if (controller.isOk()) {
hostComboBox.getItems().add(controller.hostTextField.getText()); hostComboBox.getItems().add(controller.hostTextField.getText());
hostComboBox.setValue(controller.hostTextField.getText()); hostComboBox.setValue(controller.hostTextField.getText());
@ -154,7 +157,7 @@ public class ChatController implements Initializable {
* @throws IOException si le fichier FXML n'est pas trouvé dans les ressources * @throws IOException si le fichier FXML n'est pas trouvé dans les ressources
*/ */
public <T> T showNewStage(String title, String fxmlFileName) throws IOException { public <T> T showNewStage(String title, String fxmlFileName) throws IOException {
FXMLLoader fxmlLoader = new FXMLLoader(ChatApplication.class.getResource(fxmlFileName)); FXMLLoader fxmlLoader = new FXMLLoader(ChatApplication.class.getResource(fxmlFileName), i18nBundle);
Scene scene = new Scene(fxmlLoader.load()); Scene scene = new Scene(fxmlLoader.load());
Stage stage = new Stage(); Stage stage = new Stage();
stage.initModality(Modality.APPLICATION_MODAL); stage.initModality(Modality.APPLICATION_MODAL);
@ -196,7 +199,7 @@ public class ChatController implements Initializable {
try { try {
FileChooser fileChooser = new FileChooser(); FileChooser fileChooser = new FileChooser();
Stage stage = (Stage) avatarImageView.getScene().getWindow(); Stage stage = (Stage) avatarImageView.getScene().getWindow();
fileChooser.setTitle("Select Avatar"); fileChooser.setTitle(i18nBundle.getString("changeAvatar"));
fileChooser.getExtensionFilters().addAll( fileChooser.getExtensionFilters().addAll(
new FileChooser.ExtensionFilter("Image Files", "*.png", "*.jpg") new FileChooser.ExtensionFilter("Image Files", "*.png", "*.jpg")
); );
@ -230,17 +233,18 @@ public class ChatController implements Initializable {
initPostListView(); initPostListView();
clearLists(); clearLists();
contactMap.add(this.contact); contactMap.add(this.contact);
this.contact.setConnected(true);
client.sendAuthEvent(contact); client.sendAuthEvent(contact);
this.contact.setConnected(true);
client.sendListRoomEvent(); client.sendListRoomEvent();
client.sendEvent(new rtgre.modeles.Event(rtgre.modeles.Event.LIST_CONTACTS, new JSONObject())); client.sendEvent(new rtgre.modeles.Event(rtgre.modeles.Event.LIST_CONTACTS, new JSONObject()));
client.sendEvent(new rtgre.modeles.Event(rtgre.modeles.Event.CONT, contact.toJsonObject())); client.sendEvent(new rtgre.modeles.Event(rtgre.modeles.Event.CONT, contact.toJsonObject()));
initContactListView(); initContactListView();
initPostListView(); initPostListView();
this.statusLabel.setText("Connected to %s@%s:%s".formatted(this.contact.getLogin(), host, port)); this.statusLabel.setText("%s%s@%s:%s".formatted(i18nBundle.getString("connected"), this.contact.getLogin(), host, port));
} catch (IOException e) { this.connectionButton.setText(i18nBundle.getString("disconnect"));
new Alert(Alert.AlertType.ERROR, "Erreur de connexion").showAndWait(); } catch (Exception e) {
new Alert(Alert.AlertType.ERROR, i18nBundle.getString("connectionError")).showAndWait();
connectionButton.setSelected(false); connectionButton.setSelected(false);
} }
} else if (!connectionButton.isSelected()) { } else if (!connectionButton.isSelected()) {
@ -249,7 +253,8 @@ public class ChatController implements Initializable {
if (this.client.isConnected()) { if (this.client.isConnected()) {
this.contact.setConnected(false); this.contact.setConnected(false);
} }
statusLabel.setText("Disconnected"); statusLabel.setText(i18nBundle.getString("disconnected"));
this.connectionButton.setText(i18nBundle.getString("connect"));
} }
} }
@ -265,10 +270,10 @@ public class ChatController implements Initializable {
private void checkLogin(Check.Context context) { private void checkLogin(Check.Context context) {
String login = context.get("login"); String login = context.get("login");
if (!LOGIN_PATTERN.matcher(login).matches()) { if (!LOGIN_PATTERN.matcher(login).matches()) {
context.error("Format de login non respecté"); context.error(i18nBundle.getString("loginError"));
} }
if (login.equals("system")) { if (login.equals("system")) {
context.error("Le login ne peut pas être system"); context.error(i18nBundle.getString("systemError"));
} }
@ -296,7 +301,7 @@ public class ChatController implements Initializable {
try { try {
contactsListView.setCellFactory(contactListView -> new ContactListViewCell()); contactsListView.setCellFactory(contactListView -> new ContactListViewCell());
contactsListView.setItems(contactObservableList); contactsListView.setItems(contactObservableList);
File avatars = new File(getClass().getResource("avatars.png").toURI()); //File avatars = new File(getClass().getResource("avatars.png").toURI());
//Contact fifi = new Contact("fifi", true, avatars); //Contact fifi = new Contact("fifi", true, avatars);
//contactObservableList.add(fifi); //contactObservableList.add(fifi);
//contactMap.add(fifi); //contactMap.add(fifi);
@ -362,7 +367,7 @@ public class ChatController implements Initializable {
roomSelected.getUnreadCount().setUnreadCount(0); roomSelected.getUnreadCount().setUnreadCount(0);
roomsListView.refresh(); roomsListView.refresh();
Post postSys = new Post("system", loginTextField.getText(), "Bienvenue dans le salon " + roomSelected); Post postSys = new Post("system", loginTextField.getText(), i18nBundle.getString("systemHelloRoom") + roomSelected);
postsObservableList.clear(); postsObservableList.clear();
postsObservableList.add(postSys); postsObservableList.add(postSys);
client.sendEvent(new rtgre.modeles.Event("JOIN", new JSONObject().put("room", roomSelected.getRoomName()))); client.sendEvent(new rtgre.modeles.Event("JOIN", new JSONObject().put("room", roomSelected.getRoomName())));
@ -381,7 +386,7 @@ public class ChatController implements Initializable {
contactSelected.getUnreadCount().setUnreadCount(0); contactSelected.getUnreadCount().setUnreadCount(0);
Post postSys = new Post("system", loginTextField.getText(), "Bienvenue dans la discussion avec " + contactSelected.getLogin()); Post postSys = new Post("system", loginTextField.getText(), i18nBundle.getString("systemHelloContact") + contactSelected.getLogin());
postsObservableList.clear(); postsObservableList.clear();
postsObservableList.add(postSys); postsObservableList.add(postSys);
client.sendListPostEvent(0, contactSelected.getLogin()); client.sendListPostEvent(0, contactSelected.getLogin());
@ -498,4 +503,7 @@ public class ChatController implements Initializable {
} }
} }
public void errorAlert() {
new Alert(Alert.AlertType.ERROR, i18nBundle.getString("connectionError")).showAndWait();
}
} }

View file

@ -26,17 +26,19 @@ public class ChatHostAddController implements Initializable {
private boolean ok = false; private boolean ok = false;
public static final Pattern HOST_PORT_REGEX = Pattern.compile("^([-.a-zA-Z0-9]+)(?::([0-9]{1,5}))?$"); public static final Pattern HOST_PORT_REGEX = Pattern.compile("^([-.a-zA-Z0-9]+)(?::([0-9]{1,5}))?$");
private Validator validatorHost = new Validator(); private Validator validatorHost = new Validator();
private ResourceBundle i18nBundle;
@Override @Override
public void initialize(URL url, ResourceBundle resourceBundle) { public void initialize(URL url, ResourceBundle resourceBundle) {
submitButton.setOnAction(this::onActionSubmit); submitButton.setOnAction(this::onActionSubmit);
resetButton.setOnAction(this::onActionReset); resetButton.setOnAction(this::onActionReset);
this.i18nBundle = resourceBundle;
submitButton.disableProperty().bind(validatorHost.containsErrorsProperty()); submitButton.disableProperty().bind(validatorHost.containsErrorsProperty());
TooltipWrapper<Button> submitWrapper = new TooltipWrapper<>( TooltipWrapper<Button> submitWrapper = new TooltipWrapper<>(
submitButton, submitButton,
validatorHost.containsErrorsProperty(), validatorHost.containsErrorsProperty(),
Bindings.concat("Cannot submit:\n", validatorHost.createStringBinding()) Bindings.concat(i18nBundle.getString("cannotSubmit"), validatorHost.createStringBinding())
); );
this.submitWrapper.getChildren().add(submitWrapper); this.submitWrapper.getChildren().add(submitWrapper);
@ -45,13 +47,12 @@ public class ChatHostAddController implements Initializable {
.withMethod(this::checkHost) .withMethod(this::checkHost)
.decorates(hostTextField) .decorates(hostTextField)
.immediate(); .immediate();
} }
private void checkHost(Check.Context context) { private void checkHost(Check.Context context) {
String host = context.get("host"); String host = context.get("host");
if (!HOST_PORT_REGEX.matcher(host).matches()) { if (!HOST_PORT_REGEX.matcher(host).matches()) {
context.error("Host should be a valid IP address or name with potentially port number"); context.error(i18nBundle.getString("hostError"));
} }
} }

View file

@ -95,6 +95,7 @@ public class ChatClient extends ClientTCP {
} catch (IOException e) { } catch (IOException e) {
LOGGER.severe("[%s] %s".formatted(ipPort, e)); LOGGER.severe("[%s] %s".formatted(ipPort, e));
connected = false; connected = false;
Platform.runLater(() -> listener.connectionButton.setSelected(false));
} finally { } finally {
close(); close();
} }

View file

@ -1,23 +1,37 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<?import javafx.geometry.*?> <?import javafx.geometry.Insets?>
<?import javafx.scene.control.*?> <?import javafx.scene.control.Button?>
<?import javafx.scene.image.*?> <?import javafx.scene.control.ComboBox?>
<?import javafx.scene.layout.*?> <?import javafx.scene.control.Label?>
<?import javafx.scene.control.ListView?>
<?import javafx.scene.control.Menu?>
<?import javafx.scene.control.MenuBar?>
<?import javafx.scene.control.MenuItem?>
<?import javafx.scene.control.Separator?>
<?import javafx.scene.control.SplitPane?>
<?import javafx.scene.control.TextField?>
<?import javafx.scene.control.ToggleButton?>
<?import javafx.scene.image.ImageView?>
<?import javafx.scene.layout.ColumnConstraints?>
<?import javafx.scene.layout.GridPane?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.layout.RowConstraints?>
<?import javafx.scene.layout.VBox?>
<VBox alignment="CENTER" minHeight="400.0" minWidth="600.0" prefHeight="500.0" prefWidth="800.0" xmlns="http://javafx.com/javafx/17.0.2-ea" xmlns:fx="http://javafx.com/fxml/1" fx:controller="rtgre.chat.ChatController"> <VBox alignment="CENTER" minHeight="400.0" minWidth="600.0" prefHeight="500.0" prefWidth="800.0" xmlns="http://javafx.com/javafx/23.0.1" xmlns:fx="http://javafx.com/fxml/1" fx:controller="rtgre.chat.ChatController">
<children> <children>
<MenuBar VBox.vgrow="NEVER"> <MenuBar VBox.vgrow="NEVER">
<menus> <menus>
<Menu mnemonicParsing="false" text="Edit"> <Menu mnemonicParsing="false" text="%edit">
<items> <items>
<MenuItem fx:id="hostAddMenuItem" mnemonicParsing="false" text="Add host" /> <MenuItem fx:id="hostAddMenuItem" mnemonicParsing="false" text="%addHost" />
<MenuItem fx:id="avatarMenuItem" mnemonicParsing="false" text="Change avatar" /> <MenuItem fx:id="avatarMenuItem" mnemonicParsing="false" text="%changeAvatar" />
</items> </items>
</Menu> </Menu>
<Menu mnemonicParsing="false" text="Help"> <Menu mnemonicParsing="false" text="%help">
<items> <items>
<MenuItem fx:id="aboutMenuItem" mnemonicParsing="false" text="About" /> <MenuItem fx:id="aboutMenuItem" mnemonicParsing="false" text="%about" />
</items> </items>
</Menu> </Menu>
</menus> </menus>
@ -34,9 +48,9 @@
<RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" /> <RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
</rowConstraints> </rowConstraints>
<children> <children>
<Label text="Server :" GridPane.hgrow="NEVER" /> <Label text="%server" GridPane.hgrow="NEVER" />
<Label text="Login :" GridPane.hgrow="NEVER" GridPane.rowIndex="1" /> <Label text="%login" GridPane.hgrow="NEVER" GridPane.rowIndex="1" />
<ComboBox fx:id="hostComboBox" maxWidth="1.7976931348623157E308" promptText="Choose host..." GridPane.columnIndex="2" GridPane.hgrow="ALWAYS"> <ComboBox fx:id="hostComboBox" maxWidth="1.7976931348623157E308" promptText="localhost:2024" GridPane.columnIndex="2" GridPane.hgrow="ALWAYS">
<GridPane.margin> <GridPane.margin>
<Insets bottom="5.0" top="5.0" /> <Insets bottom="5.0" top="5.0" />
</GridPane.margin> </GridPane.margin>
@ -46,7 +60,7 @@
<Insets bottom="5.0" top="5.0" /> <Insets bottom="5.0" top="5.0" />
</GridPane.margin> </GridPane.margin>
</TextField> </TextField>
<ToggleButton fx:id="connectionButton" maxHeight="1.7976931348623157E308" mnemonicParsing="false" prefWidth="100.0" text="Connection" GridPane.columnIndex="3" GridPane.halignment="RIGHT" GridPane.hgrow="NEVER" GridPane.rowSpan="2"> <ToggleButton fx:id="connectionButton" maxHeight="1.7976931348623157E308" mnemonicParsing="false" prefWidth="100.0" text="%connect" GridPane.columnIndex="3" GridPane.halignment="RIGHT" GridPane.hgrow="NEVER" GridPane.rowSpan="2">
<GridPane.margin> <GridPane.margin>
<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" /> <Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
</GridPane.margin> </GridPane.margin>
@ -70,7 +84,7 @@
</SplitPane> </SplitPane>
<HBox> <HBox>
<children> <children>
<Label text="Message :"> <Label text="%message">
<padding> <padding>
<Insets bottom="5.0" left="5.0" top="5.0" /> <Insets bottom="5.0" left="5.0" top="5.0" />
</padding> </padding>
@ -86,7 +100,7 @@
<Insets bottom="5.0" right="5.0" top="5.0" /> <Insets bottom="5.0" right="5.0" top="5.0" />
</HBox.margin> </HBox.margin>
</TextField> </TextField>
<Button fx:id="sendButton" mnemonicParsing="false" text="Send"> <Button fx:id="sendButton" mnemonicParsing="false" text="%send">
<padding> <padding>
<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" /> <Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
</padding> </padding>
@ -99,10 +113,10 @@
<Separator prefWidth="200.0" /> <Separator prefWidth="200.0" />
<HBox VBox.vgrow="NEVER"> <HBox VBox.vgrow="NEVER">
<children> <children>
<Label text="Status : " /> <Label text="%status" />
<Label fx:id="statusLabel" text="Not connected" /> <Label fx:id="statusLabel" text="%disconnected" />
<Separator maxWidth="1.7976931348623157E308" orientation="VERTICAL" HBox.hgrow="ALWAYS" /> <Separator maxWidth="1.7976931348623157E308" orientation="VERTICAL" HBox.hgrow="ALWAYS" />
<Label fx:id="dateTimeLabel" text="Today" /> <Label fx:id="dateTimeLabel" text="%dateTime" />
</children> </children>
</HBox> </HBox>
</children> </children>

View file

@ -21,7 +21,7 @@
<RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" /> <RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
</rowConstraints> </rowConstraints>
<children> <children>
<Label text="Host (IP:port)" /> <Label text="%host" />
<TextField fx:id="hostTextField" prefWidth="200.0" GridPane.columnIndex="1" /> <TextField fx:id="hostTextField" prefWidth="200.0" GridPane.columnIndex="1" />
</children> </children>
<padding> <padding>
@ -33,10 +33,10 @@
</GridPane> </GridPane>
<HBox alignment="TOP_CENTER" spacing="10.0"> <HBox alignment="TOP_CENTER" spacing="10.0">
<children> <children>
<Button fx:id="resetButton" mnemonicParsing="false" prefWidth="100.0" text="Reset" /> <Button fx:id="resetButton" mnemonicParsing="false" prefWidth="100.0" text="%reset" />
<HBox fx:id="submitWrapper"> <HBox fx:id="submitWrapper">
<children> <children>
<Button fx:id="submitButton" mnemonicParsing="false" prefWidth="100.0" text="Submit" /> <Button fx:id="submitButton" mnemonicParsing="false" prefWidth="100.0" text="%submit" />
</children> </children>
</HBox> </HBox>
</children> </children>

View file

@ -0,0 +1,25 @@
about=About
addHost=Add host
cannotSubmit=Cannot submit :\n
changeAvatar=Change avatar
connected=connected to
connect=Connect
connectionError=Connection error
dateTime=dateTime
disconnect=Disconnect
disconnected=disconnected
edit=Edit
help=Help
host=Host (IP:port)
hostError=Host should be a valid IP address or name with potentially port number
login=Login :
loginError=Wrong login syntax
message=Message :
reset=Reset
send=Send
server=Server :
status=Status :
submit=Submit
systemError=Login cannot be system
systemHelloContact=Welcome in the discussion with
systemHelloRoom=Welcome in room

View file

@ -0,0 +1,25 @@
about=À propos
addHost=Ajouter un hôte
cannotSubmit=Ne peut pas valider :\n
changeAvatar=Changer l'avatar
connected=connecté à
connect=Connecter
connectionError=Erreur de connexion
dateTime=dateTime
disconnect=Déconnecter
disconnected=déconnecté
edit=Editer
help=Aide
host=Hôte (IP:port)
hostError=L'hôte doit être une adresse IP valide ou un nom avec potentiellement un numéro de port
login=Identifiant :
loginError=Format de login non respecté
message=Message :
reset=Réinitialiser
send=Envoyer
server=Serveur :
status=Statut :
submit=Valider
systemError=Le login ne peut pas être system
systemHelloContact=Bienvenue dans la discussion avec
systemHelloRoom=Bienvenue dans le salon