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