<?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>DEV Community: Battle Of Pirates</title>
    <description>The latest articles on DEV Community by Battle Of Pirates (@battleofpirates).</description>
    <link>https://dev.to/battleofpirates</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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4209406%2Fc58742dc-fe79-4823-bf72-f56947d2c590.png</url>
      <title>DEV Community: Battle Of Pirates</title>
      <link>https://dev.to/battleofpirates</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/battleofpirates"/>
    <language>en</language>
    <item>
      <title>How I added English to my solo-dev Unity MMO without touching 130 UI files</title>
      <dc:creator>Battle Of Pirates</dc:creator>
      <pubDate>Mon, 26 Oct 2026 19:11:12 +0000</pubDate>
      <link>https://dev.to/battleofpirates/how-i-added-english-to-my-solo-dev-unity-mmo-without-touching-130-ui-files-4gdj</link>
      <guid>https://dev.to/battleofpirates/how-i-added-english-to-my-solo-dev-unity-mmo-without-touching-130-ui-files-4gdj</guid>
      <description>&lt;p&gt;My game, &lt;a href="https://battleofpirates.com" rel="noopener noreferrer"&gt;Battle of Pirates&lt;/a&gt;, is a free online pirate MMO for Windows: real-time naval PvP, fleets, fortress sieges, boss events. I build it alone in Turkey, so for two years every string in the game was hardcoded Turkish. Players here search for it as an &lt;em&gt;online korsan oyunu&lt;/em&gt;, and that was fine until the game started showing up on itch.io and IndieDB and international players arrived.&lt;/p&gt;

&lt;p&gt;The problem: about &lt;strong&gt;1,700 unique UI strings across 130 C# files&lt;/strong&gt;, all built in code (&lt;code&gt;text.text = "AYARLAR"&lt;/code&gt;), plus hundreds of server messages sent from Node.js. Wrapping every literal in a &lt;code&gt;T("...")&lt;/code&gt; call would touch every file and risk breaking a live game.&lt;/p&gt;

&lt;h2&gt;
  
  
  The trick: translate at the last moment, not at the source
&lt;/h2&gt;

&lt;p&gt;Instead of changing the code that sets text, I translate the text &lt;em&gt;after&lt;/em&gt; it is set and &lt;em&gt;before&lt;/em&gt; it is drawn. A single &lt;code&gt;MonoBehaviour&lt;/code&gt; runs late in &lt;code&gt;LateUpdate&lt;/code&gt; (&lt;code&gt;[DefaultExecutionOrder(32000)]&lt;/code&gt;), so it runs after all game code but before Unity renders the canvases:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Text&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;texts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;ReferenceEquals&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;seen&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetInstanceID&lt;/span&gt;&lt;span class="p"&gt;()],&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// unchanged, skip&lt;/span&gt;
    &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;BopLoc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Translate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="p"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Only Texts whose string reference changed are processed, so the per-frame cost is a reference comparison. When the language is Turkish, the component doesn't exist at all: zero risk for existing players.&lt;/p&gt;

&lt;h2&gt;
  
  
  One dictionary file, two kinds of entries
&lt;/h2&gt;

&lt;p&gt;Translations live in a plain tab-separated file in &lt;code&gt;Resources/&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;exact&lt;/strong&gt; entries: &lt;code&gt;AYARLAR → SETTINGS&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;fragment&lt;/strong&gt; entries for strings built by concatenation: &lt;code&gt;"Seviye " → "Level "&lt;/code&gt;, &lt;code&gt;" inci" → " pearls"&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Fragments are applied longest-first and only on word boundaries, so &lt;code&gt;" sa"&lt;/code&gt; (hours) never breaks a word like &lt;code&gt;saldırı&lt;/code&gt;. Rich-text tags such as &lt;code&gt;&amp;lt;color=...&amp;gt;&lt;/code&gt; are skipped, and each segment between tags is translated on its own.&lt;/p&gt;

&lt;h2&gt;
  
  
  Server messages for free
&lt;/h2&gt;

&lt;p&gt;Because translation happens on the client at display time, the server needed no changes. A message like &lt;code&gt;"Kaptan23 artık arkadaşın!"&lt;/code&gt; hits the fragment &lt;code&gt;" artık arkadaşın!" → " is now your friend!"&lt;/code&gt; and shows as &lt;em&gt;Kaptan23 is now your friend!&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Things I had to handle
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Input fields:&lt;/strong&gt; skip the &lt;code&gt;textComponent&lt;/code&gt; of every &lt;code&gt;InputField&lt;/code&gt;, or you'd translate what the player is typing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Switching back to Turkish live:&lt;/strong&gt; store the original string for every Text you changed, and restore it when the player switches back.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trailing spaces:&lt;/strong&gt; they kept getting stripped from the dictionary file, so the loader copies the key's leading and trailing spaces onto the value.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Result
&lt;/h2&gt;

&lt;p&gt;About 1,670 dictionary entries, one new C# file, and a language toggle on the login screen. The English update shipped as a normal patch through the game's own launcher.&lt;/p&gt;

&lt;p&gt;It's not a replacement for a real localization pipeline, but for a solo dev with a live game, it got a second language out in a day without a risky refactor.&lt;/p&gt;

&lt;p&gt;If you want to see it running, Battle of Pirates is free on &lt;a href="https://battleofpirates.com" rel="noopener noreferrer"&gt;battleofpirates.com&lt;/a&gt; and the &lt;a href="https://apps.microsoft.com/detail/9NL43LVCMVXX" rel="noopener noreferrer"&gt;Microsoft Store&lt;/a&gt;. Questions about the approach are welcome in the comments!&lt;/p&gt;

</description>
      <category>gamedev</category>
      <category>unity3d</category>
      <category>csharp</category>
      <category>localization</category>
    </item>
  </channel>
</rss>
