ihasmail Gained Nine Languages
Translations are the easiest thing in software to ship dishonestly. Nobody audits them. A user who opens a settings menu, sees their language listed, and picks it has no way to know whether a native speaker spent a week on it or whether a machine produced it in an afternoon. The interface looks the same either way — right up until it says something that lands wrong, and by then the reader has already assumed somebody meant it.
ihasmail, our JMAP webmail client for Stalwart Mail Server, gained nine interface languages today, bringing the picker to ten: German, Spanish, French, Dutch, Brazilian Portuguese, Russian, Ukrainian, Simplified Chinese and Japanese, alongside the English it was written in. All nine were produced by AI against standard dictionaries. Not one of them has been read by anybody who speaks the language.
We could have shipped that quietly. Instead each one is labelled Beta in the picker, and choosing it shows a line — in the language you just chose — saying it was machine-made, has not been checked, and linking somewhere to report anything that reads wrongly.
The same day brought the other thing people ask for most: ihasmail now behaves like a phone app when it is on a phone. Swipe, hold, pull to refresh, and push that arrives with the app closed. That is further down, and if you read mail on a phone it is probably the half you will notice first.
Why say so at all
Because the alternative is asking people to trust text nobody has verified, without telling them that is what they are doing.
A machine translation is not uniformly bad. Most of it is fine. The failure mode is that the errors are unpredictable and invisible from the inside: a word that is correct but belongs to a different register, a verb that agrees with the wrong thing, a phrase that is technically accurate and reads like a form letter from a government department. I cannot see any of that. Neither can a test suite, a coverage percentage, or a reviewer who does not speak the language. The only instrument that detects it is a person reading it.
So the Beta label is not modesty and it is not a soft launch. It is the accurate description of the artefact. A language loses it when a speaker has read the catalogue and said so — a deliberate act by a person, not something a completeness number earns.
If you speak one of the nine and are willing to read a few hundred strings, that is worth more to this project right now than any amount of further engineering.
The machinery is the part that is actually finished
The distinction worth drawing is between the translations and the thing that carries them. The translations are provisional. The framework under them is not, and it was built so that provisional translations degrade honestly.
English is the key. There is no settings.language.title to get out of sync with anything. The English sentence is the lookup key, which means a catalogue can never drift into referring to strings that no longer exist without something noticing — a key whose English has been deleted is caught by a check rather than sitting in the file looking correct and never being read.
A missing entry renders its English source. This is the property that makes the Beta label safe rather than reckless. Delete a bad line and the app shows the English it was translated from. Reporting a wrong string cannot break anything, and fixing one is a deletion in the worst case.
Plurals are asked for, never assumed. English has two forms and a great deal of software quietly assumes everyone does:
Russian: 1 письмо · 2 письма · 5 писем three forms
Ukrainian: 1 лист · 2 листи · 5 листів three forms
Japanese: 1 通のメール · 5 通のメール one form, with a counter
Chinese: 1 封邮件 · 5 封邮件 one form, with a counter
Intl.PluralRules decides which form applies, so Russian and Ukrainian get their three and Japanese and Chinese get the one they actually have. That last point took checking rather than assuming: both return other for every number, so filling in a one form for Japanese would have been inventing a distinction the language does not draw. What English puts in a plural -s, Japanese puts in a counter — 通 for letters, 件 for conversations — which is why counted strings are separate entries rather than one pattern with a number slotted in.
The same English word is not always the same word. German wants Archivieren on a button and Archiv on a folder; English uses "Archive" for both. Strings carry a context where the source is ambiguous, so the catalogue can distinguish a verb from a noun without renaming anything in the source.
Catalogues are code-split. Each is about 40–50 kB, 17–18 kB over the wire, and downloaded only if you choose that language. Reading in English costs nothing.
What a coverage number is worth
Here is the part I would rather not write, and the reason it is here anyway.
We had a script reporting 100% extraction coverage while roughly two hundred strings rendered English in every language — including in production, for weeks.
It was not lying. It reads JSX text, and every string it could see was wrapped. The ones it could not see were built from expressions: toast messages, confirmation-dialog titles, title= and aria-label= attributes, template literals. The clearest case was the calendar's own view switcher, which spelled its labels from the view identifier:
v[0].toUpperCase() + v.slice(1) → Day Week Month Agenda
Perfectly correct English and untranslatable into anything else. The galling part is that Day, Week, Month and Agenda were already sitting in all nine catalogues, unused, because the buttons never asked for them. Somebody reading the calendar in Japanese saw 日 週 月 予定リスト nowhere and Day/Week/Month/Agenda everywhere, and a report is how we found out.
All of them are wrapped now, and the check grew a second half that refuses any user-visible string which is neither wrapped where it is written nor present as a catalogue key — a string no catalogue can translate, however many languages ship. It found twenty more than a careful manual sweep had.
The general lesson has nothing to do with translation: a coverage number measures the thing it can see, and the cases it cannot see are exactly the ones nobody is checking. A metric at 100% is a statement about the metric.
Folders, dates, and what belongs to whom
Two decisions that show up immediately on a real mailbox.
Folders Stalwart gives a role — Inbox, Archive, Drafts, Sent, Deleted Items, Junk Mail — follow the interface language, because those names are the server's and mean the same thing everywhere. Folders you made keep the names you gave them, in whatever language you gave them. Verified against a live 6,289-message account: the six role folders localised and roughly twenty custom ones were left exactly as their owner named them.
That same mailbox produced the nicest confirmation that the plural machinery works on real data rather than hand-picked test values. 6,289 conversations, in Ukrainian, renders as 6289 листувань — the genitive plural a number ending in nine takes, which is neither of the two forms English would have offered.
The interface language is a separate setting from the date and number format. Wanting German dates on an English interface is a real preference and so is the reverse. Choosing a language does feed the automatic date locale, so picking 日本語 gives Japanese month and weekday names without configuring the region as well — but you can override either independently.
Only languages with a catalogue behind them appear in the picker. One offered without strings would set <html lang> to a language the page is not in, which is worse than not offering it at all: it stops a browser offering to translate a page the reader genuinely cannot read.
On a phone, it now behaves like one
ihasmail installs as a PWA, and as of today it has stopped being a desktop layout that survived being made narrow.
Swipe a message to act on it. Each direction is configurable — archive, delete, mark read or unread, star, report spam, move, or nothing at all — and the choice follows your account, so a phone and a tablet agree without being set up twice. A mouse ignores the setting entirely and keeps dragging messages into folders, which is the right gesture for a pointer.
The interesting part is that the gesture knows where it is. Swiping to delete in Deleted Items says Delete forever, because that is what it does there. In Junk Mail the spam gesture becomes Not spam. Archiving inside the archive does nothing and says so by refusing to arm — a swipe that dutifully performs a no-op is worse than one that declines.
Two details that decide whether any of this is usable:
- The axis lock is biased toward scrolling. A horizontal gesture has to be clearly horizontal before it takes over, because a list you cannot scroll without accidentally deleting something is not a list.
- The threshold scales with the screen rather than sitting at a fixed pixel count, so the same flick means the same thing on a small phone and a tablet.
Hold a message to select it, hold a folder for its menu, pull the list down to check for new mail, and swipe from the edge to go back from a conversation.
Push arrives with ihasmail closed. Your own mail server sends it: Stalwart signs the notification and pushes it to your browser, naming sender and subject, with no ihasmail server involved at all.
Worth being precise about, since the point of this article is not overstating things — Web Push always travels through your browser vendor's push service, and there is no way to build it that does not. What that service relays is a payload encrypted to a key pair your browser generated, so it can see that something arrived for you and not what. The part ihasmail removes is the application middleman: no service of ours is told who mailed you.
That one came with a lesson worth passing on: Stalwart expires a push subscription after seven days. A client that registers once and assumes it is done works perfectly for a week and then goes quiet, with nothing anywhere reporting a failure — the subscription simply stops existing. ihasmail renews on a schedule, and the flag saying push is enabled deliberately survives a sign-out, because a deploy signs everyone out and a renewal that forgets why it was renewing is a notification system with a seven-day fuse in it.
Try it, and tell us what reads wrongly
ihasmail is AGPL-3.0-or-later, targets Stalwart 0.16 or newer, runs immutably with no volume and no database, and has no paid tier or feature gate.
- Project site: ihasmail.org
- Docs: docs.ihasmail.org
- Source: github.com/Coffey-Labs/ihasmail
If you find English where you expected your own language, that is a bug and worth reporting — the extraction pass above found two hundred of them and there is no reason to believe it found the last one.
And if you read one of these languages: the report link is in Settings, next to the Beta label, and a single sentence about one badly-chosen word is a real contribution. Nine catalogues are waiting for somebody who can see what I cannot.