Essentials/Essentials/src/main/java/com/earth2me/essentials/I18n.java

319 lines
12 KiB
Java
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package com.earth2me.essentials;
import com.earth2me.essentials.utils.AdventureUtil;
import net.ess3.api.IEssentials;
import org.jetbrains.annotations.NotNull;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;
import java.nio.charset.StandardCharsets;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.MissingResourceException;
import java.util.PropertyResourceBundle;
import java.util.ResourceBundle;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.function.Function;
import java.util.logging.Level;
import java.util.regex.Pattern;
public class I18n implements net.ess3.api.II18n {
private static final String MESSAGES = "messages";
private static final Pattern NODOUBLEMARK = Pattern.compile("''");
private static final ExecutorService BUNDLE_LOADER_EXECUTOR = Executors.newFixedThreadPool(2);
private static final ResourceBundle NULL_BUNDLE = new ResourceBundle() {
@SuppressWarnings("NullableProblems")
public Enumeration<String> getKeys() {
return null;
}
protected Object handleGetObject(final @NotNull String key) {
return null;
}
};
private static I18n instance;
private final transient Locale defaultLocale = Locale.getDefault();
private final transient ResourceBundle defaultBundle;
private final transient IEssentials ess;
private transient Locale currentLocale = defaultLocale;
private final transient Map<Locale, ResourceBundle> loadedBundles = new ConcurrentHashMap<>();
private final transient List<Locale> loadingBundles = new ArrayList<>();
private transient ResourceBundle localeBundle;
private final transient Map<Locale, Map<String, MessageFormat>> messageFormatCache = new HashMap<>();
public I18n(final IEssentials ess) {
this.ess = ess;
defaultBundle = ResourceBundle.getBundle(MESSAGES, Locale.ENGLISH, new UTF8PropertiesControl());
localeBundle = defaultBundle;
}
/**
* Translates a message using the server's configured locale.
* @param tlKey The translation key.
* @param objects Translation parameters, if applicable. Note: by default, these will not be parsed for MiniMessage.
* @return The translated message.
* @see AdventureUtil#parsed(String)
*/
public static String tlLiteral(final String tlKey, final Object... objects) {
if (instance == null) {
return "";
}
return tlLocale(instance.currentLocale, tlKey, objects);
}
/**
* Translates a message using the provided locale.
* @param locale The locale to translate the key to.
* @param tlKey The translation key.
* @param objects Translation parameters, if applicable. Note: by default, these will not be parsed for MiniMessage.
* @return The translated message.
* @see AdventureUtil#parsed(String)
*/
public static String tlLocale(final Locale locale, final String tlKey, final Object... objects) {
if (instance == null) {
return "";
}
if (objects.length == 0) {
return NODOUBLEMARK.matcher(instance.translate(locale, tlKey)).replaceAll("'");
} else {
return instance.format(tlKey, objects);
}
}
public static String capitalCase(final String input) {
return input == null || input.isEmpty() ? input : input.toUpperCase(Locale.ENGLISH).charAt(0) + input.toLowerCase(Locale.ENGLISH).substring(1);
}
public void onEnable() {
instance = this;
}
public void onDisable() {
instance = null;
}
@Override
public Locale getCurrentLocale() {
return currentLocale;
}
/**
* Returns the {@link ResourceBundle} for the given {@link Locale}, if loaded. If a bundle is requested which is
* not loaded, it will be loaded asynchronously and the default bundle will be returned in the meantime.
*/
private ResourceBundle getBundle(final Locale locale) {
if (loadedBundles.containsKey(locale)) {
return loadedBundles.get(locale);
} else {
synchronized (loadingBundles) {
if (!loadingBundles.contains(locale)) {
loadingBundles.add(locale);
BUNDLE_LOADER_EXECUTOR.submit(() -> {
ResourceBundle bundle;
try {
bundle = ResourceBundle.getBundle(MESSAGES, locale, new FileResClassLoader(I18n.class.getClassLoader(), ess), new UTF8PropertiesControl());
} catch (MissingResourceException ex) {
try {
bundle = ResourceBundle.getBundle(MESSAGES, locale, new UTF8PropertiesControl());
} catch (MissingResourceException ex2) {
bundle = NULL_BUNDLE;
}
}
loadedBundles.put(locale, bundle);
synchronized (loadingBundles) {
loadingBundles.remove(locale);
}
});
}
}
return defaultBundle;
}
}
private String translate(final Locale locale, final String string) {
try {
try {
return getBundle(locale).getString(string);
} catch (final MissingResourceException ex) {
return localeBundle.getString(string);
}
} catch (final MissingResourceException ex) {
if (ess != null && ess.getSettings().isDebug()) {
ess.getLogger().log(Level.WARNING, String.format("Missing translation key \"%s\" in translation file %s", ex.getKey(), localeBundle.getLocale().toString()), ex);
}
return defaultBundle.getString(string);
}
}
private String format(final String string, final Object... objects) {
return format(currentLocale, string, objects);
}
private String format(final Locale locale, final String string, final Object... objects) {
String format = translate(locale, string);
MessageFormat messageFormat = messageFormatCache.computeIfAbsent(locale, l -> new HashMap<>()).get(format);
if (messageFormat == null) {
try {
messageFormat = new MessageFormat(format);
} catch (final IllegalArgumentException e) {
ess.getLogger().log(Level.SEVERE, "Invalid Translation key for '" + string + "': " + e.getMessage());
format = format.replaceAll("\\{(\\D*?)}", "\\[$1\\]");
messageFormat = new MessageFormat(format);
}
messageFormatCache.get(locale).put(format, messageFormat);
}
final Object[] processedArgs = mutateArgs(objects, arg -> {
final String str = arg instanceof AdventureUtil.ParsedPlaceholder ? arg.toString() : AdventureUtil.miniMessage().escapeTags(arg.toString());
return AdventureUtil.legacyToMini(str);
});
return messageFormat.format(processedArgs).replace(' ', ' '); // replace nbsp with a space
}
public static Object[] mutateArgs(final Object[] objects, final Function<Object, String> mutator) {
final Object[] args = new Object[objects.length];
for (int i = 0; i < objects.length; i++) {
final Object object = objects[i];
// MessageFormat will format these itself, troll face.
if (object instanceof Number || object instanceof Date || object == null) {
args[i] = object;
continue;
}
args[i] = mutator.apply(object);
}
return args;
}
public void updateLocale(final String loc) {
if (loc != null && !loc.isEmpty()) {
currentLocale = getLocale(loc);
}
ResourceBundle.clearCache();
loadedBundles.clear();
messageFormatCache.clear();
ess.getLogger().log(Level.INFO, String.format("Using locale %s", currentLocale.toString()));
try {
localeBundle = ResourceBundle.getBundle(MESSAGES, currentLocale, new UTF8PropertiesControl());
} catch (final MissingResourceException ex) {
localeBundle = NULL_BUNDLE;
}
}
public static Locale getLocale(final String loc) {
if (loc == null || loc.isEmpty()) {
return instance.currentLocale;
}
final String[] parts = loc.split("[_.]");
if (parts.length == 1) {
return new Locale(parts[0]);
}
if (parts.length == 2) {
return new Locale(parts[0], parts[1]);
}
if (parts.length == 3) {
return new Locale(parts[0], parts[1], parts[2]);
}
return instance.currentLocale;
}
/**
* Attempts to load properties files from the plugin directory before falling back to the jar.
*/
private static class FileResClassLoader extends ClassLoader {
private final transient File messagesFolder;
FileResClassLoader(final ClassLoader classLoader, final IEssentials ess) {
super(classLoader);
this.messagesFolder = new File(ess.getDataFolder(), "messages");
//noinspection ResultOfMethodCallIgnored
this.messagesFolder.mkdirs();
}
@Override
public URL getResource(final String string) {
final File file = new File(messagesFolder, string);
if (file.exists()) {
try {
return file.toURI().toURL();
} catch (final MalformedURLException ignored) {
}
}
return null;
}
@Override
public InputStream getResourceAsStream(final String string) {
final File file = new File(messagesFolder, string);
if (file.exists()) {
try {
return new FileInputStream(file);
} catch (final FileNotFoundException ignored) {
}
}
return null;
}
}
/**
* Reads .properties files as UTF-8 instead of ISO-8859-1, which is the default on Java 8/below.
* Java 9 fixes this by defaulting to UTF-8 for .properties files.
*/
private static class UTF8PropertiesControl extends ResourceBundle.Control {
public ResourceBundle newBundle(final String baseName, final Locale locale, final String format, final ClassLoader loader, final boolean reload) throws IOException {
final String resourceName = toResourceName(toBundleName(baseName, locale), "properties");
ResourceBundle bundle = null;
InputStream stream = null;
if (reload) {
final URL url = loader.getResource(resourceName);
if (url != null) {
final URLConnection connection = url.openConnection();
if (connection != null) {
connection.setUseCaches(false);
stream = connection.getInputStream();
}
}
} else {
stream = loader.getResourceAsStream(resourceName);
}
if (stream != null) {
try {
// use UTF-8 here, this is the important bit
bundle = new PropertyResourceBundle(new InputStreamReader(stream, StandardCharsets.UTF_8));
} finally {
stream.close();
}
}
return bundle;
}
@Override
public Locale getFallbackLocale(String baseName, Locale locale) {
if (baseName == null || locale == null) {
throw new NullPointerException();
}
return null;
}
}
}