> For the complete documentation index, see [llms.txt](https://wyne.gitbook.io/wyne-docs/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://wyne.gitbook.io/wyne-docs/text/i18n.md).

# Internationalization

Load per-language string files, resolve dotted keys, and render the result as Adventure Components through a pluggable markup interpreter.

`wutils-i18n` loads per-language string files, resolves a dotted key ("path") to a string or list of strings, and turns the result into an Adventure `Component` through a pluggable markup interpreter — MiniMessage, legacy `&`-codes, or EnhancedLegacyText. It also handles per-player language selection and optional PlaceholderAPI expansion.

Reach for it as soon as you have more than a handful of hardcoded messages, or you want non-English server support. Skip it if you only ever send one or two fixed strings — a plain `Component.text(...)` is less machinery.

This page covers setup: adding the dependency, laying out language files, picking a format and an interpreter, and wiring up `I18n`. For actually looking up and sending messages once `I18n` is built, see [Sending Messages](/wyne-docs/text/messages.md).

## Adding it to your build

```kotlin
dependencies {
    implementation("io.github.wyne10:wutils-i18n:5.6.1")
}
```

Shade it into your plugin jar like any other library dependency.

## Third-party dependencies you must supply

Every row below is `compileOnly` — `wutils-i18n` compiles against it but does not bundle it. Touch a feature without its dependency present at runtime and you get a `NoClassDefFoundError`, not a build failure. Only pull in what you actually use:

| Dependency                             | Needed for                                                                   | Degrades gracefully? |
| -------------------------------------- | ---------------------------------------------------------------------------- | -------------------- |
| Paper API 1.16.5                       | everything — `Player`, `OfflinePlayer`, `Plugin`, `YamlConfiguration`, ...   | no                   |
| `net.kyori:adventure-text-minimessage` | `MiniMessageInterpreter` / `ItemMiniMessageInterpreter`                      | no                   |
| `dev.vankka:enhancedlegacytext`        | `EnhancedLegacyInterpreter` / `ItemEnhancedLegacyInterpreter`                | no                   |
| `net.kyori:adventure-platform-bukkit`  | `BukkitComponentAudiences` (only if you opt out of the Paper-native default) | no                   |
| `me.clip:placeholderapi`               | the `getPlaceholder*` lookup methods                                         | **yes** — see below  |
| `log4j-core`                           | quieting a bundled library's logger during setup                             | no                   |

`PlaceholderAPIWrapper` is the one exception: it probes for PlaceholderAPI with `Class.forName` at class-load time and silently passes strings through unchanged if it's absent, instead of throwing. Everything else in the table above fails loudly the first time you call into a class that needs it.

If you only use `LegacyInterpreter` (the default) and the default `PaperComponentAudiences`, you need nothing beyond Paper itself.

## Laying out language files

Language files live wherever you point the builder — typically `<dataFolder>/lang/`. Each file is one language; its **filename minus extension** becomes the language code, so `en.yml` registers as `en` and `de.yml` as `de`.

Three formats are supported, auto-detected by extension:

| Extension                 | Format           | Notes                                         |
| ------------------------- | ---------------- | --------------------------------------------- |
| `.yml` (or anything else) | YAML             | The default if the extension isn't recognized |
| `.json`                   | JSON             | A flat or nested object tree                  |
| `.lang`                   | flat `key=value` | One entry per line, no nesting                |

Pick one per file; you don't have to use the same format for every language. A YAML file with nested keys looks like this:

```yaml
greeting: '<gray>Welcome, <gold><player></gold>! You have <count> new messages.'
messages:
  farewell: '<gray>Goodbye, <player>.'
  motd:
    - '<green>Server rules'
    - '<green>Be nice to <player>'
```

Nested keys resolve as dotted paths (`messages.farewell`, `messages.motd`) exactly like top-level ones — see [Sending Messages](/wyne-docs/text/messages.md) for how a path turns into text.

Bundle a default language inside your plugin jar (e.g. at `lang/en.yml` in your resources) and let WUtils copy it out on first run — see the builder walkthrough below.

## Building your `I18n` instance

`I18n` is the object you'll hold onto and query. You build it once, at startup, with `PluginI18nBuilder` (the normal entry point inside a plugin) or the plainer `BaseI18nBuilder` (useful outside a plugin context, e.g. in a test):

```java
I18n.global = new PluginI18nBuilder(plugin)
        .setLogger(plugin.getLogger())
        .setComponentInterpreter(new MiniMessageInterpreter(new EmptyValidator()))
        .setUsePlayerLanguage(true)
        .loadLanguage("lang/en.yml")
        .build();
```

Line by line:

* **`loadLanguage("lang/en.yml")`** reads a language bundled in your plugin jar. It copies the resource to `<dataFolder>/lang/en.yml` if it isn't there yet, keeps a pristine copy under `<dataFolder>/defaults/`, and registers it under the code `en`. A missing resource throws `NullPointerException` with an explicit message — check your resource path if you hit this at startup.
* **`build()`** then scans `<dataFolder>/lang` for every other language file an admin dropped in, and resolves the default language from your plugin config's `lang` key (falling back to the bundled `config.yml`'s value). There's no `setDefaultLanguageCode` call here — `PluginI18nBuilder` derives it from config. Use `setDefaultLanguageCode` only with the plain `BaseI18nBuilder`.

