diff --git a/src/main/java/com/loki/lochat/LoChat.java b/src/main/java/com/loki/lochat/LoChat.java index 5b8b375..2b10aa5 100644 --- a/src/main/java/com/loki/lochat/LoChat.java +++ b/src/main/java/com/loki/lochat/LoChat.java @@ -60,6 +60,8 @@ public final class LoChat extends JavaPlugin { configManager.getTranslationEndpoint(), configManager.getTranslationApiKey(), configManager.isTranslationEnabled(), + configManager.getTranslationProvider(), + configManager.getTranslationTimeoutMs(), getLogger() ); diff --git a/src/main/java/com/loki/lochat/config/ConfigManager.java b/src/main/java/com/loki/lochat/config/ConfigManager.java index 3454ecb..d03ac4b 100644 --- a/src/main/java/com/loki/lochat/config/ConfigManager.java +++ b/src/main/java/com/loki/lochat/config/ConfigManager.java @@ -249,6 +249,26 @@ public class ConfigManager { return accessor.getTranslationApiKey(); } + public String getTranslationProvider() { + return accessor.getTranslationProvider(); + } + + public String getTranslationMode() { + return accessor.getTranslationMode(); + } + + public int getTranslationTimeoutMs() { + return accessor.getTranslationTimeoutMs(); + } + + public java.util.List getTranslationHoverFormat() { + return accessor.getTranslationHoverFormat(); + } + + public String getTranslationBypassPermission() { + return accessor.getTranslationBypassPermission(); + } + // ========== LoPreff integration ========== public boolean isLopreffEnabled() { diff --git a/src/main/java/com/loki/lochat/config/core/ConfigAccessor.java b/src/main/java/com/loki/lochat/config/core/ConfigAccessor.java index 96db60e..1531c67 100644 --- a/src/main/java/com/loki/lochat/config/core/ConfigAccessor.java +++ b/src/main/java/com/loki/lochat/config/core/ConfigAccessor.java @@ -150,6 +150,29 @@ public class ConfigAccessor { return config.getString("chat.translation.api-key", ""); } + /** "libretranslate" (needs endpoint and usually a key) or "google" (free, no key). */ + public String getTranslationProvider() { + return config.getString("chat.translation.provider", "libretranslate"); + } + + /** "hover" shows the translation when hovering the message, "actionbar" shows it above the hotbar. */ + public String getTranslationMode() { + return config.getString("chat.translation.mode", "hover"); + } + + public int getTranslationTimeoutMs() { + return config.getInt("chat.translation.timeout-ms", 3000); + } + + public java.util.List getTranslationHoverFormat() { + java.util.List lines = config.getStringList("chat.translation.hover-format"); + return lines.isEmpty() ? java.util.List.of("&7Translation:", "&f{translation}") : lines; + } + + public String getTranslationBypassPermission() { + return config.getString("chat.translation.bypass-permission", "lochat.translate.bypass"); + } + // ========== LoPreff integration ========== public boolean isLopreffEnabled() { diff --git a/src/main/java/com/loki/lochat/listener/ChatEventListener.java b/src/main/java/com/loki/lochat/listener/ChatEventListener.java index 3f0f6e8..89ae9b3 100644 --- a/src/main/java/com/loki/lochat/listener/ChatEventListener.java +++ b/src/main/java/com/loki/lochat/listener/ChatEventListener.java @@ -9,7 +9,9 @@ import com.loki.lochat.compat.Msg; import com.loki.lochat.core.filter.AdvancedMessageFilter; import com.loki.lochat.core.filter.FilterResult; import com.loki.lochat.renderer.EnhancedChatRenderer; +import com.loki.lochat.translate.TranslationService; import com.loki.lochat.utils.format.ChatFormatter; +import com.loki.lochat.utils.format.LuckPermsAffixes; import net.kyori.adventure.text.Component; @@ -80,6 +82,7 @@ public class ChatEventListener implements Listener { Component displayName = buildDisplayName(sender); Set recipients = new HashSet<>(event.getRecipients()); event.setCancelled(true); + prefetchTranslation(sender, messageComponent, recipients); for (Player viewer : recipients) { Component rendered = renderer.render(sender, displayName, messageComponent, viewer); Msg.send(viewer, rendered); @@ -132,6 +135,7 @@ public class ChatEventListener implements Listener { Component displayName = buildDisplayName(sender); Set recipients = new HashSet<>(event.getRecipients()); event.setCancelled(true); + prefetchTranslation(sender, messageComponent, recipients); for (Player viewer : recipients) { Component rendered = renderer.render(sender, displayName, messageComponent, viewer); Msg.send(viewer, rendered); @@ -226,9 +230,37 @@ public class ChatEventListener implements Listener { if (legacyName == null || legacyName.isBlank()) { return Component.text(sender.getName()); } + // LuckPerms prefix/suffix are added separately ({player_prefix}/{player_suffix}), do not show them twice + legacyName = LuckPermsAffixes.withoutAffixes(legacyName, LuckPermsAffixes.prefix(sender), + LuckPermsAffixes.suffix(sender)); return ChatFormatter.parse(legacyName); } + /** Translates the message for every viewer language before rendering (runs off the main thread only). */ + private void prefetchTranslation(Player sender, Component messageComponent, Set recipients) { + try { + LoChat loChat = (LoChat) plugin; + var cfg = loChat.getConfigManager(); + TranslationService ts = loChat.getTranslationService(); + if (!cfg.isTranslationEnabled() || !ts.isEnabled() || Bukkit.isPrimaryThread() + || "actionbar".equalsIgnoreCase(cfg.getTranslationMode()) + || sender.hasPermission(cfg.getTranslationBypassPermission())) { + return; + } + Set languages = new HashSet<>(); + for (Player viewer : recipients) { + if (!viewer.equals(sender)) { + languages.add(TranslationService.localeToLanguageCode(viewer.getLocale())); + } + } + if (!languages.isEmpty()) { + ts.prefetch(Msg.plain(messageComponent), languages, cfg.getTranslationTimeoutMs()); + } + } catch (Exception e) { + plugin.getLogger().fine("Translation prefetch failed: " + e.getMessage()); + } + } + private void recordStatistics(LoChat loChat, Player sender, boolean isGlobal) { playerService.recordMessage(sender.getUniqueId(), isGlobal ? "global" : "local"); } diff --git a/src/main/java/com/loki/lochat/renderer/EnhancedChatRenderer.java b/src/main/java/com/loki/lochat/renderer/EnhancedChatRenderer.java index 416bef0..fa7f830 100644 --- a/src/main/java/com/loki/lochat/renderer/EnhancedChatRenderer.java +++ b/src/main/java/com/loki/lochat/renderer/EnhancedChatRenderer.java @@ -64,12 +64,7 @@ public class EnhancedChatRenderer { if (viewer != null) { processed = mentionHandler.processMentions(processed, source, viewer); - if (cfg.isTranslationEnabled()) { - com.loki.lochat.LoChat loChat = (com.loki.lochat.LoChat) plugin; - if (loChat.getTranslationService().isEnabled()) { - sendTranslationAsync(viewer, PlainTextComponentSerializer.plainText().serialize(processed)); - } - } + processed = applyTranslation(source, viewer, message, processed); } // Получаем формат из конфига @@ -124,7 +119,7 @@ public class EnhancedChatRenderer { while (matcher.find()) { if (matcher.start() > lastEnd) { - result = result.append(Component.text(format.substring(lastEnd, matcher.start()))); + result = result.append(ChatFormatter.parse(format.substring(lastEnd, matcher.start()))); } String placeholder = matcher.group(1); @@ -134,7 +129,7 @@ public class EnhancedChatRenderer { } if (lastEnd < format.length()) { - result = result.append(Component.text(format.substring(lastEnd))); + result = result.append(ChatFormatter.parse(format.substring(lastEnd))); } return result; @@ -156,6 +151,11 @@ public class EnhancedChatRenderer { } } + String luckPermsPrefix = com.loki.lochat.utils.format.LuckPermsAffixes.prefix(player); + if (!luckPermsPrefix.isEmpty()) { + return com.loki.lochat.utils.format.ChatFormatter.parse(luckPermsPrefix); + } + return Component.empty(); } @@ -170,6 +170,11 @@ public class EnhancedChatRenderer { } } + String luckPermsSuffix = com.loki.lochat.utils.format.LuckPermsAffixes.suffix(player); + if (!luckPermsSuffix.isEmpty()) { + return com.loki.lochat.utils.format.ChatFormatter.parse(luckPermsSuffix); + } + return Component.empty(); } @@ -239,6 +244,50 @@ public class EnhancedChatRenderer { .clickEvent(ClickEvent.suggestCommand("/msg " + player.getName() + " ")); } + /** + * Adds the translation into the viewer's client language: as hover text over the message (default) + * or in the action bar. The translation must already be cached (see ChatEventListener#prefetchTranslation). + */ + private Component applyTranslation(Player source, Player viewer, Component original, Component processed) { + if (!cfg.isTranslationEnabled() || viewer.equals(source)) { + return processed; + } + com.loki.lochat.LoChat loChat = (com.loki.lochat.LoChat) plugin; + TranslationService ts = loChat.getTranslationService(); + if (!ts.isEnabled() || source.hasPermission(cfg.getTranslationBypassPermission())) { + return processed; + } + String plain = PlainTextComponentSerializer.plainText().serialize(original); + if (plain.isBlank()) { + return processed; + } + if ("actionbar".equalsIgnoreCase(cfg.getTranslationMode())) { + sendTranslationAsync(viewer, plain); + return processed; + } + String translated = ts.getCachedTranslation(plain, viewer.getLocale()); + if (translated == null || translated.isBlank() || translated.equalsIgnoreCase(plain)) { + return processed; + } + Component hover = Component.empty(); + boolean first = true; + for (String line : cfg.getTranslationHoverFormat()) { + if (!first) { + hover = hover.append(Component.newline()); + } + first = false; + int idx = line.indexOf("{translation}"); + if (idx < 0) { + hover = hover.append(ChatFormatter.parse(line)); + } else { + hover = hover.append(ChatFormatter.parse(line.substring(0, idx))) + .append(Component.text(translated)) + .append(ChatFormatter.parse(line.substring(idx + "{translation}".length()))); + } + } + return processed.hoverEvent(HoverEvent.showText(hover)); + } + private void sendTranslationAsync(Player viewer, String plainText) { if (plainText.isBlank()) { return; diff --git a/src/main/java/com/loki/lochat/translate/TranslationService.java b/src/main/java/com/loki/lochat/translate/TranslationService.java index 7eb6b60..264a395 100644 --- a/src/main/java/com/loki/lochat/translate/TranslationService.java +++ b/src/main/java/com/loki/lochat/translate/TranslationService.java @@ -33,11 +33,20 @@ public class TranslationService { private final String endpoint; private final String apiKey; private final boolean enabled; + private final String provider; + private final int timeoutMs; public TranslationService(String endpoint, String apiKey, boolean enabled, Logger logger) { + this(endpoint, apiKey, enabled, "libretranslate", 3000, logger); + } + + public TranslationService(String endpoint, String apiKey, boolean enabled, String provider, int timeoutMs, + Logger logger) { this.endpoint = endpoint != null ? endpoint.replaceAll("/+$", "") : ""; this.apiKey = apiKey; this.enabled = enabled; + this.provider = provider == null ? "libretranslate" : provider.toLowerCase(Locale.ROOT); + this.timeoutMs = Math.max(500, timeoutMs); this.logger = logger; this.httpClient = HttpClient.newBuilder() .connectTimeout(TIMEOUT) @@ -59,8 +68,37 @@ public class TranslationService { }); } + private boolean isGoogle() { + return "google".equals(provider); + } + public boolean isEnabled() { - return enabled && !endpoint.isBlank(); + return enabled && (isGoogle() || !endpoint.isBlank()); + } + + /** Cached translation for the given language, or null if it has not been translated yet. */ + public String getCachedTranslation(String text, String language) { + return getCached(text, localeToLanguageCode(language)); + } + + /** + * Translates the text into every given language in parallel and waits up to timeoutMs. + * Call it off the main thread; the results end up in the cache. + */ + public void prefetch(String text, java.util.Collection languages, long timeoutMs) { + if (!isEnabled() || text == null || text.isBlank()) { + return; + } + java.util.List> futures = new java.util.ArrayList<>(); + for (String language : languages) { + futures.add(translateAsync(text, language)); + } + try { + CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])) + .get(timeoutMs, java.util.concurrent.TimeUnit.MILLISECONDS); + } catch (Exception e) { + // timeout or failure: the message is simply sent without a translation + } } public CompletableFuture translateAsync(String text, String targetLanguage) { @@ -89,7 +127,91 @@ public class TranslationService { } } + private volatile long gtxBlockedUntil; + private volatile long chromeBlockedUntil; + + /** + * The gtx endpoint gets IP-blocked (HTTP 429 / captcha page) very quickly, so the Chrome dictionary + * endpoint is tried first and gtx is only a fallback; a blocked endpoint is skipped for a minute. + */ + private String doFetchGoogle(String text, String targetLang) throws Exception { + long now = System.currentTimeMillis(); + String q = java.net.URLEncoder.encode(text, StandardCharsets.UTF_8); + String tl = java.net.URLEncoder.encode(targetLang, StandardCharsets.UTF_8); + Exception last = null; + if (now >= chromeBlockedUntil) { + try { + return parseChromeResponse(get("https://clients5.google.com/translate_a/t?client=dict-chrome-ex&sl=auto&tl=" + + tl + "&q=" + q)); + } catch (Exception e) { + chromeBlockedUntil = now + 60_000; + last = e; + } + } + if (now >= gtxBlockedUntil) { + try { + return parseGoogleResponse(get("https://translate.googleapis.com/translate_a/single?client=gtx&sl=auto&dt=t&tl=" + + tl + "&q=" + q)); + } catch (Exception e) { + gtxBlockedUntil = now + 60_000; + last = e; + } + } + throw last != null ? last : new RuntimeException("all translation endpoints are on cooldown"); + } + + private String get(String url) throws Exception { + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(url)) + .timeout(Duration.ofMillis(timeoutMs)) + .header("User-Agent", "Mozilla/5.0") + .GET() + .build(); + HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + if (response.statusCode() != 200) { + throw new RuntimeException("Google returned " + response.statusCode()); + } + return response.body(); + } + + /** Chrome dictionary response: [["translated","detected-language"], ...]. */ + static String parseChromeResponse(String body) { + com.google.gson.JsonArray root = JsonParser.parseString(body).getAsJsonArray(); + StringBuilder sb = new StringBuilder(); + for (com.google.gson.JsonElement segment : root) { + if (segment.isJsonArray()) { + com.google.gson.JsonArray parts = segment.getAsJsonArray(); + if (parts.size() > 0 && !parts.get(0).isJsonNull()) { + sb.append(parts.get(0).getAsString()); + } + } else if (segment.isJsonPrimitive()) { + sb.append(segment.getAsString()); + break; + } + } + if (sb.length() == 0) { + throw new RuntimeException("empty translation response"); + } + return sb.toString(); + } + + /** Google response: [[["translated","original",...], ...], null, "detected-language"]. */ + static String parseGoogleResponse(String body) { + com.google.gson.JsonArray root = JsonParser.parseString(body).getAsJsonArray(); + StringBuilder sb = new StringBuilder(); + for (com.google.gson.JsonElement segment : root.get(0).getAsJsonArray()) { + com.google.gson.JsonArray parts = segment.getAsJsonArray(); + if (parts.size() > 0 && !parts.get(0).isJsonNull()) { + sb.append(parts.get(0).getAsString()); + } + } + return sb.toString(); + } + private String doFetch(String text, String targetLang) throws Exception { + if (isGoogle()) { + return doFetchGoogle(text, targetLang); + } JsonObject body = new JsonObject(); body.addProperty("q", text); body.addProperty("source", "auto"); @@ -141,12 +263,12 @@ public class TranslationService { if (locale == null || locale.isBlank()) { return "en"; } - String lang = locale.split("_")[0].toLowerCase(Locale.ROOT); - return switch (lang) { - case "zh" -> "zh-CN"; - case "zh-tw", "zh-hk" -> "zh-TW"; - default -> lang; - }; + String normalized = locale.toLowerCase(Locale.ROOT).replace('-', '_'); + if (normalized.startsWith("zh_tw") || normalized.startsWith("zh_hk")) { + return "zh-TW"; + } + String lang = normalized.split("_")[0]; + return "zh".equals(lang) ? "zh-CN" : lang; } public void clearCache() { diff --git a/src/main/java/com/loki/lochat/utils/format/LuckPermsAffixes.java b/src/main/java/com/loki/lochat/utils/format/LuckPermsAffixes.java new file mode 100644 index 0000000..35b2a5f --- /dev/null +++ b/src/main/java/com/loki/lochat/utils/format/LuckPermsAffixes.java @@ -0,0 +1,67 @@ +package com.loki.lochat.utils.format; + +import me.clip.placeholderapi.PlaceholderAPI; + +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; + +import java.util.regex.Pattern; + +/** + * LuckPerms prefix and suffix through PlaceholderAPI (%luckperms_prefix% / %luckperms_suffix%). + * Returns an empty string when PlaceholderAPI or its LuckPerms expansion is not available. + */ +public final class LuckPermsAffixes { + + private static final Pattern COLOR = Pattern.compile("(?i)(&#[0-9a-f]{6}|[&\u00A7][0-9a-fk-orx])"); + + private LuckPermsAffixes() { + } + + public static String prefix(Player player) { + return resolve(player, "%luckperms_prefix%"); + } + + public static String suffix(Player player) { + return resolve(player, "%luckperms_suffix%"); + } + + private static String resolve(Player player, String placeholder) { + if (player == null || !Bukkit.getPluginManager().isPluginEnabled("PlaceholderAPI")) { + return ""; + } + try { + String result = PlaceholderAPI.setPlaceholders(player, placeholder); + if (result == null || result.contains("%")) { + return ""; + } + return result; + } catch (Exception | LinkageError e) { + return ""; + } + } + + /** Text without color codes. */ + public static String strip(String text) { + return text == null ? "" : COLOR.matcher(text).replaceAll(""); + } + + /** + * Removes the LuckPerms prefix/suffix from a display name that already contains them + * (for example when another plugin put them into the nickname), so they are not shown twice. + * Returns the original text if nothing was removed. + */ + public static String withoutAffixes(String displayName, String prefix, String suffix) { + String plain = strip(displayName).trim(); + String pp = strip(prefix).trim(); + String sp = strip(suffix).trim(); + String result = plain; + if (!pp.isEmpty() && result.startsWith(pp)) { + result = result.substring(pp.length()).trim(); + } + if (!sp.isEmpty() && result.endsWith(sp)) { + result = result.substring(0, result.length() - sp.length()).trim(); + } + return result.equals(plain) || result.isEmpty() ? displayName : result; + } +} diff --git a/src/main/resources/config/messages.yml b/src/main/resources/config/messages.yml index 0221640..809b282 100644 --- a/src/main/resources/config/messages.yml +++ b/src/main/resources/config/messages.yml @@ -5,25 +5,25 @@ commands: mute: - usage: "&cUsage: &f/mute [time] [-s] [reason]" - time-help: "&7Time format: &f1d&7, &f2h&7, &f30m&7, &f60s&7, &fperm" - silent-help: "&7-s &7= silent mute (requires &flochat.mute.silent&7)" + usage: "&cUsage: &6/lmute [time] [-s] [reason]" + time-help: "&7Time format: &61d&7, &62h&7, &630m&7, &660s&7, &6perm" + silent-help: "&7Use &6-s &7for a silent mute (needs &6lochat.mute.silent&7)" no-silent-permission: "&cNo permission for silent mute" default-reason: "No reason specified" default-duration: "7d" cooldown: - message: "&cPlease wait &f{remaining} &cseconds" + message: "&cPlease wait &6{remaining} &cseconds" clear-chat: - lines-cleared: "&aChat cleared &7({lines} lines)" + lines-cleared: "&fChat cleared &7({lines} lines)" no-permission: "&cNo permission to clear chat" # ── Local Chat ───────────────────────────────────────────────────────────────── local-chat: - nobody-heard: "&7Nobody heard you — no players nearby" - no-players-nearby: "&7No players nearby" + nobody-heard: "&7Nobody heard you. Start your message with &6! &7to be heard everywhere." + no-players-nearby: "&7No players nearby. Start your message with &6! &7to write to everyone." # ── Filters ──────────────────────────────────────────────────────────────────── @@ -42,13 +42,13 @@ filters: # ── Mute Messages ────────────────────────────────────────────────────────────── mute: - muted-message: "&cYou are muted! &7Reason: &f{reason}&7. Remaining: &f{time}" - muted-permanent: "&cYou are permanently muted! &7Reason: &f{reason}" - player-muted: "&aPlayer &f{player} &amuted for &f{time}&a. Reason: &7{reason}" - player-muted-permanent: "&aPlayer &f{player} &amuted permanently. Reason: &7{reason}" - player-muted-silent: "&8[&7SILENT&8] &f{player} &7muted for &f{time}" - player-unmuted: "&aPlayer &f{player} &aunmuted" - not-muted: "&7Player &f{player} &7is not muted" + muted-message: "&cYou are muted! &7Reason: &6{reason}&7. Remaining: &6{time}" + muted-permanent: "&cYou are permanently muted! &7Reason: &6{reason}" + player-muted: "&fPlayer &6{player} &fmuted for &6{time}&f. Reason: &7{reason}" + player-muted-permanent: "&fPlayer &6{player} &fmuted permanently. Reason: &7{reason}" + player-muted-silent: "&8[&7SILENT&8] &6{player} &7muted for &6{time}" + player-unmuted: "&fPlayer &6{player} &funmuted" + not-muted: "&7Player &6{player} &7is not muted" player-not-found: "&cPlayer not found!" invalid-time: "&cInvalid time format!" already-muted: "&cPlayer is already muted!" @@ -66,17 +66,17 @@ gamemodes: private-messages: no-reply-target: "&cNobody to reply to" - message-sent: "&aMessage sent to &f{player}" - player-offline: "&cPlayer &f{player} &cis not online" + message-sent: "&fMessage sent to &6{player}" + player-offline: "&cPlayer &6{player} &cis not online" cannot-message-self: "&cYou cannot message yourself" # ── Ignore ───────────────────────────────────────────────────────────────────── ignore: - player-ignored: "&aPlayer &f{player} &ais now ignored" - player-unignored: "&aPlayer &f{player} &ais no longer ignored" - already-ignored: "&7Player &f{player} &7is already ignored" - not-ignored: "&7Player &f{player} &7is not ignored" + player-ignored: "&fPlayer &6{player} &fis now ignored" + player-unignored: "&fPlayer &6{player} &fis no longer ignored" + already-ignored: "&7Player &6{player} &7is already ignored" + not-ignored: "&7Player &6{player} &7is not ignored" cannot-ignore-self: "&cYou cannot ignore yourself" ignore-list-empty: "&7Your ignore list is empty" ignore-list-header: "&7Ignored players:" @@ -97,7 +97,7 @@ general: days: "d" permanent: "permanent" enabled: "&aEnabled" - disabled: "&7Disabled" + disabled: "&cDisabled" online: "&aOnline" offline: "&7Offline" diff --git a/src/main/resources/messages.yml b/src/main/resources/messages.yml index 2a1c53f..a9271b0 100644 --- a/src/main/resources/messages.yml +++ b/src/main/resources/messages.yml @@ -1,20 +1,20 @@ # LoChat player messages # All messages sent to players. Supports & color codes and placeholders. -prefix: "&8[&3LoChat&8]" +prefix: "&6Star Wars &7|" # ── Global Chat ──────────────────────────────────────────────────────────────── global: - toggled-on: "&aGlobal chat enabled" + toggled-on: "&fGlobal chat enabled" toggled-off: "&cGlobal chat disabled" disabled: "&cGlobal chat is disabled on this server" # ── Local Chat ───────────────────────────────────────────────────────────────── local: - no-players: "&cNo players nearby" - nobody-heard: "&7Nobody heard you — no players nearby" + no-players: "&7No players nearby. Start your message with &6! &7to write to everyone." + nobody-heard: "&7Nobody heard you. Start your message with &6! &7to be heard everywhere." # ── Private Messages ─────────────────────────────────────────────────────────── @@ -29,19 +29,19 @@ pm: # ── Ignore List ──────────────────────────────────────────────────────────────── ignore: - added: "&aPlayer &f{player} &ais now ignored" - removed: "&aPlayer &f{player} &ais no longer ignored" - already-ignored: "&7Player &f{player} &7is already ignored" - not-ignored: "&7Player &f{player} &7is not ignored" + added: "&fPlayer &6{player} &fis now ignored" + removed: "&fPlayer &6{player} &fis no longer ignored" + already-ignored: "&7Player &6{player} &7is already ignored" + not-ignored: "&7Player &6{player} &7is not ignored" self: "&cYou cannot ignore yourself" list-empty: "&7Your ignore list is empty" list-header: "&8\u2500\u2500\u2500\u2500\u2500 Ignored Players \u2500\u2500\u2500\u2500\u2500" - list-footer: "&7Total: &f{count} &7players" + list-footer: "&7Total: &6{count} &7players" # ── Announcements ────────────────────────────────────────────────────────────── announce: - reloaded: "&aConfiguration reloaded" + reloaded: "&fConfiguration reloaded" # ── Chat Spy ─────────────────────────────────────────────────────────────────── @@ -60,7 +60,7 @@ filter: # ── Cooldowns ────────────────────────────────────────────────────────────────── cooldown: - wait: "&cPlease wait &f{time} &cseconds" + wait: "&cPlease wait &6{time} &cseconds" # ── Anti-Spam ────────────────────────────────────────────────────────────────── @@ -72,23 +72,23 @@ antispam: # ── Clear Chat ───────────────────────────────────────────────────────────────── clearchat: - cleared: "&aChat cleared by &f{player}" + cleared: "&fChat cleared by &6{player}" # ── Errors ───────────────────────────────────────────────────────────────────── errors: no-permission: "&cNo permission" player-not-found: "&cPlayer not found" - invalid-usage: "&cUsage: &f{usage}" + invalid-usage: "&cUsage: &6{usage}" # ── General ──────────────────────────────────────────────────────────────────── general: no-permission: "&cYou don't have permission!" players-only: "&cThis command is for players only!" - config-reloaded: "&aConfiguration reloaded!" - invalid-player: "&cPlayer &f{player} &cnot found!" - cooldown-active: "&cPlease wait &f{time}&cs" + config-reloaded: "&fConfiguration reloaded!" + invalid-player: "&cPlayer &6{player} &cnot found!" + cooldown-active: "&cPlease wait &6{time}&cs" command-disabled: "&cCommand disabled!" # ── Hub ──────────────────────────────────────────────────────────────────────── @@ -99,55 +99,55 @@ hub: # ── Mute ─────────────────────────────────────────────────────────────────────── mute: - muted: "&aPlayer &f{player} &amuted for &f{time}" - unmuted: "&aPlayer &f{player} &aunmuted" - already-muted: "&7Player &f{player} &7is already muted" - not-muted: "&7Player &f{player} &7is not muted" - you-muted: "&cYou are muted! &7Remaining: &f{time}" + muted: "&fPlayer &6{player} &fmuted for &6{time}" + unmuted: "&fPlayer &6{player} &funmuted" + already-muted: "&7Player &6{player} &7is already muted" + not-muted: "&7Player &6{player} &7is not muted" + you-muted: "&cYou are muted! &7Remaining: &6{time}" permanent: "&cYou are permanently muted!" # ── Gradients ────────────────────────────────────────────────────────────────── gradient: - color-set: "&aGradient set!" - color-enabled: "&aGradient nickname enabled" + color-set: "&fGradient set!" + color-enabled: "&fGradient nickname enabled" color-disabled: "&7Gradient nickname disabled" color-reset: "&7Gradient reset" prefix-set: "&aPrefix set!" prefix-reset: "&7Prefix reset" - prefix-enabled: "&aPrefix enabled" + prefix-enabled: "&fPrefix enabled" prefix-disabled: "&7Prefix disabled" - invalid-hex: "&cInvalid hex color: &f{color}" - invalid-color: "&cInvalid color: &f{color}" - too-many-colors: "&cToo many colors! Maximum: &f{max}" - too-few-colors: "&cToo few colors! Minimum: &f{min}" - cooldown: "&cPlease wait &f{time} &cseconds" - not-enough-points: "&cNot enough points! Need: &f{price}" + invalid-hex: "&cInvalid hex color: &6{color}" + invalid-color: "&cInvalid color: &6{color}" + too-many-colors: "&cToo many colors! Maximum: &6{max}" + too-few-colors: "&cToo few colors! Minimum: &6{min}" + cooldown: "&cPlease wait &6{time} &cseconds" + not-enough-points: "&cNot enough points! Need: &6{price}" player-not-found: "&cPlayer not found" - color-help: "&cUsage: &f/color ..." - color-success: "&aGradient applied!" + color-help: "&cUsage: &6/color ..." + color-success: "&fGradient applied!" color-no-colors: "&7You have no colors set" - prefix-help: "&cUsage: &f/prefix " + prefix-help: "&cUsage: &6/prefix " prefix-success: "&aPrefix set!" prefix-success-free: "&aPrefix set!" prefix-no-prefix: "&7You have no prefix" - prefix-too-long: "&cPrefix too long! Max: &f{max}" + prefix-too-long: "&cPrefix too long! Max: &6{max}" prefix-no-colors: "&cPrefix cannot contain color codes" only-player: "&cPlayers only!" no-permission: "&cNo permission!" - reload-success: "&aConfiguration reloaded!" - info-header: "&8\u2500\u2500\u2500\u2500\u2500 &f{player} &8\u2500\u2500\u2500\u2500\u2500" - info-prefix: "&7Prefix: &f{prefix}" - info-colors: "&7Colors: &f{colors}" + reload-success: "&fConfiguration reloaded!" + info-header: "&8\u2500\u2500\u2500\u2500\u2500 &6{player} &8\u2500\u2500\u2500\u2500\u2500" + info-prefix: "&7Prefix: &6{prefix}" + info-colors: "&7Colors: &6{colors}" # ── Custom Nicknames ─────────────────────────────────────────────────────────── nick: - current: "&7Your nickname: &f{nick}" - usage: "&cUsage: &f/nick &7or &f/nick reset" + current: "&7Your nickname: &6{nick}" + usage: "&cUsage: &6/nick &7or &6/nick reset" no-nick: "&7You have no custom nickname" - reset-success: "&aNickname reset!" - set-success: "&aNickname changed to: &f{nick}" + reset-success: "&fNickname reset!" + set-success: "&fNickname changed to: &6{nick}" invalid: "&cInvalid nickname!" taken: "&cThis nickname is already taken!" error: "&cFailed to set nickname!" @@ -156,11 +156,11 @@ nick: rp: me: "&7* {player} {action}" - try-format: "&7* {player} tries {action}... &f{result}" + try-format: "&7* {player} tries {action}... &6{result}" try-success: "&a[Success!]" try-fail: "&c[Fail!]" - do-format: "&8[Scene] &f{description} &7({player})" - roll-format: "&7* {player} rolls &f{result} &7out of &f{max}" + do-format: "&8[Scene] &6{description} &7({player})" + roll-format: "&7* {player} rolls &6{result} &7out of &6{max}" roll-min-error: "&cMinimum dice value: 2" roll-max-error: "&cMaximum dice value: 1000000" roll-invalid-number: "&cPlease specify a number: /roll [max]"