فهرست منبع

Add RTL layout for Arabic and Hebrew in home search and premium paywall.

Drive layout direction from the in-app language so chat, search, and pricing feature lists read correctly in RTL while CV and profile screens stay LTR.

Co-authored-by: Cursor <cursoragent@cursor.com>
AhtashamShahzad1 1 ماه پیش
والد
کامیت
ddc7ac8191

+ 67 - 3
App for Indeed/Controllers/PremiumPlansWindowController.swift

@@ -485,6 +485,7 @@ private final class PremiumPlansViewController: NSViewController {
         let periodLabel: NSTextField
         let periodLabel: NSTextField
         let billingLabel: NSTextField?
         let billingLabel: NSTextField?
         let divider: NSBox
         let divider: NSBox
+        let featuresStack: NSStackView
         let featureLabels: [NSTextField]
         let featureLabels: [NSTextField]
         let featureIcons: [NSImageView]
         let featureIcons: [NSImageView]
         let purchaseButton: PlanPurchaseHoverButton
         let purchaseButton: PlanPurchaseHoverButton
@@ -757,6 +758,7 @@ private final class PremiumPlansViewController: NSViewController {
         ])
         ])
         premiumCloseButton = closeButton
         premiumCloseButton = closeButton
         updatePremiumCloseButtonVisibility()
         updatePremiumCloseButtonVisibility()