Outside a plugin, `BaseI18nBuilder` spells out the same two steps explicitly:

```java
I18n i18n = new BaseI18nBuilder<>()
        .setComponentInterpreter(new MiniMessageInterpreter(new EmptyValidator()))
        .loadLanguages(new File(plugin.getDataFolder(), "lang"))
        .setDefaultLanguageCode("en")
        .build();
I18n.global = i18n;
```

## The `I18n.global` field is not initialized for you

`I18n.global` is a public static field that starts out `null`. Nothing sets it until you do — this is the first thing most people hit. Several static helpers dereference it unconditionally and throw `NullPointerException` if you forgot: `Placeholder.replace(String, Component)`, `TextReplacement#asComponentReplacement()`, `ComponentReplacement#asTextReplacement()`, and a couple of `I18n` styling helpers.

Assign `I18n.global` as the very last step of building your `I18n`, in your plugin's `onEnable`, before anything else in your plugin touches i18n. `Config.global` and `JsonRegistry.global` in the sibling `config`/`json` modules are pre-constructed and always safe to use — `I18n.global` deliberately is not, because building it requires config that isn't available until your plugin is set up.

## Choosing an interpreter

The component interpreter decides which markup language your language files use, and how raw strings turn into styled `Component`s. Pick one and use it consistently across every language file:

| Interpreter                   | Markup                                                                                                            | Needs                        |
| ----------------------------- | ----------------------------------------------------------------------------------------------------------------- | ---------------------------- |
| `LegacyInterpreter` (default) | `&`-codes, hex colors                                                                                             | nothing extra                |
| `MiniMessageInterpreter`      | [MiniMessage](https://docs.advntr.dev/minimessage/index.html) tags (`<gold>`, `<bold>`, ...)                      | `adventure-text-minimessage` |
| `EnhancedLegacyInterpreter`   | [EnhancedLegacyText](https://github.com/vankka/EnhancedLegacyText) syntax on parse, legacy `&`-codes on serialize | `enhancedlegacytext`         |

Each has an `Item*` twin — `ItemLegacyInterpreter`, `ItemMiniMessageInterpreter`, `ItemEnhancedLegacyInterpreter`.

### Why the `Item*` variants exist

Vanilla Minecraft renders item lore lines italic by default. The plain interpreters don't account for that; the `Item*` variants wrap their output so italics are explicitly turned off first, letting a language file opt back into italics on purpose (an inner `<italic>` tag or `&o` still works) instead of getting it for free from the client. Use an `Item*` interpreter for anything destined for item lore or display names; use the plain variant for chat, action bars, titles, and everything else.

`ComponentInterpreters` is a small enum factory if you'd rather pick by name (e.g. read from your plugin config) than construct directly:

```java
ComponentInterpreter interpreter = ComponentInterpreters
        .valueOf(plugin.getConfig().getString("interpreter", "MINI_MESSAGE"))
        .get(new EmptyValidator());
```

An unrecognized name throws `IllegalArgumentException` at startup — a typo in config fails loudly rather than silently falling back to something else.

### What happens when a lookup path is missing

The `StringValidator` you pass to an interpreter decides the fallback:

| Validator                              | On a missing path                                                            |
| -------------------------------------- | ---------------------------------------------------------------------------- |
| `EmptyValidator` (recommended default) | returns the path itself — a gap is obvious in-game without crashing anything |
| `ReplaceValidator`                     | returns a fixed string you configure                                         |
| `ExceptionValidator`                   | throws `IllegalArgumentException` immediately                                |

## Per-player languages

`setUsePlayerLanguage(true)` (the default) makes every lookup resolve against whichever language matches the target `Player`'s client locale, falling back to your default language if there's no match. Set it `false` to always use the default language for everyone, regardless of who's asking. Only an online `Player` carries a locale WUtils can read — an `OfflinePlayer` or a console `CommandSender` always resolves to the default language.

## Sharp edges

* **`I18n.global` is `null` until you assign it.** See above — this is the error most people hit first, and the fix is just making sure you set it before anything else runs.
* **A missing bundled language resource is a hard crash.** `loadLanguage(Plugin, String)` throws `NullPointerException` if your plugin jar doesn't actually contain the resource path you gave it. Double-check the path is relative to your resources root and the file is actually packaged.
* **Two files with the same code collide silently.** `en.yml` and `en.json` both register under the code `en`; whichever loads first wins and the other is dropped without a warning. Don't ship two formats for the same language.
* **Language files can be rewritten on disk when you load them with a default.** Loading a language alongside a bundled default (as `loadLanguage(Plugin, String)` does) merges missing keys from the default into the on-disk file and writes the result back — so updating your plugin and shipping new keys will patch existing admins' language files automatically. This is intentional, but don't be surprised if a "read-only" load actually touches the file.

## See also

* [Sending Messages](/wyne-docs/text/messages.md) — looking up strings/components, applying replacements, and sending them to players.
* [the contributor wiki](https://github.com/Wyne10/WUtils/tree/master/contributing/i18n/i18n/README.md) — full lookup-resolution internals, accessor caching, and the three language-file implementations.
* [the contributor wiki's Interpreters page](https://github.com/Wyne10/WUtils/tree/master/contributing/i18n/interpreters/README.md) — every interpreter method, and the full interpreter class hierarchy.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://wyne.gitbook.io/wyne-docs/text/i18n.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
