From 8ae7510219e526d00b0d5f23b66d6ffd65055dff Mon Sep 17 00:00:00 2001 From: lieght <49651652+BAFGdeJong@users.noreply.github.com> Date: Wed, 1 Oct 2025 16:55:40 +0200 Subject: [PATCH] Added ability to load in BundledResource for localization --- app/src/main/java/org/toop/app/App.java | 8 +- .../java/org/toop/app/menu/CreditsMenu.java | 4 +- .../main/java/org/toop/app/menu/MainMenu.java | 23 +++-- app/src/main/java/org/toop/app/menu/Menu.java | 9 +- .../java/org/toop/app/menu/OptionsMenu.java | 4 +- .../main/java/org/toop/app/menu/QuitMenu.java | 11 +-- .../localization/localization.properties} | 0 .../localization/localization_nl.properties} | 0 .../org/toop/framework/asset/AssetLoader.java | 86 ++++++++++++------- .../toop/framework/asset/AssetManager.java | 18 ++++ .../asset/resources/BundledResource.java | 15 ++++ .../asset/resources/LocalizationAsset.java | 69 ++++++++------- 12 files changed, 157 insertions(+), 90 deletions(-) rename app/src/main/resources/{Localization.properties => assets/localization/localization.properties} (100%) rename app/src/main/resources/{Localization_nl.properties => assets/localization/localization_nl.properties} (100%) create mode 100644 framework/src/main/java/org/toop/framework/asset/resources/BundledResource.java diff --git a/app/src/main/java/org/toop/app/App.java b/app/src/main/java/org/toop/app/App.java index db9683d..931bd2c 100644 --- a/app/src/main/java/org/toop/app/App.java +++ b/app/src/main/java/org/toop/app/App.java @@ -8,6 +8,8 @@ import javafx.application.Application; import javafx.scene.layout.StackPane; import javafx.scene.Scene; import javafx.stage.Stage; +import org.toop.framework.asset.AssetManager; +import org.toop.framework.asset.resources.LocalizationAsset; import org.toop.local.AppContext; import java.util.Locale; @@ -18,8 +20,8 @@ public class App extends Application { private static Scene scene; private static StackPane root; - private Locale currentLocale = AppContext.getLocale(); - private ResourceBundle resourceBundle = ResourceBundle.getBundle("Localization", currentLocale); + private Locale currentLocale = AppContext.getLocale(); + private LocalizationAsset loc = AssetManager.get("localization.properties"); public static void run(String[] args) { launch(args); @@ -30,7 +32,7 @@ public class App extends Application { final StackPane root = new StackPane(new MainMenu().getPane()); final Scene scene = new Scene(root); - stage.setTitle(resourceBundle.getString("windowTitle")); + stage.setTitle(loc.getString("windowTitle", currentLocale)); stage.setMinWidth(1080); stage.setMinHeight(720); diff --git a/app/src/main/java/org/toop/app/menu/CreditsMenu.java b/app/src/main/java/org/toop/app/menu/CreditsMenu.java index 385bf42..e2f0b5f 100644 --- a/app/src/main/java/org/toop/app/menu/CreditsMenu.java +++ b/app/src/main/java/org/toop/app/menu/CreditsMenu.java @@ -1,5 +1,7 @@ package org.toop.app.menu; +import org.toop.framework.asset.AssetManager; +import org.toop.framework.asset.resources.LocalizationAsset; import org.toop.local.AppContext; import java.util.Locale; @@ -7,7 +9,7 @@ import java.util.ResourceBundle; public final class CreditsMenu extends Menu { private Locale currentLocale = AppContext.getLocale(); - private ResourceBundle resourceBundle = ResourceBundle.getBundle("Localization", currentLocale); + private LocalizationAsset loc = AssetManager.get("localization.properties"); public CreditsMenu() { } } \ No newline at end of file diff --git a/app/src/main/java/org/toop/app/menu/MainMenu.java b/app/src/main/java/org/toop/app/menu/MainMenu.java index 7874ba3..c5a39ca 100644 --- a/app/src/main/java/org/toop/app/menu/MainMenu.java +++ b/app/src/main/java/org/toop/app/menu/MainMenu.java @@ -4,31 +4,30 @@ import javafx.geometry.Pos; import javafx.scene.control.Button; import javafx.scene.image.ImageView; import javafx.scene.layout.*; -import org.toop.local.AppContext; +import org.toop.framework.asset.resources.LocalizationAsset; import java.util.Locale; -import java.util.ResourceBundle; import org.toop.framework.asset.AssetManager; import org.toop.framework.asset.resources.CssAsset; import org.toop.framework.asset.resources.ImageAsset; public final class MainMenu extends Menu { - private Locale currentLocale = AppContext.getLocale(); - private ResourceBundle resourceBundle = ResourceBundle.getBundle("Localization", currentLocale); + private Locale currentLocale = Locale.of("nl"); + private LocalizationAsset loc = AssetManager.get("localization.properties"); public MainMenu() { - final Button tictactoe = createButton(resourceBundle.getString("mainMenuSelectTicTacToe"), () -> {}); - final Button reversi = createButton(resourceBundle.getString("mainMenuSelectReversi"), () -> {}); - final Button sudoku = createButton(resourceBundle.getString("mainMenuSelectSudoku"), () -> {}); - final Button battleship = createButton(resourceBundle.getString("mainMenuSelectBattleship"), () -> {}); - final Button other = createButton(resourceBundle.getString("mainMenuSelectOther"), () -> {}); + final Button tictactoe = createButton(loc.getString("mainMenuSelectTicTacToe", currentLocale), () -> {}); + final Button reversi = createButton(loc.getString("mainMenuSelectReversi", currentLocale), () -> {}); + final Button sudoku = createButton(loc.getString("mainMenuSelectSudoku", currentLocale), () -> {}); + final Button battleship = createButton(loc.getString("mainMenuSelectBattleship", currentLocale), () -> {}); + final Button other = createButton(loc.getString("mainMenuSelectOther", currentLocale), () -> {}); final VBox gamesBox = new VBox(tictactoe, reversi, sudoku, battleship, other); gamesBox.setAlignment(Pos.TOP_CENTER); - final Button credits = createButton(resourceBundle.getString("mainMenuSelectCredits"), () -> {}); - final Button options = createButton(resourceBundle.getString("mainMenuSelectOptions"), () -> {}); - final Button quit = createButton(resourceBundle.getString("mainMenuSelectQuit"), () -> {}); + final Button credits = createButton(loc.getString("mainMenuSelectCredits", currentLocale), () -> {}); + final Button options = createButton(loc.getString("mainMenuSelectOptions", currentLocale), () -> {}); + final Button quit = createButton(loc.getString("mainMenuSelectQuit", currentLocale), () -> {}); final VBox creditsBox = new VBox(credits, options, quit); creditsBox.setAlignment(Pos.BOTTOM_CENTER); diff --git a/app/src/main/java/org/toop/app/menu/Menu.java b/app/src/main/java/org/toop/app/menu/Menu.java index fba8208..8a75209 100644 --- a/app/src/main/java/org/toop/app/menu/Menu.java +++ b/app/src/main/java/org/toop/app/menu/Menu.java @@ -6,16 +6,19 @@ import javafx.animation.FadeTransition; import javafx.scene.control.Button; import javafx.scene.layout.Pane; import javafx.util.Duration; +import org.toop.framework.asset.Asset; +import org.toop.framework.asset.AssetManager; +import org.toop.framework.asset.resources.LocalizationAsset; import org.toop.local.AppContext; +import java.util.ArrayList; import java.util.Locale; -import java.util.ResourceBundle; public abstract class Menu { protected Pane pane; public Pane getPane() { return pane; } - private Locale currentLocale = AppContext.getLocale(); - private ResourceBundle resourceBundle = ResourceBundle.getBundle("Localization", currentLocale); + private Locale currentLocale = AppContext.getLocale(); + private LocalizationAsset loc = AssetManager.get("localization.properties"); public void fadeBackgroundImage(String imagePath, float from, float to, float milliseconds) { final FadeTransition fade = new FadeTransition(Duration.millis(milliseconds), App.getRoot()); diff --git a/app/src/main/java/org/toop/app/menu/OptionsMenu.java b/app/src/main/java/org/toop/app/menu/OptionsMenu.java index 2d2bd64..ea21dcb 100644 --- a/app/src/main/java/org/toop/app/menu/OptionsMenu.java +++ b/app/src/main/java/org/toop/app/menu/OptionsMenu.java @@ -1,5 +1,7 @@ package org.toop.app.menu; +import org.toop.framework.asset.AssetManager; +import org.toop.framework.asset.resources.LocalizationAsset; import org.toop.local.AppContext; import java.util.Locale; @@ -7,7 +9,7 @@ import java.util.ResourceBundle; public final class OptionsMenu extends Menu { private Locale currentLocale = AppContext.getLocale(); - private ResourceBundle resourceBundle = ResourceBundle.getBundle("Localization", currentLocale); + private LocalizationAsset loc = AssetManager.get("localization.properties"); public OptionsMenu() { } } \ No newline at end of file diff --git a/app/src/main/java/org/toop/app/menu/QuitMenu.java b/app/src/main/java/org/toop/app/menu/QuitMenu.java index 83cbf46..3916dfc 100644 --- a/app/src/main/java/org/toop/app/menu/QuitMenu.java +++ b/app/src/main/java/org/toop/app/menu/QuitMenu.java @@ -10,29 +10,30 @@ import javafx.scene.text.Text; import org.toop.app.App; import org.toop.framework.asset.AssetManager; import org.toop.framework.asset.resources.CssAsset; +import org.toop.framework.asset.resources.LocalizationAsset; import org.toop.local.AppContext; import java.util.Locale; import java.util.ResourceBundle; public final class QuitMenu extends Menu { - private Locale currentLocale = AppContext.getLocale(); - private ResourceBundle resourceBundle = ResourceBundle.getBundle("Localization", currentLocale); + private Locale currentLocale = AppContext.getLocale(); + private LocalizationAsset loc = AssetManager.get("localization.properties"); public QuitMenu() { final Region background = new Region(); background.getStyleClass().add("quit-background"); background.setPrefSize(Double.MAX_VALUE, Double.MAX_VALUE); - final Text sure = new Text(resourceBundle.getString("quitMenuTextSure")); + final Text sure = new Text(loc.getString("quitMenuTextSure", currentLocale)); sure.getStyleClass().add("quit-text"); - final Button yes = new Button(resourceBundle.getString("quitMenuButtonYes")); + final Button yes = new Button(loc.getString("quitMenuButtonYes", currentLocale)); yes.getStyleClass().add("quit-button"); yes.setOnAction(_ -> { App.quit(); }); - final Button no = new Button(resourceBundle.getString("quitMenuButtonNo")); + final Button no = new Button(loc.getString("quitMenuButtonNo", currentLocale)); no.getStyleClass().add("quit-button"); no.setOnAction(_ -> { App.pop(); diff --git a/app/src/main/resources/Localization.properties b/app/src/main/resources/assets/localization/localization.properties similarity index 100% rename from app/src/main/resources/Localization.properties rename to app/src/main/resources/assets/localization/localization.properties diff --git a/app/src/main/resources/Localization_nl.properties b/app/src/main/resources/assets/localization/localization_nl.properties similarity index 100% rename from app/src/main/resources/Localization_nl.properties rename to app/src/main/resources/assets/localization/localization_nl.properties diff --git a/framework/src/main/java/org/toop/framework/asset/AssetLoader.java b/framework/src/main/java/org/toop/framework/asset/AssetLoader.java index 6d632fc..aef50bf 100644 --- a/framework/src/main/java/org/toop/framework/asset/AssetLoader.java +++ b/framework/src/main/java/org/toop/framework/asset/AssetLoader.java @@ -3,30 +3,27 @@ package org.toop.framework.asset; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.toop.framework.asset.events.AssetEvents; -import org.toop.framework.asset.resources.BaseResource; +import org.toop.framework.asset.resources.*; +import org.toop.framework.eventbus.EventFlow; +import org.reflections.Reflections; import java.io.File; import java.util.*; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Function; -import org.reflections.Reflections; -import org.toop.framework.asset.resources.FileExtension; -import org.toop.framework.asset.resources.FontAsset; -import org.toop.framework.eventbus.EventFlow; - public class AssetLoader { private static final Logger logger = LogManager.getLogger(AssetLoader.class); - private final List> assets = new CopyOnWriteArrayList<>(); + private static final List> assets = new CopyOnWriteArrayList<>(); private final Map> registry = new ConcurrentHashMap<>(); - private volatile int loadedCount = 0; - private volatile int totalCount = 0; + private final AtomicInteger loadedCount = new AtomicInteger(0); + private int totalCount = 0; public AssetLoader(File rootFolder) { - autoRegisterResources(); // make sure resources are registered! - + autoRegisterResources(); List foundFiles = new ArrayList<>(); fileSearcher(rootFolder, foundFiles); this.totalCount = foundFiles.size(); @@ -38,19 +35,19 @@ public class AssetLoader { } public double getProgress() { - return (this.totalCount == 0) ? 1.0 : (this.loadedCount / (double) this.totalCount); + return (totalCount == 0) ? 1.0 : (loadedCount.get() / (double) totalCount); } public int getLoadedCount() { - return this.loadedCount; + return loadedCount.get(); } public int getTotalCount() { - return this.totalCount; + return totalCount; } public List> getAssets() { - return new ArrayList<>(this.assets); + return new ArrayList<>(assets); } public void register(String extension, Function factory) { @@ -59,8 +56,7 @@ public class AssetLoader { private T resourceMapper(File file, Class type) { String ext = getExtension(file.getName()); - Function factory = this.registry.get(ext); - + Function factory = registry.get(ext); if (factory == null) return null; BaseResource resource = factory.apply(file); @@ -70,32 +66,49 @@ public class AssetLoader { "File " + file.getName() + " is not of type " + type.getSimpleName() ); } - return type.cast(resource); } private void loader(List files) { + Map bundledResources = new HashMap<>(); + for (File file : files) { BaseResource resource = resourceMapper(file, BaseResource.class); - if (resource != null) { - Asset asset = new Asset<>(file.getName(), resource); - this.assets.add(asset); - - if (resource instanceof FontAsset fontAsset) { - fontAsset.load(); + switch (resource) { + case null -> { + continue; + } + case BundledResource br -> { + String key = resource.getClass().getName() + ":" + br.getBaseName(); + if (bundledResources.containsKey(key)) { + bundledResources.get(key).loadFile(file); + resource = (BaseResource) bundledResources.get(key); + } else { + br.loadFile(file); + bundledResources.put(key, br); + } + } + case FontAsset fontAsset -> fontAsset.load(); + default -> { } - - logger.info("Loaded {} from {}", resource.getClass().getSimpleName(), file.getAbsolutePath()); - - this.loadedCount++; // TODO: Fix non atmomic operation - new EventFlow() - .addPostEvent(new AssetEvents.LoadingProgressUpdate(this.loadedCount, this.totalCount)) - .postEvent(); } + + BaseResource finalResource = resource; + boolean alreadyAdded = assets.stream() + .anyMatch(a -> a.getResource() == finalResource); + if (!alreadyAdded) { + assets.add(new Asset<>(file.getName(), resource)); + } + + logger.info("Loaded {} from {}", resource.getClass().getSimpleName(), file.getAbsolutePath()); + loadedCount.incrementAndGet(); + new EventFlow() + .addPostEvent(new AssetEvents.LoadingProgressUpdate(loadedCount.get(), totalCount)) + .postEvent(); } - logger.info("Loaded {} assets", files.size()); } + private void fileSearcher(final File folder, List foundFiles) { for (File fileEntry : Objects.requireNonNull(folder.listFiles())) { if (fileEntry.isDirectory()) { @@ -114,7 +127,7 @@ public class AssetLoader { if (!cls.isAnnotationPresent(FileExtension.class)) continue; FileExtension annotation = cls.getAnnotation(FileExtension.class); for (String ext : annotation.value()) { - this.registry.put(ext, file -> { + registry.put(ext, file -> { try { return cls.getConstructor(File.class).newInstance(file); } catch (Exception e) { @@ -125,6 +138,13 @@ public class AssetLoader { } } + private static String getBaseName(String fileName) { + int underscoreIndex = fileName.indexOf('_'); + int dotIndex = fileName.lastIndexOf('.'); + if (underscoreIndex > 0) return fileName.substring(0, underscoreIndex); + return fileName.substring(0, dotIndex); + } + public static String getExtension(String name) { int i = name.lastIndexOf('.'); return (i > 0) ? name.substring(i + 1) : ""; diff --git a/framework/src/main/java/org/toop/framework/asset/AssetManager.java b/framework/src/main/java/org/toop/framework/asset/AssetManager.java index fbb7584..4572091 100644 --- a/framework/src/main/java/org/toop/framework/asset/AssetManager.java +++ b/framework/src/main/java/org/toop/framework/asset/AssetManager.java @@ -61,4 +61,22 @@ public class AssetManager { assets.put(asset.getName(), asset); } +// public static LocalizationAsset getLocalization(Locale locale) { +// for (Asset asset : assets.values()) { +// if (asset.getResource() instanceof LocalizationAsset locAsset) { +// if (!locAsset.isLoaded()) locAsset.load(); +// if (locAsset.hasLocale(locale)) return locAsset; +// } +// } +// // fallback NL +// Locale fallback = Locale.forLanguageTag("nl"); +// for (Asset asset : assets.values()) { +// if (asset.getResource() instanceof LocalizationAsset locAsset) { +// if (!locAsset.isLoaded()) locAsset.load(); +// if (locAsset.hasLocale(fallback)) return locAsset; +// } +// } +// throw new NoSuchElementException("No localization asset available for locale: " + locale + " or fallback: nl"); +// } + } \ No newline at end of file diff --git a/framework/src/main/java/org/toop/framework/asset/resources/BundledResource.java b/framework/src/main/java/org/toop/framework/asset/resources/BundledResource.java new file mode 100644 index 0000000..b32d3cf --- /dev/null +++ b/framework/src/main/java/org/toop/framework/asset/resources/BundledResource.java @@ -0,0 +1,15 @@ +package org.toop.framework.asset.resources; + +import java.io.File; + +public interface BundledResource { + /** + * Load or merge an additional file into this resource. + */ + void loadFile(File file); + + /** + * Return a base name for grouping multiple files into this single resource. + */ + String getBaseName(); +} \ No newline at end of file diff --git a/framework/src/main/java/org/toop/framework/asset/resources/LocalizationAsset.java b/framework/src/main/java/org/toop/framework/asset/resources/LocalizationAsset.java index 27d9b83..39862c4 100644 --- a/framework/src/main/java/org/toop/framework/asset/resources/LocalizationAsset.java +++ b/framework/src/main/java/org/toop/framework/asset/resources/LocalizationAsset.java @@ -5,9 +5,10 @@ import java.nio.charset.StandardCharsets; import java.util.*; @FileExtension({"properties"}) -public class LocalizationAsset extends BaseResource implements LoadableResource { +public class LocalizationAsset extends BaseResource implements LoadableResource, BundledResource { private final Map bundles = new HashMap<>(); private boolean isLoaded = false; + private final Locale fallback = Locale.forLanguageTag(""); public LocalizationAsset(File file) { super(file); @@ -15,53 +16,57 @@ public class LocalizationAsset extends BaseResource implements LoadableResource @Override public void load() { - // Convention: file names like messages_en.properties, ui_de.properties, etc. - String baseName = getBaseName(getFile().getName()); - - // Scan the parent folder for all matching *.properties with same basename - File folder = getFile().getParentFile(); - File[] files = folder.listFiles((dir, name) -> - name.startsWith(baseName) && name.endsWith(".properties")); - - if (files != null) { - for (File f : files) { - Locale locale = extractLocale(f.getName(), baseName); - try (InputStreamReader reader = - new InputStreamReader(new FileInputStream(f), StandardCharsets.UTF_8)) { - this.bundles.put(locale, new PropertyResourceBundle(reader)); - } catch (IOException e) { - throw new RuntimeException("Failed to load localization file: " + f, e); - } - } - } - - this.isLoaded = true; + loadFile(getFile()); + isLoaded = true; } @Override public void unload() { - this.bundles.clear(); - this.isLoaded = false; + bundles.clear(); + isLoaded = false; } @Override public boolean isLoaded() { - return this.isLoaded; + return isLoaded; } public String getString(String key, Locale locale) { - ResourceBundle bundle = this.bundles.get(locale); + Locale target = findBestLocale(locale); + ResourceBundle bundle = bundles.get(target); if (bundle == null) throw new MissingResourceException( - "No bundle for locale: " + locale, getClass().getName(), key); + "No bundle for locale: " + target, getClass().getName(), key); return bundle.getString(key); } - public boolean hasLocale(Locale locale) { - return this.bundles.containsKey(locale); + private Locale findBestLocale(Locale locale) { + if (bundles.containsKey(locale)) return locale; + for (Locale l : bundles.keySet()) { + if (l.getLanguage().equals(locale.getLanguage())) return l; + } + return fallback; } public Set getAvailableLocales() { - return Collections.unmodifiableSet(this.bundles.keySet()); + return Collections.unmodifiableSet(bundles.keySet()); + } + + @Override + public void loadFile(File file) { + String baseName = getBaseName(file.getName()); + try (InputStreamReader reader = + new InputStreamReader(new FileInputStream(file), StandardCharsets.UTF_8)) { + Locale locale = extractLocale(file.getName(), baseName); + bundles.put(locale, new PropertyResourceBundle(reader)); + } catch (IOException e) { + throw new RuntimeException("Failed to load localization file: " + file, e); + } + isLoaded = true; + } + + @Override + public String getBaseName() { + return getBaseName(getFile().getName()); } private String getBaseName(String fileName) { @@ -80,6 +85,6 @@ public class LocalizationAsset extends BaseResource implements LoadableResource String localePart = fileName.substring(underscoreIndex + 1, dotIndex); return Locale.forLanguageTag(localePart.replace('_', '-')); } - return Locale.getDefault(); // fallback + return fallback; } -} \ No newline at end of file +}