<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: m.b</title>
    <description>The latest articles on Forem by m.b (@m_b_94a69cb4f572c49e856c2).</description>
    <link>https://forem.com/m_b_94a69cb4f572c49e856c2</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3927477%2F7d0d8023-d4b4-45d9-b62c-bb77675b3d35.JPG</url>
      <title>Forem: m.b</title>
      <link>https://forem.com/m_b_94a69cb4f572c49e856c2</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/m_b_94a69cb4f572c49e856c2"/>
    <language>en</language>
    <item>
      <title>How I Designed a 4-Layer i18n Architecture for Minecraft's Standard UI in Spigot</title>
      <dc:creator>m.b</dc:creator>
      <pubDate>Tue, 12 May 2026 22:07:21 +0000</pubDate>
      <link>https://forem.com/m_b_94a69cb4f572c49e856c2/how-i-designed-a-4-layer-i18n-architecture-for-minecrafts-standard-ui-in-spigot-4311</link>
      <guid>https://forem.com/m_b_94a69cb4f572c49e856c2/how-i-designed-a-4-layer-i18n-architecture-for-minecrafts-standard-ui-in-spigot-4311</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;This article is an implementation note on how I organized the i18n architecture for TreasureRun, a Minecraft Spigot plugin project.&lt;/p&gt;

