Apple's public API returns a list of the languages an app declares. Counting them looks like a one-liner, and three separate things about that field make the obvious implementation wrong.
It matters because the number gets compared. "This app supports 30 languages, mine supports 8" is a reasonable thing to want to know, and it is only useful if both sides were counted the same way.
Trap one: one code appears twice
languageCodesISO2A contains duplicate entries, and .length is therefore not the number of languages.
Verified against live data: ZH is the only code that ever repeats. It appears twice for apps shipping both Chinese scripts — Simplified and Traditional. The frequency is not marginal:
| Search term | Rows with a duplicated code |
|---|---|
| photo editor | 21 of 26 |
| weather | 12 of 27 |
One large weather app returns 55 entries and 54 distinct codes. A naive count reports 55.
So the count has to be over distinct codes. And the second consequence is subtler: because the field repeats ZH rather than distinguishing zh-Hans from zh-Hant, it cannot tell you which script an app ships. Appstro's language coverage therefore gives ZH no link through to the metadata writer — there is no defensible answer to "which Chinese".
Trap two: empty means unknown, not zero
An app returning an empty array has not told you it supports no languages. It has told you nothing.
Sampled across 2,110 apps, exactly one returned []. That rarity is the point: it is clearly a gap in the response rather than a real property of an app, and scoring it as zero would put that app at the bottom of every comparison it appears in.
This is the same rule that runs through every cohort tool here, and it is worth stating as a general principle rather than a special case: absence and zero are different facts, and rendering them the same is how a tool invents a finding. A missing value is reported as missing and excluded from the denominator.
Trap three: this is the binary, not your listing
The biggest one, and it is a category error rather than an arithmetic one.
languageCodesISO2A describes what the app binary declares. It says nothing about whether your App Store listing is localized. An app can declare twenty languages in its bundle and still show an English name and description in every one of Apple's storefronts — the two are set in different places and neither implies the other.
Those are two genuinely different projects with different costs:
| Binary languages | Listing localization | |
|---|---|---|
| What it changes | The UI a user sees inside the app | The page a stranger reads before installing |
| Where it is set | Your app's resources | App Store Connect metadata |
| What the API shows | languageCodesISO2A | The name and description per storefront |
| Who it reaches | People who already installed | People deciding whether to |
Appstro carries that as a non-dismissible banner rather than a footnote, and links across to the localization audit, which checks the other half by comparing each storefront's name and description against the US listing.
If you only do one, the listing is usually the higher-leverage half, because it acts on people who have not installed yet.
What a fair comparison needs
Once the counting is right, the comparison still has to be against something meaningful. A count on its own has the same problem a rating count has: 8 languages is a lot for a two-person utility and very little for a keyboard app.
The cohort here is the apps returned for a search term, which is a rough proxy for "apps competing for the same person". It is rough on purpose and worth knowing the limits of:
- It is one search, one storefront. Search a different term and the cohort changes.
- It mixes scales. A result row routinely contains a company with a localization team and someone shipping alone. The median absorbs that better than the mean, which is why the page reports a median.
- It is a description of what exists, not a target. "The cohort ships 14 languages" is a fact about the row. Whether you should ship 14 depends on where your users actually are, which the row does not know.
What the comparison genuinely surfaces is the specific gap: not "you are five behind" but "eight of the ten apps here ship Japanese and you do not". A named language is something you can decide about. A deficit number is not.
Codes with no name
The lookup table covers 190 language codes. A code outside it renders as the raw uppercase code with a "code only" marker.
That is deliberately ugly. The alternative is inventing a plausible language name for a code nobody verified, which is a small lie that looks like data. An unfamiliar two-letter code shown as itself is honest and immediately checkable.
What this cannot tell you
- Not the quality of any translation. A declared language is a claim in a manifest. Whether the app reads naturally in Korean is not visible from outside, and no count can stand in for a native speaker.
- Not which Chinese script. The field collapses both into
ZH. - Not whether adding a language pays. The cohort tells you what is normal in your category; it cannot tell you where your users are. Localization priority answers a narrower version of that from rating volume.
- Not whether the listing is localized. Different field, different tool, different project.
- Not a zero. An empty array is unknown, and it is reported that way.
The work
Run your app through language coverage and read two things: the distinct count against your cohort's, and the specific languages the cohort ships that you do not. Then check whether your listing is localized in the storefronts that already have your users — those are different questions, and the second one usually matters more.