← All updates

Reef Tank Log: fourteen languages, and the bugs only a screenshot could find

Reef Tank Log is live on Google Play in fourteen languages. Three translation gates passed clean and found almost nothing. Then we screenshotted every locale, and the real defects showed up in an afternoon: English labels in Arabic, a degree sign pointing the wrong way, and a verb that agreed with the wrong noun in five languages at once.

Reef Tank Log is an offline logbook for saltwater reef aquariums: water parameters, trend charts, dose schedules with local reminders, and a journal for every coral and fish. It's free on Google Play, there are no ads, and nothing leaves the phone.

It ships in fourteen languages, including Arabic and Thai. This post is about what that actually cost, because the translating was the cheap part.

Three gates that found almost nothing

Our localization checklist runs machine gates before a human looks at anything:

Parity. Every language carries exactly the same 346 keys, every {placeholder} survives, nothing is blank, nothing is still English. This one earns its keep: a script we wrote to convert units did a find-and-replace on ml and turned the placeholder {ml} into {مل} in Arabic and {มล.} in Thai. That's not a typo, it's a string that fails to render. The gate caught it in a second; no human reading the translation would have.

Style. Per-language rules that a native speaker spots instantly and a foreigner never does — French non-breaking spaces before a colon, German sentence length, three quote conventions inside one locale. Clean.

Back-translation. A second model that has never seen the English translates the target back, and we diff the round trip. Eleven flags. Six were the model failing to recognise correct idiom: the German for propagated, the Indonesian aquarium trade's habit of just saying "powerhead", the direction of an arrow under right-to-left. We left all six alone.

Three gates, one real defect. If we'd stopped there we would have shipped, and we would have been wrong.

Then we looked at it

The fourth check isn't a script. It's fourteen languages on a screen.

To make that cheap we needed the app to switch language and jump to a screen at runtime. The obvious channel doesn't work: on iOS, Platform.environment inside a Flutter isolate is an empty map, and simulator environment forwarding never reaches Dart. Compile-time flags do work, but fourteen languages times five screens is seventy builds. So the capture script now drops a small JSON file into the app's own temp directory and a debug-only hook reads it at launch. One build, seventy relaunches, about four minutes.

Here is what turned up on the first pass, none of which any gate had flagged:

  • English in every locale. The parameter names — Alkalinity, Calcium, Nitrate — came from a Dart getter, not from the translation files. Arabic, Thai and Korean users saw English.
  • Eleven more strings that had never been extracted at all: "Stability", "Stable", "Variable", "Auto", "Manual", table headers.
  • A tab bar that lit the wrong tab in Arabic. The highlight pill was positioned with an absolute left offset, but the row mirrors under right-to-left. In Arabic, the app opened on the dashboard with "More" highlighted.
  • A mirrored degree sign. ° has no direction of its own, so in an Arabic paragraph it takes the paragraph's. The temperature card read F°. The fix is a Unicode isolate around the unit.
  • Prices in the wrong currency and a permission banner — both artefacts of the screenshot simulator, both about to appear in fourteen localized store listings. A reader quoted in zloty does not want to see a dollar sign.

One verb, five wrong languages

The worst one was grammatical. A line on the trends screen said "Alkalinity held within range", with the parameter name interpolated. Every language that agrees a verb or participle with its subject got it wrong for roughly half of the parameters: "Щёлочность держалось" in Russian, "Alcalinité maintenu" in French, "Alcalinità si è mantenuto" in Italian, "Zasadowość utrzymywało się" in Polish, "القلوية ظل" in Arabic.

You cannot fix this by picking a better verb, because the subject is a runtime variable. So we deleted the verb. The line is now a colon and a prepositional phrase — "Alkalinity: within 8.2-8.6" — which agrees with nothing and is therefore right in every language. Korean had a smaller version of the same problem: a counter particle sitting on a line that has no number on it.

Thai and Arabic wanted a different renderer

The store screenshots carry a caption above the phone. Our Python compositor draws Latin and Cyrillic fine and cannot draw Thai or Arabic at all — no font fallback, and without a shaping library no shaping, so every Thai vowel mark came out as a dotted circle and Arabic came out as isolated letters running left to right.

Two attempts to rebuild the imaging library with shaping support failed. The fix was to stop fighting it: a forty-line Swift helper renders the caption with CoreText, which is already on the machine, shapes every script correctly and walks the system font fallback chain on its own. The Python side composites the result.

The listing is not the app translated

The last gate is the store, and it's the one people skip. A German reefkeeper does not search for the German translation of our English keywords — they search for what Germans type. The keyword set is built per storefront from actual search recon, not from the English list run through a translator.

Two things the metadata builder caught before upload, both of which are rejections rather than warnings: two keyword fields had drifted to 102 characters against a 100-character limit, and the support URL we'd written into the listing was a 404. A dead support link is its own rejection, entirely separate from the app.

One thing we won't repeat: the auto-renewal paragraph in the store description is now read directly out of the app's own translation file, so the words a reviewer sees in the listing are literally the words on the paywall. Two hand-maintained copies drift, and the one that drifts is the one that gets rejected.

Where it is now

Reef Tank Log is live on Google Play in fourteen languages. The iPhone build exists — it's Flutter — but it isn't on the App Store yet; we'll write that one up when it lands.

The useful takeaway isn't about reef tanks. It's that mechanical translation gates can only see words, and the defects that embarrass you are structural: a layout that mirrors, a character with no direction, a verb agreeing with a variable. The cheapest instrument for finding them is a screenshot of every locale, and it's only cheap if you build the capture rig first.