Files
ihasmail-inbuxa/web/src/lib/mailbox/mailboxName.ts
T
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

80 lines
3.6 KiB
TypeScript

import { tc } from "@/lib/i18n";
import type { Mailbox } from "@/jmap/types";
/**
* What to call a folder on screen.
*
* Stalwart names the standard folders once, when the account is created, in
* whatever language the server was set up in — and never renames them
* afterwards, because the name is stored data every other client has mapped.
* So a German reader on an English-provisioned account sees "Deleted Items"
* in an otherwise German app, and there is nothing the server can be asked to
* do about it: the account locale exists in `x:AccountSettings`, but writing it
* needs `sysAccountSettingsSet`, which the built-in user role does not carry.
*
* The role is the way out. JMAP tags the standard folders — `inbox`, `trash`,
* `drafts` and the rest — and ihasmail already trusts the role rather than the
* name everywhere it matters, so the display name can follow the interface
* language without anything being written to the server.
*
* Only the roles. A folder somebody made and called "Newsletters" keeps that
* name, because those are their words and translating them would be inventing
* a folder they never made.
*
* The cost, and it is real: another client on the same account still shows
* "Deleted Items", because that is what the folder is called. Within ihasmail
* this stays consistent — everything that names a folder goes through here,
* including the "moved to …" toast, which exists precisely so that message
* does not name somewhere the reader cannot find.
*/
/*
* Every one of these is translated in the "folder" context, including the
* unambiguous ones. Two of them genuinely need it -- "Archive" is also the
* button that archives, "Important" is also a priority tag, and German wants a
* different word for each -- and applying it to only those two would leave the
* next person to notice which. A context on all of them is one rule.
*/
const ROLE_NAMES: Record<string, () => string> = {
inbox: () => tc("folder", "Inbox"),
archive: () => tc("folder", "Archive"),
drafts: () => tc("folder", "Drafts"),
sent: () => tc("folder", "Sent"),
trash: () => tc("folder", "Deleted Items"),
junk: () => tc("folder", "Junk Mail"),
important: () => tc("folder", "Important"),
all: () => tc("folder", "All mail"),
};
/** The folder's name as the reader should see it. */
export function mailboxDisplayName(mailbox: { name: string; role?: string | null } | null | undefined): string {
if (!mailbox) return "";
const localized = mailbox.role ? ROLE_NAMES[mailbox.role] : undefined;
return localized ? localized() : mailbox.name;
}
/**
* Whether this folder's displayed name is ihasmail's rather than the server's.
*
* Anything that *edits* the name has to know: a rename dialog prefilled with
* "Papierkorb" would rename the folder to that on the server the moment
* somebody pressed Save, which is a real change made by accident to a folder
* they were only looking at. Renaming a role folder is refused anyway, but
* relying on that would be relying on a rule enforced somewhere else.
*/
export function isLocalizedName(mailbox: { role?: string | null } | null | undefined): boolean {
return Boolean(mailbox?.role && mailbox.role in ROLE_NAMES);
}
/** A path of folder names, for a picker that shows where a folder sits. */
export function mailboxDisplayPath(mailbox: Mailbox, all: Record<string, Mailbox>): string {
const parts: string[] = [];
let cur: Mailbox | undefined = mailbox;
const seen = new Set<string>();
while (cur && !seen.has(cur.id)) {
seen.add(cur.id);
parts.unshift(mailboxDisplayName(cur));
cur = cur.parentId ? all[cur.parentId] : undefined;
}
return parts.join(" / ");
}