Skip to content

MessageService

Basti edited this page Sep 27, 2026 · 2 revisions

Shared MiniMessage helper for Paper plugins. It centralizes locale syncing, placeholder injection, and prefix formatting so every plugin reads lang/*.yml the same way (built on top of LangLoader).

Why Use It? 🎯

  • Copies every bundled language file into the plugin data folder on the first load after the plugin starts (details in LangLoader).
  • Merges missing keys into the active locale file on every load (e.g. after a config reload) without touching customized values.
  • Wraps MiniMessage to deserialize components, strings, and console-friendly plain text.
  • Injects prefix tags automatically (<prefix>, <prefix_label>) so locales stay DRY.
  • Provides format helpers for arbitrary placeholders (maps → Placeholder.unparsed).

Highlights ✨

  • Locale files stay synced via built-in LangLoader integration.
  • Prefix + label helpers keep chat formatting consistent across commands.
  • Works with Adventure components and plain-text, console-safe fallbacks.
  • Shared log(...) helper writes localized messages to any java.util.logging.Logger.

Limitations ⚠️

  • Paper only: it needs Adventure and MiniMessage at runtime, which Paper provides and Spigot doesn't.
  • One language per instance: everyone sees the server's language; there is no per-player or client-language rendering.
  • MiniMessage only: &c-style codes show up as text. Legacy § codes are turned into the matching tags when the file is loaded, with one WARNING per load that names the keys (see Tips & Notes).
  • Placeholders: only the <name> syntax; {name}, %name% and PlaceholderAPI aren't resolved, and a placeholder without a value stays as literal text. Values are inserted as plain text, so they can't carry colors, hover or click actions. Names must be lowercase letters, digits, _ or -: playerName or player.name throws an IllegalArgumentException when the message is rendered, and a null value throws a NullPointerException.
  • Single texts only: a YAML list (e.g. lore lines) renders as [a, b], and there is no list or "has key" accessor. Keys are dotted paths, so a key name can't contain a dot.
  • Missing keys: a key missing from the active locale falls back to the bundled base locale (en_US, or the alphabetically first bundled locale if the plugin ships no en_US; see LangLoader); a key missing there too renders as the key itself, without a log line.
  • Prefix: the key is fixed as ui.prefix. <prefix_label> is inserted as plain text, so it can't be styled, and neither load nor reload resets it: call setPrefixLabel after each load.
  • Reloading: reload(FileConfiguration) reads only the language key and doesn't reload the config itself; syncLocaleFile doesn't refresh the cached messages, so call load afterwards.
  • Threads: rendering is safe from any thread: load builds the new texts first and swaps them in at once, so a message rendered during a load uses the old or the new texts. load itself reads and writes files on the calling thread, so call it on the main thread, and never run two loads at once.
  • Key renames: setLegacyKeyMigrations renames by key name in every section, not by path, and rejects a list with conflicting or chained names or depth changes at once (see LangLoader).
  • No plurals or number and date formatting.

Quick Start 🚀

  1. Instantiate MessageService during plugin enable with your desired prefix + fallback label.
  2. Load the configured locale (defaults to en_US) and immediately set the prefix_label from config.
  3. Reuse the same service to format MiniMessage components anywhere in your plugin.

References 📚

Method Purpose
MessageService(JavaPlugin, String defaultPrefix, String defaultLabel) Configure prefix styling + fallback label. Blank prefix → <gray>[<prefix_label>]</gray>; blank label → the plugin's name.
setLegacyKeyMigrations(Map<String, String>) Registers old → new dotted key renames applied to the locale file on load/syncLocaleFile, so translators' edits survive a rename. They work by key name in every section, like ConfigKeyMigrator.rewriteLegacyKeysInFile; a map with conflicting or chained names or depth changes throws an IllegalArgumentException right here. Call before the first load.
setLegacyDefaultsRoot(String jarFolder) Jar folder holding the old version's lang files as lang/<locale>.yml, e.g. legacy/1.x/. An old key found next to its renamed key (e.g. after a downgrade) is removed while it still has that old text; see ConfigKeyMigrator.
load(String locale) Syncs/loads lang/<locale>.yml (reading it once), then replaces the cached texts and the prefix at once (ui.prefix from the locale, if present), and records it as the active language. If the file isn't valid YAML, uses the locale's bundled texts and leaves the file unchanged. The first call per instance also copies missing bundled locales to disk (LangLoader.ensureBundledLocales); later calls skip that, so reloads don't rescan the plugin jar.
getLastAddedKeys() Keys the last load merged into the active locale file from the bundled defaults, for reporting them without a second syncLocaleFile. Empty if the file isn't valid YAML.
reload(FileConfiguration config) Convenience wrapper: reads the language key from your config (default: current locale) before calling load.
setPrefixLabel(String label) Sets the <prefix_label> value; blank/null restores the constructor fallback.
getLanguage() Returns the active locale code in canonical form (de-de → de_DE; invalid values → the base locale, see LangLoader).
component(String key) Returns a Component built from MiniMessage markup plus <prefix> tags. Unknown keys render the key itself.
format(String key, Map<String, String> replacements) Like component, but fills custom placeholders (e.g., <player>).
plain(String key[, Map]) Serializes the component to plain text (all formatting stripped) for logs.
log(Logger, Level, String key[, Map]) Resolves the key via plain(...) and writes it to the given logger at the given level.
getBundledLocales(JavaPlugin) (static) Delegates to LangLoader.getBundledLocales: locale codes bundled in the JAR or present in the data folder's lang/. Useful for config UIs.
syncLocaleFile(String locale) Forces a merge of the given locale file on disk (applying registered legacy key renames) and returns the added keys.

Placeholder Reference 🔖

  • <prefix> – full MiniMessage prefix: the locale's ui.prefix value if set, otherwise the constructor's defaultPrefix (itself defaulting to <gray>[<prefix_label>]</gray>).
  • <prefix_label> – human-readable label controlled via config or constructor fallback.
  • Any extra tokens can be added to your locale files; supply values through messages.format(key, Map.of("token", value)).
  • The constants PREFIX_KEY (ui.prefix), PREFIX_PLACEHOLDER and PREFIX_LABEL_PLACEHOLDER name these for tests such as LocaleFileChecker.

Color Names 🎨

MiniMessage knows exactly these 16 named colors (plus the spellings grey and dark_grey). Any other name, e.g. <light_red>, is not a color and shows up as literal text in chat.

Tag Hex Legacy code
<black> #000000 §0
<dark_blue> #0000AA §1
<dark_green> #00AA00 §2
<dark_aqua> #00AAAA §3
<dark_red> #AA0000 §4
<dark_purple> #AA00AA §5
<gold> #FFAA00 §6
<gray> #AAAAAA §7
<dark_gray> #555555 §8
<blue> #5555FF §9
<green> #55FF55 §a
<aqua> #55FFFF §b
<red> #FF5555 §c
<light_purple> #FF55FF §d
<yellow> #FFFF55 §e
<white> #FFFFFF §f
  • <red> is the bright red (often called "light red"); <dark_red> is the darker one.
  • The legacy codes are listed for reference only; write tags in lang files. & codes show up as text, and § codes are converted with a warning (see Tips & Notes).
  • Any other shade works as a hex color: <#FF8800>text</#FF8800> or <color:#FF8800>text</color>.
  • Formatting tags such as <bold>, <italic>, <underlined>, <gradient:…> and <rainbow> are described in the MiniMessage format reference.

Examples 🧪

import net.kroet.turtlelib.helper.MessageService;

public final class MyPlugin extends JavaPlugin {
    private MessageService messages;

    @Override
    public void onEnable() {
        saveDefaultConfig();
        messages = new MessageService(this, "<green>[<prefix_label>]</green>", "MyPlugin");
        messages.setLegacyKeyMigrations(Map.of("log.update-found", "log.update_found")); // optional
        messages.load(getConfig().getString("language", "en_US"));
        messages.setPrefixLabel(getConfig().getString("chat_prefix_label", "MyPlugin"));
        messages.log(getLogger(), Level.INFO, "log.update_found", Map.of("current", "1.0.0"));
    }

    public void reloadLocales() {
        messages.load(getConfig().getString("language"));
        messages.setPrefixLabel(getConfig().getString("chat_prefix_label"));
    }
}

Tips & Notes 💡

  • Locale values are normalized by LangLoader.resolveLocale (en_US, en_US.yml and en-us all work); the rules are on the LangLoader page.
  • A lang file with a YAML syntax error doesn't break the plugin's texts: each load, at startup and on reload alike, logs one warning naming the line and column, uses the bundled texts of that locale and leaves the file unchanged. The first load after the admin fixes it picks up their texts again.
  • Legacy § codes in a lang file would make MiniMessage throw an exception, so load turns them into tags: a color becomes <reset><color> (as in legacy text, a color ends bold and the like), §l and the other formats become their tags, §r becomes <reset> and §x§R§R§G§G§B§B becomes <#RRGGBB>. One WARNING per load names the affected keys, e.g. Language en_US: 2 text(s) use legacy section-sign color codes ...: ui.prefix, command.usage.
  • Pair with ConfigWatcher to call messages.reload(getConfig()) whenever configs change on disk.
  • MiniMessage comes with the compileOnly Paper API and is provided by Paper at runtime, so shading TurtleLib doesn't bundle MiniMessage into your jar.

Relationships to Other Helpers 🔗

  • Builds on top of LangLoader for disk sync + locale discovery.
  • Works well with ConfigWatcher to auto-reload locales whenever files change.
  • Legacy key renames are applied through ConfigKeyMigrator (via LangLoader).

Clone this wiki locally