+        applyLayoutDirection()
     }
     }
 
 
     private func updatePremiumCloseButtonVisibility() {
     private func updatePremiumCloseButtonVisibility() {
@@ -829,12 +831,13 @@ private final class PremiumPlansViewController: NSViewController {
         let featuresStack = NSStackView(views: featureRows)
         let featuresStack = NSStackView(views: featureRows)
         featuresStack.orientation = .vertical
         featuresStack.orientation = .vertical
         featuresStack.spacing = FeatureListMetrics.rowSpacing
         featuresStack.spacing = FeatureListMetrics.rowSpacing
-        featuresStack.alignment = .leading
+        featuresStack.alignment = .width
         featuresStack.edgeInsets = FeatureListMetrics.edgeInsets
         featuresStack.edgeInsets = FeatureListMetrics.edgeInsets
         featuresStack.setContentCompressionResistancePriority(.required, for: .vertical)
         featuresStack.setContentCompressionResistancePriority(.required, for: .vertical)
         featuresStack.setContentHuggingPriority(.defaultHigh, for: .vertical)
         featuresStack.setContentHuggingPriority(.defaultHigh, for: .vertical)
         for row in featureRows {
         for row in featureRows {
             row.setContentCompressionResistancePriority(.required, for: .vertical)
             row.setContentCompressionResistancePriority(.required, for: .vertical)
+            row.widthAnchor.constraint(equalTo: featuresStack.widthAnchor).isActive = true
         }
         }
         let featuresScroll = makeFeatureListScroll(featuresStack: featuresStack)
         let featuresScroll = makeFeatureListScroll(featuresStack: featuresStack)
 
 
@@ -868,6 +871,7 @@ private final class PremiumPlansViewController: NSViewController {
                 periodLabel: periodLabel,
                 periodLabel: periodLabel,
                 billingLabel: plan.billedLine.isEmpty ? nil : billingLabel,
                 billingLabel: plan.billedLine.isEmpty ? nil : billingLabel,
                 divider: divider,
                 divider: divider,
+                featuresStack: featuresStack,
                 featureLabels: featureLabels,
                 featureLabels: featureLabels,
                 featureIcons: featureIcons,
                 featureIcons: featureIcons,
                 purchaseButton: selectButton,
                 purchaseButton: selectButton,
@@ -951,11 +955,16 @@ private final class PremiumPlansViewController: NSViewController {
         let label = NSTextField(wrappingLabelWithString: text)
         let label = NSTextField(wrappingLabelWithString: text)
         label.font = .systemFont(ofSize: 13, weight: .medium)
         label.font = .systemFont(ofSize: 13, weight: .medium)
         label.textColor = Theme.primaryText
         label.textColor = Theme.primaryText
-        label.alignment = .left
         label.lineBreakMode = .byWordWrapping
         label.lineBreakMode = .byWordWrapping
         label.maximumNumberOfLines = 0
         label.maximumNumberOfLines = 0
         label.cell?.wraps = true
         label.cell?.wraps = true
         label.cell?.isScrollable = false
         label.cell?.isScrollable = false
+        label.attributedStringValue = AppLayoutDirection.attributedString(
+            text,
+            font: label.font ?? .systemFont(ofSize: 13, weight: .medium),
+            color: Theme.primaryText
+        )
+        AppLayoutDirection.configureTextField(label)
         label.setContentCompressionResistancePriority(.required, for: .vertical)
         label.setContentCompressionResistancePriority(.required, for: .vertical)
         label.setContentHuggingPriority(.required, for: .vertical)
         label.setContentHuggingPriority(.required, for: .vertical)
         label.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
         label.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
@@ -967,9 +976,42 @@ private final class PremiumPlansViewController: NSViewController {
         row.alignment = .top
         row.alignment = .top
         row.distribution = .fill
         row.distribution = .fill
         row.translatesAutoresizingMaskIntoConstraints = false
         row.translatesAutoresizingMaskIntoConstraints = false
+        row.userInterfaceLayoutDirection = AppLayoutDirection.interface
         return row
         return row
     }
     }
 
 
+    private func applyLayoutDirection() {
+        let direction = AppLayoutDirection.interface
+        view.userInterfaceLayoutDirection = direction
+
+        for target in pricingCardTargets {
+            target.card.userInterfaceLayoutDirection = direction
+            target.featuresStack.userInterfaceLayoutDirection = direction
+            target.featuresStack.alignment = .width
+
+            if let scroll = target.featuresStack.enclosingFeatureListScrollView() {
+                scroll.userInterfaceLayoutDirection = direction
+                scroll.documentView?.userInterfaceLayoutDirection = direction
+            }
+
+            guard let plan = plans.first(where: { $0.id == target.planId }) else { continue }
+            for (index, label) in target.featureLabels.enumerated() where index < plan.features.count {
+                let font = label.font ?? .systemFont(ofSize: 13, weight: .medium)
+                let color = label.textColor ?? Theme.primaryText
+                label.attributedStringValue = AppLayoutDirection.attributedString(
+                    plan.features[index],
+                    font: font,
+                    color: color
+                )
+                AppLayoutDirection.configureTextField(label)
+            }
+
+            for row in target.featuresStack.arrangedSubviews {
+                row.userInterfaceLayoutDirection = direction
+            }
+        }
+    }
+
     private func inlinePriceInfoLabel(oldPrice: String?, newPrice: String?) -> NSTextField {
     private func inlinePriceInfoLabel(oldPrice: String?, newPrice: String?) -> NSTextField {
         guard let oldPrice, let newPrice else {
         guard let oldPrice, let newPrice else {
             return NSTextField(labelWithString: "")
             return NSTextField(labelWithString: "")
@@ -1314,7 +1356,14 @@ private final class PremiumPlansViewController: NSViewController {
             target.billedPillLabel?.stringValue = plan.billedPill
             target.billedPillLabel?.stringValue = plan.billedPill
             target.billedPillLabel?.isHidden = plan.billedPill.isEmpty
             target.billedPillLabel?.isHidden = plan.billedPill.isEmpty
             for (index, label) in target.featureLabels.enumerated() where index < plan.features.count {
             for (index, label) in target.featureLabels.enumerated() where index < plan.features.count {
-                label.stringValue = plan.features[index]
+                let font = label.font ?? .systemFont(ofSize: 13, weight: .medium)
+                let color = label.textColor ?? Theme.primaryText
+                label.attributedStringValue = AppLayoutDirection.attributedString(
+                    plan.features[index],
+                    font: font,
+                    color: color
+                )
+                AppLayoutDirection.configureTextField(label)
             }
             }
             target.purchaseButton.title = String(format: L("Get %@"), plan.title)
             target.purchaseButton.title = String(format: L("Get %@"), plan.title)
         }
         }
@@ -1350,6 +1399,7 @@ private final class PremiumPlansViewController: NSViewController {
         }
         }
         updateSubscriptionPrimaryFooter()
         updateSubscriptionPrimaryFooter()
         applyStorePricing()
         applyStorePricing()
+        applyLayoutDirection()
     }
     }
 
 
     private func dismissPremiumSheetFromParentIfNeeded() {
     private func dismissPremiumSheetFromParentIfNeeded() {
@@ -1415,6 +1465,7 @@ private final class PremiumPlansViewController: NSViewController {
         }
         }
 
 
         premiumCloseButton?.refreshAppearance(hovered: false)
         premiumCloseButton?.refreshAppearance(hovered: false)
+        applyLayoutDirection()
     }
     }
 
 
     private func planIconTint(planId: String) -> NSColor {
     private func planIconTint(planId: String) -> NSColor {
@@ -1425,3 +1476,16 @@ private final class PremiumPlansViewController: NSViewController {
         }
         }
     }
     }
 }
 }
