-
Notifications
You must be signed in to change notification settings - Fork 7
Localization
What this page covers: the message system built on @FCLocale + LocaleMessage. You declare a
message once, inline, with one translation per language; EverNifeCore syncs it to a per-plugin language
file, resolves the language to render it in, resolves its ${placeholders}, and sends it as a
FancyText (so hover/click come for free). This is how commands, argument parsers, and access
validations talk to players.
import br.com.finalcraft.evernifecore.locale.FCLocale;
import br.com.finalcraft.evernifecore.locale.LocaleMessage;
import br.com.finalcraft.evernifecore.locale.LocaleType;
public class TemplateMessages {
@FCLocale(lang = LocaleType.EN_US, text = "&eYour profile &7- &fLevel: &a${level} &7| &fCoins: &6${coins}")
@FCLocale(lang = LocaleType.PT_BR, text = "&eSeu perfil &7- &fNível: &a${level} &7| &fCoins: &6${coins}")
public static LocaleMessage PROFILE_INFO;
}Send it, declaring the placeholders - bare key in the declaration, ${key} in the text:
TemplateMessages.PROFILE_INFO
.addPlaceholder("level", profile.level)
.addPlaceholder("coins", profile.coins)
.send(sender);The field is a public static LocaleMessage; it starts null and is populated when the class is
scanned (see Loading). Each @FCLocale on the field is one translation.
Stack one @FCLocale per language on the same field (the annotation is @Repeatable, so you just
write several):
| Attribute | Type | Default | Purpose |
|---|---|---|---|
text |
String |
"" |
The message text (color codes with & or §). |
lang |
String |
EN_US |
Which language this translation is for. |
hover |
String |
"" |
Hover tooltip text. |
click |
String |
"" |
Click action payload (a command by default). |
clickType |
ClickActionType |
RUN_COMMAND |
RUN_COMMAND / SUGGEST_COMMAND / OPEN_URL / NONE. |
children |
Child[] |
{} |
Extra appended pieces, each with its own text/hover/click. |
@FCLocale(
lang = LocaleType.EN_US,
text = "&aClick here",
hover = "&7Opens the wiki",
click = "https://github.com/EverNife/EverNifeCore/wiki",
clickType = ClickActionType.OPEN_URL
)
public static LocaleMessage WIKI_LINK;LocaleType ships three constants - EN_US, PT_BR and ZH_CN - but lang is a plain String,
so any language tag (e.g. "ES_ES") is valid; add another @FCLocale with that tag to provide the
translation. LocaleType.register("ES_ES") makes the tag known to /eclocale as well.
The static fields are populated by a scan. Command classes, argument parsers, and access
validations are scanned automatically when you register them - their @FCLocale fields just work.
For a plain holder class (like TemplateMessages above), scan it once at startup:
import br.com.finalcraft.evernifecore.locale.scanner.FCLocaleScanner;
FCLocaleScanner.scanForLocale(ecPluginData, false, TemplateMessages.class);The scan derives each message's key from ClassName.FIELD_NAME, then syncs the hardcoded defaults
to a language file so server owners can edit the wording. From then on the file is the source of
truth for the text.
⚠️ Gotcha -LocaleMessagefields must bepublic static. A non-static field is logged as an error and skipped, and a field with no@FCLocaleannotation warns. Two fields of the same plugin resolving to the same key is an error naming both fields: the first one registered wins.
Each plugin gets its own language files under its data folder:
plugins/<YourPlugin>/localization/
├── localization_config.yml # selects the active file (Localization.fileName)
├── lang_EN_US.yml # generated from the @FCLocale(EN_US) defaults, editable
└── lang_PT_BR.yml # generated from the @FCLocale(PT_BR) defaults, editable
A message resolves to the plugin's active language, chosen by the server owner. Admins manage it with the built-in command:
/eclocale list # show every plugin and its current locale (clickable to switch)
/eclocale set <PluginName> <Locale> # set one plugin's language
/eclocale setall <Locale> # set every plugin's language at once
/eclocale self <Locale> # a player picks the language THEY read messages in
If the active language has no translation for a given message, EverNifeCore falls back to the first
@FCLocale declared on that field.
/eclocale self is opt-in, and off by default. Turn it on in plugins/EverNifeCore/config.yml:
Settings:
Locale:
PER_PLAYER_LOCALE: trueWhile the setting is off, the per-player section is never registered, /eclocale self is hidden from
tab completion and inert if typed, and every message uses the plugin's configured locale. Turned on, each
player's choice is stored in their player data and read from cache when a message is rendered for
them - so it never blocks and never touches storage on the send path. A player who has not chosen,
and any non-player recipient, falls back to the plugin's language.
A message declares the value of ${key} (case-insensitive). The key is declared bare and cited
with its delimiters in the text:
TemplateMessages.COINS_RECEIVED // text: "&aYou received &6${amount}&a coins"
.addPlaceholder("amount", amount)
.addPlaceholder("coins", profile.coins)
.send(sender);Nothing is computed eagerly: a key the text never cites is never resolved, and a key cited twice (text and hover, say) is resolved once per render.
⚠️ Declare the bare name.addPlaceholder("%amount%", ...)registers the key exactly as written, so it never matches anything - and says so once in the console, naming the bare form. Two keys differing only in case (amountandAmount) throw at registration rather than silently shadowing each other.
For a value that depends on the receiving player, pass a Function<PlayerData, Object> - it's
evaluated per recipient, using the player's cached data, so it never blocks:
message.addPlaceholder("balance", playerData -> economy.getBalance(playerData)).send(player);For a value that needs the whole render context (recipient, their data, the command scope), use
addParser, which also takes the description an integrating plugin can list back to the user:
message.addParser("reader", "The name of whoever is reading this line",
context -> context.getSender() == null ? "console" : context.getSender().getName());Every message - locale message, hand-built piece, or chain assembled at runtime - additionally
answers for ${label} and ${subcmd}, the command label and sub-command the player typed. They are
the lowest precedence there is: a message that declares its own label shadows them.
${key} is the only closure the message engine reads, and there is one engine: each message
carries a PlaceholderProvider<RenderContext> served by a ${} RegexReplacer - the very same
RegexReplacer described on Placeholders. What changes between the two pages is only the object
the engine resolves against. You can push a fully-composed replacer into a message with
addReplacer(compoundReplacer), which runs after the message's own ${key} pass - that is how
PlaceholderAPI's %papi% tokens reach a message.
Call .custom() (or any of the add*/set* methods, which start a custom copy) to tweak a single
send without touching the shared field: override the hover/click, add a replacer, or append messages.
message.setHover("&7extra tooltip").send(sender); // per-send hover
message.setClickSuggest("/pay " + name + " ").send(sender); // click suggests a command
messageA.append(messageB).send(sender); // join two messages into one line
message.append(FancyText.of("&7 (click me)")) // append a raw FancyText too
.send(sender);
message.broadcast(); // the broadcast audience, console includedEach of those returns an ILocaleMessageBase backed by a fresh copy, so the underlying
LocaleMessage field is never mutated - concurrent sends with different placeholders are safe.
.custom() returns the concrete SendCustom when you want to hold it in a variable.
The vocabulary is the same as FancyText's: setX replaces an attribute (a message carries at
most one hover and one click), addX accumulates, append builds a chain.
message.send(sender); // one or more FCommandSender
message.send(listOfSenders); // a List
message.sendIf(sender.hasPermission("myplugin.debug"), sender); // send only when it holds
message.broadcast(); // every online player AND the console
⚠️ sendIfmakes only the delivery conditional. Whatever the message declared is still evaluated, so a value that is expensive or unsafe to compute with the condition false still belongs behind a realif.
A message delivered from an asynchronous task has no command scope, so ${label}/${subcmd} would
come up empty. Pass the context explicitly:
RenderContext context = RenderContext.of(sender, CommandMessageContext.of(label, "give"));
FCScheduler.runAsync(() -> message.send(context, sender));What the explicit context contributes is its CommandMessageContext - not the recipient. Every
recipient still gets their own render.
FancyText preview = message.getFancyText(sender); // exactly what send(sender) would deliver
boolean exists = message.isDefined(); // was ANY language registered for it?getDefaultFancyText() never returns null: a message with no registered language renders as
[LOCALE_NOT_DEFINED:<key>], so it is visible instead of silently blank. isDefined() is the
explicit question to ask when there is something else to do about it.
-
FancyText - what a resolved
LocaleMessagebecomes; hover/click building blocks. -
Command Framework -
@FCLocaleas a command/sub-command description, and message fields on commands. -
Argument Parsing - localized error messages inside a custom
ArgParser. -
Placeholders - the
Replacer/RegexReplacerengine and PlaceholderAPI bridging. - Quick Start - a minimal plugin wiring a command, a config, and a locale together.
EverNifeCore · Home · made by Petrus Pradella
Getting Started
Commands & Text
Player Data & Storage
- PlayerData & PDSections
- Accounts
- Storage Backends
- Inline Backends for Plugins
- Legacy Data Migration
- Cooldowns
Config & Minecraft Systems
- Configuration
- Scheduler & Threading
- Items & NBT
- GUI Framework
- Integrations
- Economy
- Version Compatibility
Architecture & Reference