Repository navigation
LangLoader
Basti edited this page Sep 27, 2026
·
2 revisions
Low-level helper that discovers bundled lang/*.yml resources, copies them into the plugin data folder, and keeps user-edited files in sync with new defaults.
- Central place to list every locale shipped with your plugin.
- Syncs missing files on startup (and whenever you call it) without overwriting end-user translations.
- Returns the list of keys that were merged so you can log or audit changes.
- Powers
MessageService, but can also be used independently if you prefer your own rendering pipeline.
- Handles both discovery and merging so no manual YAML diffing is needed.
- Accepts locale names with or without
.yml, simplifying config UX. - Only real locale codes (
xx_YY) are used as file names, so a config value can never point outsidelang/. - Reports merged keys so you can surface upgrade information to admins.
- Never rewrites a lang file that isn't valid YAML: the file stays byte-identical and the bundled texts stand in until it's fixed.
-
Fixed layout: bundled files must be
lang/<code>.ymlat the root of your jar, and they are copied toplugins/<Plugin>/lang/<code>.yml..yaml, subfolders or another folder name aren't supported, and every method needs aJavaPlugin. -
Locale codes: exactly two lowercase letters,
_and two uppercase letters (de_DE). Minecraft locales with three letters (fil_PH) or scripts can't be used. File names aren't normalized: a bundledde_de.ymlorde-DE.ymlis ignored without a log line, and on a case-sensitive file system an admin'sde_de.ymlisn't used; a newde_DE.ymlappears next to it. -
Base locale:
en_USif the plugin ships it, otherwise its alphabetically first bundled locale; there is no setter. Missing keys, a locale without a bundled file and an invalidlanguagevalue all use that base, and always its bundled file, never the admin's edited copy. With several locales and noen_US, the alphabet decides (de_DEbeforefr_FR); a plugin that ships no lang file at all gets empty files, and its texts show as their keys. - Only adds keys: changed default texts don't reach existing files, and keys you dropped stay on disk unless a rename removes them. A key the admin deleted comes back on the next load. Only the requested locale is merged; the other copied files stay as they are until they are loaded or synced.
-
Always writes: loading creates
lang/, copies every bundled locale and creates a file for any well-formed but unknown code, so a typo likede_DDleaves a file with the base locale's texts behind. The active file is rewritten when keys are added,<light_red>is repaired or keys are renamed; renaming can also writelang/.seen-defaults.yml. -
Encodings: bundled files must be UTF-8. Files on disk may be UTF-8 or Windows-1252 ("ANSI"). On Windows set to a Central European or Cyrillic language, "ANSI" means Windows-1250 or 1251; LangLoader still reads such a file as Windows-1252, so letters like
łorжcome out wrong. Save those files as UTF-8. -
Bukkit's YAML rules: unquoted
on,off,yesandnobecometrueandfalse, and a.in a key always means nesting ("ui.prefix": xis the same asui: {prefix: x}). - Threads: nothing is locked. Loading or syncing the same file from several threads at once can lose changes.
-
Renames by key name: a rename map renames by key name in every section, like
ConfigKeyMigrator.rewriteLegacyKeysInFile:command.no-permission→command.no_permissionalso renames ano-permissionkey in any other section. A map with conflicting or chained names, or an entry that changes the nesting depth, throws anIllegalArgumentExceptionright away, before any file is touched and even when the lang file isn't valid YAML. -
Plain value where the bundled file has a section: the admin's value (e.g.
command: "x") stays, and every load logs oneWARNINGthat names it. The section's keys aren't added to the file until the value is renamed or removed; MessageService shows the base locale's bundled texts for them meanwhile.
- Call
LangLoader.getBundledLocales(plugin)to list all packaged locales. - Load the desired locale via
loadLocale(...)so missing keys merge automatically. - On upgrades, run
syncLocale(...)for each locale you ship and log the returned keys.
| Method | Description |
|---|---|
getBundledLocales(JavaPlugin) |
Scans lang/*.yml in your plugin JAR and in plugins/<Plugin>/lang/, keeps names shaped like xx_YY, and returns them sorted (without .yml); empty if there are none. |
ensureBundledLocales(JavaPlugin) |
Copies every locale from getBundledLocales into plugins/<Plugin>/lang/ if the file is missing (existing files are never overwritten). |
loadLocale(JavaPlugin, String locale) |
Runs ensureBundledLocales, syncs the requested locale, and returns it as a YamlConfiguration with the base locale's bundled file attached as defaults. If the file isn't valid YAML, returns the locale's bundled texts instead. |
loadLocale(JavaPlugin, String locale, Map<String, String>) |
Same, but first renames legacy keys (old dotted path → new dotted path, applied by key name) in the file via ConfigKeyMigrator; a rejected map throws right away. |
syncLocale(JavaPlugin, String locale) |
Creates the file if missing, merges missing keys from the bundled resource (falling back to the base locale's), and returns the newly inserted keys (none if the file isn't valid YAML). Invalid locale → base locale. |
syncLocale(JavaPlugin, String locale, Map<String, String>) |
Same, but applies legacy key renames (by key name) first so translator edits carry over to the new key instead of being shadowed by fresh defaults; a rejected map throws right away. |
loadLocaleResource(JavaPlugin, String locale) |
Reads the bundled lang/<locale>.yml straight from the JAR (no disk copy); returns null if it doesn't exist. |
resolveLocale(JavaPlugin, String locale) |
Returns the canonical locale code (de-de → de_DE), or the base locale with a console warning if the value is no locale code (e.g. ../config). Blank → base locale without warning. |
DEFAULT_LOCALE |
Constant (en_US), the preferred base locale. If the plugin doesn't ship it, its alphabetically first bundled locale is the base. |
- Discover resources → sync to disk → reload your in-memory caches.
- Use the
List<String>returned bysyncLocaleto print upgrade notes to admins.
import net.kroet.turtlelib.helper.LangLoader;
// Discover locales bundled in src/main/resources/lang/
List<String> bundled = LangLoader.getBundledLocales(this); // e.g., ["en_US", "de_DE"]
// Load a locale file (merging missing keys) and get it as YamlConfiguration
YamlConfiguration cfg = LangLoader.loadLocale(this, "en_US");
// Force sync for a specific locale and obtain added keys
List<String> mergedKeys = LangLoader.syncLocale(this, "de_DE");
// Same, but carry a renamed key's translated value over to its new name first
List<String> migratedKeys = LangLoader.syncLocale(this, "de_DE",
Map.of("log.update-found", "log.update_found"));- Accept config values with or without
.yml, with_or-, in any case (en_US=en_US.yml=en-us).LangLoadernormalizes them automatically; anything that is noxx_YYcode falls back to the base locale with a warning that names it. - After calling
syncLocale, reload your in-memory caches (e.g., viaMessageService.load(locale)) so changes take effect. - Pair with
ConfigWatcherto re-runsyncLocalewhen admins edit files externally. - Log the merged keys during upgrades so server owners know what changed.
- Merged UTF-8 files are rewritten like
YamlConfiguration#savedoes, but via a hidden temporary file, so a crash while writing normally doesn't leave them half written: comments stay, but quoting and indentation are normalized and long lines are wrapped; only the legacy-key rename step keeps the formatting exactly as written. - A file that isn't valid UTF-8 is read as Windows-1252 ("ANSI"), so umlauts saved by an older Windows editor display correctly. Missing keys are then added as text in that encoding by ConfigDefaultsInserter, and every other byte stays as it was. A new text with characters Windows-1252 can't store, also one spread over several lines, is written as one double-quoted line with
\uXXXXescapes, which reads back unchanged; a key whose lines can't be written that way (e.g. a list) is skipped with one warning that suggests saving the file as UTF-8. -
syncLocalealso replaces the invalid color tag<light_red>(and</light_red>) with<red>directly in the file (formatting preserved) and logs one console line when it does; other spellings such as<!light_red>stay as they are; see the color table in MessageService. - A file that isn't valid YAML is left exactly as it is: no legacy-key rename, no
<light_red>repair, no merge. Each sync logs one warning with the file, the line and column, and the parser's description of the problem, e.g.lang/de_DE.yml isn't valid YAML (line 5, column 28: expected <block end>, but found '<scalar>'); using the bundled texts and leaving the file unchanged until it is fixed.loadLocalethen returns the bundled texts of that locale (the base locale's if it has none), with the base locale as defaults. - A requested locale with no bundled resource (e.g.
pt_BR) still gets a file on disk, filled from the base locale's bundled texts. When that file is created, one warning names the locale, the base locale it was filled from and the bundled ones; an existing file, e.g. the admin's own translation, is used as it is. -
MessageService#loadcallsensureBundledLocalesonly on its first call perMessageServiceinstance (normally once per plugin start), so every bundled locale is written on first start, but a locale file deleted while the server runs only comes back on the next start. CallingloadLocaledirectly still runsensureBundledLocalesevery time.
-
MessageServiceconsumesLangLoaderfor locale syncing before rendering MiniMessage. - Works with
ConfigWatcherto reload locale files immediately after filesystem edits. - Uses
ConfigKeyMigratorfor the optional legacy-key renames.