+
+private extension NSView {
+    func enclosingFeatureListScrollView() -> NSScrollView? {
+        var node: NSView? = superview
+        while let view = node {
+            if let scroll = view as? NSScrollView, scroll.documentView?.subviews.contains(where: { $0 === self }) == true {
+                return scroll
+            }
+            node = view.superview
+        }
+        return nil
+    }
+}

+ 112 - 0
App for Indeed/Services/AppLayoutDirection.swift

@@ -0,0 +1,112 @@
+//
+//  AppLayoutDirection.swift
+//  App for Indeed
+//
+
+import Cocoa
+
+/// Layout direction for the in-app language (Arabic and Hebrew are RTL).
+enum AppLayoutDirection {
+    static var isRightToLeft: Bool {
+        currentAppLanguage().isRightToLeft
+    }
+
+    static var interface: NSUserInterfaceLayoutDirection {
+        isRightToLeft ? .rightToLeft : .leftToRight
+    }
+
+    static var naturalTextAlignment: NSTextAlignment {
+        isRightToLeft ? .right : .natural
+    }
+
+    static var naturalWritingDirection: NSWritingDirection {
+        isRightToLeft ? .rightToLeft : .natural
+    }
+
+    static func naturalParagraphStyle(
+        alignment: NSTextAlignment? = nil,
+        lineBreakMode: NSLineBreakMode = .byWordWrapping
+    ) -> NSParagraphStyle {
+        let paragraph = NSMutableParagraphStyle()
+        paragraph.alignment = alignment ?? naturalTextAlignment
+        paragraph.baseWritingDirection = naturalWritingDirection
+        paragraph.lineBreakMode = lineBreakMode
+        return paragraph
+    }
+
+    static func apply(to view: NSView) {
+        view.userInterfaceLayoutDirection = interface
+    }
+
+    static func applyRecursively(to root: NSView) {
+        var stack: [NSView] = [root]
+        while let view = stack.popLast() {
+            view.userInterfaceLayoutDirection = interface
+            stack.append(contentsOf: view.subviews)
+        }
+    }
+
+    static func configureTextField(_ field: NSTextField) {
+        field.alignment = naturalTextAlignment
+        field.baseWritingDirection = naturalWritingDirection
+        if let cell = field.cell as? NSTextFieldCell {
+            cell.alignment = naturalTextAlignment
+            cell.baseWritingDirection = naturalWritingDirection
+        }
+    }
+
+    static func placeholderAttributes(
+        font: NSFont,
+        color: NSColor,
+        lineBreakMode: NSLineBreakMode = .byTruncatingTail
+    ) -> [NSAttributedString.Key: Any] {
+        [
+            .foregroundColor: color,
+            .font: font,
+            .paragraphStyle: naturalParagraphStyle(alignment: naturalTextAlignment, lineBreakMode: lineBreakMode)
+        ]
+    }
+
+    static func attributedString(
+        _ text: String,
+        font: NSFont,
+        color: NSColor,
+        lineBreakMode: NSLineBreakMode = .byWordWrapping
+    ) -> NSAttributedString {
+        NSAttributedString(string: text, attributes: [
+            .font: font,
+            .foregroundColor: color,
+            .paragraphStyle: naturalParagraphStyle(lineBreakMode: lineBreakMode)
+        ])
+    }
+
+    static func refreshTextFields(in root: NSView) {
+        var stack: [NSView] = [root]
+        while let view = stack.popLast() {
+            if let field = view as? NSTextField {
+                configureTextField(field)
+            }
+            stack.append(contentsOf: view.subviews)
+        }
+    }
+
+    @MainActor
+    static func applyToAllWindows() {
+        for window in NSApp.windows {
+            guard let content = window.contentView else { continue }
+            applyRecursively(to: content)
+            refreshTextFields(in: content)
+        }
+    }
+}
+
+extension AppLanguage {
+    var isRightToLeft: Bool {
+        switch self {
+        case .arabic, .hebrew:
+            return true
+        default:
+            return false
+        }
+    }
+}

