supportedLocaleKeys;
+ private static final String DEFAULT_LOCALE_KEY = "en";
+ private static final String I18N_ZIP_RESOURCE = "i18n.zip";
+ private static final String SUPPORTED_LOCALES_FILE = "supported-locales.txt";
+ private static final String TRANSLATION_SUPPORT_URL = "https://crowdin.com/project/uskyblock-revived";
+ private static final TagResolver SEMANTIC_STYLE_TAGS = TagResolver.resolver(
+ Placeholder.styling("muted", NamedTextColor.GRAY),
+ Placeholder.styling("primary", NamedTextColor.AQUA),
+ Placeholder.styling("secondary", NamedTextColor.GREEN),
+ Placeholder.styling("cmd", NamedTextColor.AQUA),
+ Placeholder.styling("success", NamedTextColor.GREEN),
+ Placeholder.styling("error", NamedTextColor.RED)
+ );
+
+ /**
+ * Translates the given {@link String} to the configured language and resolves MiniMessage placeholders using the
+ * provided {@link TagResolver}s. Returns the given string if no translation is available. Returns an empty
+ * component if the given key is null or empty.
+ *
+ * @param text String to translate.
+ * @param resolvers MiniMessage tag resolvers.
+ * @return Translated Component.
+ */
+ public static @NotNull Component tr(@Nullable String text, @NotNull TagResolver... resolvers) {
+ return tr(text, null, resolvers);
+ }
+
+ /**
+ * Translates the given {@link String} to the configured language and resolves MiniMessage placeholders using the
+ * provided {@link TagResolver}s. Returns the given string if no translation is available. Returns an empty
+ * component if the given key is null or empty.
+ *
+ * @param text String to translate.
+ * @param resolvers MiniMessage tag resolvers.
+ * @return Translated Component.
+ */
+ public static @NotNull Component tr(@Nullable String text, @Nullable Style style, @NotNull TagResolver... resolvers) {
+ return getI18n().tr(text, style, resolvers);
+ }
/**
- * Translates the given {@link String} to the configured language. Returns the given String if no translation is
- * available. Returns an empty String if the given key is null or empty.
- * @param s String to translate.
- * @return Translated String.
+ * Translates the given {@link String} and serializes to legacy-format text using § color/style codes.
+ *
+ * @param text String to translate.
+ * @return Translated legacy-formatted String.
*/
@NotNull
- public static String tr(@Nullable String s) {
- return getI18n().tr(s);
+ public static String trLegacy(@Nullable String text) {
+ return legacy(tr(text));
}
/**
- * Translates the given {@link String} to the configured language. Formats with the given {@link Object}. Returns
- * the given String if no translation is available. Returns an empty String if the given key is null or empty.
- * @param s String to translate.
+ * Translates and formats the given {@link String}, then serializes to legacy-format text using § color/style codes.
+ *
+ * @param text String to translate.
* @param args Arguments to format.
- * @return Translated String.
+ * @return Translated legacy-formatted String.
+ * @deprecated Use {@link #tr(String, TagResolver...)} instead.
+ */
+ @Deprecated
+ @NotNull
+ public static String trLegacy(@Nullable String text, @Nullable Object... args) {
+ return legacy(getI18n().tr(text, args));
+ }
+
+ /**
+ * Translates the given {@link String}, resolves MiniMessage placeholders using {@link TagResolver}s, and
+ * serializes to legacy-format text using § color/style codes.
+ *
+ * @param text String to translate.
+ * @param resolvers MiniMessage tag resolvers.
+ * @return Translated legacy-formatted String.
+ */
+ @NotNull
+ public static String trLegacy(@Nullable String text, @NotNull TagResolver... resolvers) {
+ return legacy(tr(text, resolvers));
+ }
+
+ /**
+ * Translates the given {@link String}, resolves MiniMessage placeholders using {@link TagResolver}s, and
+ * serializes to legacy-format text using § color/style codes.
+ *
+ * @param text String to translate.
+ * @param resolvers MiniMessage tag resolvers.
+ * @return Translated legacy-formatted String.
+ */
+ @NotNull
+ public static String trLegacy(@Nullable String text, @Nullable Style style, @NotNull TagResolver... resolvers) {
+ return legacy(tr(text, style, resolvers));
+ }
+
+ /**
+ * Creates a named MiniMessage placeholder resolver from a legacy formatted string.
+ *
+ * This is intended for edge-cases where dynamic values still originate from legacy APIs that provide {@code §}
+ * formatting.
+ *
+ * @param name Placeholder name (without angle brackets).
+ * @param legacyValue Value that may contain legacy formatting.
+ * @return Tag resolver that inserts the deserialized component.
+ */
+ @NotNull
+ public static TagResolver legacyArg(@TagPattern @NotNull String name, @Nullable String legacyValue) {
+ Component value = fromLegacy(legacyValue);
+ return Placeholder.component(name, value);
+ }
+
+
+ @NotNull
+ public static Component fromLegacy(@Nullable String legacy) {
+ return legacy != null ? getLegacySerializer().deserialize(legacy) : Component.empty();
+ }
+
+ /**
+ * Returns the URL where users can help improve translations.
+ *
+ * @return Translation contribution URL.
+ */
+ public static @NotNull String getTranslationSupportUrl() {
+ return TRANSLATION_SUPPORT_URL;
+ }
+
+ /**
+ * Returns all supported locale keys discovered from packaged translations and plugin overrides.
+ *
+ * @return Sorted list of locale keys.
+ */
+ public static @NotNull List getSupportedLocaleKeys() {
+ List result = supportedLocaleKeys;
+ if (result == null) {
+ synchronized (LOCK) {
+ result = supportedLocaleKeys;
+ if (result == null) {
+ result = discoverSupportedLocaleKeys();
+ supportedLocaleKeys = result;
+ }
+ }
+ }
+ return result;
+ }
+
+ /**
+ * Finds an exact supported locale key match.
+ * Matching is case-insensitive and treats '-' and '_' as equivalent separators.
+ *
+ * @param localeKey Locale key to match.
+ * @return Exact supported locale key if present.
+ */
+ public static @NotNull Optional findSupportedLocaleKey(@Nullable String localeKey) {
+ if (localeKey == null || localeKey.trim().isEmpty()) {
+ return Optional.empty();
+ }
+ String canonical = canonicalLocaleKey(localeKey);
+ for (String supported : getSupportedLocaleKeys()) {
+ if (canonicalLocaleKey(supported).equals(canonical)) {
+ return Optional.of(supported);
+ }
+ }
+ return Optional.empty();
+ }
+
+ /**
+ * Resolves a locale key to the best supported locale.
+ * Matching order is: exact key, exact locale, language-only, then first supported variant of the same language.
+ *
+ * @param localeKey Locale key to resolve.
+ * @return Best supported locale key, if any.
+ */
+ public static @NotNull Optional resolveSupportedLocaleKey(@Nullable String localeKey) {
+ Optional exact = findSupportedLocaleKey(localeKey);
+ if (exact.isPresent()) {
+ return exact;
+ }
+ Locale parsed = getLocale(localeKey);
+ if (parsed == null) {
+ return Optional.empty();
+ }
+ return resolveSupportedLocaleKey(parsed);
+ }
+
+ /**
+ * Resolves a locale to the best supported locale.
+ * Matching order is: exact locale, language-only, then first supported variant of the same language.
+ *
+ * @param localeToResolve Locale to resolve.
+ * @return Best supported locale key, if any.
+ */
+ public static @NotNull Optional resolveSupportedLocaleKey(@Nullable Locale localeToResolve) {
+ if (localeToResolve == null) {
+ return Optional.empty();
+ }
+ String language = localeToResolve.getLanguage();
+ String country = localeToResolve.getCountry();
+ String variant = localeToResolve.getVariant();
+ if (language.isEmpty()) {
+ return Optional.empty();
+ }
+
+ if (!country.isEmpty() && !variant.isEmpty()) {
+ Optional full = findSupportedLocaleKey(language + "_" + country + "_" + variant);
+ if (full.isPresent()) {
+ return full;
+ }
+ }
+ if (!country.isEmpty()) {
+ Optional langCountry = findSupportedLocaleKey(language + "_" + country);
+ if (langCountry.isPresent()) {
+ return langCountry;
+ }
+ } else if (!variant.isEmpty()) {
+ Optional langVariant = findSupportedLocaleKey(language + "__" + variant);
+ if (langVariant.isPresent()) {
+ return langVariant;
+ }
+ }
+
+ Optional languageOnly = findSupportedLocaleKey(language);
+ if (languageOnly.isPresent()) {
+ return languageOnly;
+ }
+
+ String languagePrefix = canonicalLocaleKey(language + "_");
+ for (String supported : getSupportedLocaleKeys()) {
+ if (canonicalLocaleKey(supported).startsWith(languagePrefix)) {
+ return Optional.of(supported);
+ }
+ }
+ return Optional.empty();
+ }
+
+ private static @NotNull List discoverSupportedLocaleKeys() {
+ Set locales = new TreeSet<>(String.CASE_INSENSITIVE_ORDER);
+ addSupportedLocaleKeysFromPluginFolder(locales);
+ addSupportedLocaleKeysFromZipInJar(locales);
+ locales.add(DEFAULT_LOCALE_KEY);
+ return List.copyOf(locales);
+ }
+
+ private static void addSupportedLocaleKeysFromZipInJar(@NotNull Set locales) {
+ try (
+ InputStream in = I18nUtil.class.getClassLoader().getResourceAsStream(I18N_ZIP_RESOURCE);
+ ZipInputStream zin = in != null ? new ZipInputStream(in, StandardCharsets.UTF_8) : null
+ ) {
+ if (zin == null) {
+ return;
+ }
+ ZipEntry nextEntry;
+ while ((nextEntry = zin.getNextEntry()) != null) {
+ String entryName = nextEntry.getName();
+ if (entryName.equalsIgnoreCase(SUPPORTED_LOCALES_FILE)) {
+ addSupportedLocaleKeysFromStream(locales, zin);
+ } else if (entryName.toLowerCase(Locale.ROOT).endsWith(".po")) {
+ addSupportedLocaleKeyFromPath(locales, entryName);
+ }
+ }
+ } catch (IOException e) {
+ log.info("Unable to load supported locales from " + I18N_ZIP_RESOURCE + ": " + e);
+ }
+ }
+
+ private static void addSupportedLocaleKeysFromPluginFolder(@NotNull Set locales) {
+ if (dataFolder == null) {
+ return;
+ }
+ File i18nFolder = new File(dataFolder, "i18n");
+ if (!i18nFolder.exists() || !i18nFolder.isDirectory()) {
+ return;
+ }
+
+ File supportedFile = new File(i18nFolder, SUPPORTED_LOCALES_FILE);
+ if (supportedFile.exists() && supportedFile.isFile()) {
+ try (InputStream in = new FileInputStream(supportedFile)) {
+ addSupportedLocaleKeysFromStream(locales, in);
+ } catch (IOException e) {
+ log.info("Unable to read supported locales from " + supportedFile + ": " + e);
+ }
+ }
+
+ File[] poFiles = i18nFolder.listFiles((dir, name) -> name.toLowerCase(Locale.ROOT).endsWith(".po"));
+ if (poFiles != null) {
+ for (File poFile : poFiles) {
+ String name = poFile.getName();
+ int suffixIndex = name.toLowerCase(Locale.ROOT).lastIndexOf(".po");
+ if (suffixIndex > 0) {
+ locales.add(name.substring(0, suffixIndex));
+ }
+ }
+ }
+ }
+
+ private static void addSupportedLocaleKeysFromStream(@NotNull Set locales, @NotNull InputStream in) throws IOException {
+ BufferedReader reader = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8));
+ String line;
+ while ((line = reader.readLine()) != null) {
+ String trimmed = line.trim();
+ if (!trimmed.isEmpty() && !trimmed.startsWith("#")) {
+ locales.add(trimmed);
+ }
+ }
+ }
+
+ private static void addSupportedLocaleKeyFromPath(@NotNull Set locales, @NotNull String path) {
+ int slash = path.lastIndexOf('/');
+ String fileName = slash >= 0 ? path.substring(slash + 1) : path;
+ int suffixIndex = fileName.toLowerCase(Locale.ROOT).lastIndexOf(".po");
+ if (suffixIndex > 0) {
+ locales.add(fileName.substring(0, suffixIndex));
+ }
+ }
+
+ private static @NotNull String canonicalLocaleKey(@NotNull String localeKey) {
+ return localeKey.trim().replace('-', '_').toLowerCase(Locale.ROOT);
+ }
+
+ /**
+ * Converts a {@link Component} to legacy-formatted text using § color/style codes.
+ *
+ * @param component component to serialize.
+ * @return legacy-formatted string.
*/
@NotNull
- public static String tr(@Nullable String s, @Nullable Object... args) {
- return getI18n().tr(s, args);
+ public static String legacy(@Nullable Component component) {
+ if (component == null) {
+ return "";
+ }
+ return getLegacySerializer().serialize(component);
}
/**
* Marks the given {@link String} for translation for the .po files.
- * @param key String to mark.
+ *
+ * @param text String to mark.
* @return Input String.
*/
- @Contract("null -> null")
- public static String marktr(@Nullable String key) {
- return key;
+ @Contract(value = "null -> null", pure = true)
+ public static String marktr(@Nullable String text) {
+ return text;
}
/**
- * Formats the given {@link String} without translating. Returns an empty String if the given String is
- * null or empty.
- * @param s String to format.
- * @param args Arguments for formatting.
- * @return Formatted String.
+ * Formats the given MiniMessage string without translating and resolves named placeholders.
+ * Returns an empty String if the given String is null or empty.
+ *
+ * @param text String as Adventure MiniMessage.
+ * @param resolvers MiniMessage resolvers.
+ * @return Formatted legacy String with color codes.
*/
@NotNull
- public static String pre(@Nullable String s, @Nullable Object... args) {
- if (s != null && !s.isEmpty()) {
- new MessageFormat(s, getLocale());
- return MessageFormat.format(s, args);
+ public static String miniToLegacy(@Nullable String text, @NotNull TagResolver... resolvers) {
+ if (text != null && !text.isEmpty()) {
+ return legacy(deserializeMiniMessage(text, resolvers));
}
return "";
}
+ /**
+ * Formats the given MiniMessage string without translating and resolves named placeholders.
+ *
+ * @param text String as Adventure MiniMessage.
+ * @param resolvers MiniMessage resolvers.
+ * @return Formatted text as a component.
+ */
+ @NotNull
+ public static Component parseMini(@Nullable String text, @NotNull TagResolver... resolvers) {
+ if (text != null && !text.isEmpty()) {
+ return deserializeMiniMessage(text, resolvers);
+ }
+ return Component.empty();
+ }
+
/**
* Gets the {@link I18n} instance representing the configured {@link Locale}. Lazy-loads if necessary.
+ *
* @return I18n instance for the configured locale.
*/
public static I18n getI18n() {
- if (i18n == null) {
+ I18n result = i18n;
+ if (result == null) {
+ throw new IllegalStateException("I18nUtil not initialized!");
+ }
+ return result;
+ }
+
+ /**
+ * Initializes the I18nUtil. This method is called whenever the plugin loads or reloads.
+ * Thread safety is ensured by synchronizing modifications of shared fields.
+ *
+ * @param folder The plugin's data folder.
+ * @param locale The desired Locale. If null, Locale.ENGLISH is used.
+ */
+ public static void initialize(@NotNull File folder, @Nullable Locale locale) {
+ synchronized (LOCK) {
+ dataFolder = folder;
+ I18nUtil.locale = locale;
i18n = new I18n(getLocale());
+ supportedLocaleKeys = null;
}
- return i18n;
}
/**
- * Returns the configured {@link Locale} or the default if unset.
+ * Returns the configured {@link Locale} or the default (Locale.ENGLISH) if unset.
+ *
* @return Configured Locale.
*/
@NotNull
@@ -99,50 +456,92 @@ public static Locale getLocale() {
/**
* Sets the {@link Locale}. Resets to the default locale if NULL is given.
+ * The I18n cache is cleared so that translations are reloaded.
+ *
* @param locale Locale to set.
*/
public static void setLocale(@Nullable Locale locale) {
- I18nUtil.locale = locale;
- clearCache();
- }
-
- /**
- * Sets the datafolder that is used to look for .po files.
- * @param folder Location of the datafolder.
- */
- public static void setDataFolder(@NotNull File folder) {
- dataFolder = folder;
- clearCache();
+ synchronized (LOCK) {
+ I18nUtil.locale = locale;
+ clearCache();
+ }
}
/**
- * Clears the I18n cache, forces a reload of the .po files the next time that {@link I18nUtil#getLocale()} is
- * accessed.
+ * Clears the I18n cache, forcing a reload of the .po files the next time that {@link I18nUtil#getLocale()} is accessed.
*/
public static void clearCache() {
- i18n = null;
+ synchronized (LOCK) {
+ i18n = new I18n(getLocale());
+ supportedLocaleKeys = null;
+ }
}
/**
* Converts the given {@link String} to a {@link Locale}.
- * @param lang Language code..
+ *
+ * @param language Language code..
* @return Locale based on the given string.
*/
- @Contract("null -> null")
- public static Locale getLocale(@Nullable String lang) {
- if (lang != null) {
- String[] parts = lang.split("[_\\-]");
+ @Contract(value = "null -> null", pure = true)
+ public static Locale getLocale(@Nullable String language) {
+ if (language != null) {
+ String[] parts = language.split("[-_]");
if (parts.length >= 3) {
- return new Locale(parts[0], parts[1], parts[2]);
+ return Locale.of(parts[0], parts[1], parts[2]);
} else if (parts.length == 2) {
- return new Locale(parts[0], parts[1]);
+ return Locale.of(parts[0], parts[1]);
} else {
- return new Locale(parts[0]);
+ return Locale.of(parts[0]);
}
}
return null;
}
+ private static @NotNull LegacyComponentSerializer getLegacySerializer() {
+ return LegacyComponentSerializer.legacySection();
+ }
+
+ private static @NotNull MiniMessage getMiniMessage() {
+ return MiniMessage.miniMessage();
+ }
+
+ @Deprecated
+ private static @Nullable Object[] normalizeArgsForMiniMessage(@Nullable Object[] args) {
+ if (args == null) {
+ return null;
+ }
+ Object[] normalized = new Object[args.length];
+ for (int i = 0; i < args.length; i++) {
+ Object arg = args[i];
+ if (arg instanceof Component component) {
+ normalized[i] = getMiniMessage().serialize(component);
+ } else {
+ normalized[i] = arg;
+ }
+ }
+ return normalized;
+ }
+
+ private static @NotNull Component deserializeMiniMessage(@NotNull String message, @NotNull TagResolver... resolvers) {
+ try {
+ return getMiniMessage().deserialize(message, resolveSemanticTags(resolvers));
+ } catch (RuntimeException e) {
+ // No legacy conversion/parsing path: preserve input as plain text if MiniMessage parsing fails.
+ return Component.text(message);
+ }
+ }
+
+ private static @NotNull TagResolver resolveSemanticTags(@NotNull TagResolver... resolvers) {
+ if (resolvers == null || resolvers.length == 0) {
+ return SEMANTIC_STYLE_TAGS;
+ }
+ TagResolver[] combined = new TagResolver[resolvers.length + 1];
+ combined[0] = SEMANTIC_STYLE_TAGS;
+ System.arraycopy(resolvers, 0, combined, 1, resolvers.length);
+ return TagResolver.resolver(combined);
+ }
+
/**
* Proxy between uSkyBlock and org.xnap.commons.i18n.I18n
*/
@@ -152,86 +551,145 @@ public static class I18n {
I18n(Locale locale) {
this.locale = locale;
+ List localeCandidates = getLocaleCandidates();
// Order of these calls is important here, because it specifies the priority of the files (git rlf/1233).
- addPropertiesFromZipInJar().ifPresent(properties -> {
- translations.putAll(properties);
- log.log(Level.INFO, "Added {0} translations from ZIP inside JAR.", properties.size());
- });
- addPropertiesFromJar().ifPresent(properties -> {
- translations.putAll(properties);
- log.log(Level.INFO, "Added {0} translations from the JAR.", properties.size());
- });
- addPropertiesFromPluginFolder().ifPresent(properties -> {
- translations.putAll(properties);
- log.log(Level.INFO, "Added {0} translations from the plugin directory.", properties.size());
- });
+ for (String localeKey : localeCandidates) {
+ addPropertiesFromZipInJar(localeKey).ifPresent(properties -> {
+ translations.putAll(properties);
+ log.log(Level.INFO, "Added {0} translations from ZIP inside JAR ({1}).",
+ new Object[]{properties.size(), localeKey});
+ });
+ }
+ for (String localeKey : localeCandidates) {
+ addPropertiesFromJar(localeKey).ifPresent(properties -> {
+ translations.putAll(properties);
+ log.log(Level.INFO, "Added {0} translations from the JAR ({1}).",
+ new Object[]{properties.size(), localeKey});
+ });
+ }
+ for (String localeKey : localeCandidates) {
+ addPropertiesFromPluginFolder(localeKey).ifPresent(properties -> {
+ translations.putAll(properties);
+ log.log(Level.INFO, "Added {0} translations from the plugin directory ({1}).",
+ new Object[]{properties.size(), localeKey});
+ });
+ }
log.log(Level.INFO, "Loaded {0} translations.", translations.size());
}
- private Optional addPropertiesFromJar() {
- try (InputStream in = getClass().getClassLoader().getResourceAsStream("po/" + locale + ".po")) {
+ private @NotNull List getLocaleCandidates() {
+ List candidates = new ArrayList<>(3);
+ String language = locale.getLanguage();
+ String country = locale.getCountry();
+ String variant = locale.getVariant();
+
+ if (!language.isEmpty()) {
+ candidates.add(language);
+ if (!country.isEmpty()) {
+ candidates.add(language + "_" + country);
+ if (!variant.isEmpty()) {
+ candidates.add(language + "_" + country + "_" + variant);
+ }
+ } else if (!variant.isEmpty()) {
+ candidates.add(language + "__" + variant);
+ }
+ }
+
+ String fullLocale = locale.toString();
+ if (!fullLocale.isEmpty() && !candidates.contains(fullLocale)) {
+ candidates.add(fullLocale);
+ }
+ return candidates;
+ }
+
+ private Optional addPropertiesFromJar(@NotNull String localeKey) {
+ try (InputStream in = getClass().getClassLoader().getResourceAsStream("po/" + localeKey + ".po")) {
if (in == null) {
return Optional.empty();
}
Properties properties = POParser.asProperties(in);
return Optional.ofNullable(properties);
} catch (IOException e) {
- log.info("Unable to read translations from po/" + locale + ".po: " + e);
+ log.info("Unable to read translations from po/" + localeKey + ".po: " + e);
}
return Optional.empty();
}
- private Optional addPropertiesFromZipInJar() {
+ private Optional addPropertiesFromZipInJar(@NotNull String localeKey) {
// We zip the .po files, since they are currently half the footprint of the jar.
try (
- InputStream in = getClass().getClassLoader().getResourceAsStream("i18n.zip");
- ZipInputStream zin = in != null ? new ZipInputStream(in, StandardCharsets.UTF_8) : null
+ InputStream in = getClass().getClassLoader().getResourceAsStream(I18N_ZIP_RESOURCE);
+ ZipInputStream zin = in != null ? new ZipInputStream(in, StandardCharsets.UTF_8) : null
) {
ZipEntry nextEntry;
do {
nextEntry = zin != null ? zin.getNextEntry() : null;
- if (nextEntry != null && nextEntry.getName().equalsIgnoreCase(locale + ".po")) {
+ if (nextEntry != null && nextEntry.getName().equalsIgnoreCase(localeKey + ".po")) {
Properties properties = POParser.asProperties(zin);
return Optional.ofNullable(properties);
}
} while (nextEntry != null);
} catch (IOException e) {
- log.info("Unable to load translations from i18n.zip!" + locale + ".po: " + e);
+ log.info("Unable to load translations from " + I18N_ZIP_RESOURCE + "!" + localeKey + ".po: " + e);
}
return Optional.empty();
}
- private Optional addPropertiesFromPluginFolder() {
- File poFile = new File(dataFolder, "i18n" + File.separator + locale + ".po");
+ private Optional addPropertiesFromPluginFolder(@NotNull String localeKey) {
+ File poFile = new File(dataFolder, "i18n" + File.separator + localeKey + ".po");
if (poFile.exists()) {
try (InputStream in = new FileInputStream(poFile)) {
Properties properties = POParser.asProperties(in);
return Optional.ofNullable(properties);
} catch (IOException e) {
- log.info("Unable to load translations from i18n" + File.separator + locale + ".po: " + e);
+ log.info("Unable to load translations from i18n" + File.separator + localeKey + ".po: " + e);
}
}
return Optional.empty();
}
- public String tr(String key, Object... args) {
- if (key == null || key.trim().isEmpty()) {
- return "";
+ /**
+ * @deprecated Use {@link #tr(String, TagResolver...)} instead.
+ */
+ @Deprecated
+ private @NotNull Component tr(@Nullable String text, @Nullable Object... args) {
+ if (text == null || text.trim().isEmpty()) {
+ return Component.empty();
+ }
+ String translated = translations.getProperty(text);
+ if (translated != null && !translated.trim().isEmpty()) {
+ return format(translated, args);
+ }
+ return format(text, args);
+ }
+
+ public @NotNull Component tr(@Nullable String text, @Nullable Style style, @NotNull TagResolver... resolvers) {
+ if (text == null || text.trim().isEmpty()) {
+ return Component.empty();
+ }
+ String translated = translations.getProperty(text);
+ Component result;
+ if (translated != null && !translated.trim().isEmpty()) {
+ result = deserializeMiniMessage(translated, resolvers);
+ } else {
+ result = deserializeMiniMessage(text, resolvers);
}
- String propKey = translations.getProperty(key);
- if (propKey != null && !propKey.trim().isEmpty()) {
- return format(propKey, args);
+ if (style != null) {
+ result = result.applyFallbackStyle(style);
}
- return format(key, args);
+ return result;
}
- private String format(String propKey, Object[] args) {
+ @Deprecated
+ private @NotNull Component format(@NotNull String text, @Nullable Object[] args) {
try {
- return new MessageFormat(propKey, getLocale()).format(args);
+ Object[] normalizedArgs = normalizeArgsForMiniMessage(args);
+ String formatted = new MessageFormat(text, getLocale()).format(normalizedArgs);
+ return deserializeMiniMessage(formatted);
} catch (IllegalArgumentException e) {
- throw new IllegalArgumentException("Problem with: '" + propKey + "'", e);
+ throw new IllegalArgumentException("Problem with: '" + text + "'", e);
}
}
diff --git a/po-utils/src/test/java/dk/lockfuglsang/minecraft/po/I18nUtilTest.java b/po-utils/src/test/java/dk/lockfuglsang/minecraft/po/I18nUtilTest.java
index 8bb4316c9..5e2958199 100644
--- a/po-utils/src/test/java/dk/lockfuglsang/minecraft/po/I18nUtilTest.java
+++ b/po-utils/src/test/java/dk/lockfuglsang/minecraft/po/I18nUtilTest.java
@@ -1,5 +1,8 @@
package dk.lockfuglsang.minecraft.po;
+import net.kyori.adventure.text.Component;
+import net.kyori.adventure.text.minimessage.tag.resolver.Formatter;
+import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder;
import org.junit.Before;
import org.junit.Test;
@@ -7,24 +10,25 @@
import java.net.URL;
import java.util.Locale;
+import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.core.Is.is;
-import static org.junit.Assert.*;
+import static org.junit.Assert.assertNull;
public class I18nUtilTest {
@Before
- public void setUp() throws Exception {
+ public void setUp() {
URL dataFolderUrl = getClass().getClassLoader().getResource("");
- I18nUtil.setDataFolder(new File(dataFolderUrl.getFile()));
+ I18nUtil.initialize(new File(dataFolderUrl.getFile()), Locale.ENGLISH);
}
@Test
public void testTr_nullKey() {
- assertThat(I18nUtil.tr(null), is(""));
+ assertThat(I18nUtil.legacy(I18nUtil.tr(null)), is(""));
}
@Test
public void testTr_emptyKey() {
- assertThat(I18nUtil.tr(""), is(""));
+ assertThat(I18nUtil.legacy(I18nUtil.tr("")), is(""));
}
@Test
@@ -32,40 +36,79 @@ public void testTr_existingKey() {
String TEST_STRING = "\u00a7eYou do not have access to that island-schematic!";
String TEST_RESULT = "\u00a7eYou have no azzess to the schemz";
- assertThat(I18nUtil.tr(TEST_STRING), is(TEST_RESULT));
+ assertThat(I18nUtil.legacy(I18nUtil.tr(TEST_STRING)), is(TEST_RESULT));
}
@Test
public void testTr_nonExistingKey() {
String TEST_STRING = "\u00a7eYou have no access to that island-schematic!";
- assertThat(I18nUtil.tr(TEST_STRING), is(TEST_STRING));
+ assertThat(I18nUtil.legacy(I18nUtil.tr(TEST_STRING)), is(TEST_STRING));
}
@Test
- public void testTr_existingKeyWithFormatting() {
- String TEST_STRING = "\u00a7eNo active cooldowns for \u00a79{0}\u00a7e found.";
- String TEST_ARG = "linksssofrechts";
- String TEST_RESULT = "\u00a74* \u00a77No expired coolupz for \u00a76" + TEST_ARG + "\u00a77 found.";
+ public void testTr_nonExistingKeyWithUnknownMiniMessageTag() {
+ String TEST_STRING = "Hello !";
+ String TEST_RESULT = "Hello !";
- assertThat(I18nUtil.tr(TEST_STRING, TEST_ARG), is(TEST_RESULT));
+ assertThat(I18nUtil.legacy(I18nUtil.tr(TEST_STRING)), is(TEST_RESULT));
}
@Test
- public void testTr_nonExistingKeyWithFormatting() {
- String TEST_STRING = "\u00a7eThis key is unknown to {0}.";
- String TEST_ARG = "Bukkit";
- String TEST_RESULT = "\u00a7eThis key is unknown to " + TEST_ARG + ".";
+ public void testTr_existingKeyWithLocaleFallbackToLanguageFile() {
+ I18nUtil.setLocale(Locale.US);
+ String TEST_STRING = "\u00a7eYou do not have access to that island-schematic!";
+ String TEST_RESULT = "\u00a7eYou have no azzess to the schemz";
+
+ assertThat(I18nUtil.legacy(I18nUtil.tr(TEST_STRING)), is(TEST_RESULT));
+ }
+
+ @Test
+ public void testTr_nonExistingKeyWithNamedUnparsedPlaceholder() {
+ String TEST_STRING = "Hello !";
+ String TEST_RESULT = "Hello World!";
+
+ assertThat(I18nUtil.legacy(I18nUtil.tr(TEST_STRING, Placeholder.unparsed("name", "World"))), is(TEST_RESULT));
+ }
+
+ @Test
+ public void testTr_nonExistingKeyWithNamedComponentPlaceholder() {
+ String TEST_STRING = "Hello !";
+ String TEST_RESULT = "Hello World!";
+
+ assertThat(I18nUtil.legacy(I18nUtil.tr(TEST_STRING, Placeholder.component("name", Component.text("World")))), is(TEST_RESULT));
+ }
+
+ @Test
+ public void testTrLegacy_nonExistingKeyWithNumberFormatter() {
+ String TEST_STRING = "Value: ";
+ String TEST_RESULT = "Value: 250.25";
- assertThat(I18nUtil.tr(TEST_STRING, TEST_ARG), is(TEST_RESULT));
+ assertThat(I18nUtil.trLegacy(TEST_STRING, Formatter.number("value", 250.25d)), is(TEST_RESULT));
}
@Test
- public void testTr_existingKeyNullArgs() {
- String TEST_STRING = "\u00a7eNo active cooldowns for \u00a79{0}\u00a7e found.";
- String TEST_RESULT = "\u00a74* \u00a77No expired coolupz for \u00a76{0}\u00a77 found.";
+ public void testTrLegacy_withLegacyArgColoredValue() {
+ String TEST_STRING = "Hello !";
+ String TEST_RESULT = "\u00a7eHello \u00a7cWorld\u00a7e!";
- assertThat(I18nUtil.tr(TEST_STRING, (Object[]) null), is(TEST_RESULT));
+ assertThat(I18nUtil.trLegacy(TEST_STRING, I18nUtil.legacyArg("name", "\u00a7cWorld")), is(TEST_RESULT));
+ }
+
+ @Test
+ public void testTrLegacy_withLegacyArgNullValue() {
+ String TEST_STRING = "Hello !";
+ String TEST_RESULT = "Hello !";
+
+ assertThat(I18nUtil.trLegacy(TEST_STRING, I18nUtil.legacyArg("name", null)), is(TEST_RESULT));
+ }
+
+ @Test
+ public void testTr_nonExistingKeyWithEscapedMiniMessageTag() {
+ String TEST_STRING = "Hello \\World";
+ String TEST_RESULT = "Hello World";
+
+ assertThat(I18nUtil.legacy(I18nUtil.tr(TEST_STRING)), is(TEST_RESULT));
}
@Test
@@ -86,29 +129,38 @@ public void testMarktr_withString() {
}
@Test
- public void testPre_nullString() {
- assertThat(I18nUtil.pre(null), is(""));
+ public void testMiniToLegacy_nullString() {
+ assertThat(I18nUtil.miniToLegacy(null), is(""));
}
@Test
- public void testPre_emptyString() {
- assertThat(I18nUtil.pre(""), is(""));
+ public void testMiniToLegacy_emptyString() {
+ assertThat(I18nUtil.miniToLegacy(""), is(""));
}
@Test
- public void testPre_withNonFormattedString() {
+ public void testMiniToLegacy_withNonFormattedString() {
String TEST_STRING = "\u00a7eThis is a test string";
- assertThat(I18nUtil.pre(TEST_STRING), is(TEST_STRING));
+ assertThat(I18nUtil.miniToLegacy(TEST_STRING), is(TEST_STRING));
}
-
+
@Test
- public void testPre_withFormattedString() {
- String TEST_STRING = "\u00a7bThis is a test for {0} regarding {1}.";
- Object[] TEST_ARGS = new String[]{"Jinxert", "Ultimate Skyblock"};
+ public void testMiniToLegacy_withFormattedString() {
+ String TEST_STRING = "This is a test for regarding .";
String TEST_RESULT = "\u00a7bThis is a test for Jinxert regarding Ultimate Skyblock.";
- assertThat(I18nUtil.pre(TEST_STRING, TEST_ARGS), is(TEST_RESULT));
+ assertThat(I18nUtil.miniToLegacy(TEST_STRING,
+ Placeholder.unparsed("user", "Jinxert"),
+ Placeholder.unparsed("plugin", "Ultimate Skyblock")), is(TEST_RESULT));
+ }
+
+ @Test
+ public void testMiniToLegacy_withSemanticAliasTags() {
+ String TEST_STRING = "Use /is home to return.";
+ String TEST_RESULT = "Use \u00a7b/is home\u00a7r \u00a77to return.";
+
+ assertThat(I18nUtil.miniToLegacy(TEST_STRING), is(TEST_RESULT));
}
@Test
diff --git a/po-utils/src/test/java/dk/lockfuglsang/minecraft/po/POParserTest.java b/po-utils/src/test/java/dk/lockfuglsang/minecraft/po/POParserTest.java
index f3edda683..0f7c522f9 100644
--- a/po-utils/src/test/java/dk/lockfuglsang/minecraft/po/POParserTest.java
+++ b/po-utils/src/test/java/dk/lockfuglsang/minecraft/po/POParserTest.java
@@ -7,7 +7,7 @@
import java.util.Properties;
import static org.hamcrest.CoreMatchers.*;
-import static org.junit.Assert.assertThat;
+import static org.hamcrest.MatcherAssert.assertThat;
public class POParserTest {
@@ -74,5 +74,4 @@ private void verifyProps(String name) throws IOException {
assertThat(properties.getProperty(key), is(value));
}
}
-
-}
\ No newline at end of file
+}
diff --git a/pom.xml b/pom.xml
deleted file mode 100644
index 07e3e91ba..000000000
--- a/pom.xml
+++ /dev/null
@@ -1,259 +0,0 @@
-
- 4.0.0
- ovh.uskyblock
- uSkyBlock
- pom
- 3.1.0-SNAPSHOT
- Ultimate SkyBlock
-
-
- 4.16.0
- 4.3.2
- 3.14.0
- 1.12.0
- 4.5.14
- 2.10.1
- 33.1.0-jre
- 23.0.0
- 3.8.6
- ${project.version}
- 1.0.8
- 1.20.6-R0.1-SNAPSHOT
- 1.7
- 7.2.19
- 7.0.9
-
- UTF-8
- ${project.artifactId}
- invalid
- dev
- msgfmt
- msgmerge
-
-
- 2.2
- 4.13.2
- 5.9.0
- 3.12.4
-
-
-
- po-utils
- bukkit-utils
- uSkyBlock-API
- uSkyBlock-APIv2
- uSkyBlock-Core
- uSkyBlock-Plugin
- uSkyBlock-FAWE
- uSkyBlock-AWE370
-
-
-
- scm:git:git://github.com/uskyblock/uSkyBlock.git
- scm:git:git://github.com/uskyblock/uSkyBlock.git
- https://github.com/uskyblock/uSkyBlock.git
-
-
-
-
- internal.repo
- Temporary Staging Repository
- file://${project.build.directory}/mvn-repo
-
-
-
-
- ${finalName}
-
-
-
- org.apache.maven.plugins
- maven-compiler-plugin
- 3.10.1
-
- 17
-
-
-
- org.apache.maven.plugins
- maven-jar-plugin
- 3.3.0
-
-
- org.apache.maven.plugins
- maven-javadoc-plugin
- 3.4.1
-
-
- org.apache.maven.plugins
- maven-shade-plugin
- 3.4.1
-
-
- org.apache.maven.plugins
- maven-source-plugin
- 3.2.1
-
-
- org.apache.maven.plugins
- maven-surefire-plugin
- 3.0.0-M5
-
-
- org.apache.maven.plugins
- maven-failsafe-plugin
- 3.0.0-M5
-
-
-
-
-
- org.apache.maven.plugins
- maven-enforcer-plugin
- 3.0.0
-
-
- enforce-maven
-
- enforce
-
-
-
-
- 3.8.1
-
-
-
-
-
-
-
-
-
-
- spigotmc.org
- https://hub.spigotmc.org/nexus/content/repositories/public
-
-
- papermc
- https://papermc.io/repo/repository/maven-public/
-
-
- sk89q-repo
-
- https://maven.enginehub.org/repo/
-
-
- sonatype-oss-snapshots
- https://oss.sonatype.org/content/repositories/snapshots/
-
-
- CodeMC
- https://repo.codemc.org/repository/maven-public
-
-
- mvdw-software
- https://repo.mvdw-software.com/content/groups/public/
-
-
- uskyblock-dependencies
- https://www.uskyblock.ovh/maven/dependencies/
-
-
- uskyblock-maven
- https://www.uskyblock.ovh/maven/uskyblock/
-
-
-
-
- apache-snapshots
- https://repository.apache.org/snapshots/
-
-
- uskyblock-dependencies
- https://www.uskyblock.ovh/maven/dependencies/
-
-
-
-
-
- net.milkbowl.vault
- VaultAPI
- true
- ${vault.version}
-
-
- org.bukkit
- bukkit
-
-
-
-
- com.github.rlf
- uSkyBlock-API
- ${api.version}
-
-
- ovh.uskyblock
- uSkyBlock-APIv2
- ${project.version}
-
-
- ovh.uskyblock
- uSkyBlock-Core
- ${project.version}
-
-
- ovh.uskyblock
- uSkyBlock-FAWE
- ${project.version}
-
-
- ovh.uskyblock
- uSkyBlock-AWE370
- ${project.version}
-
-
- ovh.uskyblock
- po-utils
- ${project.version}
-
-
- org.jetbrains
- annotations
- ${jbannotations.version}
-
-
- io.papermc
- paperlib
- ${paperlib.version}
-
-
- net.kyori
- adventure-api
- ${adventure-api.version}
-
-
- net.kyori
- adventure-platform-bukkit
- ${adventure-bukkit.version}
-
-
- com.google.code.gson
- gson
- ${gson.version}
-
-
- org.apache.httpcomponents
- httpclient
- ${apache-http.version}
-
-
- org.apache.maven
- maven-artifact
- ${maven-artifact.version}
-
-
-
-
diff --git a/prerelease.sh b/prerelease.sh
deleted file mode 100644
index 67b11897f..000000000
--- a/prerelease.sh
+++ /dev/null
@@ -1,10 +0,0 @@
-#!/bin/bash
-tag=$( git describe --tags --abbrev=0 )
-lastversion="${tag%-*}"
-tstamp=$( date +%Y%m%d%H%M )
-newversion="$lastversion-$tstamp"
-./changelog.sh $newversion $lastversion > changelog.md
-git commit -a -m "Changelog for $tag"
-git push
-git tag $newversion
-git push --tags
diff --git a/release.sh b/release.sh
deleted file mode 100644
index 741225d4c..000000000
--- a/release.sh
+++ /dev/null
@@ -1,14 +0,0 @@
-#!/bin/bash
-if [[ -z $1 ]]; then
- echo "Usage: $0 "
- exit -1
-fi
-newversion=$1
-tag=$( git describe --tags --abbrev=0 )
-tstamp=$( date +%Y%m%d%H%M )
-lastversion="${tag%-*}"
-./changelog.sh $newversion $lastversion > changelog.md
-git commit -a -m "Changelog for $tag"
-git push
-git tag $newversion
-git push --tags
diff --git a/settings.gradle.kts b/settings.gradle.kts
new file mode 100644
index 000000000..4da7e794c
--- /dev/null
+++ b/settings.gradle.kts
@@ -0,0 +1,12 @@
+/*
+ * This file was generated by the Gradle 'init' task.
+ */
+
+rootProject.name = "uSkyBlock"
+include(":uSkyBlock-FAWE")
+include(":bukkit-utils")
+include(":uSkyBlock-Core")
+include(":uSkyBlock-API")
+include(":uSkyBlock-Plugin")
+include(":po-utils")
+include(":uSkyBlock-APIv2")
diff --git a/uSkyBlock-API/build.gradle.kts b/uSkyBlock-API/build.gradle.kts
new file mode 100644
index 000000000..3466aec60
--- /dev/null
+++ b/uSkyBlock-API/build.gradle.kts
@@ -0,0 +1,20 @@
+/*
+ * This file was generated by the Gradle 'init' task.
+ */
+
+plugins {
+ id("buildlogic.java-conventions")
+}
+
+dependencies {
+ api(libs.org.jetbrains.annotations)
+ api(libs.net.kyori.adventure.api)
+ compileOnly(libs.org.spigotmc.spigot.api)
+}
+
+group = "com.github.rlf"
+description = "uSkyBlock-API"
+
+java {
+ withJavadocJar()
+}
diff --git a/uSkyBlock-API/pom.xml b/uSkyBlock-API/pom.xml
deleted file mode 100644
index 339148212..000000000
--- a/uSkyBlock-API/pom.xml
+++ /dev/null
@@ -1,99 +0,0 @@
-
-
- 4.0.0
-
- com.github.rlf
- uSkyBlock-API
- 3.1.0-SNAPSHOT
-
-
- UTF-8
-
-
-
-
- internal.repo
- Temporary Staging Repository
- file://${project.build.directory}/mvn-repo
-
-
-
-
-
- spigotmc.org
- https://hub.spigotmc.org/nexus/content/repositories/public
-
-
-
-
-
- org.spigotmc
- spigot-api
- 1.20.4-R0.1-SNAPSHOT
- provided
- true
-
-
-
- org.jetbrains
- annotations
- 23.0.0
-
-
-
-
-
- org.apache.maven.plugins
- maven-compiler-plugin
- 3.8.1
-
- 17
-
-
-
- org.apache.maven.plugins
- maven-jar-plugin
- 3.2.0
-
-
- org.apache.maven.plugins
- maven-javadoc-plugin
- 3.2.0
-
- public
- false
- none
-
-
-
- attach-javadocs
-
- jar
-
-
-
-
- javadoc
-
- deploy
-
-
-
-
- org.apache.maven.plugins
- maven-source-plugin
- 3.2.1
-
-
- attach-sources
-
- jar
-
-
-
-
-
-
-
diff --git a/uSkyBlock-API/src/main/java/us/talabrek/ultimateskyblock/api/ChallengeCompletion.java b/uSkyBlock-API/src/main/java/us/talabrek/ultimateskyblock/api/ChallengeCompletion.java
index 9d988c1c2..8139172c8 100644
--- a/uSkyBlock-API/src/main/java/us/talabrek/ultimateskyblock/api/ChallengeCompletion.java
+++ b/uSkyBlock-API/src/main/java/us/talabrek/ultimateskyblock/api/ChallengeCompletion.java
@@ -1,12 +1,20 @@
package us.talabrek.ultimateskyblock.api;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.time.Duration;
+import java.time.Instant;
+
/**
* Represents a challenge-completion.
+ *
* @since 2.7.0
*/
public interface ChallengeCompletion {
/**
* The name of the challenge.
+ *
* @return name of the challenge.
* @since 2.7.0
*/
@@ -14,13 +22,28 @@ public interface ChallengeCompletion {
/**
* The timestamp at which the cooldown runs out.
+ *
* @return The timestamp at which the cooldown runs out.
* @since 2.7.0
+ * @deprecated Use {@link #cooldownUntil()} instead.
*/
- long getCooldownUntil();
+ @Deprecated(since = "3.2.0")
+ default long getCooldownUntil() {
+ Instant cooldownUntil = cooldownUntil();
+ return cooldownUntil == null ? 0 : cooldownUntil.toEpochMilli();
+ }
+
+ /**
+ * The timestamp at which the cooldown runs out.
+ *
+ * @return The timestamp at which the cooldown runs out, or null if there is no cooldown.
+ * @since 3.2.0
+ */
+ @Nullable Instant cooldownUntil();
/**
* Whether or not the challenge is currently on cooldown.
+ *
* @return Whether or not the challenge is currently on cooldown.
* @since 2.7.0
*/
@@ -28,13 +51,27 @@ public interface ChallengeCompletion {
/**
* How many milliseconds of the cooldown is left
+ *
* @return How many milliseconds of the cooldown is left
* @since 2.7.0
+ * @deprecated Use {@link #getCooldown()} instead.
+ */
+ @Deprecated(since = "3.2.0")
+ default long getCooldownInMillis() {
+ return getCooldown().toMillis();
+ }
+
+ /**
+ * How much of the cooldown is left
+ *
+ * @return The duration of the cooldown that is left
+ * @since 3.2.0
*/
- long getCooldownInMillis();
+ @NotNull Duration getCooldown();
/**
* Total number of times the challenge has been completed
+ *
* @return Total number of times the challenge has been completed
* @since 2.7.0
*/
@@ -42,6 +79,7 @@ public interface ChallengeCompletion {
/**
* Number of times the challenge has been completed within this cooldown.
+ *
* @return Number of times the challenge has been completed within this cooldown.
* @since 2.7.0
*/
diff --git a/uSkyBlock-API/src/main/java/us/talabrek/ultimateskyblock/api/IslandInfo.java b/uSkyBlock-API/src/main/java/us/talabrek/ultimateskyblock/api/IslandInfo.java
index 37cf90b94..14dc4d6e1 100644
--- a/uSkyBlock-API/src/main/java/us/talabrek/ultimateskyblock/api/IslandInfo.java
+++ b/uSkyBlock-API/src/main/java/us/talabrek/ultimateskyblock/api/IslandInfo.java
@@ -3,11 +3,13 @@
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.OfflinePlayer;
+import org.bukkit.block.Biome;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.List;
+import java.util.Locale;
import java.util.Map;
import java.util.Set;
@@ -47,13 +49,21 @@ public interface IslandInfo {
int getMaxVillagers();
/**
- * Returns the maximum number of golems (snowmen and iron-golems) that can spawn on this island.
+ * Returns the maximum number of golems (except copper golems) that can spawn on this island.
*
* Note: Only enforced if spawn-limits
are enabled in config.yml
* @return the maximum number of golems that can spawn on this island.
*/
int getMaxGolems();
+ /**
+ * Returns the maximum number of copper-golems that can spawn on this island.
+ *
+ * Note: Only enforced if spawn-limits
are enabled in config.yml
+ * @return the maximum number of copper-golems that can spawn on this island.
+ */
+ int getMaxCopperGolems();
+
/**
* Returns the maximum number of blocks of limited types that can be placed on the island.
*
@@ -75,9 +85,28 @@ public interface IslandInfo {
/**
* The name of the biome.
+ *
+ * @deprecated Unsafe String value, use {@link #getIslandBiome()} or {@link #getBiomeName()} instead.
+ * @return The name of the biome.
+ */
+ @Deprecated(since="3.1.0")
+ default String getBiome() {
+ return getIslandBiome().name().toUpperCase(Locale.ROOT);
+ }
+
+ /**
+ * The biome of the island.
+ *
+ * @return The iceland's biome.
+ */
+ Biome getIslandBiome();
+
+ /**
+ * The name of the biome.
+ *
* @return The name of the biome.
*/
- String getBiome();
+ String getBiomeName();
/**
* The current party-size of the island.
@@ -100,6 +129,7 @@ public interface IslandInfo {
* @return True if the player has been banned from this island.
* @deprecated Use {@link IslandInfo#isBanned(OfflinePlayer)}
*/
+ @Deprecated(since = "2.7.10")
boolean isBanned(Player player);
/**
@@ -164,6 +194,7 @@ public interface IslandInfo {
* @return List of players trusted on this island.
* @deprecated Use #getTrusteeUUIDs instead
*/
+ @Deprecated(since = "2.7")
List getTrustees();
/**
diff --git a/uSkyBlock-API/src/main/java/us/talabrek/ultimateskyblock/api/UpdateChecker.java b/uSkyBlock-API/src/main/java/us/talabrek/ultimateskyblock/api/UpdateChecker.java
index 35a27242b..db79ef63b 100644
--- a/uSkyBlock-API/src/main/java/us/talabrek/ultimateskyblock/api/UpdateChecker.java
+++ b/uSkyBlock-API/src/main/java/us/talabrek/ultimateskyblock/api/UpdateChecker.java
@@ -29,7 +29,7 @@ public interface UpdateChecker {
@NotNull String getCurrentVersion();
/**
- * Fetches the latest version info from the uSkyBlock website. Returns a {@link CompletableFuture },
+ * Fetches the latest version info from the uSkyBlock website. Returns a {@link CompletableFuture},
* completes the HTTP request async. The CompletableFuture will contain NULL when version info cannot be obtained.
* @param uri URI to use for the HTTP request, official links are
* {@link UpdateChecker#URL_RELEASE} and {@link UpdateChecker#URL_STAGING}.
@@ -40,7 +40,7 @@ public interface UpdateChecker {
/**
* Compares two version numbers. Returns a negative integer, zero, or a positive integer as this
* object is less than, equal to, or greater than the specified object.
- * @see Comparable#compareTo(Object).
+ * @see Comparable#compareTo(Object)
* @param currentVersion Current version number (may contain -SNAPSHOT).
* @param newVersion New version number (may contain -SNAPSHOT).
* @return Negative integer, zero, or a positive integer as this object is less than,
diff --git a/uSkyBlock-API/src/main/java/us/talabrek/ultimateskyblock/api/model/BlockScore.java b/uSkyBlock-API/src/main/java/us/talabrek/ultimateskyblock/api/model/BlockScore.java
index 4d5456f4d..a249d0675 100644
--- a/uSkyBlock-API/src/main/java/us/talabrek/ultimateskyblock/api/model/BlockScore.java
+++ b/uSkyBlock-API/src/main/java/us/talabrek/ultimateskyblock/api/model/BlockScore.java
@@ -1,6 +1,9 @@
package us.talabrek.ultimateskyblock.api.model;
+import net.kyori.adventure.text.Component;
import org.bukkit.ChatColor;
+import org.bukkit.Material;
+import org.bukkit.block.data.BlockData;
import org.bukkit.inventory.ItemStack;
/**
@@ -14,8 +17,25 @@ public interface BlockScore {
*
* @return The type of block.
* @since v2.1.2
+ * @deprecated Converting a BlockData to an ItemStack is not supported in Minecraft. Use #getBlockData() instead.
*/
- ItemStack getBlock();
+ @Deprecated(since = "v3.1.0")
+ default ItemStack getBlock() {
+ Material material = getBlockData().getMaterial();
+ if (material.isItem()) {
+ return new ItemStack(material);
+ } else {
+ throw new UnsupportedOperationException("BlockData is not an item. Use getBlockData() instead.");
+ }
+ }
+
+ /**
+ * The type of block.
+ *
+ * @return The type of block.
+ * @since v3.1.0
+ */
+ BlockData getBlockData();
/**
* The number of blocks of this type found on the island.
@@ -48,9 +68,21 @@ public interface BlockScore {
*
* @return User displayable name of the block.
* @since v2.1.2
+ * @deprecated Use {@link #getComponentName()} instead, which supports color and formatting.
*/
+ @Deprecated
String getName();
+ /**
+ * User displayable name of the block.
+ *
+ * I.e. "Diamond Block".
+ *
+ * @return User displayable name of the block.
+ * @since v3.3.0
+ */
+ Component getComponentName();
+
/**
* The possible states of a BlockScore.
*
diff --git a/uSkyBlock-APIv2/build.gradle.kts b/uSkyBlock-APIv2/build.gradle.kts
new file mode 100644
index 000000000..141e50d41
--- /dev/null
+++ b/uSkyBlock-APIv2/build.gradle.kts
@@ -0,0 +1,18 @@
+/*
+ * This file was generated by the Gradle 'init' task.
+ */
+
+plugins {
+ id("buildlogic.java-conventions")
+}
+
+dependencies {
+ api(libs.org.jetbrains.annotations)
+ compileOnly(libs.org.spigotmc.spigot.api)
+}
+
+description = "uSkyBlock-APIv2"
+
+java {
+ withJavadocJar()
+}
diff --git a/uSkyBlock-APIv2/pom.xml b/uSkyBlock-APIv2/pom.xml
deleted file mode 100644
index 10bd75876..000000000
--- a/uSkyBlock-APIv2/pom.xml
+++ /dev/null
@@ -1,78 +0,0 @@
-
-
-
- uSkyBlock
- ovh.uskyblock
- 3.1.0-SNAPSHOT
-
- 4.0.0
- jar
- uSkyBlock-APIv2
-
-
-
- org.spigotmc
- spigot-api
- ${spigotapi.version}
- provided
- true
-
-
- org.jetbrains
- annotations
-
-
-
-
-
-
- maven-compiler-plugin
-
- 17
- utf-8
-
-
-
- org.apache.maven.plugins
- maven-jar-plugin
-
-
- org.apache.maven.plugins
- maven-javadoc-plugin
-
- public
- false
- none
-
-
-
- attach-javadocs
-
- jar
-
-
-
-
- javadoc
-
- deploy
-
-
-
-
- org.apache.maven.plugins
- maven-source-plugin
-
-
- attach-sources
-
- jar
-
-
-
-
-
-
-
diff --git a/uSkyBlock-APIv2/src/main/java/us/talabrek/ultimateskyblock/api/plugin/UpdateChecker.java b/uSkyBlock-APIv2/src/main/java/us/talabrek/ultimateskyblock/api/plugin/UpdateChecker.java
index 39d874389..e52654346 100644
--- a/uSkyBlock-APIv2/src/main/java/us/talabrek/ultimateskyblock/api/plugin/UpdateChecker.java
+++ b/uSkyBlock-APIv2/src/main/java/us/talabrek/ultimateskyblock/api/plugin/UpdateChecker.java
@@ -29,7 +29,7 @@ public interface UpdateChecker {
@NotNull String getCurrentVersion();
/**
- * Fetches the latest version info from the uSkyBlock website. Returns a {@link CompletableFuture },
+ * Fetches the latest version info from the uSkyBlock website. Returns a {@link CompletableFuture},
* completes the HTTP request async. The CompletableFuture will contain NULL when version info cannot be obtained.
* @param uri URI to use for the HTTP request, official links are
* {@link UpdateChecker#URL_RELEASE} and {@link UpdateChecker#URL_STAGING}.
@@ -40,7 +40,7 @@ public interface UpdateChecker {
/**
* Compares two version numbers. Returns a negative integer, zero, or a positive integer as this
* object is less than, equal to, or greater than the specified object.
- * @see Comparable#compareTo(Object).
+ * @see Comparable#compareTo(Object)
* @param currentVersion Current version number (may contain -SNAPSHOT).
* @param newVersion New version number (may contain -SNAPSHOT).
* @return Negative integer, zero, or a positive integer as this object is less than,
diff --git a/uSkyBlock-AWE370/README.md b/uSkyBlock-AWE370/README.md
deleted file mode 100644
index acc7ad06e..000000000
--- a/uSkyBlock-AWE370/README.md
+++ /dev/null
@@ -1,5 +0,0 @@
-# uSkyBlock-AsyncWorldEdit 3.7.0
-
-This module holds the AWE 3.7.0 dependent integration (AsyncWorldEdit-API 2.2.0).
-
-It is a separate module, so we are able to compile (and test) it in a controlled manner (side by side the AWE211 integration).
diff --git a/uSkyBlock-AWE370/pom.xml b/uSkyBlock-AWE370/pom.xml
deleted file mode 100644
index 94be33f46..000000000
--- a/uSkyBlock-AWE370/pom.xml
+++ /dev/null
@@ -1,68 +0,0 @@
-
-
-
- uSkyBlock
- ovh.uskyblock
- 3.1.0-SNAPSHOT
-
- 4.0.0
- uSkyBlock-AWE370
-
-
-
- org.primesoft.asyncworldedit
- AsyncWorldEdit-API
- [2.2.0-rc-01, 2.2.0)
- jar
-
-
- com.sk89q.worldedit
- *
-
-
-
-
- ovh.uskyblock
- uSkyBlock-Core
-
-
- org.spigotmc
- spigot-api
- ${spigotapi.version}
- true
- provided
-
-
-
- com.sk89q.worldedit
- worldedit-bukkit
- ${worldedit.version}
- provided
-
-
- org.bukkit
- bukkit
-
-
-
-
-
-
-
-
- org.apache.maven.plugins
- maven-source-plugin
-
-
- attach-sources
-
- jar
-
-
-
-
-
-
-
diff --git a/uSkyBlock-AWE370/src/main/java/us/talabrek/ultimateskyblock/handler/asyncworldedit/AWE370Adaptor.java b/uSkyBlock-AWE370/src/main/java/us/talabrek/ultimateskyblock/handler/asyncworldedit/AWE370Adaptor.java
deleted file mode 100644
index ff675a560..000000000
--- a/uSkyBlock-AWE370/src/main/java/us/talabrek/ultimateskyblock/handler/asyncworldedit/AWE370Adaptor.java
+++ /dev/null
@@ -1,200 +0,0 @@
-package us.talabrek.ultimateskyblock.handler.asyncworldedit;
-
-import com.sk89q.worldedit.EditSession;
-import com.sk89q.worldedit.MaxChangedBlocksException;
-import com.sk89q.worldedit.WorldEdit;
-import com.sk89q.worldedit.bukkit.BukkitWorld;
-import com.sk89q.worldedit.extent.clipboard.Clipboard;
-import com.sk89q.worldedit.extent.clipboard.io.ClipboardFormat;
-import com.sk89q.worldedit.extent.clipboard.io.ClipboardFormats;
-import com.sk89q.worldedit.function.operation.Operation;
-import com.sk89q.worldedit.function.operation.Operations;
-import com.sk89q.worldedit.math.BlockVector3;
-import com.sk89q.worldedit.regions.Region;
-import com.sk89q.worldedit.session.ClipboardHolder;
-import com.sk89q.worldedit.world.World;
-import org.bukkit.Bukkit;
-import org.bukkit.Location;
-import org.bukkit.entity.Player;
-import org.bukkit.scheduler.BukkitTask;
-import org.primesoft.asyncworldedit.api.IAsyncWorldEdit;
-import org.primesoft.asyncworldedit.api.blockPlacer.IBlockPlacerPlayer;
-import org.primesoft.asyncworldedit.api.playerManager.IPlayerEntry;
-import org.primesoft.asyncworldedit.api.playerManager.IPlayerManager;
-import org.primesoft.asyncworldedit.api.utils.IFuncParamEx;
-import org.primesoft.asyncworldedit.api.worldedit.IAsyncEditSessionFactory;
-import org.primesoft.asyncworldedit.api.worldedit.ICancelabeEditSession;
-import org.primesoft.asyncworldedit.api.worldedit.IThreadSafeEditSession;
-import us.talabrek.ultimateskyblock.Settings;
-import us.talabrek.ultimateskyblock.handler.AsyncWorldEditHandler;
-import us.talabrek.ultimateskyblock.player.PlayerPerk;
-import us.talabrek.ultimateskyblock.uSkyBlock;
-
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.util.Collections;
-import java.util.Iterator;
-import java.util.LinkedHashSet;
-import java.util.Set;
-import java.util.logging.Level;
-import java.util.logging.Logger;
-
-/**
- * Adaptor depending on AWE 3.7.x classes
- */
-public class AWE370Adaptor implements AWEAdaptor {
- private static final Logger log = Logger.getLogger(AWE370Adaptor.class.getName());
- static long progressEveryMs = 3000; // 2 seconds
- static double progressEveryPct = 20;
- private static final Set pendingJobs = Collections.synchronizedSet(new LinkedHashSet<>());
- private uSkyBlock plugin;
-
- private static void updateProgress(IPlayerEntry playerEntry, int queuedBlocks, int maxQueuedBlocks) {
- if (maxQueuedBlocks <= 1) {
- return; // Not the "real" number of blocks... just ignore...
- }
- if (playerEntry != null && playerEntry.isUnknown() && playerEntry.getAweMode()) {
- synchronized (pendingJobs) {
- if (queuedBlocks == maxQueuedBlocks) {
- // Either a fresh job, or a new merge
- markJobs(maxQueuedBlocks, 0);
- }
- int blocksPlaced = maxQueuedBlocks - queuedBlocks;
- boolean isFirst = true;
- for (Iterator it = pendingJobs.iterator(); it.hasNext(); ) {
- PlayerJob job = it.next();
- int left = job.progress(blocksPlaced);
- if (left > 0 && isFirst && pendingJobs.size() > 1) {
- it.remove();
- markJobs(blocksPlaced + left, queuedBlocks - left);
- }
- isFirst = false;
- }
- }
- }
- }
-
- private BukkitTask timerTask;
-
- private static void markJobs(int maxQueuedBlocks, int startOffset) {
- synchronized (pendingJobs) {
- int rest = maxQueuedBlocks;
- for (PlayerJob job : pendingJobs) {
- int missing = job.mark(rest, startOffset);
- rest -= missing;
- startOffset += missing;
- }
- }
- }
-
- private static IAsyncWorldEdit getAWE() {
- return (IAsyncWorldEdit) Bukkit.getPluginManager().getPlugin("AsyncWorldEdit");
- }
-
- @Override
- public void onEnable(uSkyBlock plugin) {
- this.plugin = plugin;
- progressEveryMs = plugin.getConfig().getInt("asyncworldedit.progressEveryMs", 3000);
- progressEveryPct = plugin.getConfig().getDouble("asyncworldedit.progressEveryPct", 20);
- }
-
- @Override
- public void registerCompletion(Player player) {
- PlayerJob newJob = new PlayerJob(player, progressEveryMs, progressEveryPct, plugin);
- pendingJobs.remove(newJob);
- pendingJobs.add(newJob);
- }
-
- @Override
- public void loadIslandSchematic(final File file, final Location origin, final PlayerPerk playerPerk) {
- final IAsyncWorldEdit awe = getAWE();
- BukkitWorld bukkitWorld = new BukkitWorld(origin.getWorld());
- int maxBlocks = ((bukkitWorld.getMaxY() - bukkitWorld.getMinY()) * Settings.island_protectionRange * Settings.island_protectionRange);
- IPlayerManager pm = awe.getPlayerManager();
- final IPlayerEntry playerEntry = pm.getUnknownPlayer();
- IThreadSafeEditSession tsSession = (IThreadSafeEditSession) createEditSession(bukkitWorld, maxBlocks);
- IFuncParamEx action = new PasteAction(origin, file);
-
- String jobName = "loadIslandSchematic";
- if (playerPerk != null) {
- Player player = Bukkit.getPlayer(playerPerk.getPlayerInfo().getUniqueId());
- registerCompletion(player);
- jobName = jobName.concat(":" + playerPerk.getPlayerInfo().getPlayerName());
- }
-
- awe.getBlockPlacer().performAsAsyncJob(tsSession, playerEntry, jobName, action);
- if (timerTask != null) {
- timerTask.cancel();
- }
- timerTask = plugin.async(new Runnable() {
- int maxSize = -1;
- @Override
- public void run() {
- IBlockPlacerPlayer playerEvents = awe.getBlockPlacer().getPlayerEvents(playerEntry);
- if (playerEvents != null) {
- int size = playerEvents.getQueue().size();
- if (maxSize == -1 || size > maxSize) {
- maxSize = size;
- }
- updateProgress(playerEntry, size, maxSize);
- } else {
- updateProgress(playerEntry, 0, maxSize);
- timerTask.cancel();
- }
- }
- }, 500, 500);
- }
-
- public EditSession createEditSession(World bukkitWorld, int maxBlocks) {
- WorldEdit worldEdit = WorldEdit.getInstance();
- IAsyncEditSessionFactory sessionFactory = (IAsyncEditSessionFactory) worldEdit.getEditSessionFactory();
- return (EditSession) sessionFactory.getThreadSafeEditSession(bukkitWorld, maxBlocks, null, getAWE().getPlayerManager().getUnknownPlayer());
- }
-
- @Override
- public void regenerate(Region region, Runnable onCompletion) {
- AsyncWorldEditHandler.NULL_ADAPTOR.regenerate(region, onCompletion);
- }
-
- @Override
- public void onDisable(uSkyBlock plugin) {
- }
-
- private static class PasteAction implements IFuncParamEx {
- private final Location origin;
- private final File file;
-
- public PasteAction(Location origin, File file) {
- this.origin = origin;
- this.file = file;
- }
-
- public Integer execute(ICancelabeEditSession editSession) {
- ClipboardFormat format = ClipboardFormats.findByFile(file);
- if (format == null) {
- log.log(Level.SEVERE, "Unable to find schematic format for file {}", file);
- return null;
- }
-
- try (InputStream inStream = new FileInputStream(file)) {
- Clipboard clipboard = format.getReader(inStream).read();
- ClipboardHolder holder = new ClipboardHolder(clipboard);
- editSession.enableQueue();
-
- BlockVector3 to = BlockVector3.at(origin.getBlockX(), origin.getBlockY(), origin.getBlockZ());
- final Operation operation = holder
- .createPaste(editSession)
- .to(to)
- .ignoreAirBlocks(true)
- .build();
- Operations.completeBlindly(operation);
- editSession.flushSession();
- } catch (IOException ex) {
- log.log(Level.WARNING, "Error while pasting schematic", ex);
- }
- return 32768;
- }
- }
-}
diff --git a/uSkyBlock-Core/build.gradle.kts b/uSkyBlock-Core/build.gradle.kts
new file mode 100644
index 000000000..417ba756b
--- /dev/null
+++ b/uSkyBlock-Core/build.gradle.kts
@@ -0,0 +1,455 @@
+import java.util.regex.Matcher
+
+plugins {
+ id("buildlogic.java-conventions")
+}
+
+dependencies {
+ api(project(":bukkit-utils"))
+ api(project(":po-utils"))
+ api(project(":uSkyBlock-API"))
+ api(project(":uSkyBlock-APIv2"))
+ api(libs.io.papermc.paperlib)
+ api(libs.org.bstats.bstats.bukkit)
+ api(libs.com.google.inject.guice)
+ api(libs.org.jetbrains.annotations)
+ testImplementation(testFixtures(project(":bukkit-utils")))
+ testImplementation(libs.org.hamcrest.hamcrest)
+ testImplementation(libs.org.hamcrest.hamcrest.library.x1)
+ testImplementation(libs.junit.junit)
+ testImplementation(libs.org.junit.vintage.junit.vintage.engine)
+ testImplementation(libs.org.mockito.mockito.core)
+ testImplementation(libs.net.kyori.adventure.api)
+ testImplementation(libs.net.kyori.adventure.text.minimessage)
+ testImplementation(libs.net.kyori.adventure.text.serializer.legacy)
+ testImplementation(libs.org.apache.commons.commons.lang3)
+ implementation(libs.net.kyori.adventure.text.serializer.plain)
+ compileOnly(libs.net.milkbowl.vault.vaultapi)
+ compileOnly(libs.org.spigotmc.spigot.api)
+ compileOnly(libs.org.mvplugins.multiverse.core.multiverse.core)
+ compileOnly(libs.org.mvplugins.multiverse.inventories.multiverse.inventories)
+ compileOnly(libs.com.sk89q.worldedit.worldedit.bukkit)
+ testImplementation(libs.com.sk89q.worldedit.worldedit.bukkit)
+ compileOnly(libs.com.sk89q.worldguard.worldguard.bukkit)
+ compileOnly(libs.com.google.guava.guava)
+ compileOnly(libs.com.google.code.gson.gson.x1)
+ compileOnly(libs.be.maximvdw.mvdwplaceholderapi) {
+ exclude(group = "*", module = "*")
+ }
+ compileOnly(libs.net.kyori.adventure.api)
+ compileOnly(libs.net.kyori.adventure.platform.bukkit)
+ compileOnly(libs.net.kyori.adventure.text.minimessage)
+ compileOnly(libs.net.kyori.adventure.text.serializer.legacy)
+ compileOnly(libs.org.apache.commons.commons.lang3)
+ compileOnly(libs.org.apache.httpcomponents.httpclient)
+ compileOnly(libs.org.apache.maven.maven.artifact)
+}
+
+description = "uSkyBlock-Core"
+
+java {
+ withJavadocJar()
+}
+
+val i18nDir = file("src/main/i18n")
+val generatedI18nDir = layout.buildDirectory.dir("generated/i18n")
+val supportedLocalesFile = generatedI18nDir.map { it.file("supported-locales.txt") }
+
+enum class TranslationDomain(
+ val id: String,
+ val precedence: Int
+) {
+ PLAYER_FACING("player_facing", 3),
+ ADMIN_OPS("admin_ops", 2),
+ SYSTEM_DEBUG("system_debug", 1)
+}
+
+val translationDomains = TranslationDomain.entries
+val domainPotFiles = translationDomains.associateWith { domain -> file("$i18nDir/keys.${domain.id}.pot") }
+val domainLocaleDirs = translationDomains.associateWith { domain -> file("$i18nDir/${domain.id}") }
+val mergedPotFile = generatedI18nDir.map { it.file("keys.pot") }
+val combinedExtractionPotFile = generatedI18nDir.map { it.file("keys.all.pot") }
+val mergedLocalesDir = generatedI18nDir.map { it.dir("locales") }
+val generatedExtraTranslations = mapOf(
+ "xx_PIRATE" to generatedI18nDir.map { it.file("xx_PIRATE.po") },
+ "xx_lol_US" to generatedI18nDir.map { it.file("xx_lol_US.po") }
+)
+val generatedExtraLocaleKeys = generatedExtraTranslations.keys
+
+val curatedPotHeader = """
+# uSkyBlock translation template
+# Copyright (C) 2026 uSkyBlock contributors
+# This file is distributed under GPL-3.0 license.
+# Translators should preserve MiniMessage tags/placeholders exactly.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: uSkyBlock\n"
+"Report-Msgid-Bugs-To: https://github.com/uskyblock/uSkyBlock/issues\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: minoneer \n"
+"Language-Team: LANGUAGE \n"
+"Language: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+""".trimIndent()
+
+fun executeCommand(arguments: List, workingDir: File = rootProject.projectDir) {
+ val process = try {
+ ProcessBuilder(arguments).directory(workingDir).inheritIO().start()
+ } catch (e: Exception) {
+ throw GradleException("Unable to execute command: ${arguments.joinToString(" ")}", e)
+ }
+ val exitCode = process.waitFor()
+ if (exitCode != 0) {
+ throw GradleException("Command failed (exit $exitCode): ${arguments.joinToString(" ")}")
+ }
+}
+
+fun verifyToolAvailable(tool: String) {
+ try {
+ ProcessBuilder(tool, "--version").start().waitFor()
+ } catch (e: Exception) {
+ throw GradleException("$tool not found. Please install gettext tools.")
+ }
+}
+
+fun classifyReferencePath(referencePath: String): TranslationDomain {
+ val normalized = referencePath.replace('\\', '/')
+ val isSystemDebug = normalized.endsWith("command/admin/DebugCommand.java") ||
+ normalized.endsWith("command/admin/GetIslandDataCommand.java") ||
+ normalized.endsWith("command/admin/SetIslandDataCommand.java") ||
+ normalized.endsWith("command/admin/ItemInfoCommand.java") ||
+ normalized.endsWith("command/admin/ImportCommand.java") ||
+ normalized.endsWith("command/admin/FlushCommand.java") ||
+ normalized.endsWith("command/admin/task/PurgeTask.java") ||
+ normalized.endsWith("command/admin/task/PurgeScanTask.java") ||
+ normalized.endsWith("command/admin/task/ProtectAllTask.java") ||
+ normalized.endsWith("imports/USBImporterExecutor.java") ||
+ normalized.endsWith("bukkit-utils/src/main/java/dk/lockfuglsang/minecraft/command/DocumentCommand.java") ||
+ normalized.endsWith("bukkit-utils/src/main/java/dk/lockfuglsang/minecraft/command/PlainTextCommandVisitor.java")
+ if (isSystemDebug) {
+ return TranslationDomain.SYSTEM_DEBUG
+ }
+ if (normalized.contains("/command/admin/")) {
+ return TranslationDomain.ADMIN_OPS
+ }
+ return TranslationDomain.PLAYER_FACING
+}
+
+fun classifyEntry(entry: String): TranslationDomain {
+ val referencedDomains = entry.lineSequence()
+ .filter { it.startsWith("#: ") }
+ .map { classifyReferencePath(it.removePrefix("#: ").trim()) }
+ .toList()
+ if (referencedDomains.isEmpty()) {
+ return TranslationDomain.PLAYER_FACING
+ }
+ return referencedDomains.maxByOrNull { it.precedence } ?: TranslationDomain.PLAYER_FACING
+}
+
+fun parsePotEntries(content: String): List {
+ val normalized = content.replace("\r\n", "\n").trim()
+ if (normalized.isEmpty()) {
+ return emptyList()
+ }
+ return normalized.split("\n\n")
+ .drop(1) // skip header entry
+ .map { it.trim() }
+ .filter { it.isNotEmpty() }
+}
+
+fun writePotFile(potFile: File, entries: List) {
+ potFile.parentFile.mkdirs()
+ val content = buildString {
+ append(curatedPotHeader).append("\n\n")
+ entries.forEach { entry ->
+ append(entry.trimEnd()).append("\n\n")
+ }
+ }
+ potFile.writeText(content)
+}
+
+fun sortReferenceCommentBlocks(content: String): String {
+ val lines = content.split('\n')
+ val output = mutableListOf()
+ var i = 0
+ while (i < lines.size) {
+ if (lines[i].startsWith("#: ")) {
+ val refs = mutableListOf()
+ while (i < lines.size && lines[i].startsWith("#: ")) {
+ refs.add(lines[i])
+ i++
+ }
+ refs.sortBy { it.lowercase() }
+ output.addAll(refs)
+ } else {
+ output.add(lines[i])
+ i++
+ }
+ }
+ return output.joinToString("\n")
+}
+
+fun postProcessPotFile(potFile: File) {
+ if (!potFile.exists()) {
+ return
+ }
+ var content = potFile.readText().replace("csharp-format", "java-format")
+ potFile.writeText(content)
+ executeCommand(
+ listOf(
+ "msgcat",
+ "-s",
+ "--no-wrap",
+ "--add-location=file",
+ "-o",
+ potFile.absolutePath,
+ potFile.absolutePath
+ )
+ )
+ content = potFile.readText().replace(Regex("\"POT-Creation-Date:.*\\n"), "")
+ if (content.contains("\n#: ")) {
+ content = content.replaceFirst(
+ Regex("(?s)^.*?\\n\\n(?=#: )"),
+ Matcher.quoteReplacement("$curatedPotHeader\n\n")
+ )
+ content = sortReferenceCommentBlocks(content)
+ } else {
+ content = "$curatedPotHeader\n"
+ }
+ potFile.writeText(content)
+}
+
+val mergeDomainTranslations = tasks.register("mergeDomainTranslations") {
+ group = "translation"
+ description = "Merges domain-specific .po files into locale .po files"
+ dependsOn("extractTranslation")
+ val includePatterns = translationDomains.map { "${it.id}/*.po" }
+ inputs.files(fileTree(i18nDir) { include(*includePatterns.toTypedArray()) })
+ inputs.files(domainPotFiles.values)
+ outputs.dir(mergedLocalesDir)
+
+ doFirst {
+ verifyToolAvailable("msgcat")
+ }
+
+ doLast {
+ val outputDir = mergedLocalesDir.get().asFile
+ outputDir.mkdirs()
+ outputDir.listFiles { _, name -> name.lowercase().endsWith(".po") }?.forEach { mergedFile ->
+ if (!mergedFile.delete()) {
+ throw GradleException("Unable to delete stale merged locale file: ${mergedFile.absolutePath}")
+ }
+ }
+
+ val domainFiles = fileTree(i18nDir) { include(*includePatterns.toTypedArray()) }.files
+ if (domainFiles.isEmpty()) {
+ logger.lifecycle("No domain translation files found in ${i18nDir.absolutePath}; skipping mergeDomainTranslations.")
+ return@doLast
+ }
+
+ val locales = domainFiles
+ .map { it.nameWithoutExtension }
+ .toSortedSet(String.CASE_INSENSITIVE_ORDER)
+
+ locales.forEach { locale ->
+ val sources = translationDomains.map { domain ->
+ val translatedDomainFile = domainLocaleDirs.getValue(domain).resolve("$locale.po")
+ if (translatedDomainFile.exists()) translatedDomainFile else domainPotFiles.getValue(domain)
+ }
+ val mergedLocaleFile = outputDir.resolve("$locale.po")
+ executeCommand(
+ listOf(
+ "msgcat",
+ "-s",
+ "--no-wrap",
+ "--add-location=file",
+ "-o",
+ mergedLocaleFile.absolutePath
+ ) + sources.map { it.absolutePath }
+ )
+ logger.lifecycle("Merged domain translations for locale {}", locale)
+ }
+ }
+}
+
+val generateSupportedLocales = tasks.register("generateSupportedLocales") {
+ group = "translation"
+ description = "Generates a stable list of supported locale keys from .po files"
+ dependsOn(mergeDomainTranslations)
+ inputs.files(fileTree(mergedLocalesDir) { include("*.po") })
+ outputs.file(supportedLocalesFile)
+ doLast {
+ val locales = fileTree(mergedLocalesDir) { include("*.po") }
+ .files
+ .map { it.nameWithoutExtension }
+ .toMutableSet()
+ locales.addAll(generatedExtraLocaleKeys)
+ val sortedLocales = locales.sortedBy { it.lowercase() }
+
+ val outputFile = supportedLocalesFile.get().asFile
+ outputFile.parentFile.mkdirs()
+ outputFile.writeText(sortedLocales.joinToString(separator = "\n", postfix = "\n"))
+ }
+}
+
+val i18nZip = tasks.register("i18nZip") {
+ group = "build"
+ description = "Zips the .po files into i18n.zip"
+ dependsOn(generateSupportedLocales, "generateExtraTranslations")
+ from(mergedLocalesDir) {
+ include("*.po")
+ }
+ generatedExtraTranslations.values.forEach { generatedPo ->
+ from(generatedPo) {
+ into("")
+ }
+ }
+ from(supportedLocalesFile) {
+ into("")
+ }
+ archiveFileName.set("i18n.zip")
+ // Keep archive output outside processResources destination to avoid self-copy truncation.
+ destinationDirectory.set(generatedI18nDir)
+}
+
+val i18nZipFile = i18nZip.flatMap { it.archiveFile }
+
+tasks.processResources {
+ from(i18nZipFile)
+ inputs.file(i18nZipFile)
+
+ val props = mapOf(
+ "projectVersion" to project.version,
+ "buildNumber" to (System.getenv("GITHUB_RUN_NUMBER") ?: "DEV"),
+ "gsonVersion" to libs.versions.com.google.code.gson.gson.x1.get(),
+ "guiceVersion" to libs.versions.com.google.inject.guice.get(),
+ "guavaVersion" to libs.versions.com.google.guava.guava.get(),
+ "adventureApiVersion" to libs.versions.net.kyori.adventure.api.get(),
+ "adventureBukkitVersion" to libs.versions.net.kyori.adventure.platform.bukkit.get(),
+ "apacheCommonsVersion" to libs.versions.org.apache.commons.commons.lang3.get(),
+ "apacheHttpVersion" to libs.versions.org.apache.httpcomponents.httpclient.get(),
+ "mavenArtifactVersion" to libs.versions.org.apache.maven.maven.artifact.get()
+ )
+ inputs.properties(props)
+ filesMatching("plugin.yml") {
+ expand(props)
+ }
+}
+
+tasks.register("extractTranslation") {
+ group = "translation"
+ description = "Extracts translatable strings into domain-specific .pot files"
+
+ val bukkitUtilsDir = project(":bukkit-utils").projectDir
+ val coreDir = projectDir
+ val javaFiles = (
+ fileTree(bukkitUtilsDir.resolve("src/main/java")) { include("**/*.java") }.files +
+ fileTree(coreDir.resolve("src/main/java")) { include("**/*.java") }.files
+ )
+ .sortedBy { it.relativeTo(rootProject.projectDir).invariantSeparatorsPath }
+ val javaFilesRelativePaths = javaFiles.map { it.relativeTo(rootProject.projectDir).invariantSeparatorsPath }
+
+ inputs.files(javaFiles)
+ outputs.file(combinedExtractionPotFile)
+ outputs.file(mergedPotFile)
+ domainPotFiles.values.forEach { outputs.file(it) }
+
+ doFirst {
+ verifyToolAvailable("xgettext")
+ verifyToolAvailable("msgcat")
+ }
+
+ doLast {
+ val allPotFile = combinedExtractionPotFile.get().asFile
+ allPotFile.parentFile.mkdirs()
+
+ executeCommand(
+ listOf(
+ "xgettext",
+ "--language=C#", // C# parser handles Java lambdas and + concatenation better than Java parser in xgettext
+ "--keyword=tr",
+ "--keyword=trLegacy",
+ "--keyword=marktr",
+ "--keyword=sendTr:2",
+ "--keyword=sendErrorTr:2",
+ "--from-code=UTF-8",
+ "--add-comments=I18N:",
+ "--add-location=file",
+ "--output=${allPotFile.absolutePath}"
+ ) + javaFilesRelativePaths,
+ rootProject.projectDir
+ )
+ postProcessPotFile(allPotFile)
+
+ val entries = parsePotEntries(allPotFile.readText())
+ val classifiedEntries = entries.map { entry -> entry to classifyEntry(entry) }
+
+ val domainCounts = mutableMapOf()
+ translationDomains.forEach { domain ->
+ val domainEntries = classifiedEntries
+ .filter { (_, classifiedDomain) -> classifiedDomain == domain }
+ .map { (entry, _) -> entry }
+ domainCounts[domain] = domainEntries.size
+ val domainPotFile = domainPotFiles.getValue(domain)
+ writePotFile(domainPotFile, domainEntries)
+ postProcessPotFile(domainPotFile)
+ }
+
+ val mergedEntries = classifiedEntries
+ .map { (entry, _) -> entry }
+ val mergedPot = mergedPotFile.get().asFile
+ writePotFile(mergedPot, mergedEntries)
+ postProcessPotFile(mergedPot)
+
+ logger.lifecycle(
+ "Extracted translation templates: player_facing={}, admin_ops={}, system_debug={}, merged={}",
+ domainCounts[TranslationDomain.PLAYER_FACING] ?: 0,
+ domainCounts[TranslationDomain.ADMIN_OPS] ?: 0,
+ domainCounts[TranslationDomain.SYSTEM_DEBUG] ?: 0,
+ mergedEntries.size
+ )
+ }
+}
+
+val generateExtraTranslations = tasks.register("generateExtraTranslations") {
+ group = "translation"
+ description = "Generates Pirate and Kitteh translations"
+ dependsOn(mergeDomainTranslations)
+ inputs.file(mergedPotFile)
+ outputs.files(generatedExtraTranslations.values)
+
+ doLast {
+ val mergedPot = mergedPotFile.get().asFile
+ val pirateScript = file("$i18nDir/en2pirate.pl")
+ val kittehScript = file("$i18nDir/en2kitteh.pl")
+ val pirateOutput = generatedExtraTranslations.getValue("xx_PIRATE").get().asFile
+ val kittehOutput = generatedExtraTranslations.getValue("xx_lol_US").get().asFile
+ executeCommand(
+ listOf(
+ "perl",
+ pirateScript.absolutePath,
+ mergedPot.absolutePath,
+ pirateOutput.absolutePath
+ )
+ )
+ executeCommand(
+ listOf(
+ "perl",
+ kittehScript.absolutePath,
+ mergedPot.absolutePath,
+ kittehOutput.absolutePath
+ )
+ )
+ }
+}
+
+tasks.register("updateTranslation") {
+ group = "translation"
+ description = "Updates translation sources for Crowdin"
+ dependsOn("extractTranslation", "mergeDomainTranslations", "generateExtraTranslations")
+}
diff --git a/uSkyBlock-Core/pom.xml b/uSkyBlock-Core/pom.xml
deleted file mode 100644
index 33503cbbb..000000000
--- a/uSkyBlock-Core/pom.xml
+++ /dev/null
@@ -1,549 +0,0 @@
-
-
-
- uSkyBlock
- ovh.uskyblock
- 3.1.0-SNAPSHOT
-
- 4.0.0
- jar
- uSkyBlock-Core
-
-
-
-
- org.apache.maven.plugins
- maven-compiler-plugin
-
- 17
- utf-8
-
-
-
- com.google.code.maven-replacer-plugin
- replacer
- 1.5.3
-
-
- cleanup-po-files
- process-resources
-
- replace
-
-
- ${basedir}/src/main/po
- *.po, *.pot
-
-
- #: .*\n
-
-
-
- "POT-Creation-Date:.*\n
-
-
-
-
-
-
-
-
- org.codehaus.mojo
- buildnumber-maven-plugin
- 1.4
-
-
- generate-resources
-
- create
-
-
-
-
- 7
- false
- false
- DEV
-
-
-
- org.apache.maven.plugins
- maven-shade-plugin
-
-
- package
-
- shade
-
-
- false
- true
-
-
- ovh.uskyblock:bukkit-utils
- io.papermc:paperlib
- org.bstats:*
- ovh.uskyblock:po-utils
-
-
-
-
- dk.lockfuglsang.minecraft
- us.talabrek.ultimateskyblock.utils
-
-
- io.papermc.lib
- us.talabrek.ultimateskyblock.paperlib
-
-
- org.bstats
- us.talabrek.ultimateskyblock.metrics
-
-
-
-
- *:*
-
- META-INF/*.MF
- META-INF/*.SF
- META-INF/*.DSA
- META-INF/*.RSA
- module-info.class
- META-INF.*
-
-
-
-
-
-
-
-
- maven-clean-plugin
- 3.1.0
-
-
-
- src/main/po
-
- *~
-
-
- *.po
- *.pot
-
-
-
-
-
-
- org.apache.maven.plugins
- maven-antrun-plugin
- 1.8
-
-
- prepare-package
- run
-
-
-
-
-
-
-
-
-
- org.apache.maven.plugins
- maven-javadoc-plugin
-
- public
- false
- none
-
-
-
- attach-javadocs
-
- jar
-
-
-
-
- javadoc
-
- deploy
-
-
-
-
- org.apache.maven.plugins
- maven-source-plugin
-
-
- attach-sources
-
- jar
-
-
-
-
-
- org.apache.maven.plugins
- maven-surefire-plugin
-
-
- org.apache.maven.plugins
- maven-failsafe-plugin
-
-
-
-
- .
- true
- src/main/resources
-
- *.yml
- README.md
- **/*.properties
-
-
-
- .
- false
- src/main/resources
-
- schematics/*
- structures/*
-
-
-
- .
- ${basedir}
-
- README.md
- LICENSE.txt
-
-
-
-
-
- .
- src/test/resources
- false
-
-
- imported
- src/main/resources
- true
-
- levelConfig.yml
-
-
-
-
-
-
-
- i18n
-
- false
-
-
-
-
- com.github.rlf
- gettext-maven-plugin
- 1.2.10
-
-
- find-bukkit-utils-msgids
- generate-sources
-
- gettext
-
-
- ${project.parent.basedir}/bukkit-utils/src/main/java
-
-
-
- find-msgids
- generate-sources
-
- gettext
-
-
-
- -j
-
-
-
-
- update-po-files
- generate-resources
-
- merge
-
-
- ${msgmergeCmd}
-
- -N
-
-
-
-
- clear-fuzzy
- generate-resources
-
- attrib
-
-
-
- --clear-fuzzy
- --empty
- --no-obsolete
-
-
- xx_PIRATE.po
- xx_lol_US.po
-
-
-
-
- report-po-completion
- process-resources
-
- report
-
-
- ${msgfmtCmd}
-
-
-
-
- ${project.build.directory}/classes
- 2
- ${project.basedir}/src/main/po
- us.talabrek.ultimateskyblock.i18n.Messages
- properties
-
- --no-location
-
-
-
-
-
-
-
-
-
-
- ovh.uskyblock
- bukkit-utils
- 3.1.0-SNAPSHOT
-
-
- ovh.uskyblock
- bukkit-utils
- 3.1.0-SNAPSHOT
- test-jar
- test
-
-
- ovh.uskyblock
- po-utils
-
-
- com.github.rlf
- uSkyBlock-API
-
-
- ovh.uskyblock
- uSkyBlock-APIv2
-
-
- net.milkbowl.vault
- VaultAPI
- provided
- true
-
-
-
- org.spigotmc
- spigot-api
- ${spigotapi.version}
- provided
- true
-
-
- com.google.code.gson
- gson
-
-
-
-
-
- io.papermc
- paperlib
- compile
-
-
-
- com.onarandombox.multiversecore
- Multiverse-Core
- 4.3.1
- provided
- true
-
-
- *
- *
-
-
-
-
- com.onarandombox.multiverseinventories
- Multiverse-Inventories
- 4.2.3
- provided
- true
-
-
- *
- *
-
-
-
-
-
- com.sk89q.worldedit
- worldedit-bukkit
- ${worldedit.version}
- provided
-
-
- org.bukkit
- bukkit
-
-
-
-
-
- com.sk89q.worldguard
- worldguard-bukkit
- ${worldguard.version}
- provided
-
-
- com.sk89q
- worldedit
-
-
-
-
-
- org.bstats
- bstats-bukkit
- 3.0.1
- compile
-
-
-
- com.google.guava
- guava
- ${guava.version}
- provided
-
-
-
- com.google.code.gson
- gson
- provided
-
-
- be.maximvdw
- MVdWPlaceholderAPI
- 3.0.1-SNAPSHOT
- provided
-
-
-
- *
- *
-
-
-
-
- net.kyori
- adventure-api
- provided
-
-
- net.kyori
- adventure-platform-bukkit
- provided
-
-
-
- org.jetbrains
- annotations
-
-
- org.apache.commons
- commons-lang3
- ${apache-commons.version}
- provided
-
-
- org.apache.commons
- commons-text
- ${apache-commons-text.version}
- provided
-
-
-
- org.apache.httpcomponents
- httpclient
- provided
-
-
- org.apache.maven
- maven-artifact
- provided
-
-
-
- org.hamcrest
- hamcrest
- ${hamcrest.version}
- test
-
-
- org.hamcrest
- hamcrest-library
- ${hamcrest.version}
- test
-
-
- junit
- junit
- ${junit.version}
- test
-
-
- org.junit.vintage
- junit-vintage-engine
- ${junit-vintage-engine.version}
- test
-
-
- org.mockito
- mockito-core
- ${mockito.version}
- test
-
-
-
diff --git a/uSkyBlock-Core/src/main/po/.gitignore b/uSkyBlock-Core/src/main/i18n/.gitignore
similarity index 100%
rename from uSkyBlock-Core/src/main/po/.gitignore
rename to uSkyBlock-Core/src/main/i18n/.gitignore
diff --git a/uSkyBlock-Core/src/main/i18n/admin_ops/ar.po b/uSkyBlock-Core/src/main/i18n/admin_ops/ar.po
new file mode 100644
index 000000000..ab234cfa8
--- /dev/null
+++ b/uSkyBlock-Core/src/main/i18n/admin_ops/ar.po
@@ -0,0 +1,602 @@
+# uSkyBlock translation template
+# Copyright (C) 2026 uSkyBlock contributors
+# This file is distributed under GPL-3.0 license.
+# Translators should preserve MiniMessage tags/placeholders exactly.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: uSkyBlock\n"
+"Report-Msgid-Bugs-To: https://github.com/uskyblock/uSkyBlock/issues\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: minoneer \n"
+"Language-Team: LANGUAGE \n"
+"Language: ar\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+"X-Generator: po_ai_translate (OpenAI)\n"
+
+#: uSkyBlock-Core/src/main/java/us/talabrek/ultimateskyblock/command/admin/ProtectAllCommand.java
+msgid "- Protect-All (