fix: show localized system folder names in the filter rule folder picker

This commit is contained in:
Linus Rath
2026-10-31 13:54:39 +01:00
parent 7eb7289472
commit 56c8b98bb6
3 changed files with 88 additions and 1 deletions
@@ -0,0 +1,55 @@
// The filter rule's "Move to folder" picker listed the server's folder names
// ("Sent Items", "Junk Mail", "Deleted Items") where the rest of the app
// shows the localized role names (localizeMailboxName).
import { describe, expect, it, vi } from 'vitest';
import fs from 'node:fs';
import path from 'node:path';
vi.mock('expo-localization', () => ({ getLocales: () => [] }));
import { translate } from '../../i18n';
import { buildMailboxTargets } from '../../lib/sieve/rule-actions';
import { folderOptionLabel, mailboxesByTargetId } from '../filters/folder-option-label';
import type { Mailbox } from '../../api/types';
const mb = (id: string, name: string, role: string | null, parentId: string | null = null): Mailbox => ({
id, name, role, parentId, sortOrder: 0, totalEmails: 0, unreadEmails: 0, totalThreads: 0, unreadThreads: 0,
myRights: {} as Mailbox['myRights'], isSubscribed: true,
} as unknown as Mailbox);
const mailboxes = [
mb('i', 'Inbox', 'inbox'),
mb('s', 'Sent Items', 'sent'),
mb('j', 'Junk Mail', 'junk'),
mb('d', 'Deleted Items', 'trash'),
mb('p', 'Projects', null),
mb('a', 'Alpha', null, 'p'),
];
function labels(locale: 'en' | 'de') {
const t = (key: string, fallback?: string) => translate(locale, key, fallback);
const byId = mailboxesByTargetId(mailboxes);
return Object.fromEntries(buildMailboxTargets(mailboxes).map((target) => [target.id, folderOptionLabel(target, byId.get(target.id), t)]));
}
describe('filter rule folder picker labels', () => {
it('names system folders by role, like the sidebar', () => {
const en = labels('en');
expect(en.s).toBe(translate('en', 'sidebar.mailboxes.sent'));
expect(en.j).toBe(translate('en', 'sidebar.mailboxes.spam'));
expect(en.d).toBe(translate('en', 'sidebar.mailboxes.trash'));
expect(en.s).not.toBe('Sent Items');
expect(labels('de').d).toBe(translate('de', 'sidebar.mailboxes.trash'));
});
it('keeps user folders and their indent', () => {
const en = labels('en');
expect(en.p).toBe('Projects');
expect(en.a).toBe(' Alpha');
});
it('is what the rule editor lists', () => {
const source = fs.readFileSync(path.resolve(__dirname, '../filters/FilterRuleModal.tsx'), 'utf8');
expect(source).toContain('label: folderOptionLabel(target, mailboxById.get(target.id), t)');
});
});
+6 -1
View File
@@ -45,6 +45,7 @@ import { SafeAreaModal } from '../SafeAreaModal';
import { useFilterStore } from '../../stores/filter-store';
import { forwardsOfEditedRule, ruleForwardLimit } from '../../lib/sieve/forward-limit';
import { MODAL_ORIENTATIONS } from '../../lib/modal-orientations';
import { folderOptionLabel, mailboxesByTargetId } from './folder-option-label';
import { useModalKeyboardPadding } from '../../lib/sheet-bottom-inset';
const ALL_FIELDS: FilterConditionField[] = ['from', 'to', 'cc', 'subject', 'header', 'size', 'body', 'attachment', 'all'];
@@ -131,6 +132,7 @@ export function FilterRuleModal({
const periodProblem = periodOn ? periodError(activeFrom, activeUntil) : null;
const mailboxTargets = useMemo(() => buildMailboxTargets(mailboxes), [mailboxes]);
const mailboxById = useMemo(() => mailboxesByTargetId(mailboxes), [mailboxes]);
// A message can collect only so many forwards on the server (#1155, #1208).
const listedRules = useFilterStore((s) => s.rules);
@@ -216,7 +218,10 @@ export function FilterRuleModal({
// listed (deleted, or its account's folders not loaded) stays selectable
// under its path, so saving doesn't silently retarget the rule.
const folderOptions = (action: FilterAction) => {
const options = mailboxTargets.map((target) => ({ value: target.id, label: target.label }));
const options = mailboxTargets.map((target) => ({
value: target.id,
label: folderOptionLabel(target, mailboxById.get(target.id), t),
}));
const selected = mailboxIdFor(action, mailboxTargets);
if (!options.some((o) => o.value === selected) && (selected || action.value)) {
options.unshift({ value: selected, label: action.value || selected });
@@ -0,0 +1,27 @@
import { localizeMailboxName } from '../../lib/mailbox-label';
import type { MailboxTarget } from '../../lib/sieve/rule-actions';
import type { Mailbox } from '../../api/types';
type Translate = (key: string, fallback?: string) => string;
/**
* A filter rule's folder picker row: the folder's indent and its name as the
* rest of the app shows it. The targets carry the server name, so system
* folders read "Sent Items", "Junk Mail" or "Deleted Items" here and
* "Sent", "Spam" or "Trash" everywhere else. The rule itself keeps the
* server path (fileinto needs it); only the label changes.
*/
export function folderOptionLabel(
target: MailboxTarget,
mailbox: Pick<Mailbox, 'role' | 'name'> | undefined,
t: Translate,
): string {
if (!mailbox?.role) return target.label;
const indent = /^ */.exec(target.label)?.[0] ?? '';
return indent + localizeMailboxName(mailbox.role, mailbox.name, t);
}
/** Mailboxes by the id the targets use (the server id of a shared folder). */
export function mailboxesByTargetId(mailboxes: Mailbox[]): Map<string, Mailbox> {
return new Map(mailboxes.map((m) => [m.originalId ?? m.id, m]));
}