Skip to content

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.

Why Use It? 🎯

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

Highlights ✨

  • 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 outside lang/.
  • 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.

Limitations ⚠️

  • Fixed layout: bundled files must be lang/<code>.yml at the root of your jar, and they are copied to plugins/<Plugin>/lang/<code>.yml. .yaml, subfolders or another folder name aren't supported, and every method needs a JavaPlugin.
  • 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 bundled de_de.yml or de-DE.yml is ignored without a log line, and on a case-sensitive file system an admin's de_de.yml isn't used; a new de_DE.yml appears next to it.
  • Base locale: en_US if the plugin ships it, otherwise its alphabetically first bundled locale; there is no setter. Missing keys, a locale without a bundled file and an invalid language value all use that base, and always its bundled file, never the admin's edited copy. With several locales and no en_US, the alphabet decides (de_DE before fr_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 like de_DD leaves 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 write lang/.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, yes and no become true and false, and a . in a key always means nesting ("ui.prefix": x is the same as ui: {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_permission also renames a no-permission key in any other section. A map with conflicting or chained names, or an entry that changes the nesting depth, throws an IllegalArgumentException right 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 one WARNING that 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.

Quick Start 🚀

  1. Call LangLoader.getBundledLocales(plugin) to list all packaged locales.
  2. Load the desired locale via loadLocale(...) so missing keys merge automatically.
  3. On upgrades, run syncLocale(...) for each locale you ship and log the returned keys.

References 📚

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.

Locale Workflow 🧩

  • Discover resources → sync to disk → reload your in-memory caches.
  • Use the List<String> returned by syncLocale to print upgrade notes to admins.

Examples 🧪

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

Tips & Notes 💡

  • Accept config values with or without .yml, with _ or -, in any case (en_US = en_US.yml = en-us). LangLoader normalizes them automatically; anything that is no xx_YY code falls back to the base locale with a warning that names it.
  • After calling syncLocale, reload your in-memory caches (e.g., via MessageService.load(locale)) so changes take effect.
  • Pair with ConfigWatcher to re-run syncLocale when admins edit files externally.
  • Log the merged keys during upgrades so server owners know what changed.
  • Merged UTF-8 files are rewritten like YamlConfiguration#save does, 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 \uXXXX escapes, 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.
  • syncLocale also 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. loadLocale then 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#load calls ensureBundledLocales only on its first call per MessageService instance (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. Calling loadLocale directly still runs ensureBundledLocales every time.

Relationships to Other Helpers 🔗

  • MessageService consumes LangLoader for locale syncing before rendering MiniMessage.
  • Works with ConfigWatcher to reload locale files immediately after filesystem edits.
  • Uses ConfigKeyMigrator for the optional legacy-key renames.

Clone this wiki locally