Skip to content

Latest commit

 

History

History
202 lines (147 loc) · 6.08 KB

File metadata and controls

202 lines (147 loc) · 6.08 KB

🌐 Internationalization (i18n) – YAML + MiniMessage

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.


What you get

  • 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)

File locations

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.


YAML format (MiniMessage)

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"

Dependencies

Gradle (Kotlin DSL):

dependencies {
    implementation("net.kyori:adventure-text-minimessage:4.14.0")
    implementation("org.yaml:snakeyaml:2.2")
}

Bootstrapping (already integrated)

ForgedPlugin registers i18n services during enablePlugin():

  • Copies defaults from /messages/<lang>.yml inside your JAR (if missing)
  • Loads <data>/messages/*.yml
  • Registers the following services in the DI container:
    • YamlMessageSource
    • LocaleResolver
    • I18n

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());
}

Using i18n in your plugin

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>).


Runtime reload

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); }
});

Locale resolution

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());
}

Best practices

  • 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 /lang command to let users switch their locale (store per-user in your DB/config and honor it in your LocaleResolver).

Troubleshooting

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.


Reference

Core classes

  • YamlMessageSource – finds and loads <data>/messages/*.yml, reload support
  • I18n – renders MiniMessage components; placeholder map via I18n.vars(...)
  • LocaleResolver – strategy interface to determine locale
  • DefaultLocaleResolver – constant fallback locale

ForgedPlugin hooks

  • pluginDefaultLocale()
  • pluginSupportedLocales()
  • pluginLocaleResolver()

Happy localizing! Keep your messages clean, colorful, and configurable.