From f54e07b984e30d31dc808a5e532c1de1cb90127d Mon Sep 17 00:00:00 2001 From: extio-de Date: Fri, 15 May 2026 16:32:18 +0200 Subject: [PATCH] Refactor Localization Service - Updated LocalizationService interface to load localization data from a Path instead of InputStream. - Enhanced LocalizationServiceImpl to handle loading and saving of localization data from directories. - Introduced LocalizationLanguage and LocalizationMetadata classes to encapsulate language entries and metadata. - Refactored Localizations class to manage language files and metadata more effectively. - Added unit tests for loading localizations from directory and verifying language translations. - Created YAML files for English and German languages along with a metadata file for localization information. --- README.md | 4 +- data/i18n/de.yaml | 13 + data/i18n/en.yaml | 13 + data/i18n/i18n.yaml | 42 ---- data/i18n/metadata.yaml | 9 + pom.xml | 2 +- .../i18n/LocalizationAutoConfiguration.java | 23 +- .../game_engine/i18n/LocalizationEditor.java | 76 ++---- .../i18n/LocalizationLanguage.java | 41 ++++ .../i18n/LocalizationMetadata.java | 44 ++++ .../game_engine/i18n/LocalizationService.java | 3 +- .../i18n/LocalizationServiceImpl.java | 205 ++++++++++++---- .../extio/game_engine/i18n/Localizations.java | 83 +++---- .../i18n/LocalizationManagerTest.java | 225 +++++++++--------- 14 files changed, 470 insertions(+), 313 deletions(-) create mode 100644 data/i18n/de.yaml create mode 100644 data/i18n/en.yaml delete mode 100644 data/i18n/i18n.yaml create mode 100644 data/i18n/metadata.yaml create mode 100644 src/main/java/de/extio/game_engine/i18n/LocalizationLanguage.java create mode 100644 src/main/java/de/extio/game_engine/i18n/LocalizationMetadata.java diff --git a/README.md b/README.md index cf9e760..10cd14c 100644 --- a/README.md +++ b/README.md @@ -1016,7 +1016,7 @@ Requires `StaticResourceService` and `StorageService` to be available. ### Internationalization (i18n) #### Purpose and Overview -A YAML-based localization system supporting multiple languages. Localizations are loaded from a structured YAML file containing translation entries organized by language code. The system supports both integer and string-based translation keys and provides fallback to default text when translations are missing. +A YAML-based localization system supporting multiple languages. Localizations are loaded from a directory bundle with one metadata file and one file per language, allowing metadata and translation entries to evolve independently. The system supports both integer and string-based translation keys and provides fallback to default text when translations are missing. Includes an integrated `LocalizationEditor` - a Swing-based GUI tool for managing translations, adding new languages, and editing localization entries during development. @@ -1026,7 +1026,7 @@ Includes an integrated `LocalizationEditor` - a Swing-based GUI tool for managin **Properties**: - `game-engine.i18n.enabled` (default: `true`) - `game-engine.i18n.load-on-start` (default: `true`) - Auto-load localizations on startup -- `game-engine.i18n.resource` (default: `i18n/i18n.yaml`) - Path to localization resource +- `game-engine.i18n.resource` (default: `i18n/metadata.yaml`) - Path to the localization metadata resource; sibling `*.yaml` files are loaded as individual languages #### Exposed Spring Beans - **`LocalizationService`**: Main interface for translation, language management, and localization editing diff --git a/data/i18n/de.yaml b/data/i18n/de.yaml new file mode 100644 index 0000000..0f49797 --- /dev/null +++ b/data/i18n/de.yaml @@ -0,0 +1,13 @@ +--- +shortName: "de" +entries: + test-1: "hallo" + test-2: "welt" + test-3: "Willkommen zu Exo's Spieleengine Demo!" + test-4: "Design auswählen" + test-5: "Dies ist eine Nachricht\\n123...\\nSchönen Tag noch!" + test-6: "Ok" + test-7: "Audio umschalten" + test-8: "ScrollArea Demo" + test-9: "Sprache auswählen" + test-10: "Aktuell: " \ No newline at end of file diff --git a/data/i18n/en.yaml b/data/i18n/en.yaml new file mode 100644 index 0000000..bb93f24 --- /dev/null +++ b/data/i18n/en.yaml @@ -0,0 +1,13 @@ +--- +shortName: "en" +entries: + test-1: "hello" + test-2: "world" + test-3: "Welcome to Exo's Game Engine Demo!" + test-4: "Select Theme" + test-5: "This is a message\\n123...\\nHave a nice day!" + test-6: "OK" + test-7: "Toggle Audio" + test-8: "ScrollArea Demo" + test-9: "Select Language" + test-10: "Current: " \ No newline at end of file diff --git a/data/i18n/i18n.yaml b/data/i18n/i18n.yaml deleted file mode 100644 index 3f25ae8..0000000 --- a/data/i18n/i18n.yaml +++ /dev/null @@ -1,42 +0,0 @@ ---- -curId: 10 -descriptions: - test-2: "" - test-1: "aaa" - test-8: "" - test-7: "" - test-6: "" - test-5: "" - test-4: "" - test-3: "" -languages: - en: - test-1: "hello" - test-2: "world" - test-3: "Welcome to Exo's Game Engine Demo!" - test-4: "Select Theme" - test-5: "This is a message\\n123...\\nHave a nice day!" - test-6: "OK" - test-7: "Toggle Audio" - test-8: "ScrollArea Demo" - test-9: "Select Language" - test-10: "Current: " - de: - test-1: "hallo" - test-2: "welt" - test-3: "Willkommen zu Exo's Spieleengine Demo!" - test-4: "Design auswählen" - test-5: "Dies ist eine Nachricht\\n123...\\nSchönen Tag noch!" - test-6: "Ok" - test-7: "Audio umschalten" - test-8: "ScrollArea Demo" - test-9: "Sprache auswählen" - test-10: "Aktuell: " -languagesInfo: - en: - name: "English" - shortName: "en" - de: - name: "Deutsch" - shortName: "de" -prefix: "test" diff --git a/data/i18n/metadata.yaml b/data/i18n/metadata.yaml new file mode 100644 index 0000000..ed0a235 --- /dev/null +++ b/data/i18n/metadata.yaml @@ -0,0 +1,9 @@ +--- +languagesInfo: + en: + name: "English" + shortName: "en" + de: + name: "Deutsch" + shortName: "de" +prefix: "test" \ No newline at end of file diff --git a/pom.xml b/pom.xml index a12732c..713c85b 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ 4.0.0 de.extio game-engine - 1.0.13 + 1.0.14 25 diff --git a/src/main/java/de/extio/game_engine/i18n/LocalizationAutoConfiguration.java b/src/main/java/de/extio/game_engine/i18n/LocalizationAutoConfiguration.java index 231927e..934a1a1 100644 --- a/src/main/java/de/extio/game_engine/i18n/LocalizationAutoConfiguration.java +++ b/src/main/java/de/extio/game_engine/i18n/LocalizationAutoConfiguration.java @@ -1,19 +1,13 @@ package de.extio.game_engine.i18n; -import java.io.File; -import java.net.URI; import java.nio.file.Path; -import java.util.stream.StreamSupport; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.context.annotation.Bean; -import de.extio.game_engine.resource.StaticResource; import de.extio.game_engine.resource.StaticResourceService; import de.extio.game_engine.storage.StorageService; @@ -21,24 +15,13 @@ import de.extio.game_engine.storage.StorageService; @ConditionalOnProperty(name = "game-engine.i18n.enabled", havingValue = "true", matchIfMissing = true) public class LocalizationAutoConfiguration { - private static final Logger LOGGER = LoggerFactory.getLogger(LocalizationServiceImpl.class); - @Bean @ConditionalOnMissingBean - LocalizationService localizationManager(@Value("${game-engine.i18n.load-on-start:true}") final boolean loadOnStart, @Value("${game-engine.i18n.resource:i18n/i18n.yaml}") final String resource, final StaticResourceService staticResourceService, final StorageService storageService) { - final var localizationManager = new LocalizationServiceImpl(storageService); + LocalizationService localizationManager(@Value("${game-engine.i18n.load-on-start:true}") final boolean loadOnStart, @Value("${game-engine.i18n.resource:i18n/metadata.yaml}") final String resource, final StaticResourceService staticResourceService, final StorageService storageService) { + final var localizationManager = new LocalizationServiceImpl(storageService, staticResourceService); if (loadOnStart) { - final var path = new File(resource).toPath(); - final var staticResource = new StaticResource(path.getParent() != null ? StreamSupport.stream(path.getParent().spliterator(), false).map(Path::toString).toList() : null, path.getFileName().toString()); - staticResourceService.loadStreamByPath(staticResource).ifPresent(stream -> { - try (stream) { - localizationManager.load(stream); - } - catch (final Exception e) { - LOGGER.error("An exception occured while loading localizations from resource: " + resource, e); - } - }); + localizationManager.load(Path.of(resource)); } return localizationManager; diff --git a/src/main/java/de/extio/game_engine/i18n/LocalizationEditor.java b/src/main/java/de/extio/game_engine/i18n/LocalizationEditor.java index fbf3a50..5ae1f6a 100644 --- a/src/main/java/de/extio/game_engine/i18n/LocalizationEditor.java +++ b/src/main/java/de/extio/game_engine/i18n/LocalizationEditor.java @@ -13,13 +13,9 @@ import java.awt.event.ActionListener; import java.awt.event.ComponentAdapter; import java.awt.event.ComponentEvent; import java.io.File; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; -import java.io.OutputStream; +import java.nio.file.Path; import java.lang.reflect.InvocationTargetException; import java.util.Iterator; -import java.util.LinkedHashMap; import java.util.Map; import javax.swing.JButton; @@ -41,8 +37,6 @@ import javax.swing.border.EmptyBorder; import javax.swing.filechooser.FileFilter; import javax.swing.table.AbstractTableModel; -import de.extio.game_engine.util.ObjectSerialization; - @SuppressWarnings("serial") public class LocalizationEditor extends JFrame { @@ -133,7 +127,7 @@ public class LocalizationEditor extends JFrame { * Create the frame. */ public LocalizationEditor() { - this.localizationManagerImpl = new LocalizationServiceImpl(null); + this.localizationManagerImpl = new LocalizationServiceImpl(null, null); this.setTitle("i18n Editor"); this.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE); @@ -169,20 +163,9 @@ public class LocalizationEditor extends JFrame { final File cwd = new File("."); final JFileChooser chooser = new JFileChooser(); chooser.setCurrentDirectory(cwd); - chooser.setFileFilter(new FileFilter() { - - @Override - public String getDescription() { - return "*.yaml"; - } - - @Override - public boolean accept(final File f) { - return !f.isFile() || f.getName().endsWith("yaml"); - } - }); + chooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY); - final int ret = chooser.showDialog(null, "Select file to import"); + final int ret = chooser.showDialog(null, "Select localization directory"); if (ret == JFileChooser.CANCEL_OPTION) { return; } @@ -191,10 +174,9 @@ public class LocalizationEditor extends JFrame { LocalizationEditor.this.localizationManagerImpl.reset(); try { - LocalizationEditor.this.localizationManagerImpl.reset(); - LocalizationEditor.this.localizationManagerImpl.load(new FileInputStream(fileName)); + LocalizationEditor.this.localizationManagerImpl.loadWoService(Path.of(fileName)); } - catch (final FileNotFoundException e1) { + catch (final Exception e1) { JOptionPane.showMessageDialog(null, "An exception occured while loading: " + e1.toString()); return; } @@ -239,10 +221,7 @@ public class LocalizationEditor extends JFrame { public void actionPerformed(final ActionEvent e) { try { LocalizationEditor.this.localizationManagerImpl.getLocalizations().setPrefix(LocalizationEditor.this.txtTextprefix.getText()); - - try (OutputStream output = new FileOutputStream(LocalizationEditor.this.lastFileName)) { - ObjectSerialization.serialize(LocalizationEditor.this.localizationManagerImpl.getLocalizations(), output, false, false, false, null, null); - } + LocalizationEditor.this.localizationManagerImpl.save(Path.of(LocalizationEditor.this.lastFileName)); } catch (final Exception exc) { JOptionPane.showMessageDialog(null, "An exception occured while saving: " + exc.toString()); @@ -263,18 +242,7 @@ public class LocalizationEditor extends JFrame { final File cwd = new File("."); final JFileChooser chooser = new JFileChooser(); chooser.setCurrentDirectory(cwd); - chooser.setFileFilter(new FileFilter() { - - @Override - public String getDescription() { - return "*.yaml"; - } - - @Override - public boolean accept(final File f) { - return !f.isFile() || f.getName().endsWith("yaml"); - } - }); + chooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY); final int ret = chooser.showSaveDialog(null); if (ret == JFileChooser.CANCEL_OPTION) { @@ -286,9 +254,7 @@ public class LocalizationEditor extends JFrame { LocalizationEditor.this.localizationManagerImpl.getLocalizations().setPrefix(LocalizationEditor.this.txtTextprefix.getText()); try { - try (OutputStream output = new FileOutputStream(fileName)) { - ObjectSerialization.serialize(LocalizationEditor.this.localizationManagerImpl.getLocalizations(), output, false, false, false, null, null); - } + LocalizationEditor.this.localizationManagerImpl.save(Path.of(fileName)); } catch (final Exception exc) { JOptionPane.showMessageDialog(null, "An exception occured while saving: " + exc.toString()); @@ -339,7 +305,7 @@ public class LocalizationEditor extends JFrame { public void actionPerformed(final ActionEvent e) { final String idStr = JOptionPane.showInputDialog(LocalizationEditor.this, "Enter localization id"); - final Map mapping = LocalizationEditor.this.localizationManagerImpl.getLocalizations().getLanguages().values().iterator().next(); + final Map mapping = LocalizationEditor.this.localizationManagerImpl.getLocalizations().getLanguageFiles().values().iterator().next().getEntries(); int idx = 0; for (final String mid : mapping.keySet()) { if (mid.equals(idStr)) { @@ -361,10 +327,10 @@ public class LocalizationEditor extends JFrame { public void actionPerformed(final ActionEvent e) { final String search = JOptionPane.showInputDialog(LocalizationEditor.this, "Enter search term"); - final Iterator> it = LocalizationEditor.this.localizationManagerImpl.getLocalizations().getLanguages().values().iterator(); + final Iterator it = LocalizationEditor.this.localizationManagerImpl.getLocalizations().getLanguageFiles().values().iterator(); while (it.hasNext()) { int idx = 0; - for (final String text : it.next().values()) { + for (final String text : it.next().getEntries().values()) { if (text.toLowerCase().contains(search.toLowerCase())) { LocalizationEditor.this.table.changeSelection(idx, 0, false, false); LocalizationEditor.this.table.scrollRectToVisible(LocalizationEditor.this.table.getCellRect(idx, 0, true)); @@ -454,7 +420,7 @@ public class LocalizationEditor extends JFrame { } for (final Language lang : LocalizationEditor.this.localizationManagerImpl.getLanguages()) { - final String value = LocalizationEditor.this.localizationManagerImpl.getLocalizations().getLanguages().get(lang.getShortName()).get(oldId); + final String value = LocalizationEditor.this.localizationManagerImpl.getLocalizations().getLanguageEntries(lang.getShortName()).get(oldId); LocalizationEditor.this.localizationManagerImpl.put(lang.getShortName(), idStr, value); } @@ -490,8 +456,9 @@ public class LocalizationEditor extends JFrame { @Override public Object getValueAt(final int rowIndex, final int columnIndex) { - final String id = LocalizationEditor.this.localizationManagerImpl.getLocalizations().getLanguages().values().stream() + final String id = LocalizationEditor.this.localizationManagerImpl.getLocalizations().getLanguageFiles().values().stream() .findFirst() + .map(LocalizationLanguage::getEntries) .map(mapping -> mapping.keySet().stream().skip(rowIndex).findFirst().orElse("-1")) .orElse("-1"); if (columnIndex == 0) { @@ -499,9 +466,9 @@ public class LocalizationEditor extends JFrame { } final String lang = LocalizationEditor.this.localizationManagerImpl.getLanguages().get(columnIndex - 1).getShortName(); - final String value = LocalizationEditor.this.localizationManagerImpl.getLocalizations().getLanguages().entrySet().stream() + final String value = LocalizationEditor.this.localizationManagerImpl.getLocalizations().getLanguageFiles().entrySet().stream() .filter(entry -> entry.getKey().equals(lang)) - .map(entry -> entry.getValue().getOrDefault(id, "")) + .map(entry -> entry.getValue().getEntries().getOrDefault(id, "")) .findFirst() .orElse(""); return value; @@ -509,7 +476,7 @@ public class LocalizationEditor extends JFrame { @Override public int getRowCount() { - return LocalizationEditor.this.localizationManagerImpl.getLocalizations().getLanguages().values().stream().findFirst().map(mapping -> Integer.valueOf(mapping.size())).orElse(Integer.valueOf(0)); + return LocalizationEditor.this.localizationManagerImpl.getLocalizations().getLanguageFiles().values().stream().findFirst().map(LocalizationLanguage::getEntries).map(mapping -> Integer.valueOf(mapping.size())).orElse(Integer.valueOf(0)); } @Override @@ -533,15 +500,16 @@ public class LocalizationEditor extends JFrame { @Override public void setValueAt(final Object aValue, final int rowIndex, final int columnIndex) { - final String id = LocalizationEditor.this.localizationManagerImpl.getLocalizations().getLanguages().values().stream() + final String id = LocalizationEditor.this.localizationManagerImpl.getLocalizations().getLanguageFiles().values().stream() .findFirst() + .map(LocalizationLanguage::getEntries) .map(mapping -> mapping.keySet().stream().skip(rowIndex).findFirst().orElse(null)) .orElse(null); if (id == null) { return; } - final String langShortName = LocalizationEditor.this.localizationManagerImpl.getLocalizations().getLanguages().keySet().stream().skip(columnIndex - 1).findFirst().orElse(null); + final String langShortName = LocalizationEditor.this.localizationManagerImpl.getLocalizations().getLanguageFiles().keySet().stream().skip(columnIndex - 1).findFirst().orElse(null); if (langShortName == null) { return; } @@ -572,7 +540,7 @@ public class LocalizationEditor extends JFrame { } final Language result = new Language(name, lang); this.localizationManagerImpl.getLocalizations().getLanguagesInfo().put(result.getShortName(), result); - this.localizationManagerImpl.getLocalizations().getLanguages().put(result.getShortName(), new LinkedHashMap<>()); + this.localizationManagerImpl.getLocalizations().ensureLanguageFile(result.getShortName()); ((AbstractTableModel) this.table.getModel()).fireTableStructureChanged(); diff --git a/src/main/java/de/extio/game_engine/i18n/LocalizationLanguage.java b/src/main/java/de/extio/game_engine/i18n/LocalizationLanguage.java new file mode 100644 index 0000000..2abc886 --- /dev/null +++ b/src/main/java/de/extio/game_engine/i18n/LocalizationLanguage.java @@ -0,0 +1,41 @@ +package de.extio.game_engine.i18n; + +import java.util.LinkedHashMap; +import java.util.Map; + +public final class LocalizationLanguage { + + private String shortName; + + private Map entries = new LinkedHashMap<>(); + + public LocalizationLanguage() { + + } + + public LocalizationLanguage(final String shortName) { + this.shortName = shortName; + } + + public LocalizationLanguage(final LocalizationLanguage other) { + this.shortName = other.shortName; + this.entries = new LinkedHashMap<>(other.entries); + } + + public String getShortName() { + return this.shortName; + } + + public void setShortName(final String shortName) { + this.shortName = shortName; + } + + public Map getEntries() { + return this.entries; + } + + public void setEntries(final Map entries) { + this.entries = new LinkedHashMap<>(entries); + } + +} \ No newline at end of file diff --git a/src/main/java/de/extio/game_engine/i18n/LocalizationMetadata.java b/src/main/java/de/extio/game_engine/i18n/LocalizationMetadata.java new file mode 100644 index 0000000..901914e --- /dev/null +++ b/src/main/java/de/extio/game_engine/i18n/LocalizationMetadata.java @@ -0,0 +1,44 @@ +package de.extio.game_engine.i18n; + +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Map.Entry; + +public final class LocalizationMetadata { + + private String prefix; + + private Map languagesInfo = new LinkedHashMap<>(); + + public LocalizationMetadata() { + + } + + public LocalizationMetadata(final LocalizationMetadata other) { + this.prefix = other.prefix; + this.languagesInfo = new LinkedHashMap<>(); + for (final Entry entry : other.languagesInfo.entrySet()) { + this.languagesInfo.put(entry.getKey(), new Language(entry.getValue())); + } + } + + public String getPrefix() { + return this.prefix; + } + + public void setPrefix(final String prefix) { + this.prefix = prefix; + } + + public Map getLanguagesInfo() { + return this.languagesInfo; + } + + public void setLanguagesInfo(final Map languagesInfo) { + this.languagesInfo = new LinkedHashMap<>(); + for (final Entry entry : languagesInfo.entrySet()) { + this.languagesInfo.put(entry.getKey(), new Language(entry.getValue())); + } + } + +} \ No newline at end of file diff --git a/src/main/java/de/extio/game_engine/i18n/LocalizationService.java b/src/main/java/de/extio/game_engine/i18n/LocalizationService.java index 4b8e5e6..9b00df6 100644 --- a/src/main/java/de/extio/game_engine/i18n/LocalizationService.java +++ b/src/main/java/de/extio/game_engine/i18n/LocalizationService.java @@ -1,6 +1,7 @@ package de.extio.game_engine.i18n; import java.io.InputStream; +import java.nio.file.Path; import java.util.List; public interface LocalizationService { @@ -9,7 +10,7 @@ public interface LocalizationService { void reset(); - void load(InputStream stream); + void load(Path metaDataPath); void setLanguage(String lang); diff --git a/src/main/java/de/extio/game_engine/i18n/LocalizationServiceImpl.java b/src/main/java/de/extio/game_engine/i18n/LocalizationServiceImpl.java index 8b98c05..cd662cb 100644 --- a/src/main/java/de/extio/game_engine/i18n/LocalizationServiceImpl.java +++ b/src/main/java/de/extio/game_engine/i18n/LocalizationServiceImpl.java @@ -1,16 +1,25 @@ package de.extio.game_engine.i18n; +import java.io.IOException; import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.ConcurrentModificationException; -import java.util.LinkedHashMap; +import java.util.HashSet; +import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Map.Entry; +import java.util.Objects; import java.util.Optional; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import de.extio.game_engine.resource.StaticResource; +import de.extio.game_engine.resource.StaticResourceService; import de.extio.game_engine.storage.StorageService; import de.extio.game_engine.util.ObjectSerialization; @@ -20,11 +29,17 @@ public class LocalizationServiceImpl implements LocalizationService { public final static String NOT_FOUND_PREFIX = "{i18n."; + public static final String METADATA_FILE_NAME = "metadata.yaml"; + + public static final String LANGUAGE_FILE_SUFFIX = ".yaml"; + private static final List LANGUAGE_STORAGE_PATH = List.of("gameEngine"); private static final String LANGUAGE_STORAGE_NAME = "currentLanguage"; private final StorageService storageService; + + private final StaticResourceService staticResourceService; private Localizations localizations = new Localizations(); @@ -32,13 +47,17 @@ public class LocalizationServiceImpl implements LocalizationService { private String currentLanguageName; - LocalizationServiceImpl(final StorageService storageService) { + private int nextId = 1; + + LocalizationServiceImpl(final StorageService storageService, final StaticResourceService staticResourceService) { this.storageService = storageService; + this.staticResourceService = staticResourceService; } @Override public void resetEntries() { - this.localizations.getLanguages().forEach((lang, mapping) -> mapping.clear()); + this.localizations.getLanguageFiles().values().forEach(language -> language.getEntries().clear()); + this.nextId = 1; } @Override @@ -46,68 +65,131 @@ public class LocalizationServiceImpl implements LocalizationService { this.localizations = new Localizations(); this.currentLanguage = null; this.currentLanguageName = null; + this.nextId = 1; } - @Override - public void load(final InputStream stream) { - Localizations localizations; - try { - localizations = ObjectSerialization.deserialize(Localizations.class, stream, false, false, null, null, null); - } - catch (final Exception exc) { - LOGGER.error("An exception occured while loading localizations", exc); - return; - } - - if (localizations != null) { - localizations.getLanguages().forEach((lang, newMapping) -> { - final Map localizationEntries = this.localizations.getLanguages().computeIfAbsent(lang, k -> new LinkedHashMap<>()); - for (final Entry newEntry : newMapping.entrySet()) { - if (!localizationEntries.containsKey(newEntry.getKey()) || (newEntry.getValue() != null && !newEntry.getValue().isEmpty())) { - localizationEntries.put(newEntry.getKey(), newEntry.getValue()); + public void load(final Path metaDataPath) { + final List resourcePath = metaDataPath.getParent() == null ? List.of() : StreamSupport.stream(metaDataPath.getParent().spliterator(), false).map(Path::toString).toList(); + final var metadataFileName = metaDataPath.getFileName().toString(); + final var staticResource = new StaticResource(resourcePath.isEmpty() ? null : resourcePath, metadataFileName); + staticResourceService.loadStreamByPath(staticResource).ifPresent(stream -> { + try (stream) { + final var metadata = ObjectSerialization.deserialize(LocalizationMetadata.class, stream, false, false, null, null, null); + final var languages = new ArrayList(); + for (final String fileName : staticResourceService.listPath(resourcePath)) { + if (metadataFileName.equals(fileName) || !fileName.endsWith(LANGUAGE_FILE_SUFFIX)) { + continue; } + staticResourceService.loadStreamByPath(resourcePath, fileName).ifPresent(languageStream -> { + try (languageStream) { + final var language = ObjectSerialization.deserialize(LocalizationLanguage.class, languageStream, false, false, null, null, null); + if (language != null) { + languages.add(language); + } + } + catch (final Exception e) { + throw new RuntimeException(e); + } + }); } - }); - this.localizations.getLanguagesInfo().putAll(localizations.getLanguagesInfo()); - - int maxId = 0; - for (final Map entries : this.localizations.getLanguages().values()) { - for (final String locId : entries.keySet()) { - final int intId = parseNumericId(locId); - if (intId > -1) { - maxId = Math.max(maxId, intId); - } + this.reset(); + this.load(metadata, languages); + } + catch (final Exception e) { + LOGGER.error("An exception occured while loading localizations from resource: " + metaDataPath, e); + } + }); + } + + void loadWoService(final Path directory) throws IOException { + Objects.requireNonNull(directory, "directory"); + this.reset(); + try (final var metadataStream = Files.newInputStream(directory.resolve(METADATA_FILE_NAME))) { + final var metadata = ObjectSerialization.deserialize(LocalizationMetadata.class, metadataStream, false, false, null, null, null); + final List languages; + try (final Stream files = Files.list(directory)) { + languages = files + .filter(Files::isRegularFile) + .map(Path::getFileName) + .map(Path::toString) + .filter(fileName -> !METADATA_FILE_NAME.equals(fileName) && fileName.endsWith(LANGUAGE_FILE_SUFFIX)) + .sorted() + .map(fileName -> this.loadLanguage(directory.resolve(fileName))) + .toList(); + } + this.load(metadata, languages); + } + } + + void load(final LocalizationMetadata metadata, final List languages) { + if (metadata != null) { + this.localizations.setPrefix(metadata.getPrefix()); + metadata.getLanguagesInfo().forEach((shortName, language) -> this.localizations.getLanguagesInfo().put(shortName, new Language(language))); + this.localizations.getLanguagesInfo().keySet().forEach(this.localizations::ensureLanguageFile); + } + for (final LocalizationLanguage language : languages) { + if (language == null || language.getShortName() == null || language.getShortName().isBlank()) { + continue; + } + final var localizationEntries = this.localizations.ensureLanguageFile(language.getShortName()).getEntries(); + for (final Entry newEntry : language.getEntries().entrySet()) { + if (!localizationEntries.containsKey(newEntry.getKey()) || (newEntry.getValue() != null && !newEntry.getValue().isEmpty())) { + localizationEntries.put(newEntry.getKey(), newEntry.getValue()); } } - this.localizations.setCurId(maxId); - - this.localizations.setPrefix(localizations.getPrefix()); - - LOGGER.info("Loaded localizations for {} languages", localizations.getLanguages().size()); + this.localizations.getLanguagesInfo().computeIfAbsent(language.getShortName(), shortName -> new Language(shortName, shortName)); } - + this.recalculateNextId(); + LOGGER.info("Loaded localizations for {} languages", this.localizations.getLanguageFiles().size()); this.loadPersistedLanguageName(); if (this.currentLanguageName == null) { this.currentLanguageName = "en"; } this.setLanguage(this.currentLanguageName); - + } + + public void save(final Path directory) throws IOException { + Objects.requireNonNull(directory, "directory"); + Files.createDirectories(directory); + try (final var metadataStream = Files.newOutputStream(directory.resolve(METADATA_FILE_NAME))) { + ObjectSerialization.serialize(this.localizations.getMetadata(), metadataStream, false, false, false, null, null); + } + final var expectedFiles = new HashSet(); + expectedFiles.add(METADATA_FILE_NAME); + for (final LocalizationLanguage language : this.localizations.getLanguageFiles().values()) { + if (language.getShortName() == null || language.getShortName().isBlank()) { + continue; + } + final var fileName = languageFileName(language.getShortName()); + expectedFiles.add(fileName); + try (final var languageStream = Files.newOutputStream(directory.resolve(fileName))) { + ObjectSerialization.serialize(language, languageStream, false, false, false, null, null); + } + } + try (final Stream files = Files.list(directory)) { + for (final Path file : files.filter(Files::isRegularFile).toList()) { + final var fileName = file.getFileName().toString(); + if (fileName.endsWith(LANGUAGE_FILE_SUFFIX) && !expectedFiles.contains(fileName)) { + Files.deleteIfExists(file); + } + } + } } @Override public void setLanguage(final String lang) { - if (!this.localizations.getLanguages().containsKey(lang)) { + if (this.localizations.getLanguageFile(lang) == null) { return; } this.currentLanguageName = lang; - this.currentLanguage = this.localizations.getLanguages().get(lang); + this.currentLanguage = this.localizations.getLanguageEntries(lang); this.persistCurrentLanguageName(); } @Override public List getLanguages() { - if (this.localizations.getLanguagesInfo() == null || this.localizations.getLanguagesInfo().isEmpty()) { + if (this.localizations.getLanguagesInfo().isEmpty()) { return List.of(); } return List.copyOf(this.localizations.getLanguagesInfo().values()); @@ -157,12 +239,13 @@ public class LocalizationServiceImpl implements LocalizationService { @Override public void put(final String lang, final String id, final String value) { - final Map mapping = this.localizations.getLanguages().get(lang); + final Map mapping = this.localizations.getLanguageEntries(lang); if (mapping == null) { return; } mapping.put(id, value); + this.updateNextId(id); if (!this.localizations.getLanguagesInfo().containsKey(lang)) { this.localizations.getLanguagesInfo().put(lang, new Language(lang, lang)); } @@ -170,13 +253,14 @@ public class LocalizationServiceImpl implements LocalizationService { @Override public void remove(final String id) { - this.localizations.getLanguages().forEach((k, v) -> v.remove(id)); + this.localizations.getLanguageFiles().values().forEach(language -> language.getEntries().remove(id)); + this.recalculateNextId(); } @Override public Integer getNextId() { - final int result = this.localizations.getCurId() + 1; - this.localizations.setCurId(result); + final int result = this.nextId; + this.nextId++; return Integer.valueOf(result); } @@ -204,6 +288,39 @@ public class LocalizationServiceImpl implements LocalizationService { } } + public static String languageFileName(final String shortName) { + return shortName + LANGUAGE_FILE_SUFFIX; + } + + private LocalizationLanguage loadLanguage(final Path file) { + try (final var languageStream = Files.newInputStream(file)) { + return ObjectSerialization.deserialize(LocalizationLanguage.class, languageStream, false, false, null, null, null); + } + catch (final Exception e) { + throw new RuntimeException("Could not load localization language file " + file, e); + } + } + + private void recalculateNextId() { + int maxId = 0; + for (final LocalizationLanguage language : this.localizations.getLanguageFiles().values()) { + for (final String locId : language.getEntries().keySet()) { + final int intId = parseNumericId(locId); + if (intId > -1) { + maxId = Math.max(maxId, intId); + } + } + } + this.nextId = maxId + 1; + } + + private void updateNextId(final String id) { + final int numericId = parseNumericId(id); + if (numericId > -1) { + this.nextId = Math.max(this.nextId, numericId + 1); + } + } + private void persistCurrentLanguageName() { if (this.storageService == null) { return; diff --git a/src/main/java/de/extio/game_engine/i18n/Localizations.java b/src/main/java/de/extio/game_engine/i18n/Localizations.java index 391cd45..8bca554 100644 --- a/src/main/java/de/extio/game_engine/i18n/Localizations.java +++ b/src/main/java/de/extio/game_engine/i18n/Localizations.java @@ -9,61 +9,64 @@ import java.util.Map.Entry; */ public final class Localizations { - private String prefix; + private LocalizationMetadata metadata = new LocalizationMetadata(); - private Map> languages = new LinkedHashMap<>(); - - private Map languagesInfo = new LinkedHashMap<>(); - - private int curId; + private Map languageFiles = new LinkedHashMap<>(); public Localizations() { } public Localizations(final Localizations other) { - this.languages = new LinkedHashMap<>(); - for (final Entry> entry : other.languages.entrySet()) { - this.languages.put(entry.getKey(), new LinkedHashMap<>(entry.getValue())); + this.metadata = new LocalizationMetadata(other.metadata); + this.languageFiles = new LinkedHashMap<>(); + for (final Entry entry : other.languageFiles.entrySet()) { + this.languageFiles.put(entry.getKey(), new LocalizationLanguage(entry.getValue())); } - this.languagesInfo = new LinkedHashMap<>(); - for (final Entry entry : other.languagesInfo.entrySet()) { - this.languagesInfo.put(entry.getKey(), new Language(entry.getValue())); + } + + public LocalizationMetadata getMetadata() { + return this.metadata; + } + + public void setMetadata(final LocalizationMetadata metadata) { + this.metadata = metadata == null ? new LocalizationMetadata() : new LocalizationMetadata(metadata); + } + + public Map getLanguageFiles() { + return this.languageFiles; + } + + public void setLanguageFiles(final Map languageFiles) { + this.languageFiles = new LinkedHashMap<>(); + for (final Entry entry : languageFiles.entrySet()) { + this.languageFiles.put(entry.getKey(), new LocalizationLanguage(entry.getValue())); } - this.curId = other.curId; - this.prefix = other.prefix; - } - - public Map> getLanguages() { - return this.languages; - } - - public void setLanguages(final Map> languages) { - this.languages = new LinkedHashMap<>(languages); - } - - public Map getLanguagesInfo() { - return this.languagesInfo; - } - - public void setLanguagesInfo(final Map languagesInfo) { - this.languagesInfo = new LinkedHashMap<>(languagesInfo); - } - - public int getCurId() { - return this.curId; - } - - public void setCurId(final int curId) { - this.curId = curId; } public String getPrefix() { - return this.prefix; + return this.metadata.getPrefix(); } public void setPrefix(final String prefix) { - this.prefix = prefix; + this.metadata.setPrefix(prefix); + } + + public Map getLanguagesInfo() { + return this.metadata.getLanguagesInfo(); + } + + public LocalizationLanguage getLanguageFile(final String shortName) { + return this.languageFiles.get(shortName); + } + + public LocalizationLanguage ensureLanguageFile(final String shortName) { + return this.languageFiles.computeIfAbsent(shortName, key -> new LocalizationLanguage(key)); + } + + public Map getLanguageEntries(final String shortName) { + final var languageFile = this.getLanguageFile(shortName); + return languageFile == null ? null : languageFile.getEntries(); } } diff --git a/src/test/java/de/extio/game_engine/i18n/LocalizationManagerTest.java b/src/test/java/de/extio/game_engine/i18n/LocalizationManagerTest.java index d3ed058..f47a919 100644 --- a/src/test/java/de/extio/game_engine/i18n/LocalizationManagerTest.java +++ b/src/test/java/de/extio/game_engine/i18n/LocalizationManagerTest.java @@ -2,11 +2,11 @@ package de.extio.game_engine.i18n; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; -import java.io.ByteArrayInputStream; -import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -14,25 +14,36 @@ import java.util.Optional; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import de.extio.game_engine.resource.StaticResourceService; import de.extio.game_engine.storage.StorageService; class LocalizationManagerTest { @Mock private StorageService storageService; + + @Mock + private StaticResourceService staticResourceService; - private LocalizationService localizationManager; + private LocalizationServiceImpl localizationManager; @BeforeEach void setUp() { MockitoAnnotations.openMocks(this); - this.localizationManager = new LocalizationServiceImpl(storageService); + this.localizationManager = new LocalizationServiceImpl(storageService, staticResourceService); + } + + private static LocalizationLanguage language(final String shortName, final Map entries) { + final var language = new LocalizationLanguage(shortName); + language.setEntries(new LinkedHashMap<>(entries)); + return language; } @Test @@ -44,40 +55,31 @@ class LocalizationManagerTest { @Test void testResetEntries() { - Localizations localizations = this.localizationManager.getLocalizations(); - Language english = new Language("English", "en"); + final Localizations localizations = this.localizationManager.getLocalizations(); + final Language english = new Language("English", "en"); localizations.getLanguagesInfo().put("en", english); - localizations.getLanguages().put("en", new LinkedHashMap<>(Map.of("greeting", "Hello"))); + localizations.ensureLanguageFile("en").setEntries(new LinkedHashMap<>(Map.of("greeting", "Hello"))); this.localizationManager.setLanguage("en"); this.localizationManager.resetEntries(); - assertTrue(localizations.getLanguages().get("en").isEmpty()); + assertTrue(localizations.getLanguageEntries("en").isEmpty()); } @Test void testLoadLocalizations() { - Localizations localizationsToSave = new Localizations(); - Language english = new Language("English", "en"); - Map englishEntries = new LinkedHashMap<>(Map.of("greeting", "Hello", "farewell", "Goodbye")); - localizationsToSave.getLanguagesInfo().put("en", english); - localizationsToSave.getLanguages().put("en", englishEntries); - localizationsToSave.setPrefix("i18n."); - localizationsToSave.setCurId(1); + final LocalizationMetadata metadata = new LocalizationMetadata(); + final Language english = new Language("English", "en"); + final Map englishEntries = new LinkedHashMap<>(Map.of("greeting", "Hello", "farewell", "Goodbye")); + metadata.getLanguagesInfo().put("en", english); + metadata.setPrefix("i18n."); + this.localizationManager.load(metadata, List.of(language("en", englishEntries))); - byte[] serialized = de.extio.game_engine.util.ObjectSerialization.serialize( - localizationsToSave, false, false, false, null, digest -> { - }); - - InputStream stream = new ByteArrayInputStream(serialized); - - this.localizationManager.load(stream); - - Localizations loaded = this.localizationManager.getLocalizations(); + final Localizations loaded = this.localizationManager.getLocalizations(); assertNotNull(loaded); assertEquals("i18n.", loaded.getPrefix()); - List languages = this.localizationManager.getLanguages(); + final List languages = this.localizationManager.getLanguages(); assertEquals(1, languages.size()); assertEquals("en", languages.getFirst().getShortName()); @@ -86,15 +88,33 @@ class LocalizationManagerTest { assertEquals("Goodbye", this.localizationManager.translate("farewell")); } + @Test + void testLoadLocalizationsFromDirectory(@TempDir final Path directory) throws Exception { + final LocalizationMetadata metadata = new LocalizationMetadata(); + metadata.getLanguagesInfo().put("en", new Language("English", "en")); + metadata.getLanguagesInfo().put("de", new Language("German", "de")); + metadata.setPrefix("i18n."); + Files.write(directory.resolve(LocalizationServiceImpl.METADATA_FILE_NAME), de.extio.game_engine.util.ObjectSerialization.serialize(metadata, false, false, false, null, digest -> {})); + Files.write(directory.resolve(LocalizationServiceImpl.languageFileName("en")), de.extio.game_engine.util.ObjectSerialization.serialize(language("en", Map.of("i18n.greeting", "Hello")), false, false, false, null, digest -> {})); + Files.write(directory.resolve(LocalizationServiceImpl.languageFileName("de")), de.extio.game_engine.util.ObjectSerialization.serialize(language("de", Map.of("i18n.greeting", "Hallo")), false, false, false, null, digest -> {})); + + this.localizationManager.loadWoService(directory); + + // this.localizationManager.setLanguage("en"); + assertEquals("Hello", this.localizationManager.translate("i18n.greeting")); + this.localizationManager.setLanguage("de"); + assertEquals("Hallo", this.localizationManager.translate("i18n.greeting")); + } + @Test void testSetLanguage() { - Localizations localizations = this.localizationManager.getLocalizations(); - Language english = new Language("English", "en"); - Language german = new Language("German", "de"); + final Localizations localizations = this.localizationManager.getLocalizations(); + final Language english = new Language("English", "en"); + final Language german = new Language("German", "de"); localizations.getLanguagesInfo().put("en", english); localizations.getLanguagesInfo().put("de", german); - localizations.getLanguages().put("en", new LinkedHashMap<>(Map.of("greeting", "Hello"))); - localizations.getLanguages().put("de", new LinkedHashMap<>(Map.of("greeting", "Hallo"))); + localizations.ensureLanguageFile("en").setEntries(new LinkedHashMap<>(Map.of("greeting", "Hello"))); + localizations.ensureLanguageFile("de").setEntries(new LinkedHashMap<>(Map.of("greeting", "Hallo"))); this.localizationManager.setLanguage("de"); @@ -103,10 +123,10 @@ class LocalizationManagerTest { @Test void testSetLanguageInvalid() { - Localizations localizations = this.localizationManager.getLocalizations(); - Language english = new Language("English", "en"); + final Localizations localizations = this.localizationManager.getLocalizations(); + final Language english = new Language("English", "en"); localizations.getLanguagesInfo().put("en", english); - localizations.getLanguages().put("en", new LinkedHashMap<>(Map.of("greeting", "Hello"))); + localizations.ensureLanguageFile("en").setEntries(new LinkedHashMap<>(Map.of("greeting", "Hello"))); this.localizationManager.setLanguage("en"); this.localizationManager.setLanguage("fr"); @@ -116,15 +136,15 @@ class LocalizationManagerTest { @Test void testGetLanguages() { - Localizations localizations = this.localizationManager.getLocalizations(); - Language english = new Language("English", "en"); - Language german = new Language("German", "de"); + final Localizations localizations = this.localizationManager.getLocalizations(); + final Language english = new Language("English", "en"); + final Language german = new Language("German", "de"); localizations.getLanguagesInfo().put("en", english); localizations.getLanguagesInfo().put("de", german); - localizations.getLanguages().put("en", new LinkedHashMap<>(Map.of("greeting", "Hello"))); - localizations.getLanguages().put("de", new LinkedHashMap<>(Map.of("greeting", "Hallo"))); + localizations.ensureLanguageFile("en").setEntries(new LinkedHashMap<>(Map.of("greeting", "Hello"))); + localizations.ensureLanguageFile("de").setEntries(new LinkedHashMap<>(Map.of("greeting", "Hallo"))); - List languages = this.localizationManager.getLanguages(); + final List languages = this.localizationManager.getLanguages(); assertEquals(2, languages.size()); assertTrue(languages.stream().anyMatch(lang -> "en".equals(lang.getShortName()))); @@ -133,10 +153,10 @@ class LocalizationManagerTest { @Test void testGetCurrentLanguage() { - Localizations localizations = this.localizationManager.getLocalizations(); - Language english = new Language("English", "en"); + final Localizations localizations = this.localizationManager.getLocalizations(); + final Language english = new Language("English", "en"); localizations.getLanguagesInfo().put("en", english); - localizations.getLanguages().put("en", new LinkedHashMap<>(Map.of("greeting", "Hello"))); + localizations.ensureLanguageFile("en").setEntries(new LinkedHashMap<>(Map.of("greeting", "Hello"))); this.localizationManager.setLanguage("en"); assertEquals("en", this.localizationManager.getCurrentLanguage()); @@ -144,103 +164,100 @@ class LocalizationManagerTest { @Test void testTranslateWithStringId() { - Localizations localizations = this.localizationManager.getLocalizations(); - Language english = new Language("English", "en"); + final Localizations localizations = this.localizationManager.getLocalizations(); + final Language english = new Language("English", "en"); localizations.getLanguagesInfo().put("en", english); - localizations.getLanguages().put("en", new LinkedHashMap<>(Map.of("greeting", "Hello"))); + localizations.ensureLanguageFile("en").setEntries(new LinkedHashMap<>(Map.of("greeting", "Hello"))); this.localizationManager.setLanguage("en"); - String result = this.localizationManager.translate("greeting"); + final String result = this.localizationManager.translate("greeting"); assertEquals("Hello", result); } @Test void testTranslateWithStringIdNotFound() { - Localizations localizations = this.localizationManager.getLocalizations(); - Language english = new Language("English", "en"); + final Localizations localizations = this.localizationManager.getLocalizations(); + final Language english = new Language("English", "en"); localizations.getLanguagesInfo().put("en", english); - localizations.getLanguages().put("en", new LinkedHashMap<>()); + localizations.ensureLanguageFile("en").setEntries(new LinkedHashMap<>()); this.localizationManager.setLanguage("en"); - String result = this.localizationManager.translate("unknown"); + final String result = this.localizationManager.translate("unknown"); assertEquals("unknown", result); } @Test void testTranslateWithStringIdAndDefault() { - Localizations localizations = this.localizationManager.getLocalizations(); - Language english = new Language("English", "en"); + final Localizations localizations = this.localizationManager.getLocalizations(); + final Language english = new Language("English", "en"); localizations.getLanguagesInfo().put("en", english); - localizations.getLanguages().put("en", new LinkedHashMap<>()); + localizations.ensureLanguageFile("en").setEntries(new LinkedHashMap<>()); this.localizationManager.setLanguage("en"); - String result = this.localizationManager.translate("unknown", "Default Text"); + final String result = this.localizationManager.translate("unknown", "Default Text"); assertEquals("Default Text", result); } @Test void testPut() { - Localizations localizations = this.localizationManager.getLocalizations(); - Language english = new Language("English", "en"); + final Localizations localizations = this.localizationManager.getLocalizations(); + final Language english = new Language("English", "en"); localizations.getLanguagesInfo().put("en", english); - localizations.getLanguages().put("en", new LinkedHashMap<>()); + localizations.ensureLanguageFile("en").setEntries(new LinkedHashMap<>()); this.localizationManager.setLanguage("en"); this.localizationManager.put("en", "greeting", "Hello"); - String result = this.localizationManager.translate("greeting"); + final String result = this.localizationManager.translate("greeting"); assertEquals("Hello", result); } @Test void testPutInvalidLanguage() { - Localizations localizations = this.localizationManager.getLocalizations(); - Language english = new Language("English", "en"); + final Localizations localizations = this.localizationManager.getLocalizations(); + final Language english = new Language("English", "en"); localizations.getLanguagesInfo().put("en", english); - localizations.getLanguages().put("en", new LinkedHashMap<>()); + localizations.ensureLanguageFile("en").setEntries(new LinkedHashMap<>()); this.localizationManager.put("fr", "greeting", "Bonjour"); - assertTrue(localizations.getLanguages().get("en").isEmpty()); + assertTrue(localizations.getLanguageEntries("en").isEmpty()); } @Test void testRemove() { - Localizations localizations = this.localizationManager.getLocalizations(); - Language english = new Language("English", "en"); - Map entries = new LinkedHashMap<>(Map.of("greeting", "Hello", "farewell", "Goodbye")); + final Localizations localizations = this.localizationManager.getLocalizations(); + final Language english = new Language("English", "en"); + final Map entries = new LinkedHashMap<>(Map.of("greeting", "Hello", "farewell", "Goodbye")); localizations.getLanguagesInfo().put("en", english); - localizations.getLanguages().put("en", entries); + localizations.ensureLanguageFile("en").setEntries(entries); this.localizationManager.setLanguage("en"); this.localizationManager.remove("greeting"); - assertFalse(localizations.getLanguages().get("en").containsKey("greeting")); - assertTrue(localizations.getLanguages().get("en").containsKey("farewell")); + assertFalse(localizations.getLanguageEntries("en").containsKey("greeting")); + assertTrue(localizations.getLanguageEntries("en").containsKey("farewell")); } @Test void testGetNextId() { - Localizations localizations = this.localizationManager.getLocalizations(); - localizations.setCurId(5); + final Localizations localizations = this.localizationManager.getLocalizations(); + localizations.ensureLanguageFile("en").setEntries(new LinkedHashMap<>(Map.of("test-5", "Hello"))); + this.localizationManager.load(localizations.getMetadata(), List.of(localizations.getLanguageFile("en"))); - Integer nextId = this.localizationManager.getNextId(); + final Integer nextId = this.localizationManager.getNextId(); assertEquals(6, nextId); - assertEquals(6, localizations.getCurId()); } @Test void testGetNextIdIncremental() { - Localizations localizations = this.localizationManager.getLocalizations(); - localizations.setCurId(0); - - Integer id1 = this.localizationManager.getNextId(); - Integer id2 = this.localizationManager.getNextId(); - Integer id3 = this.localizationManager.getNextId(); + final Integer id1 = this.localizationManager.getNextId(); + final Integer id2 = this.localizationManager.getNextId(); + final Integer id3 = this.localizationManager.getNextId(); assertEquals(1, id1); assertEquals(2, id2); @@ -249,7 +266,7 @@ class LocalizationManagerTest { @Test void testGetLocalizations() { - Localizations localizations = this.localizationManager.getLocalizations(); + final Localizations localizations = this.localizationManager.getLocalizations(); assertNotNull(localizations); assertEquals(localizations, this.localizationManager.getLocalizations()); @@ -257,14 +274,14 @@ class LocalizationManagerTest { @Test void testTranslateCaching() { - Localizations localizations = this.localizationManager.getLocalizations(); - Language english = new Language("English", "en"); + final Localizations localizations = this.localizationManager.getLocalizations(); + final Language english = new Language("English", "en"); localizations.getLanguagesInfo().put("en", english); - localizations.getLanguages().put("en", new LinkedHashMap<>(Map.of("1", "Hello"))); + localizations.ensureLanguageFile("en").setEntries(new LinkedHashMap<>(Map.of("1", "Hello"))); this.localizationManager.setLanguage("en"); - String result1 = this.localizationManager.translate("1"); - String result2 = this.localizationManager.translate("1"); + final String result1 = this.localizationManager.translate("1"); + final String result2 = this.localizationManager.translate("1"); assertEquals("Hello", result1); assertEquals("Hello", result2); @@ -273,19 +290,19 @@ class LocalizationManagerTest { @Test void testMultipleLanguages() { - Localizations localizations = this.localizationManager.getLocalizations(); - Language english = new Language("English", "en"); - Language german = new Language("German", "de"); + final Localizations localizations = this.localizationManager.getLocalizations(); + final Language english = new Language("English", "en"); + final Language german = new Language("German", "de"); localizations.getLanguagesInfo().put("en", english); localizations.getLanguagesInfo().put("de", german); - localizations.getLanguages().put("en", new LinkedHashMap<>(Map.of("greeting", "Hello"))); - localizations.getLanguages().put("de", new LinkedHashMap<>(Map.of("greeting", "Hallo"))); + localizations.ensureLanguageFile("en").setEntries(new LinkedHashMap<>(Map.of("greeting", "Hello"))); + localizations.ensureLanguageFile("de").setEntries(new LinkedHashMap<>(Map.of("greeting", "Hallo"))); this.localizationManager.setLanguage("en"); - String englishGreeting = this.localizationManager.translate("greeting"); + final String englishGreeting = this.localizationManager.translate("greeting"); this.localizationManager.setLanguage("de"); - String germanGreeting = this.localizationManager.translate("greeting"); + final String germanGreeting = this.localizationManager.translate("greeting"); assertEquals("Hello", englishGreeting); assertEquals("Hallo", germanGreeting); @@ -293,10 +310,10 @@ class LocalizationManagerTest { @Test void testPersistCurrentLanguage() { - Localizations localizations = this.localizationManager.getLocalizations(); - Language english = new Language("English", "en"); + final Localizations localizations = this.localizationManager.getLocalizations(); + final Language english = new Language("English", "en"); localizations.getLanguagesInfo().put("en", english); - localizations.getLanguages().put("en", new LinkedHashMap<>(Map.of("greeting", "Hello"))); + localizations.ensureLanguageFile("en").setEntries(new LinkedHashMap<>(Map.of("greeting", "Hello"))); this.localizationManager.setLanguage("en"); @@ -308,20 +325,10 @@ class LocalizationManagerTest { when(storageService.loadByPath(String.class, List.of("gameEngine"), "currentLanguage")) .thenReturn(Optional.of("de")); - // Load localizations with "de" - Localizations localizationsToSave = new Localizations(); - Language german = new Language("German", "de"); - Map germanEntries = new LinkedHashMap<>(Map.of("greeting", "Hallo")); - localizationsToSave.getLanguagesInfo().put("de", german); - localizationsToSave.getLanguages().put("de", germanEntries); - localizationsToSave.setPrefix("i18n."); - localizationsToSave.setCurId(1); - - byte[] serialized = de.extio.game_engine.util.ObjectSerialization.serialize( - localizationsToSave, false, false, false, null, digest -> {}); - InputStream stream = new ByteArrayInputStream(serialized); - - this.localizationManager.load(stream); + final LocalizationMetadata metadata = new LocalizationMetadata(); + metadata.getLanguagesInfo().put("de", new Language("German", "de")); + metadata.setPrefix("i18n."); + this.localizationManager.load(metadata, List.of(language("de", Map.of("greeting", "Hallo")))); assertEquals("de", this.localizationManager.getCurrentLanguage()); }