+ 127 - 37
App for Indeed/Views/DashboardView.swift

@@ -97,17 +97,40 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
     }
     }
 
 
     /// Multiline `NSTextField` often ignores `alignment` for wrapped runs; explicit paragraph alignment matches the title.
     /// Multiline `NSTextField` often ignores `alignment` for wrapped runs; explicit paragraph alignment matches the title.
-    private static func jobListingDescriptionAttributedString(_ plain: String) -> NSAttributedString {
-        let paragraph = NSMutableParagraphStyle()
-        paragraph.alignment = .left
-        paragraph.lineBreakMode = .byWordWrapping
-        paragraph.baseWritingDirection = .leftToRight
-        let font = NSFont.systemFont(ofSize: 13, weight: .regular)
-        return NSAttributedString(string: plain, attributes: [
-            .font: font,
-            .foregroundColor: Theme.secondaryText,
-            .paragraphStyle: paragraph
-        ])
+    private static func jobListingDescriptionAttributedString(
+        _ plain: String,
+        forceLeftToRight: Bool = false
+    ) -> NSAttributedString {
+        if forceLeftToRight {
+            let paragraph = NSMutableParagraphStyle()
+            paragraph.alignment = .left
+            paragraph.lineBreakMode = .byWordWrapping
+            paragraph.baseWritingDirection = .leftToRight
+            return NSAttributedString(string: plain, attributes: [
+                .font: NSFont.systemFont(ofSize: 13, weight: .regular),
+                .foregroundColor: Theme.secondaryText,
+                .paragraphStyle: paragraph
+            ])
+        }
+        return AppLayoutDirection.attributedString(
+            plain,
+            font: NSFont.systemFont(ofSize: 13, weight: .regular),
+            color: Theme.secondaryText
+        )
+    }
+
+    private static func chatBubbleAttributedString(_ text: String, color: NSColor, font: NSFont) -> NSAttributedString {
+        AppLayoutDirection.attributedString(text, font: font, color: color)
+    }
+
+    private func searchFieldPlaceholder() -> NSAttributedString {
+        NSAttributedString(
+            string: L("Ask for roles, skills, salary, or job descriptions..."),
+            attributes: AppLayoutDirection.placeholderAttributes(
+                font: .systemFont(ofSize: 14, weight: .regular),
+                color: Theme.secondaryText
+            )
+        )
     }
     }
 
 
     /// Horizontal row for sidebar + main; plain view + constraints keep both panels top/bottom aligned (stack view height alignment was inconsistent).
     /// Horizontal row for sidebar + main; plain view + constraints keep both panels top/bottom aligned (stack view height alignment was inconsistent).
@@ -271,13 +294,7 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
         savedJobsPageTitleLabel.stringValue = L("Saved Jobs")
         savedJobsPageTitleLabel.stringValue = L("Saved Jobs")
         nonHomeSubtitleLabel.stringValue = L("This area is not available in the preview build. Use Home to search jobs.")
         nonHomeSubtitleLabel.stringValue = L("This area is not available in the preview build. Use Home to search jobs.")
 
 
-        jobKeywordsField.placeholderAttributedString = NSAttributedString(
-            string: L("Ask for roles, skills, salary, or job descriptions..."),
-            attributes: [
-                .foregroundColor: Theme.secondaryText,
-                .font: NSFont.systemFont(ofSize: 14, weight: .regular)
-            ]
-        )
+        jobKeywordsField.placeholderAttributedString = searchFieldPlaceholder()
         jobSearchIcon.setAccessibilityLabel(L("Ask AI"))
         jobSearchIcon.setAccessibilityLabel(L("Ask AI"))
         findJobsButton.setAccessibilityLabel(L("Send"))
         findJobsButton.setAccessibilityLabel(L("Send"))
 
 
@@ -295,6 +312,7 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
         rebuildFeatureShortcutCards()
         rebuildFeatureShortcutCards()
         trailingLoadMoreJobsButton?.title = L("Show more jobs")
         trailingLoadMoreJobsButton?.title = L("Show more jobs")
         refreshWelcomeChatMessageForCurrentLanguage()
         refreshWelcomeChatMessageForCurrentLanguage()
