<?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: Gift Club</title>
    <description>The latest articles on DEV Community by Gift Club (@giftclub).</description>
    <link>https://dev.to/giftclub</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%2F4228158%2F7c2d2d91-129f-4df5-9cbc-acee6bb5c554.png</url>
      <title>DEV Community: Gift Club</title>
      <link>https://dev.to/giftclub</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/giftclub"/>
    <language>en</language>
    <item>
      <title>We translated our game into 9 languages. These 7 things didn't translate.</title>
      <dc:creator>Gift Club</dc:creator>
      <pubDate>Thu, 05 Nov 2026 10:37:13 +0000</pubDate>
      <link>https://dev.to/giftclub/we-translated-our-game-into-9-languages-these-7-things-didnt-translate-1k5b</link>
      <guid>https://dev.to/giftclub/we-translated-our-game-into-9-languages-these-7-things-didnt-translate-1k5b</guid>
      <description>&lt;p&gt;Our small team's browser game now runs in English, Russian, Ukrainian, Spanish, Polish, Italian, Turkish, German and French. That's more than 8,500 strings per language.&lt;/p&gt;

&lt;p&gt;I expected the volume to be the hard part. It wasn't. The hard part was finding out how many assumptions about language we'd built into our code.&lt;/p&gt;

&lt;p&gt;Here are seven of them.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. A placeholder can't take a suffix
&lt;/h2&gt;

&lt;p&gt;In English, this is fine:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Someone got {{name}}'s gift first
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Turkish has no separate word for "of". It adds a suffix to the name, and the suffix depends on the name's vowels:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Ahmet'in   Murat'ın   Ali'nin   Ömür'ün
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your string doesn't know the name, so it can't know the suffix. We stopped trying to pick one and reworded the sentence so the variable stands alone:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Hediyeyi biri önce aldı: {{name}}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's roughly "Someone got the gift first: {{name}}".&lt;/p&gt;

&lt;p&gt;If you need a suffix, put it on a fixed word next to the variable. "In {{name}}" became &lt;code&gt;{{name}} oyununda&lt;/code&gt;, literally "in the {{name}} game", where the suffix sits on "game".&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lesson:&lt;/strong&gt; never attach grammar directly to a variable.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. The percent sign moves
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;label&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;%&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// works until it doesn't&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Language&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;English&lt;/td&gt;
&lt;td&gt;30%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Turkish&lt;/td&gt;
&lt;td&gt;%30&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;German&lt;/td&gt;
&lt;td&gt;30 %&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;French&lt;/td&gt;
&lt;td&gt;30 %&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The sign leads in Turkish. In German and French there's a space before it, and it has to be a no-break space, or the &lt;code&gt;%&lt;/code&gt; can wrap onto a line by itself.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Intl&lt;/code&gt; already knows this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;Intl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;NumberFormat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;tr&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;percent&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.3&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// "%30"&lt;/span&gt;
&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;Intl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;NumberFormat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;de&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;percent&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.3&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// "30 %"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check its output against what you want for each language, though. It gives "30 %" for Spanish and Russian too, for example.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. French punctuation needs its own space
&lt;/h2&gt;

&lt;p&gt;French puts a space before &lt;code&gt;!&lt;/code&gt;, &lt;code&gt;?&lt;/code&gt;, &lt;code&gt;:&lt;/code&gt; and &lt;code&gt;;&lt;/code&gt;. A normal space lets the punctuation wrap onto the next line, alone, which looks like a bug. So our French strings use a no-break space:&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="nl"&gt;"levelUpTitle"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Niveau supérieur&lt;/span&gt;&lt;span class="se"&gt;\u&lt;/span&gt;&lt;span class="s2"&gt;00a0!"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The catch is that a no-break space looks exactly like a normal one in most editors, so you won't catch this by reading the file.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. "{{name}} invited you" has a gender problem
&lt;/h2&gt;

