From d17277fab2b63452b40adb8a7462f80a2aeaaeda Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Wed, 28 Oct 2026 05:01:37 +0100 Subject: [PATCH] fix: point Mail's arrows, indents and panels the right way in right-to-left languages, and keep English subjects and sizes readable there --- components/email/__tests__/mail-rtl.test.ts | 38 +++++++++++++++++++ components/email/email-composer.tsx | 6 +-- components/email/email-viewer.tsx | 30 +++++++-------- components/email/plugin-search-results.tsx | 2 +- components/email/thread-list-item.tsx | 8 ++-- components/layout/sidebar.tsx | 2 +- components/mail/mail-app.tsx | 4 +- .../templates/template-manager-modal.tsx | 2 +- components/templates/template-picker.tsx | 2 +- 9 files changed, 66 insertions(+), 28 deletions(-) create mode 100644 components/email/__tests__/mail-rtl.test.ts diff --git a/components/email/__tests__/mail-rtl.test.ts b/components/email/__tests__/mail-rtl.test.ts new file mode 100644 index 000000000..d3a07fb0f --- /dev/null +++ b/components/email/__tests__/mail-rtl.test.ts @@ -0,0 +1,38 @@ +// @vitest-environment node +import fs from 'fs'; +import path from 'path'; +import { describe, expect, it } from 'vitest'; + +// R11: directional icons flip only through the global list in globals.css; a +// local rtl: flip on top of it turns them back the wrong way (the phone +// search's back arrow pointed into the content in Arabic). Offsets use +// logical properties so subfolders indent on the reading side. + +const root = path.resolve(__dirname, '../../..'); +const mailFiles = [ + 'components/mail/mail-app.tsx', + 'components/email/email-viewer.tsx', + 'components/email/email-composer.tsx', + 'components/email/plugin-search-results.tsx', + 'components/email/thread-list-item.tsx', + 'components/layout/sidebar.tsx', + 'components/templates/template-picker.tsx', + 'components/templates/template-manager-modal.tsx', +]; +const read = (file: string) => fs.readFileSync(path.join(root, file), 'utf-8'); + +describe('Mail in right-to-left languages', () => { + it.each(mailFiles)('%s adds no local flip to directional icons', (file) => { + expect(read(file)).not.toMatch(/rtl:(-scale-x-100|rotate-180)/); + }); + + it.each(mailFiles)('%s uses no physical left offsets', (file) => { + const code = read(file).replace(/\/\/.*$/gm, ''); + expect(code).not.toMatch(/paddingLeft:|\bleft-[03]\b|\btext-left\b|-right-1\b/); + }); + + it('isolates subjects in the list, so an English subject reads right in Arabic', () => { + const source = read('components/email/thread-list-item.tsx'); + expect(source.match(/\{subjectSnippet/g)).toHaveLength(4); + }); +}); diff --git a/components/email/email-composer.tsx b/components/email/email-composer.tsx index 682a1cb62..33a222135 100644 --- a/components/email/email-composer.tsx +++ b/components/email/email-composer.tsx @@ -4385,11 +4385,11 @@ export function EmailComposer({ // Real byte progress from the transport; see // ComposerAttachment.progress for who reports it.
) : ( -
+
)}
)} @@ -5007,7 +5007,7 @@ const AutocompleteDropdown = React.forwardRef 0 && "border-t border-border" )} onMouseDown={(e) => { diff --git a/components/email/email-viewer.tsx b/components/email/email-viewer.tsx index cefae0293..142267f94 100644 --- a/components/email/email-viewer.tsx +++ b/components/email/email-viewer.tsx @@ -4509,11 +4509,11 @@ export function EmailViewer({ inert={!moreMenuOpen} className={cn( "bg-background border-s border-border z-[70]", - isPaneScoped ? "absolute inset-y-0 right-0 w-72" : "fixed inset-y-0 right-0 w-72 sm:hidden", + isPaneScoped ? "absolute inset-y-0 end-0 w-72" : "fixed inset-y-0 end-0 w-72 sm:hidden", "transform", moreMenuSlideEnabled && "transition-transform duration-300 ease-in-out", "flex flex-col", - moreMenuOpen ? "translate-x-0" : "translate-x-full" + moreMenuOpen ? "translate-x-0" : "translate-x-full rtl:-translate-x-full" )}> {/* The fixed panel spans the full viewport height, so in the iOS PWA its header would sit under the status bar without the safe-area @@ -4748,7 +4748,7 @@ export function EmailViewer({ )}
-

@@ -4831,7 +4831,7 @@ export function EmailViewer({ {email.size > 0 && (
- {formatFileSize(email.size)} + {formatFileSize(email.size)}
)}

@@ -5079,7 +5079,7 @@ export function EmailViewer({ {getAttachmentDisplayName(attachment.name, attachment.type)} - {formatFileSize(attachment.size)} + {formatFileSize(attachment.size)}
- {formatFileSize(attachment.size)} + {formatFileSize(attachment.size)}
@@ -5593,7 +5593,7 @@ export function EmailViewer({ {email.subject || {t('details.no_subject')}} )} - {formatFileSize(email.size)} + {formatFileSize(email.size)} {topMimeType && ( ยท {topMimeType} )} @@ -5823,7 +5823,7 @@ export function EmailViewer({ ); @@ -5903,7 +5903,7 @@ export function EmailViewer({ {getAttachmentDisplayName(attachment.name, attachment.type)} - {formatFileSize(attachment.size)} + {formatFileSize(attachment.size)}
- {formatFileSize(attachment.size)} + {formatFileSize(attachment.size)}
diff --git a/components/email/thread-list-item.tsx b/components/email/thread-list-item.tsx index c7e1cc1a2..d85dcdef9 100644 --- a/components/email/thread-list-item.tsx +++ b/components/email/thread-list-item.tsx @@ -707,7 +707,7 @@ const SingleEmailItem = React.forwardRef( 'min-w-0 truncate', isUnread ? (mvRow ? 'font-bold text-foreground' : 'font-semibold text-foreground') : 'text-foreground/90' )}> - {subjectSnippet ? : (email.subject || '(no subject)')} + {subjectSnippet ? : (email.subject || '(no subject)')} {(previewSnippet || inlinePreview) && ( @@ -852,7 +852,7 @@ const SingleEmailItem = React.forwardRef( ? "font-semibold text-foreground" : "font-normal text-foreground/90" )}> - {subjectSnippet ? : (email.subject || "(no subject)")} + {subjectSnippet ? : (email.subject || "(no subject)")}
@@ -1362,7 +1362,7 @@ export const ThreadListItem = React.forwardRef - {subjectSnippet ? : (latestEmail.subject || '(no subject)')} + {subjectSnippet ? : (latestEmail.subject || '(no subject)')} {(previewSnippet || inlinePreview) && ( @@ -1514,7 +1514,7 @@ export const ThreadListItem = React.forwardRef - {subjectSnippet ? : (latestEmail.subject || "(no subject)")} + {subjectSnippet ? : (latestEmail.subject || "(no subject)")}
diff --git a/components/layout/sidebar.tsx b/components/layout/sidebar.tsx index c3441201a..854ae24a0 100644 --- a/components/layout/sidebar.tsx +++ b/components/layout/sidebar.tsx @@ -413,7 +413,7 @@ function SidebarRow({ {!isCollapsed && (
{hasChildren && onExpandToggle ? ( {t('sidebar.search_placeholder')}
@@ -4963,7 +4963,7 @@ export function MailApp({ linkSegments: routeSegments }: MailAppProps = {}) { > {!isAdvancedSearchOpen && activeFilterCount(searchFilters) > 0 && ( - + {activeFilterCount(searchFilters)} )} diff --git a/components/templates/template-manager-modal.tsx b/components/templates/template-manager-modal.tsx index a12276691..300449ce8 100644 --- a/components/templates/template-manager-modal.tsx +++ b/components/templates/template-manager-modal.tsx @@ -99,7 +99,7 @@ export function TemplateManagerModal({ isOpen, onClose }: TemplateManagerModalPr {!isCreating && !editingId && (
- + setSearchQuery(e.target.value)} diff --git a/components/templates/template-picker.tsx b/components/templates/template-picker.tsx index 533d858b2..d0381773a 100644 --- a/components/templates/template-picker.tsx +++ b/components/templates/template-picker.tsx @@ -150,7 +150,7 @@ export function TemplatePicker({ isOpen, onClose, onSelect }: TemplatePickerProp
- + setSearchQuery(e.target.value)}