+        applyHomeLayoutDirection()
         AppMenuLocalizer.apply()
         AppMenuLocalizer.apply()
     }
     }
 
 
@@ -424,13 +442,7 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
         searchCard.layer?.backgroundColor = (searchHovering ? Theme.neutralHoverFill : Theme.cardBackground).cgColor
         searchCard.layer?.backgroundColor = (searchHovering ? Theme.neutralHoverFill : Theme.cardBackground).cgColor
         searchCard.layer?.borderColor = (searchHovering ? Theme.searchBarBorderHover : Theme.searchBarBorder).cgColor
         searchCard.layer?.borderColor = (searchHovering ? Theme.searchBarBorderHover : Theme.searchBarBorder).cgColor
         jobKeywordsField.textColor = Theme.primaryText
         jobKeywordsField.textColor = Theme.primaryText
-        jobKeywordsField.placeholderAttributedString = NSAttributedString(
-            string: L("Ask for roles, skills, salary, or job descriptions..."),
-            attributes: [
-                .foregroundColor: Theme.secondaryText,
-                .font: NSFont.systemFont(ofSize: 14, weight: .regular)
-            ]
-        )
+        jobKeywordsField.placeholderAttributedString = searchFieldPlaceholder()
         jobSearchIcon.contentTintColor = Theme.brandBlue
         jobSearchIcon.contentTintColor = Theme.brandBlue
         let ctaHovering = findJobsButton.isHovering
         let ctaHovering = findJobsButton.isHovering
         findJobsCTAPill.layer?.backgroundColor = (ctaHovering ? Theme.brandBlueHover : Theme.brandBlue).cgColor
         findJobsCTAPill.layer?.backgroundColor = (ctaHovering ? Theme.brandBlueHover : Theme.brandBlue).cgColor
@@ -457,11 +469,55 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
         configureSidebar()
         configureSidebar()
         reloadSavedJobsListings()
         reloadSavedJobsListings()
         rebuildChatUI()
         rebuildChatUI()
+        applyHomeLayoutDirection()
         applyProSubscriptionToSidebar()
         applyProSubscriptionToSidebar()
         updateFreeJobSearchQuotaLabel()
         updateFreeJobSearchQuotaLabel()
         needsLayout = true
         needsLayout = true
     }
     }
 
 
+    /// Home search, chat, and shell chrome follow the in-app language; CV/profile subtrees stay LTR.
+    private func applyHomeLayoutDirection() {
+        let direction = AppLayoutDirection.interface
+
+        userInterfaceLayoutDirection = direction
+        panelsRow.userInterfaceLayoutDirection = direction
+        chromeContainer.userInterfaceLayoutDirection = direction
+        sidebar.userInterfaceLayoutDirection = direction
+        mainOverlay.userInterfaceLayoutDirection = direction
+        searchBarColumn.userInterfaceLayoutDirection = direction
+        searchCard.userInterfaceLayoutDirection = direction
+        chatScrollView.userInterfaceLayoutDirection = direction
+        chatDocumentView.userInterfaceLayoutDirection = direction
+        chatStack.userInterfaceLayoutDirection = direction
+        featureCardsRow.userInterfaceLayoutDirection = direction
+        welcomeHeroHost.userInterfaceLayoutDirection = direction
+
+        AppLayoutDirection.configureTextField(jobKeywordsField)
+        jobKeywordsField.placeholderAttributedString = searchFieldPlaceholder()
+
+        greetingLabel.baseWritingDirection = AppLayoutDirection.naturalWritingDirection
+        subtitleLabel.baseWritingDirection = AppLayoutDirection.naturalWritingDirection
+
+        for card in featureCardsRow.arrangedSubviews {
+            (card as? FeatureShortcutCardView)?.applyLayoutDirection()
+        }
+
+        refreshChatBubbleTextDirection()
+        nonHomeHost.userInterfaceLayoutDirection = .leftToRight
+    }
+
+    private func refreshChatBubbleTextDirection() {
+        func walk(_ view: NSView) {
+            if let label = view as? ChatBubbleLabel {
+                let color = label.textColor ?? Theme.primaryText
+                let font = label.font ?? .systemFont(ofSize: 13.5, weight: .regular)
+                label.attributedStringValue = Self.chatBubbleAttributedString(label.stringValue, color: color, font: font)
+            }
+            for sub in view.subviews { walk(sub) }
+        }
+        walk(chatStack)
+    }
+
     private func refreshSettingsPageAppearance(in root: NSView) {
     private func refreshSettingsPageAppearance(in root: NSView) {
         for view in root.subviewsRecursive() {
         for view in root.subviewsRecursive() {
             guard let rawID = view.identifier?.rawValue else { continue }
             guard let rawID = view.identifier?.rawValue else { continue }
@@ -728,6 +784,7 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
         ])
         ])
         registerSubscriptionObserverOnce()
         registerSubscriptionObserverOnce()
         refreshLocalizedStrings()
         refreshLocalizedStrings()
