Bläddra i källkod

Add RTL layout for Arabic and Hebrew CV templates and fix ar/he strings.

Résumé previews and filled documents now mirror correctly in RTL languages, with improved template names and demo copy in Arabic and Hebrew.

Co-authored-by: Cursor <cursoragent@cursor.com>
AhtashamShahzad1 1 månad sedan
förälder
incheckning
c7f0927b7a

+ 53 - 0
App for Indeed/Services/CVResumeAppearance.swift

@@ -62,4 +62,57 @@ enum CVResumeAppearance {
     static func sectionHeadingColor(for template: CVTemplate) -> NSColor {
         accentColor(for: template)
     }
+
+    // MARK: - Document layout (RTL for Arabic / Hebrew)
+
+    static var isRightToLeft: Bool {
+        AppLayoutDirection.isRightToLeft
+    }
+
+    static var documentInterfaceLayoutDirection: NSUserInterfaceLayoutDirection {
+        isRightToLeft ? .rightToLeft : .leftToRight
+    }
+
+    static var documentTextAlignment: NSTextAlignment {
+        isRightToLeft ? .right : .left
+    }
+
+    static var documentWritingDirection: NSWritingDirection {
+        isRightToLeft ? .rightToLeft : .leftToRight
+    }
+
+    static func resolvedDocumentAlignment(_ alignment: NSTextAlignment) -> NSTextAlignment {
+        alignment == .left ? documentTextAlignment : alignment
+    }
+
+    static func applyDocumentLayout(to view: NSView) {
+        view.userInterfaceLayoutDirection = documentInterfaceLayoutDirection
+        for child in view.subviews {
+            applyDocumentLayout(to: child)
+        }
+    }
+
+    static func documentParagraphStyle(
+        alignment: NSTextAlignment,
+        lineBreakMode: NSLineBreakMode = .byWordWrapping
+    ) -> NSParagraphStyle {
+        let paragraph = NSMutableParagraphStyle()
+        paragraph.alignment = resolvedDocumentAlignment(alignment)
+        paragraph.baseWritingDirection = documentWritingDirection
+        paragraph.lineBreakMode = lineBreakMode
+        return paragraph
+    }
+
+    static func applyDocumentTextLayout(to field: NSTextField, alignment: NSTextAlignment? = nil) {
+        let sourceAlignment = alignment ?? field.alignment
+        let resolved = resolvedDocumentAlignment(sourceAlignment)
+        field.alignment = resolved
+        field.baseWritingDirection = documentWritingDirection
+        let lineBreak: NSLineBreakMode = field.maximumNumberOfLines == 0 ? .byWordWrapping : .byTruncatingTail
+        field.attributedStringValue = NSAttributedString(string: field.stringValue, attributes: [
+            .font: field.font ?? .systemFont(ofSize: 12),
+            .foregroundColor: field.textColor ?? .labelColor,
+            .paragraphStyle: documentParagraphStyle(alignment: resolved, lineBreakMode: lineBreak)
+        ])
+    }
 }

+ 12 - 35
App for Indeed/Views/CVProfileDocumentView.swift

@@ -208,7 +208,7 @@ final class CVProfileDocumentView: NSView {
         translatesAutoresizingMaskIntoConstraints = false
         wantsLayer = true
         layer?.backgroundColor = NSColor.clear.cgColor
-        userInterfaceLayoutDirection = .leftToRight
+        userInterfaceLayoutDirection = CVResumeAppearance.documentInterfaceLayoutDirection
         setContentHuggingPriority(.defaultLow, for: .horizontal)
         installCardContent()
     }
