Commit Graph
7 Commits
Author SHA1 Message Date
jcoffey-dev bd6a605d61 Group six more clusters out of web/src/lib
Takes the flat module count from 66 to 42, continuing what admin/ and
calendar/ started.

  lib/mailbox/  archiveDate, emptyFolder, folderMove, labelTree,
                mailboxName, mailboxRoute
  lib/sieve/    sieve, sieveApply, sieveFolders
  lib/input/    keyboard, swipe, touch, listSelection, dropUpload
  lib/notify/   notify, webpush, webpushEnable
  lib/sw/       swCache, swFacts, staleBuild
  lib/text/     html, markdown, text, emlName

FOUR THINGS THE FILENAMES GET WRONG, each checked by reading the file
rather than trusting what it is called:

  - appFolder is not a mailbox. It is the `ihasmail` folder in JMAP
    *Files*, where the client keeps signature images and synced settings.
    It stays flat.
  - format holds no formatting of text. It re-exports the date and clock
    formatters, so it belongs with dates/datetime, not with text/.
  - preview is the file viewer deciding what it can show without
    downloading, and source is where to point someone asking for this
    instance's AGPL source. Neither is about text.
  - notify is not Web Push. It is the tab title, the favicon badge and
    the new-mail sound -- in-app notification, which is why it sits with
    webpush rather than under sw/ with the service worker's own concerns.

threadScroll stays flat too: it decides where a conversation opens, which
is view state rather than a gesture, and input/ is honest only if
everything in it interprets something the reader did.

