diff --git a/src/main/java/dev/marcospaulo/canalhandia/Canalhandia.java b/src/main/java/dev/marcospaulo/canalhandia/Canalhandia.java index 0e998a6..d922851 100644 --- a/src/main/java/dev/marcospaulo/canalhandia/Canalhandia.java +++ b/src/main/java/dev/marcospaulo/canalhandia/Canalhandia.java @@ -5,6 +5,7 @@ import net.kyori.adventure.text.event.ClickEvent; import net.kyori.adventure.text.format.NamedTextColor; import net.kyori.adventure.text.format.TextDecoration; import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer; +import net.kyori.adventure.translation.TranslationStore; import org.bukkit.Bukkit; import org.bukkit.GameRule; import org.bukkit.Location; @@ -99,6 +100,7 @@ public final class Canalhandia extends JavaPlugin implements Listener { private Poll poll; private Titles titles; private DeathGift deathGift; + private TranslationStore> i18n; @Override public void onEnable() { @@ -106,6 +108,7 @@ public final class Canalhandia extends JavaPlugin implements Listener { // Ship the editable catalogues; false = never overwrite the operator's copy. saveResource("conquistas-catalogo.yml", false); saveResource("marcos-catalogo.yml", false); + i18n = I18n.install(i18n, getLogger()); settings = new Settings(this); notes = new Notes(new java.io.File(getDataFolder(), "notas.yml")); mail = new Mail(new java.io.File(getDataFolder(), "recados.yml")); @@ -252,6 +255,11 @@ public final class Canalhandia extends JavaPlugin implements Listener { deathGift.reload(); } + /** Reloads the i18n bundles from the jar and re-registers the translator. */ + void reloadI18n() { + i18n = I18n.install(i18n, getLogger()); + } + /** The weekly ranking baseline. Never null. */ WeeklyStats weeklyStats() { return weeklyStats; @@ -775,11 +783,12 @@ public final class Canalhandia extends JavaPlugin implements Listener { button = button.clickEvent(ClickEvent.runCommand( "/canalhandia reagir " + mourning.id() + " f")); } + Component prompt = Lang.tr(bedrock + ? "canalhandia.morte.luto.digitar" + : "canalhandia.morte.luto.prestar", + Component.text(name)); return Component.text(" ").append(button) - .append(Component.text(bedrock - ? "digite /f para prestar luto por " + name - : "prestar luto por " + name, - NamedTextColor.GRAY)); + .append(prompt.color(NamedTextColor.GRAY)); }), 2L); getServer().getScheduler().runTaskLater(this, () -> { @@ -789,9 +798,10 @@ public final class Canalhandia extends JavaPlugin implements Listener { int shown = Math.min(who.size(), settings.summaryNames()); String text = String.join(", ", who.subList(0, shown)) + (who.size() > shown ? " +" + (who.size() - shown) : ""); + Component summary = Lang.tr("canalhandia.morte.luto.resumo", + Component.text(text), Component.text(name)); Bukkit.broadcast(Component.text(" ") - .append(Component.text(text + " prestaram luto por " + name + ".", - NamedTextColor.GRAY))); + .append(summary.color(NamedTextColor.GRAY))); } if (liveReactions == mourning) { liveReactions = null; diff --git a/src/main/java/dev/marcospaulo/canalhandia/CanalhandiaCommand.java b/src/main/java/dev/marcospaulo/canalhandia/CanalhandiaCommand.java index cabe0d9..193d95d 100644 --- a/src/main/java/dev/marcospaulo/canalhandia/CanalhandiaCommand.java +++ b/src/main/java/dev/marcospaulo/canalhandia/CanalhandiaCommand.java @@ -107,6 +107,7 @@ final class CanalhandiaCommand implements CommandExecutor, TabCompleter { if (admin(sender)) { plugin.reloadConfig(); plugin.reloadCatalogo(); + plugin.reloadI18n(); plugin.rescheduleTimer(); plugin.rescheduleMilestones(); Msg.ok(sender, "Recarregado: " + Achievement.values().length diff --git a/src/main/java/dev/marcospaulo/canalhandia/I18n.java b/src/main/java/dev/marcospaulo/canalhandia/I18n.java new file mode 100644 index 0000000..2c9a45a --- /dev/null +++ b/src/main/java/dev/marcospaulo/canalhandia/I18n.java @@ -0,0 +1,77 @@ +package dev.marcospaulo.canalhandia; + +import net.kyori.adventure.key.Key; +import net.kyori.adventure.translation.GlobalTranslator; +import net.kyori.adventure.translation.TranslationStore; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.util.Locale; +import java.util.PropertyResourceBundle; +import java.util.ResourceBundle; +import java.util.logging.Logger; + +/** + * i18n: registers an Adventure {@link TranslationStore} (key + * {@code canalhandia}) into the {@link GlobalTranslator}, populated from the + * bundled {@code lang/messages_pt.properties} (source of truth) and + * {@code lang/messages_en.properties}. + * + *
Per-viewer rendering is automatic: Paper runs every component sent to an + * audience through the {@code GlobalTranslator} in the viewer's own locale, so + * one broadcast shows each player their own language. No per-player lookup. + * + *
Language resolution: registry default is {@code en} (the base bundle).
+ * {@code pt} and {@code pt_BR} fall back to the PT bundle; the store does the
+ * locale fallback, unknown locales hit the default. That's the whole rule.
+ */
+final class I18n {
+
+ static final Key SOURCE = Key.key("canalhandia");
+ static final Locale DEFAULT = Locale.ENGLISH;
+ /** Escape single quotes so MessageFormat does not swallow apostrophes. */
+ private static final boolean ESCAPE_QUOTES = true;
+ private static final String PT = "lang/messages_pt.properties";
+ private static final String EN = "lang/messages_en.properties";
+
+ private I18n() {
+ }
+
+ /**
+ * Loads both bundles and registers the store with the global translator.
+ * Removes any previously registered store first, so {@code /canalhandia
+ * reload} does not stack sources.
+ */
+ static TranslationStore> install(TranslationStore> previous, Logger logger) {
+ if (previous != null) {
+ GlobalTranslator.translator().removeSource(previous);
+ }
+ TranslationStore.StringBased The key is rendered per-viewer by the {@link GlobalTranslator} source that
+ * {@link I18n} registers; args are inserted by {@code MessageFormat} ({@code {0}},
+ * {@code {1}}, …).
+ */
+final class Lang {
+
+ private Lang() {
+ }
+
+ static Component tr(String key, Component... args) {
+ return Component.translatable(key, args);
+ }
+}
\ No newline at end of file
diff --git a/src/main/resources/lang/messages_en.properties b/src/main/resources/lang/messages_en.properties
new file mode 100644
index 0000000..03ed83b
--- /dev/null
+++ b/src/main/resources/lang/messages_en.properties
@@ -0,0 +1,7 @@
+# Canalhandia — English (translated from messages_pt.properties).
+# Never alter {0}/{1} placeholders or MiniMessage <...> tags.
+
+# Mourning (Canalhandia.onDeath) — F button under each death message.
+canalhandia.morte.luto.prestar=pay respects for {0}
+canalhandia.morte.luto.digitar=type /f to pay respects for {0}
+canalhandia.morte.luto.resumo={0} paid respects for {1}.
\ No newline at end of file
diff --git a/src/main/resources/lang/messages_pt.properties b/src/main/resources/lang/messages_pt.properties
new file mode 100644
index 0000000..48aad2e
--- /dev/null
+++ b/src/main/resources/lang/messages_pt.properties
@@ -0,0 +1,7 @@
+# Canalhandia — portugues (fonte de verdade). Padroes MessageFormat: {0}, {1}, ...
+# NUNCA altere os placeholders {0}/{1} nem as tags MiniMessage <...>.
+
+# Luto (Canalhandia.onDeath) — botao F sob cada mensagem de morte.
+canalhandia.morte.luto.prestar=prestar luto por {0}
+canalhandia.morte.luto.digitar=digite /f para prestar luto por {0}
+canalhandia.morte.luto.resumo={0} prestaram luto por {1}.
\ No newline at end of file
diff --git a/src/test/java/dev/marcospaulo/canalhandia/I18nTest.java b/src/test/java/dev/marcospaulo/canalhandia/I18nTest.java
new file mode 100644
index 0000000..c7a2a67
--- /dev/null
+++ b/src/test/java/dev/marcospaulo/canalhandia/I18nTest.java
@@ -0,0 +1,109 @@
+package dev.marcospaulo.canalhandia;
+
+import net.kyori.adventure.key.Key;
+import net.kyori.adventure.text.Component;
+import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
+import net.kyori.adventure.translation.GlobalTranslator;
+import net.kyori.adventure.translation.TranslationStore;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.nio.charset.StandardCharsets;
+import java.text.MessageFormat;
+import java.util.Locale;
+import java.util.PropertyResourceBundle;
+import java.util.ResourceBundle;
+import java.util.TreeSet;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+/**
+ * The two contracts the i18n bundles must hold: identical key sets across PT
+ * and EN, and one translatable rendering differently per locale. The store's
+ * own {@code translate(key, locale)} is an exact-locale lookup (no fallback);
+ * the fallback chain runs in {@link GlobalTranslator#render}, which is what
+ * the locale-resolution tests exercise.
+ */
+class I18nTest {
+
+ private TranslationStore.StringBased