feat(i18n): Implement localization management with Language and LocalizationManager classes

This commit is contained in:
extio-de
2025-12-23 19:04:19 +01:00
parent 69b5db1408
commit c36e047109
7 changed files with 758 additions and 0 deletions

View File

@@ -0,0 +1,82 @@
package de.extio.game_engine.i18n;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
public final class Language {
private String name;
private String shortName;
public Language() {
}
@JsonCreator
public Language(@JsonProperty("name") final String name, @JsonProperty("shortName") final String shortName) {
this.name = name;
this.shortName = shortName;
}
public Language(final Language other) {
this.name = other.name;
this.shortName = other.shortName;
}
public String getName() {
return this.name;
}
public void setName(final String name) {
this.name = name;
}
public String getShortName() {
return this.shortName;
}
public void setShortName(final String shortName) {
this.shortName = shortName;
}
@Override
public String toString() {
final StringBuilder builder = new StringBuilder();
builder.append("Language [name=");
builder.append(this.name);
builder.append(", shortName=");
builder.append(this.shortName);
builder.append("]");
return builder.toString();
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((this.shortName == null) ? 0 : this.shortName.hashCode());
return result;
}
@Override
public boolean equals(final Object obj) {
if (this == obj) {
return true;
}
if ((obj == null) || (this.getClass() != obj.getClass())) {
return false;
}
final Language other = (Language) obj;
if (this.shortName == null) {
if (other.shortName != null) {
return false;
}
}
else if (!this.shortName.equals(other.shortName)) {
return false;
}
return true;
}
}

View File

@@ -0,0 +1,36 @@
package de.extio.game_engine.i18n;
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;
@AutoConfiguration
@ConditionalOnProperty(name = "game-engine.i18n.enabled", havingValue = "true", matchIfMissing = true)
public class LocalizationAutoConfiguration {
private static final Logger LOGGER = LoggerFactory.getLogger(LocalizationManagerImpl.class);
@Bean
@ConditionalOnMissingBean
LocalizationManager localizationManager(@Value("${game-engine.i18n.load-on-start:true}") final boolean loadOnStart, @Value("${game-engine.i18n.resource:i18n.yaml}") final String resource) {
final var localizationManager = new LocalizationManagerImpl();
if (loadOnStart) {
try (var resourceStream = LocalizationAutoConfiguration.class.getClassLoader().getResourceAsStream(resource)) {
if (resourceStream != null) {
localizationManager.load(resourceStream);
}
}
catch (final Exception exc) {
LOGGER.error("An exception occured while loading localizations from resource: " + resource, exc);
}
}
return localizationManager;
}
}

View File

@@ -0,0 +1,36 @@
package de.extio.game_engine.i18n;
import java.io.InputStream;
import java.util.List;
public interface LocalizationManager {
void resetEntries();
void reset();
void load(InputStream stream);
void setLanguage(String lang);
List<Language> getLanguages();
String getCurrentLanguage();
String translate(Integer id);
String translate(Integer id, String defaultText);
String translate(String id);
String translate(String id, String defaultText);
void put(String lang, String id, String value);
void remove(String id);
Integer getNextId();
Localizations getLocalizations();
}

View File

