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
โโโ achievement/
โ โโโ messages.{lang}.json # Achievement titles, descriptions and unlock 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_exceptionmessages.<action>โ e.g.messages.create_area_success<feature>.command.<verb>.<part>โ e.g.component.command.look.area_iditems.<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.propertieswith-SNAPSHOTstripped (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
forceUpdatePluginVersionin 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:
- Buffer each entry into
.translation-staging.jsonat the project root (gitignored). - 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.",
"placeholders": {
"0": "the area name",
"1": "the new width in blocks",
"2": "the new height in blocks",
"3": "the new depth in blocks"
},
"usageType": ["chat"],
"audience": ["creator"],
"permission": ["lasers.edit"],
"feature": ["area"]
}
]
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.
The fields after it are the code's metadata (next section). They are staged rather than left for later because they are facts about the call site you have in front of you โ which surface renders the text, which permission guards it, what each argument means. An hour later, recovering them means tracing the value through the class that finally draws it and grepping every path that reaches the code. The flush transcribes them into the metadata file; it does not re-investigate them.
โ ๏ธ Orphan check: if
.translation-staging.jsonexists at the start of a session, stop and decide whether to flush it (write staged entries to language files) or discard it (git cleanthe 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.
Translation metadata โ <domain>.meta.jsonโ
A translation code is not finished until its metadata entry is. Alongside the 25 language files, every domain ships one language-independent metadata file that documents each code once, for translators and for translation editors: where and when the text appears, what each {N} stands for, who can see it and under which permission, and on which surface it is rendered. It is never applied at runtime.
This is the developer side of the contract. What a non-developer translator does with it โ and everything they need before that: GitLab account, access token, installing the desktop translation editor, the merge-request cycle โ is the translators guide โ the same page in French and English.
The file sits next to the language files it documents, with the language segment replaced by meta:
| Language files | Metadata file |
|---|---|
lang/errors.{lang}.json | lang/errors.meta.json |
lang/area/messages.{lang}.json | lang/area/messages.meta.json |
Entries are listed in the same order as the codes of en.json, so the two files diff side by side.
What an entry must answerโ
An entry counts as complete when it carries comment, usageType, audience, permission and feature โ plus placeholders as soon as the value contains a {N}. Where each answer comes from:
commentโ one to three English sentences for someone who never opens the game: which command, menu or event shows the text, and any tone or space caveat. Never restate the value. This is the stagedcontext, verbatim.placeholdersโ what each index means, read from the arguments at the call site ({"0": "the area name"}). Translators reorder placeholders freely, so this is exactly what they cannot guess.usageTypeโ the surface that finally renders the value, using only the names declared intranslationlib.jsonat the repository root (chat,item_name,item_lore,menu_title,actionbar,hologram,map,logโฆ). The sink decides, not the method that resolved the code โ the Text sinks table in the plugin'sAGENTS.mdmaps every renderer of this plugin.audienceโplayer,creator,adminorconsole, from the guard and the call context, not from the tone of the text.permissionโ the nodes under which the text can appear.plugin.ymldeclares three (lasers.edit,lasers.admin, and the parentlasers.*that guards nothing itself); the Permission model table inAGENTS.mdsays which guard protects what, and is what an entry is written from โ never a remembered count.[]means the paths were read and none is guarded โ a claim, not a default.featureโ the functional area, kebab-case, from the package or the feature class (portal,race,area-resetโฆ), never from the surface.
Never invent one of these: a permission node or a usage type the sources do not back costs a translator a wrong translation. When a fact cannot be established, say so in reviewNeeded โ in words, with what you looked for โ and leave the field out.
The full format โ every field, its type, and how the length and colour constraints are resolved from translationlib.json โ is specified once in TranslationLib's README.md ยง Translation metadata. Read it there: it is deliberately not copied into this page nor into the skills, because a second copy drifts.
Keeping it in stepโ
TranslationLib checks coverage at every startup: a code of the default language with no entry logs has no metadata entry, and an entry whose code no longer exists logs Stale metadata entry. Treat both like a missing translation.
| You do this | Then |
|---|---|
| add a code | add its entry, at the same position the code has in en.json |
| reword or move a code | refresh the entry if the meaning, placeholders, audience, guard or surface changed; move it with the code |
| delete a code | delete its entry in the same change |
The update-translation-meta skill does all three, and the staging flush invokes it for every code it writes. For a whole domain โ or a domain with no metadata at all โ use the /translation-meta-fill command. To check without changing anything:
python .claude/scripts/translation-meta-audit.py .
It reports every code without an entry, every stale entry, every incomplete or mis-shaped one, and every permission node or usage type an entry claims that the sources do not back. Run it before committing a change that touched translations. Its first line is the version of the vendored kit it came from โ it must match the one in TranslationLib's translation-metadata-kit/README.md, or the copy in .claude/ is behind.
Its companion answers the other question โ which domains exist and how many languages each one actually ships (25 everywhere here, but that is read, never assumed), and, given codes, which language files carry them and whether their metadata entry exists:
python .claude/scripts/translation-domains.py . # every domain
python .claude/scripts/translation-domains.py . messages.my_new_code # one code
โ ๏ธ Metadata files never carry
forceUpdatePluginVersionโ that marker belongs to the language files, on the single entry whose published value changed.
Style conventions (Minecraft formatting codes)โ
The authoritative table lives in the plugin's AGENTS.md, section Value style conventions โ one row per role, each backed by a count over the 1 613 English values and by a real code you can open. It is the table the translation skills read (the style field of a staged entry names one of its rows), so a new convention is added there, in the same change that establishes it. The summary below is for orientation only.
| Role | Style |
|---|---|
| Error / refusal | opens ยงc |
| Success | opens ยงa |
| Neutral information, item description | opens ยง7 โ and every line after a \n re-opens ยง7 |
| Caution in chat | opens ยงe; an irreversible action's item description opens ยง6ยงlWarning! then ยง7 |
| Item / menu-icon name | opens ยงf; a paired on/off icon uses ยงa active / ยงc inactive |
| Interpolated value | ยง6{N}, then re-open the colour the sentence was in |
| Label : value line | ยง8 - ยงlยงf<label>ยงrยง8: ยง6<value> |
Colour codes are always written ยง, never & (no English value uses the & form). Reset with ยงr whenever a style shouldn't bleed into the next chunk. These conventions apply to new codes: roughly a quarter of the existing values open with no colour at all, and those are legacy โ converge on the table, don't copy them.
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:
- Create a
*.<lang>.jsoncounterpart for each existing*.en.jsonfile (currently ~30 files). - Set the
languageblock with the correctlanguageCode,languagename, and"isDefault": false. - Update
ConfigData.CONFIG_HEADERto list the new language. - Update the wiki pages:
Installation/Config.mdandInstallation/Translations.md.
Translation libraryโ
The plugin uses TranslationLib (fr.skytale:translation-lib) which auto-discovers translation files from the lang/ resource directory. No manual registration is needed when adding new files.
AI assistant skillsโ
Five Claude Code skills under .claude/skills/ automate the most repetitive translation operations and enforce the rules above:
| Skill | Purpose |
|---|---|
stage-translation | Buffer a new or modified code into .translation-staging.json mid-feature (preferred path for โฅ 3 codes per task), with its context and the metadata facts of its call site |
flush-translation-staging | At end-of-task, write all staged entries to every language file in one batched pass; auto-applies forceUpdatePluginVersion for modify entries, then hands the staged facts to update-translation-meta |
update-translation-meta | Write, refresh or delete a code's entry in <domain>.meta.json โ the metadata half of the work (see Translation metadata above) |
add-translation-code | Add a single brand-new code directly into all 25 language files (ad-hoc, 1โ2 codes) |
modify-translation-value | Change a published code value directly with forceUpdatePluginVersion (ad-hoc, 1โ2 codes) |
See AI Prompts and Skills for the full list.