# Locales Bookshelf-Locales is an embeddable localization library for Lodestone plugins. You initialize it once, ship whatever default translations you want, and read translations by key. Server owners can add or override languages by dropping files into a folder — no plugin change required. The module is platform-agnostic: it depends only on Adventure, Gson, and Bookshelf-API, so the same code runs on Paper, Velocity, or any JVM plugin. --- ## Maven / Gradle **Repository:** *Gradle (Kotlin DSL):* ```kotlin repositories { maven("https://jitpack.io") } ``` *Maven:* ```xml <repository> <id>jitpack.io</id> <url>https://jitpack.io</url> </repository> ``` **Dependency:** *Gradle (Kotlin DSL):* ```kotlin implementation("com.github.Lodestones:Bookshelf-Locales:1.0.0") ``` *Maven:* ```xml <dependency> <groupId>com.github.Lodestones</groupId> <artifactId>Bookshelf-Locales</artifactId> <version>1.0.0</version> </dependency> ``` > [!tip] Shading > Bookshelf-Locales is a library, not a server plugin. Shade it into your jar (`implementation` / `compile`), or add it as `provided` if something else on the server already supplies it. Bookshelf-API is a `provided` dependency of the module, so your plugin still needs Bookshelf available at runtime for `VariableContext` and `MiniMessageHelper`. --- ## Setup Build one `LocaleManager` when your plugin enables and hold onto it. ```java LocaleManager locales = LocaleManager.builder() .defaultLocale("en_us") .bundled(getClass().getClassLoader(), "locales", "en_us", "ja_jp") .remote("https://cdn.example.com/locales/manifest.json") .folder(getDataFolder().toPath().resolve("locales")) .exportBundledDefaults(true) .logger(getLogger()::warning) .build(); ``` ### Builder Methods | Method | Description | |---|---| | `defaultLocale(String)` | Locale used when none is given, and the fallback for untranslated keys. Defaults to `en_us`. | | `bundled(ClassLoader, String, String...)` | Locales shipped inside your jar, e.g. `locales/en_us.json`. Codes are declared up front because a jar cannot be listed reliably at runtime. | | `remote(String)` | Locales fetched from a manifest URL you host. See [[#Hosting Your Own Locales]]. | | `remote(String, Consumer<RemoteLocaleSource>)` | As above, exposing the source so you can set headers, a timeout, or an offline cache. | | `folder(Path)` | The server owner's folder of `*.json` locales. Also the export target below. | | `exportBundledDefaults(boolean)` | Writes bundled locales into the folder when the file is absent, so owners have something real to edit. Never overwrites an existing file. | | `logger(Consumer<String>)` | Where load warnings go. Silent by default. | | `source(LocaleSource)` | Adds any source directly, including your own implementation. | --- ## Source Precedence Sources are read **in the order they were added**, and a later source wins **per key**. With the setup above that reads: ``` server owner's folder > hosted manifest > bundled defaults ``` Per-key precedence is what lets an owner override a single line without copying your entire file — keys they did not write still resolve to your defaults. A lookup that finds nothing falls back in this order: 1. The requested locale 2. The default locale 3. The translation key itself Because of step 3, a plugin with no locale files at all still runs — it renders raw keys, which is also the fastest way for a translator to see what needs writing. --- ## Locale File Format Flat JSON, the same shape Minecraft's own lang files use: ```json { "locale.display_name": "English", "locale.sort_order": "0", "myplugin.welcome": "<green>Welcome, <player>!", "myplugin.motd": "<gray>Line one<br><gray>Line two" } ``` Nested objects are flattened with dots, so these are equivalent: ```json { "menu": { "title": "Shop" } } { "menu.title": "Shop" } ``` Arrays join with newlines, which reads well for item lore: ```json { "myplugin.lore": ["<gray>First line", "<gray>Second line"] } ``` ### Value Syntax | Syntax | Effect | |---|---| | `<player>`, `<count>` | `VariableContext` placeholders | | `{other.key}` | Embeds another translation. Self-reference is detected, not looped | | `<br>` | Line break | | `<uppercase>`, `<lowercase>`, `<capitalize>` | Casing, applied before MiniMessage renders | | MiniMessage tags | Colors, gradients, hovers — anything Adventure supports | ### Reserved Keys | Key | Description | |---|---| | `locale.display_name` | The locale's own name for itself, used by `getDisplayName(code)`. Falls back to the code. | | `locale.sort_order` | Integer controlling position in `getLocales()`. Lower comes first; unset sorts last. | --- ## Reading Translations `get(...)` returns a rendered `Component`; `_get(...)` returns the raw MiniMessage string. ```java String locale = locales.localeOrDefault(player.locale().toString()); player.sendMessage(locales.get("myplugin.welcome", locale, VariableContext.of("player", player.getName()))); String raw = locales._get("myplugin.welcome", locale); List<Component> lore = locales.getIntoList("myplugin.lore", locale); ``` | Method | Return Type | Description | |---|---|---| | `get(String key)` | `Component` | Rendered, default locale. | | `get(String key, String locale)` | `Component` | Rendered in a locale. | | `get(String key, String locale, VariableContext context)` | `Component` | Rendered with placeholders applied. | | `_get(...)` | `String` | Same three overloads, returning the raw MiniMessage string. | | `getIntoList(...)` | `List<Component>` | One component per line — values split on `\n` and `<br>`. | | `_getIntoList(...)` | `List<String>` | Same, as raw strings. | | `hasLocale(String locale)` | `boolean` | Whether that locale loaded at all. | | `hasKey(String key, String locale)` | `boolean` | Whether that locale defines the key itself, ignoring fallback. | | `getLocales()` | `List<String>` | Loaded codes, ordered for a language picker. | | `getDisplayName(String locale)` | `String` | The locale's own name for itself. | | `getDefaultLocale()` / `setDefaultLocale(String)` | `String` / `void` | Read or change the fallback locale at runtime. | | `localeOrDefault(String locale)` | `String` | The code if it loaded, otherwise the default. Lets you pass a player's client locale straight through. | Rendered components have italics explicitly disabled, since the same strings are used for chat and for item lore, and lore renders italic by default. --- ## Reloading ```java locales.reloadFromDisk(); // bundled + folder + in-memory only. Fast, no network locales.reloadFromCloud(); // remote manifest only. Blocks on HTTP locales.reload(); // everything ``` Each has an async twin returning `CompletableFuture<Void>`: `reloadFromDiskAsync()`, `reloadFromCloudAsync()`, `reloadAsync()`. > [!warning] Never fetch on the main thread > `reloadFromCloud()` and `reload()` block on HTTP. From a command handler, use `reloadFromCloudAsync()`. A partial reload keeps what the other sources last returned and re-merges in the configured order, so precedence never shifts. If a source suddenly returns nothing — an unreachable host with no cache, or an emptied folder — the last good copy is kept and a warning is logged, rather than the plugin dropping to raw keys. For a `/reloadlocales` style command, `reloadFromDisk()` is usually what an admin means: they just edited a file. --- ## Hosting Your Own Locales Hard-code one manifest URL and ship no locale files at all. New languages and fixed typos then reach servers on the next reload, with no plugin update. The manifest is a flat JSON object of language code to download URL: ```json { "en_us": "https://cdn.example.com/locales/en_us.json", "ja_jp": "https://cdn.example.com/locales/ja_jp.json" } ``` Relative entries (`"en_us.json"`) resolve against the manifest's own URL. Each linked document is an ordinary locale file. ```java .remote("https://cdn.example.com/locales/manifest.json", source -> source .header("Authorization", "Bearer " + token) .timeout(Duration.ofSeconds(5)) .cacheFolder(getDataFolder().toPath().resolve("locales-cache"))) ``` | Method | Description | |---|---| | `header(String, String)` | Sent with the manifest request and every locale download. | | `timeout(Duration)` | Per-request timeout. Defaults to 10 seconds. | | `cacheFolder(Path)` | Writes each successful download to disk and serves that copy when the host is unreachable. | With a cache folder set, a CDN outage costs you fresh text, not all text. Hosted locales are still *defaults* — register the source before `folder(...)` so a server owner's local edit outranks the hosted copy. --- ## Custom Sources Implement `LocaleSource` to load translations from anywhere — a database, Redis, a config section. ```java public interface LocaleSource { Map<String, Map<String, String>> load(Consumer<String> problems); String describe(); default boolean isRemote() { return false; } } ``` Return language code to translations. Never throw for a missing or malformed entry: a broken locale should cost that locale, not your plugin's startup — report it through `problems` instead. Return `true` from `isRemote()` if loading goes over the network, so the source is grouped with `reloadFromCloud()` rather than `reloadFromDisk()`. Built-in implementations: `FolderLocaleSource`, `ResourceLocaleSource`, `RemoteLocaleSource`, `MapLocaleSource`. --- ## Related Pages - [[Bookshelf/Server Owners/Features/Locales]] — Server owner guide to adding and editing languages - [[Bookshelf/Developers/API Reference]] — Full interface documentation - [[Bookshelf/Developers/Overview]] — Developer overview and dependency setup