+        applyHomeLayoutDirection()
     }
     }
 
 
     private func registerSubscriptionObserverOnce() {
     private func registerSubscriptionObserverOnce() {
@@ -863,6 +920,7 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
                 target: self,
                 target: self,
                 action: spec.action
                 action: spec.action
             )
             )
+            card.applyLayoutDirection()
             featureCardsRow.addArrangedSubview(card)
             featureCardsRow.addArrangedSubview(card)
         }
         }
     }
     }
@@ -1057,7 +1115,7 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
         titleField.textColor = Theme.brandBlue
         titleField.textColor = Theme.brandBlue
         titleField.maximumNumberOfLines = 2
         titleField.maximumNumberOfLines = 2
         titleField.lineBreakMode = .byWordWrapping
         titleField.lineBreakMode = .byWordWrapping
-        titleField.alignment = .left
+        titleField.alignment = context == .homeSearchResults ? AppLayoutDirection.naturalTextAlignment : .left
         titleField.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
         titleField.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
         titleField.translatesAutoresizingMaskIntoConstraints = false
         titleField.translatesAutoresizingMaskIntoConstraints = false
 
 
@@ -1085,11 +1143,16 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
         descriptionField.textColor = Theme.secondaryText
         descriptionField.textColor = Theme.secondaryText
         descriptionField.maximumNumberOfLines = 2
         descriptionField.maximumNumberOfLines = 2
         descriptionField.lineBreakMode = .byWordWrapping
         descriptionField.lineBreakMode = .byWordWrapping
-        descriptionField.alignment = .left
-        descriptionField.baseWritingDirection = .leftToRight
-        descriptionField.attributedStringValue = Self.jobListingDescriptionAttributedString(job.description)
+        descriptionField.alignment = context == .homeSearchResults ? AppLayoutDirection.naturalTextAlignment : .left
+        descriptionField.baseWritingDirection = context == .homeSearchResults
+            ? AppLayoutDirection.naturalWritingDirection
+            : .leftToRight
+        descriptionField.attributedStringValue = Self.jobListingDescriptionAttributedString(
+            job.description,
+            forceLeftToRight: context == .savedJobsPage
+        )
         if let cell = descriptionField.cell as? NSTextFieldCell {
         if let cell = descriptionField.cell as? NSTextFieldCell {
-            cell.alignment = .left
+            cell.alignment = descriptionField.alignment
             cell.wraps = true
             cell.wraps = true
         }
         }
         descriptionField.setContentHuggingPriority(.defaultLow, for: .horizontal)
         descriptionField.setContentHuggingPriority(.defaultLow, for: .horizontal)
@@ -1451,10 +1514,10 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
             field.delegate = self
             field.delegate = self
             field.placeholderAttributedString = NSAttributedString(
             field.placeholderAttributedString = NSAttributedString(
                 string: placeholder,
                 string: placeholder,
-                attributes: [
-                    .foregroundColor: Theme.secondaryText,
-                    .font: NSFont.systemFont(ofSize: 14, weight: .regular)
-                ]
+                attributes: AppLayoutDirection.placeholderAttributes(
+                    font: .systemFont(ofSize: 14, weight: .regular),
+                    color: Theme.secondaryText
+                )
             )
             )
             field.cell?.usesSingleLineMode = true
             field.cell?.usesSingleLineMode = true
             field.cell?.wraps = false
             field.cell?.wraps = false
@@ -1469,6 +1532,7 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
         jobSearchIcon.contentTintColor = Theme.brandBlue
         jobSearchIcon.contentTintColor = Theme.brandBlue
 
 
         configureField(jobKeywordsField, placeholder: L("Ask for roles, skills, salary, or job descriptions..."))
         configureField(jobKeywordsField, placeholder: L("Ask for roles, skills, salary, or job descriptions..."))
