Skip to main content

Translations โ€” Code Structure

File locationโ€‹

All translation files are located in core/src/main/resources/lang/ and organized by domain:

lang/
โ”œโ”€โ”€ errors.{lang}.json # Error messages
โ”œโ”€โ”€ global.{lang}.json # Global prefixes (info, warning, error, debug)
โ”œโ”€โ”€ messages.{lang}.json # General gameplay messages
โ”œโ”€โ”€ area/
โ”‚ โ””โ”€โ”€ messages.{lang}.json # Area CRUD messages
โ”œโ”€โ”€ commands/
โ”‚ โ”œโ”€โ”€ area.{lang}.json # /le area command descriptions
โ”‚ โ”œโ”€โ”€ clipboard.{lang}.json # /le clipboard command descriptions
โ”‚ โ”œโ”€โ”€ component.{lang}.json # /le component command descriptions
โ”‚ โ”œโ”€โ”€ componentinfo.{lang}.json # /le info command & component inspector texts
โ”‚ โ”œโ”€โ”€ editor.{lang}.json # /le editor command description
โ”‚ โ”œโ”€โ”€ help.{lang}.json # /le help command description
โ”‚ โ”œโ”€โ”€ lang.{lang}.json # /le lang command descriptions
โ”‚ โ”œโ”€โ”€ schematics.{lang}.json # /le schematic command descriptions
โ”‚ โ””โ”€โ”€ stats.{lang}.json # /le stats command descriptions
โ”œโ”€โ”€ race/ # Race builder
โ”œโ”€โ”€ shortcutbar/ # Editor hotbar & GUI menu translations
โ”‚ โ”œโ”€โ”€ shortcutbar.{lang}.json
โ”‚ โ”œโ”€โ”€ elevator.{lang}.json
โ”‚ โ”œโ”€โ”€ lock-key_chest.{lang}.json
โ”‚ โ”œโ”€โ”€ music_block.{lang}.json
โ”‚ โ”œโ”€โ”€ range.{lang}.json
โ”‚ โ”œโ”€โ”€ rotation.{lang}.json
โ”‚ โ”œโ”€โ”€ area_configuration/
โ”‚ โ”œโ”€โ”€ color_selection/
โ”‚ โ”œโ”€โ”€ component_configuration/
โ”‚ โ”œโ”€โ”€ delete_area/
โ”‚ โ”œโ”€โ”€ place_component/
โ”‚ โ”œโ”€โ”€ sync/
โ”‚ โ””โ”€โ”€ victory_area/
โ””โ”€โ”€ stats/
โ”œโ”€โ”€ gui.{lang}.json # Statistics GUI menu
โ””โ”€โ”€ messages.{lang}.json # Statistics chat messages

Naming conventionโ€‹

Each file follows the pattern <domain>.<languageCode>.json (e.g., errors.en.json, errors.fr.json).

Translation codes inside a file follow a hierarchical snake_case pattern:

  • errors.<domain>.<what> โ€” e.g. errors.area.too_small_exception
  • messages.<action> โ€” e.g. messages.create_area_success
  • <feature>.command.<verb>.<part> โ€” e.g. component.command.look.area_id
  • items.<name>.{name,description} โ€” e.g. items.portal_surface.description

Place new entries next to thematically related codes, not at the bottom of the file.

Supported languages (25)โ€‹

ar (Arabic), bn (Bengali), cs (Czech), da (Danish), de (German), el (Greek), en (English, default), es (Spanish), fi (Finnish), fr (French), hi (Hindi), hu (Hungarian), it (Italian), ja (Japanese), ko (Korean), nl (Dutch), no (Norwegian), pl (Polish), pt (Portuguese), ro (Romanian), ru (Russian), sv (Swedish), tr (Turkish), uk (Ukrainian), zh (Chinese).

Translation completeness โ€” required everywhereโ€‹

Every translation code MUST exist in every <domain>.<languageCode>.json file of its domain. The plugin emits a "Translations are missing" warning at startup for any code missing from a language file. The same warning fires for the opposite case: a key still present in non-en files but removed from en.json. When you remove a key, remove it from every language file too.

Translate for real in each language. Never leave the English string as a placeholder in non-English files. en.json is itself a real translation that ships to English players. If you're unsure about a language, translate as best you can and flag those languages so a reviewer can spot-check.

Modifying a published translation value โ€” forceUpdatePluginVersionโ€‹

When you change the value of a translation code that already shipped in a prior release, you must add forceUpdatePluginVersion to that entry โ€” otherwise admins who already installed the plugin keep their previously-loaded value and your edit has no effect on live servers.

TranslationLib writes its JSON into plugins/<plugin>/lang/ on first load and then treats the admin's stored copy as authoritative. forceUpdatePluginVersion tells the library: for any server upgrading from a plugin version strictly lower than X, overwrite the admin's stored value.

{
"code": "area.command.victory",
"translation": "ยงaยงlVictory!",
"forceUpdatePluginVersion": "8.26.1"
}

