LEPSU Translations — Code Structure
How user-facing strings are translated in LE Play Server Utils (LEPSU). The mechanics mirror the LasersEnigma translation system — same library, same staging workflow, same forceUpdatePluginVersion semantics — but retargeted to LEPSU's Maven layout and wired through a route-B messaging bridge. Read the LE page first for the shared concepts; this page documents only what differs for LEPSU.
Own TranslationLib instance + cross-plugin shared store
LEPSU owns its own TranslationLib instance — TranslationLib.getInstance(plugin), bound in LEPlayServerUtilsModule.configure(). It auto-loads src/main/resources/lang/ and publishes LEPSU's language capabilities to TranslationLib's cross-plugin shared store (plugins/TranslationLib/players.json). Because the store is shared, a single global language choice drives every TranslationLib plugin on the server at once: a player running /le lang (LasersEnigma) or /puzzle lang (LEPSU) sets one server-wide preferred language, and LEPSU's own text follows it. See Puzzle command — /puzzle lang for the in-game command.
Route-B messaging bridge
LEPSU does not call TranslationLib directly from feature code. It emits a translated message through MessageSender:
msg.sendTranslated(player, MessageType.ERROR, "puzzle.not_in_a_puzzle_room", params…);
MessageSender.sendTranslated resolves the player's current language, fetches the translation, deserialises the legacy §… value to an Adventure Component (LegacyComponentSerializer.legacySection()), and renders it through the standard [LPU] prefix + type-icon formatting — exactly like a normal MessageSender call. This is the route-B bridge: the §… legacy styling in every translation value is the contract and must be preserved verbatim across languages.
File location & domain layout (feature-based)
Translation files live under src/main/resources/lang/, domain-split to mirror LEPSU's feature packages (puzzle/, portal/, room/, lobby/, player/, …):
lang/
├── errors.{lang}.json # cross-cutting errors
├── global.{lang}.json # info/warning/error/debug prefixes
├── messages.{lang}.json # general messages
├── puzzle/
│ └── messages.{lang}.json # puzzle feature (← the pilot lives here)
├── portal/
│ └── messages.{lang}.json
├── room/
├── lobby/
└── player/
A domain is <subfolder>/<prefix> (e.g. puzzle/messages) or just <prefix> at the lang root (e.g. errors, global). TranslationLib discovers the files recursively, so sub-folders need no registration.
Naming convention
Codes are hierarchical snake_case, feature-first, with no lepsu. prefix — each plugin owns its own TranslationLib store, so the namespace is implicit (LEPSU codes never collide with LasersEnigma codes):
<feature>.<what>— e.g.puzzle.not_in_a_puzzle_room<feature>.command.<verb>.<part>— e.g.portal.command.create.successerrors.<feature>.<what>— cross-cutting errors in the flaterrorsdomainitems.<name>.{name,description}— e.g.items.teleporter.description
Place new entries next to thematically related codes, not at the bottom of the file.
Supported languages
LEPSU ships English (en, default) and French (fr) today. Full parity with LasersEnigma's 25 languages (ar bn cs da de el en es fi fr hi hu it ja ko nl no pl pt ro ru sv tr uk zh) lands in a dedicated issue (#777). Until then, a code must exist in every language file that currently exists for its domain (en + fr) or the plugin emits a "Translations are missing" warning at startup. The same warning fires for the opposite drift — a key in fr but missing from en.
Modifying a published value — forceUpdatePluginVersion
Identical to LE, except the version source is pom.xml. When you change the value of a code that already shipped, add forceUpdatePluginVersion to that entry (per-entry, every language) set to the pom.xml <version> with -SNAPSHOT stripped (e.g. 1.29.0-SNAPSHOT → "1.29.0"). Without it, admins who already installed the plugin keep their stored value and your edit has no effect on live servers. Brand-new codes don't need it. Never bump the file-wide defaultForceUpdatePluginVersion. See the LE page for the full rationale.
Mid-feature staging workflow
When introducing or modifying translations as part of a larger feature/fix, do not edit the language files inline. Buffer each entry into .translation-staging.json at the project root (gitignored) with a mandatory context field, then write all staged entries to the language files in one batched pass at end-of-task (one Edit per file). The context tells translators when the code fires, what each {N} represents, and what tone to use, so non-English translations don't drift into wrong literals.
[
{
"operation": "add",
"domain": "puzzle/messages",
"code": "puzzle.not_in_a_puzzle_room",
"en": "§cYou are not in a puzzle room",
"context": "Sent when a player runs a puzzle command while standing outside any puzzle room. Imperative error tone; no placeholders."
}
]
⚠️ Orphan check: if
.translation-staging.jsonexists at the start of a session, stop and decide whether to flush it or discard it — never silently merge a stale buffer with new work.⚠️ Never commit the staging file — a
.githooks/pre-commitguard blocks any commit while it survives (mirrors the LasersEnigma orphan guard). Flush it first.
Editing safety — Edit tool only
Never edit a language file with Write, sed, -replace, or any full rewrite. The files mix CRLF/LF line endings and with/without UTF-8 BOM; full rewrites silently corrupt Arabic / Bengali / Greek / Hindi / Japanese / Korean / Russian / Ukrainian / Chinese glyphs (once #777 adds them) into mojibake that still parses as JSON but renders ??? in-game. Use Edit only, one file at a time (or batched per file when flushing multiple staged entries).
AI assistant skills
Four repo-local Claude Code skills under LEPSU's .claude/skills/ automate these operations — the LasersEnigma counterparts, retargeted to src/main/resources/lang/ and the pom.xml version (they are not the workspace-synced skills):
| Skill | Purpose |
|---|---|
stage-translation | Buffer a new or modified code into .translation-staging.json mid-feature (preferred for ≥ 3 codes per task) |
flush-translation-staging | At end-of-task, write all staged entries to every language file of the domain in one batched pass; auto-applies forceUpdatePluginVersion for modify entries; self-checks completeness before deleting the buffer |
add-translation-code | Add a single brand-new code directly into every language file of its domain (ad-hoc, 1–2 codes) |
modify-translation-value | Change a published code value directly with forceUpdatePluginVersion from pom.xml (ad-hoc, 1–2 codes) |
A .githooks/pre-commit hook backs the workflow by rejecting any commit while an unflushed .translation-staging.json survives. See AI Prompts and Skills for the full tooling map.