Repository navigation
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).
- Copies every bundled language file into the plugin data folder on the first
loadafter 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
formathelpers for arbitrary placeholders (maps →Placeholder.unparsed).
- Locale files stay synced via built-in
LangLoaderintegration. - 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 anyjava.util.logging.Logger.
- 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 oneWARNINGper 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-:playerNameorplayer.namethrows anIllegalArgumentExceptionwhen the message is rendered, and anullvalue throws aNullPointerException. -
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 noen_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 neitherloadnorreloadresets it: callsetPrefixLabelafter each load. -
Reloading:
reload(FileConfiguration)reads only thelanguagekey and doesn't reload the config itself;syncLocaleFiledoesn't refresh the cached messages, so callloadafterwards. -
Threads: rendering is safe from any thread:
loadbuilds the new texts first and swaps them in at once, so a message rendered during a load uses the old or the new texts.loaditself reads and writes files on the calling thread, so call it on the main thread, and never run two loads at once. -
Key renames:
setLegacyKeyMigrationsrenames 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.
- Instantiate
MessageServiceduring plugin enable with your desired prefix + fallback label. - Load the configured locale (defaults to
en_US) and immediately set theprefix_labelfrom config. - Reuse the same service to format MiniMessage components anywhere in your plugin.
| 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. |
-
<prefix>– full MiniMessage prefix: the locale'sui.prefixvalue if set, otherwise the constructor'sdefaultPrefix(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_PLACEHOLDERandPREFIX_LABEL_PLACEHOLDERname these for tests such as LocaleFileChecker.
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.
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"));
}
}- Locale values are normalized by
LangLoader.resolveLocale(en_US,en_US.ymlanden-usall 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 firstloadafter the admin fixes it picks up their texts again. - Legacy
§codes in a lang file would make MiniMessage throw an exception, soloadturns them into tags: a color becomes<reset><color>(as in legacy text, a color ends bold and the like),§land the other formats become their tags,§rbecomes<reset>and§x§R§R§G§G§B§Bbecomes<#RRGGBB>. OneWARNINGper 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
ConfigWatcherto callmessages.reload(getConfig())whenever configs change on disk. - MiniMessage comes with the
compileOnlyPaper API and is provided by Paper at runtime, so shading TurtleLib doesn't bundle MiniMessage into your jar.
- Builds on top of
LangLoaderfor disk sync + locale discovery. - Works well with
ConfigWatcherto auto-reload locales whenever files change. - Legacy key renames are applied through
ConfigKeyMigrator(viaLangLoader).