No behavior change. Almost every reference was on the @/ alias; eight
relative imports in files that did not move, or that moved away from a
sibling, needed rewriting by hand.
2026-09-15 23:17:50 -07:00
jcoffey-dev d1731efdb9 Use American English spelling throughout 2026-09-15 11:45:58 -07:00
jcoffey-dev b10149fd54 Translate three lists the code was rendering raw
A native speaker reviewing the German catalogue reported strings appearing in
English (#247). Three of the reported areas turned out not to be missing
translations at all: the strings were there, and the code was rendering the
English source instead of asking for one.

The Sieve rule dialog rendered HEADER_CHOICES and HEADER_OPS labels directly.
All sixteen are already in every catalogue -- "Subject" has been "Betreff" in
de.ts all along, which is exactly the inconsistency the reporter noticed
against the Out-of-office page, where it already reads Betreff. Wrapping the
two dropdowns fixes nine languages at once and adds nothing to any catalogue.

The keyboard shortcut panel rendered each binding's group and description
directly. Those are registered in English at the call sites and should stay
that way -- the binding table is data and the English is the catalogue key --
so the panel translates them at render instead. A binding added anywhere is
then translatable without its registrar knowing i18n exists.

The palette grid marks every name translate="no". That is right for ihasmail,
Dracula, Gruvbox, Rosé Pine and Tokyo Night, which are names. "Classic" is an
adjective describing the theme, not a name, so PaletteMeta gains a
`translatable` flag for the one entry that is a word. Flagging the exception
beats dropping the attribute from all six.

No catalogue changes here: the strings the first two need are already present
in all nine. "Classic" needs an entry, which lands with each language.
2026-09-03 11:37:05 -07:00
jcoffey-dev 87383440bb German, generated by AI and marked Beta until somebody signs it off
The first language, and the first one where the honest thing to say is not
flattering: no native speaker has read it. That is stated in the app rather
than in a commit nobody reads, because it is the fact a reader needs to judge
what they are looking at. Somebody told a translation is unchecked forgives an
odd sentence and reports it; somebody told it was reviewed reasonably concludes
the product is sloppy. The setting carries a link straight to a report, which
is the whole review process here.

`beta` is a property of the language, not of the catalogue's completeness. A
file can be word-for-word finished and still read like a machine wrote it, and
that is what the flag marks. Removing it is a person's decision.

Register is "Sie", consistently, and written down in the file so the next
language and the next contributor inherit the decision rather than re-take it.
Thunderbird and Outlook use it; ihasmail is as often a company's mail as
somebody's own, where "du" from software the workplace deployed reads as
presumptuous. Where a string can dodge the question it does, which is ordinary
good German UI. The glossary at the top of the file fixes the vocabulary once
-- Posteingang, Papierkorb, Entwürfe, archivieren -- because inconsistency
reads as amateur far more than an imperfect word choice does. "Label" and
"Spam" stay English, since translating them would name things no German mail
client calls that.

766 of 781 strings. The fifteen left are product names, bare URLs and example
addresses, which should stay English and now do.

Two things this turned up that the earlier work had hidden:

Labels defined as module-level constants -- the entire settings navigation,
the theme cards, the swipe choices, the date formats, the sharing permissions
-- are evaluated once, before any catalogue loads, so they could only ever be
English. Nothing failed; the German build simply had an English sidebar. They
are translated where they render now, which keeps the constant as data and
makes its English text the key.

And the codemod's narrowed rule, which let it take 73 more strings last time,
was too broad after all: text stranded after an inline <a> or <strong> came
through as sentence fragments -- ", and what a new account starts on." Eight
of them, rebuilt with tNode so the sentence stays whole and the element is a
named hole a translator can move.

scripts/i18n-catalog-check.mjs is new and earned itself immediately: it found
three keys invented that the code never asks for, which is the silent failure
in a catalogue -- a translation that looks right, is never looked up, and
renders English for ever. It also had to be taught about t(variable), because
it cried wolf 33 times over the constants above, and a check that cries wolf
gets switched off.

Verified in the browser rather than only in tests, which is where the settings
sidebar being English was visible and nowhere else.
2026-08-31 11:06:54 -07:00
jcoffey-dev 3f4b33cb51 Finish extraction: 100%, and a coverage number worth believing
The 143 the codemod refused turned out to be two different things, and only
one of them needed a person.

A third were phrases sitting next to an icon -- `<Plus /> New rule`. The
refusal rule was "has siblings", which is broader than the danger: what breaks
a translation is a sibling that renders *text*, splitting a sentence into
fragments no one can reorder. An element beside a phrase does not. Narrowing
the rule to text-producing siblings let the codemod take 73 more.

The rest were real sentences with values in the middle, rebuilt by hand as
named placeholders -- "Your active script “{name}” was written by hand",
"Waiting on the server — goes out {when}." Named rather than positional
because a translator moves the parts around; counted things go through
plural() so Russian and Ukrainian get their three forms rather than English's
two.

Sentences with an element inside them needed something new. `Open <code>mailto:
</code> links in ihasmail` has two obvious treatments and both are wrong:
splitting it into two t() calls hands over fragments that cannot be reordered,
and dropping the <code> keeps the sentence whole but loses the monospace that
said "this is a literal". tNode() keeps the sentence whole and makes the
element a named hole in it, so a translator sees one sentence and can put the
hole where their language wants it. The German test asserts exactly that: the
same call renders the code first when the catalogue says so.

The coverage number was also lying, and it is worth saying how. It counted
text inside <code> and inside translate="no" as untranslated work, and
placeholders like "123456" and "+1 555 0100" -- a one-time code and a phone
format. None of those will ever be translated, so the report sat at 21 with 6
real items left. A number with an unreachable floor is something to argue with
rather than act on, so the tool now applies the same rules the codemod does.

596 wrapped, nothing remaining. Verified in the browser across 15 views, which
is where the last bulk pass hid a bug the tests could not see: no entities, no
unfilled placeholders, no raw t( in rendered text, and the toggle switches that
looked like emptied labels are text-free by design.
2026-08-31 10:41:24 -07:00
jcoffey-dev 8ea611f7f7 Extract 515 strings by codemod, and the two bugs only a screenshot caught
Wrapping ~1,000 strings by hand is a thousand chances to mistype the copy
itself, and a parser does not get bored. scripts/i18n-extract.mjs does the
mechanical part -- JSX text and the attributes a person actually reads -- and
refuses the rest rather than guessing. 78% now: 515 wrapped, 143 left.

What it refuses matters as much as what it does. Text split around an
interpolation arrives as separate fragments, and wrapping each on its own
produces "Move " and " messages", which no translator can do anything with;
those are listed for a person to rebuild as sentences. So is anything
containing a double quote, which would end the literal.

Three things it had to be taught, each found by running it:

- <code>, <kbd> and <pre> are not prose. The first run wrapped `label:name`
  inside <code> -- a search operator, where translating it breaks the thing it
  documents. Subtrees marked translate="no" are skipped for the same reason.
- `t` is a natural name for a callback parameter and several files already use
  it, so an import called `t` is shadowed inside those callbacks -- silently,
  wherever the local happens to be callable. The name is checked per file now
  and aliased to `translate` where it is taken.
- JSX decodes HTML entities and a JS string literal does not, so
  `Language &amp; region` moved into t("...") and rendered the entity on screen.

That last one is the one worth remembering. Typecheck passed, 443 tests
passed, and the page said "Language &amp; region" in plain sight. It took
looking at a screenshot, and then a sweep of ten views to find the second
occurrence in a sentence I had written by hand earlier the same day. Nothing
in the toolchain was ever going to catch it: it is valid TypeScript rendering
valid text that happens to be wrong.

The codemod decodes entities now, and checks for a quote after decoding rather
than before.
2026-08-31 09:58:33 -07:00
jcoffey-dev 645b8b510f ihasmail 2.0: rebuild as Stalwart-first JMAP webmail
Replace the FastAPI/HTMX prototype with a Node/Hono session proxy and a
React 19/Vite SPA. Mail (conversation view, search operators, labels,
sanitised HTML, privacy image proxy, invites, undo send, templates),
calendar (month/week/day/agenda, invites, free/busy, categories,
context menus), contacts (JSContact, groups, vCard), files, Sieve filter
builder (incl. filter-from-message with retroactive apply), vacation,
identities with default + Reply-To, PWA/mobile layout, push via SSE,
in-memory mock Stalwart for dev, Docker + CI.
2026-08-23 01:07:13 -07:00