The Business Reality4 min read

My Store Descriptions Were Missing Their Apostrophes. For Months

After fixing the keyword surface I extended the same audit to the body copy. 40 slots had every diacritic stripped, French had lost its apostrophes so the sentences were broken, and the Vietnamese and Polish descriptions were not readable at all in that state.

#i18n#aso#quality#gotchas
The same French sentence twice. The top line is correctly written with apostrophes and diacritics. The bottom is the same sentence with them gone, words split apart, a small red square marking each missing position, and a label saying the loss is not legibility but searchability.
Hard to read is not the problem. Nobody finding it by those words is the problem.

When did you last verify that your store description is actually written correctly in that language?

I never had. Translations went in, the character counts looked right, the deploy succeeded — good enough. In reality my French description had shipped with every apostrophe gone, and had been live like that for months.

Why I went past keywords

After fixing the keyword surface blocked by a limit that didn't exist, I extended the same audit to the body copy. The reason is simple: you can stuff in every search term you want, but if the body isn't spelled correctly, none of those terms match.

I picked one test. If a diacritic that must appear in that language appears zero times in the body, treat it as stripped. Short strings trip that test even when they're fine, so I only counted bodies over 400 characters.

40 slots were flagged

And it wasn't only diacritics.

The French body had lost every apostrophe. qu'elle n'est had become qu elle n est; d'ennemis had become d ennemis. That isn't a typo, that's a broken sentence. Italian had shipped l'Heavy as l Heavy.

Turkish was worse. Slow Time'ı had become Slow Time u, and yüzde 70'ini had become yuzde 70 ini. In a language where the apostrophe attaches a case suffix, turning it into a space splits one word into two. To the search index those are simply different words.

Vietnamese and Polish are not readable in that state at all. Vietnamese tone marks carry meaning; the same is true in Polish. For months, those two descriptions weren't "readable but odd" — they were unreadable.

Two more things the same audit found

One app had 2,192 characters of the English original sitting in its German and Portuguese slots. Not "untranslated" — the source text had been copied into the translation slot. The deploy succeeded, the character count was healthy, and no check anywhere objected.

And in seven locales every line began with two spaces. A paste out of a code block had shipped straight to the store.

How I found them

Detection is "does a required character for this language appear zero times in a long body?"

REQUIRED = {
    "fr": "àâçéèêëîïôùûü",
    "pl": "ąćęłńóśźż",
    "vi": "ăâđêôơư",
    # ...
}
 
if len(body) >= 400 and not any(c in body for c in REQUIRED[lang]):
    suspect.append((app, lang))

Alongside it I look for words that can only exist when something has been stripped — forms like horaires decales, attivita, Nachtschwesternkraeften cannot occur in correct spelling. That signal has far fewer false positives.

When fixing, I replace word by word and die if any target is missing:

for wrong, right in replacements:
    if wrong not in body:
        raise RuntimeError(f"replacement target not found: {wrong}")
    body = body.replace(wrong, right)

That's there to prevent a silent no-op. A replacement script that replaces nothing still exits zero.

When would you fix it?

One useful fact. A version sitting in review lets you edit the description in place. No new build, no cancelled review. A version already on sale is locked and returns 409, which means you need a new release to fix it.

So the ordering is: fix the ones currently in review first. Result: 40 slots down to 2. The remaining two are on-sale apps and rode the next release.

Three-line self-check

  1. Is there a normalization step anywhere in your translation pipeline? unicodedata.normalize plus an ASCII encode, iconv //TRANSLIT, or a reused slug generator — those three are the usual culprits.
  2. Have you ever read the deployed copy back? A write API returning 200 and the stored value matching what you sent are different claims.
  3. Does your replacement script succeed when it replaces nothing? Then it guarantees nothing.

The honest part

I never established why the text was stripped. Something appears to normalize to ASCII at some point in the chain, but I could not identify the step. Without the cause I cannot prevent a recurrence — what I have is an auditor that detects one. That's an alarm, not a fix.

My first auditor was more than half false positives. Short strings legitimately contain no diacritics. It only became useful once I added the 400-character floor. Italian uses few diacritics natively, so even a healthy body only shows about eight — automation couldn't separate those and I had to read the words myself.

The third layer of the same audit was the subtitle — 36 subtitle slots were still the English original, which is a different kind of hole again.

If you ship multilingual store listings, open one French description right now and look for apostrophes. It takes thirty seconds.

Related