NextForge provides a built-in YAML-based i18n system so every plugin can ship and use its own translations, fully editable by server owners. Messages are authored in MiniMessage and loaded in UTF-8.
Works per plugin, isolated by its classloader. Supports runtime reloads and custom locale resolvers.
- YAML files per locale (e.g.,
en.yml,de.yml) under the plugin’s data folder - MiniMessage formatting (colors, gradients, click/hover, placeholders)
- Default + supported locales per plugin
- Reload at runtime (
/reload-style or your own command) - Pluggable LocaleResolver (e.g., derive locale from a player or config)
At first start, default YAMLs are copied from your plugin JAR (/messages/<lang>.yml) into the data folder:
<plugin-data>/messages/
├─ en.yml
└─ de.yml
You can add more locales by dropping
fr.yml,es.yml, etc. into that directory.
Each YAML is a simple map of key: value. Values are MiniMessage strings.
Use named placeholders like <name>, <count>, etc.
en.yml
app.start: "<green>Starting NextForge...</green>"
user.greet: "Hello, <yellow><name></yellow>!"
items.count: "You have <aqua><count></aqua> item(s)."
time.left: "Time left: <gold><seconds></gold>s"de.yml
app.start: "<green>NextForge wird gestartet...</green>"
user.greet: "Hallo, <yellow><name></yellow>!"
items.count: "Du hast <aqua><count></aqua> Gegenstand/Gegenstände."
time.left: "Verbleibende Zeit: <gold><seconds></gold>s"Gradle (Kotlin DSL):
dependencies {
implementation("net.kyori:adventure-text-minimessage:4.14.0")
implementation("org.yaml:snakeyaml:2.2")
}ForgedPlugin registers i18n services during enablePlugin():
- Copies defaults from
/messages/<lang>.ymlinside your JAR (if missing) - Loads
<data>/messages/*.yml - Registers the following services in the DI container:
YamlMessageSourceLocaleResolverI18n
You can override:
@Override protected java.util.Locale pluginDefaultLocale() { return java.util.Locale.ENGLISH; }
@Override protected java.util.Set<java.util.Locale> pluginSupportedLocales() {
return java.util.Set.of(java.util.Locale.ENGLISH, java.util.Locale.GERMAN);
}
@Override protected gg.nextforge.core.i18n.LocaleResolver pluginLocaleResolver() {
return new gg.nextforge.core.i18n.DefaultLocaleResolver(pluginDefaultLocale());
}The I18n service renders MiniMessage Components and supports placeholder variables.
import net.kyori.adventure.text.Component;
import gg.nextforge.core.i18n.I18n;
import static gg.nextforge.core.i18n.I18n.vars;
// Obtain I18n (unqualified registration):
I18n i18n = services().get(I18n.class).orElseThrow();
// With audience-specific locale (resolver decides the locale for the object you pass)
Component hello = i18n.component(player, "user.greet", vars("name", "Soldier"));
// Without audience (plugin default locale)
Component start = i18n.component("app.start");If you only need the raw string (e.g., for logs), use:
String raw = i18n.raw(null, "app.start");The helper
vars("key","value",...)builds a placeholder map; placeholders in YAML must match (e.g.,<name>).
Reload the YAML files at runtime (e.g., admin command):
services().get(gg.nextforge.core.i18n.YamlMessageSource.class).ifPresent(src -> {
try { src.reload(); } catch (Exception e) { getSLF4JLogger().error("i18n reload failed", e); }
});LocaleResolver chooses which locale to use for a given audience object.
Default implementation always returns the plugin’s default locale.
Custom example (Bukkit/Spigot/Paper):
public final class BukkitLocaleResolver implements gg.nextforge.core.i18n.LocaleResolver {
private final java.util.Locale fallback;
public BukkitLocaleResolver(java.util.Locale fallback) { this.fallback = fallback; }
@Override public java.util.Locale resolve(Object audience) {
if (audience instanceof org.bukkit.entity.Player p) {
String tag = p.getLocale(); // e.g., "de_de"
return java.util.Locale.forLanguageTag(tag.replace('_','-'));
}
return fallback;
}
}Use it in your plugin:
@Override
protected gg.nextforge.core.i18n.LocaleResolver pluginLocaleResolver() {
return new BukkitLocaleResolver(pluginDefaultLocale());
}- Keep keys stable; avoid renaming in updates when possible.
- Prefer named placeholders (
<name>,<count>) over positional ones. - Validate your MiniMessage strings; malformed tags will throw at render time.
- Consider adding a
/langcommand to let users switch their locale (store per-user in your DB/config and honor it in yourLocaleResolver).
Message shows the key instead of text
→ Key not found in selected locale and default locale. Add it to en.yml/de.yml.
MiniMessage error
→ Syntax issue (unclosed tag, invalid color). Validate with a MiniMessage tester, or log the raw string before rendering.
Files didn’t appear
→ Ensure you ship /messages/en.yml (and others) in your plugin JAR resources. On first run, they are copied to <data>/messages.
My players still see English
→ Your LocaleResolver likely returns the default locale. Implement one that reads player locale/preferences.
Core classes
YamlMessageSource– finds and loads<data>/messages/*.yml, reload supportI18n– renders MiniMessage components; placeholder map viaI18n.vars(...)LocaleResolver– strategy interface to determine localeDefaultLocaleResolver– constant fallback locale
ForgedPlugin hooks
pluginDefaultLocale()pluginSupportedLocales()pluginLocaleResolver()
Happy localizing! Keep your messages clean, colorful, and configurable.