&lt;p&gt;When building multilingual messages in a Spigot plugin, custom plugin messages are relatively straightforward to manage with files such as &lt;code&gt;languages/*.yml&lt;/code&gt; or &lt;code&gt;messages.yml&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;However, Minecraft's built-in standard UI text and system messages cannot be fully controlled by a Spigot plugin alone.&lt;/p&gt;

&lt;p&gt;Examples include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Standard text resolved by the Minecraft client's language files&lt;/li&gt;
&lt;li&gt;Translatable components included in server-to-client packets&lt;/li&gt;
&lt;li&gt;Text that can be supplemented through ResourcePack lang JSON files&lt;/li&gt;
&lt;li&gt;Client-side language state that may need Fabric runtime sync&lt;/li&gt;
&lt;li&gt;Login screens, settings screens, and other areas that the server cannot reach&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Instead of trying to handle everything inside Spigot, TreasureRun separates the problem into four responsibilities:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;ProtocolLib boundary layer&lt;/li&gt;
&lt;li&gt;Server-side ResourcePack&lt;/li&gt;
&lt;li&gt;Fabric runtime language sync&lt;/li&gt;
&lt;li&gt;Pure Java packet localizer&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This article explains how I separated Minecraft-dependent boundaries from pure i18n logic and made the core packet localization logic testable without starting a Minecraft server.&lt;/p&gt;




&lt;h2&gt;
  
  
  Background: Spigot alone cannot fully control Minecraft standard UI
&lt;/h2&gt;

&lt;p&gt;Localizing custom messages in a Spigot plugin is not difficult by itself.&lt;/p&gt;

&lt;p&gt;For example, a plugin can manage its own messages with files such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;languages/ja.yml&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;languages/en.yml&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;languages/de.yml&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;languages/fr.yml&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;languages/ojp.yml&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For custom game messages, the plugin can simply look up the player's selected language and return the corresponding string.&lt;/p&gt;

&lt;p&gt;Minecraft's standard UI is different.&lt;/p&gt;

&lt;p&gt;Minecraft has built-in translation keys that are resolved by the client-side language files.&lt;/p&gt;

&lt;p&gt;Server-to-client JSON components can also include a &lt;code&gt;translate&lt;/code&gt; key, such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"translate"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"multiplayer.player.left"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"with"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"text"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"PlayerName"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This kind of text cannot be handled just by replacing calls to &lt;code&gt;sendMessage()&lt;/code&gt; on the Spigot side.&lt;/p&gt;




&lt;h2&gt;
  
  
  Understanding the problem
&lt;/h2&gt;

&lt;p&gt;Minecraft text belongs to different layers, and the way it can be controlled depends on where it is owned.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;Main owner&lt;/th&gt;
&lt;th&gt;Can Spigot alone control it?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Custom plugin messages&lt;/td&gt;
&lt;td&gt;Spigot plugin&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Some chat, title, and ActionBar packet messages&lt;/td&gt;
&lt;td&gt;Server-to-client packet&lt;/td&gt;
&lt;td&gt;Partially&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Minecraft standard translation keys&lt;/td&gt;
&lt;td&gt;Client lang JSON&lt;/td&gt;
&lt;td&gt;Difficult with Spigot alone&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lang keys that ResourcePack can override&lt;/td&gt;
&lt;td&gt;ResourcePack&lt;/td&gt;
&lt;td&gt;Partially&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Runtime client language state&lt;/td&gt;
&lt;td&gt;Fabric Mod&lt;/td&gt;
&lt;td&gt;Requires client-side support&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Login screen and settings UI&lt;/td&gt;
&lt;td&gt;Minecraft client&lt;/td&gt;
&lt;td&gt;Basically no&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;In other words, Minecraft standard UI i18n is not just a translation file problem.&lt;/p&gt;

&lt;p&gt;It is also a boundary design problem: I needed to identify which layer owns each piece of text.&lt;/p&gt;




&lt;h2&gt;
  
  
  Solution: separating the system into four layers
&lt;/h2&gt;

&lt;p&gt;TreasureRun separates Minecraft standard UI i18n into four layers.&lt;/p&gt;

&lt;p&gt;The following diagram shows the overall structure.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart TD
    A["Minecraft Client&amp;lt;br/&amp;gt;Player UI"] --&amp;gt; B["Server-side ResourcePack&amp;lt;br/&amp;gt;Minecraft standard lang JSON"]
    A --&amp;gt; C["Fabric runtime sync&amp;lt;br/&amp;gt;Client-side language state / built-in ResourcePack"]

    D["Spigot Plugin&amp;lt;br/&amp;gt;TreasureRun game logic"] --&amp;gt; E["ProtocolLib Boundary Layer&amp;lt;br/&amp;gt;PacketEvent / Player / WrappedChatComponent"]
    E --&amp;gt; F["Pure Java Packet Localizer&amp;lt;br/&amp;gt;PacketI18nJsonLocalizer"]
    F --&amp;gt; G["Translator Callback&amp;lt;br/&amp;gt;languages/*.yml"]
    G --&amp;gt; F
    F --&amp;gt; E
    E --&amp;gt; A

    H["lang-map.yml&amp;lt;br/&amp;gt;TreasureRun lang code ↔ Minecraft locale code"] --&amp;gt; B
    H --&amp;gt; C
    H --&amp;gt; D

    I["JUnit Tests&amp;lt;br/&amp;gt;Boundary / Delegation / Pure i18n"] --&amp;gt; F
    I --&amp;gt; E
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this design, Minecraft / Bukkit / ProtocolLib dependent code is kept inside the &lt;code&gt;ProtocolLib Boundary Layer&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The JSON component parsing, translation key mapping, placeholder extraction, and translator callback handling are separated into the &lt;code&gt;Pure Java Packet Localizer&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;ResourcePack and Fabric runtime sync complement the client-side display and language state. &lt;code&gt;lang-map.yml&lt;/code&gt; acts as a single source of truth for mapping TreasureRun language codes to Minecraft locale codes.&lt;/p&gt;

&lt;p&gt;This separation allows the core packet i18n logic to be tested with JUnit without starting a Minecraft server.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Responsibility&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;ProtocolLib boundary layer&lt;/td&gt;
&lt;td&gt;Observes server-to-client packets and writes modified JSON components back&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Server-side ResourcePack&lt;/td&gt;
&lt;td&gt;Supplements Minecraft standard translation keys with lang JSON files&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fabric runtime sync&lt;/td&gt;
&lt;td&gt;Complements client-side language state and built-in ResourcePack behavior&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pure Java packet localizer&lt;/td&gt;
&lt;td&gt;Parses packet JSON, maps translation keys, extracts placeholders, and remains testable&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The important point is separating Minecraft-dependent boundary code from pure i18n logic.&lt;/p&gt;




&lt;h2&gt;
  
  
  Layer 1: ProtocolLib boundary layer
&lt;/h2&gt;

&lt;p&gt;The ProtocolLib boundary layer handles packet events, player lookup, packet type checks, and reading/writing &lt;code&gt;WrappedChatComponent&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This layer depends heavily on Bukkit, ProtocolLib, and the Minecraft runtime.&lt;/p&gt;

&lt;p&gt;If JSON parsing and translation logic are written directly in this layer, the logic becomes difficult to test without running a Minecraft server.&lt;/p&gt;

&lt;p&gt;In TreasureRun, the ProtocolLib listener is treated as a boundary adapter. The actual JSON localization logic is delegated to the pure Java side.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Process&lt;/th&gt;
&lt;th&gt;Owned by&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Receive &lt;code&gt;PacketEvent&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;ProtocolLib boundary layer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Look up the player's language&lt;/td&gt;
&lt;td&gt;ProtocolLib boundary layer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Extract JSON from &lt;code&gt;WrappedChatComponent&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;ProtocolLib boundary layer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Find &lt;code&gt;translate&lt;/code&gt; keys in JSON&lt;/td&gt;
&lt;td&gt;Pure Java localizer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Extract placeholders&lt;/td&gt;
&lt;td&gt;Pure Java localizer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Convert the result back into a JSON text component&lt;/td&gt;
&lt;td&gt;Pure Java localizer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Write the result back to the packet&lt;/td&gt;
&lt;td&gt;ProtocolLib boundary layer&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Layer 2: Server-side ResourcePack
&lt;/h2&gt;

&lt;p&gt;The ResourcePack layer supplements Minecraft standard translation keys as lang JSON files.&lt;/p&gt;

&lt;p&gt;ResourcePacks can affect standard translation keys that are resolved on the Minecraft client side.&lt;/p&gt;

&lt;p&gt;In TreasureRun, I placed language JSON files in the ResourcePack based on Minecraft 1.20.1 standard translation keys.&lt;/p&gt;

&lt;p&gt;Example paths:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;resourcepacks/treasurerun-i18n-pack/assets/minecraft/lang/ja_jp.json
resourcepacks/treasurerun-i18n-pack/assets/minecraft/lang/en_us.json
resourcepacks/treasurerun-i18n-pack/assets/minecraft/lang/ojp_jp.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The ResourcePack is sent to the player after joining the server.&lt;/p&gt;

&lt;p&gt;However, ResourcePack is not a complete solution.&lt;/p&gt;

&lt;p&gt;For example, it cannot fully guarantee control over:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Login UI&lt;/li&gt;
&lt;li&gt;Authentication screens&lt;/li&gt;
&lt;li&gt;Client settings screens&lt;/li&gt;
&lt;li&gt;Fully client-local UI&lt;/li&gt;
&lt;li&gt;Text shown before the server sends packets or ResourcePacks&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So I do not describe this project as fully controlling the entire Minecraft UI.&lt;/p&gt;

&lt;p&gt;More accurately, it is a practical layered design for localizing the parts that can realistically be reached after a player joins the server.&lt;/p&gt;




&lt;h2&gt;
  
  
  Layer 3: Fabric runtime language sync
&lt;/h2&gt;

&lt;p&gt;ResourcePack alone cannot always handle runtime client language state.&lt;/p&gt;

&lt;p&gt;For this reason, the Fabric Mod side handles runtime language sync.&lt;/p&gt;

&lt;p&gt;In TreasureRun, the Fabric side is responsible for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Registering built-in ResourcePack support&lt;/li&gt;
&lt;li&gt;Reading &lt;code&gt;lang-map.yml&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Receiving the selected language code from the server&lt;/li&gt;
&lt;li&gt;Mapping TreasureRun language codes to Minecraft locale codes&lt;/li&gt;
&lt;li&gt;Complementing the client-side language state&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A key point is that the server does not send huge translation JSON files for all languages every time.&lt;/p&gt;

&lt;p&gt;Instead, the server basically sends the selected language code.&lt;/p&gt;

&lt;p&gt;The translation resources themselves live on the ResourcePack / Fabric Mod side.&lt;/p&gt;

&lt;p&gt;This keeps the communication payload small and makes &lt;code&gt;lang-map.yml&lt;/code&gt; the single source of truth for language mapping.&lt;/p&gt;




&lt;h2&gt;
  
  
  Layer 4: Pure Java packet localizer
&lt;/h2&gt;

&lt;p&gt;The most important part of this design is the pure Java packet localizer.&lt;/p&gt;

&lt;p&gt;TreasureRun separates packet JSON localization into a class named &lt;code&gt;PacketI18nJsonLocalizer&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This class does not depend on Bukkit, ProtocolLib, Fabric, or Minecraft runtime APIs.&lt;/p&gt;

&lt;p&gt;It only knows:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Raw JSON string&lt;/li&gt;
&lt;li&gt;Selected language code&lt;/li&gt;
&lt;li&gt;Translator callback&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Conceptually, its responsibility looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;PacketI18nJsonLocalizer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;localizeJson&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;rawJson&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;lang&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;translator&lt;/span&gt;
&lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The pure Java localizer finds &lt;code&gt;translate&lt;/code&gt; keys inside JSON components and maps Minecraft standard translation keys to TreasureRun i18n keys.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;multiplayer.player.left
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;can be handled as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;minecraft.packet.multiplayer.player.left
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then it extracts placeholders and passes them to the translator callback.&lt;/p&gt;

&lt;p&gt;Because this class is independent from the Minecraft runtime, it can be tested with JUnit.&lt;/p&gt;




&lt;h2&gt;
  
  
  Boundary tests
&lt;/h2&gt;

&lt;p&gt;This design is not just separated in theory. It is also protected by tests.&lt;/p&gt;

&lt;p&gt;Two representative tests are:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Pure i18n package boundary test
&lt;/h3&gt;

&lt;p&gt;This test verifies that &lt;code&gt;plugin.i18n&lt;/code&gt; does not import Bukkit, ProtocolLib, Fabric, or Minecraft runtime APIs.&lt;/p&gt;

&lt;p&gt;In other words, it checks that the pure i18n package is not contaminated by platform dependencies.&lt;/p&gt;

&lt;p&gt;This works as an architectural fitness function.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. ProtocolLib listener delegation test
&lt;/h3&gt;

&lt;p&gt;This test verifies that the ProtocolLib listener delegates JSON parsing to &lt;code&gt;PacketI18nJsonLocalizer&lt;/code&gt; instead of directly owning the parsing logic.&lt;/p&gt;

&lt;p&gt;This helps keep the packet boundary layer separate from the testable pure Java logic.&lt;/p&gt;




&lt;h2&gt;
  
  
  &lt;code&gt;lang-map.yml&lt;/code&gt; as the single source of truth
&lt;/h2&gt;

&lt;p&gt;TreasureRun centralizes the mapping between TreasureRun language codes and Minecraft locale codes in &lt;code&gt;lang-map.yml&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;mappings&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;ja&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;        &lt;span class="s"&gt;ja_jp&lt;/span&gt;
  &lt;span class="na"&gt;en&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;        &lt;span class="s"&gt;en_us&lt;/span&gt;
  &lt;span class="na"&gt;de&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;        &lt;span class="s"&gt;de_de&lt;/span&gt;
  &lt;span class="na"&gt;fr&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;        &lt;span class="s"&gt;fr_fr&lt;/span&gt;
  &lt;span class="na"&gt;es&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;        &lt;span class="s"&gt;es_es&lt;/span&gt;
  &lt;span class="na"&gt;ojp&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;       &lt;span class="s"&gt;ojp_jp&lt;/span&gt;
  &lt;span class="na"&gt;asl_gloss&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;asl_us&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With this design, adding a new language does not require adding another Java &lt;code&gt;switch&lt;/code&gt; statement.&lt;/p&gt;

&lt;p&gt;The intended flow for adding a new language is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Add one line to &lt;code&gt;lang-map.yml&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Add &lt;code&gt;languages/&amp;lt;lang&amp;gt;.yml&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Add &lt;code&gt;&amp;lt;minecraft_locale&amp;gt;.json&lt;/code&gt; to the ResourcePack&lt;/li&gt;
&lt;li&gt;Add &lt;code&gt;&amp;lt;minecraft_locale&amp;gt;.json&lt;/code&gt; to the Fabric side&lt;/li&gt;
&lt;li&gt;Let CI verify the consistency&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This keeps language expansion configuration-driven instead of hard-coding every language in Java.&lt;/p&gt;




&lt;h2&gt;
  
  
  CI checks
&lt;/h2&gt;

&lt;p&gt;As the number of i18n files grows, localization systems become easier to break.&lt;/p&gt;

&lt;p&gt;TreasureRun uses GitHub Actions to run checks such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Java compile&lt;/li&gt;
&lt;li&gt;YAML syntax check&lt;/li&gt;
&lt;li&gt;Required key check&lt;/li&gt;
&lt;li&gt;Referenced key check&lt;/li&gt;
&lt;li&gt;Duplicate key check&lt;/li&gt;
&lt;li&gt;Suspect key check&lt;/li&gt;
&lt;li&gt;ResourcePack SHA1 check&lt;/li&gt;
&lt;li&gt;i18n expansion check&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These checks are run with a combination of Gradle tests and custom verification scripts.&lt;/p&gt;

&lt;p&gt;For example, the checks verify whether referenced translation keys are actually defined, whether duplicate or suspicious keys remain, and whether the ResourcePack SHA1 matches the generated artifact.&lt;/p&gt;

&lt;p&gt;For multilingual systems, adding translations is only one part of the problem.&lt;/p&gt;

&lt;p&gt;It is just as important to detect broken states early.&lt;/p&gt;




&lt;h2&gt;
  
  
  Runtime verification
&lt;/h2&gt;

&lt;p&gt;At runtime, I checked the system through logs such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;ResourcePack sent&lt;/li&gt;
&lt;li&gt;ResourcePack accepted&lt;/li&gt;
&lt;li&gt;ResourcePack loaded&lt;/li&gt;
&lt;li&gt;PacketI18n translate audit, monitored through &lt;code&gt;PacketI18nJsonLocalizer&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;PacketI18n replace, handled through &lt;code&gt;PacketI18nJsonLocalizer&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Translation missing: 0&lt;/li&gt;
&lt;li&gt;I18n Missing key warning: 0&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The important point is that I did not just check whether it “seemed to work.”&lt;/p&gt;

&lt;p&gt;I wanted to see which layer worked and how far the system actually reached.&lt;/p&gt;




&lt;h2&gt;
  
  
  What this design can do
&lt;/h2&gt;

&lt;p&gt;This design makes it possible to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Manage custom plugin messages with &lt;code&gt;languages/*.yml&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Supplement Minecraft standard translation keys through ResourcePack lang JSON&lt;/li&gt;
&lt;li&gt;Observe translatable components in server-to-client packets&lt;/li&gt;
&lt;li&gt;Replace some packet JSON based on the player's selected language&lt;/li&gt;
&lt;li&gt;Complement runtime language sync on the Fabric side&lt;/li&gt;
&lt;li&gt;Test the pure Java localizer without starting a Minecraft server&lt;/li&gt;
&lt;li&gt;Detect broken i18n configuration through CI&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  What this design cannot do
&lt;/h2&gt;

&lt;p&gt;There are still things this design cannot fully control.&lt;/p&gt;

&lt;p&gt;A Spigot plugin alone cannot guarantee control over:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Login UI&lt;/li&gt;
&lt;li&gt;Authentication screens&lt;/li&gt;
&lt;li&gt;Client settings screens&lt;/li&gt;
&lt;li&gt;Fully client-local UI&lt;/li&gt;
&lt;li&gt;Text displayed before packets or ResourcePacks are sent&lt;/li&gt;
&lt;li&gt;Text fully owned by the Minecraft client internally&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So the scope of this project is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This is not a system that fully translates the entire Minecraft UI. It is a layered design that separates Spigot-limited Minecraft standard UI boundaries into ProtocolLib, ResourcePack, Fabric, and a pure Java localizer, focusing on the parts that can realistically be reached after joining the server.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  What I learned
&lt;/h2&gt;

&lt;p&gt;The biggest lesson from this implementation was that i18n is not only translation work.&lt;/p&gt;

&lt;p&gt;In a platform like Minecraft, I had to ask questions such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Is this text owned by the server?&lt;/li&gt;
&lt;li&gt;Is it owned by the client?&lt;/li&gt;
&lt;li&gt;Can it be observed in packets?&lt;/li&gt;
&lt;li&gt;Can it be supplemented with a ResourcePack?&lt;/li&gt;
&lt;li&gt;Does it need help from a Fabric Mod?&lt;/li&gt;
&lt;li&gt;Is it effectively outside the server side's control?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In that sense, i18n was also a boundary design problem.&lt;/p&gt;

&lt;p&gt;In TreasureRun, I separated Minecraft-dependent boundaries from pure logic so that the core i18n behavior could be tested as a normal Java component.&lt;/p&gt;




&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Spigot alone cannot fully control Minecraft standard UI text.&lt;/p&gt;

&lt;p&gt;TreasureRun separates the problem into four layers:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;ProtocolLib boundary layer&lt;/li&gt;
&lt;li&gt;Server-side ResourcePack&lt;/li&gt;
&lt;li&gt;Fabric runtime language sync&lt;/li&gt;
&lt;li&gt;Pure Java packet localizer&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;With this separation, Minecraft-dependent boundary code stays inside adapter-like layers, while pure packet JSON localization logic can be tested with JUnit.&lt;/p&gt;

&lt;p&gt;This design is based on the constraints of Minecraft as a platform.&lt;/p&gt;

&lt;p&gt;The key was deciding which parts should be handled on the server side, which parts should be handled by ResourcePack or Fabric, and which parts are simply outside the practical reach of a Spigot plugin.&lt;/p&gt;

&lt;p&gt;i18n was not just about adding more translation files.&lt;/p&gt;

&lt;p&gt;It was about identifying which layer owns each piece of text and designing around the boundaries that can actually be controlled.&lt;/p&gt;

</description>
      <category>java</category>
      <category>minecraft</category>
      <category>i18n</category>
      <category>architecture</category>
    </item>
  </channel>
</rss>
