diff --git a/millions/pom.xml b/millions/pom.xml index fed2ae7..117b7d9 100644 --- a/millions/pom.xml +++ b/millions/pom.xml @@ -26,7 +26,7 @@ org.openjfx javafx-controls - 25.0.3 + 25.0.1 diff --git a/millions/src/main/java/no/ntnu/gruppe53/controller/GameController.java b/millions/src/main/java/no/ntnu/gruppe53/controller/GameController.java index 1147e1f..5228eb9 100644 --- a/millions/src/main/java/no/ntnu/gruppe53/controller/GameController.java +++ b/millions/src/main/java/no/ntnu/gruppe53/controller/GameController.java @@ -1,25 +1,25 @@ package no.ntnu.gruppe53.controller; import java.math.BigDecimal; -import java.math.RoundingMode; -import java.util.LinkedHashMap; import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; import javafx.scene.control.Alert; import javafx.stage.Stage; +import javafx.util.Subscription; import no.ntnu.gruppe53.model.*; +import no.ntnu.gruppe53.service.LanguageManager; import no.ntnu.gruppe53.service.StockLineChartService; import no.ntnu.gruppe53.view.*; /** * Represents the main controller for the views of the stock game. + *

Uses a {@link LanguageManager} for methods to change text dynamically.

*/ public class GameController { private Player player; private Exchange exchange; private List stocks; + private final LanguageManager lm = LanguageManager.getInstance(); private final ViewController vm; @@ -29,6 +29,9 @@ public class GameController { private PurchaseCalculator purchaseCalculator; private SaleCalculator saleCalculator; + private Subscription selectedStockPriceSubscription; + private Subscription selectedSharePriceSubscription; + /** * Initializes the {@link ViewController} to be used for switching views. * @param vm {@code ViewController} to be used when switching views @@ -112,6 +115,7 @@ public void showTransactionHistory() { * Initializes the {@link NavigationBar} and {@link FooterBar} for the controller. *

Binds functionality to exposed buttons and lists in views.

*

Sets subscriptions for observable values for views.

+ *

Sets the method for setting the selected language locale for the footer.

* @param nav the NavigationBar view * @param footer the FooterBar view */ @@ -120,6 +124,10 @@ public void initialize(NavigationBar nav, FooterBar footer) { nav.onPortfolioButtonClick(() -> vm.switchView("portfolio")); nav.onHistoryButtonClick(() -> vm.switchView("history")); + footer.onLanguageChange(selected -> { + lm.setLocale(selected.locale()); + }); + footer.onAdvanceButtonClick(() -> { if (exchange != null) { exchange.advance(); @@ -127,6 +135,11 @@ public void initialize(NavigationBar nav, FooterBar footer) { }); marketView.onStockSelection(stock -> { + if (selectedStockPriceSubscription != null) { + selectedStockPriceSubscription.unsubscribe(); + selectedStockPriceSubscription = null; + } + if (stock != null) { marketView.setSelectedStockLabel(stock.getSymbol() + " - " + stock.getCompany()); marketView.setupPriceSubscription(stock, currentStock -> { @@ -135,6 +148,16 @@ public void initialize(NavigationBar nav, FooterBar footer) { }); marketView.setPurchaseCalculator(calculatePurchase(marketView.getSelectedStock(), marketView.getQuantityPurchase())); + + selectedStockPriceSubscription = stock.salesPriceProperty().subscribe(newPrice -> { + if (marketView.getSelectedStock() != null) { + marketView.setPurchaseCalculator(calculatePurchase( + marketView.getSelectedStock(), + marketView.getQuantityPurchase() + )); + } + }); + } else { marketView.setSelectedStockLabel(""); marketView.clearChart(); @@ -146,17 +169,28 @@ public void initialize(NavigationBar nav, FooterBar footer) { if (stock != null) purchaseStock(stock.getSymbol(), marketView.getQuantityPurchase()); }); - marketView.onQuantitySelect(() -> { - if (marketView.getSelectedStock() != null) { + marketView.onQuantitySelect(() -> { + if (marketView.getSelectedStock() != null) { marketView.setPurchaseCalculator(calculatePurchase(marketView.getSelectedStock(), marketView.getQuantityPurchase())); } - }); + }); portfolioView.onShareSelection(share -> { + if (selectedSharePriceSubscription != null) { + selectedSharePriceSubscription.unsubscribe(); + selectedSharePriceSubscription = null; + } if (share != null) { portfolioView.setSelectedShareLabel(share.getStock().getSymbol() + " - " + share.getStock().getCompany()); portfolioView.setSaleCalculator(calculateSale(portfolioView.getSelectedShare())); + + selectedSharePriceSubscription = share.getStock().salesPriceProperty().subscribe(newPrice -> { + if (portfolioView.getSelectedShare() != null) { + portfolioView.setSaleCalculator(calculateSale(portfolioView.getSelectedShare())); + } + }); + } else { portfolioView.setSelectedShareLabel(""); } @@ -206,7 +240,7 @@ private void sellShare(Share selectedShare) { /** * Creates a {@link PurchaseCalculator} that can be used for the - * for the setPurchaseCalculator() function in the {@link MarketView}. + * setPurchaseCalculator() function in the {@link MarketView}. * @param stock the currently selected stock * @param quantity the currently selected quantity * @return {@link PurchaseCalculator} @@ -224,8 +258,8 @@ private PurchaseCalculator calculatePurchase(Stock stock, int quantity) { /** * Creates a {@link SaleCalculator} that can be used for the - * for the setSaleCalculator() function in the {@link PortfolioView}. - * @param share + * setSaleCalculator() function in the {@link PortfolioView}. + * @param share the share to use the calculator on * @return {@link SaleCalculator} */ private SaleCalculator calculateSale(Share share) { diff --git a/millions/src/main/java/no/ntnu/gruppe53/controller/StartViewController.java b/millions/src/main/java/no/ntnu/gruppe53/controller/StartViewController.java index de9e1ae..82d8fde 100644 --- a/millions/src/main/java/no/ntnu/gruppe53/controller/StartViewController.java +++ b/millions/src/main/java/no/ntnu/gruppe53/controller/StartViewController.java @@ -1,16 +1,23 @@ package no.ntnu.gruppe53.controller; +import no.ntnu.gruppe53.service.LanguageManager; +import no.ntnu.gruppe53.service.LanguageOption; +import no.ntnu.gruppe53.service.LanguageRegistry; import no.ntnu.gruppe53.view.StartView; import javafx.application.Platform; +import java.util.List; + /** * The controller for {@link StartView}, * the first view for the player upon launching the app. - *

Contains two choices: + *

Contains three choices: *

+ *

Uses a {@link LanguageManager} to change the UI language.

*/ public class StartViewController { /** @@ -25,6 +32,25 @@ public StartViewController(StartView view, ViewController vm) { gameController.startNewGame(); }); + view.setOnLanguage(() -> { + LanguageManager lm = LanguageManager.getInstance(); + List availableLanguages = LanguageRegistry.getLanguages(); + + int currentIndex = 0; + for (int i = 0; i < availableLanguages.size(); i++) { + if (availableLanguages.get(i).locale().equals(lm.getLocale())) { + currentIndex = i; + break; + } + } + + int nextIndex = (currentIndex + 1) % availableLanguages.size(); + LanguageOption nextLanguage = availableLanguages.get(nextIndex); + + lm.setLocale(nextLanguage.locale()); + }); + + view.setOnQuit(Platform::exit); } } \ No newline at end of file diff --git a/millions/src/main/java/no/ntnu/gruppe53/service/LanguageManager.java b/millions/src/main/java/no/ntnu/gruppe53/service/LanguageManager.java new file mode 100644 index 0000000..776f15a --- /dev/null +++ b/millions/src/main/java/no/ntnu/gruppe53/service/LanguageManager.java @@ -0,0 +1,97 @@ +package no.ntnu.gruppe53.service; + +import javafx.beans.binding.Bindings; +import javafx.beans.binding.StringBinding; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleObjectProperty; + +import java.util.Locale; +import java.util.ResourceBundle; + +/** + * Manages the language used in the user-interface. + *

The possible languages to be used are set by {@link LanguageRegistry}.

+ */ +public class LanguageManager { + + /** + * Sets a default language and creates an observable {@link ResourceBundle}. + */ + private final ObjectProperty bundleProperty = + new SimpleObjectProperty<>(); + + private LanguageManager() { + setLocale(LanguageRegistry.getDefaultLocale()); + } + + /** + * Static Inner Class to lazily create the instance + */ + private static class LanguageManagerInner { + private static final LanguageManager INSTANCE = + new LanguageManager(); + } + + /** + * Returns the instance of the language manager + * + * @return the instance of the language manager + */ + public static LanguageManager getInstance() { + return LanguageManagerInner.INSTANCE; + } + + /** + * Returns the observable resource bundle. + * + * @return an observable resource bundle + */ + public ObjectProperty getBundleProperty() { + return bundleProperty; + } + + /** + * Sets the locale of the instance to a given locale which determines the properties file to load + * values from. + * + * @param locale the locale to change to + */ + public void setLocale(Locale locale) { + ResourceBundle bundle = + ResourceBundle.getBundle("i18n.lang", locale); + + bundleProperty.set(bundle); + } + + /** + * Returns the string value of the given key in a .properties file. + * + * @param key the key to look for in the .properties file + * @return the value of the key + */ + public String getString(String key) { + return bundleProperty.get().getString(key); + } + + /** + * Returns the current locale. + * + * @return the current locale + */ + public Locale getLocale() { + return bundleProperty.get().getLocale(); + } + + /** + * Creates a {@link StringBinding} to a given string and associates it with a given key in a .properties file + * + * @param key the key to associate the string binding with + * @return a string binding observable + */ + public StringBinding bindString(String key) { + return Bindings.createStringBinding( + () -> getString(key), + getBundleProperty() + ); + } +} \ No newline at end of file diff --git a/millions/src/main/java/no/ntnu/gruppe53/service/LanguageOption.java b/millions/src/main/java/no/ntnu/gruppe53/service/LanguageOption.java new file mode 100644 index 0000000..cd607b1 --- /dev/null +++ b/millions/src/main/java/no/ntnu/gruppe53/service/LanguageOption.java @@ -0,0 +1,12 @@ +package no.ntnu.gruppe53.service; + +import java.util.Locale; + +/** + * Acts as a data-carrier representing a language with a string name and a {@link Locale}. + * + * @param name the name of the language + * @param locale the locale of the language + */ +public record LanguageOption(String name, Locale locale) {} + diff --git a/millions/src/main/java/no/ntnu/gruppe53/service/LanguageRegistry.java b/millions/src/main/java/no/ntnu/gruppe53/service/LanguageRegistry.java new file mode 100644 index 0000000..1122d2a --- /dev/null +++ b/millions/src/main/java/no/ntnu/gruppe53/service/LanguageRegistry.java @@ -0,0 +1,54 @@ +package no.ntnu.gruppe53.service; + +import java.util.List; +import java.util.Locale; + +/** + * Represents a registry of allowed {@link LanguageOption}s. + */ +public class LanguageRegistry { + + /** + * Sets all possible languages that can be used. + */ + private static final List languages = List.of( + new LanguageOption("English", new Locale("en")), + new LanguageOption("Norsk", new Locale("no")) + ); + + /** + * Sets the default language to be used. + *

Searches through the registry to find english and sets it as default.

+ */ + private static final LanguageOption defaultLang = languages.stream() + .filter(lang -> lang.locale().getLanguage().equals("en")) + .findFirst() + .orElse(languages.getFirst()); + + /** + * Returns all registered languages + * + * @return a list of registered languages + */ + public static List getLanguages() { + return languages; + } + + /** + * Returns the default language. + * + * @return the default language + */ + public static LanguageOption getDefaultLanguage() { + return defaultLang; + } + + /** + * Returns the default locale of the default language. + * + * @return the default locale + */ + public static Locale getDefaultLocale() { + return defaultLang.locale(); + } +} diff --git a/millions/src/main/java/no/ntnu/gruppe53/view/CSVChooserView.java b/millions/src/main/java/no/ntnu/gruppe53/view/CSVChooserView.java index 092e53f..a60d295 100644 --- a/millions/src/main/java/no/ntnu/gruppe53/view/CSVChooserView.java +++ b/millions/src/main/java/no/ntnu/gruppe53/view/CSVChooserView.java @@ -3,15 +3,18 @@ import javafx.scene.control.Alert; import javafx.stage.FileChooser; import javafx.stage.Stage; +import no.ntnu.gruppe53.service.LanguageManager; import java.io.File; /** * Represents an {@link Alert} and {@link FileChooser} for informing user of .csv requirement and picking said file. + *

Uses a {@link LanguageManager} to insert set text to target language.

*/ public class CSVChooserView { private final FileChooser fileChooser = new FileChooser(); Alert newGameAlert = new Alert(Alert.AlertType.INFORMATION); + private final LanguageManager lm = LanguageManager.getInstance(); /** * Returns the dialog for the alert and file chooser and exposes it to controllers. @@ -21,13 +24,9 @@ public class CSVChooserView { */ public File getDialog(Stage stage) { // Notifies the user about .csv selection - newGameAlert.setTitle("Please read:"); - newGameAlert.setHeaderText("CSV File Required"); - newGameAlert.setContentText("Please select a .csv file of the stocks to be used for the exchange " + - " in the next window.\n\nIf none is selected (or if .csv file is empty/malformed), a default file will be used." + - "\n\nThe file needs to be formated as: StockTicker,CorporationName,PriceTypeDouble." + - "\n\nExample: NVDA,Nvidia,191.27" + "\n\n'#' signifies comments, and along with lines not formatted " + - "correctly, will be ignored."); + newGameAlert.setTitle(null); + newGameAlert.headerTextProperty().bind(lm.bindString("csvHeader")); + newGameAlert.contentTextProperty().bind(lm.bindString("csvContent")); // Sets the dimensions of the alert to show all text, and as a band-aid, be resizable newGameAlert.setResizable(true); @@ -38,11 +37,12 @@ public File getDialog(Stage stage) { newGameAlert.showAndWait(); // File chooser to choose .csv file - fileChooser.setTitle("Select CSV File"); + fileChooser.setTitle(lm.getString("csvFileChooserTitle")); // Limits to .csv file only + fileChooser.getExtensionFilters().clear(); fileChooser.getExtensionFilters().add( - new FileChooser.ExtensionFilter("CSV Files", "*.csv") + new FileChooser.ExtensionFilter(lm.getString("csvFileFilterLabel"), "*.csv") ); return fileChooser.showOpenDialog(stage); } diff --git a/millions/src/main/java/no/ntnu/gruppe53/view/FooterBar.java b/millions/src/main/java/no/ntnu/gruppe53/view/FooterBar.java index b5cc00c..52c37e6 100644 --- a/millions/src/main/java/no/ntnu/gruppe53/view/FooterBar.java +++ b/millions/src/main/java/no/ntnu/gruppe53/view/FooterBar.java @@ -1,18 +1,25 @@ package no.ntnu.gruppe53.view; -import java.math.RoundingMode; +import java.util.Locale; +import java.util.function.Consumer; + import javafx.geometry.Insets; import javafx.geometry.Pos; import javafx.scene.control.Button; +import javafx.scene.control.ComboBox; import javafx.scene.control.Label; +import javafx.scene.control.ListCell; import javafx.scene.layout.BorderPane; import javafx.scene.layout.HBox; import javafx.scene.layout.Priority; import javafx.scene.layout.Region; import javafx.util.Subscription; -import no.ntnu.gruppe53.model.Player; + import no.ntnu.gruppe53.controller.GameController; import no.ntnu.gruppe53.model.Exchange; +import no.ntnu.gruppe53.service.LanguageManager; +import no.ntnu.gruppe53.service.LanguageOption; +import no.ntnu.gruppe53.service.LanguageRegistry; import no.ntnu.gruppe53.model.Stock; /* @@ -24,6 +31,7 @@ /** * A {@link HBox} view representing the footer/bottom to be used in a {@link BorderPane} layout. + *

Uses a {@link LanguageManager} to change text to target language dynamically.

*/ public class FooterBar extends HBox { @@ -37,17 +45,64 @@ public class FooterBar extends HBox { private Subscription biggestLoserSubscription; private Subscription biggestWinnerSubscription; - private Exchange exchange; + private Exchange exchange; + private final LanguageManager lm = LanguageManager.getInstance(); + + private final ComboBox languageBox; /** * Constructs the layout of the view and its components. + *

Constructs a {@link ComboBox} to let the user switch language on the fly.

+ *

Sets the text of the UI elements dynamically.

*/ public FooterBar() { this.setSpacing(10); this.setPadding(new Insets(15, 12, 15, 12)); - this.setStyle("-fx-background-color: #00bcf0;" + - "-fx-border-color: #303539 transparent transparent transparent;" + - "-fx-border-width: 5 0 0 0;"); + this.setStyle("-fx-background-color: #00bcf0;" + + "-fx-border-color: #303539 transparent transparent transparent;" + + "-fx-border-width: 5 0 0 0;"); + + languageBox = new ComboBox<>(); + languageBox.getItems().addAll(LanguageRegistry.getLanguages()); + + Locale activeLocale = lm.getLocale(); + LanguageOption activeOption = LanguageRegistry.getLanguages().stream() + .filter(lang -> lang.locale().equals(activeLocale)) + .findFirst() + .orElse(LanguageRegistry.getDefaultLanguage()); + + languageBox.setValue(activeOption); + + // Listens for changes in language from other sources (e.g. startView) + lm.getBundleProperty().subscribe(bundle -> { + java.util.Locale currentLocale = lm.getLocale(); + + LanguageOption matchingOption = LanguageRegistry.getLanguages().stream() + .filter(lang -> lang.locale().equals(currentLocale)) + .findFirst() + .orElse(null); + + if (matchingOption != null && !matchingOption.equals(languageBox.getValue())) { + languageBox.setValue(matchingOption); + } + }); + + languageBox.setCellFactory(list -> new ListCell<>() { + private Subscription cellSub; + @Override + protected void updateItem(LanguageOption item, boolean empty) { + super.updateItem(item, empty); + if (cellSub != null) cellSub.unsubscribe(); + + if (empty || item == null) { + setText(null); + } else { + cellSub = lm.getBundleProperty().subscribe(bundle -> setText(item.name())); + } + } + }); + + languageBox.setButtonCell(languageBox.getCellFactory().call(null)); HBox loserBox = new HBox(); @@ -88,41 +143,47 @@ public FooterBar() { winnerBox.getChildren().addAll(biggestWinnerLabelText, biggestWinnerLabel, biggestWinnerPriceLabel); - Region spacer = new Region(); - HBox.setHgrow(spacer, Priority.ALWAYS); + Region spacer = new Region(); + HBox.setHgrow(spacer, Priority.ALWAYS); - HBox currentWeekBox = new HBox(); - currentWeekBox.setSpacing(10); + HBox currentWeekBox = new HBox(); + currentWeekBox.setSpacing(10); - currentWeekBox.setStyle("-fx-background-color: #ffe556;" + - "-fx-text-fill: #303539;" + - "-fx-background-radius: 8;" + - "-fx-border-color: #303539;" + - "-fx-border-radius: 8;" - ); - currentWeekBox.setPadding(new Insets(8, 14, 8, 14)); - currentWeekBox.setAlignment(Pos.CENTER); - - currentWeekLabel = new Label(); - currentWeekBox.getChildren().add(currentWeekLabel); + currentWeekBox.setStyle("-fx-background-color: #ffe556;" + + "-fx-text-fill: #303539;" + + "-fx-background-radius: 8;" + + "-fx-border-color: #303539;" + + "-fx-border-radius: 8;" + ); + currentWeekBox.setPadding(new Insets(8, 14, 8, 14)); + currentWeekBox.setAlignment(Pos.CENTER); + Label currentWeekLabelText = new Label(); + currentWeekLabelText.textProperty().bind(lm.bindString("currentWeekLabelText")); + currentWeekLabel = new Label(); + currentWeekBox.getChildren().addAll(currentWeekLabelText, currentWeekLabel); + advanceWeekButton = new Button(); + advanceWeekButton.textProperty().bind(lm.bindString("advanceWeekButton")); + advanceWeekButton.setStyle("-fx-background-color: #ffe556; -fx-text-fill: #303539;"); - advanceWeekButton = new Button("Advance Week"); - advanceWeekButton.setStyle("-fx-background-color: #ffe556; -fx-text-fill: #303539;"); - - this.getChildren().addAll(loserBox, winnerBox, spacer, currentWeekBox, advanceWeekButton); - } + this.getChildren().addAll(languageBox, loserBox, winnerBox, spacer, currentWeekBox, advanceWeekButton); + } - public void setExchange(Exchange exchange) { - this.exchange = exchange; - currentWeekSubscription = exchange.getCurrentWeekProperty().subscribe(newVal -> { - if (exchange != null) { - currentWeekLabel.setText("Current Week: " + newVal); - } else { - currentWeekLabel.setText("Current Week: None"); - } - }); + /** + * Sets the exchange and subscribes to week updates. + * + * @param exchange the {@link Exchange} used in the game + */ + public void setExchange(Exchange exchange) { + this.exchange = exchange; + currentWeekSubscription = exchange.getCurrentWeekProperty().subscribe(newVal -> { + if (exchange != null) { + currentWeekLabel.setText(newVal.toString()); + } else { + currentWeekLabel.setText("None"); + } + }); if (biggestLoserSubscription != null) { biggestLoserSubscription.unsubscribe(); @@ -143,8 +204,7 @@ public void setExchange(Exchange exchange) { biggestWinnerLabel.setText(winner.getSymbol() + " - " + winner.getCompany()); biggestWinnerPriceLabel.setText(" $" + winner.getLatestPriceChange()); }); - - } + } /** * Exposes the advanceWeekButton to {@link GameController} so it can @@ -156,3 +216,18 @@ public void onAdvanceButtonClick(Runnable action) { advanceWeekButton.setOnAction(e -> action.run()); } } + + /** + * Exoposes the LanguageOption ComboButton to the controller. + * + * @param action the action to be performed when selecting the language + */ + public void onLanguageChange(Consumer action) { + languageBox.setOnAction(e -> { + LanguageOption selected = languageBox.getValue(); + if (selected != null) { + action.accept(selected); + } + }); + } +} \ No newline at end of file diff --git a/millions/src/main/java/no/ntnu/gruppe53/view/MarketView.java b/millions/src/main/java/no/ntnu/gruppe53/view/MarketView.java index 380ef7b..bab3197 100644 --- a/millions/src/main/java/no/ntnu/gruppe53/view/MarketView.java +++ b/millions/src/main/java/no/ntnu/gruppe53/view/MarketView.java @@ -16,6 +16,7 @@ import javafx.util.Subscription; import no.ntnu.gruppe53.model.*; import no.ntnu.gruppe53.service.FormatBigDecimal; +import no.ntnu.gruppe53.service.LanguageManager; /* Yellow Color, Hex: #ffe556 RGB(255, 229, 86) @@ -29,15 +30,14 @@ * price history statistics in the form of a {@link LineChart} through the * {@link StockLineChartView} class, as well as buying {@link Stock}s. *

Extends borderpane to allow easy organizing of the internal layout.

+ *

Uses a {@link LanguageManager} to set text to the targer language dynamically.

*/ public class MarketView extends BorderPane { + private final LanguageManager lm = LanguageManager.getInstance(); private Button purchaseButton; private Label selectedStock; - - - //Dynamic Labels/Text public TextField gross; public TextField commission; @@ -52,7 +52,6 @@ public class MarketView extends BorderPane { private PurchaseCalculator purchaseCalculator; private final StockLineChartView stockLineChartView; - private Subscription playerSubscription; private Subscription priceSubscription; private Subscription grossSubscription; private Subscription commissionSubscription; @@ -71,6 +70,7 @@ public MarketView() { /** * Constructs the view and its components. *

Subscribes to a stock's sales price to update the value immediately.

+ *

Subscribes to the current language to change text immediately.

*

Uses a cell factory to format the string found in the stocksListView.

* * @return the constructed MarketView @@ -83,7 +83,8 @@ private VBox centerPane() { //Blue Background color vBox.setStyle("-fx-background-color: #00bcf0;"); - Label titleLabel = new Label("Stock Market"); + Label titleLabel = new Label(); + titleLabel.textProperty().bind(lm.bindString("marketTitle")); titleLabel.setStyle("-fx-font-size: 20px; -fx-font-weight: bold; "); VBox titleBox = new VBox(titleLabel); @@ -97,14 +98,15 @@ private VBox centerPane() { titleBox.setPadding(new Insets(4)); titleBox.setMaxWidth(200); - + // Stock Graph stockLineChartView.getChart().setMinHeight(0); stockLineChartView.getChart().setMaxHeight(Double.MAX_VALUE); stockLineChartView.getChart().setMinWidth(0); stockLineChartView.getChart().setMaxWidth(Double.MAX_VALUE); + stockLineChartView.getChart().getXAxis().labelProperty().bind(lm.bindString("xAxisLabel")); + stockLineChartView.getChart().getYAxis().labelProperty().bind(lm.bindString("yAxisLabel")); - - + // List of stocks stocksListView = new ListView<>(); stocksListView.setMinHeight(0); stocksListView.setMaxHeight(Double.MAX_VALUE); @@ -114,6 +116,7 @@ private VBox centerPane() { stocksListView.setCellFactory(list -> new ListCell<>() { private Subscription cellSubscription; + private Subscription langSubscription; @Override protected void updateItem(Stock stock, boolean empty) { @@ -123,22 +126,37 @@ protected void updateItem(Stock stock, boolean empty) { cellSubscription.unsubscribe(); cellSubscription = null; } + if (langSubscription != null) { + langSubscription.unsubscribe(); + langSubscription = null; + } if (empty || stock == null) { setText(null); } else { - cellSubscription = stock.salesPriceProperty().subscribe(newPrice -> { - setText(stock.getSymbol() + " - " + stock.getCompany() + " | Price: $" + - FormatBigDecimal.formatNumber(newPrice) - + " | Price change: " + FormatBigDecimal.formatNumber(stock.getLatestPriceChange()) - + " | Highest price: " + FormatBigDecimal.formatNumber(stock.getHighestPrice()) - + " | Lowest price: " + FormatBigDecimal.formatNumber(stock.getLowestPrice())); - }); + Runnable updateTextAction = () -> { + String template = lm.getString("stockCellFormat"); + setText(String.format(template, + stock.getSymbol(), + stock.getCompany(), + FormatBigDecimal.formatNumber(stock.salesPriceProperty().get()), + FormatBigDecimal.formatNumber(stock.getLatestPriceChange()), + FormatBigDecimal.formatNumber(stock.getHighestPrice()), + FormatBigDecimal.formatNumber(stock.getLowestPrice()) + )); + }; + + cellSubscription = stock.salesPriceProperty().subscribe(newPrice -> updateTextAction.run()); + langSubscription = lm.getBundleProperty().subscribe(bundle -> updateTextAction.run()); + + updateTextAction.run(); } } }); - HBox stockListBox = new HBox(stocksListView); + + // Container for list of stocks + HBox stockListBox = new HBox(stocksListView); stockListBox.setStyle("-fx-background-color: #ffe556;" + "-fx-text-fill: #303539;" + "-fx-background-radius: 8;" + @@ -146,6 +164,7 @@ protected void updateItem(Stock stock, boolean empty) { "-fx-border-radius: 8;" ); + // Container for graph of stocks HBox stockChartBox = new HBox(stockLineChartView.getChart()); stockChartBox.setStyle("-fx-background-color: #ffe556;" + "-fx-text-fill: #303539;" + @@ -176,14 +195,16 @@ protected void updateItem(Stock stock, boolean empty) { HBox selectedStockBox = new HBox(); - Label selectedStockLabel = new Label("Selected Stock: "); + Label selectedStockLabel = new Label(); + selectedStockLabel.textProperty().bind(lm.bindString("selectedStock")); selectedStock = new Label(""); selectedStockBox.getChildren().addAll(selectedStockLabel, selectedStock); HBox quantityBox = new HBox(); - Label quantityLabel = new Label("Quantity: "); + Label quantityLabel = new Label(); + quantityLabel.textProperty().bind(lm.bindString("purchaseQuantity")); quantitySpinner = new Spinner<>(1, 1_000_000, 1); quantitySpinner.setEditable(true); @@ -191,11 +212,13 @@ protected void updateItem(Stock stock, boolean empty) { HBox calculationBox = new HBox(); - Label grossLabel = new Label("Gross: "); + Label grossLabel = new Label(); + grossLabel.textProperty().bind(lm.bindString("purchaseGross")); gross = new TextField(); gross.setEditable(false); gross.setMaxWidth(200); - Label commissionLabel = new Label("Commission: "); + Label commissionLabel = new Label(); + commissionLabel.textProperty().bind(lm.bindString("purchaseCommission")); commission = new TextField(); commission.setEditable(false); commission.setMaxWidth(200); @@ -204,14 +227,16 @@ protected void updateItem(Stock stock, boolean empty) { HBox totalBox = new HBox(); - Label totalLabel = new Label("Total: "); + Label totalLabel = new Label(); + totalLabel.textProperty().bind(lm.bindString("purchaseTotal")); total = new TextField(""); total.setEditable(false); total.setMaxWidth(200); totalBox.getChildren().addAll(totalLabel, total); - purchaseButton = new Button("Purchase"); + purchaseButton = new Button(); + purchaseButton.textProperty().bind(lm.bindString("purchaseButton")); VBox purchaseBox = new VBox(5); diff --git a/millions/src/main/java/no/ntnu/gruppe53/view/NavigationBar.java b/millions/src/main/java/no/ntnu/gruppe53/view/NavigationBar.java index 77dddfb..16bbc9e 100644 --- a/millions/src/main/java/no/ntnu/gruppe53/view/NavigationBar.java +++ b/millions/src/main/java/no/ntnu/gruppe53/view/NavigationBar.java @@ -10,6 +10,7 @@ import javafx.util.Subscription; import no.ntnu.gruppe53.model.Exchange; import no.ntnu.gruppe53.model.Player; +import no.ntnu.gruppe53.service.LanguageManager; import java.math.RoundingMode; @@ -27,8 +28,10 @@ * clicking on the buttons representing the desired view.

*

The {@link no.ntnu.gruppe53.controller.GameController} handles * the actions for the buttons.

+ *

Uses a {@link LanguageManager} to set text of UI elements dynamically.

*/ public class NavigationBar extends HBox { + private final LanguageManager lm = LanguageManager.getInstance(); private final Button marketButton; private final Button portfolioButton; @@ -52,9 +55,10 @@ public class NavigationBar extends HBox { private Label netWorthLabel; private Label moneyLabel; - - - + /** + * Constructs the UI elements of the navigationBar. + *

Binds the language manager to the textProperty of the UI elements to dynamically change the text.

+ */ public NavigationBar() { this.setPadding(new Insets(15, 12, 15, 12)); @@ -65,11 +69,14 @@ public NavigationBar() { "-fx-border-width: 0 0 5 0;"); - marketButton = new Button("Market"); + marketButton = new Button(); + marketButton.textProperty().bind(lm.bindString("marketButton")); marketButton.setStyle("-fx-background-color: #ffe556; -fx-text-fill: #303539;"); - portfolioButton = new Button("Portfolio"); + portfolioButton = new Button(); + portfolioButton.textProperty().bind(lm.bindString("portfolioButton")); portfolioButton.setStyle("-fx-background-color: #ffe556; -fx-text-fill: #303539;"); - historyButton = new Button("History"); + historyButton = new Button(); + historyButton.textProperty().bind(lm.bindString("historyButton")); historyButton.setStyle("-fx-background-color: #ffe556; -fx-text-fill: #303539;"); Region spacer = new Region(); @@ -79,7 +86,8 @@ public NavigationBar() { HBox playerBox = new HBox(); - Label playerLabelText = new Label("Player: "); + Label playerLabelText = new Label(); + playerLabelText.textProperty().bind(lm.bindString("navPlayerLabelText")); playerLabel = new Label("Player"); playerBox.setStyle("-fx-background-color: #ffe556;" + @@ -112,7 +120,8 @@ public NavigationBar() { HBox moneyBox = new HBox(); - Label moneyLabelText = new Label("Money: "); + Label moneyLabelText = new Label(); + moneyLabelText.textProperty().bind(lm.bindString("navMoneyLabelText")); moneyLabel = new Label(); moneyBox.setStyle("-fx-background-color: #ffe556;" + @@ -127,7 +136,8 @@ public NavigationBar() { HBox networthBox = new HBox(); - Label networthLabelText = new Label("Net Worth: "); + Label networthLabelText = new Label(); + networthLabelText.textProperty().bind(lm.bindString("navNetWorthLabelText")); netWorthLabel = new Label(); networthBox.setStyle("-fx-background-color: #ffe556;" + @@ -188,16 +198,16 @@ public void bindPlayer(Player player) { playerLabel.setText(player.getName()); netWorthSubscription = player.getNetWorthProperty().subscribe(newVal -> { if (newVal != null) { - netWorthLabel.setText("Net Worth: $" + newVal.setScale(2, RoundingMode.HALF_EVEN)); + netWorthLabel.setText("$" + newVal.setScale(2, RoundingMode.HALF_EVEN)); } else { - netWorthLabel.setText("Net Worth: $0.00"); + netWorthLabel.setText("$0.00"); } }); moneySubscription = player.getMoneyProperty().subscribe(newVal -> { if (newVal != null) { - moneyLabel.setText("Money: $" + newVal.setScale(2, RoundingMode.HALF_EVEN)); + moneyLabel.setText("$" + newVal.setScale(2, RoundingMode.HALF_EVEN)); } else { - moneyLabel.setText("Money: $0.00"); + moneyLabel.setText("$0.00"); } }); diff --git a/millions/src/main/java/no/ntnu/gruppe53/view/PlayerNameChooserView.java b/millions/src/main/java/no/ntnu/gruppe53/view/PlayerNameChooserView.java index 669c5f2..0711ae2 100644 --- a/millions/src/main/java/no/ntnu/gruppe53/view/PlayerNameChooserView.java +++ b/millions/src/main/java/no/ntnu/gruppe53/view/PlayerNameChooserView.java @@ -6,6 +6,7 @@ import javafx.scene.control.*; import javafx.scene.layout.HBox; import javafx.scene.layout.VBox; +import no.ntnu.gruppe53.service.LanguageManager; import java.util.Optional; @@ -19,18 +20,22 @@ * Represents a prompt for the {@link Player}'s name */ public class PlayerNameChooserView { - Dialog dialog = new Dialog<>(); + private final LanguageManager lm = LanguageManager.getInstance(); + Dialog dialog; /** * Returns the dialog where the player can input a name of maximum 50 characters. *

A {@link TextFormatter} is used to set the maximum amount * of characters for the player name.

+ *

Uses a {@link LanguageManager} to determine text language.

* * @return the dialog for player name input */ public Optional getDialog() { + dialog = new Dialog<>(); + // Sets dialog title and header text - dialog.setTitle("Player Name"); + dialog.setTitle(null); dialog.setHeaderText(null); // Sets the dimensions for the dialog pane @@ -41,8 +46,10 @@ public Optional getDialog() { ButtonType confirmButton = new ButtonType("Confirm", ButtonBar.ButtonData.OK_DONE); - dialog.getDialogPane().getButtonTypes().add(confirmButton); + Button actualConfirmButton = (Button) dialog.getDialogPane().lookupButton(confirmButton); + + actualConfirmButton.textProperty().bind(lm.bindString("playerNameConfirmButton")); TextField nameField = new TextField(); @@ -73,9 +80,8 @@ public Optional getDialog() { counterLabel.setText(newVal.length() + " / 50"); }); - Label infoLabel = new Label( - "Please enter your player name:" - ); + Label infoLabel = new Label(); + infoLabel.textProperty().bind(lm.bindString("playerNameInfo")); VBox layout = new VBox(15); diff --git a/millions/src/main/java/no/ntnu/gruppe53/view/PlayerStartingMoneyChooserView.java b/millions/src/main/java/no/ntnu/gruppe53/view/PlayerStartingMoneyChooserView.java index 82b0c50..7dd6d65 100644 --- a/millions/src/main/java/no/ntnu/gruppe53/view/PlayerStartingMoneyChooserView.java +++ b/millions/src/main/java/no/ntnu/gruppe53/view/PlayerStartingMoneyChooserView.java @@ -5,15 +5,18 @@ import javafx.geometry.Insets; import javafx.scene.control.*; import javafx.scene.layout.VBox; +import no.ntnu.gruppe53.service.LanguageManager; import java.math.BigDecimal; import java.util.Optional; /** * Represents the prompt for the {@link Player}'s starting money. + *

Uses a {@link LanguageManager} to set text to target language.

*/ public class PlayerStartingMoneyChooserView { Dialog dialog = new Dialog<>(); + private final LanguageManager lm = LanguageManager.getInstance(); /** * Returns the dialog for the player's starting money. @@ -25,7 +28,7 @@ public class PlayerStartingMoneyChooserView { public Optional getDialog() { // Sets dialog title and header text - dialog.setTitle("Select starting money:"); + dialog.setTitle(null); dialog.setHeaderText(null); // Sets the dimensions for the dialog pane @@ -36,9 +39,12 @@ public Optional getDialog() { ButtonType confirmButton = new ButtonType("Confirm", ButtonBar.ButtonData.OK_DONE); - dialog.getDialogPane().getButtonTypes().add(confirmButton); + Button actualConfirmButton = (Button) dialog.getDialogPane().lookupButton(confirmButton); + + actualConfirmButton.textProperty().bind(lm.bindString("playerStartingMoneyConfirmButton")); + Spinner spinner = new Spinner<>(); // Sets allowed values and step size for the spinner @@ -78,9 +84,8 @@ public Optional getDialog() { spinner.setPrefWidth(200); - Label infoLabel = new Label( - "Select your starting money balance for trading:" - ); + Label infoLabel = new Label(); + infoLabel.textProperty().bind(lm.bindString("playerStartingMoneyInfoLabel")); VBox layout = new VBox(15); diff --git a/millions/src/main/java/no/ntnu/gruppe53/view/PortfolioView.java b/millions/src/main/java/no/ntnu/gruppe53/view/PortfolioView.java index 7388c16..290d375 100644 --- a/millions/src/main/java/no/ntnu/gruppe53/view/PortfolioView.java +++ b/millions/src/main/java/no/ntnu/gruppe53/view/PortfolioView.java @@ -2,7 +2,6 @@ import java.math.BigDecimal; import java.math.RoundingMode; -import java.util.function.BiConsumer; import java.util.function.Consumer; import javafx.collections.ObservableList; @@ -23,8 +22,8 @@ import no.ntnu.gruppe53.model.Player; import no.ntnu.gruppe53.model.SaleCalculator; import no.ntnu.gruppe53.model.Share; -import no.ntnu.gruppe53.model.Stock; import no.ntnu.gruppe53.service.FormatBigDecimal; +import no.ntnu.gruppe53.service.LanguageManager; /* Yellow Color, Hex: #ffe556 RGB(255, 229, 86) @@ -40,13 +39,12 @@ * and the ability to sell the share for money.

*

Extends the {@link BorderPane}

for easy layout setup. *

Events are handled by the {@link no.ntnu.gruppe53.controller.GameController}.

+ *

Uses a {@link LanguageManager} to dynamically set text based on target language.

*/ public class PortfolioView extends BorderPane { - private Button sellButton; + private final LanguageManager lm = LanguageManager.getInstance(); - //NavigationBar Buttons - private Button marketButton; - private Button portfolioButton; + private Button sellButton; //Static Labels private Label titleLabel; @@ -79,7 +77,8 @@ public PortfolioView() { /** * Constructs the view of the portfolio. *

Subscribes to shares for sales price updates.

- *

Uses a cell factory to format the displayed string representing each share.

+ *

Uses a cell factory to format the displayed string representing each share. + * The sell factory has the text dynamically set by the language manager.

* * @return a VBox of the portfolio view */ @@ -90,7 +89,8 @@ private VBox centerPane() { //Blue Background color vBox.setStyle("-fx-background-color: #00bcf0; "); - titleLabel = new Label("Portfolio"); + titleLabel = new Label(); + titleLabel.textProperty().bind(lm.bindString("portfolioTitle")); titleLabel.setStyle("-fx-font-size: 20px; -fx-font-weight: bold; -fx-text-fill: #303539;"); VBox titleBox = new VBox(titleLabel); @@ -110,6 +110,7 @@ private VBox centerPane() { portfolioListView.setCellFactory(list -> new ListCell<>() { private Subscription cellSubscription; + private Subscription langSubscription; @Override protected void updateItem(Share item, boolean empty) { @@ -119,15 +120,32 @@ protected void updateItem(Share item, boolean empty) { cellSubscription.unsubscribe(); cellSubscription = null; } + if (langSubscription != null) { + langSubscription.unsubscribe(); + langSubscription = null; + } if (empty || item == null || item.getStock() == null) { setText(null); setGraphic(null); } else { setText(null); - cellSubscription = item.getStock().salesPriceProperty().subscribe(currentPrice -> { + + + Runnable updateGraphicAction = () -> { + BigDecimal currentPrice = item.getStock().salesPriceProperty().get(); setGraphic(createColoredShareText(item, currentPrice)); + }; + + cellSubscription = item.getStock().salesPriceProperty().subscribe(currentPrice -> { + updateGraphicAction.run(); }); + + langSubscription = lm.getBundleProperty().subscribe(bundle -> { + updateGraphicAction.run(); + }); + + updateGraphicAction.run(); } } }); @@ -150,22 +168,26 @@ protected void updateItem(Share item, boolean empty) { HBox selectedShareBox = new HBox(); - Label selectedShareLabel = new Label("Selected Share: "); + Label selectedShareLabel = new Label(); + selectedShareLabel.textProperty().bind(lm.bindString("selectedShare")); selectedShare = new Label(); selectedShareBox.getChildren().addAll(selectedShareLabel, selectedShare); HBox calculationBox = new HBox(); - Label grossLabel = new Label("Gross: "); + Label grossLabel = new Label(); + grossLabel.textProperty().bind(lm.bindString("portfolioGross")); gross = new TextField(); gross.setEditable(false); gross.setMaxWidth(200); - Label commissionLabel = new Label("Commission: "); + Label commissionLabel = new Label(); + commissionLabel.textProperty().bind(lm.bindString("portfolioCommission")); commission = new TextField(); commission.setEditable(false); commission.setMaxWidth(200); - Label taxLabel = new Label("Tax: "); + Label taxLabel = new Label(); + taxLabel.textProperty().bind(lm.bindString("portfolioTax")); tax = new TextField(); tax.setEditable(false); tax.setMaxWidth(200); @@ -176,14 +198,16 @@ protected void updateItem(Share item, boolean empty) { HBox totalBox = new HBox(); - Label totalLabel = new Label("Total: "); + Label totalLabel = new Label(); + totalLabel.textProperty().bind(lm.bindString("portfolioTotal")); total = new TextField(); total.setEditable(false); total.setMaxWidth(200); totalBox.getChildren().addAll(totalLabel, total); - sellButton = new Button("Sell"); + sellButton = new Button(); + sellButton.textProperty().bind(lm.bindString("sellButton")); VBox saleBox = new VBox(5); @@ -316,11 +340,16 @@ private TextFlow createColoredShareText(Share share, BigDecimal currentPrice) { String text = share.getStock().getSymbol() + " - " + share.getStock().getCompany() - + " | Qty: " + + " | " + + lm.getString("portfolioShareQuantity") + share.getQuantity() - + " | Buy price: $" + + " | " + + lm.getString("portfolioBuyPrice") + + "$" + FormatBigDecimal.formatNumber(share.getPurchasePrice()) - + " | Current Price: $"; + + " | " + + lm.getString("portfolioCurrentPrice") + + "$"; Text t1 = new Text(text); Text tValue = new Text(FormatBigDecimal.formatNumber(currentPrice)); diff --git a/millions/src/main/java/no/ntnu/gruppe53/view/StartView.java b/millions/src/main/java/no/ntnu/gruppe53/view/StartView.java index 5756028..a49d52b 100644 --- a/millions/src/main/java/no/ntnu/gruppe53/view/StartView.java +++ b/millions/src/main/java/no/ntnu/gruppe53/view/StartView.java @@ -5,16 +5,23 @@ import javafx.scene.image.Image; import javafx.scene.image.ImageView; import javafx.scene.layout.VBox; + +import javafx.util.Subscription; import no.ntnu.gruppe53.controller.StartViewController; import no.ntnu.gruppe53.controller.ViewController; +import no.ntnu.gruppe53.service.LanguageManager; /** * Represents the first page seen by the user of the application. + *

Uses a {@link LanguageManager} for dynamically setting button and label text. + * Subscribes to changes in language set by the manager.

*/ public class StartView extends VBox { private final Button newGameButton; + private final Button languageButton; private final Button quitButton; private final Image logo; + private Subscription languageSubscription; /** * Constructs the view and sets parameters to be exposed for the {@link StartViewController}. @@ -22,6 +29,8 @@ public class StartView extends VBox { * @param vm the {@link ViewController} for the view */ public StartView(ViewController vm) { + LanguageManager lm = LanguageManager.getInstance(); + this.setAlignment(Pos.CENTER); this.setSpacing(20); @@ -32,29 +41,47 @@ public StartView(ViewController vm) { logoView.setFitWidth(300); logoView.setPreserveRatio(true); - newGameButton = new Button("New Game"); - quitButton = new Button("Quit"); + newGameButton = new Button(); + newGameButton.textProperty().bind(lm.bindString("newGame")); + + languageButton = new Button(); + languageButton.setPrefWidth(200); + languageButton.setStyle("-fx-background-color: #ffe556; -fx-text-fill: #303539;"); + languageSubscription = lm.getBundleProperty().subscribe(bundle -> { + languageButton.setText(lm.getString("languageButtonLabel")); + }); + + quitButton = new Button(); + quitButton.textProperty().bind(lm.bindString("quitGame")); newGameButton.setPrefWidth(200); newGameButton.setStyle("-fx-background-color: #ffe556; -fx-text-fill: #303539;"); quitButton.setPrefWidth(200); quitButton.setStyle("-fx-background-color: #ffe556; -fx-text-fill: #303539;"); - this.getChildren().addAll(logoView, newGameButton, quitButton); + this.getChildren().addAll(logoView, newGameButton, languageButton, quitButton); } /** - * Sets the method to be run when + * Sets the method to be run when the new game button is pressed * - * @param action + * @param action the method to execute on button press */ public void setOnNewGame(Runnable action) { newGameButton.setOnAction(e -> action.run()); } /** + * Sets the method to be run when the languageButton is pressed + * + * @param action the method to execute on button press + */ + public void setOnLanguage(Runnable action) {languageButton.setOnAction(e -> action.run());} + + /** + * Sets the method to be run when the quit game button is pressed * - * @param action + * @param action the method to execute on button press */ public void setOnQuit(Runnable action) { quitButton.setOnAction(e -> action.run()); diff --git a/millions/src/main/java/no/ntnu/gruppe53/view/TransactionArchiveView.java b/millions/src/main/java/no/ntnu/gruppe53/view/TransactionArchiveView.java index 630adc8..fb5e11b 100644 --- a/millions/src/main/java/no/ntnu/gruppe53/view/TransactionArchiveView.java +++ b/millions/src/main/java/no/ntnu/gruppe53/view/TransactionArchiveView.java @@ -10,11 +10,14 @@ import javafx.scene.layout.VBox; import java.math.BigDecimal; import java.math.RoundingMode; + +import javafx.util.Subscription; import no.ntnu.gruppe53.model.Player; import no.ntnu.gruppe53.model.Transaction; import no.ntnu.gruppe53.model.Purchase; import no.ntnu.gruppe53.model.Sale; import no.ntnu.gruppe53.service.FormatBigDecimal; +import no.ntnu.gruppe53.service.LanguageManager; /* Yellow Color, Hex: #ffe556 RGB(255, 229, 86) @@ -28,6 +31,7 @@ *

Extends the {@link BorderPane} for easy layout setup.

*/ public class TransactionArchiveView extends BorderPane { + private final LanguageManager lm = LanguageManager.getInstance(); private ListView historyListView; private Label titleLabel; @@ -46,6 +50,7 @@ public TransactionArchiveView() { *

Assigns red text to purchases, and green text to sales.

*

Uses the formatBigDecimal method to convert the BigDecimal to a * string with 2 decimals.

+ *

Uses a {@link LanguageManager} to dynamically set text based on target language.

* * @return a VBox of the constructed view of the transaction archive */ @@ -54,7 +59,8 @@ private VBox centerPane() { vBox.setPadding(new Insets(15, 12, 15, 12)); vBox.setStyle("-fx-background-color: #00bcf0; "); - titleLabel = new Label("Transaction History"); + titleLabel = new Label(); + titleLabel.textProperty().bind(lm.bindString("historyTitle")); titleLabel.setStyle("-fx-font-size: 20px; -fx-font-weight: bold; -fx-text-fill: #303539;"); VBox titleBox = new VBox(titleLabel); @@ -73,28 +79,50 @@ private VBox centerPane() { historyListView.setMaxHeight(Double.MAX_VALUE); historyListView.setCellFactory(list -> new ListCell<>() { + private Subscription langSubscription; + @Override protected void updateItem(Transaction item, boolean empty) { super.updateItem(item, empty); + if (langSubscription != null) { + langSubscription.unsubscribe(); + langSubscription = null; + } + if (empty || item == null) { setText(null); setStyle(""); } else { - String type = (item instanceof Purchase) ? "PURCHASE" : "SALE"; - String symbol = item.getShare().getStock().getSymbol(); - BigDecimal quantity = item.getShare().getQuantity(); - BigDecimal total = item.getCalculator().calculateTotal(); - - String cellText = String.format("Week %d | %s | %s | Qty: %s | Total: $%s", - item.getWeek(), - type, - symbol, - FormatBigDecimal.formatNumber(quantity), - FormatBigDecimal.formatNumber(total) - ); - - setText(cellText); + Runnable updateTextAction = () -> { + String typeStr; + // Changed to allow for more transaction types in the future + if (item instanceof Purchase) { + typeStr = lm.getString("historyTypePurchase"); + } else if (item instanceof Sale) { + typeStr = lm.getString("historyTypeSale"); + } else { + typeStr = "ERROR"; + } + + BigDecimal quantity = item.getShare().getQuantity(); + BigDecimal total = item.getCalculator().calculateTotal(); + String symbol = item.getShare().getStock().getSymbol(); + + String template = lm.getString("historyCellFormat"); + + setText(String.format(template, + item.getWeek(), + typeStr, + symbol, + FormatBigDecimal.formatNumber(quantity), + FormatBigDecimal.formatNumber(total) + )); + }; + + langSubscription = lm.getBundleProperty().subscribe(bundle -> updateTextAction.run()); + + updateTextAction.run(); if (item instanceof Purchase) { setStyle("-fx-text-fill: #c0392b; -fx-font-weight: bold;"); @@ -105,6 +133,7 @@ protected void updateItem(Transaction item, boolean empty) { } }); + VBox historyListBox = new VBox(historyListView); historyListBox.setStyle("-fx-background-color: #ffe556;" + "-fx-text-fill: #303539;" + diff --git a/millions/src/main/resources/i18n/lang.properties b/millions/src/main/resources/i18n/lang.properties new file mode 100644 index 0000000..ac7a7dd --- /dev/null +++ b/millions/src/main/resources/i18n/lang.properties @@ -0,0 +1,62 @@ +# Default file to use if there is an error with language locale + +# StartView +newGame = New Game +languageButtonLabel = Toggle Language +quitGame = Quit Game + +# PlayerNameChooserView +playerNameConfirmButton = Confirm +playerNameInfo = Please enter your player name: + +# PlayerStartingMoneyChooserView +playerStartingMoneyConfirmButton = Confirm +playerStartingMoneyInfoLabel = Select your starting money balance for trading: + +# CSVChooserView +csvHeader = CSV File Required +csvContent = Please select a .csv file of the stocks to be used for the exchange in the next window.\n\nIf none is selected (or if .csv file is empty/malformed), a default file will be used.\n\nThe file needs to be formated as: StockTicker,CorporationName,PriceTypeDouble.\n\nExample: NVDA,Nvidia,191.27\n\n'#' signifies comments, and along with lines not formatted correctly, will be ignored. +csvFileChooserTitle = Select CSV File +csvFileFilterLabel = CSV Files + +# NavigationBar +navMoneyLabelText = Money: +navNetWorthLabelText = Net Worth: +navPlayerLabelText = Player: +marketButton = Market +portfolioButton = Portfolio +historyButton = History + +# FooterBar +currentWeekLabelText = Current Week: +advanceWeekButton = Advance Week + +# MarketView +marketTitle = Stock Market +xAxisLabel = Week +yAxisLabel = Price +selectedStock = Selected Stock: +purchaseQuantity = Quantity: +purchaseGross = Gross: +purchaseCommission = Commission: +purchaseTotal = Total: +purchaseButton = Purchase +stockCellFormat=%s - %s | Price: $%s | Price change: %s | Highest price: %s | Lowest price: %s + +# PortfolioView +portfolioTitle = Portfolio +selectedShare = Selected Share: +portfolioGross = Gross: +portfolioCommission = Commission: +portfolioTax = Tax: +portfolioTotal = Total: +sellButton = Sell +portfolioShareQuantity = Qty: +portfolioBuyPrice = Buy price: +portfolioCurrentPrice = Current Price: + +# TransactionArchiveView +historyTitle=Transaction History +historyTypePurchase=Purchase +historyTypeSale=Sale +historyCellFormat=Week %d | %s | %s | Qty: %s | Total: $%s diff --git a/millions/src/main/resources/i18n/lang_en.properties b/millions/src/main/resources/i18n/lang_en.properties new file mode 100644 index 0000000..3e162bc --- /dev/null +++ b/millions/src/main/resources/i18n/lang_en.properties @@ -0,0 +1,62 @@ +# File for the english language + +# StartView +newGame = New Game +languageButtonLabel = Toggle Language +quitGame = Quit Game + +# PlayerNameChooserView +playerNameConfirmButton = Confirm +playerNameInfo = Please enter your player name: + +# PlayerStartingMoneyChooserView +playerStartingMoneyConfirmButton = Confirm +playerStartingMoneyInfoLabel = Select your starting money balance for trading: + +# CSVChooserView +csvHeader = CSV File Required +csvContent = Please select a .csv file of the stocks to be used for the exchange in the next window.\n\nIf none is selected (or if .csv file is empty/malformed), a default file will be used.\n\nThe file needs to be formated as: StockTicker,CorporationName,PriceTypeDouble.\n\nExample: NVDA,Nvidia,191.27\n\n'#' signifies comments, and along with lines not formatted correctly, will be ignored. +csvFileChooserTitle = Select CSV File +csvFileFilterLabel = CSV Files + +# NavigationBar +navMoneyLabelText = Money: +navNetWorthLabelText = Net Worth: +navPlayerLabelText = Player: +marketButton = Market +portfolioButton = Portfolio +historyButton = History + +# FooterBar +currentWeekLabelText = Current Week: +advanceWeekButton = Advance Week + +# MarketView +marketTitle = Stock Market +xAxisLabel = Week +yAxisLabel = Price +selectedStock = Selected Stock: +purchaseQuantity = Quantity: +purchaseGross = Gross: +purchaseCommission = Commission: +purchaseTotal = Total: +purchaseButton = Purchase +stockCellFormat=%s - %s | Price: $%s | Price change: %s | Highest price: %s | Lowest price: %s + +# PortfolioView +portfolioTitle = Portfolio +selectedShare = Selected Share: +portfolioGross = Gross: +portfolioCommission = Commission: +portfolioTax = Tax: +portfolioTotal = Total: +sellButton = Sell +portfolioShareQuantity = Qty: +portfolioBuyPrice = Buy price: +portfolioCurrentPrice = Current Price: + +# TransactionArchiveView +historyTitle=Transaction History +historyTypePurchase=Purchase +historyTypeSale=Sale +historyCellFormat=Week %d | %s | %s | Qty: %s | Total: $%s diff --git a/millions/src/main/resources/i18n/lang_no.properties b/millions/src/main/resources/i18n/lang_no.properties new file mode 100644 index 0000000..63261ec --- /dev/null +++ b/millions/src/main/resources/i18n/lang_no.properties @@ -0,0 +1,62 @@ +# File for the norwegian language + +# StartView +newGame = Nytt Spill +languageButtonLabel = Skift Språk +quitGame = Avslutt + +# PlayerNameChooserView +playerNameConfirmButton = Bekreft +playerNameInfo = Vennligst skriv spillernavnet ditt: + +# PlayerStartingMoneyChooserView +playerStartingMoneyConfirmButton = Bekreft +playerStartingMoneyInfoLabel = Velg startsaldo for aksjehandel: + +# CSVChooserView +csvHeader = CSV Fil Påkrevd +csvContent = Vennligst velg en .csv fil som inneholder aksjene som skal brukes på børsen i neste vindu. \n\nDersom ingen fil velges (eller .csv filen er tom eller har feil struktur), vil en standard fil bli brukt. \n\nFilen må formates slik: Aksjesymbol,FirmaNavn,PrisTypeDobbel. \n\nEksempel: NVDA,Nvidia,191.27\n\n'#' brukes for kommentarer, og som med feilformaterte linjer, vil slike linjer bli ignorert. +csvFileChooserTitle = Velg CSV fil +csvFileFilterLabel = CSV Filer + +# NavigationBar +navMoneyLabelText = Saldo: +navNetWorthLabelText = Nettoformue: +navPlayerLabelText = Spiller: +marketButton = Marked +portfolioButton = Portefølje +historyButton = Historikk + +# FooterBar +currentWeekLabelText = Gjeldende Uke: +advanceWeekButton = Neste Uke + +# MarketView +marketTitle = Aksjemarked +xAxisLabel = Uke +yAxisLabel = Pris +selectedStock = Valgt aksje: +purchaseQuantity = Antall: +purchaseGross = Brutto: +purchaseCommission = Kurtasje: +purchaseTotal = Total: +purchaseButton = Kjøp +stockCellFormat=%s - %s | Pris: $%s | Prisendring: %s | Høyeste pris: %s | Laveste pris: %s + +# PortfolioView +portfolioTitle = Portefølje +selectedShare = Valgt andel: +portfolioGross = Brutto: +portfolioCommission = Kurtasje: +portfolioTax = Skatt: +portfolioTotal = Total: +sellButton = Selg +portfolioShareQuantity = Ant: +portfolioBuyPrice = Kjøpspris: +portfolioCurrentPrice = Gjeldende pris: + +# TransactionArchiveView +historyTitle=Transaksjonshistorikk +historyTypePurchase=Kjøp +historyTypeSale=Salg +historyCellFormat=Uke %d | %s | %s | Ant: %s | Total: $%s \ No newline at end of file diff --git a/millions/src/test/java/no/ntnu/gruppe53/service/LanguageManagerTest.java b/millions/src/test/java/no/ntnu/gruppe53/service/LanguageManagerTest.java new file mode 100644 index 0000000..4f04ae2 --- /dev/null +++ b/millions/src/test/java/no/ntnu/gruppe53/service/LanguageManagerTest.java @@ -0,0 +1,68 @@ +package no.ntnu.gruppe53.service; + +import javafx.beans.binding.StringBinding; +import javafx.beans.property.ObjectProperty; +import org.junit.jupiter.api.Test; + +import java.lang.reflect.Constructor; +import java.util.Locale; + +import static org.junit.jupiter.api.Assertions.*; + +class LanguageManagerTest { + + @Test + void constructorShouldBePrivate() throws Exception { + Constructor constructor = + LanguageManager.class.getDeclaredConstructor(); + + assertTrue(java.lang.reflect.Modifier.isPrivate(constructor.getModifiers())); + } + + @Test + void instanceShouldBeUniform() { + var lm1 = LanguageManager.getInstance(); + var lm2 = LanguageManager.getInstance(); + + assertEquals(lm1, lm2, "The instance should be unchanged."); + } + + @Test + void shouldLoadStringsCorrectly() { + LanguageManager manager = LanguageManager.getInstance(); + + manager.setLocale(new Locale("en")); + + String value = manager.getString("hello"); + + assertEquals("Hello world", value); + } + + @Test + void shouldReturnCorrectLocale() { + LanguageManager manager = LanguageManager.getInstance(); + + manager.setLocale(new Locale("en")); + + assertEquals(new Locale("en"), manager.getLocale()); + } + + @Test + void getBundlePropertyShouldBeObservable() { + assertInstanceOf(ObjectProperty.class, LanguageManager.getInstance().getBundleProperty()); + } + + @Test + void bindStringShouldBeReactiveOnLocaleChange() { + LanguageManager lm = LanguageManager.getInstance(); + + lm.setLocale(new Locale("en")); + StringBinding binding = lm.bindString("hello"); + + assertEquals("Hello world", binding.get(), "Should return english version of key."); + + lm.setLocale(new java.util.Locale("no")); + + assertEquals("Hallo verden", binding.get(), "Should return norwegian version of key."); + } +} \ No newline at end of file diff --git a/millions/src/test/java/no/ntnu/gruppe53/service/LanguageRegistryTest.java b/millions/src/test/java/no/ntnu/gruppe53/service/LanguageRegistryTest.java new file mode 100644 index 0000000..ab72057 --- /dev/null +++ b/millions/src/test/java/no/ntnu/gruppe53/service/LanguageRegistryTest.java @@ -0,0 +1,37 @@ +package no.ntnu.gruppe53.service; + +import org.junit.jupiter.api.Test; + +import java.util.Locale; + +import static org.junit.jupiter.api.Assertions.*; + +class LanguageRegistryTest { + @Test + void getLanguageShouldReturnCorrectAmountAndCorrectLanguages() { + var languages = LanguageRegistry.getLanguages(); + boolean hasEnglish = languages.stream() + .anyMatch(lang -> lang.locale().equals(new Locale("en"))); + boolean hasNorwegian = languages.stream() + .anyMatch(lang -> lang.locale().equals(new Locale("no"))); + + assertEquals(2, languages.size(), "Should be 2 languages."); + assertTrue(hasEnglish, "English should be a supported language."); + assertTrue(hasNorwegian, "Norwegian should be a supported language."); + } + + @Test + void getDefaultLanguageShouldEqualEnglish() { + assertEquals("English", LanguageRegistry.getDefaultLanguage().name(), + "Default language name should be 'English'."); + assertEquals(new Locale("en"), LanguageRegistry.getDefaultLanguage().locale(), + "Default language locale should be 'en'."); + } + + @Test + void getDefaultLocaleShouldReturnEnglish() { + assertEquals(new Locale("en"), LanguageRegistry.getDefaultLocale(), + "Default language locale should be 'en'."); + } + +} \ No newline at end of file diff --git a/millions/src/test/resources/i18n/lang_en.properties b/millions/src/test/resources/i18n/lang_en.properties new file mode 100644 index 0000000..6d5d7ab --- /dev/null +++ b/millions/src/test/resources/i18n/lang_en.properties @@ -0,0 +1 @@ +hello = Hello world \ No newline at end of file diff --git a/millions/src/test/resources/i18n/lang_no.properties b/millions/src/test/resources/i18n/lang_no.properties new file mode 100644 index 0000000..05bdb92 --- /dev/null +++ b/millions/src/test/resources/i18n/lang_no.properties @@ -0,0 +1 @@ +hello = Hallo verden \ No newline at end of file