&lt;p&gt;English verbs don't care who did the inviting. Russian past-tense verbs do, and your string doesn't know the player's gender. Each language got its own answer:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Language&lt;/th&gt;
&lt;th&gt;String&lt;/th&gt;
&lt;th&gt;How it deals with gender&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Spanish&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{{name}} te invitó&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The verb has no gender here&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Turkish&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{{name}} seni davet etti&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The language has no grammatical gender&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Polish&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{{name}} cię zaprasza&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Present tense avoids it ("is inviting you")&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Russian&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{{name}} пригласил(а) тебя&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Both forms in one word&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That &lt;code&gt;(а)&lt;/code&gt; adds the feminine ending. It looks a little odd at first, but Russian-speaking users see it all the time in apps.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lesson:&lt;/strong&gt; rewording is often better than adding gender logic to your code.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. A correct translation can still be wrong
&lt;/h2&gt;

&lt;p&gt;Telegram's official Russian word for a story is «история». It's correct, and it's also the everyday word for "history".&lt;/p&gt;

&lt;p&gt;So a button that means "Post a story" could read as something about history. We switched to «сторис», the word people actually use, even though it isn't the official one. It's a borrowed word that doesn't change its ending, which also makes it easy to drop into any sentence.&lt;/p&gt;

&lt;p&gt;No linter would ever catch this. You only find it by reading the string where it appears and asking what a real person would think it means.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Tone needs a check, too
&lt;/h2&gt;

&lt;p&gt;Most of these languages have a formal and an informal "you". We use the informal one everywhere, because it's a game and the formal one feels like a letter from a bank.&lt;/p&gt;

&lt;p&gt;Translators, human or machine, drift back to the formal one, so we added a check that runs on every locale file edit and flags formal words:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;FORMAL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;es&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;(?&amp;lt;!\w)usted(?!\w)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;I&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="c1"&gt;# ...one pattern per language
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same script reports any key that's missing from a language. Plurals can trip that up, because English only needs &lt;code&gt;_one&lt;/code&gt; and &lt;code&gt;_other&lt;/code&gt; while Russian and Polish also use &lt;code&gt;_few&lt;/code&gt; and &lt;code&gt;_many&lt;/code&gt;. A naive comparison reports false errors, so we strip the plural suffix before comparing keys:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'[paths(scalars) | join(".") | sub("_(zero|one|two|few|many|other)$"; "")]
       | unique | .[]'&lt;/span&gt; en.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  7. Long words don't fit
&lt;/h2&gt;

&lt;p&gt;Every layout we had was tested in English first, and English is short.&lt;/p&gt;

&lt;p&gt;"Tap a tile to dig" fits a button easily. The Russian version, «Нажми на клетку, чтобы копать», pushed the button off the screen and took the price with it:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyj0i13zmo1ueb3imnvo0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyj0i13zmo1ueb3imnvo0.png" alt="Before: a long Russian label pushes a green button off the screen edge. After: a shorter label fits inside the button with its price." width="800" height="409"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We didn't try to squeeze it in. We shortened the text to «Выбери клетку» ("Pick a tile") and made the label truncate before the price, so the part that matters always stays visible.&lt;/p&gt;

&lt;p&gt;Our Match-3 score row broke in a sneakier way. In this narrow row, the score ran into the moves label, and the Russian clock, «44 с», wrapped onto two lines:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fudjql3igxncpp7ou9pfe.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fudjql3igxncpp7ou9pfe.png" alt="Before: the score overlaps the moves label and the clock wraps onto two lines. After: everything stays on one row and the moves label is shortened with an ellipsis." width="780" height="524"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The fix was to decide what's allowed to shrink: the score and the clock keep their width and never wrap, and the moves label gives way first. It's not pretty at this width, but everything's still readable, and you can see the fix wasn't free.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lesson:&lt;/strong&gt; test on your smallest phone in your longest language, and decide up front which element gives way.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd tell myself before starting
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Translate sentences, not fragments.&lt;/strong&gt; Almost every problem above gets worse when you glue strings together.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Let the variable stand alone.&lt;/strong&gt; Don't attach grammar or units to it in your code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Write down your fixed terms and tone.&lt;/strong&gt; Otherwise every new batch of strings drifts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test your longest language on your smallest phone.&lt;/strong&gt; That's where layouts break first.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Automate what you can, then read the rest in context.&lt;/strong&gt; A script can find a missing key, but it can't tell you that a word means "history".&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What's the worst translation bug you've ever shipped? I'd love to hear it, especially in a language I haven't run into yet.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>gamedev</category>
      <category>i18n</category>
    </item>
  </channel>
</rss>