+        AppLayoutDirection.configureTextField(jobKeywordsField)
 
 
         let ctaHeight: CGFloat = 42
         let ctaHeight: CGFloat = 42
         let ctaCorner = ctaHeight / 2
         let ctaCorner = ctaHeight / 2
@@ -2706,7 +2770,7 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
         let nameLabel = NSTextField(labelWithString: AppMarketingLinks.appDisplayName)
         let nameLabel = NSTextField(labelWithString: AppMarketingLinks.appDisplayName)
         nameLabel.font = .systemFont(ofSize: 11, weight: .semibold)
         nameLabel.font = .systemFont(ofSize: 11, weight: .semibold)
         nameLabel.textColor = Theme.secondaryText
         nameLabel.textColor = Theme.secondaryText
-        nameLabel.alignment = .left
+        nameLabel.alignment = AppLayoutDirection.naturalTextAlignment
         nameLabel.translatesAutoresizingMaskIntoConstraints = false
         nameLabel.translatesAutoresizingMaskIntoConstraints = false
 
 
         let bubble = makeChatBubbleContainer(text: text, isUser: false)
         let bubble = makeChatBubbleContainer(text: text, isUser: false)
@@ -2733,7 +2797,6 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
 
 
         host.addSubview(avatar)
         host.addSubview(avatar)
         host.addSubview(column)
         host.addSubview(column)
-        host.userInterfaceLayoutDirection = .leftToRight
         NSLayoutConstraint.activate([
         NSLayoutConstraint.activate([
             avatar.leadingAnchor.constraint(equalTo: host.leadingAnchor),
             avatar.leadingAnchor.constraint(equalTo: host.leadingAnchor),
             avatar.topAnchor.constraint(equalTo: host.topAnchor),
             avatar.topAnchor.constraint(equalTo: host.topAnchor),
@@ -2770,7 +2833,13 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
         label.textColor = isUser ? .white : Theme.primaryText
         label.textColor = isUser ? .white : Theme.primaryText
         label.maximumNumberOfLines = 0
         label.maximumNumberOfLines = 0
         label.lineBreakMode = .byWordWrapping
         label.lineBreakMode = .byWordWrapping
-        label.alignment = .left
+        label.alignment = AppLayoutDirection.naturalTextAlignment
+        label.baseWritingDirection = AppLayoutDirection.naturalWritingDirection
+        label.attributedStringValue = Self.chatBubbleAttributedString(
+            text,
+            color: label.textColor ?? Theme.primaryText,
+            font: label.font ?? .systemFont(ofSize: 13.5, weight: .regular)
+        )
         label.translatesAutoresizingMaskIntoConstraints = false
         label.translatesAutoresizingMaskIntoConstraints = false
         label.setContentHuggingPriority(.defaultLow, for: .horizontal)
         label.setContentHuggingPriority(.defaultLow, for: .horizontal)
         label.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
         label.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
@@ -3925,6 +3994,27 @@ private final class FeatureShortcutCardView: NSView {
         subtitleField?.textColor = AppDashboardTheme.featureSecondaryText
         subtitleField?.textColor = AppDashboardTheme.featureSecondaryText
         chevronView?.contentTintColor = accent
         chevronView?.contentTintColor = accent
         updateSelectionAppearance()
         updateSelectionAppearance()
+        applyLayoutDirection()
+    }
+
+    func applyLayoutDirection() {
+        let rtl = AppLayoutDirection.isRightToLeft
+        userInterfaceLayoutDirection = AppLayoutDirection.interface
+        let align: NSTextAlignment = rtl ? .right : .left
+        titleField?.alignment = align
+        subtitleField?.alignment = align
+        if let cell = titleField?.cell as? NSTextFieldCell {
+            cell.alignment = align
+            cell.baseWritingDirection = AppLayoutDirection.naturalWritingDirection
+        }
+        if let cell = subtitleField?.cell as? NSTextFieldCell {
+            cell.alignment = align
+            cell.baseWritingDirection = AppLayoutDirection.naturalWritingDirection
+        }
+        chevronView?.image = NSImage(
+            systemSymbolName: rtl ? "arrow.left" : "arrow.right",
+            accessibilityDescription: nil
+        )
     }
     }
 
 
     override func viewDidChangeEffectiveAppearance() {
     override func viewDidChangeEffectiveAppearance() {