@@ -0,0 +1,202 @@
package de.extio.game_engine.i18n;
import java.io.InputStream;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import de.extio.game_engine.util.ObjectSerialization;
public class LocalizationManagerImpl implements LocalizationManager {
private static final Logger LOGGER = LoggerFactory.getLogger(LocalizationManagerImpl.class);
public final static String NOT_FOUND_PREFIX = "{i18n.";
private Localizations localizations = new Localizations();
private Map<String, String> currentLanguage;
private String currentLanguageName;
private final Map<Integer, String> intLookup = new HashMap<>();
@Override
public void resetEntries() {
this.localizations.getLanguages().forEach((lang, mapping) -> mapping.clear());
this.intLookup.clear();
}
@Override
public void reset() {
this.localizations = new Localizations();
this.currentLanguage = null;
this.currentLanguageName = null;
this.intLookup.clear();
}
@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<String, String> localizationEntries = this.localizations.getLanguages().computeIfAbsent(lang, k -> new LinkedHashMap<>());
for (final Entry<String, String> newEntry : newMapping.entrySet()) {
if (!localizationEntries.containsKey(newEntry.getKey()) || (newEntry.getValue() != null && !newEntry.getValue().isEmpty())) {
localizationEntries.put(newEntry.getKey(), newEntry.getValue());
}
}
});
this.localizations.getLanguagesInfo().putAll(localizations.getLanguagesInfo());
this.localizations.getDescriptions().putAll(localizations.getDescriptions());
int maxId = 0;
for (final Map<String, String> 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.localizations.setCurId(maxId);
this.localizations.setPrefix(localizations.getPrefix());
this.intLookup.clear();
}
if (this.currentLanguageName == null) {
this.currentLanguageName = "en";
}
this.setLanguage(this.currentLanguageName);
}
@Override
public void setLanguage(final String lang) {
if (!this.localizations.getLanguages().containsKey(lang)) {
return;
}
this.currentLanguageName = lang;
this.currentLanguage = this.localizations.getLanguages().get(lang);
this.intLookup.clear();
}
@Override
public List<Language> getLanguages() {
if (this.localizations.getLanguagesInfo() == null || this.localizations.getLanguagesInfo().isEmpty()) {
return List.of();
}
return List.copyOf(this.localizations.getLanguagesInfo().values());
}
@Override
public String getCurrentLanguage() {
return this.currentLanguageName;
}
@Override
public String translate(final Integer id) {
String result = this.intLookup.get(id);
if (result != null) {
return result;
}
result = this.translate(String.valueOf(id));
this.intLookup.put(id, result);
return result;
}
@Override
public String translate(final Integer id, final String defaultText) {
String result = this.translate(id);
if (result.startsWith(NOT_FOUND_PREFIX)) {
result = defaultText;
}
return result;
}
@Override
public String translate(final String id) {
if (this.currentLanguage == null) {
return NOT_FOUND_PREFIX + id + "}";
}
return this.currentLanguage.computeIfAbsent(id, key -> NOT_FOUND_PREFIX + key + "}");
}
@Override
public String translate(final String id, final String defaultText) {
String result = this.translate(id);
if (result.startsWith(NOT_FOUND_PREFIX)) {
result = defaultText;
}
return result;
}
@Override
public void put(final String lang, final String id, final String value) {
final Map<String, String> mapping = this.localizations.getLanguages().get(lang);
if (mapping == null) {
return;
}
mapping.put(id, value);
if (!this.localizations.getLanguagesInfo().containsKey(lang)) {
this.localizations.getLanguagesInfo().put(lang, new Language(lang, lang));
}
this.intLookup.clear();
}
@Override
public void remove(final String id) {
this.localizations.getLanguages().forEach((k, v) -> v.remove(id));
this.intLookup.clear();
}
@Override
public Integer getNextId() {
final int result = this.localizations.getCurId() + 1;
this.localizations.setCurId(result);
return Integer.valueOf(result);
}
@Override
public Localizations getLocalizations() {
return this.localizations;
}
private static int parseNumericId(final String id) {
try {
return Integer.parseInt(id);
}
catch (final NumberFormatException e) {
final int dashIndex = id.lastIndexOf("-");
if (dashIndex == -1 || dashIndex == id.length() - 1) {
return -1;
}
try {
return Integer.parseInt(id.substring(dashIndex + 1));
}
catch (final NumberFormatException ex) {
return -1;
}
}
}
}

View File

@@ -0,0 +1,81 @@
package de.extio.game_engine.i18n;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Map.Entry;
/**
* This is the container holding localized texts (i18n)
*/
public final class Localizations {
private String prefix;
private Map<String, Map<String, String>> languages = new LinkedHashMap<>();
private Map<String, Language> languagesInfo = new LinkedHashMap<>();
private Map<String, String> descriptions = new HashMap<>();
private int curId;
public Localizations() {
}
public Localizations(final Localizations other) {
this.languages = new LinkedHashMap<>();
for (final Entry<String, Map<String, String>> entry : other.languages.entrySet()) {
this.languages.put(entry.getKey(), new LinkedHashMap<>(entry.getValue()));
}
this.languagesInfo = new LinkedHashMap<>();
for (final Entry<String, Language> entry : other.languagesInfo.entrySet()) {
this.languagesInfo.put(entry.getKey(), new Language(entry.getValue()));
}
this.descriptions = new HashMap<>(other.descriptions);
this.curId = other.curId;
this.prefix = other.prefix;
}
public Map<String, Map<String, String>> getLanguages() {
return this.languages;
}
public void setLanguages(final Map<String, Map<String, String>> languages) {
this.languages = languages;
}
public Map<String, Language> getLanguagesInfo() {
return this.languagesInfo;
}
public void setLanguagesInfo(final Map<String, Language> languagesInfo) {
this.languagesInfo = languagesInfo;
}
public int getCurId() {
return this.curId;
}
public void setCurId(final int curId) {
this.curId = curId;
}
public Map<String, String> getDescriptions() {
return this.descriptions;
}
public void setDescriptions(final Map<String, String> descriptions) {
this.descriptions = descriptions;
}
public String getPrefix() {
return this.prefix;
}
public void setPrefix(final String prefix) {
this.prefix = prefix;
}
}