@@ -227,7 +227,7 @@ final class CVProfileDocumentView: NSView {
 
         let root = buildRoot()
         root.translatesAutoresizingMaskIntoConstraints = false
-        enforceLeftToRightLayout(on: root)
+        CVResumeAppearance.applyDocumentLayout(to: root)
         card.addSubview(root)
 
         addSubview(card)
@@ -590,7 +590,7 @@ final class CVProfileDocumentView: NSView {
             tag.layer?.cornerRadius = 6
             tag.translatesAutoresizingMaskIntoConstraints = false
             let lab = label(s, font: .systemFont(ofSize: 11, weight: .semibold), color: theme.blended(withFraction: 0.35, of: style.ink) ?? style.ink, maxLines: 1)
-            lab.alignment = .center
+            CVResumeAppearance.applyDocumentTextLayout(to: lab, alignment: .center)
             lab.translatesAutoresizingMaskIntoConstraints = false
             tag.addSubview(lab)
             NSLayoutConstraint.activate([
@@ -795,9 +795,7 @@ final class CVProfileDocumentView: NSView {
         let role = label(roleText, font: style.roleFont, color: style.muted, maxLines: 2)
         let contact = label(contactText, font: style.contactFont, color: style.muted.withAlphaComponent(0.9), maxLines: 3)
         if centeredHead {
-            name.alignment = .center
-            role.alignment = .center
-            contact.alignment = .center
+            applyCenteredTextLayout(name, role, contact)
         }
 
         let rule = executiveHeaderRule(wide: variant % 3 == 0)
@@ -963,9 +961,7 @@ final class CVProfileDocumentView: NSView {
             head.addArrangedSubview(initialsBadge(for: nameText))
         }
         if template.headline == .centered {
-            name.alignment = .center
-            role.alignment = .center
-            contact.alignment = .center
+            applyCenteredTextLayout(name, role, contact)
         }
         head.addArrangedSubview(name)
         head.addArrangedSubview(role)
@@ -1187,9 +1183,7 @@ final class CVProfileDocumentView: NSView {
         switch template.headline {
         case .centered:
             textCol.alignment = .centerX
-            name.alignment = .center
-            role.alignment = .center
-            contact.alignment = .center
+            applyCenteredTextLayout(name, role, contact)
             let accent = headlineAccent()
             let stack = NSStackView(views: [textCol, accent])
             stack.orientation = .vertical
@@ -1198,9 +1192,7 @@ final class CVProfileDocumentView: NSView {
             return stack
         case .avatarStacked:
             textCol.alignment = .centerX
-            name.alignment = .center
-            role.alignment = .center
-            contact.alignment = .center
+            applyCenteredTextLayout(name, role, contact)
             let accent = headlineAccent()
             let avatar = initialsBadge(for: nameText)
             let stack = NSStackView(views: [avatar, textCol, accent])
@@ -1232,6 +1224,7 @@ final class CVProfileDocumentView: NSView {
         t.font = .systemFont(ofSize: 13, weight: .bold)
         t.textColor = template.themeColor
         t.alignment = .center
+        CVResumeAppearance.applyDocumentTextLayout(to: t, alignment: .center)
         t.translatesAutoresizingMaskIntoConstraints = false
         let wrap = NSView()
         wrap.translatesAutoresizingMaskIntoConstraints = false
@@ -1566,11 +1559,9 @@ final class CVProfileDocumentView: NSView {
         return label(s, font: style.sectionFont, color: style.sectionInk, maxLines: 1)
     }
 
-    /// Résumé templates are designed LTR (columns, rails, sidebars). Keep layout LTR even when UI copy is Arabic.
-    private func enforceLeftToRightLayout(on view: NSView) {
-        view.userInterfaceLayoutDirection = .leftToRight
-        for child in view.subviews {
-            enforceLeftToRightLayout(on: child)
+    private func applyCenteredTextLayout(_ fields: NSTextField...) {
+        for field in fields {
+            CVResumeAppearance.applyDocumentTextLayout(to: field, alignment: .center)
         }
     }
 
@@ -1587,24 +1578,10 @@ final class CVProfileDocumentView: NSView {
         t.translatesAutoresizingMaskIntoConstraints = false
         t.font = font
         t.textColor = color
-        t.alignment = .left
-        applyLeftToRightTextLayout(to: t)
+        CVResumeAppearance.applyDocumentTextLayout(to: t)
         return t
     }
 
-    /// Mixed Arabic/English body copy still measures and wraps in column width when base direction is LTR.
-    private func applyLeftToRightTextLayout(to field: NSTextField) {
-        let paragraph = NSMutableParagraphStyle()
-        paragraph.alignment = field.alignment
-        paragraph.baseWritingDirection = .leftToRight
-        paragraph.lineBreakMode = field.maximumNumberOfLines == 0 ? .byWordWrapping : .byTruncatingTail
-        field.attributedStringValue = NSAttributedString(string: field.stringValue, attributes: [
-            .font: field.font ?? .systemFont(ofSize: 12),
-            .foregroundColor: field.textColor ?? .labelColor,
-            .paragraphStyle: paragraph
-        ])
-    }
-
     private func displayable(_ value: String, placeholder: String) -> String {
         let t = value.trimmingCharacters(in: .whitespacesAndNewlines)
         return t.isEmpty ? placeholder : t

+ 7 - 15
App for Indeed/Views/CVTemplateMiniPreview.swift

@@ -76,7 +76,7 @@ final class CVTemplatePreviewView: NSView {
         super.init(frame: .zero)
         wantsLayer = true
         translatesAutoresizingMaskIntoConstraints = false
-        userInterfaceLayoutDirection = .leftToRight
+        userInterfaceLayoutDirection = CVResumeAppearance.documentInterfaceLayoutDirection
         configurePaper()
     }
 
@@ -106,7 +106,7 @@ final class CVTemplatePreviewView: NSView {
 
         let root = buildResumeRoot()
         root.translatesAutoresizingMaskIntoConstraints = false
-        enforceLeftToRightLayout(on: root)
+        CVResumeAppearance.applyDocumentLayout(to: root)
         paper.addSubview(root)
         NSLayoutConstraint.activate([
             root.leadingAnchor.constraint(equalTo: paper.leadingAnchor, constant: 7),
@@ -1009,13 +1009,6 @@ final class CVTemplatePreviewView: NSView {
         return row
     }
 
-    private func enforceLeftToRightLayout(on view: NSView) {
-        view.userInterfaceLayoutDirection = .leftToRight
-        for child in view.subviews {
-            enforceLeftToRightLayout(on: child)
-        }
-    }
-
     private func makeLabel(
         _ text: String,
         font: NSFont,
@@ -1033,20 +1026,19 @@ final class CVTemplatePreviewView: NSView {
         tf.translatesAutoresizingMaskIntoConstraints = false
         tf.font = font
         tf.textColor = color
-        tf.alignment = alignment
+        let resolvedAlignment = CVResumeAppearance.resolvedDocumentAlignment(alignment)
+        tf.alignment = resolvedAlignment
+        tf.baseWritingDirection = CVResumeAppearance.documentWritingDirection
         tf.isEditable = false
         tf.isSelectable = false
         tf.drawsBackground = false
         tf.isBordered = false
         tf.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
-        let paragraph = NSMutableParagraphStyle()
-        paragraph.alignment = alignment
-        paragraph.baseWritingDirection = .leftToRight
-        paragraph.lineBreakMode = maxLines == 1 ? .byTruncatingTail : .byWordWrapping
+        let lineBreak: NSLineBreakMode = maxLines == 1 ? .byTruncatingTail : .byWordWrapping
         tf.attributedStringValue = NSAttributedString(string: text, attributes: [
             .font: font,
             .foregroundColor: color,
-            .paragraphStyle: paragraph
+            .paragraphStyle: CVResumeAppearance.documentParagraphStyle(alignment: alignment, lineBreakMode: lineBreak)
         ])
         return tf
     }

+ 41 - 41
App for Indeed/ar.lproj/Localizable.strings

@@ -267,56 +267,56 @@
 "Clear Path" = "مسار واضح";
 "Pinstripe" = "خطوط رفيعة";
 "Briefing" = "إحاطة";
-"Quorum" = "نصاب";
-"Docket" = "جدول الأعمال";
-"Conduit" = "قناة";
-"Principal" = "رئيسي";
-"Charter" = "ميثاق";
-"Vertex" = "قمة";
-"Linea" = "خط";
-"Prism" = "منشور";
-"Circuit" = "دائرة";
-"North" = "شمال";
-"Axis" = "محور";
-"Marigold" = "القطيفة";
-"Ember" = "جمرة";
-"Lattice" = "شبكة";
-"Bloom" = "ازدهار";
-"Studio" = "استوديو";
-"Kite" = "طائرة ورقية";
-"Regent" = "وصي";
-"Monarch" = "ملك";
-"Sterling" = "استرليني";
-"Summit" = "قمة";
-"Estate" = "عقار";
-"Chairman" = "رئيس مجلس الإدارة";
-"Blue Ocean" = "المحيط الأزرق";
+"Quorum" = "كوروم";
+"Docket" = "دوكيت";
+"Conduit" = "كوندويت";
+"Principal" = "برينسيبل";
+"Charter" = "تشارتر";
+"Vertex" = "فيرتكس";
+"Linea" = "لينيا";
+"Prism" = "بريزم";
+"Circuit" = "سيركيت";
+"North" = "نورث";
+"Axis" = "أكسيس";
+"Marigold" = "ماريغولد";
+"Ember" = "إمبر";
+"Lattice" = "لاتيس";
+"Bloom" = "بلوم";
+"Studio" = "ستوديو";
+"Kite" = "كايت";
+"Regent" = "ريجنت";
+"Monarch" = "مونارك";
+"Sterling" = "ستيرلينغ";
+"Summit" = "ساميت";
+"Estate" = "إستيت";
+"Chairman" = "تشيرمان";
+"Blue Ocean" = "بلو أوشن";
 
 // MARK: - محتوى معاينة السيرة الذاتية التجريبي
-"Sarah Johnson" = "سارة جونسون";
+"Sarah Johnson" = "سارة آل راشد";
 "Google" = "جوجل";
 "Figma" = "Figma";
 "SQL" = "SQL";
-"sarah.johnson@email.com" = "sarah.johnson@email.com";
-"(415) 555-0198" = "(415) 555-0198";
-"Senior Product Manager" = "مدير منتج أول";
-"Group PM, Consumer Growth & Activation" = "مدير منتج جماعي، نمو المستهلكين وتفعيلهم";
-"Google · Mountain View, CA · 2019 – Present" = "جوجل · ماونتن فيو، كاليفورنيا · 2019 – الحاضر";
-"Stanford University" = "جامعة ستanford";
+"sarah.johnson@email.com" = "sara.alrashid@example.com";
+"(415) 555-0198" = "+966 50 555 0198";
+"Senior Product Manager" = "مديرة منتجات أولى";
+"Group PM, Consumer Growth & Activation" = "مديرة منتجات جماعية، نمو المستهلكين والتفعيل";
+"Google · Mountain View, CA · 2019 – Present" = "جوجل · الرياض، السعودية · 2019 – حتى الآن";
+"Stanford University" = "جامعة الملك عبدالله للعلوم والتقنية";
 "M.S. Management Science & Engineering" = "ماجستير في علوم الإدارة والهندسة";
 "2014 – 2016" = "2014 – 2016";
-"Mountain View, CA" = "ماونتن فيو، كاليفورنيا";
-"Product leader shipping roadmap, discovery, and analytics for high-scale consumer experiences." = "قائد منتج يقدم خارطة الطريق، الاكتشاف، والتحليلات لتجارب المستهلكين واسعة النطاق.";
-"Defined multi-year platform strategy with exec stakeholders and quarterly OKRs." = "حدد استراتيجية المنصة متعددة السنوات مع أصحاب المصلحة التنفيذيين وأهداف النتائج الرئيسية الربع سنوية.";
-"Partnered with engineering and design to launch experiments improving activation by 12%." = "تعاون مع الهندسة والتصميم لإطلاق تجارب حسّنت التنشيط بنسبة 12%.";
-"Stood up quarterly business reviews with finance and GTM, aligning spend to north-star metrics." = "أنشأ مراجعات الأعمال الربع سنوية مع المالية والذهاب إلى السوق، مما يوفق الإنفاق مع مقاييس النجم الشمالي.";
-"Presented roadmap shifts to the leadership team and translated trade-offs into clear investment asks." = "قدم تحولات خارطة الطريق لفريق القيادة وترجم المقايضات إلى طلبات استثمار واضحة.";
-"2019 – Present" = "2019 – الحاضر";
-"2019 – Present · Led cross-functional pods from discovery through launch and post-ship learning." = "2019 – الحاضر · قاد فرقًا متعددة التخصصات من الاكتشاف حتى الإطلاق والتعلم بعد الإطلاق.";
-"+12% activation · $4.2M ARR influenced" = "+12% تفعيل · 4.2 مليون دولار ARR متأثرة";
+"Mountain View, CA" = "الرياض، السعودية";
+"Product leader shipping roadmap, discovery, and analytics for high-scale consumer experiences." = "قائدة منتجات تقود خارطة الطريق والاكتشاف والتحليلات لتجارب المستهلكين واسعة النطاق.";
+"Defined multi-year platform strategy with exec stakeholders and quarterly OKRs." = "وضعت استراتيجية منصة متعددة السنوات مع أصحاب المصلحة التنفيذيين وأهداف رئيسية ربع سنوية.";
+"Partnered with engineering and design to launch experiments improving activation by 12%." = "تعاونت مع فرق الهندسة والتصميم لإطلاق تجارب حسّنت التفعيل بنسبة 12%.";
+"Stood up quarterly business reviews with finance and GTM, aligning spend to north-star metrics." = "أقمت مراجعات أعمال ربع سنوية مع المالية وفرق التسويق، بما يتماشى مع مؤشرات الأداء الرئيسية.";
+"Presented roadmap shifts to the leadership team and translated trade-offs into clear investment asks." = "عرضت تحولات خارطة الطريق على فريق القيادة وحوّلت المقايضات إلى طلبات استثمار واضحة.";
+"2019 – Present" = "2019 – حتى الآن";
+"2019 – Present · Led cross-functional pods from discovery through launch and post-ship learning." = "2019 – حتى الآن · قادت فرقًا متعددة التخصصات من الاكتشاف حتى الإطلاق والتعلم بعد الإطلاق.";
+"+12% activation · $4.2M ARR influenced" = "تفعيل +12% · 4.2 مليون دولار إيرادات متكررة متأثرة";
 "Figma · SQL · Amplitude · Jira · BigQuery" = "Figma · SQL · Amplitude · Jira · BigQuery";
 "Product Strategy" = "استراتيجية المنتج";
-"A/B Testing" = "اختبار A/B";
+"A/B Testing" = "اختبارات A/B";
 "Roadmapping" = "تخطيط خارطة الطريق";
 "CONTACT" = "جهات الاتصال";
 "SKILLS" = "المهارات";

+ 31 - 31
App for Indeed/he.lproj/Localizable.strings

@@ -266,57 +266,57 @@
 "Clear Path" = "נתיב ברור";
 "Pinstripe" = "פסים דקים";
 "Briefing" = "תדריך";
-"Quorum" = "מניין חוקי";
-"Docket" = "יומן";
-"Conduit" = "צינור";
-"Principal" = "ראשי";
-"Charter" = "אמנה";
-"Vertex" = "קודקוד";
-"Linea" = "קו";
-"Prism" = "מנסרה";
-"Circuit" = "מעגל";
-"North" = "צפון";
-"Axis" = "ציר";
-"Marigold" = "ציפורני חתול";
-"Ember" = "גחלת";
-"Lattice" = "סריג";
-"Bloom" = "פריחה";
+"Quorum" = "קוורום";
+"Docket" = "דוקט";
+"Conduit" = "קונדוויט";
+"Principal" = "פרינסיפל";
+"Charter" = "צ'ארטר";
+"Vertex" = "ורטקס";
+"Linea" = "לינאה";
+"Prism" = "פריזם";
+"Circuit" = "סירקיט";
+"North" = "נורת";
+"Axis" = "אקסיס";
+"Marigold" = "מריגולד";
+"Ember" = "אמבר";
+"Lattice" = "לטיס";
+"Bloom" = "בלום";
 "Studio" = "סטודיו";
-"Kite" = "עפיפון";
-"Regent" = "עוצר";
+"Kite" = "קייט";
+"Regent" = "רג'נט";
 "Monarch" = "מונרך";
 "Sterling" = "סטרלינג";
-"Summit" = "פסגה";
-"Estate" = "אחוזה";
-"Chairman" = "יושב ראש";
-"Blue Ocean" = "אוקיינוס כחול";
+"Summit" = "סאמיט";
+"Estate" = "אסטייט";
+"Chairman" = "צ'יירמן";
+"Blue Ocean" = "בלו אושן";
 
 // MARK: - תוכן תצוגה מקדימה להדגמת קורות חיים
 "Sarah Johnson" = "שרה כהן";
 "Google" = "גוגל";
 "Figma" = "Figma";
 "SQL" = "SQL";
-"sarah.johnson@email.com" = "sarah.cohen@example.com";
+"sarah.johnson@email.com" = "sara.cohen@example.com";
 "(415) 555-0198" = "+972 50 555 0198";
 "Senior Product Manager" = "מנהלת מוצר בכירה";
-"Group PM, Consumer Growth & Activation" = "מנהלת מוצר קבוצתית, צמיחה והפעלת צרכנים";
-"Google · Mountain View, CA · 2019 – Present" = "Google · תל אביב · 2019 – הווה";
+"Group PM, Consumer Growth & Activation" = "מנהלת מוצר קבוצתית, צמיחת צרכנים והפעלה";
+"Google · Mountain View, CA · 2019 – Present" = "גוגל · תל אביב · 2019 – עד היום";
 "Stanford University" = "אוניברסיטת תל אביב";
-"M.S. Management Science & Engineering" = "M.S. מדעי הניהול והנדסה";
+"M.S. Management Science & Engineering" = "M.Sc. במדעי הניהול והנדסה";
 "2014 – 2016" = "2014 – 2016";
 "Mountain View, CA" = "תל אביב";
 "Product leader shipping roadmap, discovery, and analytics for high-scale consumer experiences." = "מנהיגת מוצר המובילה מפת דרכים, גילוי וניתוח לחוויות צרכנים בקנה מידה גבוה.";
-"Defined multi-year platform strategy with exec stakeholders and quarterly OKRs." = "הגדירה אסטרטגיית פלטפורמה רב-שנתית עם בעלי עניין בכירים ו-OKRs רבעוניים.";
+"Defined multi-year platform strategy with exec stakeholders and quarterly OKRs." = "הגדירה אסטרטגיית פלטפורמה רב-שנתית עם בעלי עניין בכירים ויעדי OKR רבעוניים.";
 "Partnered with engineering and design to launch experiments improving activation by 12%." = "שיתפה פעולה עם הנדסה ועיצוב כדי להשיק ניסויים ששיפרו את ההפעלה ב-12%.";
-"Stood up quarterly business reviews with finance and GTM, aligning spend to north-star metrics." = "הקימה סקירות עסקיות רבעוניות עם כספים ו-GTM, תוך יישור ההוצאה למדדי כוכב צפון.";
+"Stood up quarterly business reviews with finance and GTM, aligning spend to north-star metrics." = "הקימה סקירות עסקיות רבעוניות עם כספים ושיווק, תוך יישור ההוצאות למדדי ביצוע מרכזיים.";
 "Presented roadmap shifts to the leadership team and translated trade-offs into clear investment asks." = "הציגה שינויי מפת דרכים לצוות ההנהגה ותרגמה פשרות לבקשות השקעה ברורות.";
-"2019 – Present" = "2019 – הווה";
-"2019 – Present · Led cross-functional pods from discovery through launch and post-ship learning." = "2019 – הווה · הוביל צוותים רב-תחומיים מגילוי דרך השקה ועד למידה לאחר ההשקה.";
-"+12% activation · $4.2M ARR influenced" = "+12% הפעלה · 4.2M$ ARR הושפעו";
+"2019 – Present" = "2019 – עד היום";
+"2019 – Present · Led cross-functional pods from discovery through launch and post-ship learning." = "2019 – עד היום · הובילה צוותים רב-תחומיים מגילוי דרך השקה ועד למידה לאחר ההשקה.";
+"+12% activation · $4.2M ARR influenced" = "הפעלה +12% · 4.2M$ הכנסות חוזרות הושפעו";
 "Figma · SQL · Amplitude · Jira · BigQuery" = "Figma · SQL · Amplitude · Jira · BigQuery";
 "Product Strategy" = "אסטרטגיית מוצר";
 "A/B Testing" = "בדיקות A/B";
-"Roadmapping" = "מיפוי דרכים";
+"Roadmapping" = "תכנון מפת דרכים";
 "CONTACT" = "יצירת קשר";
 "SKILLS" = "כישורים";
 "PROFILE" = "פרופיל";