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.storage.StorageServiceAutoConfiguration
de.extio.game_engine.module.ModuleManagerAutoConfiguration
de.extio.game_engine.i18n.LocalizationAutoConfiguration