View File

@@ -4,3 +4,4 @@ de.extio.game_engine.renderer.RendererAutoConfiguration
de.extio.game_engine.renderer.g2d.G2DAutoConfiguration de.extio.game_engine.renderer.g2d.G2DAutoConfiguration
de.extio.game_engine.storage.StorageServiceAutoConfiguration de.extio.game_engine.storage.StorageServiceAutoConfiguration
de.extio.game_engine.module.ModuleManagerAutoConfiguration de.extio.game_engine.module.ModuleManagerAutoConfiguration
de.extio.game_engine.i18n.LocalizationAutoConfiguration

View File

@@ -0,0 +1,320 @@
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 java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
class LocalizationManagerTest {
private LocalizationManager localizationManager;
@BeforeEach
void setUp() {
this.localizationManager = new LocalizationManagerImpl();
}
@Test
void testReset() {
this.localizationManager.reset();
assertNotNull(this.localizationManager.getLocalizations());
assertTrue(this.localizationManager.getLanguages().isEmpty());
}
@Test
void testResetEntries() {
Localizations localizations = this.localizationManager.getLocalizations();
Language english = new Language("English", "en");
localizations.getLanguagesInfo().put("en", english);
localizations.getLanguages().put("en", new LinkedHashMap<>(Map.of("greeting", "Hello")));
this.localizationManager.setLanguage("en");
this.localizationManager.resetEntries();
assertTrue(localizations.getLanguages().get("en").isEmpty());
}
@Test
void testLoadLocalizations() {
Localizations localizationsToSave = new Localizations();
Language english = new Language("English", "en");
Map<String, String> 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);
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();
assertNotNull(loaded);
assertEquals("i18n.", loaded.getPrefix());
List<Language> languages = this.localizationManager.getLanguages();
assertEquals(1, languages.size());
assertEquals("en", languages.getFirst().getShortName());
assertEquals("en", this.localizationManager.getCurrentLanguage());
assertEquals("Hello", this.localizationManager.translate("greeting"));
assertEquals("Goodbye", this.localizationManager.translate("farewell"));
}
@Test
void testSetLanguage() {
Localizations localizations = this.localizationManager.getLocalizations();
Language english = new Language("English", "en");
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")));
this.localizationManager.setLanguage("de");
assertEquals("de", this.localizationManager.getCurrentLanguage());
}
@Test
void testSetLanguageInvalid() {
Localizations localizations = this.localizationManager.getLocalizations();
Language english = new Language("English", "en");
localizations.getLanguagesInfo().put("en", english);
localizations.getLanguages().put("en", new LinkedHashMap<>(Map.of("greeting", "Hello")));
this.localizationManager.setLanguage("en");
this.localizationManager.setLanguage("fr");
assertEquals("en", this.localizationManager.getCurrentLanguage());
}
@Test
void testGetLanguages() {
Localizations localizations = this.localizationManager.getLocalizations();
Language english = new Language("English", "en");
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")));
List<Language> languages = this.localizationManager.getLanguages();
assertEquals(2, languages.size());
assertTrue(languages.stream().anyMatch(lang -> "en".equals(lang.getShortName())));
assertTrue(languages.stream().anyMatch(lang -> "de".equals(lang.getShortName())));
}
@Test
void testGetCurrentLanguage() {
Localizations localizations = this.localizationManager.getLocalizations();
Language english = new Language("English", "en");
localizations.getLanguagesInfo().put("en", english);
localizations.getLanguages().put("en", new LinkedHashMap<>(Map.of("greeting", "Hello")));
this.localizationManager.setLanguage("en");
assertEquals("en", this.localizationManager.getCurrentLanguage());
}
@Test
void testTranslateWithIntegerId() {
Localizations localizations = this.localizationManager.getLocalizations();
Language english = new Language("English", "en");
localizations.getLanguagesInfo().put("en", english);
localizations.getLanguages().put("en", new LinkedHashMap<>(Map.of("1", "Hello")));
this.localizationManager.setLanguage("en");
String result = this.localizationManager.translate(1);
assertEquals("Hello", result);
}
@Test
void testTranslateWithIntegerIdNotFound() {
Localizations localizations = this.localizationManager.getLocalizations();
Language english = new Language("English", "en");
localizations.getLanguagesInfo().put("en", english);
localizations.getLanguages().put("en", new LinkedHashMap<>());
this.localizationManager.setLanguage("en");
String result = this.localizationManager.translate(999);
assertTrue(result.startsWith(LocalizationManagerImpl.NOT_FOUND_PREFIX));
}
@Test
void testTranslateWithIntegerIdAndDefault() {
Localizations localizations = this.localizationManager.getLocalizations();
Language english = new Language("English", "en");
localizations.getLanguagesInfo().put("en", english);
localizations.getLanguages().put("en", new LinkedHashMap<>());
this.localizationManager.setLanguage("en");
String result = this.localizationManager.translate(999, "Default Text");
assertEquals("Default Text", result);
}
@Test
void testTranslateWithStringId() {
Localizations localizations = this.localizationManager.getLocalizations();
Language english = new Language("English", "en");
localizations.getLanguagesInfo().put("en", english);
localizations.getLanguages().put("en", new LinkedHashMap<>(Map.of("greeting", "Hello")));
this.localizationManager.setLanguage("en");
String result = this.localizationManager.translate("greeting");
assertEquals("Hello", result);
}
@Test
void testTranslateWithStringIdNotFound() {
Localizations localizations = this.localizationManager.getLocalizations();
Language english = new Language("English", "en");
localizations.getLanguagesInfo().put("en", english);
localizations.getLanguages().put("en", new LinkedHashMap<>());
this.localizationManager.setLanguage("en");
String result = this.localizationManager.translate("unknown");
assertTrue(result.startsWith(LocalizationManagerImpl.NOT_FOUND_PREFIX));
}
@Test
void testTranslateWithStringIdAndDefault() {
Localizations localizations = this.localizationManager.getLocalizations();
Language english = new Language("English", "en");
localizations.getLanguagesInfo().put("en", english);
localizations.getLanguages().put("en", new LinkedHashMap<>());
this.localizationManager.setLanguage("en");
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");
localizations.getLanguagesInfo().put("en", english);
localizations.getLanguages().put("en", new LinkedHashMap<>());
this.localizationManager.setLanguage("en");
this.localizationManager.put("en", "greeting", "Hello");
String result = this.localizationManager.translate("greeting");
assertEquals("Hello", result);
}
@Test
void testPutInvalidLanguage() {
Localizations localizations = this.localizationManager.getLocalizations();
Language english = new Language("English", "en");
localizations.getLanguagesInfo().put("en", english);
localizations.getLanguages().put("en", new LinkedHashMap<>());
this.localizationManager.put("fr", "greeting", "Bonjour");
assertTrue(localizations.getLanguages().get("en").isEmpty());
}
@Test
void testRemove() {
Localizations localizations = this.localizationManager.getLocalizations();
Language english = new Language("English", "en");
Map<String, String> entries = new LinkedHashMap<>(Map.of("greeting", "Hello", "farewell", "Goodbye"));
localizations.getLanguagesInfo().put("en", english);
localizations.getLanguages().put("en", entries);
this.localizationManager.setLanguage("en");
this.localizationManager.remove("greeting");
assertFalse(localizations.getLanguages().get("en").containsKey("greeting"));
assertTrue(localizations.getLanguages().get("en").containsKey("farewell"));
}
@Test
void testGetNextId() {
Localizations localizations = this.localizationManager.getLocalizations();
localizations.setCurId(5);
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();
assertEquals(1, id1);
assertEquals(2, id2);
assertEquals(3, id3);
}
@Test
void testGetLocalizations() {
Localizations localizations = this.localizationManager.getLocalizations();
assertNotNull(localizations);
assertEquals(localizations, this.localizationManager.getLocalizations());
}
@Test
void testTranslateCaching() {
Localizations localizations = this.localizationManager.getLocalizations();
Language english = new Language("English", "en");
localizations.getLanguagesInfo().put("en", english);
localizations.getLanguages().put("en", new LinkedHashMap<>(Map.of("1", "Hello")));
this.localizationManager.setLanguage("en");
String result1 = this.localizationManager.translate(1);
String result2 = this.localizationManager.translate(1);
assertEquals("Hello", result1);
assertEquals("Hello", result2);
assertEquals(result1, result2);
}
@Test
void testMultipleLanguages() {
Localizations localizations = this.localizationManager.getLocalizations();
Language english = new Language("English", "en");
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")));
this.localizationManager.setLanguage("en");
String englishGreeting = this.localizationManager.translate("greeting");
this.localizationManager.setLanguage("de");
String germanGreeting = this.localizationManager.translate("greeting");
assertEquals("Hello", englishGreeting);
assertEquals("Hallo", germanGreeting);
}
}