feat: hover translation (Google, Chrome endpoint with gtx fallback), LuckPerms prefixes in chat, restyled messages
Some checks are pending
Build & Test / Build LoChat (push) Waiting to run
Some checks are pending
Build & Test / Build LoChat (push) Waiting to run
This commit is contained in:
parent
bd39118b14
commit
260986ff11
9 changed files with 396 additions and 81 deletions
|
|
@ -60,6 +60,8 @@ public final class LoChat extends JavaPlugin {
|
|||
configManager.getTranslationEndpoint(),
|
||||
configManager.getTranslationApiKey(),
|
||||
configManager.isTranslationEnabled(),
|
||||
configManager.getTranslationProvider(),
|
||||
configManager.getTranslationTimeoutMs(),
|
||||
getLogger()
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -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<String> getTranslationHoverFormat() {
|
||||
return accessor.getTranslationHoverFormat();
|
||||
}
|
||||
|
||||
public String getTranslationBypassPermission() {
|
||||
return accessor.getTranslationBypassPermission();
|
||||
}
|
||||
|
||||
// ========== LoPreff integration ==========
|
||||
|
||||
public boolean isLopreffEnabled() {
|
||||
|
|
|
|||
|
|
@ -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<String> getTranslationHoverFormat() {
|
||||
java.util.List<String> 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() {
|
||||
|
|
|
|||
|
|
@ -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<Player> 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<Player> 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<Player> 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<String> 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");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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<String> languages, long timeoutMs) {
|
||||
if (!isEnabled() || text == null || text.isBlank()) {
|
||||
return;
|
||||
}
|
||||
java.util.List<CompletableFuture<String>> 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<String> 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<String> 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() {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -5,25 +5,25 @@
|
|||
|
||||
commands:
|
||||
mute:
|
||||
usage: "&cUsage: &f/mute <player> [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 <player> [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"
|
||||
|
||||
|
|
|
|||
|
|
@ -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 <hex1> <hex2> ..."
|
||||
color-success: "&aGradient applied!"
|
||||
color-help: "&cUsage: &6/color <hex1> <hex2> ..."
|
||||
color-success: "&fGradient applied!"
|
||||
color-no-colors: "&7You have no colors set"
|
||||
prefix-help: "&cUsage: &f/prefix <text>"
|
||||
prefix-help: "&cUsage: &6/prefix <text>"
|
||||
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 <name> &7or &f/nick reset"
|
||||
current: "&7Your nickname: &6{nick}"
|
||||
usage: "&cUsage: &6/nick <name> &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]"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue