OpenTripPlanner/domain-core/src/main/java/org/opentripplanner/core/model/i18n/TranslatedString.java
2026-02-09 16:37:46 +02:00

189 lines
6.8 KiB
Java

package org.opentripplanner.core.model.i18n;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
/**
* This is for translated strings for which translations are read from OSM or GTFS alerts.
* <p>
* This can be translated street names, GTFS alerts and notes.
*
* @author Hannes Junnila
*/
public class TranslatedString implements I18NString, Serializable {
/**
* Store all translations, so we don't get memory overhead for identical strings As this is
* static, it isn't serialized when saving the graph.
*/
private static final HashMap<Map<String, String>, I18NString> TRANSLATION_CACHE = new HashMap<>();
private final Map<String, String> translations = new HashMap<>();
private TranslatedString(Map<String, String> translations) {
for (Map.Entry<String, String> i : translations.entrySet()) {
if (i.getKey() == null) {
this.translations.put(null, i.getValue());
} else {
this.translations.put(i.getKey().toLowerCase(), i.getValue());
}
}
}
public static I18NString getI18NString(String untranslated, String... translations) {
if (translations.length % 2 != 0) {
throw new IllegalStateException("An even number of translations must be supplied.");
}
var map = new HashMap<String, String>();
map.put(null, untranslated);
for (int i = 0; i < translations.length - 1; i += 2) {
map.put(translations[i], translations[i + 1]);
}
return getI18NString(map, false);
}
/**
* Gets a deduplicated I18NString. If the translations only have a single value, return a
* NonLocalizedString, otherwise a TranslatedString. The resulting I18NString is interned for
* memory efficiency.
* <p>
* This should be used when calling this method during graph building. This should not be called
* from a real-time updater as this is not thread-safe and may cause a memory leak.
*
* @param translations A Map of languages and translations, a null language is the default
* translation
* @param forceTranslatedString Should the language information be kept, even when only a single
* translation is provided. This is useful when the language
* information is important or is presented to the user.
*/
public static I18NString getDeduplicatedI18NString(
Map<String, String> translations,
boolean forceTranslatedString
) {
return getI18NString(translations, true, forceTranslatedString);
}
/**
* Gets a non-deduplicated I18NString. If the translations only have a single value, return a
* NonLocalizedString, otherwise a TranslatedString. The resulting I18NString is NOT interned.
* <p>
* This should be used from real-time updaters to avoid memory leaks. For graph building, use
* {@link #getDeduplicatedI18NString(Map, boolean)} instead.
*
* @param translations A Map of languages and translations, a null language is the default
* translation
* @param forceTranslatedString Should the language information be kept, even when only a single
* translation is provided. This is useful when the language
* information is important or is presented to the user.
*/
public static I18NString getI18NString(
Map<String, String> translations,
boolean forceTranslatedString
) {
return getI18NString(translations, false, forceTranslatedString);
}
/**
* Gets an I18NString. If the translations only have a single value, return a NonLocalizedString,
* otherwise a TranslatedString
*
* @param translations A Map of languages and translations, a null language is the default
* translation
* @param intern Should the resulting I18NString be interned. This should be used when calling
* this method during graph building. This should not be called from a real-time
* updater as this is not thread-safe and may cause a memory leak.
* @param forceTranslatedString Should the language information be kept, even when only a single
* translation is provided. This is useful when the language
* information is important or is presented to the user.
*/
static I18NString getI18NString(
Map<String, String> translations,
boolean intern,
boolean forceTranslatedString
) {
if (translations.isEmpty()) {
throw new IllegalArgumentException("At least one translation must be provided");
}
if (TRANSLATION_CACHE.containsKey(translations)) {
return TRANSLATION_CACHE.get(translations);
} else {
I18NString ret;
// Check if we only have one name, even under multiple languages
boolean allValuesEqual = new HashSet<>(translations.values()).size() == 1;
var firstLanguage = translations.keySet().iterator().next();
boolean onlySingleUntranslatedLanguage =
translations.size() == 1 && (firstLanguage == null || firstLanguage.isBlank());
if (forceTranslatedString && !onlySingleUntranslatedLanguage) {
ret = new TranslatedString(translations);
} else if (allValuesEqual) {
ret = new NonLocalizedString(translations.values().iterator().next());
} else {
ret = new TranslatedString(translations);
}
if (intern) {
TRANSLATION_CACHE.put(translations, ret);
}
return ret;
}
}
@Override
public int hashCode() {
return Objects.hash(translations);
}
@Override
public boolean equals(Object other) {
return (
(other instanceof TranslatedString) &&
this.translations.equals(((TranslatedString) other).translations)
);
}
/**
* @return The default translation
*/
@Override
public String toString() {
return translations.containsKey(null)
? translations.get(null)
: translations.values().iterator().next();
}
/**
* @return The available languages
*/
public Collection<String> getLanguages() {
return translations.keySet();
}
/**
* @return The available translations
*/
public List<Entry<String, String>> getTranslations() {
return new ArrayList<>(translations.entrySet());
}
/**
* @param locale Wanted locale
* @return The translation in the wanted language if it exists, otherwise the default translation
*/
@Override
public String toString(Locale locale) {
String language = null;
if (locale != null) {
language = locale.getLanguage().toLowerCase();
}
return translations.containsKey(language) ? translations.get(language) : toString();
}
}