package moe.kyokobot.bot.i18n; import com.google.common.base.Charsets; import com.ibm.icu.text.MessageFormat; import com.ibm.icu.util.ULocale; import com.mewna.catnip.entity.guild.Guild; import com.mewna.catnip.entity.guild.Member; import com.mewna.catnip.entity.user.User; import moe.kyokobot.shared.entity.GuildData; import moe.kyokobot.shared.entity.UserData; import moe.kyokobot.shared.i18n.Language; import org.apache.commons.codec.language.bm.Lang; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.net.URL; import java.util.*; import java.util.concurrent.ConcurrentHashMap; public class I18n { private static final Logger logger = LoggerFactory.getLogger(I18n.class); private Map<Language, MessageMap> languages; @SuppressWarnings("squid:S2629") public void loadMessages() { logger.debug("Loading messages..."); languages = new EnumMap<>(Language.class); for (Language l : Language.values()) { if (l != Language.DEFAULT) try { File f = new File("./messages/" + l.getShortName() + "/messages.properties"); URL url; if (f.exists()) { url = f.toURI().toURL(); } else { url = getClass().getResource("/" + l.getShortName() + "/messages.properties"); if (url == null) { logger.warn("Messages file for language {} does not exist.", l.name()); continue; } } logger.debug("Loaded messages for language {} from {}", l.getLocalized(), url); loadMessages(l, url.openStream()); } catch (Exception e) { logger.error("Error while loading language!", e); } } } /** * Loads messages for given language message map from InputStream. * * @param language The language we want to load messages for. * @param inputStream The input stream which contains message file. */ public void loadMessages(Language language, InputStream inputStream) throws IOException { Properties p = new Properties(); p.load(new InputStreamReader(inputStream, Charsets.UTF_8)); loadMessages(language, p); } /** * Loads messages for given language message map from Properties object. * * @param language The language we want to load messages for. * @param properties Properties instance with messages. */ public void loadMessages(Language language, Properties properties) { var messageMap = languages.computeIfAbsent(language, MessageMap::new); properties.forEach((k, v) -> messageMap.registerMessage((String) k, (String) v)); } /** * @param language The language you want to get message map of. * @return Immutable message map of specified language. */ public Map<String, String> getMessageMap(@NotNull Language language) { return languages.get(language).immutableMap(); } /** * @param language Preferred language for messages. * @param key Translation key. * @return Translated message, English fallback or provided key if missing. */ @NotNull public String get(@NotNull Language language, @NotNull String key) { if (!languages.containsKey(Objects.requireNonNull(language))) { language = Language.ENGLISH; } var map = languages.get(language); var message = map.message(key); if (message == null) { message = languages.get(Language.ENGLISH).message(key); } return Objects.requireNonNullElse(message, key); } /** * @param language Preferred language for messages. * @param key Translation key. * @return MessageFormat object for specified translation key or null if it doesn't exist. */ @Nullable public MessageFormat getICU(@NotNull Language language, @NotNull String key) { if (!languages.containsKey(Objects.requireNonNull(language))) { language = Language.ENGLISH; } var map = languages.get(language); var message = map.getICU(key); if (message == null) { message = languages.get(Language.ENGLISH).getICU(key); } return message; } /** * Gets language of specified guild. * * @param guild The guild which we want to get language of. * @return The language of specified guild. */ public Language getLanguage(@NotNull Guild guild) { try { var l = GuildData.get(guild.idAsLong()).language(); return l == Language.DEFAULT ? Language.ENGLISH : l; } catch (Exception e) { logger.error("Error while getting guild language!", e); return Language.ENGLISH; } } /** * Gets language of specified member. * * @param member The member which we want to get language of. * @return The language of specified member. */ public Language getLanguage(Member member) { try { Language l = UserData.get(member.idAsLong()).language(); return l == Language.DEFAULT ? getLanguage(member.guild()) : l; } catch (Exception e) { logger.error("An error occurred while trying to get user language!", e); return Language.ENGLISH; } } /** * Gets language of specified user. * * @param user The user which we want to get language of. * @return The language of specified user. */ public Language getLanguage(User user) { try { Language l = UserData.get(user.idAsLong()).language(); return l == Language.DEFAULT ? Language.ENGLISH : l; } catch (Exception e) { logger.error("Error while getting user language!", e); return Language.ENGLISH; } } class MessageMap { private final Language language; private final Map<Language, ULocale> uLocaleMap; private final Map<String, String> messages; private final Map<String, MessageFormat> icuCache; MessageMap(Language language) { this.language = language; this.uLocaleMap = new EnumMap<>(Language.class); this.messages = new HashMap<>(); this.icuCache = new ConcurrentHashMap<>(); } void registerMessage(String key, String value) { messages.put(key, value); } String message(String key) { return messages.get(key); } MessageFormat getICU(String key) { return messages.containsKey(key) ? icuCache.computeIfAbsent(key, t -> new MessageFormat(messages.get(key), getULocale(language))) : null; } Map<String, String> immutableMap() { return Collections.unmodifiableMap(messages); } private ULocale getULocale(Language language) { return uLocaleMap.computeIfAbsent(language, l -> ULocale.forLocale(l.getLocale())); } } }