Rules:

  • Value to write: current gradle.properties with -SNAPSHOT stripped (e.g. 8.26.1-SNAPSHOT โ†’ "8.26.1"). Never include -SNAPSHOT โ€” the release tooling strips it, and the value must match the final published version.
  • Apply in every language file you edit (en + all non-en).
  • Bundle the value change and forceUpdatePluginVersion in the same Edit so you cannot ship one without the other.
  • Not needed for brand-new codes โ€” those are written on first load.
  • If the entry already has a forceUpdatePluginVersion, overwrite it with the current version. If the existing one is higher than the current version, stop and ask โ€” someone else is mid-flight on the same key.

defaultForceUpdatePluginVersionโ€‹

Files often start with a defaultForceUpdatePluginVersion at the top. Do not bump that โ€” it force-overwrites every unmarked key, trampling unrelated admin customisations. Always mark the single entry you changed via the per-entry forceUpdatePluginVersion.

Mid-feature staging workflowโ€‹

When introducing or modifying translations as part of a larger feature/fix, do not edit the 25 language files inline. Instead:

  1. Buffer each entry into .translation-staging.json at the project root (gitignored).
  2. At end-of-task โ€” after the Java implementation is stable, before build/commit โ€” write all staged entries to the language files in one batched pass (one Edit per file containing every staged entry).

Staging entry shape:

[
{
"operation": "add",
"domain": "area/messages",
"code": "messages.area_resize_success",
"en": "ยง7Area ยง6{0}ยง7 resized to ยง6{1}x{2}x{3}ยง7.",
"context": "Sent once after a successful resize via the area edit menu. {0}=area name (admin string, may contain spaces); {1}/{2}/{3}=new X/Y/Z dimensions in blocks. Not sent on cancel or invalid resize."
}
]

The context field is mandatory โ€” it tells translators when the code fires, what each {N} semantically represents and what tone to use, so non-English translations don't drift into wrong literals. The plugin will warn "Translations are missing" between staging and flush; that is expected and disappears once the flush runs.

โš ๏ธ Orphan check: if .translation-staging.json exists at the start of a session, stop and decide whether to flush it (write staged entries to language files) or discard it (git clean the file). A stale staging file from a previous session must never be silently merged with new work.

โš ๏ธ Never commit the staging file โ€” it must be flushed first.

For one-off ad-hoc changes (1โ€“2 codes, no other translation work in flight), it's still fine to edit the language files directly without staging.

Style conventions (Minecraft formatting codes)โ€‹

Use the role-based prefix for every new code; reset with ยงr whenever a style shouldn't bleed into the next text chunk.

RoleStyleExample
Errorยงc prefix"ยงcThe area is too small."
Warningยง6 prefix"ยง6Warning: โ€ฆ"
Debugยงe prefix"ยงeDebug: โ€ฆ"
Item nameยงf (often ยงl bold)"ยงfPlace cube creation button"
Item descriptionยง7, \nยง7 per line"ยง7Creates a button.\nยง7Right-click to use."
Interpolated paramยง6{N}"ยง7Area ID: ยง6{0}"
Label : valueยงlยงf<label>ยงrยง8: ยง6<value>"ยง8 - ยงlยงfArea IDยงrยง8: ยง6{0}"

These conventions apply to new codes. Older entries that are uncolored or inconsistent don't need to be retrofitted, but don't follow them as a model either โ€” converge on the table above.

MessageFormat placeholders and escapingโ€‹

The plugin uses Java's MessageFormat for parameter substitution:

  • {0}, {1}, โ€ฆ are positional placeholders. The Java caller passes parameters in order: TranslationUtils.sendTranslatedMessage(player, "code", arg0, arg1).
  • Every language file must keep the same indices in the same semantic roles. Word order may differ (e.g. Japanese), but every {N} must be present in every translation. Same count, same meaning.
  • '{0}' renders as the literal {0} (single quotes escape the placeholder).
  • A single quote is written '' โ€” e.g. French apostrophes are doubled: l''aire. Un-doubling them breaks the format string at runtime.

Editing safety โ€” Edit tool onlyโ€‹

Never edit translation files with Write, sed, -replace, or any full rewrite. The 25 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 into mojibake that still parses as JSON but renders as ??? in-game.

Use Edit only, one file at a time (or batched per file when staging-flushing multiple entries).

Adding a new languageโ€‹

To add a built-in language:

  1. Create a *.<lang>.json counterpart for each existing *.en.json file (currently ~30 files).
  2. Set the language block with the correct languageCode, language name, and "isDefault": false.
  3. Update ConfigData.CONFIG_HEADER to list the new language.
  4. Update the wiki pages: Installation/Config.md and Installation/Translations.md.

Translation libraryโ€‹

The plugin uses TranslationLib (fr.skytale.translationlib) which auto-discovers translation files from the lang/ resource directory. No manual registration is needed when adding new files.

AI assistant skillsโ€‹

Four Claude Code skills under .claude/skills/ automate the most repetitive translation operations and enforce the rules above:

SkillPurpose
stage-translationBuffer a new or modified code into .translation-staging.json mid-feature (preferred path for โ‰ฅ 3 codes per task)
flush-translation-stagingAt end-of-task, write all staged entries to every language file in one batched pass; auto-applies forceUpdatePluginVersion for modify entries
add-translation-codeAdd a single brand-new code directly into all 25 language files (ad-hoc, 1โ€“2 codes)
modify-translation-valueChange a published code value directly with forceUpdatePluginVersion (ad-hoc, 1โ€“2 codes)

See AI Prompts and Skills for the full list.