Просмотр исходного кода

Add English and Arabic localization across the app.

Introduce Localizable.strings with AppLocalization helpers, wire UI copy through L(), and add Arabic translations including Pro, PDF, Mac, and dynamic CV template names.

Co-authored-by: Cursor <cursoragent@cursor.com>
AhtashamShahzad1 дней назад: 4
Родитель
Сommit
75f5cafd97

+ 9 - 8
App for Indeed.xcodeproj/project.pbxproj

@@ -39,20 +39,20 @@
39
 			);
39
 			);
40
 			sourceTree = "<group>";
40
 			sourceTree = "<group>";
41
 		};
41
 		};
42
-		27D852922FB1D369008DF557 /* app-connect */ = {
42
+		27D852812FB1D367008DF557 /* Products */ = {
43
 			isa = PBXGroup;
43
 			isa = PBXGroup;
44
 			children = (
44
 			children = (
45
-				27D852912FB1D369008DF557 /* AppConnect.xcconfig */,
45
+				27D852802FB1D367008DF557 /* App for Indeed.app */,
46
 			);
46
 			);
47
-			path = "app-connect";
47
+			name = Products;
48
 			sourceTree = "<group>";
48
 			sourceTree = "<group>";
49
 		};
49
 		};
50
-		27D852812FB1D367008DF557 /* Products */ = {
50
+		27D852922FB1D369008DF557 /* app-connect */ = {
51
 			isa = PBXGroup;
51
 			isa = PBXGroup;
52
 			children = (
52
 			children = (
53
-				27D852802FB1D367008DF557 /* App for Indeed.app */,
53
+				27D852912FB1D369008DF557 /* AppConnect.xcconfig */,
54
 			);
54
 			);
55
-			name = Products;
55
+			path = "app-connect";
56
 			sourceTree = "<group>";
56
 			sourceTree = "<group>";
57
 		};
57
 		};
58
 /* End PBXGroup section */
58
 /* End PBXGroup section */
@@ -101,6 +101,7 @@
101
 			knownRegions = (
101
 			knownRegions = (
102
 				en,
102
 				en,
103
 				Base,
103
 				Base,
104
+				ar,
104
 			);
105
 			);
105
 			mainGroup = 27D852772FB1D367008DF557;
106
 			mainGroup = 27D852772FB1D367008DF557;
106
 			minimizedProjectReferenceProxies = 1;
107
 			minimizedProjectReferenceProxies = 1;
@@ -263,7 +264,7 @@
263
 				CODE_SIGN_STYLE = Automatic;
264
 				CODE_SIGN_STYLE = Automatic;
264
 				COMBINE_HIDPI_IMAGES = YES;
265
 				COMBINE_HIDPI_IMAGES = YES;
265
 				CURRENT_PROJECT_VERSION = 1;
266
 				CURRENT_PROJECT_VERSION = 1;
266
-				DEVELOPMENT_TEAM = NNC7V99779;
267
+				DEVELOPMENT_TEAM = "";
267
 				ENABLE_APP_SANDBOX = YES;
268
 				ENABLE_APP_SANDBOX = YES;
268
 				ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES;
269
 				ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES;
269
 				ENABLE_USER_SELECTED_FILES = readonly;
270
 				ENABLE_USER_SELECTED_FILES = readonly;
@@ -303,7 +304,7 @@
303
 				CODE_SIGN_STYLE = Automatic;
304
 				CODE_SIGN_STYLE = Automatic;
304
 				COMBINE_HIDPI_IMAGES = YES;
305
 				COMBINE_HIDPI_IMAGES = YES;
305
 				CURRENT_PROJECT_VERSION = 1;
306
 				CURRENT_PROJECT_VERSION = 1;
306
-				DEVELOPMENT_TEAM = NNC7V99779;
307
+				DEVELOPMENT_TEAM = "";
307
 				ENABLE_APP_SANDBOX = YES;
308
 				ENABLE_APP_SANDBOX = YES;
308
 				ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES;
309
 				ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES;
309
 				ENABLE_USER_SELECTED_FILES = readonly;
310
 				ENABLE_USER_SELECTED_FILES = readonly;

+ 1 - 0
App for Indeed/AppDelegate.swift

@@ -49,6 +49,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {
49
 
49
 
50
     func applicationWillFinishLaunching(_ notification: Notification) {
50
     func applicationWillFinishLaunching(_ notification: Notification) {
51
         AppAppearanceManager.shared.apply()
51
         AppAppearanceManager.shared.apply()
52
+        AppLanguageManager.shared.applyStoredPreferenceOnLaunch()
52
     }
53
     }
53
 
54
 
54
     func applicationDidFinishLaunching(_ aNotification: Notification) {
55
     func applicationDidFinishLaunching(_ aNotification: Notification) {

+ 23 - 7
App for Indeed/Controllers/IndeedJobBrowserWindowController.swift

@@ -31,9 +31,10 @@ final class IndeedJobBrowserViewController: NSViewController, WKNavigationDelega
31
     private let backButton = NSButton()
31
     private let backButton = NSButton()
32
     private let forwardButton = NSButton()
32
     private let forwardButton = NSButton()
33
     private let reloadButton = NSButton()
33
     private let reloadButton = NSButton()
34
-    private let dismissEmbeddedButton = NSButton(title: "Home", target: nil, action: nil)
34
+    private let dismissEmbeddedButton = NSButton(title: L("Home"), target: nil, action: nil)
35
     private let toolbarContainer = NSView()
35
     private let toolbarContainer = NSView()
36
     private var appearanceObserver: NSObjectProtocol?
36
     private var appearanceObserver: NSObjectProtocol?
37
+    private var languageObserver: NSObjectProtocol?
37
 
38
 
38
     override func loadView() {
39
     override func loadView() {
39
         view = NSView(frame: NSRect(x: 0, y: 0, width: 920, height: 720))
40
         view = NSView(frame: NSRect(x: 0, y: 0, width: 920, height: 720))
@@ -55,7 +56,7 @@ final class IndeedJobBrowserViewController: NSViewController, WKNavigationDelega
55
         dismissEmbeddedButton.isBordered = true
56
         dismissEmbeddedButton.isBordered = true
56
         dismissEmbeddedButton.target = self
57
         dismissEmbeddedButton.target = self
57
         dismissEmbeddedButton.action = #selector(dismissEmbedded)
58
         dismissEmbeddedButton.action = #selector(dismissEmbedded)
58
-        dismissEmbeddedButton.toolTip = "Return to the previous screen"
59
+        dismissEmbeddedButton.toolTip = L("Return to the previous screen")
59
 
60
 
60
         toolbarContainer.translatesAutoresizingMaskIntoConstraints = false
61
         toolbarContainer.translatesAutoresizingMaskIntoConstraints = false
61
         toolbarContainer.wantsLayer = true
62
         toolbarContainer.wantsLayer = true
@@ -112,6 +113,13 @@ final class IndeedJobBrowserViewController: NSViewController, WKNavigationDelega
112
         ) { [weak self] _ in
113
         ) { [weak self] _ in
113
             self?.applyCurrentAppearance()
114
             self?.applyCurrentAppearance()
114
         }
115
         }
116
+        languageObserver = NotificationCenter.default.addObserver(
117
+            forName: AppLanguageManager.didChangeNotification,
118
+            object: nil,
119
+            queue: .main
120
+        ) { [weak self] _ in
121
+            self?.applyLocalizedStrings()
122
+        }
115
 
123
 
116
         if let pendingURL {
124
         if let pendingURL {
117
             webView.load(URLRequest(url: pendingURL))
125
             webView.load(URLRequest(url: pendingURL))
@@ -123,6 +131,9 @@ final class IndeedJobBrowserViewController: NSViewController, WKNavigationDelega
123
         if let appearanceObserver {
131
         if let appearanceObserver {
124
             NotificationCenter.default.removeObserver(appearanceObserver)
132
             NotificationCenter.default.removeObserver(appearanceObserver)
125
         }
133
         }
134
+        if let languageObserver {
135
+            NotificationCenter.default.removeObserver(languageObserver)
136
+        }
126
     }
137
     }
127
 
138
 
128
     func loadPage(_ url: URL) {
139
     func loadPage(_ url: URL) {
@@ -166,6 +177,11 @@ final class IndeedJobBrowserViewController: NSViewController, WKNavigationDelega
166
         reloadButton.contentTintColor = accent
177
         reloadButton.contentTintColor = accent
167
     }
178
     }
168
 
179
 
180
+    private func applyLocalizedStrings() {
181
+        dismissEmbeddedButton.title = L("Home")
182
+        dismissEmbeddedButton.toolTip = L("Return to the previous screen")
183
+    }
184
+
169
     private func updateNavigationButtons() {
185
     private func updateNavigationButtons() {
170
         backButton.isEnabled = webView.canGoBack
186
         backButton.isEnabled = webView.canGoBack
171
         forwardButton.isEnabled = webView.canGoForward
187
         forwardButton.isEnabled = webView.canGoForward
@@ -272,7 +288,7 @@ final class IndeedJobBrowserViewController: NSViewController, WKNavigationDelega
272
     ) {
288
     ) {
273
         let alert = NSAlert()
289
         let alert = NSAlert()
274
         alert.messageText = message
290
         alert.messageText = message
275
-        alert.addButton(withTitle: NSLocalizedString("OK", comment: "Web alert dismiss"))
291
+        alert.addButton(withTitle: L("OK"))
276
         alert.runModal()
292
         alert.runModal()
277
         completionHandler()
293
         completionHandler()
278
     }
294
     }
@@ -285,8 +301,8 @@ final class IndeedJobBrowserViewController: NSViewController, WKNavigationDelega
285
     ) {
301
     ) {
286
         let alert = NSAlert()
302
         let alert = NSAlert()
287
         alert.messageText = message
303
         alert.messageText = message
288
-        alert.addButton(withTitle: NSLocalizedString("OK", comment: "Web confirm accept"))
289
-        alert.addButton(withTitle: NSLocalizedString("Cancel", comment: "Web confirm cancel"))
304
+        alert.addButton(withTitle: L("OK"))
305
+        alert.addButton(withTitle: L("Cancel"))
290
         completionHandler(alert.runModal() == .alertFirstButtonReturn)
306
         completionHandler(alert.runModal() == .alertFirstButtonReturn)
291
     }
307
     }
292
 
308
 
@@ -299,8 +315,8 @@ final class IndeedJobBrowserViewController: NSViewController, WKNavigationDelega
299
     ) {
315
     ) {
300
         let alert = NSAlert()
316
         let alert = NSAlert()
301
         alert.messageText = prompt
317
         alert.messageText = prompt
302
-        alert.addButton(withTitle: NSLocalizedString("OK", comment: "Web prompt accept"))
303
-        alert.addButton(withTitle: NSLocalizedString("Cancel", comment: "Web prompt cancel"))
318
+        alert.addButton(withTitle: L("OK"))
319
+        alert.addButton(withTitle: L("Cancel"))
304
         let field = NSTextField(frame: NSRect(x: 0, y: 0, width: 280, height: 24))
320
         let field = NSTextField(frame: NSRect(x: 0, y: 0, width: 280, height: 24))
305
         field.stringValue = defaultText ?? ""
321
         field.stringValue = defaultText ?? ""
306
         alert.accessoryView = field
322
         alert.accessoryView = field

+ 160 - 90
App for Indeed/Controllers/PremiumPlansWindowController.swift

@@ -8,7 +8,7 @@ final class PremiumPlansWindowController: NSWindowController {
8
     init() {
8
     init() {
9
         let viewController = PremiumPlansViewController()
9
         let viewController = PremiumPlansViewController()
10
         let window = NSWindow(contentViewController: viewController)
10
         let window = NSWindow(contentViewController: viewController)
11
-        window.title = "Premium Plans"
11
+        window.title = L("Premium Plans")
12
         // Borderless avoids titled-window chrome: its rounded titlebar frame often leaves dark wedges at
12
         // Borderless avoids titled-window chrome: its rounded titlebar frame often leaves dark wedges at
13
         // the corners when combined with a custom full-bleed paywall (this window is only shown as a sheet).
13
         // the corners when combined with a custom full-bleed paywall (this window is only shown as a sheet).
14
         window.styleMask = [.borderless, .closable, .resizable]
14
         window.styleMask = [.borderless, .closable, .resizable]
@@ -293,7 +293,7 @@ private final class PremiumPlansViewController: NSViewController {
293
             wantsLayer = true
293
             wantsLayer = true
294
             layer?.cornerRadius = 15
294
             layer?.cornerRadius = 15
295
             bezelStyle = .regularSquare
295
             bezelStyle = .regularSquare
296
-            image = NSImage(systemSymbolName: "xmark", accessibilityDescription: "Close")
296
+            image = NSImage(systemSymbolName: "xmark", accessibilityDescription: L("Close"))
297
             imageScaling = .scaleProportionallyDown
297
             imageScaling = .scaleProportionallyDown
298
             focusRingType = .none
298
             focusRingType = .none
299
             translatesAutoresizingMaskIntoConstraints = false
299
             translatesAutoresizingMaskIntoConstraints = false
@@ -480,6 +480,7 @@ private final class PremiumPlansViewController: NSViewController {
480
         let featureLabels: [NSTextField]
480
         let featureLabels: [NSTextField]
481
         let featureIcons: [NSImageView]
481
         let featureIcons: [NSImageView]
482
         let purchaseButton: PlanPurchaseHoverButton
482
         let purchaseButton: PlanPurchaseHoverButton
483
+        let billedPillLabel: NSTextField?
483
     }
484
     }
484
 
485
 
485
     private enum FeatureListMetrics {
486
     private enum FeatureListMetrics {
@@ -497,68 +498,73 @@ private final class PremiumPlansViewController: NSViewController {
497
     private var premiumCloseButton: PremiumCloseHoverButton?
498
     private var premiumCloseButton: PremiumCloseHoverButton?
498
     private var subscriptionStatusObservation: NSObjectProtocol?
499
     private var subscriptionStatusObservation: NSObjectProtocol?
499
     private var appearanceObserver: NSObjectProtocol?
500
     private var appearanceObserver: NSObjectProtocol?
501
+    private var languageObserver: NSObjectProtocol?
500
 
502
 
501
     /// Core Pro capabilities shown on every pricing card (replaces generic “All premium features”).
503
     /// Core Pro capabilities shown on every pricing card (replaces generic “All premium features”).
502
-    private static let proCapabilityFeatures = [
503
-        "Unlimited AI job search on Home",
504
-        "Save jobs & open listings in-app",
505
-        "CV Maker, profiles & PDF export",
506
-        "Role, company & skill shortcuts"
507
-    ]
508
-
509
-    private let plans: [Plan] = [
510
-        Plan(
511
-            id: "weekly",
512
-            title: "Weekly",
513
-            subtitle: "Flexible and commitment-free",
514
-            period: "/ week",
515
-            billedPill: "",
516
-            billedLine: "",
517
-            crossedPrice: nil,
518
-            savingsText: nil,
519
-            features: proCapabilityFeatures + [
520
-                "Perfect for short-term job hunts",
521
-                "Cancel anytime"
522
-            ],
523
-            iconName: "paperplane.fill",
524
-            iconTint: Theme.iconTint,
525
-            highlight: false
526
-        ),
527
-        Plan(
528
-            id: "monthly",
529
-            title: "Monthly",
530
-            subtitle: "Balanced for regular productivity",
531
-            period: "/ month",
532
-            billedPill: "",
533
-            billedLine: "",
534
-            crossedPrice: nil,
535
-            savingsText: nil,
536
-            features: proCapabilityFeatures + [
537
-                "Best for regular job seekers",
538
-                "Priority support"
539
-            ],
540
-            iconName: "bolt.fill",
541
-            iconTint: Theme.accent,
542
-            highlight: true
543
-        ),
544
-        Plan(
545
-            id: "yearly",
546
-            title: "Yearly",
547
-            subtitle: "Best value for long-term users",
548
-            period: "/ year",
549
-            billedPill: "3 days free trial",
550
-            billedLine: "",
551
-            crossedPrice: nil,
552
-            savingsText: nil,
553
-            features: proCapabilityFeatures + [
554
-                "Lowest effective monthly cost",
555
-                "Ideal for long-term use"
556
-            ],
557
-            iconName: "crown.fill",
558
-            iconTint: Theme.successText,
559
-            highlight: false
560
-        )
561
-    ]
504
+    private var proCapabilityFeatures: [String] {
505
+        [
506
+            L("Unlimited AI job search on Home"),
507
+            L("Save jobs & open listings in-app"),
508
+            L("CV Maker, profiles & PDF export"),
509
+            L("Role, company & skill shortcuts")
510
+        ]
511
+    }
512
+
513
+    private var plans: [Plan] {
514
+        [
515
+            Plan(
516
+                id: "weekly",
517
+                title: L("Weekly"),
518
+                subtitle: L("Flexible and commitment-free"),
519
+                period: L("/ week"),
520
+                billedPill: "",
521
+                billedLine: "",
522
+                crossedPrice: nil,
523
+                savingsText: nil,
524
+                features: proCapabilityFeatures + [
525
+                    L("Perfect for short-term job hunts"),
526
+                    L("Cancel anytime")
527
+                ],
528
+                iconName: "paperplane.fill",
529
+                iconTint: Theme.iconTint,
530
+                highlight: false
531
+            ),
532
+            Plan(
533
+                id: "monthly",
534
+                title: L("Monthly"),
535
+                subtitle: L("Balanced for regular productivity"),
536
+                period: L("/ month"),
537
+                billedPill: "",
538
+                billedLine: "",
539
+                crossedPrice: nil,
540
+                savingsText: nil,
541
+                features: proCapabilityFeatures + [
542
+                    L("Best for regular job seekers"),
543
+                    L("Priority support")
544
+                ],
545
+                iconName: "bolt.fill",
546
+                iconTint: Theme.accent,
547
+                highlight: true
548
+            ),
549
+            Plan(
550
+                id: "yearly",
551
+                title: L("Yearly"),
552
+                subtitle: L("Best value for long-term users"),
553
+                period: L("/ year"),
554
+                billedPill: L("3 days free trial"),
555
+                billedLine: "",
556
+                crossedPrice: nil,
557
+                savingsText: nil,
558
+                features: proCapabilityFeatures + [
559
+                    L("Lowest effective monthly cost"),
560
+                    L("Ideal for long-term use")
561
+                ],
562
+                iconName: "crown.fill",
563
+                iconTint: Theme.successText,
564
+                highlight: false
565
+            )
566
+        ]
567
+    }
562
 
568
 
563
     private let pageGradient = CAGradientLayer()
569
     private let pageGradient = CAGradientLayer()
564
     private var premiumTitleLabel: NSTextField?
570
     private var premiumTitleLabel: NSTextField?
@@ -574,6 +580,9 @@ private final class PremiumPlansViewController: NSViewController {
574
         if let appearanceObserver {
580
         if let appearanceObserver {
575
             NotificationCenter.default.removeObserver(appearanceObserver)
581
             NotificationCenter.default.removeObserver(appearanceObserver)
576
         }
582
         }
583
+        if let languageObserver {
584
+            NotificationCenter.default.removeObserver(languageObserver)
585
+        }
577
     }
586
     }
578
 
587
 
579
     override func viewDidLoad() {
588
     override func viewDidLoad() {
@@ -585,6 +594,13 @@ private final class PremiumPlansViewController: NSViewController {
585
         ) { [weak self] _ in
594
         ) { [weak self] _ in
586
             self?.applyCurrentAppearance()
595
             self?.applyCurrentAppearance()
587
         }
596
         }
597
+        languageObserver = NotificationCenter.default.addObserver(
598
+            forName: AppLanguageManager.didChangeNotification,
599
+            object: nil,
600
+            queue: .main
601
+        ) { [weak self] _ in
602
+            self?.applyLocalizedStrings()
603
+        }
588
         subscriptionStatusObservation = NotificationCenter.default.addObserver(
604
         subscriptionStatusObservation = NotificationCenter.default.addObserver(
589
             forName: .subscriptionStatusDidChange,
605
             forName: .subscriptionStatusDidChange,
590
             object: nil,
606
             object: nil,
@@ -627,13 +643,13 @@ private final class PremiumPlansViewController: NSViewController {
627
         crownIcon.image = NSImage(systemSymbolName: "crown.fill", accessibilityDescription: nil)
643
         crownIcon.image = NSImage(systemSymbolName: "crown.fill", accessibilityDescription: nil)
628
         crownIcon.contentTintColor = NSColor(srgbRed: 254 / 255, green: 214 / 255, blue: 92 / 255, alpha: 1)
644
         crownIcon.contentTintColor = NSColor(srgbRed: 254 / 255, green: 214 / 255, blue: 92 / 255, alpha: 1)
629
 
645
 
630
-        let title = NSTextField(labelWithString: "Upgrade to Pro")
646
+        let title = NSTextField(labelWithString: L("Upgrade to Pro"))
631
         title.font = .systemFont(ofSize: 40, weight: .semibold)
647
         title.font = .systemFont(ofSize: 40, weight: .semibold)
632
         title.textColor = Theme.primaryText
648
         title.textColor = Theme.primaryText
633
         title.alignment = .center
649
         title.alignment = .center
634
         premiumTitleLabel = title
650
         premiumTitleLabel = title
635
 
651
 
636
-        let subtitle = NSTextField(labelWithString: "Unlock unlimited access to premium tools and boost your productivity.")
652
+        let subtitle = NSTextField(labelWithString: L("Unlock unlimited access to premium tools and boost your productivity."))
637
         subtitle.font = .systemFont(ofSize: 14, weight: .regular)
653
         subtitle.font = .systemFont(ofSize: 14, weight: .regular)
638
         subtitle.textColor = Theme.secondaryText
654
         subtitle.textColor = Theme.secondaryText
639
         subtitle.alignment = .center
655
         subtitle.alignment = .center
@@ -800,7 +816,7 @@ private final class PremiumPlansViewController: NSViewController {
800
 
816
 
801
         let selectButton = PlanPurchaseHoverButton(
817
         let selectButton = PlanPurchaseHoverButton(
802
             planId: plan.id,
818
             planId: plan.id,
803
-            title: "Get \(plan.title)",
819
+            title: String(format: L("Get %@"), plan.title),
804
             isPrimaryStyle: plan.highlight,
820
             isPrimaryStyle: plan.highlight,
805
             target: self,
821
             target: self,
806
             action: #selector(didTapSelectPlan)
822
             action: #selector(didTapSelectPlan)
@@ -830,7 +846,8 @@ private final class PremiumPlansViewController: NSViewController {
830
                 divider: divider,
846
                 divider: divider,
831
                 featureLabels: featureLabels,
847
                 featureLabels: featureLabels,
832
                 featureIcons: featureIcons,
848
                 featureIcons: featureIcons,
833
-                purchaseButton: selectButton
849
+                purchaseButton: selectButton,
850
+                billedPillLabel: plan.billedPill.isEmpty ? nil : topRightTag
834
             )
851
             )
835
         )
852
         )
836
 
853
 
@@ -965,10 +982,10 @@ private final class PremiumPlansViewController: NSViewController {
965
 
982
 
966
     private func makeTrustRow() -> NSView {
983
     private func makeTrustRow() -> NSView {
967
         let badges = NSStackView(views: [
984
         let badges = NSStackView(views: [
968
-            trustBadge(icon: "shield.fill", title: "Secure Payments", subtitle: "Your payment is 100% secure."),
969
-            trustBadge(icon: "arrow.counterclockwise", title: "Cancel Anytime", subtitle: "No commitment, cancel anytime."),
970
-            trustBadge(icon: "headphones", title: "24/7 Support", subtitle: "We're here to help you anytime."),
971
-            trustBadge(icon: "lock.fill", title: "Privacy First", subtitle: "Your data is safe with us.")
985
+            trustBadge(icon: "shield.fill", title: L("Secure Payments"), subtitle: L("Your payment is 100% secure.")),
986
+            trustBadge(icon: "arrow.counterclockwise", title: L("Cancel Anytime"), subtitle: L("No commitment, cancel anytime.")),
987
+            trustBadge(icon: "headphones", title: L("24/7 Support"), subtitle: L("We're here to help you anytime.")),
988
+            trustBadge(icon: "lock.fill", title: L("Privacy First"), subtitle: L("Your data is safe with us."))
972
         ])
989
         ])
973
         badges.orientation = .horizontal
990
         badges.orientation = .horizontal
974
         badges.alignment = .centerY
991
         badges.alignment = .centerY
@@ -997,10 +1014,10 @@ private final class PremiumPlansViewController: NSViewController {
997
         subscriptionPrimaryFooterButton = primary.button
1014
         subscriptionPrimaryFooterButton = primary.button
998
 
1015
 
999
         let entries: [(text: String, action: Selector)] = [
1016
         let entries: [(text: String, action: Selector)] = [
1000
-            ("Restore Purchase", #selector(didTapRestorePurchases)),
1001
-            ("Privacy Policy", #selector(didTapFooterPrivacyPolicy)),
1002
-            ("Terms of Use", #selector(didTapFooterTermsOfServices)),
1003
-            ("Support", #selector(didTapFooterSupport))
1017
+            (L("Restore Purchase"), #selector(didTapRestorePurchases)),
1018
+            (L("Privacy Policy"), #selector(didTapFooterPrivacyPolicy)),
1019
+            (L("Terms of Use"), #selector(didTapFooterTermsOfServices)),
1020
+            (L("Support"), #selector(didTapFooterSupport))
1004
         ]
1021
         ]
1005
 
1022
 
1006
         let cells = [primary.container] + entries.enumerated().map { index, entry in
1023
         let cells = [primary.container] + entries.enumerated().map { index, entry in
@@ -1049,8 +1066,8 @@ private final class PremiumPlansViewController: NSViewController {
1049
     }
1066
     }
1050
 
1067
 
1051
     private enum PrimaryFooterSubscriptionTitle {
1068
     private enum PrimaryFooterSubscriptionTitle {
1052
-        static let manage = "Manage Subscription"
1053
-        static let continueFree = "Continue with free plan"
1069
+        static var manage: String { L("Manage Subscription") }
1070
+        static var continueFree: String { L("Continue with free plan") }
1054
     }
1071
     }
1055
 
1072
 
1056
     private func subscriptionPrimaryFooterTitle() -> String {
1073
     private func subscriptionPrimaryFooterTitle() -> String {
@@ -1163,10 +1180,10 @@ private final class PremiumPlansViewController: NSViewController {
1163
     private func periodSuffix(for period: Product.SubscriptionPeriod) -> String {
1180
     private func periodSuffix(for period: Product.SubscriptionPeriod) -> String {
1164
         let value = period.value
1181
         let value = period.value
1165
         switch period.unit {
1182
         switch period.unit {
1166
-        case .day: return value == 1 ? "/ day" : "/ \(value) days"
1167
-        case .week: return value == 1 ? "/ week" : "/ \(value) weeks"
1168
-        case .month: return value == 1 ? "/ month" : "/ \(value) months"
1169
-        case .year: return value == 1 ? "/ year" : "/ \(value) years"
1183
+        case .day: return value == 1 ? L("/ day") : String(format: L("/ %d days"), value)
1184
+        case .week: return value == 1 ? L("/ week") : String(format: L("/ %d weeks"), value)
1185
+        case .month: return value == 1 ? L("/ month") : String(format: L("/ %d months"), value)
1186
+        case .year: return value == 1 ? L("/ year") : String(format: L("/ %d years"), value)
1170
         @unknown default: return ""
1187
         @unknown default: return ""
1171
         }
1188
         }
1172
     }
1189
     }
@@ -1185,10 +1202,10 @@ private final class PremiumPlansViewController: NSViewController {
1185
             guard completed else { return }
1202
             guard completed else { return }
1186
             AppRatingCoordinator.shared.scheduleReviewAfterSubscriptionPurchase()
1203
             AppRatingCoordinator.shared.scheduleReviewAfterSubscriptionPurchase()
1187
             let alert = NSAlert()
1204
             let alert = NSAlert()
1188
-            alert.messageText = "You're subscribed"
1189
-            alert.informativeText = "Thank you — Pro features are now available."
1205
+            alert.messageText = L("You're subscribed")
1206
+            alert.informativeText = L("Thank you — Pro features are now available.")
1190
             alert.alertStyle = .informational
1207
             alert.alertStyle = .informational
1191
-            alert.addButton(withTitle: "OK")
1208
+            alert.addButton(withTitle: L("OK"))
1192
             if let window = view.window {
1209
             if let window = view.window {
1193
                 alert.beginSheetModal(for: window) { [weak self] _ in
1210
                 alert.beginSheetModal(for: window) { [weak self] _ in
1194
                     self?.dismissPremiumSheetFromParentIfNeeded()
1211
                     self?.dismissPremiumSheetFromParentIfNeeded()
@@ -1212,14 +1229,14 @@ private final class PremiumPlansViewController: NSViewController {
1212
             let active = subscriptionStore.isProActive
1229
             let active = subscriptionStore.isProActive
1213
             let alert = NSAlert()
1230
             let alert = NSAlert()
1214
             if active {
1231
             if active {
1215
-                alert.messageText = "Purchases restored"
1216
-                alert.informativeText = "Your subscription is active."
1232
+                alert.messageText = L("Purchases restored")
1233
+                alert.informativeText = L("Your subscription is active.")
1217
             } else {
1234
             } else {
1218
-                alert.messageText = "No subscription found"
1219
-                alert.informativeText = "There was nothing to restore for this Apple ID."
1235
+                alert.messageText = L("No subscription found")
1236
+                alert.informativeText = L("There was nothing to restore for this Apple ID.")
1220
             }
1237
             }
1221
             alert.alertStyle = .informational
1238
             alert.alertStyle = .informational
1222
-            alert.addButton(withTitle: "OK")
1239
+            alert.addButton(withTitle: L("OK"))
1223
             if let window = view.window {
1240
             if let window = view.window {
1224
                 alert.beginSheetModal(for: window) { [weak self] _ in
1241
                 alert.beginSheetModal(for: window) { [weak self] _ in
1225
                     if active {
1242
                     if active {
@@ -1241,7 +1258,7 @@ private final class PremiumPlansViewController: NSViewController {
1241
 
1258
 
1242
     private func presentPurchaseError(_ error: Error) {
1259
     private func presentPurchaseError(_ error: Error) {
1243
         let alert = NSAlert()
1260
         let alert = NSAlert()
1244
-        alert.messageText = "Something went wrong"
1261
+        alert.messageText = L("Something went wrong")
1245
         if let localized = error as? LocalizedError {
1262
         if let localized = error as? LocalizedError {
1246
             var parts: [String] = []
1263
             var parts: [String] = []
1247
             if let description = localized.errorDescription {
1264
             if let description = localized.errorDescription {
@@ -1255,7 +1272,7 @@ private final class PremiumPlansViewController: NSViewController {
1255
             alert.informativeText = error.localizedDescription
1272
             alert.informativeText = error.localizedDescription
1256
         }
1273
         }
1257
         alert.alertStyle = .warning
1274
         alert.alertStyle = .warning
1258
-        alert.addButton(withTitle: "OK")
1275
+        alert.addButton(withTitle: L("OK"))
1259
         if let window = view.window {
1276
         if let window = view.window {
1260
             alert.beginSheetModal(for: window)
1277
             alert.beginSheetModal(for: window)
1261
         } else {
1278
         } else {
@@ -1263,6 +1280,59 @@ private final class PremiumPlansViewController: NSViewController {
1263
         }
1280
         }
1264
     }
1281
     }
1265
 
1282
 
1283
+    private func applyLocalizedStrings() {
1284
+        view.window?.title = L("Premium Plans")
1285
+        premiumTitleLabel?.stringValue = L("Upgrade to Pro")
1286
+        premiumSubtitleLabel?.stringValue = L("Unlock unlimited access to premium tools and boost your productivity.")
1287
+
1288
+        for target in pricingCardTargets {
1289
+            guard let plan = plans.first(where: { $0.id == target.planId }) else { continue }
1290
+            target.titleLabel.stringValue = plan.title
1291
+            target.subtitleLabel.stringValue = plan.subtitle
1292
+            if subscriptionStore.product(forPlanKey: plan.id) == nil {
1293
+                target.periodLabel.stringValue = plan.period
1294
+            }
1295
+            target.billedPillLabel?.stringValue = plan.billedPill
1296
+            target.billedPillLabel?.isHidden = plan.billedPill.isEmpty
1297
+            for (index, label) in target.featureLabels.enumerated() where index < plan.features.count {
1298
+                label.stringValue = plan.features[index]
1299
+            }
1300
+            target.purchaseButton.title = String(format: L("Get %@"), plan.title)
1301
+        }
1302
+
1303
+        if let trustBadgesRow {
1304
+            let trustData: [(String, String)] = [
1305
+                (L("Secure Payments"), L("Your payment is 100% secure.")),
1306
+                (L("Cancel Anytime"), L("No commitment, cancel anytime.")),
1307
+                (L("24/7 Support"), L("We're here to help you anytime.")),
1308
+                (L("Privacy First"), L("Your data is safe with us."))
1309
+            ]
1310
+            for (index, subview) in trustBadgesRow.arrangedSubviews.enumerated() {
1311
+                guard let badge = subview as? NSStackView, index < trustData.count else { continue }
1312
+                for case let textStack as NSStackView in badge.arrangedSubviews {
1313
+                    let labels = textStack.arrangedSubviews.compactMap { $0 as? NSTextField }
1314
+                    if labels.count >= 2 {
1315
+                        labels[0].stringValue = trustData[index].0
1316
+                        labels[1].stringValue = trustData[index].1
1317
+                    }
1318
+                }
1319
+            }
1320
+        }
1321
+
1322
+        let footerTitles = [
1323
+            subscriptionPrimaryFooterTitle(),
1324
+            L("Restore Purchase"),
1325
+            L("Privacy Policy"),
1326
+            L("Terms of Use"),
1327
+            L("Support")
1328
+        ]
1329
+        for (index, button) in footerLinkButtons.enumerated() where index < footerTitles.count {
1330
+            button.title = footerTitles[index]
1331
+        }
1332
+        updateSubscriptionPrimaryFooter()
1333
+        applyStorePricing()
1334
+    }
1335
+
1266
     private func dismissPremiumSheetFromParentIfNeeded() {
1336
     private func dismissPremiumSheetFromParentIfNeeded() {
1267
         guard let sheet = view.window, let parent = sheet.sheetParent else { return }
1337
         guard let sheet = view.window, let parent = sheet.sheetParent else { return }
1268
         parent.endSheet(sheet)
1338
         parent.endSheet(sheet)

+ 6 - 6
App for Indeed/Models/DashboardModels.swift

@@ -30,13 +30,13 @@ protocol DashboardDataProviding {
30
 final class MockDashboardDataProvider: DashboardDataProviding {
30
 final class MockDashboardDataProvider: DashboardDataProviding {
31
     func loadDashboardData() -> DashboardData {
31
     func loadDashboardData() -> DashboardData {
32
         DashboardData(
32
         DashboardData(
33
-            subtitle: "Find your perfect job with the power of AI.",
33
+            subtitle: L("Find your perfect job with the power of AI."),
34
             sidebarItems: [
34
             sidebarItems: [
35
-                SidebarItem(title: "Home", systemImage: "house.fill", badge: nil),
36
-                SidebarItem(title: "Saved Jobs", systemImage: "heart", badge: nil),
37
-                SidebarItem(title: "CV Maker", systemImage: "doc.text", badge: nil),
38
-                SidebarItem(title: "Profile", systemImage: "person", badge: nil),
39
-                SidebarItem(title: "Settings", systemImage: "gearshape", badge: nil)
35
+                SidebarItem(title: L("Home"), systemImage: "house.fill", badge: nil),
36
+                SidebarItem(title: L("Saved Jobs"), systemImage: "heart", badge: nil),
37
+                SidebarItem(title: L("CV Maker"), systemImage: "doc.text", badge: nil),
38
+                SidebarItem(title: L("Profile"), systemImage: "person", badge: nil),
39
+                SidebarItem(title: L("Settings"), systemImage: "gearshape", badge: nil)
40
             ],
40
             ],
41
             jobListings: [
41
             jobListings: [
42
                 JobListing(
42
                 JobListing(

+ 5 - 5
App for Indeed/Services/AppLaunchCoordinator.swift

@@ -14,19 +14,19 @@ enum AppLaunchCoordinator {
14
 
14
 
15
     /// Subscription refresh and product catalog load before the dashboard appears.
15
     /// Subscription refresh and product catalog load before the dashboard appears.
16
     static func performStartup(update: StatusUpdate) async {
16
     static func performStartup(update: StatusUpdate) async {
17
-        update("Starting \(AppMarketingLinks.displayName)…", 0.12)
17
+        update(String(format: L("Starting %@…"), AppMarketingLinks.displayName), 0.12)
18
         try? await Task.sleep(nanoseconds: 180_000_000)
18
         try? await Task.sleep(nanoseconds: 180_000_000)
19
 
19
 
20
-        update("Checking your Pro subscription…", 0.38)
20
+        update(L("Checking your Pro subscription…"), 0.38)
21
         await SubscriptionStore.shared.refreshEntitlements(deep: true)
21
         await SubscriptionStore.shared.refreshEntitlements(deep: true)
22
 
22
 
23
-        update("Loading premium plans from the App Store…", 0.62)
23
+        update(L("Loading premium plans from the App Store…"), 0.62)
24
         await SubscriptionStore.shared.loadProducts()
24
         await SubscriptionStore.shared.loadProducts()
25
 
25
 
26
-        update("Preparing your job search workspace…", 0.86)
26
+        update(L("Preparing your job search workspace…"), 0.86)
27
         NotificationCenter.default.post(name: .subscriptionStatusDidChange, object: nil)
27
         NotificationCenter.default.post(name: .subscriptionStatusDidChange, object: nil)
28
         try? await Task.sleep(nanoseconds: 220_000_000)
28
         try? await Task.sleep(nanoseconds: 220_000_000)
29
 
29
 
30
-        update("Almost ready…", 1.0)
30
+        update(L("Almost ready…"), 1.0)
31
     }
31
     }
32
 }
32
 }

+ 98 - 0
App for Indeed/Services/AppLocalization.swift

@@ -0,0 +1,98 @@
1
+//
2
+//  AppLocalization.swift
3
+//  App for Indeed
4
+//
5
+//  English-first localization using Localizable.strings (same pattern as the LinkedIn app).
6
+//  Add more locales by creating `<locale>.lproj/Localizable.strings` and extending AppLanguage.
7
+//
8
+
9
+import Foundation
10
+
11
+enum AppLanguage: String, CaseIterable {
12
+    case english = "English"
13
+    case arabic = "Arabic"
14
+
15
+    var localeIdentifier: String {
16
+        switch self {
17
+        case .english:
18
+            return "en"
19
+        case .arabic:
20
+            return "ar"
21
+        }
22
+    }
23
+
24
+    static var systemLanguage: AppLanguage {
25
+        let preferred = Locale.preferredLanguages.first ?? "en"
26
+        for language in AppLanguage.allCases where preferred.hasPrefix(language.localeIdentifier) {
27
+            return language
28
+        }
29
+        return .english
30
+    }
31
+
32
+    var localizedDisplayName: String {
33
+        switch self {
34
+        case .english:
35
+            return "English"
36
+        case .arabic:
37
+            return "العربية"
38
+        }
39
+    }
40
+}
41
+
42
+func appLocalized(_ key: String, language: AppLanguage) -> String {
43
+    guard let path = Bundle.main.path(forResource: language.localeIdentifier, ofType: "lproj"),
44
+          let bundle = Bundle(path: path) else {
45
+        return key
46
+    }
47
+    return bundle.localizedString(forKey: key, value: key, table: nil)
48
+}
49
+
50
+func currentAppLanguage() -> AppLanguage {
51
+    let code = UserDefaults.standard.string(forKey: "com.appforindeed.preferredLanguage") ?? "en"
52
+    return AppLanguage.allCases.first(where: { $0.localeIdentifier == code }) ?? .english
53
+}
54
+
55
+/// Resolves copy for the user’s currently selected language.
56
+func L(_ key: String) -> String {
57
+    appLocalized(key, language: currentAppLanguage())
58
+}
59
+
60
+/// Localized CV template title; `name` is always the English localization key.
61
+func localizedTemplateName(_ nameKey: String) -> String {
62
+    L(nameKey)
63
+}
64
+
65
+@MainActor
66
+final class AppLanguageManager {
67
+    static let shared = AppLanguageManager()
68
+
69
+    static let didChangeNotification = Notification.Name("AppLanguageManager.didChange")
70
+
71
+    private enum UserDefaultsKey {
72
+        static let preferredLanguage = "com.appforindeed.preferredLanguage"
73
+    }
74
+
75
+    var current: AppLanguage {
76
+        currentAppLanguage()
77
+    }
78
+
79
+    func applyStoredPreferenceOnLaunch() {
80
+        if UserDefaults.standard.string(forKey: UserDefaultsKey.preferredLanguage) == nil {
81
+            setLanguage(AppLanguage.systemLanguage, notify: false)
82
+        }
83
+    }
84
+
85
+    func setLanguage(_ language: AppLanguage, notify: Bool = true) {
86
+        let code = language.localeIdentifier
87
+        UserDefaults.standard.set(code, forKey: UserDefaultsKey.preferredLanguage)
88
+        UserDefaults.standard.set([code], forKey: "AppleLanguages")
89
+        if notify {
90
+            NotificationCenter.default.post(name: Self.didChangeNotification, object: self)
91
+        }
92
+    }
93
+
94
+    func setLanguage(code: String, notify: Bool = true) {
95
+        guard let language = AppLanguage.allCases.first(where: { $0.localeIdentifier == code }) else { return }
96
+        setLanguage(language, notify: notify)
97
+    }
98
+}

+ 3 - 3
App for Indeed/Services/UserFacingErrorMessage.swift

@@ -11,13 +11,13 @@ enum UserFacingErrorMessage {
11
                  .cannotFindHost,
11
                  .cannotFindHost,
12
                  .cannotConnectToHost,
12
                  .cannotConnectToHost,
13
                  .dnsLookupFailed:
13
                  .dnsLookupFailed:
14
-                return "We couldn't reach the server. Check your internet connection and try again."
14
+                return L("We couldn't reach the server. Check your internet connection and try again.")
15
             case .cancelled:
15
             case .cancelled:
16
-                return "The search was cancelled. Try again when you're ready."
16
+                return L("The search was cancelled. Try again when you're ready.")
17
             default:
17
             default:
18
                 break
18
                 break
19
             }
19
             }
20
         }
20
         }
21
-        return "Something went wrong while searching. Please try again in a moment."
21
+        return L("Something went wrong while searching. Please try again in a moment.")
22
     }
22
     }
23
 }
23
 }

+ 1 - 1
App for Indeed/Subscription/SubscriptionStore.swift

@@ -151,7 +151,7 @@ enum SubscriptionStoreError: LocalizedError {
151
     var errorDescription: String? {
151
     var errorDescription: String? {
152
         switch self {
152
         switch self {
153
         case .productUnavailable:
153
         case .productUnavailable:
154
-            return "That subscription isn’t available from the App Store right now."
154
+            return L("That subscription isn’t available from the App Store right now.")
155
         }
155
         }
156
     }
156
     }
157
 
157
 

+ 32 - 10
App for Indeed/Views/CVFilledPreviewPageView.swift

@@ -106,9 +106,9 @@ final class CVFilledPreviewPageView: NSView {
106
 
106
 
107
     var onDismiss: (() -> Void)?
107
     var onDismiss: (() -> Void)?
108
 
108
 
109
-    private let backButton = NSButton(title: "← Profiles", target: nil, action: nil)
110
-    private let titleLabel = NSTextField(labelWithString: "CV preview")
111
-    private let exportButton = CVPreviewPrimaryCTAButton(title: "Export PDF…", target: nil, action: nil)
109
+    private let backButton = NSButton(title: L("← Profiles"), target: nil, action: nil)
110
+    private let titleLabel = NSTextField(labelWithString: L("CV preview"))
111
+    private let exportButton = CVPreviewPrimaryCTAButton(title: L("Export PDF…"), target: nil, action: nil)
112
     private let scrollView = NSScrollView()
112
     private let scrollView = NSScrollView()
113
     private let documentView = CVPreviewFlippedDocumentView()
113
     private let documentView = CVPreviewFlippedDocumentView()
114
     private let contentStack = NSStackView()
114
     private let contentStack = NSStackView()
@@ -119,6 +119,7 @@ final class CVFilledPreviewPageView: NSView {
119
 
119
 
120
     private let subtitleLabel = NSTextField(wrappingLabelWithString: "")
120
     private let subtitleLabel = NSTextField(wrappingLabelWithString: "")
121
     private var appearanceObserver: NSObjectProtocol?
121
     private var appearanceObserver: NSObjectProtocol?
122
+    private var languageObserver: NSObjectProtocol?
122
 
123
 
123
     override init(frame frameRect: NSRect) {
124
     override init(frame frameRect: NSRect) {
124
         super.init(frame: frameRect)
125
         super.init(frame: frameRect)
@@ -138,7 +139,7 @@ final class CVFilledPreviewPageView: NSView {
138
         exportButton.target = self
139
         exportButton.target = self
139
         exportButton.action = #selector(didTapExportPDF)
140
         exportButton.action = #selector(didTapExportPDF)
140
 
141
 
141
-        subtitleLabel.stringValue = "Layout matches the CV Maker thumbnail for this template. Export a PDF that matches what you see here (fonts, columns, colours, and rules)."
142
+        subtitleLabel.stringValue = L("Layout matches the CV Maker thumbnail for this template. Export a PDF that matches what you see here (fonts, columns, colours, and rules).")
142
         subtitleLabel.font = .systemFont(ofSize: 12, weight: .regular)
143
         subtitleLabel.font = .systemFont(ofSize: 12, weight: .regular)
143
         subtitleLabel.maximumNumberOfLines = 0
144
         subtitleLabel.maximumNumberOfLines = 0
144
 
145
 
@@ -200,13 +201,24 @@ final class CVFilledPreviewPageView: NSView {
200
         ) { [weak self] _ in
201
         ) { [weak self] _ in
201
             self?.applyCurrentAppearance()
202
             self?.applyCurrentAppearance()
202
         }
203
         }
204
+        languageObserver = NotificationCenter.default.addObserver(
205
+            forName: AppLanguageManager.didChangeNotification,
206
+            object: nil,
207
+            queue: .main
208
+        ) { [weak self] _ in
209
+            self?.applyLocalizedStrings()
210
+        }
203
         applyCurrentAppearance()
211
         applyCurrentAppearance()
212
+        applyLocalizedStrings()
204
     }
213
     }
205
 
214
 
206
     deinit {
215
     deinit {
207
         if let appearanceObserver {
216
         if let appearanceObserver {
208
             NotificationCenter.default.removeObserver(appearanceObserver)
217
             NotificationCenter.default.removeObserver(appearanceObserver)
209
         }
218
         }
219
+        if let languageObserver {
220
+            NotificationCenter.default.removeObserver(languageObserver)
221
+        }
210
     }
222
     }
211
 
223
 
212
     @available(*, unavailable)
224
     @available(*, unavailable)
@@ -227,6 +239,16 @@ final class CVFilledPreviewPageView: NSView {
227
         exportButton.applyCurrentAppearance()
239
         exportButton.applyCurrentAppearance()
228
     }
240
     }
229
 
241
 
242
+    func applyLocalizedStrings() {
243
+        backButton.title = L("← Profiles")
244
+        titleLabel.stringValue = L("CV preview")
245
+        exportButton.title = L("Export PDF…")
246
+        subtitleLabel.stringValue = L("Layout matches the CV Maker thumbnail for this template. Export a PDF that matches what you see here (fonts, columns, colours, and rules).")
247
+        if let profile = lastProfile, let template = lastTemplate {
248
+            configure(profile: profile, template: template)
249
+        }
250
+    }
251
+
230
     func configure(profile: SavedProfile, template: CVTemplate) {
252
     func configure(profile: SavedProfile, template: CVTemplate) {
231
         lastProfile = profile
253
         lastProfile = profile
232
         lastTemplate = template
254
         lastTemplate = template
@@ -237,8 +259,8 @@ final class CVFilledPreviewPageView: NSView {
237
         let doc = CVProfileDocumentView(profile: profile, template: template)
259
         let doc = CVProfileDocumentView(profile: profile, template: template)
238
         profileDocumentView = doc
260
         profileDocumentView = doc
239
         contentStack.addArrangedSubview(doc)
261
         contentStack.addArrangedSubview(doc)
240
-        let profileTitle = profile.profileDisplayName.isEmpty ? "Untitled profile" : profile.profileDisplayName
241
-        titleLabel.stringValue = "\(template.name) · \(profileTitle)"
262
+        let profileTitle = profile.profileDisplayName.isEmpty ? L("Untitled profile") : profile.profileDisplayName
263
+        titleLabel.stringValue = "\(template.localizedName) · \(profileTitle)"
242
     }
264
     }
243
 
265
 
244
     @objc private func didTapBack() {
266
     @objc private func didTapBack() {
@@ -263,14 +285,14 @@ final class CVFilledPreviewPageView: NSView {
263
         // text. Rasterising what is actually drawn on screen preserves the full layout.
285
         // text. Rasterising what is actually drawn on screen preserves the full layout.
264
         let data = doc.pdfDataMatchingScreenAppearance() ?? doc.dataWithPDF(inside: bounds)
286
         let data = doc.pdfDataMatchingScreenAppearance() ?? doc.dataWithPDF(inside: bounds)
265
         guard !data.isEmpty else {
287
         guard !data.isEmpty else {
266
-            presentExportError("The résumé could not be rendered to PDF (empty output). Try scrolling the preview so it lays out, then export again.")
288
+            presentExportError(L("The résumé could not be rendered to PDF (empty output). Try scrolling the preview so it lays out, then export again."))
267
             return
289
             return
268
         }
290
         }
269
 
291
 
270
         let panel = NSSavePanel()
292
         let panel = NSSavePanel()
271
         panel.canCreateDirectories = true
293
         panel.canCreateDirectories = true
272
         panel.allowedContentTypes = [.pdf]
294
         panel.allowedContentTypes = [.pdf]
273
-        let base = lastTemplate?.name ?? "CV"
295
+        let base = lastTemplate?.name ?? L("CV")
274
         let safe = base.replacingOccurrences(of: "/", with: "-")
296
         let safe = base.replacingOccurrences(of: "/", with: "-")
275
         panel.nameFieldStringValue = "\(safe).pdf"
297
         panel.nameFieldStringValue = "\(safe).pdf"
276
 
298
 
@@ -300,10 +322,10 @@ final class CVFilledPreviewPageView: NSView {
300
 
322
 
301
     private func presentExportError(_ message: String) {
323
     private func presentExportError(_ message: String) {
302
         let alert = NSAlert()
324
         let alert = NSAlert()
303
-        alert.messageText = "Couldn’t save PDF"
325
+        alert.messageText = L("Couldn't save PDF")
304
         alert.informativeText = message
326
         alert.informativeText = message
305
         alert.alertStyle = .warning
327
         alert.alertStyle = .warning
306
-        alert.addButton(withTitle: "OK")
328
+        alert.addButton(withTitle: L("OK"))
307
         if let window {
329
         if let window {
308
             alert.beginSheetModal(for: window, completionHandler: nil)
330
             alert.beginSheetModal(for: window, completionHandler: nil)
309
         } else {
331
         } else {

+ 47 - 22
App for Indeed/Views/CVMakerPageView.swift

@@ -18,8 +18,8 @@ enum CVCategoryGroup: Hashable {
18
 
18
 
19
     var title: String {
19
     var title: String {
20
         switch self {
20
         switch self {
21
-        case .designBased: return "Design-Based"
22
-        case .professionBased: return "Profession-Based"
21
+        case .designBased: return L("Design-Based")
22
+        case .professionBased: return L("Profession-Based")
23
         }
23
         }
24
     }
24
     }
25
 }
25
 }
@@ -29,11 +29,11 @@ enum CVDesignFamily: String, CaseIterable, Hashable {
29
 
29
 
30
     var title: String {
30
     var title: String {
31
         switch self {
31
         switch self {
32
-        case .professional: return "Professional"
33
-        case .modern: return "Modern"
34
-        case .creative: return "Creative"
35
-        case .minimal: return "Minimal"
36
-        case .executive: return "Executive"
32
+        case .professional: return L("Professional")
33
+        case .modern: return L("Modern")
34
+        case .creative: return L("Creative")
35
+        case .minimal: return L("Minimal")
36
+        case .executive: return L("Executive")
37
         }
37
         }
38
     }
38
     }
39
 }
39
 }
@@ -46,9 +46,9 @@ enum CVTemplateLayoutType: String, Hashable {
46
 
46
 
47
     var gallerySubtitle: String {
47
     var gallerySubtitle: String {
48
         switch self {
48
         switch self {
49
-        case .atsSingleColumn: return "ATS layout"
50
-        case .twoColumnSidebarLeading: return "Sidebar left"
51
-        case .twoColumnSidebarTrailing: return "Sidebar right"
49
+        case .atsSingleColumn: return L("ATS layout")
50
+        case .twoColumnSidebarLeading: return L("Sidebar left")
51
+        case .twoColumnSidebarTrailing: return L("Sidebar right")
52
         }
52
         }
53
     }
53
     }
54
 }
54
 }
@@ -121,6 +121,9 @@ struct CVTemplate: Hashable {
121
         NSColor(srgbRed: themeRed, green: themeGreen, blue: themeBlue, alpha: 1)
121
         NSColor(srgbRed: themeRed, green: themeGreen, blue: themeBlue, alpha: 1)
122
     }
122
     }
123
 
123
 
124
+    /// User-facing template title for the active language (`name` is the English localization key).
125
+    var localizedName: String { localizedTemplateName(name) }
126
+
124
     /// Optional bundle image name; `nil` means render a live vector/text preview.
127
     /// Optional bundle image name; `nil` means render a live vector/text preview.
125
     var previewImageAssetName: String? { nil }
128
     var previewImageAssetName: String? { nil }
126
 
129
 
@@ -579,19 +582,20 @@ final class CVMakerPageView: NSView {
579
     }
582
     }
580
 
583
 
581
     private var appearanceObserver: NSObjectProtocol?
584
     private var appearanceObserver: NSObjectProtocol?
585
+    private var languageObserver: NSObjectProtocol?
582
 
586
 
583
     private let pageGradientLayer = CAGradientLayer()
587
     private let pageGradientLayer = CAGradientLayer()
584
     private let filterChrome = NSVisualEffectView()
588
     private let filterChrome = NSVisualEffectView()
585
     private let filterStack = NSStackView()
589
     private let filterStack = NSStackView()
586
 
590
 
587
-    private let titleLabel = NSTextField(labelWithString: "Templates")
588
-    private let subtitleLabel = NSTextField(labelWithString: "Polished layouts with live previews — pick a style that fits your story.")
591
+    private let titleLabel = NSTextField(labelWithString: L("Templates"))
592
+    private let subtitleLabel = NSTextField(labelWithString: L("Polished layouts with live previews — pick a style that fits your story."))
589
     private let groupTabsRow = NSStackView()
593
     private let groupTabsRow = NSStackView()
590
     private let familyChipsRow = NSStackView()
594
     private let familyChipsRow = NSStackView()
591
     private let scrollView = NSScrollView()
595
     private let scrollView = NSScrollView()
592
     private let gridDocument = TopFlippedView()
596
     private let gridDocument = TopFlippedView()
593
     private let gridStack = NSStackView()
597
     private let gridStack = NSStackView()
594
-    private let ctaButton = CVHoverableButton(title: "Use Template & Select Profile  →", target: nil, action: nil)
598
+    private let ctaButton = CVHoverableButton(title: L("Use Template & Select Profile  →"), target: nil, action: nil)
595
 
599
 
596
     private var selectedGroup: CVCategoryGroup = .professionBased
600
     private var selectedGroup: CVCategoryGroup = .professionBased
597
     private var selectedFamily: CVDesignFamily? = nil // nil == "All"
601
     private var selectedFamily: CVDesignFamily? = nil // nil == "All"
@@ -648,13 +652,24 @@ final class CVMakerPageView: NSView {
648
         ) { [weak self] _ in
652
         ) { [weak self] _ in
649
             self?.applyCurrentAppearance()
653
             self?.applyCurrentAppearance()
650
         }
654
         }
655
+        languageObserver = NotificationCenter.default.addObserver(
656
+            forName: AppLanguageManager.didChangeNotification,
657
+            object: nil,
658
+            queue: .main
659
+        ) { [weak self] _ in
660
+            self?.applyLocalizedStrings()
661
+        }
651
         applyCurrentAppearance()
662
         applyCurrentAppearance()
663
+        applyLocalizedStrings()
652
     }
664
     }
653
 
665
 
654
     deinit {
666
     deinit {
655
         if let appearanceObserver {
667
         if let appearanceObserver {
656
             NotificationCenter.default.removeObserver(appearanceObserver)
668
             NotificationCenter.default.removeObserver(appearanceObserver)
657
         }
669
         }
670
+        if let languageObserver {
671
+            NotificationCenter.default.removeObserver(languageObserver)
672
+        }
658
     }
673
     }
659
 
674
 
660
     @available(*, unavailable)
675
     @available(*, unavailable)
@@ -685,6 +700,16 @@ final class CVMakerPageView: NSView {
685
         updateSelectedChipStates()
700
         updateSelectedChipStates()
686
     }
701
     }
687
 
702
 
703
+    func applyLocalizedStrings() {
704
+        titleLabel.stringValue = L("Templates")
705
+        subtitleLabel.stringValue = L("Polished layouts with live previews — pick a style that fits your story.")
706
+        styleCTAButton(ctaButton)
707
+        configureGroupTabs()
708
+        reloadFamilyChips()
709
+        reloadTemplateGrid()
710
+        updateSelectedChipStates()
711
+    }
712
+
688
     // MARK: Setup
713
     // MARK: Setup
689
 
714
 
690
     private func configureLayout() {
715
     private func configureLayout() {
@@ -850,7 +875,7 @@ final class CVMakerPageView: NSView {
850
         familyChipButtons.removeAll()
875
         familyChipButtons.removeAll()
851
 
876
 
852
         let allCount = templates(forGroup: selectedGroup, family: nil).count
877
         let allCount = templates(forGroup: selectedGroup, family: nil).count
853
-        let allChip = CVChipButton(title: "All", badgeText: "\(allCount)", leadingSymbol: nil, style: .pillSmall)
878
+        let allChip = CVChipButton(title: L("All"), badgeText: "\(allCount)", leadingSymbol: nil, style: .pillSmall)
854
         allChip.translatesAutoresizingMaskIntoConstraints = false
879
         allChip.translatesAutoresizingMaskIntoConstraints = false
855
         allChip.onSelect = { [weak self] in self?.didSelectFamily(nil) }
880
         allChip.onSelect = { [weak self] in self?.didSelectFamily(nil) }
856
         familyChipsRow.addArrangedSubview(allChip)
881
         familyChipsRow.addArrangedSubview(allChip)
@@ -904,7 +929,7 @@ final class CVMakerPageView: NSView {
904
 
929
 
905
         let templates = visibleTemplates
930
         let templates = visibleTemplates
906
         if templates.isEmpty {
931
         if templates.isEmpty {
907
-            let empty = NSTextField(labelWithString: "No templates yet for this category.")
932
+            let empty = NSTextField(labelWithString: L("No templates yet for this category."))
908
             empty.font = .systemFont(ofSize: 13)
933
             empty.font = .systemFont(ofSize: 13)
909
             empty.textColor = Palette.secondaryText
934
             empty.textColor = Palette.secondaryText
910
             gridStack.addArrangedSubview(empty)
935
             gridStack.addArrangedSubview(empty)
@@ -1044,7 +1069,7 @@ final class CVMakerPageView: NSView {
1044
             chosen = nil
1069
             chosen = nil
1045
         }
1070
         }
1046
         guard let template = chosen else {
1071
         guard let template = chosen else {
1047
-            presentPlaceholderAlert(title: "Pick a template", message: "Select a template first, then choose a profile to continue.")
1072
+            presentPlaceholderAlert(title: L("Pick a template"), message: L("Select a template first, then choose a profile to continue."))
1048
             return
1073
             return
1049
         }
1074
         }
1050
         onContinueToProfileSelection?(template)
1075
         onContinueToProfileSelection?(template)
@@ -1060,7 +1085,7 @@ final class CVMakerPageView: NSView {
1060
     }
1085
     }
1061
 
1086
 
1062
     private func styleCTAButton(_ button: CVHoverableButton) {
1087
     private func styleCTAButton(_ button: CVHoverableButton) {
1063
-        button.title = "Use Template & Select Profile  →"
1088
+        button.title = L("Use Template & Select Profile  →")
1064
         button.font = .systemFont(ofSize: 14, weight: .semibold)
1089
         button.font = .systemFont(ofSize: 14, weight: .semibold)
1065
         button.isBordered = false
1090
         button.isBordered = false
1066
         button.bezelStyle = .rounded
1091
         button.bezelStyle = .rounded
@@ -1082,8 +1107,8 @@ final class CVMakerPageView: NSView {
1082
 
1107
 
1083
     private func beginLoadingAICatalogIfPossible() {
1108
     private func beginLoadingAICatalogIfPossible() {
1084
         guard OpenAIConfiguration.hasAPIKey else { return }
1109
         guard OpenAIConfiguration.hasAPIKey else { return }
1085
-        let defaultSubtitle = subtitleLabel.stringValue
1086
-        subtitleLabel.stringValue = "Fetching AI-curated templates…"
1110
+        let defaultSubtitle = L("Polished layouts with live previews — pick a style that fits your story.")
1111
+        subtitleLabel.stringValue = L("Fetching AI-curated templates…")
1087
         CVTemplateFetchService.shared.fetchTemplates { [weak self] result in
1112
         CVTemplateFetchService.shared.fetchTemplates { [weak self] result in
1088
             DispatchQueue.main.async {
1113
             DispatchQueue.main.async {
1089
                 guard let self else { return }
1114
                 guard let self else { return }
@@ -1098,7 +1123,7 @@ final class CVMakerPageView: NSView {
1098
                         self.updateSelectedChipStates()
1123
                         self.updateSelectedChipStates()
1099
                     }
1124
                     }
1100
                 case .failure:
1125
                 case .failure:
1101
-                    self.subtitleLabel.stringValue = "Couldn’t load AI templates — showing the built-in gallery."
1126
+                    self.subtitleLabel.stringValue = L("Couldn’t load AI templates — showing the built-in gallery.")
1102
                     DispatchQueue.main.asyncAfter(deadline: .now() + 5.5) { [weak self] in
1127
                     DispatchQueue.main.asyncAfter(deadline: .now() + 5.5) { [weak self] in
1103
                         self?.subtitleLabel.stringValue = defaultSubtitle
1128
                         self?.subtitleLabel.stringValue = defaultSubtitle
1104
                     }
1129
                     }
@@ -1112,7 +1137,7 @@ final class CVMakerPageView: NSView {
1112
         alert.messageText = title
1137
         alert.messageText = title
1113
         alert.informativeText = message
1138
         alert.informativeText = message
1114
         alert.alertStyle = .informational
1139
         alert.alertStyle = .informational
1115
-        alert.addButton(withTitle: "OK")
1140
+        alert.addButton(withTitle: L("OK"))
1116
         if let window {
1141
         if let window {
1117
             alert.beginSheetModal(for: window)
1142
             alert.beginSheetModal(for: window)
1118
         } else {
1143
         } else {
@@ -1383,7 +1408,7 @@ private final class CVTemplateCard: NSView {
1383
         preview.translatesAutoresizingMaskIntoConstraints = false
1408
         preview.translatesAutoresizingMaskIntoConstraints = false
1384
         previewSurface.addSubview(preview)
1409
         previewSurface.addSubview(preview)
1385
 
1410
 
1386
-        nameLabel.stringValue = template.name
1411
+        nameLabel.stringValue = template.localizedName
1387
         nameLabel.font = .systemFont(ofSize: 14, weight: .semibold)
1412
         nameLabel.font = .systemFont(ofSize: 14, weight: .semibold)
1388
         nameLabel.textColor = palette.primaryText
1413
         nameLabel.textColor = palette.primaryText
1389
         nameLabel.isBordered = false
1414
         nameLabel.isBordered = false

+ 55 - 55
App for Indeed/Views/CVProfileDocumentView.swift

@@ -313,8 +313,8 @@ final class CVProfileDocumentView: NSView {
313
     private func modernClassicBandDocument() -> NSView {
313
     private func modernClassicBandDocument() -> NSView {
314
         let theme = template.themeColor
314
         let theme = template.themeColor
315
         let white = NSColor.white
315
         let white = NSColor.white
316
-        let nameText = displayable(profile.personal.fullName, placeholder: "Your name")
317
-        let roleText = displayable(profile.personal.jobTitle, placeholder: "Professional headline")
316
+        let nameText = displayable(profile.personal.fullName, placeholder: L("Your name"))
317
+        let roleText = displayable(profile.personal.jobTitle, placeholder: L("Professional headline"))
318
 
318
 
319
         let header = NSView()
319
         let header = NSView()
320
         header.translatesAutoresizingMaskIntoConstraints = false
320
         header.translatesAutoresizingMaskIntoConstraints = false
@@ -405,10 +405,10 @@ final class CVProfileDocumentView: NSView {
405
         inner.spacing = 10
405
         inner.spacing = 10
406
         inner.alignment = .leading
406
         inner.alignment = .leading
407
         inner.translatesAutoresizingMaskIntoConstraints = false
407
         inner.translatesAutoresizingMaskIntoConstraints = false
408
-        let nameText = displayable(profile.personal.fullName, placeholder: "Your name")
409
-        let roleText = displayable(profile.personal.jobTitle, placeholder: "Professional headline")
408
+        let nameText = displayable(profile.personal.fullName, placeholder: L("Your name"))
409
+        let roleText = displayable(profile.personal.jobTitle, placeholder: L("Professional headline"))
410
         let contactParts = [profile.personal.email, profile.personal.phone].filter { !$0.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty }
410
         let contactParts = [profile.personal.email, profile.personal.phone].filter { !$0.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty }
411
-        let contactLine = contactParts.isEmpty ? "Add contact in your profile" : contactParts.joined(separator: " · ")
411
+        let contactLine = contactParts.isEmpty ? L("Add contact in your profile") : contactParts.joined(separator: " · ")
412
 
412
 
413
         inner.addArrangedSubview(label(nameText, font: .systemFont(ofSize: 21, weight: .bold), color: style.ink, maxLines: 2))
413
         inner.addArrangedSubview(label(nameText, font: .systemFont(ofSize: 21, weight: .bold), color: style.ink, maxLines: 2))
414
         inner.addArrangedSubview(label(roleText, font: .systemFont(ofSize: 14, weight: .semibold), color: theme, maxLines: 2))
414
         inner.addArrangedSubview(label(roleText, font: .systemFont(ofSize: 14, weight: .semibold), color: theme, maxLines: 2))
@@ -426,8 +426,8 @@ final class CVProfileDocumentView: NSView {
426
 
426
 
427
     private func modernSplitHeaderDocument() -> NSView {
427
     private func modernSplitHeaderDocument() -> NSView {
428
         let theme = template.themeColor
428
         let theme = template.themeColor
429
-        let nameText = displayable(profile.personal.fullName, placeholder: "Your name")
430
-        let roleText = displayable(profile.personal.jobTitle, placeholder: "Professional headline")
429
+        let nameText = displayable(profile.personal.fullName, placeholder: L("Your name"))
430
+        let roleText = displayable(profile.personal.jobTitle, placeholder: L("Professional headline"))
431
         let loc = profile.personal.address.trimmingCharacters(in: .whitespacesAndNewlines)
431
         let loc = profile.personal.address.trimmingCharacters(in: .whitespacesAndNewlines)
432
 
432
 
433
         let left = NSStackView()
433
         let left = NSStackView()
@@ -505,13 +505,13 @@ final class CVProfileDocumentView: NSView {
505
         v.alignment = .leading
505
         v.alignment = .leading
506
 
506
 
507
         if includeSummaryInMain, let summary = nonEmpty(profile.careerSummary) {
507
         if includeSummaryInMain, let summary = nonEmpty(profile.careerSummary) {
508
-            v.addArrangedSubview(modernSectionRow(symbol: "person.crop.circle", title: "About", theme: theme))
508
+            v.addArrangedSubview(modernSectionRow(symbol: "person.crop.circle", title: L("About"), theme: theme))
509
             v.addArrangedSubview(paragraph(summary, compact: compact))
509
             v.addArrangedSubview(paragraph(summary, compact: compact))
510
         }
510
         }
511
 
511
 
512
         let jobs = profile.workExperiences.filter { !$0.isEffectivelyEmpty }
512
         let jobs = profile.workExperiences.filter { !$0.isEffectivelyEmpty }
513
         if !jobs.isEmpty {
513
         if !jobs.isEmpty {
514
-            v.addArrangedSubview(modernSectionRow(symbol: "briefcase.fill", title: "Experience", theme: theme))
514
+            v.addArrangedSubview(modernSectionRow(symbol: "briefcase.fill", title: L("Experience"), theme: theme))
515
             for (index, job) in jobs.enumerated() {
515
             for (index, job) in jobs.enumerated() {
516
                 v.addArrangedSubview(experienceBlock(job: job, compact: compact))
516
                 v.addArrangedSubview(experienceBlock(job: job, compact: compact))
517
                 if index == 0 {
517
                 if index == 0 {
@@ -522,7 +522,7 @@ final class CVProfileDocumentView: NSView {
522
 
522
 
523
         let schools = profile.educations.filter { !$0.isEffectivelyEmpty }
523
         let schools = profile.educations.filter { !$0.isEffectivelyEmpty }
524
         if !schools.isEmpty {
524
         if !schools.isEmpty {
525
-            v.addArrangedSubview(modernSectionRow(symbol: "graduationcap.fill", title: "Education", theme: theme))
525
+            v.addArrangedSubview(modernSectionRow(symbol: "graduationcap.fill", title: L("Education"), theme: theme))
526
             for edu in schools {
526
             for edu in schools {
527
                 v.addArrangedSubview(educationBlock(edu: edu, compact: compact))
527
                 v.addArrangedSubview(educationBlock(edu: edu, compact: compact))
528
             }
528
             }
@@ -546,16 +546,16 @@ final class CVProfileDocumentView: NSView {
546
         }
546
         }
547
 
547
 
548
         if let summary = nonEmpty(profile.careerSummary) {
548
         if let summary = nonEmpty(profile.careerSummary) {
549
-            box.addArrangedSubview(modernSectionRow(symbol: "person.crop.circle", title: "About", theme: theme))
549
+            box.addArrangedSubview(modernSectionRow(symbol: "person.crop.circle", title: L("About"), theme: theme))
550
             box.addArrangedSubview(paragraph(summary, compact: true))
550
             box.addArrangedSubview(paragraph(summary, compact: true))
551
         }
551
         }
552
         if let hi = highlightsBodyText() {
552
         if let hi = highlightsBodyText() {
553
-            box.addArrangedSubview(modernSectionRow(symbol: "star.fill", title: "Highlights", theme: theme))
553
+            box.addArrangedSubview(modernSectionRow(symbol: "star.fill", title: L("Highlights"), theme: theme))
554
             box.addArrangedSubview(paragraph(hi, compact: true))
554
             box.addArrangedSubview(paragraph(hi, compact: true))
555
         }
555
         }
556
         if box.arrangedSubviews.isEmpty {
556
         if box.arrangedSubviews.isEmpty {
557
-            box.addArrangedSubview(modernSectionRow(symbol: "person.crop.circle", title: "About", theme: theme))
558
-            box.addArrangedSubview(paragraph("Add a career summary or interests in your profile to populate this column.", compact: true))
557
+            box.addArrangedSubview(modernSectionRow(symbol: "person.crop.circle", title: L("About"), theme: theme))
558
+            box.addArrangedSubview(paragraph(L("Add a career summary or interests in your profile to populate this column."), compact: true))
559
         }
559
         }
560
         return box
560
         return box
561
     }
561
     }
@@ -628,8 +628,8 @@ final class CVProfileDocumentView: NSView {
628
 
628
 
629
     private func creativeSingleColumnDocument() -> NSView {
629
     private func creativeSingleColumnDocument() -> NSView {
630
         let theme = template.themeColor
630
         let theme = template.themeColor
631
-        let nameText = displayable(profile.personal.fullName, placeholder: "Your name")
632
-        let roleText = displayable(profile.personal.jobTitle, placeholder: "Professional headline")
631
+        let nameText = displayable(profile.personal.fullName, placeholder: L("Your name"))
632
+        let roleText = displayable(profile.personal.jobTitle, placeholder: L("Professional headline"))
633
 
633
 
634
         let banner = NSView()
634
         let banner = NSView()
635
         banner.translatesAutoresizingMaskIntoConstraints = false
635
         banner.translatesAutoresizingMaskIntoConstraints = false
@@ -669,8 +669,8 @@ final class CVProfileDocumentView: NSView {
669
         sidebarStack.layer?.cornerRadius = variant % 2 == 0 ? 10 : 8
669
         sidebarStack.layer?.cornerRadius = variant % 2 == 0 ? 10 : 8
670
         sidebarStack.edgeInsets = NSEdgeInsets(top: 16, left: 16, bottom: 16, right: 16)
670
         sidebarStack.edgeInsets = NSEdgeInsets(top: 16, left: 16, bottom: 16, right: 16)
671
 
671
 
672
-        let nm = displayable(profile.personal.fullName, placeholder: "Your name")
673
-        let role = displayable(profile.personal.jobTitle, placeholder: "Professional headline")
672
+        let nm = displayable(profile.personal.fullName, placeholder: L("Your name"))
673
+        let role = displayable(profile.personal.jobTitle, placeholder: L("Professional headline"))
674
         sidebarStack.addArrangedSubview(label(nm, font: .systemFont(ofSize: 18, weight: .bold), color: onSidebar, maxLines: 2))
674
         sidebarStack.addArrangedSubview(label(nm, font: .systemFont(ofSize: 18, weight: .bold), color: onSidebar, maxLines: 2))
675
         sidebarStack.addArrangedSubview(label(role, font: .systemFont(ofSize: 13, weight: .medium), color: onSidebar.withAlphaComponent(0.85), maxLines: 2))
675
         sidebarStack.addArrangedSubview(label(role, font: .systemFont(ofSize: 13, weight: .medium), color: onSidebar.withAlphaComponent(0.85), maxLines: 2))
676
         if !profile.personal.email.isEmpty {
676
         if !profile.personal.email.isEmpty {
@@ -679,7 +679,7 @@ final class CVProfileDocumentView: NSView {
679
         if !profile.personal.phone.isEmpty {
679
         if !profile.personal.phone.isEmpty {
680
             sidebarStack.addArrangedSubview(label(profile.personal.phone, font: .systemFont(ofSize: 11.5), color: onSidebar.withAlphaComponent(0.82), maxLines: 1))
680
             sidebarStack.addArrangedSubview(label(profile.personal.phone, font: .systemFont(ofSize: 11.5), color: onSidebar.withAlphaComponent(0.82), maxLines: 1))
681
         }
681
         }
682
-        sidebarStack.addArrangedSubview(creativeSidebarHeading("STRENGTHS", onSidebar: onSidebar, accent: theme))
682
+        sidebarStack.addArrangedSubview(creativeSidebarHeading(L("STRENGTHS"), onSidebar: onSidebar, accent: theme))
683
         for token in skillTokensFromProfile(max: 8) {
683
         for token in skillTokensFromProfile(max: 8) {
684
             sidebarStack.addArrangedSubview(label("\(skillPrefix)\(token)", font: .systemFont(ofSize: 12, weight: .semibold), color: onSidebar.withAlphaComponent(0.92), maxLines: 2))
684
             sidebarStack.addArrangedSubview(label("\(skillPrefix)\(token)", font: .systemFont(ofSize: 12, weight: .semibold), color: onSidebar.withAlphaComponent(0.92), maxLines: 2))
685
         }
685
         }
@@ -730,7 +730,7 @@ final class CVProfileDocumentView: NSView {
730
         row.orientation = .horizontal
730
         row.orientation = .horizontal
731
         row.spacing = 8
731
         row.spacing = 8
732
         row.translatesAutoresizingMaskIntoConstraints = false
732
         row.translatesAutoresizingMaskIntoConstraints = false
733
-        let lab = label("PORTFOLIO SNAPSHOT", font: .systemFont(ofSize: 12, weight: .heavy), color: style.ink, maxLines: 1)
733
+        let lab = label(L("PORTFOLIO SNAPSHOT"), font: .systemFont(ofSize: 12, weight: .heavy), color: style.ink, maxLines: 1)
734
         row.addArrangedSubview(stripe)
734
         row.addArrangedSubview(stripe)
735
         row.addArrangedSubview(lab)
735
         row.addArrangedSubview(lab)
736
         v.addSubview(row)
736
         v.addSubview(row)
@@ -751,12 +751,12 @@ final class CVProfileDocumentView: NSView {
751
         stack.alignment = .leading
751
         stack.alignment = .leading
752
         stack.addArrangedSubview(creativeMainHeader(theme: theme))
752
         stack.addArrangedSubview(creativeMainHeader(theme: theme))
753
         if let summary = nonEmpty(profile.careerSummary) {
753
         if let summary = nonEmpty(profile.careerSummary) {
754
-            stack.addArrangedSubview(sectionHeading("Profile"))
754
+            stack.addArrangedSubview(sectionHeading(L("Profile")))
755
             stack.addArrangedSubview(paragraph(summary, compact: false))
755
             stack.addArrangedSubview(paragraph(summary, compact: false))
756
         }
756
         }
757
         let jobs = profile.workExperiences.filter { !$0.isEffectivelyEmpty }
757
         let jobs = profile.workExperiences.filter { !$0.isEffectivelyEmpty }
758
         if !jobs.isEmpty {
758
         if !jobs.isEmpty {
759
-            stack.addArrangedSubview(sectionHeading("Impact"))
759
+            stack.addArrangedSubview(sectionHeading(L("Impact")))
760
             for job in jobs {
760
             for job in jobs {
761
                 let titleLine = [job.jobTitle, job.company].filter { !$0.isEmpty }.joined(separator: " — ")
761
                 let titleLine = [job.jobTitle, job.company].filter { !$0.isEmpty }.joined(separator: " — ")
762
                 if !titleLine.isEmpty {
762
                 if !titleLine.isEmpty {
@@ -770,7 +770,7 @@ final class CVProfileDocumentView: NSView {
770
         }
770
         }
771
         let schools = profile.educations.filter { !$0.isEffectivelyEmpty }
771
         let schools = profile.educations.filter { !$0.isEffectivelyEmpty }
772
         if !schools.isEmpty {
772
         if !schools.isEmpty {
773
-            stack.addArrangedSubview(sectionHeading("Education"))
773
+            stack.addArrangedSubview(sectionHeading(L("Education")))
774
             for edu in schools {
774
             for edu in schools {
775
                 stack.addArrangedSubview(educationBlock(edu: edu, compact: false))
775
                 stack.addArrangedSubview(educationBlock(edu: edu, compact: false))
776
             }
776
             }
@@ -783,12 +783,12 @@ final class CVProfileDocumentView: NSView {
783
 
783
 
784
     private func buildExecutiveDocument() -> NSView {
784
     private func buildExecutiveDocument() -> NSView {
785
         let centeredHead = (variant % 2) == 0
785
         let centeredHead = (variant % 2) == 0
786
-        let nameText = displayable(profile.personal.fullName, placeholder: "Your name")
787
-        let roleText = displayable(profile.personal.jobTitle, placeholder: "Professional headline")
786
+        let nameText = displayable(profile.personal.fullName, placeholder: L("Your name"))
787
+        let roleText = displayable(profile.personal.jobTitle, placeholder: L("Professional headline"))
788
         let contactParts = [profile.personal.email, profile.personal.phone, profile.personal.address].filter {
788
         let contactParts = [profile.personal.email, profile.personal.phone, profile.personal.address].filter {
789
             !$0.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
789
             !$0.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
790
         }
790
         }
791
-        let contactText = contactParts.isEmpty ? "Add contact details in your profile" : contactParts.joined(separator: " · ")
791
+        let contactText = contactParts.isEmpty ? L("Add contact details in your profile") : contactParts.joined(separator: " · ")
792
 
792
 
793
         let name = label(nameText, font: style.nameFont, color: style.ink, maxLines: 2)
793
         let name = label(nameText, font: style.nameFont, color: style.ink, maxLines: 2)
794
         let role = label(roleText, font: style.roleFont, color: style.muted, maxLines: 2)
794
         let role = label(roleText, font: style.roleFont, color: style.muted, maxLines: 2)
@@ -852,7 +852,7 @@ final class CVProfileDocumentView: NSView {
852
         stack.spacing = (compact ? style.bodyBlockSpacing : style.bodyBlockSpacing + 2) - (tightLeading ? 2 : 0)
852
         stack.spacing = (compact ? style.bodyBlockSpacing : style.bodyBlockSpacing + 2) - (tightLeading ? 2 : 0)
853
         stack.alignment = .leading
853
         stack.alignment = .leading
854
 
854
 
855
-        let summaryTitle = (variant % 6 == 3) ? "Summary" : "Professional Summary"
855
+        let summaryTitle = (variant % 6 == 3) ? L("Summary") : L("Professional Summary")
856
         if let summary = nonEmpty(profile.careerSummary) {
856
         if let summary = nonEmpty(profile.careerSummary) {
857
             stack.addArrangedSubview(sectionHeading(summaryTitle))
857
             stack.addArrangedSubview(sectionHeading(summaryTitle))
858
             stack.addArrangedSubview(paragraph(summary, compact: compact))
858
             stack.addArrangedSubview(paragraph(summary, compact: compact))
@@ -860,7 +860,7 @@ final class CVProfileDocumentView: NSView {
860
 
860
 
861
         let jobs = profile.workExperiences.filter { !$0.isEffectivelyEmpty }
861
         let jobs = profile.workExperiences.filter { !$0.isEffectivelyEmpty }
862
         if !jobs.isEmpty {
862
         if !jobs.isEmpty {
863
-            stack.addArrangedSubview(sectionHeading("Selected Experience"))
863
+            stack.addArrangedSubview(sectionHeading(L("Selected Experience")))
864
             for job in jobs {
864
             for job in jobs {
865
                 stack.addArrangedSubview(executiveExperienceBlock(job: job, compact: compact))
865
                 stack.addArrangedSubview(executiveExperienceBlock(job: job, compact: compact))
866
             }
866
             }
@@ -868,7 +868,7 @@ final class CVProfileDocumentView: NSView {
868
 
868
 
869
         let schools = profile.educations.filter { !$0.isEffectivelyEmpty }
869
         let schools = profile.educations.filter { !$0.isEffectivelyEmpty }
870
         if !schools.isEmpty {
870
         if !schools.isEmpty {
871
-            stack.addArrangedSubview(sectionHeading("Education"))
871
+            stack.addArrangedSubview(sectionHeading(L("Education")))
872
             for edu in schools {
872
             for edu in schools {
873
                 stack.addArrangedSubview(educationBlock(edu: edu, compact: compact))
873
                 stack.addArrangedSubview(educationBlock(edu: edu, compact: compact))
874
             }
874
             }
@@ -913,24 +913,24 @@ final class CVProfileDocumentView: NSView {
913
 
913
 
914
         let skills = skillTokensFromProfile(max: 12)
914
         let skills = skillTokensFromProfile(max: 12)
915
         if !skills.isEmpty {
915
         if !skills.isEmpty {
916
-            stack.addArrangedSubview(sectionHeading("Core Competencies"))
916
+            stack.addArrangedSubview(sectionHeading(L("Core Competencies")))
917
             for s in skills {
917
             for s in skills {
918
                 stack.addArrangedSubview(label("· \(s)", font: style.bodyFont, color: style.ink, maxLines: 0))
918
                 stack.addArrangedSubview(label("· \(s)", font: style.bodyFont, color: style.ink, maxLines: 0))
919
             }
919
             }
920
         }
920
         }
921
 
921
 
922
         if let cert = nonEmpty(profile.certificates) {
922
         if let cert = nonEmpty(profile.certificates) {
923
-            stack.addArrangedSubview(sectionHeading("Tools"))
923
+            stack.addArrangedSubview(sectionHeading(L("Tools")))
924
             stack.addArrangedSubview(paragraph(cert, compact: true))
924
             stack.addArrangedSubview(paragraph(cert, compact: true))
925
         }
925
         }
926
 
926
 
927
         if showMetrics, let hi = highlightsBodyText() {
927
         if showMetrics, let hi = highlightsBodyText() {
928
-            stack.addArrangedSubview(sectionHeading("Impact"))
928
+            stack.addArrangedSubview(sectionHeading(L("Impact")))
929
             stack.addArrangedSubview(paragraph(hi, compact: true))
929
             stack.addArrangedSubview(paragraph(hi, compact: true))
930
         }
930
         }
931
 
931
 
932
         if stack.arrangedSubviews.isEmpty {
932
         if stack.arrangedSubviews.isEmpty {
933
-            stack.addArrangedSubview(sectionHeading("Contact"))
933
+            stack.addArrangedSubview(sectionHeading(L("Contact")))
934
             for line in contactLines() {
934
             for line in contactLines() {
935
                 stack.addArrangedSubview(label(line, font: style.contactFont, color: style.ink, maxLines: 0))
935
                 stack.addArrangedSubview(label(line, font: style.contactFont, color: style.ink, maxLines: 0))
936
             }
936
             }
@@ -941,12 +941,12 @@ final class CVProfileDocumentView: NSView {
941
     // MARK: - Minimal (matches gallery light typography)
941
     // MARK: - Minimal (matches gallery light typography)
942
 
942
 
943
     private func buildMinimalDocument() -> NSView {
943
     private func buildMinimalDocument() -> NSView {
944
-        let nameText = displayable(profile.personal.fullName, placeholder: "Your name")
945
-        let roleText = displayable(profile.personal.jobTitle, placeholder: "Professional headline")
944
+        let nameText = displayable(profile.personal.fullName, placeholder: L("Your name"))
945
+        let roleText = displayable(profile.personal.jobTitle, placeholder: L("Professional headline"))
946
         let contactParts = [profile.personal.email, profile.personal.phone].filter {
946
         let contactParts = [profile.personal.email, profile.personal.phone].filter {
947
             !$0.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
947
             !$0.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
948
         }
948
         }
949
-        let contactText = contactParts.isEmpty ? "Add contact in your profile" : contactParts.joined(separator: "   ")
949
+        let contactText = contactParts.isEmpty ? L("Add contact in your profile") : contactParts.joined(separator: "   ")
950
 
950
 
951
         let nameWeight: NSFont.Weight = (variant % 3 == 0) ? .ultraLight : .light
951
         let nameWeight: NSFont.Weight = (variant % 3 == 0) ? .ultraLight : .light
952
         let nameSize: CGFloat = template.headline == .centered ? 22 + CGFloat(variant % 2) : 20 + CGFloat(variant % 3)
952
         let nameSize: CGFloat = template.headline == .centered ? 22 + CGFloat(variant % 2) : 20 + CGFloat(variant % 3)
@@ -1013,14 +1013,14 @@ final class CVProfileDocumentView: NSView {
1013
 
1013
 
1014
         let appendSummary: () -> Void = { [self] in
1014
         let appendSummary: () -> Void = { [self] in
1015
             if let summary = nonEmpty(profile.careerSummary) {
1015
             if let summary = nonEmpty(profile.careerSummary) {
1016
-                stack.addArrangedSubview(sectionHeading("Profile"))
1016
+                stack.addArrangedSubview(sectionHeading(L("Profile")))
1017
                 stack.addArrangedSubview(paragraph(summary, compact: false))
1017
                 stack.addArrangedSubview(paragraph(summary, compact: false))
1018
             }
1018
             }
1019
         }
1019
         }
1020
         let appendExperience: () -> Void = { [self] in
1020
         let appendExperience: () -> Void = { [self] in
1021
             let jobs = profile.workExperiences.filter { !$0.isEffectivelyEmpty }
1021
             let jobs = profile.workExperiences.filter { !$0.isEffectivelyEmpty }
1022
             if !jobs.isEmpty {
1022
             if !jobs.isEmpty {
1023
-                stack.addArrangedSubview(sectionHeading("Experience"))
1023
+                stack.addArrangedSubview(sectionHeading(L("Experience")))
1024
                 for job in jobs {
1024
                 for job in jobs {
1025
                     stack.addArrangedSubview(experienceBlock(job: job, compact: false))
1025
                     stack.addArrangedSubview(experienceBlock(job: job, compact: false))
1026
                 }
1026
                 }
@@ -1029,7 +1029,7 @@ final class CVProfileDocumentView: NSView {
1029
         let appendEducation: () -> Void = { [self] in
1029
         let appendEducation: () -> Void = { [self] in
1030
             let schools = profile.educations.filter { !$0.isEffectivelyEmpty }
1030
             let schools = profile.educations.filter { !$0.isEffectivelyEmpty }
1031
             if !schools.isEmpty {
1031
             if !schools.isEmpty {
1032
-                stack.addArrangedSubview(sectionHeading("Education"))
1032
+                stack.addArrangedSubview(sectionHeading(L("Education")))
1033
                 for edu in schools {
1033
                 for edu in schools {
1034
                     stack.addArrangedSubview(educationBlock(edu: edu, compact: false))
1034
                     stack.addArrangedSubview(educationBlock(edu: edu, compact: false))
1035
                 }
1035
                 }
@@ -1056,13 +1056,13 @@ final class CVProfileDocumentView: NSView {
1056
         stack.alignment = .leading
1056
         stack.alignment = .leading
1057
         let skills = skillTokensFromProfile(max: 12)
1057
         let skills = skillTokensFromProfile(max: 12)
1058
         if skills.isEmpty {
1058
         if skills.isEmpty {
1059
-            stack.addArrangedSubview(sectionHeading("Contact"))
1059
+            stack.addArrangedSubview(sectionHeading(L("Contact")))
1060
             for line in contactLines() {
1060
             for line in contactLines() {
1061
                 stack.addArrangedSubview(label(line, font: style.contactFont, color: style.muted, maxLines: 0))
1061
                 stack.addArrangedSubview(label(line, font: style.contactFont, color: style.muted, maxLines: 0))
1062
             }
1062
             }
1063
             return stack
1063
             return stack
1064
         }
1064
         }
1065
-        stack.addArrangedSubview(sectionHeading("Skills"))
1065
+        stack.addArrangedSubview(sectionHeading(L("Skills")))
1066
         for (i, s) in skills.enumerated() {
1066
         for (i, s) in skills.enumerated() {
1067
             let prefix = numbered ? "\(i + 1). " : "·  "
1067
             let prefix = numbered ? "\(i + 1). " : "·  "
1068
             stack.addArrangedSubview(label("\(prefix)\(s)", font: style.bodyCompactFont, color: style.muted, maxLines: 0))
1068
             stack.addArrangedSubview(label("\(prefix)\(s)", font: style.bodyCompactFont, color: style.muted, maxLines: 0))
@@ -1168,10 +1168,10 @@ final class CVProfileDocumentView: NSView {
1168
     }
1168
     }
1169
 
1169
 
1170
     private func headerBlock() -> NSView {
1170
     private func headerBlock() -> NSView {
1171
-        let nameText = displayable(profile.personal.fullName, placeholder: "Your name")
1172
-        let roleText = displayable(profile.personal.jobTitle, placeholder: "Professional headline")
1171
+        let nameText = displayable(profile.personal.fullName, placeholder: L("Your name"))
1172
+        let roleText = displayable(profile.personal.jobTitle, placeholder: L("Professional headline"))
1173
         let contactParts = [profile.personal.email, profile.personal.phone, profile.personal.address].filter { !$0.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty }
1173
         let contactParts = [profile.personal.email, profile.personal.phone, profile.personal.address].filter { !$0.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty }
1174
-        let contactText = contactParts.isEmpty ? "Add contact details in your profile" : contactParts.joined(separator: " · ")
1174
+        let contactText = contactParts.isEmpty ? L("Add contact details in your profile") : contactParts.joined(separator: " · ")
1175
 
1175
 
1176
         let roleColor = style.roleUsesThemeColor ? template.themeColor : style.muted
1176
         let roleColor = style.roleUsesThemeColor ? template.themeColor : style.muted
1177
         let name = label(nameText, font: style.nameFont, color: style.ink, maxLines: 2)
1177
         let name = label(nameText, font: style.nameFont, color: style.ink, maxLines: 2)
@@ -1256,7 +1256,7 @@ final class CVProfileDocumentView: NSView {
1256
             return "\(a)\(b)".uppercased()
1256
             return "\(a)\(b)".uppercased()
1257
         }
1257
         }
1258
         if let first = parts.first { return String(first.prefix(2)).uppercased() }
1258
         if let first = parts.first { return String(first.prefix(2)).uppercased() }
1259
-        return "CV"
1259
+        return L("CV")
1260
     }
1260
     }
1261
 
1261
 
1262
     private func headlineAccent() -> NSView {
1262
     private func headlineAccent() -> NSView {
@@ -1315,14 +1315,14 @@ final class CVProfileDocumentView: NSView {
1315
         }
1315
         }
1316
         box.edgeInsets = NSEdgeInsets(top: tinted ? 14 : 0, left: tinted ? 14 : 0, bottom: tinted ? 14 : 0, right: tinted ? 14 : 0)
1316
         box.edgeInsets = NSEdgeInsets(top: tinted ? 14 : 0, left: tinted ? 14 : 0, bottom: tinted ? 14 : 0, right: tinted ? 14 : 0)
1317
 
1317
 
1318
-        box.addArrangedSubview(sectionHeading("Contact"))
1318
+        box.addArrangedSubview(sectionHeading(L("Contact")))
1319
         for line in contactLines() {
1319
         for line in contactLines() {
1320
             box.addArrangedSubview(label(line, font: style.contactFont, color: style.ink, maxLines: 0))
1320
             box.addArrangedSubview(label(line, font: style.contactFont, color: style.ink, maxLines: 0))
1321
         }
1321
         }
1322
 
1322
 
1323
         let skills = skillTokensFromProfile(max: 8)
1323
         let skills = skillTokensFromProfile(max: 8)
1324
         if !skills.isEmpty {
1324
         if !skills.isEmpty {
1325
-            box.addArrangedSubview(sectionHeading("Skills"))
1325
+            box.addArrangedSubview(sectionHeading(L("Skills")))
1326
             if variant % 5 == 2 {
1326
             if variant % 5 == 2 {
1327
                 box.addArrangedSubview(skillTagRow(theme: template.themeColor, maxTags: 5))
1327
                 box.addArrangedSubview(skillTagRow(theme: template.themeColor, maxTags: 5))
1328
             } else {
1328
             } else {
@@ -1333,10 +1333,10 @@ final class CVProfileDocumentView: NSView {
1333
         }
1333
         }
1334
 
1334
 
1335
         if variant % 7 == 3, let tools = nonEmpty(profile.certificates) {
1335
         if variant % 7 == 3, let tools = nonEmpty(profile.certificates) {
1336
-            box.addArrangedSubview(sectionHeading("Tools"))
1336
+            box.addArrangedSubview(sectionHeading(L("Tools")))
1337
             box.addArrangedSubview(paragraph(tools, compact: true))
1337
             box.addArrangedSubview(paragraph(tools, compact: true))
1338
         } else if let ancillary = combinedAncillaryText(), !ancillary.isEmpty {
1338
         } else if let ancillary = combinedAncillaryText(), !ancillary.isEmpty {
1339
-            box.addArrangedSubview(sectionHeading("Languages & more"))
1339
+            box.addArrangedSubview(sectionHeading(L("Languages & more")))
1340
             box.addArrangedSubview(paragraph(ancillary, compact: true))
1340
             box.addArrangedSubview(paragraph(ancillary, compact: true))
1341
         }
1341
         }
1342
 
1342
 
@@ -1353,7 +1353,7 @@ final class CVProfileDocumentView: NSView {
1353
         let summaryBody: NSView? = nonEmpty(profile.careerSummary).map { paragraph($0, compact: compact) }
1353
         let summaryBody: NSView? = nonEmpty(profile.careerSummary).map { paragraph($0, compact: compact) }
1354
 
1354
 
1355
         let jobs = profile.workExperiences.filter { !$0.isEffectivelyEmpty }
1355
         let jobs = profile.workExperiences.filter { !$0.isEffectivelyEmpty }
1356
-        let experienceHeading = sectionHeading("Experience")
1356
+        let experienceHeading = sectionHeading(L("Experience"))
1357
         var experienceBlocks: [NSView] = []
1357
         var experienceBlocks: [NSView] = []
1358
         for job in jobs {
1358
         for job in jobs {
1359
             experienceBlocks.append(experienceBlock(job: job, compact: compact))
1359
             experienceBlocks.append(experienceBlock(job: job, compact: compact))
@@ -1379,7 +1379,7 @@ final class CVProfileDocumentView: NSView {
1379
         }
1379
         }
1380
         let appendEducation: () -> Void = { [self] in
1380
         let appendEducation: () -> Void = { [self] in
1381
             if !schools.isEmpty {
1381
             if !schools.isEmpty {
1382
-                v.addArrangedSubview(sectionHeading("Education"))
1382
+                v.addArrangedSubview(sectionHeading(L("Education")))
1383
                 educationBlocks.forEach { v.addArrangedSubview($0) }
1383
                 educationBlocks.forEach { v.addArrangedSubview($0) }
1384
             }
1384
             }
1385
         }
1385
         }
@@ -1400,15 +1400,15 @@ final class CVProfileDocumentView: NSView {
1400
 
1400
 
1401
     private func appendCertificatesInterestsReferrals(to v: NSStackView, compact: Bool) {
1401
     private func appendCertificatesInterestsReferrals(to v: NSStackView, compact: Bool) {
1402
         if let cert = nonEmpty(profile.certificates) {
1402
         if let cert = nonEmpty(profile.certificates) {
1403
-            v.addArrangedSubview(sectionHeading("Certificates"))
1403
+            v.addArrangedSubview(sectionHeading(L("Certificates")))
1404
             v.addArrangedSubview(paragraph(cert, compact: compact))
1404
             v.addArrangedSubview(paragraph(cert, compact: compact))
1405
         }
1405
         }
1406
         if let interests = nonEmpty(profile.interests) {
1406
         if let interests = nonEmpty(profile.interests) {
1407
-            v.addArrangedSubview(sectionHeading("Interests"))
1407
+            v.addArrangedSubview(sectionHeading(L("Interests")))
1408
             v.addArrangedSubview(paragraph(interests, compact: compact))
1408
             v.addArrangedSubview(paragraph(interests, compact: compact))
1409
         }
1409
         }
1410
         if let ref = nonEmpty(profile.referral) {
1410
         if let ref = nonEmpty(profile.referral) {
1411
-            v.addArrangedSubview(sectionHeading("Referrals"))
1411
+            v.addArrangedSubview(sectionHeading(L("Referrals")))
1412
             v.addArrangedSubview(paragraph(ref, compact: compact))
1412
             v.addArrangedSubview(paragraph(ref, compact: compact))
1413
         }
1413
         }
1414
     }
1414
     }
@@ -1551,7 +1551,7 @@ final class CVProfileDocumentView: NSView {
1551
 
1551
 
1552
     /// Gallery + ATS “Clear Path” style use “Profile”; other families keep the neutral résumé label.
1552
     /// Gallery + ATS “Clear Path” style use “Profile”; other families keep the neutral résumé label.
1553
     private var summarySectionTitle: String {
1553
     private var summarySectionTitle: String {
1554
-        template.family == .professional ? "Profile" : "Summary"
1554
+        template.family == .professional ? L("Profile") : L("Summary")
1555
     }
1555
     }
1556
 
1556
 
1557
     private func sectionHeading(_ raw: String) -> NSTextField {
1557
     private func sectionHeading(_ raw: String) -> NSTextField {

+ 43 - 43
App for Indeed/Views/CVTemplateMiniPreview.swift

@@ -31,28 +31,28 @@ struct CVTemplateCardPalette {
31
 // MARK: - Demo résumé content
31
 // MARK: - Demo résumé content
32
 
32
 
33
 fileprivate enum CVPreviewDemoContent {
33
 fileprivate enum CVPreviewDemoContent {
34
-    static let fullName = "Sarah Johnson"
34
+    static var fullName: String { L("Sarah Johnson") }
35
     /// Shown in the header / contact band (broad role).
35
     /// Shown in the header / contact band (broad role).
36
-    static let title = "Senior Product Manager"
36
+    static var title: String { L("Senior Product Manager") }
37
     /// Scoped title under Experience so it is not a verbatim repeat of the header line.
37
     /// Scoped title under Experience so it is not a verbatim repeat of the header line.
38
-    static let experienceRole = "Group PM, Consumer Growth & Activation"
39
-    static let company = "Google"
40
-    static let companyLine = "Google · Mountain View, CA · 2019 – Present"
41
-    static let university = "Stanford University"
42
-    static let degree = "M.S. Management Science & Engineering"
43
-    static let educationYears = "2014 – 2016"
44
-    static let email = "sarah.johnson@email.com"
45
-    static let phone = "(415) 555-0198"
46
-    static let location = "Mountain View, CA"
47
-    static let summary = "Product leader shipping roadmap, discovery, and analytics for high-scale consumer experiences."
48
-    static let bullet1 = "Defined multi-year platform strategy with exec stakeholders and quarterly OKRs."
49
-    static let bullet2 = "Partnered with engineering and design to launch experiments improving activation by 12%."
50
-    static let bullet3 = "Stood up quarterly business reviews with finance and GTM, aligning spend to north-star metrics."
38
+    static var experienceRole: String { L("Group PM, Consumer Growth & Activation") }
39
+    static var company: String { L("Google") }
40
+    static var companyLine: String { L("Google · Mountain View, CA · 2019 – Present") }
41
+    static var university: String { L("Stanford University") }
42
+    static var degree: String { L("M.S. Management Science & Engineering") }
43
+    static var educationYears: String { L("2014 – 2016") }
44
+    static var email: String { L("sarah.johnson@email.com") }
45
+    static var phone: String { L("(415) 555-0198") }
46
+    static var location: String { L("Mountain View, CA") }
47
+    static var summary: String { L("Product leader shipping roadmap, discovery, and analytics for high-scale consumer experiences.") }
48
+    static var bullet1: String { L("Defined multi-year platform strategy with exec stakeholders and quarterly OKRs.") }
49
+    static var bullet2: String { L("Partnered with engineering and design to launch experiments improving activation by 12%.") }
50
+    static var bullet3: String { L("Stood up quarterly business reviews with finance and GTM, aligning spend to north-star metrics.") }
51
     /// Sidebar “highlights” blurb kept distinct from the experience bullets.
51
     /// Sidebar “highlights” blurb kept distinct from the experience bullets.
52
-    static let careerHighlights = "Presented roadmap shifts to the leadership team and translated trade-offs into clear investment asks."
52
+    static var careerHighlights: String { L("Presented roadmap shifts to the leadership team and translated trade-offs into clear investment asks.") }
53
     /// Single tools line reused wherever the résumé lists a stack (avoids scattered near-duplicate strings).
53
     /// Single tools line reused wherever the résumé lists a stack (avoids scattered near-duplicate strings).
54
-    static let toolsLine = "Figma · SQL · Amplitude · Jira · BigQuery"
55
-    static let skillsList = ["Product Strategy", "SQL", "Figma", "A/B Testing", "Roadmapping"]
54
+    static var toolsLine: String { L("Figma · SQL · Amplitude · Jira · BigQuery") }
55
+    static var skillsList: [String] { [L("Product Strategy"), L("SQL"), L("Figma"), L("A/B Testing"), L("Roadmapping") ] }
56
 }
56
 }
57
 
57
 
58
 // MARK: - Mini preview
58
 // MARK: - Mini preview
@@ -273,11 +273,11 @@ final class CVTemplatePreviewView: NSView {
273
         inner.orientation = .vertical
273
         inner.orientation = .vertical
274
         inner.spacing = 5
274
         inner.spacing = 5
275
         inner.alignment = .leading
275
         inner.alignment = .leading
276
-        inner.addArrangedSubview(sectionHeading("CONTACT"))
276
+        inner.addArrangedSubview(sectionHeading(L("CONTACT")))
277
         inner.addArrangedSubview(makeLabel(CVPreviewDemoContent.email, font: .systemFont(ofSize: 6.2), color: palette.previewInk, alignment: .left, maxLines: 2))
277
         inner.addArrangedSubview(makeLabel(CVPreviewDemoContent.email, font: .systemFont(ofSize: 6.2), color: palette.previewInk, alignment: .left, maxLines: 2))
278
         inner.addArrangedSubview(makeLabel(CVPreviewDemoContent.phone, font: .systemFont(ofSize: 6.2), color: palette.previewMuted, alignment: .left, maxLines: 1))
278
         inner.addArrangedSubview(makeLabel(CVPreviewDemoContent.phone, font: .systemFont(ofSize: 6.2), color: palette.previewMuted, alignment: .left, maxLines: 1))
279
         inner.addArrangedSubview(makeLabel(CVPreviewDemoContent.location, font: .systemFont(ofSize: 6.2), color: palette.previewMuted, alignment: .left, maxLines: 1))
279
         inner.addArrangedSubview(makeLabel(CVPreviewDemoContent.location, font: .systemFont(ofSize: 6.2), color: palette.previewMuted, alignment: .left, maxLines: 1))
280
-        inner.addArrangedSubview(sectionHeading("SKILLS"))
280
+        inner.addArrangedSubview(sectionHeading(L("SKILLS")))
281
         if variant % 5 == 2 {
281
         if variant % 5 == 2 {
282
             inner.addArrangedSubview(tagRow(theme: template.themeColor))
282
             inner.addArrangedSubview(tagRow(theme: template.themeColor))
283
         } else {
283
         } else {
@@ -286,7 +286,7 @@ final class CVTemplatePreviewView: NSView {
286
             }
286
             }
287
         }
287
         }
288
         if variant % 7 == 3 {
288
         if variant % 7 == 3 {
289
-            inner.addArrangedSubview(sectionHeading("TOOLS"))
289
+            inner.addArrangedSubview(sectionHeading(L("TOOLS")))
290
             inner.addArrangedSubview(makeLabel(CVPreviewDemoContent.toolsLine, font: .systemFont(ofSize: 5.9), color: palette.previewMuted, alignment: .left, maxLines: 1))
290
             inner.addArrangedSubview(makeLabel(CVPreviewDemoContent.toolsLine, font: .systemFont(ofSize: 5.9), color: palette.previewMuted, alignment: .left, maxLines: 1))
291
         }
291
         }
292
         box.addArrangedSubview(inner)
292
         box.addArrangedSubview(inner)
@@ -301,11 +301,11 @@ final class CVTemplatePreviewView: NSView {
301
         let sp: CGFloat = compact ? 6.2 : 6.5
301
         let sp: CGFloat = compact ? 6.2 : 6.5
302
 
302
 
303
         let profileBlock: () -> Void = {
303
         let profileBlock: () -> Void = {
304
-            stack.addArrangedSubview(self.sectionHeading("PROFILE"))
304
+            stack.addArrangedSubview(self.sectionHeading(L("PROFILE")))
305
             stack.addArrangedSubview(self.makeLabel(CVPreviewDemoContent.summary, font: .systemFont(ofSize: sp), color: self.palette.previewInk, alignment: .left, maxLines: 3))
305
             stack.addArrangedSubview(self.makeLabel(CVPreviewDemoContent.summary, font: .systemFont(ofSize: sp), color: self.palette.previewInk, alignment: .left, maxLines: 3))
306
         }
306
         }
307
         let experienceBlock: () -> Void = {
307
         let experienceBlock: () -> Void = {
308
-            stack.addArrangedSubview(self.sectionHeading("EXPERIENCE"))
308
+            stack.addArrangedSubview(self.sectionHeading(L("EXPERIENCE")))
309
             stack.addArrangedSubview(self.makeLabel(CVPreviewDemoContent.experienceRole, font: .systemFont(ofSize: 6.8, weight: .semibold), color: self.palette.previewInk, alignment: .left, maxLines: 1))
309
             stack.addArrangedSubview(self.makeLabel(CVPreviewDemoContent.experienceRole, font: .systemFont(ofSize: 6.8, weight: .semibold), color: self.palette.previewInk, alignment: .left, maxLines: 1))
310
             stack.addArrangedSubview(self.makeLabel(CVPreviewDemoContent.companyLine, font: .systemFont(ofSize: 6.2, weight: .medium), color: self.template.themeColor, alignment: .left, maxLines: 1))
310
             stack.addArrangedSubview(self.makeLabel(CVPreviewDemoContent.companyLine, font: .systemFont(ofSize: 6.2, weight: .medium), color: self.template.themeColor, alignment: .left, maxLines: 1))
311
             stack.addArrangedSubview(self.bulletRow(CVPreviewDemoContent.bullet1, size: sp))
311
             stack.addArrangedSubview(self.bulletRow(CVPreviewDemoContent.bullet1, size: sp))
@@ -320,7 +320,7 @@ final class CVTemplatePreviewView: NSView {
320
             profileBlock()
320
             profileBlock()
321
             experienceBlock()
321
             experienceBlock()
322
         }
322
         }
323
-        stack.addArrangedSubview(sectionHeading("EDUCATION"))
323
+        stack.addArrangedSubview(sectionHeading(L("EDUCATION")))
324
         stack.addArrangedSubview(makeLabel(CVPreviewDemoContent.university, font: .systemFont(ofSize: 6.6, weight: .semibold), color: palette.previewInk, alignment: .left, maxLines: 1))
324
         stack.addArrangedSubview(makeLabel(CVPreviewDemoContent.university, font: .systemFont(ofSize: 6.6, weight: .semibold), color: palette.previewInk, alignment: .left, maxLines: 1))
325
         stack.addArrangedSubview(makeLabel("\(CVPreviewDemoContent.degree) · \(CVPreviewDemoContent.educationYears)", font: .systemFont(ofSize: 6.2), color: palette.previewMuted, alignment: .left, maxLines: 2))
325
         stack.addArrangedSubview(makeLabel("\(CVPreviewDemoContent.degree) · \(CVPreviewDemoContent.educationYears)", font: .systemFont(ofSize: 6.2), color: palette.previewMuted, alignment: .left, maxLines: 2))
326
         return stack
326
         return stack
@@ -467,7 +467,7 @@ final class CVTemplatePreviewView: NSView {
467
         let onW = NSColor.white
467
         let onW = NSColor.white
468
         right.addArrangedSubview(makeLabel(CVPreviewDemoContent.email, font: .systemFont(ofSize: 5.9, weight: .medium), color: onW.withAlphaComponent(0.95), alignment: .left, maxLines: 2))
468
         right.addArrangedSubview(makeLabel(CVPreviewDemoContent.email, font: .systemFont(ofSize: 5.9, weight: .medium), color: onW.withAlphaComponent(0.95), alignment: .left, maxLines: 2))
469
         right.addArrangedSubview(makeLabel(CVPreviewDemoContent.phone, font: .systemFont(ofSize: 5.9, weight: .medium), color: onW.withAlphaComponent(0.9), alignment: .left, maxLines: 1))
469
         right.addArrangedSubview(makeLabel(CVPreviewDemoContent.phone, font: .systemFont(ofSize: 5.9, weight: .medium), color: onW.withAlphaComponent(0.9), alignment: .left, maxLines: 1))
470
-        right.addArrangedSubview(makeLabel("Open to relocation", font: .systemFont(ofSize: 5.6, weight: .regular), color: onW.withAlphaComponent(0.75), alignment: .left, maxLines: 1))
470
+        right.addArrangedSubview(makeLabel(L("Open to relocation"), font: .systemFont(ofSize: 5.6, weight: .regular), color: onW.withAlphaComponent(0.75), alignment: .left, maxLines: 1))
471
 
471
 
472
         let top = NSStackView(views: [left, right])
472
         let top = NSStackView(views: [left, right])
473
         top.orientation = .horizontal
473
         top.orientation = .horizontal
@@ -493,9 +493,9 @@ final class CVTemplatePreviewView: NSView {
493
             box.layer?.cornerRadius = 4
493
             box.layer?.cornerRadius = 4
494
         }
494
         }
495
         box.edgeInsets = NSEdgeInsets(top: 4, left: 4, bottom: 4, right: 4)
495
         box.edgeInsets = NSEdgeInsets(top: 4, left: 4, bottom: 4, right: 4)
496
-        box.addArrangedSubview(modernSectionRow(symbol: "person.crop.circle", title: "About", theme: theme))
496
+        box.addArrangedSubview(modernSectionRow(symbol: "person.crop.circle", title: L("About"), theme: theme))
497
         box.addArrangedSubview(makeLabel(CVPreviewDemoContent.summary, font: .systemFont(ofSize: 6), color: palette.previewInk, alignment: .left, maxLines: 4))
497
         box.addArrangedSubview(makeLabel(CVPreviewDemoContent.summary, font: .systemFont(ofSize: 6), color: palette.previewInk, alignment: .left, maxLines: 4))
498
-        box.addArrangedSubview(modernSectionRow(symbol: "star.fill", title: "Highlights", theme: theme))
498
+        box.addArrangedSubview(modernSectionRow(symbol: "star.fill", title: L("Highlights"), theme: theme))
499
         box.addArrangedSubview(makeLabel(CVPreviewDemoContent.careerHighlights, font: .systemFont(ofSize: 6), color: palette.previewMuted, alignment: .left, maxLines: 3))
499
         box.addArrangedSubview(makeLabel(CVPreviewDemoContent.careerHighlights, font: .systemFont(ofSize: 6), color: palette.previewMuted, alignment: .left, maxLines: 3))
500
         return box
500
         return box
501
     }
501
     }
@@ -520,11 +520,11 @@ final class CVTemplatePreviewView: NSView {
520
         stack.orientation = .vertical
520
         stack.orientation = .vertical
521
         stack.spacing = 5
521
         stack.spacing = 5
522
         stack.alignment = .leading
522
         stack.alignment = .leading
523
-        stack.addArrangedSubview(modernSectionRow(symbol: "briefcase.fill", title: "Experience", theme: theme))
523
+        stack.addArrangedSubview(modernSectionRow(symbol: "briefcase.fill", title: L("Experience"), theme: theme))
524
         stack.addArrangedSubview(makeLabel("\(CVPreviewDemoContent.experienceRole) — \(CVPreviewDemoContent.company)", font: .systemFont(ofSize: 6.6, weight: .semibold), color: palette.previewInk, alignment: .left, maxLines: 2))
524
         stack.addArrangedSubview(makeLabel("\(CVPreviewDemoContent.experienceRole) — \(CVPreviewDemoContent.company)", font: .systemFont(ofSize: 6.6, weight: .semibold), color: palette.previewInk, alignment: .left, maxLines: 2))
525
         stack.addArrangedSubview(makeLabel("2019 – Present · Led cross-functional pods from discovery through launch and post-ship learning.", font: .systemFont(ofSize: 6.1), color: palette.previewMuted, alignment: .left, maxLines: 2))
525
         stack.addArrangedSubview(makeLabel("2019 – Present · Led cross-functional pods from discovery through launch and post-ship learning.", font: .systemFont(ofSize: 6.1), color: palette.previewMuted, alignment: .left, maxLines: 2))
526
         stack.addArrangedSubview(tagRow(theme: theme))
526
         stack.addArrangedSubview(tagRow(theme: theme))
527
-        stack.addArrangedSubview(modernSectionRow(symbol: "graduationcap.fill", title: "Education", theme: theme))
527
+        stack.addArrangedSubview(modernSectionRow(symbol: "graduationcap.fill", title: L("Education"), theme: theme))
528
         stack.addArrangedSubview(makeLabel("\(CVPreviewDemoContent.university), \(CVPreviewDemoContent.degree) (\(CVPreviewDemoContent.educationYears))", font: .systemFont(ofSize: 6.2), color: palette.previewInk, alignment: .left, maxLines: 2))
528
         stack.addArrangedSubview(makeLabel("\(CVPreviewDemoContent.university), \(CVPreviewDemoContent.degree) (\(CVPreviewDemoContent.educationYears))", font: .systemFont(ofSize: 6.2), color: palette.previewInk, alignment: .left, maxLines: 2))
529
         return stack
529
         return stack
530
     }
530
     }
@@ -623,15 +623,15 @@ final class CVTemplatePreviewView: NSView {
623
         stack.alignment = .leading
623
         stack.alignment = .leading
624
 
624
 
625
         let edu: () -> Void = {
625
         let edu: () -> Void = {
626
-            stack.addArrangedSubview(self.sectionHeading("EDUCATION"))
626
+            stack.addArrangedSubview(self.sectionHeading(L("EDUCATION")))
627
             stack.addArrangedSubview(self.makeLabel("\(CVPreviewDemoContent.university) — \(CVPreviewDemoContent.degree) · \(CVPreviewDemoContent.educationYears)", font: .systemFont(ofSize: 6.1, weight: .light), color: self.palette.previewInk, alignment: .left, maxLines: 2))
627
             stack.addArrangedSubview(self.makeLabel("\(CVPreviewDemoContent.university) — \(CVPreviewDemoContent.degree) · \(CVPreviewDemoContent.educationYears)", font: .systemFont(ofSize: 6.1, weight: .light), color: self.palette.previewInk, alignment: .left, maxLines: 2))
628
         }
628
         }
629
         let prof: () -> Void = {
629
         let prof: () -> Void = {
630
-            stack.addArrangedSubview(self.sectionHeading("PROFILE"))
630
+            stack.addArrangedSubview(self.sectionHeading(L("PROFILE")))
631
             stack.addArrangedSubview(self.makeLabel(CVPreviewDemoContent.summary, font: .systemFont(ofSize: 6.3, weight: .light), color: self.palette.previewInk, alignment: .left, maxLines: 3))
631
             stack.addArrangedSubview(self.makeLabel(CVPreviewDemoContent.summary, font: .systemFont(ofSize: 6.3, weight: .light), color: self.palette.previewInk, alignment: .left, maxLines: 3))
632
         }
632
         }
633
         let exp: () -> Void = {
633
         let exp: () -> Void = {
634
-            stack.addArrangedSubview(self.sectionHeading("EXPERIENCE"))
634
+            stack.addArrangedSubview(self.sectionHeading(L("EXPERIENCE")))
635
             stack.addArrangedSubview(self.makeLabel("\(CVPreviewDemoContent.experienceRole) · \(CVPreviewDemoContent.company)", font: .systemFont(ofSize: 6.5, weight: .regular), color: self.palette.previewInk, alignment: .left, maxLines: 2))
635
             stack.addArrangedSubview(self.makeLabel("\(CVPreviewDemoContent.experienceRole) · \(CVPreviewDemoContent.company)", font: .systemFont(ofSize: 6.5, weight: .regular), color: self.palette.previewInk, alignment: .left, maxLines: 2))
636
             stack.addArrangedSubview(self.makeLabel(CVPreviewDemoContent.bullet1, font: .systemFont(ofSize: 6, weight: .light), color: self.palette.previewMuted, alignment: .left, maxLines: 2))
636
             stack.addArrangedSubview(self.makeLabel(CVPreviewDemoContent.bullet1, font: .systemFont(ofSize: 6, weight: .light), color: self.palette.previewMuted, alignment: .left, maxLines: 2))
637
             stack.addArrangedSubview(self.makeLabel(CVPreviewDemoContent.bullet2, font: .systemFont(ofSize: 6, weight: .light), color: self.palette.previewMuted, alignment: .left, maxLines: 2))
637
             stack.addArrangedSubview(self.makeLabel(CVPreviewDemoContent.bullet2, font: .systemFont(ofSize: 6, weight: .light), color: self.palette.previewMuted, alignment: .left, maxLines: 2))
@@ -654,7 +654,7 @@ final class CVTemplatePreviewView: NSView {
654
         stack.orientation = .vertical
654
         stack.orientation = .vertical
655
         stack.spacing = 6
655
         stack.spacing = 6
656
         stack.alignment = .leading
656
         stack.alignment = .leading
657
-        stack.addArrangedSubview(sectionHeading("SKILLS"))
657
+        stack.addArrangedSubview(sectionHeading(L("SKILLS")))
658
         for (i, s) in CVPreviewDemoContent.skillsList.enumerated() {
658
         for (i, s) in CVPreviewDemoContent.skillsList.enumerated() {
659
             let prefix = numbered ? "\(i + 1). " : "·  "
659
             let prefix = numbered ? "\(i + 1). " : "·  "
660
             stack.addArrangedSubview(makeLabel("\(prefix)\(s)", font: .systemFont(ofSize: 6, weight: .light), color: palette.previewMuted, alignment: .left, maxLines: 1))
660
             stack.addArrangedSubview(makeLabel("\(prefix)\(s)", font: .systemFont(ofSize: 6, weight: .light), color: palette.previewMuted, alignment: .left, maxLines: 1))
@@ -728,16 +728,16 @@ final class CVTemplatePreviewView: NSView {
728
         stack.orientation = .vertical
728
         stack.orientation = .vertical
729
         stack.spacing = (compact ? 4 : 5) - (tightLeading ? 1 : 0)
729
         stack.spacing = (compact ? 4 : 5) - (tightLeading ? 1 : 0)
730
         stack.alignment = .leading
730
         stack.alignment = .leading
731
-        let sumTitle = (layoutVariant % 6 == 3) ? "SUMMARY" : "PROFESSIONAL SUMMARY"
731
+        let sumTitle = (layoutVariant % 6 == 3) ? L("SUMMARY") : L("PROFESSIONAL SUMMARY")
732
         stack.addArrangedSubview(sectionHeading(sumTitle))
732
         stack.addArrangedSubview(sectionHeading(sumTitle))
733
         stack.addArrangedSubview(makeLabel(CVPreviewDemoContent.summary, font: serif, color: ink, alignment: .left, maxLines: 3))
733
         stack.addArrangedSubview(makeLabel(CVPreviewDemoContent.summary, font: serif, color: ink, alignment: .left, maxLines: 3))
734
-        stack.addArrangedSubview(sectionHeading("SELECTED EXPERIENCE"))
734
+        stack.addArrangedSubview(sectionHeading(L("SELECTED EXPERIENCE")))
735
         let jobFont = NSFontManager.shared.convert(serif, toHaveTrait: .boldFontMask)
735
         let jobFont = NSFontManager.shared.convert(serif, toHaveTrait: .boldFontMask)
736
         stack.addArrangedSubview(makeLabel("\(CVPreviewDemoContent.experienceRole), \(CVPreviewDemoContent.company)", font: jobFont, color: ink, alignment: .left, maxLines: 2))
736
         stack.addArrangedSubview(makeLabel("\(CVPreviewDemoContent.experienceRole), \(CVPreviewDemoContent.company)", font: jobFont, color: ink, alignment: .left, maxLines: 2))
737
         stack.addArrangedSubview(makeLabel("2019 – Present", font: serif, color: theme, alignment: .left, maxLines: 1))
737
         stack.addArrangedSubview(makeLabel("2019 – Present", font: serif, color: theme, alignment: .left, maxLines: 1))
738
         stack.addArrangedSubview(makeLabel(CVPreviewDemoContent.bullet1, font: serif, color: muted, alignment: .left, maxLines: 2))
738
         stack.addArrangedSubview(makeLabel(CVPreviewDemoContent.bullet1, font: serif, color: muted, alignment: .left, maxLines: 2))
739
         stack.addArrangedSubview(makeLabel(CVPreviewDemoContent.bullet2, font: serif, color: muted, alignment: .left, maxLines: 2))
739
         stack.addArrangedSubview(makeLabel(CVPreviewDemoContent.bullet2, font: serif, color: muted, alignment: .left, maxLines: 2))
740
-        stack.addArrangedSubview(sectionHeading("EDUCATION"))
740
+        stack.addArrangedSubview(sectionHeading(L("EDUCATION")))
741
         stack.addArrangedSubview(makeLabel("\(CVPreviewDemoContent.university), \(CVPreviewDemoContent.degree) · \(CVPreviewDemoContent.educationYears)", font: serif, color: ink, alignment: .left, maxLines: 2))
741
         stack.addArrangedSubview(makeLabel("\(CVPreviewDemoContent.university), \(CVPreviewDemoContent.degree) · \(CVPreviewDemoContent.educationYears)", font: serif, color: ink, alignment: .left, maxLines: 2))
742
         return stack
742
         return stack
743
     }
743
     }
@@ -756,14 +756,14 @@ final class CVTemplatePreviewView: NSView {
756
             stack.layer?.cornerRadius = layoutVariant % 4 == 2 ? 5 : 3
756
             stack.layer?.cornerRadius = layoutVariant % 4 == 2 ? 5 : 3
757
             stack.edgeInsets = NSEdgeInsets(top: 5, left: 5, bottom: 5, right: 5)
757
             stack.edgeInsets = NSEdgeInsets(top: 5, left: 5, bottom: 5, right: 5)
758
         }
758
         }
759
-        stack.addArrangedSubview(sectionHeading("CORE COMPETENCIES"))
759
+        stack.addArrangedSubview(sectionHeading(L("CORE COMPETENCIES")))
760
         for s in CVPreviewDemoContent.skillsList {
760
         for s in CVPreviewDemoContent.skillsList {
761
             stack.addArrangedSubview(makeLabel("· \(s)", font: serif, color: palette.previewInk, alignment: .left, maxLines: 1))
761
             stack.addArrangedSubview(makeLabel("· \(s)", font: serif, color: palette.previewInk, alignment: .left, maxLines: 1))
762
         }
762
         }
763
-        stack.addArrangedSubview(sectionHeading("TOOLS"))
763
+        stack.addArrangedSubview(sectionHeading(L("TOOLS")))
764
         stack.addArrangedSubview(makeLabel(CVPreviewDemoContent.toolsLine, font: serif, color: palette.previewMuted, alignment: .left, maxLines: 2))
764
         stack.addArrangedSubview(makeLabel(CVPreviewDemoContent.toolsLine, font: serif, color: palette.previewMuted, alignment: .left, maxLines: 2))
765
         if showMetrics {
765
         if showMetrics {
766
-            stack.addArrangedSubview(sectionHeading("IMPACT"))
766
+            stack.addArrangedSubview(sectionHeading(L("IMPACT")))
767
             stack.addArrangedSubview(makeLabel("+12% activation · $4.2M ARR influenced", font: serif, color: palette.previewInk, alignment: .left, maxLines: 2))
767
             stack.addArrangedSubview(makeLabel("+12% activation · $4.2M ARR influenced", font: serif, color: palette.previewInk, alignment: .left, maxLines: 2))
768
         }
768
         }
769
         return stack
769
         return stack
@@ -791,7 +791,7 @@ final class CVTemplatePreviewView: NSView {
791
         sidebar.addArrangedSubview(makeLabel(CVPreviewDemoContent.title, font: .systemFont(ofSize: 6.5, weight: .medium), color: onSidebar.withAlphaComponent(0.85), alignment: .left, maxLines: 2))
791
         sidebar.addArrangedSubview(makeLabel(CVPreviewDemoContent.title, font: .systemFont(ofSize: 6.5, weight: .medium), color: onSidebar.withAlphaComponent(0.85), alignment: .left, maxLines: 2))
792
         sidebar.addArrangedSubview(makeLabel(CVPreviewDemoContent.email, font: .systemFont(ofSize: 5.8), color: onSidebar.withAlphaComponent(0.8), alignment: .left, maxLines: 1))
792
         sidebar.addArrangedSubview(makeLabel(CVPreviewDemoContent.email, font: .systemFont(ofSize: 5.8), color: onSidebar.withAlphaComponent(0.8), alignment: .left, maxLines: 1))
793
         sidebar.addArrangedSubview(makeLabel(CVPreviewDemoContent.phone, font: .systemFont(ofSize: 5.8), color: onSidebar.withAlphaComponent(0.8), alignment: .left, maxLines: 1))
793
         sidebar.addArrangedSubview(makeLabel(CVPreviewDemoContent.phone, font: .systemFont(ofSize: 5.8), color: onSidebar.withAlphaComponent(0.8), alignment: .left, maxLines: 1))
794
-        sidebar.addArrangedSubview(creativeSidebarHeading("STRENGTHS", onSidebar: onSidebar, accent: theme))
794
+        sidebar.addArrangedSubview(creativeSidebarHeading(L("STRENGTHS"), onSidebar: onSidebar, accent: theme))
795
         for s in CVPreviewDemoContent.skillsList.prefix(5) {
795
         for s in CVPreviewDemoContent.skillsList.prefix(5) {
796
             sidebar.addArrangedSubview(makeLabel("\(skillPrefix)\(s)", font: .systemFont(ofSize: 6, weight: .semibold), color: onSidebar.withAlphaComponent(0.92), alignment: .left, maxLines: 1))
796
             sidebar.addArrangedSubview(makeLabel("\(skillPrefix)\(s)", font: .systemFont(ofSize: 6, weight: .semibold), color: onSidebar.withAlphaComponent(0.92), alignment: .left, maxLines: 1))
797
         }
797
         }
@@ -801,14 +801,14 @@ final class CVTemplatePreviewView: NSView {
801
         main.spacing = 4 + CGFloat(layoutVariant % 3)
801
         main.spacing = 4 + CGFloat(layoutVariant % 3)
802
         main.alignment = .leading
802
         main.alignment = .leading
803
         main.addArrangedSubview(creativeMainHeader(theme: theme))
803
         main.addArrangedSubview(creativeMainHeader(theme: theme))
804
-        main.addArrangedSubview(sectionHeading("PROFILE"))
804
+        main.addArrangedSubview(sectionHeading(L("PROFILE")))
805
         main.addArrangedSubview(makeLabel(CVPreviewDemoContent.summary, font: .systemFont(ofSize: 6.2), color: palette.previewInk, alignment: .left, maxLines: 3))
805
         main.addArrangedSubview(makeLabel(CVPreviewDemoContent.summary, font: .systemFont(ofSize: 6.2), color: palette.previewInk, alignment: .left, maxLines: 3))
806
-        main.addArrangedSubview(sectionHeading("IMPACT"))
806
+        main.addArrangedSubview(sectionHeading(L("IMPACT")))
807
         main.addArrangedSubview(makeLabel("\(CVPreviewDemoContent.company) — \(CVPreviewDemoContent.experienceRole)", font: .systemFont(ofSize: 6.6, weight: .heavy), color: palette.previewInk, alignment: .left, maxLines: 2))
807
         main.addArrangedSubview(makeLabel("\(CVPreviewDemoContent.company) — \(CVPreviewDemoContent.experienceRole)", font: .systemFont(ofSize: 6.6, weight: .heavy), color: palette.previewInk, alignment: .left, maxLines: 2))
808
         let bMark = (layoutVariant % 2 == 0) ? "—  " : "▸  "
808
         let bMark = (layoutVariant % 2 == 0) ? "—  " : "▸  "
809
         main.addArrangedSubview(makeLabel("\(bMark)\(CVPreviewDemoContent.bullet1)", font: .systemFont(ofSize: 6), color: palette.previewMuted, alignment: .left, maxLines: 2))
809
         main.addArrangedSubview(makeLabel("\(bMark)\(CVPreviewDemoContent.bullet1)", font: .systemFont(ofSize: 6), color: palette.previewMuted, alignment: .left, maxLines: 2))
810
         main.addArrangedSubview(makeLabel("\(bMark)\(CVPreviewDemoContent.bullet2)", font: .systemFont(ofSize: 6), color: palette.previewMuted, alignment: .left, maxLines: 2))
810
         main.addArrangedSubview(makeLabel("\(bMark)\(CVPreviewDemoContent.bullet2)", font: .systemFont(ofSize: 6), color: palette.previewMuted, alignment: .left, maxLines: 2))
811
-        main.addArrangedSubview(sectionHeading("EDUCATION"))
811
+        main.addArrangedSubview(sectionHeading(L("EDUCATION")))
812
         main.addArrangedSubview(makeLabel("\(CVPreviewDemoContent.university) · \(CVPreviewDemoContent.degree) · \(CVPreviewDemoContent.educationYears)", font: .systemFont(ofSize: 6.1), color: palette.previewInk, alignment: .left, maxLines: 2))
812
         main.addArrangedSubview(makeLabel("\(CVPreviewDemoContent.university) · \(CVPreviewDemoContent.degree) · \(CVPreviewDemoContent.educationYears)", font: .systemFont(ofSize: 6.1), color: palette.previewInk, alignment: .left, maxLines: 2))
813
 
813
 
814
         let sidebarMult = 0.32 + CGFloat(layoutVariant % 3) * 0.02
814
         let sidebarMult = 0.32 + CGFloat(layoutVariant % 3) * 0.02

+ 207 - 109
App for Indeed/Views/DashboardView.swift

@@ -120,7 +120,7 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
120
     private let findJobsButton = HoverableButton()
120
     private let findJobsButton = HoverableButton()
121
     private let findJobsCTAPill = HoverableView()
121
     private let findJobsCTAPill = HoverableView()
122
     private let sendIconView = NSImageView()
122
     private let sendIconView = NSImageView()
123
-    private let sendLabel = NSTextField(labelWithString: "Send")
123
+    private let sendLabel = NSTextField(labelWithString: L("Send"))
124
     private let sendContentStack = NSStackView()
124
     private let sendContentStack = NSStackView()
125
     private let findJobsCTAHost = NSView()
125
     private let findJobsCTAHost = NSView()
126
     private let welcomeHeroHost = NSView()
126
     private let welcomeHeroHost = NSView()
@@ -129,7 +129,7 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
129
     private let welcomeLogoView = IndeedLogoView(displayHeight: 40, variant: .compact)
129
     private let welcomeLogoView = IndeedLogoView(displayHeight: 40, variant: .compact)
130
     private let featureCardsRow = NSStackView()
130
     private let featureCardsRow = NSStackView()
131
     private enum FeatureShortcut: Int { case role = 0, company = 1, skill = 2 }
131
     private enum FeatureShortcut: Int { case role = 0, company = 1, skill = 2 }
132
-    private let clearChatButton = NSButton(title: "Clear chat", target: nil, action: nil)
132
+    private let clearChatButton = NSButton(title: L("Clear chat"), target: nil, action: nil)
133
     private let chatScrollView = NSScrollView()
133
     private let chatScrollView = NSScrollView()
134
     private let chatDocumentView = JobListingsDocumentView()
134
     private let chatDocumentView = JobListingsDocumentView()
135
     private let chatStack = NSStackView()
135
     private let chatStack = NSStackView()
@@ -141,7 +141,7 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
141
     private let nonHomeTitleLabel = NSTextField(labelWithString: "")
141
     private let nonHomeTitleLabel = NSTextField(labelWithString: "")
142
     private let nonHomeSubtitleLabel = NSTextField(wrappingLabelWithString: "")
142
     private let nonHomeSubtitleLabel = NSTextField(wrappingLabelWithString: "")
143
     private let savedJobsPageContainer = NSView()
143
     private let savedJobsPageContainer = NSView()
144
-    private let savedJobsPageTitleLabel = NSTextField(labelWithString: "Saved Jobs")
144
+    private let savedJobsPageTitleLabel = NSTextField(labelWithString: L("Saved Jobs"))
145
     private let savedJobsPageSubtitleLabel = NSTextField(wrappingLabelWithString: "")
145
     private let savedJobsPageSubtitleLabel = NSTextField(wrappingLabelWithString: "")
146
     private let savedJobsScrollView = NSScrollView()
146
     private let savedJobsScrollView = NSScrollView()
147
     private let savedJobsDocumentView = JobListingsDocumentView()
147
     private let savedJobsDocumentView = JobListingsDocumentView()
@@ -193,6 +193,7 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
193
     private weak var sidebarUpgradeButton: HoverableButton?
193
     private weak var sidebarUpgradeButton: HoverableButton?
194
     private var subscriptionObserver: NSObjectProtocol?
194
     private var subscriptionObserver: NSObjectProtocol?
195
     private var appearanceObserver: NSObjectProtocol?
195
     private var appearanceObserver: NSObjectProtocol?
196
+    private var languageObserver: NSObjectProtocol?
196
     /// Retains the system share picker until the user picks a destination or dismisses the menu.
197
     /// Retains the system share picker until the user picks a destination or dismisses the menu.
197
     private var appSharePicker: NSSharingServicePicker?
198
     private var appSharePicker: NSSharingServicePicker?
198
 
199
 
@@ -226,6 +227,9 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
226
         if let appearanceObserver {
227
         if let appearanceObserver {
227
             NotificationCenter.default.removeObserver(appearanceObserver)
228
             NotificationCenter.default.removeObserver(appearanceObserver)
228
         }
229
         }
230
+        if let languageObserver {
231
+            NotificationCenter.default.removeObserver(languageObserver)
232
+        }
229
     }
233
     }
230
 
234
 
231
     override func viewDidChangeEffectiveAppearance() {
235
     override func viewDidChangeEffectiveAppearance() {
@@ -241,6 +245,103 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
241
         ) { [weak self] _ in
245
         ) { [weak self] _ in
242
             self?.applyCurrentAppearance()
246
             self?.applyCurrentAppearance()
243
         }
247
         }
248
+        languageObserver = NotificationCenter.default.addObserver(
249
+            forName: AppLanguageManager.didChangeNotification,
250
+            object: nil,
251
+            queue: .main
252
+        ) { [weak self] _ in
253
+            self?.refreshLocalizedStrings()
254
+        }
255
+    }
256
+
257
+    private func refreshLocalizedStrings() {
258
+        greetingLabel.stringValue = L("Welcome")
259
+        subtitleLabel.stringValue = L("Find your perfect job with the power of AI.")
260
+        sendLabel.stringValue = L("Send")
261
+        clearChatButton.title = L("Clear chat")
262
+        clearChatButton.toolTip = L("Remove all messages and start a new conversation")
263
+        savedJobsPageTitleLabel.stringValue = L("Saved Jobs")
264
+        nonHomeSubtitleLabel.stringValue = L("This area is not available in the preview build. Use Home to search jobs.")
265
+
266
+        jobKeywordsField.placeholderAttributedString = NSAttributedString(
267
+            string: L("Ask for roles, skills, salary, or job descriptions..."),
268
+            attributes: [
269
+                .foregroundColor: Theme.secondaryText,
270
+                .font: NSFont.systemFont(ofSize: 14, weight: .regular)
271
+            ]
272
+        )
273
+        jobSearchIcon.setAccessibilityLabel(L("Ask AI"))
274
+        findJobsButton.setAccessibilityLabel(L("Send"))
275
+
276
+        appearanceModeSegment?.setLabel(L("System"), forSegment: 0)
277
+        appearanceModeSegment?.setLabel(L("Light"), forSegment: 1)
278
+        appearanceModeSegment?.setLabel(L("Dark"), forSegment: 2)
279
+
280
+        refreshLanguagePopUp()
281
+        refreshSettingsLocalizedLabels()
282
+        refreshSidebarItemTitles()
283
+        updateFreeJobSearchQuotaLabel()
284
+        applyProSubscriptionToSidebar()
285
+        configureSidebar()
286
+        reloadSavedJobsListings()
287
+        rebuildFeatureShortcutCards()
288
+        trailingLoadMoreJobsButton?.title = L("Show more jobs")
289
+    }
290
+
291
+    private func refreshSidebarItemTitles() {
292
+        currentSidebarItems = currentSidebarItems.map { item in
293
+            SidebarItem(
294
+                title: localizedSidebarTitle(forSystemImage: item.systemImage),
295
+                systemImage: item.systemImage,
296
+                badge: item.badge
297
+            )
298
+        }
299
+    }
300
+
301
+    private func localizedSidebarTitle(forSystemImage systemImage: String) -> String {
302
+        switch systemImage {
303
+        case "house.fill":
304
+            return L("Home")
305
+        case "heart":
306
+            return L("Saved Jobs")
307
+        case "doc.text":
308
+            return L("CV Maker")
309
+        case "person":
310
+            return L("Profile")
311
+        case "gearshape":
312
+            return L("Settings")
313
+        default:
314
+            return L("Home")
315
+        }
316
+    }
317
+
318
+    private func refreshLanguagePopUp() {
319
+        guard let popup = languagePopUp else { return }
320
+        let selectedCode = AppLanguageManager.shared.current.localeIdentifier
321
+        popup.removeAllItems()
322
+        for language in AppLanguage.allCases {
323
+            popup.addItem(withTitle: language.localizedDisplayName)
324
+            popup.lastItem?.representedObject = language.localeIdentifier
325
+        }
326
+        if let index = AppLanguage.allCases.firstIndex(where: { $0.localeIdentifier == selectedCode }) {
327
+            popup.selectItem(at: index)
328
+        }
329
+        popup.isEnabled = !AppLanguage.allCases.isEmpty
330
+    }
331
+
332
+    private func refreshSettingsLocalizedLabels() {
333
+        for view in settingsPageContainer.subviewsRecursive() {
334
+            guard let rawID = view.identifier?.rawValue else { continue }
335
+            let sectionPrefix = SettingsAppearanceID.sectionHeader + "."
336
+            let rowPrefix = SettingsAppearanceID.rowTitle + "."
337
+            if rawID.hasPrefix(sectionPrefix) {
338
+                let key = String(rawID.dropFirst(sectionPrefix.count))
339
+                (view as? NSTextField)?.stringValue = L(key)
340
+            } else if rawID.hasPrefix(rowPrefix) {
341
+                let key = String(rawID.dropFirst(rowPrefix.count))
342
+                (view as? NSTextField)?.stringValue = L(key)
343
+            }
344
+        }
244
     }
345
     }
245
 
346
 
246
     override func viewDidMoveToWindow() {
347
     override func viewDidMoveToWindow() {
@@ -262,7 +363,7 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
262
 
363
 
263
     func render(_ data: DashboardData) {
364
     func render(_ data: DashboardData) {
264
         dismissIndeedJobBrowserEmbedded()
365
         dismissIndeedJobBrowserEmbedded()
265
-        greetingLabel.stringValue = "Welcome"
366
+        greetingLabel.stringValue = L("Welcome")
266
         subtitleLabel.stringValue = data.subtitle
367
         subtitleLabel.stringValue = data.subtitle
267
         currentSidebarItems = data.sidebarItems
368
         currentSidebarItems = data.sidebarItems
268
         if selectedSidebarIndex >= currentSidebarItems.count {
369
         if selectedSidebarIndex >= currentSidebarItems.count {
@@ -303,7 +404,7 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
303
         searchCard.layer?.borderColor = (searchHovering ? Theme.searchBarBorderHover : Theme.searchBarBorder).cgColor
404
         searchCard.layer?.borderColor = (searchHovering ? Theme.searchBarBorderHover : Theme.searchBarBorder).cgColor
304
         jobKeywordsField.textColor = Theme.primaryText
405
         jobKeywordsField.textColor = Theme.primaryText
305
         jobKeywordsField.placeholderAttributedString = NSAttributedString(
406
         jobKeywordsField.placeholderAttributedString = NSAttributedString(
306
-            string: "Ask for roles, skills, salary, or job descriptions...",
407
+            string: L("Ask for roles, skills, salary, or job descriptions..."),
307
             attributes: [
408
             attributes: [
308
                 .foregroundColor: Theme.secondaryText,
409
                 .foregroundColor: Theme.secondaryText,
309
                 .font: NSFont.systemFont(ofSize: 14, weight: .regular)
410
                 .font: NSFont.systemFont(ofSize: 14, weight: .regular)
@@ -318,11 +419,11 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
318
 
419
 
319
         appearanceModeSegment?.selectedSegment = AppAppearanceManager.shared.mode.segmentIndex
420
         appearanceModeSegment?.selectedSegment = AppAppearanceManager.shared.mode.segmentIndex
320
         if let langPopUp = languagePopUp {
421
         if let langPopUp = languagePopUp {
321
-            let saved = UserDefaults.standard.string(forKey: Self.languageUserDefaultsKey) ?? "en"
322
-            if let index = Self.supportedLanguages.firstIndex(where: { $0.code == saved }) {
422
+            let saved = AppLanguageManager.shared.current.localeIdentifier
423
+            if let index = AppLanguage.allCases.firstIndex(where: { $0.localeIdentifier == saved }) {
323
                 langPopUp.selectItem(at: index)
424
                 langPopUp.selectItem(at: index)
324
             }
425
             }
325
-            langPopUp.isEnabled = !Self.supportedLanguages.isEmpty
426
+            langPopUp.isEnabled = !AppLanguage.allCases.isEmpty
326
         }
427
         }
327
         cvMakerPageView.applyCurrentAppearance()
428
         cvMakerPageView.applyCurrentAppearance()
328
         profilesListPageView.applyCurrentAppearance()
429
         profilesListPageView.applyCurrentAppearance()
@@ -340,7 +441,8 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
340
 
441
 
341
     private func refreshSettingsPageAppearance(in root: NSView) {
442
     private func refreshSettingsPageAppearance(in root: NSView) {
342
         for view in root.subviewsRecursive() {
443
         for view in root.subviewsRecursive() {
343
-            switch view.identifier?.rawValue {
444
+            guard let rawID = view.identifier?.rawValue else { continue }
445
+            switch rawID {
344
             case SettingsAppearanceID.section:
446
             case SettingsAppearanceID.section:
345
                 view.layer?.backgroundColor = Theme.settingsGroupBackground.cgColor
447
                 view.layer?.backgroundColor = Theme.settingsGroupBackground.cgColor
346
                 view.layer?.borderColor = Theme.border.cgColor
448
                 view.layer?.borderColor = Theme.border.cgColor
@@ -351,12 +453,12 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
351
                 for case let icon as NSImageView in view.subviews {
453
                 for case let icon as NSImageView in view.subviews {
352
                     icon.contentTintColor = Theme.brandBlue
454
                     icon.contentTintColor = Theme.brandBlue
353
                 }
455
                 }
354
-            case SettingsAppearanceID.sectionHeader:
355
-                (view as? NSTextField)?.textColor = Theme.secondaryText
356
-            case SettingsAppearanceID.rowTitle:
357
-                (view as? NSTextField)?.textColor = Theme.primaryText
358
             default:
456
             default:
359
-                break
457
+                if rawID.hasPrefix(SettingsAppearanceID.sectionHeader + ".") {
458
+                    (view as? NSTextField)?.textColor = Theme.secondaryText
459
+                } else if rawID.hasPrefix(SettingsAppearanceID.rowTitle + ".") {
460
+                    (view as? NSTextField)?.textColor = Theme.primaryText
461
+                }
360
             }
462
             }
361
         }
463
         }
362
     }
464
     }
@@ -524,7 +626,7 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
524
         clearChatButton.font = .systemFont(ofSize: 12, weight: .medium)
626
         clearChatButton.font = .systemFont(ofSize: 12, weight: .medium)
525
         clearChatButton.target = self
627
         clearChatButton.target = self
526
         clearChatButton.action = #selector(didTapClearChat)
628
         clearChatButton.action = #selector(didTapClearChat)
527
-        clearChatButton.toolTip = "Remove all messages and start a new conversation"
629
+        clearChatButton.toolTip = L("Remove all messages and start a new conversation")
528
         clearChatButton.setContentHuggingPriority(.required, for: .horizontal)
630
         clearChatButton.setContentHuggingPriority(.required, for: .horizontal)
529
         chatHeaderRow.addArrangedSubview(chatHeaderLeadingSpacer)
631
         chatHeaderRow.addArrangedSubview(chatHeaderLeadingSpacer)
530
         chatHeaderRow.addArrangedSubview(clearChatButton)
632
         chatHeaderRow.addArrangedSubview(clearChatButton)
@@ -589,6 +691,7 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
589
             welcomeHeroContent.widthAnchor.constraint(equalTo: mainOverlay.widthAnchor, constant: -32)
691
             welcomeHeroContent.widthAnchor.constraint(equalTo: mainOverlay.widthAnchor, constant: -32)
590
         ])
692
         ])
591
         registerSubscriptionObserverOnce()
693
         registerSubscriptionObserverOnce()
694
+        refreshLocalizedStrings()
592
     }
695
     }
593
 
696
 
594
     private func registerSubscriptionObserverOnce() {
697
     private func registerSubscriptionObserverOnce() {
@@ -612,15 +715,15 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
612
 
715
 
613
         let descriptionWidth: CGFloat = 158
716
         let descriptionWidth: CGFloat = 158
614
         if active {
717
         if active {
615
-            headline.stringValue = "You're on Pro"
616
-            upgradeDescription.stringValue = "Manage billing, renewals, and plans in Premium."
718
+            headline.stringValue = L("You're on Pro")
719
+            upgradeDescription.stringValue = L("Manage billing, renewals, and plans in Premium.")
617
             upgradeDescription.preferredMaxLayoutWidth = descriptionWidth
720
             upgradeDescription.preferredMaxLayoutWidth = descriptionWidth
618
-            upgradeButton.title = "Manage Subscription"
721
+            upgradeButton.title = L("Manage Subscription")
619
         } else {
722
         } else {
620
-            headline.stringValue = "Upgrade to Pro"
621
-            upgradeDescription.stringValue = "Unlimited AI matches, smart alerts, and interview prep—all in one place."
723
+            headline.stringValue = L("Upgrade to Pro")
724
+            upgradeDescription.stringValue = L("Unlimited AI matches, smart alerts, and interview prep—all in one place.")
622
             upgradeDescription.preferredMaxLayoutWidth = descriptionWidth
725
             upgradeDescription.preferredMaxLayoutWidth = descriptionWidth
623
-            upgradeButton.title = "Try Pro"
726
+            upgradeButton.title = L("Try Pro")
624
         }
727
         }
625
         updateFreeJobSearchQuotaLabel()
728
         updateFreeJobSearchQuotaLabel()
626
     }
729
     }
@@ -635,8 +738,8 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
635
         let remaining = FreeTierJobSearchQuota.remainingUserMessages(isProActive: false)
738
         let remaining = FreeTierJobSearchQuota.remainingUserMessages(isProActive: false)
636
         freeJobSearchQuotaLabel.isHidden = false
739
         freeJobSearchQuotaLabel.isHidden = false
637
         freeJobSearchQuotaLabel.stringValue = remaining == 1
740
         freeJobSearchQuotaLabel.stringValue = remaining == 1
638
-            ? "1 reply left"
639
-            : "\(remaining) replies left"
741
+            ? L("1 reply left")
742
+            : String(format: L("%d replies left"), remaining)
640
     }
743
     }
641
 
744
 
642
     /// Returns `false` and presents the paywall when the user does not have an active Pro subscription.
745
     /// Returns `false` and presents the paywall when the user does not have an active Pro subscription.
@@ -696,7 +799,7 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
696
         featureCardsRow.alignment = .top
799
         featureCardsRow.alignment = .top
697
         featureCardsRow.translatesAutoresizingMaskIntoConstraints = false
800
         featureCardsRow.translatesAutoresizingMaskIntoConstraints = false
698
 
801
 
699
-        let specs: [(symbol: String, title: String, subtitle: String, action: Selector)] = [
802
+        let specs: [(symbol: String, titleKey: String, subtitleKey: String, action: Selector)] = [
700
             ("briefcase", "Role", "Explore similar or better job roles", #selector(didTapFeatureRole)),
803
             ("briefcase", "Role", "Explore similar or better job roles", #selector(didTapFeatureRole)),
701
             ("building.2", "Company", "Find opportunities at other companies", #selector(didTapFeatureCompany)),
804
             ("building.2", "Company", "Find opportunities at other companies", #selector(didTapFeatureCompany)),
702
             ("chevron.left.forwardslash.chevron.right", "Skill", "Match jobs that fit your skills", #selector(didTapFeatureSkill))
805
             ("chevron.left.forwardslash.chevron.right", "Skill", "Match jobs that fit your skills", #selector(didTapFeatureSkill))
@@ -704,8 +807,8 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
704
         for spec in specs {
807
         for spec in specs {
705
             let card = FeatureShortcutCardView(
808
             let card = FeatureShortcutCardView(
706
                 symbolName: spec.symbol,
809
                 symbolName: spec.symbol,
707
-                title: spec.title,
708
-                subtitle: spec.subtitle,
810
+                title: L(spec.titleKey),
811
+                subtitle: L(spec.subtitleKey),
709
                 target: self,
812
                 target: self,
710
                 action: spec.action
813
                 action: spec.action
711
             )
814
             )
@@ -841,7 +944,7 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
841
 
944
 
842
     private func jobListingHostSubtitle(_ job: JobListing) -> String {
945
     private func jobListingHostSubtitle(_ job: JobListing) -> String {
843
         guard let raw = job.url, let url = URL(string: raw), let host = url.host?.lowercased() else {
946
         guard let raw = job.url, let url = URL(string: raw), let host = url.host?.lowercased() else {
844
-            return "Indeed"
947
+            return L("Indeed")
845
         }
948
         }
846
         if host.hasPrefix("www.") {
949
         if host.hasPrefix("www.") {
847
             return String(host.dropFirst(4))
950
             return String(host.dropFirst(4))
@@ -943,7 +1046,7 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
943
         descriptionField.tag = 502
1046
         descriptionField.tag = 502
944
         descriptionField.translatesAutoresizingMaskIntoConstraints = false
1047
         descriptionField.translatesAutoresizingMaskIntoConstraints = false
945
 
1048
 
946
-        let applyButton = JobPayloadButton(title: "Apply", target: self, action: #selector(didTapJobApply(_:)))
1049
+        let applyButton = JobPayloadButton(title: L("Apply"), target: self, action: #selector(didTapJobApply(_:)))
947
         applyButton.jobPayload = job
1050
         applyButton.jobPayload = job
948
         applyButton.cardContext = context
1051
         applyButton.cardContext = context
949
         applyButton.isBordered = false
1052
         applyButton.isBordered = false
@@ -962,7 +1065,7 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
962
         applyButton.setContentCompressionResistancePriority(.required, for: .horizontal)
1065
         applyButton.setContentCompressionResistancePriority(.required, for: .horizontal)
963
 
1066
 
964
         let savedOn = isJobSaved(job)
1067
         let savedOn = isJobSaved(job)
965
-        let savedButton = SaveJobPayloadButton(title: savedOn ? "Saved" : "Save", target: self, action: #selector(didTapJobSaved(_:)))
1068
+        let savedButton = SaveJobPayloadButton(title: savedOn ? L("Saved") : L("Save"), target: self, action: #selector(didTapJobSaved(_:)))
966
         savedButton.jobPayload = job
1069
         savedButton.jobPayload = job
967
         savedButton.cardContext = context
1070
         savedButton.cardContext = context
968
         savedButton.setButtonType(.toggle)
1071
         savedButton.setButtonType(.toggle)
@@ -986,7 +1089,7 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
986
         let dismissButton = JobPayloadButton()
1089
         let dismissButton = JobPayloadButton()
987
         dismissButton.jobPayload = job
1090
         dismissButton.jobPayload = job
988
         dismissButton.cardContext = context
1091
         dismissButton.cardContext = context
989
-        dismissButton.image = NSImage(systemSymbolName: "xmark", accessibilityDescription: "Dismiss")
1092
+        dismissButton.image = NSImage(systemSymbolName: "xmark", accessibilityDescription: L("Dismiss"))
990
         dismissButton.imagePosition = .imageOnly
1093
         dismissButton.imagePosition = .imageOnly
991
         dismissButton.imageScaling = .scaleProportionallyDown
1094
         dismissButton.imageScaling = .scaleProportionallyDown
992
         dismissButton.symbolConfiguration = NSImage.SymbolConfiguration(pointSize: 12, weight: .semibold)
1095
         dismissButton.symbolConfiguration = NSImage.SymbolConfiguration(pointSize: 12, weight: .semibold)
@@ -995,7 +1098,7 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
995
         dismissButton.contentTintColor = Theme.secondaryText
1098
         dismissButton.contentTintColor = Theme.secondaryText
996
         dismissButton.target = self
1099
         dismissButton.target = self
997
         dismissButton.action = #selector(didTapJobDismiss(_:))
1100
         dismissButton.action = #selector(didTapJobDismiss(_:))
998
-        dismissButton.toolTip = context == .savedJobsPage ? "Remove from saved" : "Dismiss"
1101
+        dismissButton.toolTip = context == .savedJobsPage ? L("Remove from saved") : L("Dismiss")
999
         dismissButton.focusRingType = .none
1102
         dismissButton.focusRingType = .none
1000
         dismissButton.wantsLayer = true
1103
         dismissButton.wantsLayer = true
1001
         dismissButton.layer?.cornerRadius = 8
1104
         dismissButton.layer?.cornerRadius = 8
@@ -1199,7 +1302,7 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
1199
         let willSave = !isJobSaved(job)
1302
         let willSave = !isJobSaved(job)
1200
         applySavedState(willSave, for: job)
1303
         applySavedState(willSave, for: job)
1201
         sender.state = willSave ? .on : .off
1304
         sender.state = willSave ? .on : .off
1202
-        sender.title = willSave ? "Saved" : "Save"
1305
+        sender.title = willSave ? L("Saved") : L("Save")
1203
         sender.image = NSImage(systemSymbolName: willSave ? "heart.fill" : "heart", accessibilityDescription: nil)
1306
         sender.image = NSImage(systemSymbolName: willSave ? "heart.fill" : "heart", accessibilityDescription: nil)
1204
         styleJobSavedButton(sender)
1307
         styleJobSavedButton(sender)
1205
         if isSavedJobsSidebarIndex(selectedSidebarIndex) {
1308
         if isSavedJobsSidebarIndex(selectedSidebarIndex) {
@@ -1313,10 +1416,10 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
1313
 
1416
 
1314
         jobSearchIcon.translatesAutoresizingMaskIntoConstraints = false
1417
         jobSearchIcon.translatesAutoresizingMaskIntoConstraints = false
1315
         jobSearchIcon.symbolConfiguration = NSImage.SymbolConfiguration(pointSize: 16, weight: .semibold)
1418
         jobSearchIcon.symbolConfiguration = NSImage.SymbolConfiguration(pointSize: 16, weight: .semibold)
1316
-        jobSearchIcon.image = NSImage(systemSymbolName: "sparkles", accessibilityDescription: "Ask AI")
1419
+        jobSearchIcon.image = NSImage(systemSymbolName: "sparkles", accessibilityDescription: L("Ask AI"))
1317
         jobSearchIcon.contentTintColor = Theme.brandBlue
1420
         jobSearchIcon.contentTintColor = Theme.brandBlue
1318
 
1421
 
1319
-        configureField(jobKeywordsField, placeholder: "Ask for roles, skills, salary, or job descriptions...")
1422
+        configureField(jobKeywordsField, placeholder: L("Ask for roles, skills, salary, or job descriptions..."))
1320
 
1423
 
1321
         let ctaHeight: CGFloat = 42
1424
         let ctaHeight: CGFloat = 42
1322
         let ctaCorner = ctaHeight / 2
1425
         let ctaCorner = ctaHeight / 2
@@ -1366,7 +1469,7 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
1366
         findJobsButton.pointerCursor = true
1469
         findJobsButton.pointerCursor = true
1367
         findJobsButton.target = self
1470
         findJobsButton.target = self
1368
         findJobsButton.action = #selector(didSubmitSearch)
1471
         findJobsButton.action = #selector(didSubmitSearch)
1369
-        findJobsButton.setAccessibilityLabel("Send")
1472
+        findJobsButton.setAccessibilityLabel(L("Send"))
1370
         findJobsButton.hoverHandler = { [weak self] hovering in
1473
         findJobsButton.hoverHandler = { [weak self] hovering in
1371
             self?.findJobsCTAPill.layer?.backgroundColor = (hovering ? Theme.brandBlueHover : Theme.brandBlue).cgColor
1474
             self?.findJobsCTAPill.layer?.backgroundColor = (hovering ? Theme.brandBlueHover : Theme.brandBlue).cgColor
1372
         }
1475
         }
@@ -1503,7 +1606,7 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
1503
         nonHomeSubtitleLabel.textColor = Theme.secondaryText
1606
         nonHomeSubtitleLabel.textColor = Theme.secondaryText
1504
         nonHomeSubtitleLabel.alignment = .center
1607
         nonHomeSubtitleLabel.alignment = .center
1505
         nonHomeSubtitleLabel.maximumNumberOfLines = 0
1608
         nonHomeSubtitleLabel.maximumNumberOfLines = 0
1506
-        nonHomeSubtitleLabel.stringValue = "This area is not available in the preview build. Use Home to search jobs."
1609
+        nonHomeSubtitleLabel.stringValue = L("This area is not available in the preview build. Use Home to search jobs.")
1507
 
1610
 
1508
         let genericStack = NSStackView(views: [nonHomeTitleLabel, nonHomeSubtitleLabel])
1611
         let genericStack = NSStackView(views: [nonHomeTitleLabel, nonHomeSubtitleLabel])
1509
         genericStack.orientation = .vertical
1612
         genericStack.orientation = .vertical
@@ -1613,7 +1716,7 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
1613
 
1716
 
1614
     /// Switches the main panel to **Profile** so the user can pick a saved CV profile after choosing a template in CV Maker.
1717
     /// Switches the main panel to **Profile** so the user can pick a saved CV profile after choosing a template in CV Maker.
1615
     private func selectProfileSidebarForCVMakerFlow() {
1718
     private func selectProfileSidebarForCVMakerFlow() {
1616
-        guard let index = currentSidebarItems.firstIndex(where: { $0.title == "Profile" }) else { return }
1719
+        guard let index = currentSidebarItems.firstIndex(where: { $0.title == L("Profile") }) else { return }
1617
         selectSidebarItem(at: index)
1720
         selectSidebarItem(at: index)
1618
     }
1721
     }
1619
 
1722
 
@@ -1719,13 +1822,13 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
1719
     private func confirmDeleteProfile(id: UUID) {
1822
     private func confirmDeleteProfile(id: UUID) {
1720
         let displayName = SavedProfilesStore.profile(id: id)?.profileDisplayName ?? ""
1823
         let displayName = SavedProfilesStore.profile(id: id)?.profileDisplayName ?? ""
1721
         let alert = NSAlert()
1824
         let alert = NSAlert()
1722
-        alert.messageText = "Delete this profile?"
1825
+        alert.messageText = L("Delete this profile?")
1723
         alert.informativeText = displayName.isEmpty
1826
         alert.informativeText = displayName.isEmpty
1724
-            ? "This profile will be removed from this Mac."
1725
-            : "“\(displayName)” will be removed from this Mac."
1827
+            ? L("This profile will be removed from this Mac.")
1828
+            : String(format: L("“%@” will be removed from this Mac."), displayName)
1726
         alert.alertStyle = .warning
1829
         alert.alertStyle = .warning
1727
-        alert.addButton(withTitle: "Cancel")
1728
-        alert.addButton(withTitle: "Delete")
1830
+        alert.addButton(withTitle: L("Cancel"))
1831
+        alert.addButton(withTitle: L("Delete"))
1729
         guard let window = window else {
1832
         guard let window = window else {
1730
             let response = alert.runModal()
1833
             let response = alert.runModal()
1731
             if response == .alertSecondButtonReturn {
1834
             if response == .alertSecondButtonReturn {
@@ -1758,19 +1861,19 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
1758
         contentStack.alignment = .leading
1861
         contentStack.alignment = .leading
1759
         contentStack.translatesAutoresizingMaskIntoConstraints = false
1862
         contentStack.translatesAutoresizingMaskIntoConstraints = false
1760
 
1863
 
1761
-        let appearanceTitle = NSTextField(labelWithString: "Appearance")
1864
+        let appearanceTitle = NSTextField(labelWithString: L("Appearance"))
1762
         appearanceTitle.font = .systemFont(ofSize: 12, weight: .semibold)
1865
         appearanceTitle.font = .systemFont(ofSize: 12, weight: .semibold)
1763
         appearanceTitle.textColor = Theme.secondaryText
1866
         appearanceTitle.textColor = Theme.secondaryText
1764
         appearanceTitle.alignment = .left
1867
         appearanceTitle.alignment = .left
1765
-        appearanceTitle.identifier = NSUserInterfaceItemIdentifier(SettingsAppearanceID.sectionHeader)
1868
+        appearanceTitle.identifier = NSUserInterfaceItemIdentifier("\(SettingsAppearanceID.sectionHeader).Appearance")
1766
 
1869
 
1767
         let themeSegment = makeAppearanceModeSegment()
1870
         let themeSegment = makeAppearanceModeSegment()
1768
         appearanceModeSegment = themeSegment
1871
         appearanceModeSegment = themeSegment
1769
         let langPopUp = makeLanguagePopUp()
1872
         let langPopUp = makeLanguagePopUp()
1770
         languagePopUp = langPopUp
1873
         languagePopUp = langPopUp
1771
         let appearanceSection = makeSettingsSection(rows: [
1874
         let appearanceSection = makeSettingsSection(rows: [
1772
-            makeSettingsRow(title: "Theme", systemImage: "circle.lefthalf.filled", accessory: themeSegment, tapAction: nil),
1773
-            makeSettingsRow(title: "Language", systemImage: "character.bubble", accessory: langPopUp, tapAction: nil)
1875
+            makeSettingsRow(localizationKey: "Theme", systemImage: "circle.lefthalf.filled", accessory: themeSegment, tapAction: nil),
1876
+            makeSettingsRow(localizationKey: "Language", systemImage: "character.bubble", accessory: langPopUp, tapAction: nil)
1774
         ])
1877
         ])
1775
 
1878
 
1776
         let appearanceStack = NSStackView(views: [appearanceTitle, appearanceSection])
1879
         let appearanceStack = NSStackView(views: [appearanceTitle, appearanceSection])
@@ -1780,21 +1883,21 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
1780
         appearanceStack.translatesAutoresizingMaskIntoConstraints = false
1883
         appearanceStack.translatesAutoresizingMaskIntoConstraints = false
1781
 
1884
 
1782
         let settingsSection = makeSettingsSection(rows: [
1885
         let settingsSection = makeSettingsSection(rows: [
1783
-            makeSettingsRow(title: "Share App", systemImage: "square.and.arrow.up", accessory: nil, tapAction: #selector(didTapShareApp)),
1784
-            makeSettingsRow(title: "More Apps", systemImage: "square.grid.2x2", accessory: nil, tapAction: #selector(didTapMoreApps))
1886
+            makeSettingsRow(localizationKey: "Share App", systemImage: "square.and.arrow.up", accessory: nil, tapAction: #selector(didTapShareApp)),
1887
+            makeSettingsRow(localizationKey: "More Apps", systemImage: "square.grid.2x2", accessory: nil, tapAction: #selector(didTapMoreApps))
1785
         ])
1888
         ])
1786
 
1889
 
1787
-        let aboutTitle = NSTextField(labelWithString: "About")
1890
+        let aboutTitle = NSTextField(labelWithString: L("About"))
1788
         aboutTitle.font = .systemFont(ofSize: 12, weight: .semibold)
1891
         aboutTitle.font = .systemFont(ofSize: 12, weight: .semibold)
1789
         aboutTitle.textColor = Theme.secondaryText
1892
         aboutTitle.textColor = Theme.secondaryText
1790
         aboutTitle.alignment = .left
1893
         aboutTitle.alignment = .left
1791
-        aboutTitle.identifier = NSUserInterfaceItemIdentifier(SettingsAppearanceID.sectionHeader)
1894
+        aboutTitle.identifier = NSUserInterfaceItemIdentifier("\(SettingsAppearanceID.sectionHeader).About")
1792
 
1895
 
1793
         let aboutSection = makeSettingsSection(rows: [
1896
         let aboutSection = makeSettingsSection(rows: [
1794
-            makeSettingsRow(title: "Website", systemImage: "globe", accessory: nil, tapAction: #selector(didTapWebsite)),
1795
-            makeSettingsRow(title: "Support", systemImage: "questionmark.circle", accessory: nil, tapAction: #selector(didTapSupport)),
1796
-            makeSettingsRow(title: "Terms of Use", systemImage: "doc.text", accessory: nil, tapAction: #selector(didTapTermsOfUse)),
1797
-            makeSettingsRow(title: "Privacy Policy", systemImage: "shield", accessory: nil, tapAction: #selector(didTapPrivacyPolicy))
1897
+            makeSettingsRow(localizationKey: "Website", systemImage: "globe", accessory: nil, tapAction: #selector(didTapWebsite)),
1898
+            makeSettingsRow(localizationKey: "Support", systemImage: "questionmark.circle", accessory: nil, tapAction: #selector(didTapSupport)),
1899
+            makeSettingsRow(localizationKey: "Terms of Use", systemImage: "doc.text", accessory: nil, tapAction: #selector(didTapTermsOfUse)),
1900
+            makeSettingsRow(localizationKey: "Privacy Policy", systemImage: "shield", accessory: nil, tapAction: #selector(didTapPrivacyPolicy))
1798
         ])
1901
         ])
1799
 
1902
 
1800
         let aboutStack = NSStackView(views: [aboutTitle, aboutSection])
1903
         let aboutStack = NSStackView(views: [aboutTitle, aboutSection])
@@ -1823,7 +1926,7 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
1823
 
1926
 
1824
     private func makeAppearanceModeSegment() -> NSSegmentedControl {
1927
     private func makeAppearanceModeSegment() -> NSSegmentedControl {
1825
         let segment = NSSegmentedControl(
1928
         let segment = NSSegmentedControl(
1826
-            labels: ["System", "Light", "Dark"],
1929
+            labels: [L("System"), L("Light"), L("Dark")],
1827
             trackingMode: .selectOne,
1930
             trackingMode: .selectOne,
1828
             target: self,
1931
             target: self,
1829
             action: #selector(appearanceModeChanged(_:))
1932
             action: #selector(appearanceModeChanged(_:))
@@ -1841,30 +1944,24 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
1841
         AppAppearanceManager.shared.mode = mode
1944
         AppAppearanceManager.shared.mode = mode
1842
     }
1945
     }
1843
 
1946
 
1844
-    private static let languageUserDefaultsKey = "com.appforindeed.preferredLanguage"
1845
-
1846
-    private static let supportedLanguages: [(code: String, title: String)] = [
1847
-        ("en", "English")
1848
-    ]
1849
-
1850
     private func makeLanguagePopUp() -> NSPopUpButton {
1947
     private func makeLanguagePopUp() -> NSPopUpButton {
1851
         let popup = NSPopUpButton(frame: .zero, pullsDown: false)
1948
         let popup = NSPopUpButton(frame: .zero, pullsDown: false)
1852
         popup.translatesAutoresizingMaskIntoConstraints = false
1949
         popup.translatesAutoresizingMaskIntoConstraints = false
1853
         popup.removeAllItems()
1950
         popup.removeAllItems()
1854
 
1951
 
1855
-        for lang in Self.supportedLanguages {
1856
-            popup.addItem(withTitle: lang.title)
1857
-            popup.lastItem?.representedObject = lang.code
1952
+        for language in AppLanguage.allCases {
1953
+            popup.addItem(withTitle: language.localizedDisplayName)
1954
+            popup.lastItem?.representedObject = language.localeIdentifier
1858
         }
1955
         }
1859
 
1956
 
1860
-        let saved = UserDefaults.standard.string(forKey: Self.languageUserDefaultsKey) ?? "en"
1861
-        if let index = Self.supportedLanguages.firstIndex(where: { $0.code == saved }) {
1957
+        let currentCode = AppLanguageManager.shared.current.localeIdentifier
1958
+        if let index = AppLanguage.allCases.firstIndex(where: { $0.localeIdentifier == currentCode }) {
1862
             popup.selectItem(at: index)
1959
             popup.selectItem(at: index)
1863
         }
1960
         }
1864
 
1961
 
1865
         popup.target = self
1962
         popup.target = self
1866
         popup.action = #selector(languageChanged(_:))
1963
         popup.action = #selector(languageChanged(_:))
1867
-        popup.isEnabled = !Self.supportedLanguages.isEmpty
1964
+        popup.isEnabled = !AppLanguage.allCases.isEmpty
1868
         popup.setContentHuggingPriority(.required, for: .horizontal)
1965
         popup.setContentHuggingPriority(.required, for: .horizontal)
1869
         popup.setContentCompressionResistancePriority(.required, for: .horizontal)
1966
         popup.setContentCompressionResistancePriority(.required, for: .horizontal)
1870
         return popup
1967
         return popup
@@ -1872,8 +1969,7 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
1872
 
1969
 
1873
     @objc private func languageChanged(_ sender: NSPopUpButton) {
1970
     @objc private func languageChanged(_ sender: NSPopUpButton) {
1874
         guard let code = sender.selectedItem?.representedObject as? String else { return }
1971
         guard let code = sender.selectedItem?.representedObject as? String else { return }
1875
-        UserDefaults.standard.set(code, forKey: Self.languageUserDefaultsKey)
1876
-        UserDefaults.standard.set([code], forKey: "AppleLanguages")
1972
+        AppLanguageManager.shared.setLanguage(code: code)
1877
     }
1973
     }
1878
 
1974
 
1879
     private func makeSettingsSection(rows: [NSView]) -> NSView {
1975
     private func makeSettingsSection(rows: [NSView]) -> NSView {
@@ -1912,7 +2008,7 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
1912
         return section
2008
         return section
1913
     }
2009
     }
1914
 
2010
 
1915
-    private func makeSettingsRow(title: String, systemImage: String, accessory: NSView?, tapAction: Selector? = nil) -> NSView {
2011
+    private func makeSettingsRow(localizationKey: String, systemImage: String, accessory: NSView?, tapAction: Selector? = nil) -> NSView {
1916
         let row = NSView()
2012
         let row = NSView()
1917
         row.translatesAutoresizingMaskIntoConstraints = false
2013
         row.translatesAutoresizingMaskIntoConstraints = false
1918
         row.wantsLayer = true
2014
         row.wantsLayer = true
@@ -1927,14 +2023,14 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
1927
         let icon = NSImageView()
2023
         let icon = NSImageView()
1928
         icon.translatesAutoresizingMaskIntoConstraints = false
2024
         icon.translatesAutoresizingMaskIntoConstraints = false
1929
         icon.symbolConfiguration = NSImage.SymbolConfiguration(pointSize: 13, weight: .medium)
2025
         icon.symbolConfiguration = NSImage.SymbolConfiguration(pointSize: 13, weight: .medium)
1930
-        icon.image = NSImage(systemSymbolName: systemImage, accessibilityDescription: title)
2026
+        icon.image = NSImage(systemSymbolName: systemImage, accessibilityDescription: L(localizationKey))
1931
         icon.contentTintColor = Theme.brandBlue
2027
         icon.contentTintColor = Theme.brandBlue
1932
 
2028
 
1933
-        let titleLabel = NSTextField(labelWithString: title)
2029
+        let titleLabel = NSTextField(labelWithString: L(localizationKey))
1934
         titleLabel.font = .systemFont(ofSize: 14, weight: .medium)
2030
         titleLabel.font = .systemFont(ofSize: 14, weight: .medium)
1935
         titleLabel.textColor = Theme.primaryText
2031
         titleLabel.textColor = Theme.primaryText
1936
         titleLabel.alignment = .left
2032
         titleLabel.alignment = .left
1937
-        titleLabel.identifier = NSUserInterfaceItemIdentifier(SettingsAppearanceID.rowTitle)
2033
+        titleLabel.identifier = NSUserInterfaceItemIdentifier("\(SettingsAppearanceID.rowTitle).\(localizationKey)")
1938
 
2034
 
1939
         let rowStack = NSStackView()
2035
         let rowStack = NSStackView()
1940
         rowStack.orientation = .horizontal
2036
         rowStack.orientation = .horizontal
@@ -1997,8 +2093,8 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
1997
             $0.removeFromSuperview()
2093
             $0.removeFromSuperview()
1998
         }
2094
         }
1999
         if savedJobOrder.isEmpty {
2095
         if savedJobOrder.isEmpty {
2000
-            savedJobsPageSubtitleLabel.stringValue = "Save jobs from Home to see them here."
2001
-            let empty = NSTextField(wrappingLabelWithString: "No saved jobs yet. Search on Home, then tap Save on a listing.")
2096
+            savedJobsPageSubtitleLabel.stringValue = L("Save jobs from Home to see them here.")
2097
+            let empty = NSTextField(wrappingLabelWithString: L("No saved jobs yet. Search on Home, then tap Save on a listing."))
2002
             empty.font = .systemFont(ofSize: 14, weight: .regular)
2098
             empty.font = .systemFont(ofSize: 14, weight: .regular)
2003
             empty.textColor = Theme.secondaryText
2099
             empty.textColor = Theme.secondaryText
2004
             empty.alignment = .left
2100
             empty.alignment = .left
@@ -2008,7 +2104,9 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
2008
             empty.widthAnchor.constraint(equalTo: savedJobsStack.widthAnchor).isActive = true
2104
             empty.widthAnchor.constraint(equalTo: savedJobsStack.widthAnchor).isActive = true
2009
             return
2105
             return
2010
         }
2106
         }
2011
-        savedJobsPageSubtitleLabel.stringValue = "\(savedJobOrder.count) saved \(savedJobOrder.count == 1 ? "position" : "positions")"
2107
+        savedJobsPageSubtitleLabel.stringValue = savedJobOrder.count == 1
2108
+            ? L("1 saved position")
2109
+            : String(format: L("%d saved positions"), savedJobOrder.count)
2012
         for job in savedJobOrder {
2110
         for job in savedJobOrder {
2013
             let card = makeJobListingCard(job, context: .savedJobsPage)
2111
             let card = makeJobListingCard(job, context: .savedJobsPage)
2014
             savedJobsStack.addArrangedSubview(card)
2112
             savedJobsStack.addArrangedSubview(card)
@@ -2018,27 +2116,27 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
2018
 
2116
 
2019
     private func isSavedJobsSidebarIndex(_ index: Int) -> Bool {
2117
     private func isSavedJobsSidebarIndex(_ index: Int) -> Bool {
2020
         guard index >= 0, index < currentSidebarItems.count else { return false }
2118
         guard index >= 0, index < currentSidebarItems.count else { return false }
2021
-        return currentSidebarItems[index].title == "Saved Jobs"
2119
+        return currentSidebarItems[index].title == L("Saved Jobs")
2022
     }
2120
     }
2023
 
2121
 
2024
     private func isHomeSidebarIndex(_ index: Int) -> Bool {
2122
     private func isHomeSidebarIndex(_ index: Int) -> Bool {
2025
         guard index >= 0, index < currentSidebarItems.count else { return false }
2123
         guard index >= 0, index < currentSidebarItems.count else { return false }
2026
-        return currentSidebarItems[index].title == "Home"
2124
+        return currentSidebarItems[index].title == L("Home")
2027
     }
2125
     }
2028
 
2126
 
2029
     private func isSettingsSidebarIndex(_ index: Int) -> Bool {
2127
     private func isSettingsSidebarIndex(_ index: Int) -> Bool {
2030
         guard index >= 0, index < currentSidebarItems.count else { return false }
2128
         guard index >= 0, index < currentSidebarItems.count else { return false }
2031
-        return currentSidebarItems[index].title == "Settings"
2129
+        return currentSidebarItems[index].title == L("Settings")
2032
     }
2130
     }
2033
 
2131
 
2034
     private func isCVMakerSidebarIndex(_ index: Int) -> Bool {
2132
     private func isCVMakerSidebarIndex(_ index: Int) -> Bool {
2035
         guard index >= 0, index < currentSidebarItems.count else { return false }
2133
         guard index >= 0, index < currentSidebarItems.count else { return false }
2036
-        return currentSidebarItems[index].title == "CV Maker"
2134
+        return currentSidebarItems[index].title == L("CV Maker")
2037
     }
2135
     }
2038
 
2136
 
2039
     private func isProfileSidebarIndex(_ index: Int) -> Bool {
2137
     private func isProfileSidebarIndex(_ index: Int) -> Bool {
2040
         guard index >= 0, index < currentSidebarItems.count else { return false }
2138
         guard index >= 0, index < currentSidebarItems.count else { return false }
2041
-        return currentSidebarItems[index].title == "Profile"
2139
+        return currentSidebarItems[index].title == L("Profile")
2042
     }
2140
     }
2043
 
2141
 
2044
     private func updateMainContentVisibility() {
2142
     private func updateMainContentVisibility() {
@@ -2132,17 +2230,17 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
2132
 
2230
 
2133
     @objc private func didTapFeatureRole() {
2231
     @objc private func didTapFeatureRole() {
2134
         selectFeatureShortcut(.role)
2232
         selectFeatureShortcut(.role)
2135
-        focusSearchField(seed: "Find roles similar to: ")
2233
+        focusSearchField(seed: L("Find roles similar to: "))
2136
     }
2234
     }
2137
 
2235
 
2138
     @objc private func didTapFeatureCompany() {
2236
     @objc private func didTapFeatureCompany() {
2139
         selectFeatureShortcut(.company)
2237
         selectFeatureShortcut(.company)
2140
-        focusSearchField(seed: "Find jobs at company: ")
2238
+        focusSearchField(seed: L("Find jobs at company: "))
2141
     }
2239
     }
2142
 
2240
 
2143
     @objc private func didTapFeatureSkill() {
2241
     @objc private func didTapFeatureSkill() {
2144
         selectFeatureShortcut(.skill)
2242
         selectFeatureShortcut(.skill)
2145
-        focusSearchField(seed: "Find jobs that require skill: ")
2243
+        focusSearchField(seed: L("Find jobs that require skill: "))
2146
     }
2244
     }
2147
 
2245
 
2148
     private func selectFeatureShortcut(_ shortcut: FeatureShortcut) {
2246
     private func selectFeatureShortcut(_ shortcut: FeatureShortcut) {
@@ -2196,7 +2294,7 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
2196
 
2294
 
2197
     @objc private func didTapMoreApps() {
2295
     @objc private func didTapMoreApps() {
2198
         guard let url = AppMarketingLinks.developerAppsURL else {
2296
         guard let url = AppMarketingLinks.developerAppsURL else {
2199
-            presentAppMarketingConfigurationAlert(feature: "More Apps")
2297
+            presentAppMarketingConfigurationAlert(feature: L("More Apps"))
2200
             return
2298
             return
2201
         }
2299
         }
2202
         NSWorkspace.shared.open(url)
2300
         NSWorkspace.shared.open(url)
@@ -2204,14 +2302,10 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
2204
 
2302
 
2205
     private func presentAppMarketingConfigurationAlert(feature: String) {
2303
     private func presentAppMarketingConfigurationAlert(feature: String) {
2206
         let alert = NSAlert()
2304
         let alert = NSAlert()
2207
-        alert.messageText = "\(feature) isn’t available yet"
2208
-        alert.informativeText = """
2209
-        Add your Mac App Store IDs in the target’s build settings:
2210
-        • AppStoreAppID — numeric app ID from App Store Connect
2211
-        • AppStoreDeveloperID — numeric developer ID (for your other apps page)
2212
-        """
2305
+        alert.messageText = String(format: L("%@ isn’t available yet"), feature)
2306
+        alert.informativeText = L("Add your Mac App Store IDs in the target’s build settings:\n• AppStoreAppID — numeric app ID from App Store Connect\n• AppStoreDeveloperID — numeric developer ID (for your other apps page)")
2213
         alert.alertStyle = .informational
2307
         alert.alertStyle = .informational
2214
-        alert.addButton(withTitle: "OK")
2308
+        alert.addButton(withTitle: L("OK"))
2215
         if let window {
2309
         if let window {
2216
             alert.beginSheetModal(for: window)
2310
             alert.beginSheetModal(for: window)
2217
         } else {
2311
         } else {
@@ -2246,7 +2340,7 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
2246
 
2340
 
2247
     @objc private func didTapLoadMoreJobs() {
2341
     @objc private func didTapLoadMoreJobs() {
2248
         guard ensureProAccessForJobSearch() else { return }
2342
         guard ensureProAccessForJobSearch() else { return }
2249
-        let prompt = "Show more jobs"
2343
+        let prompt = L("Show more jobs")
2250
         guard !isAwaitingResponse, isContinuationPrompt(prompt) else { return }
2344
         guard !isAwaitingResponse, isContinuationPrompt(prompt) else { return }
2251
         if anchorUserJobQuery(excludingLatestUserMessage: prompt) == nil { return }
2345
         if anchorUserJobQuery(excludingLatestUserMessage: prompt) == nil { return }
2252
         appendChatBubble(text: prompt, isUser: true)
2346
         appendChatBubble(text: prompt, isUser: true)
@@ -2309,15 +2403,15 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
2309
     private func makeAssistantSearchReply(query: String, newJobsCount: Int, isContinuation: Bool) -> String {
2403
     private func makeAssistantSearchReply(query: String, newJobsCount: Int, isContinuation: Bool) -> String {
2310
         if newJobsCount == 0 {
2404
         if newJobsCount == 0 {
2311
             if isContinuation {
2405
             if isContinuation {
2312
-                return "I couldn't find new matches for \u{201C}\(query)\u{201D}. Try a different angle or a more specific keyword."
2406
+                return String(format: L("I couldn't find new matches for “%@”. Try a different angle or a more specific keyword."), query)
2313
             }
2407
             }
2314
-            return "No jobs found for \u{201C}\(query)\u{201D}. Try another title, skill, company, or location."
2408
+            return String(format: L("No jobs found for “%@”. Try another title, skill, company, or location."), query)
2315
         }
2409
         }
2316
-        let plural = newJobsCount == 1 ? "match" : "matches"
2410
+        let matchWord = newJobsCount == 1 ? L("match") : L("matches")
2317
         if isContinuation {
2411
         if isContinuation {
2318
-            return "Here are \(newJobsCount) more \(plural) for \u{201C}\(query)\u{201D}."
2412
+            return String(format: L("Here are %d more %@ for “%@”."), newJobsCount, matchWord, query)
2319
         }
2413
         }
2320
-        return "Found \(newJobsCount) \(plural) for \u{201C}\(query)\u{201D}. Tap Apply to open the listing or Save to revisit later."
2414
+        return String(format: L("Found %d %@ for “%@”. Tap Apply to open the listing or Save to revisit later."), newJobsCount, matchWord, query)
2321
     }
2415
     }
2322
 
2416
 
2323
     private func resolvedSearchQuery(for prompt: String) -> String {
2417
     private func resolvedSearchQuery(for prompt: String) -> String {
@@ -2347,6 +2441,10 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
2347
 
2441
 
2348
     private func isContinuationPrompt(_ prompt: String) -> Bool {
2442
     private func isContinuationPrompt(_ prompt: String) -> Bool {
2349
         let normalized = prompt.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
2443
         let normalized = prompt.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
2444
+        let showMoreJobs = L("Show more jobs").trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
2445
+        if normalized == showMoreJobs {
2446
+            return true
2447
+        }
2350
         let continuationPhrases: Set<String> = [
2448
         let continuationPhrases: Set<String> = [
2351
             "more",
2449
             "more",
2352
             "show more",
2450
             "show more",
@@ -2423,7 +2521,7 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
2423
         chatMessages.removeAll()
2521
         chatMessages.removeAll()
2424
         lastSearchResults.removeAll()
2522
         lastSearchResults.removeAll()
2425
         clearChatStack()
2523
         clearChatStack()
2426
-        let welcome = "Tell me what role you want and I will return job descriptions, key skills, and a quick fit summary."
2524
+        let welcome = L("Tell me what role you want and I will return job descriptions, key skills, and a quick fit summary.")
2427
         chatMessages.append(ChatMessage(role: "assistant", content: welcome))
2525
         chatMessages.append(ChatMessage(role: "assistant", content: welcome))
2428
         appendChatBubble(text: welcome, isUser: false)
2526
         appendChatBubble(text: welcome, isUser: false)
2429
     }
2527
     }
@@ -2620,7 +2718,7 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
2620
         row.translatesAutoresizingMaskIntoConstraints = false
2718
         row.translatesAutoresizingMaskIntoConstraints = false
2621
         let button = HoverableButton()
2719
         let button = HoverableButton()
2622
         button.pointerCursor = true
2720
         button.pointerCursor = true
2623
-        button.title = "Show more jobs"
2721
+        button.title = L("Show more jobs")
2624
         button.font = .systemFont(ofSize: 12, weight: .semibold)
2722
         button.font = .systemFont(ofSize: 12, weight: .semibold)
2625
         button.bezelStyle = .rounded
2723
         button.bezelStyle = .rounded
2626
         button.controlSize = .regular
2724
         button.controlSize = .regular
@@ -2730,10 +2828,10 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
2730
         rowHost.layer?.cornerRadius = 8
2828
         rowHost.layer?.cornerRadius = 8
2731
         rowHost.restingBackgroundColor = isSelected ? Theme.selectionFill : nil
2829
         rowHost.restingBackgroundColor = isSelected ? Theme.selectionFill : nil
2732
         rowHost.hoverBackgroundColor = isSelected ? Theme.selectionFillHover : Theme.sidebarRowHoverFill
2830
         rowHost.hoverBackgroundColor = isSelected ? Theme.selectionFillHover : Theme.sidebarRowHoverFill
2733
-        rowHost.setAccessibilityLabel("Indeed")
2831
+        rowHost.setAccessibilityLabel(L("Indeed"))
2734
         rowHost.setAccessibilityRole(.button)
2832
         rowHost.setAccessibilityRole(.button)
2735
         rowHost.setAccessibilitySelected(isSelected)
2833
         rowHost.setAccessibilitySelected(isSelected)
2736
-        rowHost.setAccessibilityHelp("Open Indeed to search and apply for jobs")
2834
+        rowHost.setAccessibilityHelp(L("Open Indeed to search and apply for jobs"))
2737
 
2835
 
2738
         let row = NSStackView()
2836
         let row = NSStackView()
2739
         row.orientation = .horizontal
2837
         row.orientation = .horizontal
@@ -2749,7 +2847,7 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
2749
         icon.widthAnchor.constraint(equalToConstant: Self.sidebarNavIconSize).isActive = true
2847
         icon.widthAnchor.constraint(equalToConstant: Self.sidebarNavIconSize).isActive = true
2750
         icon.heightAnchor.constraint(equalToConstant: Self.sidebarNavIconSize).isActive = true
2848
         icon.heightAnchor.constraint(equalToConstant: Self.sidebarNavIconSize).isActive = true
2751
 
2849
 
2752
-        let text = NSTextField(labelWithString: "Indeed")
2850
+        let text = NSTextField(labelWithString: L("Indeed"))
2753
         text.font = .systemFont(ofSize: 14, weight: .medium)
2851
         text.font = .systemFont(ofSize: 14, weight: .medium)
2754
         text.textColor = isSelected ? Theme.brandBlue : Theme.secondaryText
2852
         text.textColor = isSelected ? Theme.brandBlue : Theme.secondaryText
2755
         text.refusesFirstResponder = true
2853
         text.refusesFirstResponder = true
@@ -2859,7 +2957,7 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
2859
             let sidebarHorizontalInset = sidebar.edgeInsets.left + sidebar.edgeInsets.right
2957
             let sidebarHorizontalInset = sidebar.edgeInsets.left + sidebar.edgeInsets.right
2860
             rowHost.widthAnchor.constraint(equalTo: sidebar.widthAnchor, constant: -sidebarHorizontalInset).isActive = true
2958
             rowHost.widthAnchor.constraint(equalTo: sidebar.widthAnchor, constant: -sidebarHorizontalInset).isActive = true
2861
 
2959
 
2862
-            if item.title == "Home" {
2960
+            if item.title == L("Home") {
2863
                 addIndeedSidebarLaunchRow()
2961
                 addIndeedSidebarLaunchRow()
2864
             }
2962
             }
2865
         }
2963
         }
@@ -2896,7 +2994,7 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
2896
         proIcon.image = NSImage(systemSymbolName: "sparkles", accessibilityDescription: nil)
2994
         proIcon.image = NSImage(systemSymbolName: "sparkles", accessibilityDescription: nil)
2897
         proIcon.contentTintColor = Theme.proAccent
2995
         proIcon.contentTintColor = Theme.proAccent
2898
 
2996
 
2899
-        let proEyebrow = NSTextField(labelWithString: "Premium")
2997
+        let proEyebrow = NSTextField(labelWithString: L("Premium"))
2900
         proEyebrow.font = .systemFont(ofSize: 11, weight: .heavy)
2998
         proEyebrow.font = .systemFont(ofSize: 11, weight: .heavy)
2901
         proEyebrow.textColor = Theme.proAccent
2999
         proEyebrow.textColor = Theme.proAccent
2902
         proEyebrow.alignment = .center
3000
         proEyebrow.alignment = .center
@@ -2906,12 +3004,12 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
2906
         eyebrowRow.spacing = 6
3004
         eyebrowRow.spacing = 6
2907
         eyebrowRow.alignment = .centerY
3005
         eyebrowRow.alignment = .centerY
2908
 
3006
 
2909
-        let headline = NSTextField(labelWithString: "Upgrade to Pro")
3007
+        let headline = NSTextField(labelWithString: L("Upgrade to Pro"))
2910
         headline.font = .systemFont(ofSize: 16, weight: .bold)
3008
         headline.font = .systemFont(ofSize: 16, weight: .bold)
2911
         headline.textColor = Theme.primaryText
3009
         headline.textColor = Theme.primaryText
2912
         headline.alignment = .center
3010
         headline.alignment = .center
2913
 
3011
 
2914
-        let upgradeDescription = NSTextField(wrappingLabelWithString: "Unlimited AI matches, smart alerts, and interview prep—all in one place.")
3012
+        let upgradeDescription = NSTextField(wrappingLabelWithString: L("Unlimited AI matches, smart alerts, and interview prep—all in one place."))
2915
         upgradeDescription.font = .systemFont(ofSize: 12, weight: .regular)
3013
         upgradeDescription.font = .systemFont(ofSize: 12, weight: .regular)
2916
         upgradeDescription.textColor = Theme.secondaryText
3014
         upgradeDescription.textColor = Theme.secondaryText
2917
         upgradeDescription.alignment = .center
3015
         upgradeDescription.alignment = .center
@@ -2920,7 +3018,7 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
2920
         let innerContentWidth = cardWidth - 28
3018
         let innerContentWidth = cardWidth - 28
2921
         upgradeDescription.preferredMaxLayoutWidth = innerContentWidth
3019
         upgradeDescription.preferredMaxLayoutWidth = innerContentWidth
2922
 
3020
 
2923
-        let upgradeButton = HoverableButton(title: "Try Pro", target: self, action: #selector(didTapUpgradeToPro))
3021
+        let upgradeButton = HoverableButton(title: L("Try Pro"), target: self, action: #selector(didTapUpgradeToPro))
2924
         upgradeButton.isBordered = false
3022
         upgradeButton.isBordered = false
2925
         upgradeButton.bezelStyle = .rounded
3023
         upgradeButton.bezelStyle = .rounded
2926
         upgradeButton.font = .systemFont(ofSize: 13, weight: .bold)
3024
         upgradeButton.font = .systemFont(ofSize: 13, weight: .bold)
@@ -3051,7 +3149,7 @@ private final class OpenAIJobSearchService {
3051
             completion(.failure(NSError(
3149
             completion(.failure(NSError(
3052
                 domain: "OpenAIJobSearchService",
3150
                 domain: "OpenAIJobSearchService",
3053
                 code: 1,
3151
                 code: 1,
3054
-                userInfo: [NSLocalizedDescriptionKey: "Job search is unavailable."]
3152
+                userInfo: [NSLocalizedDescriptionKey: L("Job search is unavailable.")]
3055
             )))
3153
             )))
3056
             return
3154
             return
3057
         }
3155
         }

+ 7 - 7
App for Indeed/Views/LoadingView.swift

@@ -29,14 +29,14 @@ final class LoadingView: NSView {
29
     private let iconWell = NSView()
29
     private let iconWell = NSView()
30
     private let logoView = IndeedLogoView(displayHeight: 40, variant: .compact)
30
     private let logoView = IndeedLogoView(displayHeight: 40, variant: .compact)
31
     private let aiBadgeHost = NSView()
31
     private let aiBadgeHost = NSView()
32
-    private let aiBadgeLabel = NSTextField(labelWithString: "AI-POWERED")
32
+    private let aiBadgeLabel = NSTextField(labelWithString: L("AI-POWERED"))
33
     private let titleLabel = NSTextField(labelWithString: AppMarketingLinks.displayName)
33
     private let titleLabel = NSTextField(labelWithString: AppMarketingLinks.displayName)
34
-    private let subtitleLabel = NSTextField(labelWithString: "Find your perfect job with the power of AI.")
35
-    private let statusLabel = NSTextField(labelWithString: "Starting up…")
34
+    private let subtitleLabel = NSTextField(labelWithString: L("Find your perfect job with the power of AI."))
35
+    private let statusLabel = NSTextField(labelWithString: L("Starting up…"))
36
     private let progressBar = LoadingProgressBarView()
36
     private let progressBar = LoadingProgressBarView()
37
     private let thinkingIndicator = ChatThinkingIndicatorView(
37
     private let thinkingIndicator = ChatThinkingIndicatorView(
38
         compact: false,
38
         compact: false,
39
-        accessibilityLabel: "Loading \(AppMarketingLinks.displayName)"
39
+        accessibilityLabel: String(format: L("Loading %@"), AppMarketingLinks.displayName)
40
     )
40
     )
41
 
41
 
42
     override init(frame frameRect: NSRect) {
42
     override init(frame frameRect: NSRect) {
@@ -87,7 +87,7 @@ final class LoadingView: NSView {
87
     func setStatus(_ message: String, progress: CGFloat) {
87
     func setStatus(_ message: String, progress: CGFloat) {
88
         statusLabel.stringValue = message
88
         statusLabel.stringValue = message
89
         progressBar.setProgress(progress, animated: true)
89
         progressBar.setProgress(progress, animated: true)
90
-        setAccessibilityLabel("Loading \(AppMarketingLinks.displayName). \(message)")
90
+        setAccessibilityLabel(String(format: L("Loading %@. %@"), AppMarketingLinks.displayName, message))
91
     }
91
     }
92
 
92
 
93
     func startAnimating() {
93
     func startAnimating() {
@@ -236,7 +236,7 @@ final class LoadingView: NSView {
236
         installPageGradient()
236
         installPageGradient()
237
         setAccessibilityElement(true)
237
         setAccessibilityElement(true)
238
         setAccessibilityRole(.group)
238
         setAccessibilityRole(.group)
239
-        setAccessibilityLabel("Loading \(AppMarketingLinks.displayName)")
239
+        setAccessibilityLabel(String(format: L("Loading %@"), AppMarketingLinks.displayName))
240
     }
240
     }
241
 
241
 
242
     private func installPageGradient() {
242
     private func installPageGradient() {
@@ -449,7 +449,7 @@ private final class LoadingProgressBarView: NSView {
449
 
449
 
450
         setAccessibilityElement(true)
450
         setAccessibilityElement(true)
451
         setAccessibilityRole(.progressIndicator)
451
         setAccessibilityRole(.progressIndicator)
452
-        setAccessibilityLabel("Loading progress")
452
+        setAccessibilityLabel(L("Loading progress"))
453
     }
453
     }
454
 }
454
 }
455
 
455
 

+ 74 - 53
App for Indeed/Views/MyProfilePageView.swift

@@ -196,13 +196,13 @@ final class MyProfilePageView: NSView {
196
     private let interestsField = NSTextField()
196
     private let interestsField = NSTextField()
197
     private let languagesField = NSTextField()
197
     private let languagesField = NSTextField()
198
     private let referralField = NSTextField()
198
     private let referralField = NSTextField()
199
-    private let saveButton = ProfilePrimaryButton(title: "Save Profile  →", target: nil, action: nil)
199
+    private let saveButton = ProfilePrimaryButton(title: L("Save Profile  →"), target: nil, action: nil)
200
 
200
 
201
     private var nameEmailRow: ProfileDualFieldRow!
201
     private var nameEmailRow: ProfileDualFieldRow!
202
     private var phoneJobRow: ProfileDualFieldRow!
202
     private var phoneJobRow: ProfileDualFieldRow!
203
 
203
 
204
     private let topChrome = NSView()
204
     private let topChrome = NSView()
205
-    private let backButton = NSButton(title: "← All profiles", target: nil, action: nil)
205
+    private let backButton = NSButton(title: L("← All profiles"), target: nil, action: nil)
206
     private let contextLabel = NSTextField(labelWithString: "")
206
     private let contextLabel = NSTextField(labelWithString: "")
207
     private var editingProfileID: UUID?
207
     private var editingProfileID: UUID?
208
 
208
 
@@ -218,6 +218,7 @@ final class MyProfilePageView: NSView {
218
 
218
 
219
     private var referralHelperLabel: NSTextField?
219
     private var referralHelperLabel: NSTextField?
220
     private var appearanceObserver: NSObjectProtocol?
220
     private var appearanceObserver: NSObjectProtocol?
221
+    private var languageObserver: NSObjectProtocol?
221
 
222
 
222
     /// Force left-to-right geometry so profile fields span the full width even when the window uses RTL layout.
223
     /// Force left-to-right geometry so profile fields span the full width even when the window uses RTL layout.
223
     override var userInterfaceLayoutDirection: NSUserInterfaceLayoutDirection {
224
     override var userInterfaceLayoutDirection: NSUserInterfaceLayoutDirection {
@@ -235,13 +236,24 @@ final class MyProfilePageView: NSView {
235
         ) { [weak self] _ in
236
         ) { [weak self] _ in
236
             self?.applyCurrentAppearance()
237
             self?.applyCurrentAppearance()
237
         }
238
         }
239
+        languageObserver = NotificationCenter.default.addObserver(
240
+            forName: AppLanguageManager.didChangeNotification,
241
+            object: nil,
242
+            queue: .main
243
+        ) { [weak self] _ in
244
+            self?.applyLocalizedStrings()
245
+        }
238
         applyCurrentAppearance()
246
         applyCurrentAppearance()
247
+        applyLocalizedStrings()
239
     }
248
     }
240
 
249
 
241
     deinit {
250
     deinit {
242
         if let appearanceObserver {
251
         if let appearanceObserver {
243
             NotificationCenter.default.removeObserver(appearanceObserver)
252
             NotificationCenter.default.removeObserver(appearanceObserver)
244
         }
253
         }
254
+        if let languageObserver {
255
+            NotificationCenter.default.removeObserver(languageObserver)
256
+        }
245
     }
257
     }
246
 
258
 
247
     required init?(coder: NSCoder) {
259
     required init?(coder: NSCoder) {
@@ -266,6 +278,15 @@ final class MyProfilePageView: NSView {
266
         saveButton.applyCurrentAppearance()
278
         saveButton.applyCurrentAppearance()
267
     }
279
     }
268
 
280
 
281
+    func applyLocalizedStrings() {
282
+        backButton.title = L("← All profiles")
283
+        saveButton.title = L("Save Profile  →")
284
+        contextLabel.stringValue = editingProfileID == nil ? L("New profile") : L("Edit profile")
285
+        renumberWorkExperienceEntries()
286
+        renumberEducationEntries()
287
+        ProfileThemeAppearance.refreshFormSubtree(formStack)
288
+    }
289
+
269
     override func viewDidMoveToWindow() {
290
     override func viewDidMoveToWindow() {
270
         super.viewDidMoveToWindow()
291
         super.viewDidMoveToWindow()
271
         guard window != nil else { return }
292
         guard window != nil else { return }
@@ -381,7 +402,7 @@ final class MyProfilePageView: NSView {
381
         contextLabel.translatesAutoresizingMaskIntoConstraints = false
402
         contextLabel.translatesAutoresizingMaskIntoConstraints = false
382
         contextLabel.font = .systemFont(ofSize: 15, weight: .semibold)
403
         contextLabel.font = .systemFont(ofSize: 15, weight: .semibold)
383
         contextLabel.textColor = ProfilePagePalette.primaryText
404
         contextLabel.textColor = ProfilePagePalette.primaryText
384
-        contextLabel.stringValue = "New profile"
405
+        contextLabel.stringValue = L("New profile")
385
         contextLabel.backgroundColor = .clear
406
         contextLabel.backgroundColor = .clear
386
         contextLabel.isBordered = false
407
         contextLabel.isBordered = false
387
         contextLabel.isEditable = false
408
         contextLabel.isEditable = false
@@ -433,22 +454,22 @@ final class MyProfilePageView: NSView {
433
         ])
454
         ])
434
 
455
 
435
         addFullWidthArrangedSubview(
456
         addFullWidthArrangedSubview(
436
-            labeledGroup(title: "Profile Name *", field: profileNameField, placeholder: "Marketing Director Profile")
457
+            labeledGroup(title: L("Profile Name *"), field: profileNameField, placeholder: L("Marketing Director Profile"))
437
         )
458
         )
438
-        addFullWidthArrangedSubview(sectionHeading("Personal Information"))
459
+        addFullWidthArrangedSubview(sectionHeading(L("Personal Information")))
439
 
460
 
440
-        let nameGroup = labeledGroup(title: "Full Name *", field: fullNameField, placeholder: "John Doe")
441
-        let emailGroup = labeledGroup(title: "Email *", field: emailField, placeholder: "john@example.com")
461
+        let nameGroup = labeledGroup(title: L("Full Name *"), field: fullNameField, placeholder: L("John Doe"))
462
+        let emailGroup = labeledGroup(title: L("Email *"), field: emailField, placeholder: L("john@example.com"))
442
         nameEmailRow = ProfileDualFieldRow(left: nameGroup, right: emailGroup, spacing: 12)
463
         nameEmailRow = ProfileDualFieldRow(left: nameGroup, right: emailGroup, spacing: 12)
443
         addFullWidthArrangedSubview(nameEmailRow)
464
         addFullWidthArrangedSubview(nameEmailRow)
444
 
465
 
445
-        let phoneGroup = labeledGroup(title: "Phone", field: phoneField, placeholder: "+1 (555) 123-4567")
446
-        let jobGroup = labeledGroup(title: "Job Title *", field: jobTitleField, placeholder: "Software Engineer")
466
+        let phoneGroup = labeledGroup(title: L("Phone"), field: phoneField, placeholder: L("+1 (555) 123-4567"))
467
+        let jobGroup = labeledGroup(title: L("Job Title *"), field: jobTitleField, placeholder: L("Software Engineer"))
447
         phoneJobRow = ProfileDualFieldRow(left: phoneGroup, right: jobGroup, spacing: 12)
468
         phoneJobRow = ProfileDualFieldRow(left: phoneGroup, right: jobGroup, spacing: 12)
448
         addFullWidthArrangedSubview(phoneJobRow)
469
         addFullWidthArrangedSubview(phoneJobRow)
449
 
470
 
450
         addFullWidthArrangedSubview(
471
         addFullWidthArrangedSubview(
451
-            labeledGroup(title: "Address", field: addressField, placeholder: "123 Main St, City, State, ZIP")
472
+            labeledGroup(title: L("Address"), field: addressField, placeholder: L("123 Main St, City, State, ZIP"))
452
         )
473
         )
453
         addFullWidthArrangedSubview(careerSummaryBlock())
474
         addFullWidthArrangedSubview(careerSummaryBlock())
454
         addFullWidthArrangedSubview(horizontalSeparator())
475
         addFullWidthArrangedSubview(horizontalSeparator())
@@ -458,8 +479,8 @@ final class MyProfilePageView: NSView {
458
         addFullWidthArrangedSubview(horizontalSeparator())
479
         addFullWidthArrangedSubview(horizontalSeparator())
459
         addFullWidthArrangedSubview(
480
         addFullWidthArrangedSubview(
460
             multilineProfileBlock(
481
             multilineProfileBlock(
461
-                title: "Certificates / Rewards",
462
-                placeholder: "List your certificates and awards...",
482
+                title: L("Certificates / Rewards"),
483
+                placeholder: L("List your certificates and awards..."),
463
                 field: certificatesField,
484
                 field: certificatesField,
464
                 minHeight: 100
485
                 minHeight: 100
465
             )
486
             )
@@ -467,8 +488,8 @@ final class MyProfilePageView: NSView {
467
         addFullWidthArrangedSubview(horizontalSeparator())
488
         addFullWidthArrangedSubview(horizontalSeparator())
468
         addFullWidthArrangedSubview(
489
         addFullWidthArrangedSubview(
469
             multilineProfileBlock(
490
             multilineProfileBlock(
470
-                title: "Interests",
471
-                placeholder: "List your interests and hobbies...",
491
+                title: L("Interests"),
492
+                placeholder: L("List your interests and hobbies..."),
472
                 field: interestsField,
493
                 field: interestsField,
473
                 minHeight: 100
494
                 minHeight: 100
474
             )
495
             )
@@ -476,8 +497,8 @@ final class MyProfilePageView: NSView {
476
         addFullWidthArrangedSubview(horizontalSeparator())
497
         addFullWidthArrangedSubview(horizontalSeparator())
477
         addFullWidthArrangedSubview(
498
         addFullWidthArrangedSubview(
478
             multilineProfileBlock(
499
             multilineProfileBlock(
479
-                title: "Languages",
480
-                placeholder: "List languages you speak (e.g., English - Native, Spanish - Fluent)...",
500
+                title: L("Languages"),
501
+                placeholder: L("List languages you speak (e.g., English - Native, Spanish - Fluent)..."),
481
                 field: languagesField,
502
                 field: languagesField,
482
                 minHeight: 100
503
                 minHeight: 100
483
             )
504
             )
@@ -497,7 +518,7 @@ final class MyProfilePageView: NSView {
497
 
518
 
498
     func prepareNewProfile() {
519
     func prepareNewProfile() {
499
         editingProfileID = nil
520
         editingProfileID = nil
500
-        contextLabel.stringValue = "New profile"
521
+        contextLabel.stringValue = L("New profile")
501
         applyForm(
522
         applyForm(
502
             from: SavedProfile(
523
             from: SavedProfile(
503
                 id: UUID(),
524
                 id: UUID(),
@@ -516,7 +537,7 @@ final class MyProfilePageView: NSView {
516
 
537
 
517
     func loadSavedProfile(_ profile: SavedProfile) {
538
     func loadSavedProfile(_ profile: SavedProfile) {
518
         editingProfileID = profile.id
539
         editingProfileID = profile.id
519
-        contextLabel.stringValue = "Edit profile"
540
+        contextLabel.stringValue = L("Edit profile")
520
         applyForm(from: profile)
541
         applyForm(from: profile)
521
     }
542
     }
522
 
543
 
@@ -741,7 +762,7 @@ final class MyProfilePageView: NSView {
741
     }
762
     }
742
 
763
 
743
     private func careerSummaryBlock() -> NSView {
764
     private func careerSummaryBlock() -> NSView {
744
-        let label = NSTextField(labelWithString: "Career Summary")
765
+        let label = NSTextField(labelWithString: L("Career Summary"))
745
         label.font = .systemFont(ofSize: 12, weight: .medium)
766
         label.font = .systemFont(ofSize: 12, weight: .medium)
746
         label.textColor = ProfilePagePalette.secondaryText
767
         label.textColor = ProfilePagePalette.secondaryText
747
         label.translatesAutoresizingMaskIntoConstraints = false
768
         label.translatesAutoresizingMaskIntoConstraints = false
@@ -763,7 +784,7 @@ final class MyProfilePageView: NSView {
763
         careerField.setContentHuggingPriority(.defaultLow, for: .horizontal)
784
         careerField.setContentHuggingPriority(.defaultLow, for: .horizontal)
764
         careerField.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
785
         careerField.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
765
         careerField.placeholderAttributedString = NSAttributedString(
786
         careerField.placeholderAttributedString = NSAttributedString(
766
-            string: "Brief overview of your professional background and key achievements...",
787
+            string: L("Brief overview of your professional background and key achievements..."),
767
             attributes: [
788
             attributes: [
768
                 .foregroundColor: ProfilePagePalette.secondaryText,
789
                 .foregroundColor: ProfilePagePalette.secondaryText,
769
                 .font: NSFont.systemFont(ofSize: 14, weight: .regular),
790
                 .font: NSFont.systemFont(ofSize: 14, weight: .regular),
@@ -879,16 +900,16 @@ final class MyProfilePageView: NSView {
879
     }
900
     }
880
 
901
 
881
     private func referralBlock() -> NSView {
902
     private func referralBlock() -> NSView {
882
-        let label = NSTextField(labelWithString: "Referral (Optional)")
903
+        let label = NSTextField(labelWithString: L("Referral (Optional)"))
883
         label.font = .systemFont(ofSize: 12, weight: .medium)
904
         label.font = .systemFont(ofSize: 12, weight: .medium)
884
         label.textColor = ProfilePagePalette.secondaryText
905
         label.textColor = ProfilePagePalette.secondaryText
885
         label.translatesAutoresizingMaskIntoConstraints = false
906
         label.translatesAutoresizingMaskIntoConstraints = false
886
         ProfileLayoutEnforcement.applyLeftAlignedTextField(label)
907
         ProfileLayoutEnforcement.applyLeftAlignedTextField(label)
887
 
908
 
888
-        styleSingleLineField(referralField, placeholder: "Referred by (Company/Person Name)")
909
+        styleSingleLineField(referralField, placeholder: L("Referred by (Company/Person Name)"))
889
         let wrap = roundedFieldChrome(containing: referralField, minHeight: 40)
910
         let wrap = roundedFieldChrome(containing: referralField, minHeight: 40)
890
 
911
 
891
-        let helper = NSTextField(wrappingLabelWithString: "If someone referred you for this job, enter their name or company here")
912
+        let helper = NSTextField(wrappingLabelWithString: L("If someone referred you for this job, enter their name or company here"))
892
         helper.font = .systemFont(ofSize: 11, weight: .regular)
913
         helper.font = .systemFont(ofSize: 11, weight: .regular)
893
         helper.textColor = ProfilePagePalette.secondaryText
914
         helper.textColor = ProfilePagePalette.secondaryText
894
         helper.translatesAutoresizingMaskIntoConstraints = false
915
         helper.translatesAutoresizingMaskIntoConstraints = false
@@ -925,14 +946,14 @@ final class MyProfilePageView: NSView {
925
     }
946
     }
926
 
947
 
927
     private func workExperienceSection() -> NSView {
948
     private func workExperienceSection() -> NSView {
928
-        let title = NSTextField(labelWithString: "Work Experience")
949
+        let title = NSTextField(labelWithString: L("Work Experience"))
929
         title.font = .systemFont(ofSize: 15, weight: .semibold)
950
         title.font = .systemFont(ofSize: 15, weight: .semibold)
930
         title.textColor = ProfilePagePalette.primaryText
951
         title.textColor = ProfilePagePalette.primaryText
931
         title.translatesAutoresizingMaskIntoConstraints = false
952
         title.translatesAutoresizingMaskIntoConstraints = false
932
         title.setContentHuggingPriority(.defaultLow, for: .horizontal)
953
         title.setContentHuggingPriority(.defaultLow, for: .horizontal)
933
         ProfileLayoutEnforcement.applyLeftAlignedTextField(title)
954
         ProfileLayoutEnforcement.applyLeftAlignedTextField(title)
934
 
955
 
935
-        let addButton = NSButton(title: "+ Add Another", target: self, action: #selector(didTapAddWorkExperience))
956
+        let addButton = NSButton(title: L("+ Add Another"), target: self, action: #selector(didTapAddWorkExperience))
936
         addButton.translatesAutoresizingMaskIntoConstraints = false
957
         addButton.translatesAutoresizingMaskIntoConstraints = false
937
         addButton.bezelStyle = .rounded
958
         addButton.bezelStyle = .rounded
938
         addButton.isBordered = true
959
         addButton.isBordered = true
@@ -972,14 +993,14 @@ final class MyProfilePageView: NSView {
972
     }
993
     }
973
 
994
 
974
     private func educationSection() -> NSView {
995
     private func educationSection() -> NSView {
975
-        let title = NSTextField(labelWithString: "Education")
996
+        let title = NSTextField(labelWithString: L("Education"))
976
         title.font = .systemFont(ofSize: 15, weight: .semibold)
997
         title.font = .systemFont(ofSize: 15, weight: .semibold)
977
         title.textColor = ProfilePagePalette.primaryText
998
         title.textColor = ProfilePagePalette.primaryText
978
         title.translatesAutoresizingMaskIntoConstraints = false
999
         title.translatesAutoresizingMaskIntoConstraints = false
979
         title.setContentHuggingPriority(.defaultLow, for: .horizontal)
1000
         title.setContentHuggingPriority(.defaultLow, for: .horizontal)
980
         ProfileLayoutEnforcement.applyLeftAlignedTextField(title)
1001
         ProfileLayoutEnforcement.applyLeftAlignedTextField(title)
981
 
1002
 
982
-        let addButton = NSButton(title: "+ Add Another", target: self, action: #selector(didTapAddEducation))
1003
+        let addButton = NSButton(title: L("+ Add Another"), target: self, action: #selector(didTapAddEducation))
983
         addButton.translatesAutoresizingMaskIntoConstraints = false
1004
         addButton.translatesAutoresizingMaskIntoConstraints = false
984
         addButton.bezelStyle = .rounded
1005
         addButton.bezelStyle = .rounded
985
         addButton.isBordered = true
1006
         addButton.isBordered = true
@@ -1128,16 +1149,16 @@ final class MyProfilePageView: NSView {
1128
         window?.makeFirstResponder(nil)
1149
         window?.makeFirstResponder(nil)
1129
         let profile = captureSavedProfileForSave()
1150
         let profile = captureSavedProfileForSave()
1130
         var missing: [String] = []
1151
         var missing: [String] = []
1131
-        if profile.profileDisplayName.isEmpty { missing.append("Profile name") }
1132
-        if profile.personal.fullName.isEmpty { missing.append("Full Name") }
1133
-        if profile.personal.email.isEmpty { missing.append("Email") }
1134
-        if profile.personal.jobTitle.isEmpty { missing.append("Job Title") }
1152
+        if profile.profileDisplayName.isEmpty { missing.append(L("Profile name")) }
1153
+        if profile.personal.fullName.isEmpty { missing.append(L("Full Name")) }
1154
+        if profile.personal.email.isEmpty { missing.append(L("Email")) }
1155
+        if profile.personal.jobTitle.isEmpty { missing.append(L("Job Title")) }
1135
         guard missing.isEmpty else {
1156
         guard missing.isEmpty else {
1136
             let alert = NSAlert()
1157
             let alert = NSAlert()
1137
-            alert.messageText = "Complete required fields"
1138
-            alert.informativeText = "Please fill in: " + missing.joined(separator: ", ") + "."
1158
+            alert.messageText = L("Complete required fields")
1159
+            alert.informativeText = String(format: L("Please fill in: %@."), missing.joined(separator: ", "))
1139
             alert.alertStyle = .informational
1160
             alert.alertStyle = .informational
1140
-            alert.addButton(withTitle: "OK")
1161
+            alert.addButton(withTitle: L("OK"))
1141
             if let window = window {
1162
             if let window = window {
1142
                 alert.beginSheetModal(for: window) { _ in }
1163
                 alert.beginSheetModal(for: window) { _ in }
1143
             } else {
1164
             } else {
@@ -1161,7 +1182,7 @@ private enum ProfileEntryCardLayout {
1161
 private final class WorkExperienceEntryView: NSView {
1182
 private final class WorkExperienceEntryView: NSView {
1162
     var onDelete: (() -> Void)?
1183
     var onDelete: (() -> Void)?
1163
 
1184
 
1164
-    private let subtitleLabel = NSTextField(labelWithString: "Experience 1")
1185
+    private let subtitleLabel = NSTextField(labelWithString: String(format: L("Experience %d"), 1))
1165
     private let deleteButton = NSButton()
1186
     private let deleteButton = NSButton()
1166
     private let jobTitleField = NSTextField()
1187
     private let jobTitleField = NSTextField()
1167
     private let companyField = NSTextField()
1188
     private let companyField = NSTextField()
@@ -1179,7 +1200,7 @@ private final class WorkExperienceEntryView: NSView {
1179
     }
1200
     }
1180
 
1201
 
1181
     func setExperienceIndex(_ index: Int) {
1202
     func setExperienceIndex(_ index: Int) {
1182
-        subtitleLabel.stringValue = "Experience \(index)"
1203
+        subtitleLabel.stringValue = String(format: L("Experience %d"), index)
1183
     }
1204
     }
1184
 
1205
 
1185
     func setDeleteHidden(_ hidden: Bool) {
1206
     func setDeleteHidden(_ hidden: Bool) {
@@ -1236,10 +1257,10 @@ private final class WorkExperienceEntryView: NSView {
1236
         deleteButton.target = self
1257
         deleteButton.target = self
1237
         deleteButton.action = #selector(didTapDelete)
1258
         deleteButton.action = #selector(didTapDelete)
1238
         if #available(macOS 11.0, *) {
1259
         if #available(macOS 11.0, *) {
1239
-            deleteButton.image = NSImage(systemSymbolName: "trash", accessibilityDescription: "Remove experience")
1260
+            deleteButton.image = NSImage(systemSymbolName: "trash", accessibilityDescription: L("Remove experience"))
1240
             deleteButton.imagePosition = .imageOnly
1261
             deleteButton.imagePosition = .imageOnly
1241
         } else {
1262
         } else {
1242
-            deleteButton.title = "Remove"
1263
+            deleteButton.title = L("Remove")
1243
             deleteButton.font = .systemFont(ofSize: 12, weight: .medium)
1264
             deleteButton.font = .systemFont(ofSize: 12, weight: .medium)
1244
         }
1265
         }
1245
 
1266
 
@@ -1255,15 +1276,15 @@ private final class WorkExperienceEntryView: NSView {
1255
         ProfileLayoutEnforcement.applyForcedLTR(to: headerRow)
1276
         ProfileLayoutEnforcement.applyForcedLTR(to: headerRow)
1256
         headerRow.translatesAutoresizingMaskIntoConstraints = false
1277
         headerRow.translatesAutoresizingMaskIntoConstraints = false
1257
 
1278
 
1258
-        let jobGroup = Self.labeledFieldStack(title: "Job Title *", field: jobTitleField, placeholder: "e.g., Software Engineer")
1259
-        let companyGroup = Self.labeledFieldStack(title: "Company Name *", field: companyField, placeholder: "e.g., Google")
1279
+        let jobGroup = Self.labeledFieldStack(title: L("Job Title *"), field: jobTitleField, placeholder: L("e.g., Software Engineer"))
1280
+        let companyGroup = Self.labeledFieldStack(title: L("Company Name *"), field: companyField, placeholder: L("e.g., Google"))
1260
         jobCompanyRow = ProfileDualFieldRow(left: jobGroup, right: companyGroup, spacing: 12)
1281
         jobCompanyRow = ProfileDualFieldRow(left: jobGroup, right: companyGroup, spacing: 12)
1261
 
1282
 
1262
-        let durationGroup = Self.labeledFieldStack(title: "Duration *", field: durationField, placeholder: "e.g., Jan 2020 - Present")
1283
+        let durationGroup = Self.labeledFieldStack(title: L("Duration *"), field: durationField, placeholder: L("e.g., Jan 2020 - Present"))
1263
         let descriptionGroup = Self.multilineLabeledStack(
1284
         let descriptionGroup = Self.multilineLabeledStack(
1264
-            title: "Description",
1285
+            title: L("Description"),
1265
             field: descriptionField,
1286
             field: descriptionField,
1266
-            placeholder: "Describe your responsibilities and achievements...",
1287
+            placeholder: L("Describe your responsibilities and achievements..."),
1267
             minHeight: 120
1288
             minHeight: 120
1268
         )
1289
         )
1269
 
1290
 
@@ -1454,7 +1475,7 @@ private final class WorkExperienceEntryView: NSView {
1454
 private final class EducationEntryView: NSView {
1475
 private final class EducationEntryView: NSView {
1455
     var onDelete: (() -> Void)?
1476
     var onDelete: (() -> Void)?
1456
 
1477
 
1457
-    private let subtitleLabel = NSTextField(labelWithString: "Education 1")
1478
+    private let subtitleLabel = NSTextField(labelWithString: String(format: L("Education %d"), 1))
1458
     private let deleteButton = NSButton()
1479
     private let deleteButton = NSButton()
1459
     private let degreeField = NSTextField()
1480
     private let degreeField = NSTextField()
1460
     private let institutionField = NSTextField()
1481
     private let institutionField = NSTextField()
@@ -1471,7 +1492,7 @@ private final class EducationEntryView: NSView {
1471
     }
1492
     }
1472
 
1493
 
1473
     func setEducationIndex(_ index: Int) {
1494
     func setEducationIndex(_ index: Int) {
1474
-        subtitleLabel.stringValue = "Education \(index)"
1495
+        subtitleLabel.stringValue = String(format: L("Education %d"), index)
1475
     }
1496
     }
1476
 
1497
 
1477
     func setDeleteHidden(_ hidden: Bool) {
1498
     func setDeleteHidden(_ hidden: Bool) {
@@ -1526,10 +1547,10 @@ private final class EducationEntryView: NSView {
1526
         deleteButton.target = self
1547
         deleteButton.target = self
1527
         deleteButton.action = #selector(didTapDelete)
1548
         deleteButton.action = #selector(didTapDelete)
1528
         if #available(macOS 11.0, *) {
1549
         if #available(macOS 11.0, *) {
1529
-            deleteButton.image = NSImage(systemSymbolName: "trash", accessibilityDescription: "Remove education")
1550
+            deleteButton.image = NSImage(systemSymbolName: "trash", accessibilityDescription: L("Remove education"))
1530
             deleteButton.imagePosition = .imageOnly
1551
             deleteButton.imagePosition = .imageOnly
1531
         } else {
1552
         } else {
1532
-            deleteButton.title = "Remove"
1553
+            deleteButton.title = L("Remove")
1533
             deleteButton.font = .systemFont(ofSize: 12, weight: .medium)
1554
             deleteButton.font = .systemFont(ofSize: 12, weight: .medium)
1534
         }
1555
         }
1535
 
1556
 
@@ -1546,21 +1567,21 @@ private final class EducationEntryView: NSView {
1546
         headerRow.translatesAutoresizingMaskIntoConstraints = false
1567
         headerRow.translatesAutoresizingMaskIntoConstraints = false
1547
 
1568
 
1548
         let degreeGroup = WorkExperienceEntryView.labeledFieldStack(
1569
         let degreeGroup = WorkExperienceEntryView.labeledFieldStack(
1549
-            title: "Degree / program *",
1570
+            title: L("Degree / program *"),
1550
             field: degreeField,
1571
             field: degreeField,
1551
-            placeholder: "e.g., BSc Computer Science"
1572
+            placeholder: L("e.g., BSc Computer Science")
1552
         )
1573
         )
1553
         let institutionGroup = WorkExperienceEntryView.labeledFieldStack(
1574
         let institutionGroup = WorkExperienceEntryView.labeledFieldStack(
1554
-            title: "Institution *",
1575
+            title: L("Institution *"),
1555
             field: institutionField,
1576
             field: institutionField,
1556
-            placeholder: "e.g., MIT"
1577
+            placeholder: L("e.g., MIT")
1557
         )
1578
         )
1558
         degreeInstitutionRow = ProfileDualFieldRow(left: degreeGroup, right: institutionGroup, spacing: 12)
1579
         degreeInstitutionRow = ProfileDualFieldRow(left: degreeGroup, right: institutionGroup, spacing: 12)
1559
 
1580
 
1560
         let yearGroup = WorkExperienceEntryView.labeledFieldStack(
1581
         let yearGroup = WorkExperienceEntryView.labeledFieldStack(
1561
-            title: "Year *",
1582
+            title: L("Year *"),
1562
             field: yearField,
1583
             field: yearField,
1563
-            placeholder: "e.g., 2020"
1584
+            placeholder: L("e.g., 2020")
1564
         )
1585
         )
1565
 
1586
 
1566
         let inner = NSStackView(views: [headerRow, degreeInstitutionRow, yearGroup])
1587
         let inner = NSStackView(views: [headerRow, degreeInstitutionRow, yearGroup])

+ 39 - 10
App for Indeed/Views/ProfilesListPageView.swift

@@ -33,12 +33,13 @@ final class ProfilesListPageView: NSView {
33
     private let scrollView = NSScrollView()
33
     private let scrollView = NSScrollView()
34
     private let documentView = ProfilesListDocumentView()
34
     private let documentView = ProfilesListDocumentView()
35
     private let contentStack = NSStackView()
35
     private let contentStack = NSStackView()
36
-    private let titleLabel = NSTextField(labelWithString: "Profiles")
36
+    private let titleLabel = NSTextField(labelWithString: L("Profiles"))
37
     private let subtitleLabel = NSTextField(wrappingLabelWithString: "")
37
     private let subtitleLabel = NSTextField(wrappingLabelWithString: "")
38
     private let emptyStateLabel = NSTextField(wrappingLabelWithString: "")
38
     private let emptyStateLabel = NSTextField(wrappingLabelWithString: "")
39
-    private let addButton = ProfilesPrimaryButton(title: "Add new profile", target: nil, action: nil)
39
+    private let addButton = ProfilesPrimaryButton(title: L("Add new profile"), target: nil, action: nil)
40
     private let pendingFlowLabel = NSTextField(wrappingLabelWithString: "")
40
     private let pendingFlowLabel = NSTextField(wrappingLabelWithString: "")
41
     private var appearanceObserver: NSObjectProtocol?
41
     private var appearanceObserver: NSObjectProtocol?
42
+    private var languageObserver: NSObjectProtocol?
42
     /// Non-`nil` after **Use Template & Select Profile** until the dashboard clears the flow (e.g. leaving Profile).
43
     /// Non-`nil` after **Use Template & Select Profile** until the dashboard clears the flow (e.g. leaving Profile).
43
     private var pendingCVTemplateDisplayName: String?
44
     private var pendingCVTemplateDisplayName: String?
44
 
45
 
@@ -57,13 +58,24 @@ final class ProfilesListPageView: NSView {
57
         ) { [weak self] _ in
58
         ) { [weak self] _ in
58
             self?.applyCurrentAppearance()
59
             self?.applyCurrentAppearance()
59
         }
60
         }
61
+        languageObserver = NotificationCenter.default.addObserver(
62
+            forName: AppLanguageManager.didChangeNotification,
63
+            object: nil,
64
+            queue: .main
65
+        ) { [weak self] _ in
66
+            self?.applyLocalizedStrings()
67
+        }
60
         applyCurrentAppearance()
68
         applyCurrentAppearance()
69
+        applyLocalizedStrings()
61
     }
70
     }
62
 
71
 
63
     deinit {
72
     deinit {
64
         if let appearanceObserver {
73
         if let appearanceObserver {
65
             NotificationCenter.default.removeObserver(appearanceObserver)
74
             NotificationCenter.default.removeObserver(appearanceObserver)
66
         }
75
         }
76
+        if let languageObserver {
77
+            NotificationCenter.default.removeObserver(languageObserver)
78
+        }
67
     }
79
     }
68
 
80
 
69
     required init?(coder: NSCoder) {
81
     required init?(coder: NSCoder) {
@@ -88,6 +100,20 @@ final class ProfilesListPageView: NSView {
88
         }
100
         }
89
     }
101
     }
90
 
102
 
103
+    func applyLocalizedStrings() {
104
+        titleLabel.stringValue = L("Profiles")
105
+        subtitleLabel.stringValue = L("Create and manage CV profiles. Each profile stores your details on this Mac.")
106
+        emptyStateLabel.stringValue = L("No profiles yet. Tap “Add new profile” to create your first one.")
107
+        addButton.title = L("Add new profile")
108
+        if let name = pendingCVTemplateDisplayName, !name.isEmpty {
109
+            pendingFlowLabel.stringValue = String(
110
+                format: L("You chose the “%@” template. Tap Build CV on a profile to preview your résumé with that layout."),
111
+                localizedTemplateName(name)
112
+            )
113
+        }
114
+        reloadFromStore()
115
+    }
116
+
91
     func reloadFromStore() {
117
     func reloadFromStore() {
92
         for row in contentStack.arrangedSubviews {
118
         for row in contentStack.arrangedSubviews {
93
             contentStack.removeArrangedSubview(row)
119
             contentStack.removeArrangedSubview(row)
@@ -113,7 +139,10 @@ final class ProfilesListPageView: NSView {
113
     func setPendingCVTemplateDisplayName(_ name: String?) {
139
     func setPendingCVTemplateDisplayName(_ name: String?) {
114
         pendingCVTemplateDisplayName = name
140
         pendingCVTemplateDisplayName = name
115
         if let name, !name.isEmpty {
141
         if let name, !name.isEmpty {
116
-            pendingFlowLabel.stringValue = "You chose the “\(name)” template. Tap Build CV on a profile to preview your résumé with that layout."
142
+            pendingFlowLabel.stringValue = String(
143
+                format: L("You chose the “%@” template. Tap Build CV on a profile to preview your résumé with that layout."),
144
+                localizedTemplateName(name)
145
+            )
117
             pendingFlowLabel.isHidden = false
146
             pendingFlowLabel.isHidden = false
118
         } else {
147
         } else {
119
             pendingFlowLabel.isHidden = true
148
             pendingFlowLabel.isHidden = true
@@ -128,11 +157,11 @@ final class ProfilesListPageView: NSView {
128
 
157
 
129
         titleLabel.font = .systemFont(ofSize: 22, weight: .semibold)
158
         titleLabel.font = .systemFont(ofSize: 22, weight: .semibold)
130
 
159
 
131
-        subtitleLabel.stringValue = "Create and manage CV profiles. Each profile stores your details on this Mac."
160
+        subtitleLabel.stringValue = L("Create and manage CV profiles. Each profile stores your details on this Mac.")
132
         subtitleLabel.font = .systemFont(ofSize: 13, weight: .regular)
161
         subtitleLabel.font = .systemFont(ofSize: 13, weight: .regular)
133
         subtitleLabel.maximumNumberOfLines = 0
162
         subtitleLabel.maximumNumberOfLines = 0
134
 
163
 
135
-        emptyStateLabel.stringValue = "No profiles yet. Tap “Add new profile” to create your first one."
164
+        emptyStateLabel.stringValue = L("No profiles yet. Tap “Add new profile” to create your first one.")
136
         emptyStateLabel.font = .systemFont(ofSize: 13, weight: .regular)
165
         emptyStateLabel.font = .systemFont(ofSize: 13, weight: .regular)
137
         emptyStateLabel.textColor = ProfilesListPalette.secondaryText
166
         emptyStateLabel.textColor = ProfilesListPalette.secondaryText
138
         emptyStateLabel.isHidden = true
167
         emptyStateLabel.isHidden = true
@@ -237,9 +266,9 @@ private final class ProfileListRowView: NSView {
237
     private let profileID: UUID
266
     private let profileID: UUID
238
     private let nameLabel = NSTextField(labelWithString: "")
267
     private let nameLabel = NSTextField(labelWithString: "")
239
     private let detailLabel = NSTextField(wrappingLabelWithString: "")
268
     private let detailLabel = NSTextField(wrappingLabelWithString: "")
240
-    private let buildCVButton = NSButton(title: "Build CV", target: nil, action: nil)
241
-    private let editButton = NSButton(title: "Edit", target: nil, action: nil)
242
-    private let deleteButton = NSButton(title: "Delete", target: nil, action: nil)
269
+    private let buildCVButton = NSButton(title: L("Build CV"), target: nil, action: nil)
270
+    private let editButton = NSButton(title: L("Edit"), target: nil, action: nil)
271
+    private let deleteButton = NSButton(title: L("Delete"), target: nil, action: nil)
243
 
272
 
244
     init(profile: SavedProfile, showBuildCV: Bool) {
273
     init(profile: SavedProfile, showBuildCV: Bool) {
245
         self.profileID = profile.id
274
         self.profileID = profile.id
@@ -254,11 +283,11 @@ private final class ProfileListRowView: NSView {
254
             layer?.cornerCurve = .continuous
283
             layer?.cornerCurve = .continuous
255
         }
284
         }
256
 
285
 
257
-        nameLabel.stringValue = profile.profileDisplayName.isEmpty ? "Untitled profile" : profile.profileDisplayName
286
+        nameLabel.stringValue = profile.profileDisplayName.isEmpty ? L("Untitled profile") : profile.profileDisplayName
258
         nameLabel.font = .systemFont(ofSize: 15, weight: .semibold)
287
         nameLabel.font = .systemFont(ofSize: 15, weight: .semibold)
259
 
288
 
260
         let detailParts = [profile.personal.fullName, profile.personal.email].filter { !$0.isEmpty }
289
         let detailParts = [profile.personal.fullName, profile.personal.email].filter { !$0.isEmpty }
261
-        detailLabel.stringValue = detailParts.isEmpty ? "No contact details yet" : detailParts.joined(separator: " · ")
290
+        detailLabel.stringValue = detailParts.isEmpty ? L("No contact details yet") : detailParts.joined(separator: " · ")
262
         detailLabel.font = .systemFont(ofSize: 12, weight: .regular)
291
         detailLabel.font = .systemFont(ofSize: 12, weight: .regular)
263
         detailLabel.maximumNumberOfLines = 2
292
         detailLabel.maximumNumberOfLines = 2
264
 
293
 

Разница между файлами не показана из-за своего большого размера
+ 352 - 0
App for Indeed/ar.lproj/Localizable.strings


+ 352 - 0
App for Indeed/en.lproj/Localizable.strings

@@ -0,0 +1,352 @@
1
+/* Localizable.strings (English) */
2
+
3
+// MARK: - Common
4
+"OK" = "OK";
5
+"Cancel" = "Cancel";
6
+"Delete" = "Delete";
7
+"Remove" = "Remove";
8
+"Dismiss" = "Dismiss";
9
+"English" = "English";
10
+"Arabic" = "Arabic";
11
+
12
+// MARK: - Launch Screen
13
+"AI-POWERED" = "AI-POWERED";
14
+"Find your perfect job with the power of AI." = "Find your perfect job with the power of AI.";
15
+"Starting up…" = "Starting up…";
16
+"Loading progress" = "Loading progress";
17
+
18
+// MARK: - Launch Status
19
+"Checking your Pro subscription…" = "Checking your Pro subscription…";
20
+"Loading premium plans from the App Store…" = "Loading premium plans from the App Store…";
21
+"Preparing your job search workspace…" = "Preparing your job search workspace…";
22
+"Almost ready…" = "Almost ready…";
23
+
24
+// MARK: - Sidebar
25
+"Home" = "Home";
26
+"Saved Jobs" = "Saved Jobs";
27
+"CV Maker" = "CV Maker";
28
+"Profile" = "Profile";
29
+"Settings" = "Settings";
30
+"Premium" = "Premium";
31
+"Indeed" = "Indeed";
32
+"Open Indeed to search and apply for jobs" = "Open Indeed to search and apply for jobs";
33
+
34
+// MARK: - Dashboard / Home
35
+"Welcome" = "Welcome";
36
+"Send" = "Send";
37
+"Clear chat" = "Clear chat";
38
+"Remove all messages and start a new conversation" = "Remove all messages and start a new conversation";
39
+"Ask for roles, skills, salary, or job descriptions..." = "Ask for roles, skills, salary, or job descriptions...";
40
+"Ask AI" = "Ask AI";
41
+"1 reply left" = "1 reply left";
42
+"Apply" = "Apply";
43
+"Save" = "Save";
44
+"Saved" = "Saved";
45
+"Remove from saved" = "Remove from saved";
46
+"Show more jobs" = "Show more jobs";
47
+"This area is not available in the preview build. Use Home to search jobs." = "This area is not available in the preview build. Use Home to search jobs.";
48
+"Save jobs from Home to see them here." = "Save jobs from Home to see them here.";
49
+"No saved jobs yet. Search on Home, then tap Save on a listing." = "No saved jobs yet. Search on Home, then tap Save on a listing.";
50
+"Tell me what role you want and I will return job descriptions, key skills, and a quick fit summary." = "Tell me what role you want and I will return job descriptions, key skills, and a quick fit summary.";
51
+"1 saved position" = "1 saved position";
52
+"Delete this profile?" = "Delete this profile?";
53
+"Find roles similar to: " = "Find roles similar to: ";
54
+"Find jobs at company: " = "Find jobs at company: ";
55
+"Find jobs that require skill: " = "Find jobs that require skill: ";
56
+"match" = "match";
57
+"matches" = "matches";
58
+
59
+// MARK: - Feature Shortcuts
60
+"Role" = "Role";
61
+"Explore similar or better job roles" = "Explore similar or better job roles";
62
+"Company" = "Company";
63
+"Find opportunities at other companies" = "Find opportunities at other companies";
64
+"Skill" = "Skill";
65
+"Match jobs that fit your skills" = "Match jobs that fit your skills";
66
+
67
+// MARK: - Pro / Subscription
68
+"Upgrade to Pro" = "Upgrade to Pro";
69
+"You're on Pro" = "You're on Pro";
70
+"Unlimited AI matches, smart alerts, and interview prep—all in one place." = "Unlimited AI matches, smart alerts, and interview prep—all in one place.";
71
+"Manage billing, renewals, and plans in Premium." = "Manage billing, renewals, and plans in Premium.";
72
+"Try Pro" = "Try Pro";
73
+"Manage Subscription" = "Manage Subscription";
74
+"Premium Plans" = "Premium Plans";
75
+"Unlock unlimited access to premium tools and boost your productivity." = "Unlock unlimited access to premium tools and boost your productivity.";
76
+"Continue with free plan" = "Continue with free plan";
77
+"Restore Purchase" = "Restore Purchase";
78
+"You're subscribed" = "You're subscribed";
79
+"Thank you — Pro features are now available." = "Thank you — Pro features are now available.";
80
+"Pro" = "Pro";
81
+"Purchases restored" = "Purchases restored";
82
+"Your subscription is active." = "Your subscription is active.";
83
+"No subscription found" = "No subscription found";
84
+"There was nothing to restore for this Apple ID." = "There was nothing to restore for this Apple ID.";
85
+"Something went wrong" = "Something went wrong";
86
+"That subscription isn’t available from the App Store right now." = "That subscription isn’t available from the App Store right now.";
87
+"Unlimited AI job search on Home" = "Unlimited AI job search on Home";
88
+"Save jobs & open listings in-app" = "Save jobs & open listings in-app";
89
+"CV Maker, profiles & PDF export" = "CV Maker, profiles & PDF export";
90
+"Role, company & skill shortcuts" = "Role, company & skill shortcuts";
91
+
92
+// MARK: - Paywall Plans
93
+"Weekly" = "Weekly";
94
+"Flexible and commitment-free" = "Flexible and commitment-free";
95
+"Monthly" = "Monthly";
96
+"Balanced for regular productivity" = "Balanced for regular productivity";
97
+"Yearly" = "Yearly";
98
+"Best value for long-term users" = "Best value for long-term users";
99
+"/ week" = "/ week";
100
+"/ month" = "/ month";
101
+"/ year" = "/ year";
102
+"3 days free trial" = "3 days free trial";
103
+"Perfect for short-term job hunts" = "Perfect for short-term job hunts";
104
+"Cancel anytime" = "Cancel anytime";
105
+"Best for regular job seekers" = "Best for regular job seekers";
106
+"Priority support" = "Priority support";
107
+"Lowest effective monthly cost" = "Lowest effective monthly cost";
108
+"Ideal for long-term use" = "Ideal for long-term use";
109
+
110
+// MARK: - Paywall Trust
111
+"Secure Payments" = "Secure Payments";
112
+"Your payment is 100% secure." = "Your payment is 100% secure.";
113
+"Cancel Anytime" = "Cancel Anytime";
114
+"No commitment, cancel anytime." = "No commitment, cancel anytime.";
115
+"24/7 Support" = "24/7 Support";
116
+"We're here to help you anytime." = "We're here to help you anytime.";
117
+"Privacy First" = "Privacy First";
118
+"Your data is safe with us." = "Your data is safe with us.";
119
+
120
+// MARK: - Settings
121
+"Appearance" = "Appearance";
122
+"Theme" = "Theme";
123
+"Language" = "Language";
124
+"Share App" = "Share App";
125
+"More Apps" = "More Apps";
126
+"About" = "About";
127
+"Website" = "Website";
128
+"Support" = "Support";
129
+"Terms of Use" = "Terms of Use";
130
+"Privacy Policy" = "Privacy Policy";
131
+"System" = "System";
132
+"Light" = "Light";
133
+"Dark" = "Dark";
134
+
135
+// MARK: - Profiles
136
+"Profiles" = "Profiles";
137
+"Add new profile" = "Add new profile";
138
+"Create and manage CV profiles. Each profile stores your details on this Mac." = "Create and manage CV profiles. Each profile stores your details on this Mac.";
139
+"No profiles yet. Tap “Add new profile” to create your first one." = "No profiles yet. Tap “Add new profile” to create your first one.";
140
+"Build CV" = "Build CV";
141
+"Edit" = "Edit";
142
+"Untitled profile" = "Untitled profile";
143
+"No contact details yet" = "No contact details yet";
144
+"← Profiles" = "← Profiles";
145
+
146
+// MARK: - Profile Editor
147
+"Save Profile  →" = "Save Profile  →";
148
+"← All profiles" = "← All profiles";
149
+"New profile" = "New profile";
150
+"Edit profile" = "Edit profile";
151
+"Profile Name *" = "Profile Name *";
152
+"Marketing Director Profile" = "Marketing Director Profile";
153
+"Personal Information" = "Personal Information";
154
+"Full Name *" = "Full Name *";
155
+"John Doe" = "John Doe";
156
+"Email *" = "Email *";
157
+"john@example.com" = "john@example.com";
158
+"Phone" = "Phone";
159
+"+1 (555) 123-4567" = "+1 (555) 123-4567";
160
+"Job Title *" = "Job Title *";
161
+"Software Engineer" = "Software Engineer";
162
+"Address" = "Address";
163
+"123 Main St, City, State, ZIP" = "123 Main St, City, State, ZIP";
164
+"Certificates / Rewards" = "Certificates / Rewards";
165
+"List your certificates and awards..." = "List your certificates and awards...";
166
+"Interests" = "Interests";
167
+"List your interests and hobbies..." = "List your interests and hobbies...";
168
+"Languages" = "Languages";
169
+"List languages you speak (e.g., English - Native, Spanish - Fluent)..." = "List languages you speak (e.g., English - Native, Spanish - Fluent)...";
170
+"Career Summary" = "Career Summary";
171
+"Brief overview of your professional background and key achievements..." = "Brief overview of your professional background and key achievements...";
172
+"Referral (Optional)" = "Referral (Optional)";
173
+"Referred by (Company/Person Name)" = "Referred by (Company/Person Name)";
174
+"If someone referred you for this job, enter their name or company here" = "If someone referred you for this job, enter their name or company here";
175
+"Work Experience" = "Work Experience";
176
+"Education" = "Education";
177
+"+ Add Another" = "+ Add Another";
178
+"Complete required fields" = "Complete required fields";
179
+"Remove experience" = "Remove experience";
180
+"Remove education" = "Remove education";
181
+"Company Name *" = "Company Name *";
182
+"Duration *" = "Duration *";
183
+"Description" = "Description";
184
+"e.g., Software Engineer" = "e.g., Software Engineer";
185
+"e.g., Google" = "e.g., Google";
186
+"e.g., Jan 2020 - Present" = "e.g., Jan 2020 - Present";
187
+"Describe your responsibilities and achievements..." = "Describe your responsibilities and achievements...";
188
+"Degree / program *" = "Degree / program *";
189
+"Institution *" = "Institution *";
190
+"Year *" = "Year *";
191
+"e.g., BSc Computer Science" = "e.g., BSc Computer Science";
192
+"e.g., MIT" = "e.g., MIT";
193
+"e.g., 2020" = "e.g., 2020";
194
+"Profile name" = "Profile name";
195
+"Full Name" = "Full Name";
196
+"Email" = "Email";
197
+"Job Title" = "Job Title";
198
+
199
+// MARK: - CV Maker
200
+"Templates" = "Templates";
201
+"Polished layouts with live previews — pick a style that fits your story." = "Polished layouts with live previews — pick a style that fits your story.";
202
+"Use Template & Select Profile  →" = "Use Template & Select Profile  →";
203
+"All" = "All";
204
+"No templates yet for this category." = "No templates yet for this category.";
205
+"Pick a template" = "Pick a template";
206
+"Select a template first, then choose a profile to continue." = "Select a template first, then choose a profile to continue.";
207
+"Fetching AI-curated templates…" = "Fetching AI-curated templates…";
208
+"Couldn’t load AI templates — showing the built-in gallery." = "Couldn’t load AI templates — showing the built-in gallery.";
209
+"Design-Based" = "Design-Based";
210
+"Profession-Based" = "Profession-Based";
211
+"Professional" = "Professional";
212
+"Modern" = "Modern";
213
+"Creative" = "Creative";
214
+"Minimal" = "Minimal";
215
+"Executive" = "Executive";
216
+"ATS layout" = "ATS layout";
217
+"Sidebar left" = "Sidebar left";
218
+"Sidebar right" = "Sidebar right";
219
+
220
+// MARK: - CV Preview
221
+"CV preview" = "CV preview";
222
+"Export PDF…" = "Export PDF…";
223
+"Layout matches the CV Maker thumbnail for this template. Export a PDF that matches what you see here (fonts, columns, colours, and rules)." = "Layout matches the CV Maker thumbnail for this template. Export a PDF that matches what you see here (fonts, columns, colours, and rules).";
224
+"The résumé could not be rendered to PDF (empty output). Try scrolling the preview so it lays out, then export again." = "The résumé could not be rendered to PDF (empty output). Try scrolling the preview so it lays out, then export again.";
225
+"Couldn’t save PDF" = "Couldn’t save PDF";
226
+"Your name" = "Your name";
227
+"Professional headline" = "Professional headline";
228
+"Experience" = "Experience";
229
+"Highlights" = "Highlights";
230
+"Summary" = "Summary";
231
+"Contact" = "Contact";
232
+"Skills" = "Skills";
233
+"Tools" = "Tools";
234
+"Languages & more" = "Languages & more";
235
+"Certificates" = "Certificates";
236
+"Referrals" = "Referrals";
237
+"Professional Summary" = "Professional Summary";
238
+"Selected Experience" = "Selected Experience";
239
+"Core Competencies" = "Core Competencies";
240
+"Impact" = "Impact";
241
+"Add contact in your profile" = "Add contact in your profile";
242
+"Add contact details in your profile" = "Add contact details in your profile";
243
+"Add a career summary or interests in your profile to populate this column." = "Add a career summary or interests in your profile to populate this column.";
244
+"CV" = "CV";
245
+"Open to relocation" = "Open to relocation";
246
+"STRENGTHS" = "STRENGTHS";
247
+"PORTFOLIO SNAPSHOT" = "PORTFOLIO SNAPSHOT";
248
+"Close" = "Close";
249
+"/ day" = "/ day";
250
+"/ %d days" = "/ %d days";
251
+"/ %d weeks" = "/ %d weeks";
252
+"/ %d months" = "/ %d months";
253
+"/ %d years" = "/ %d years";
254
+
255
+// MARK: - CV Template Names
256
+"Paper White" = "Paper White";
257
+"Swiss" = "Swiss";
258
+"Mono" = "Mono";
259
+"Airy" = "Airy";
260
+"Tabular" = "Tabular";
261
+"Facet" = "Facet";
262
+"Corporate" = "Corporate";
263
+"Atlas" = "Atlas";
264
+"Ledger" = "Ledger";
265
+"Harbor" = "Harbor";
266
+"Clear Path" = "Clear Path";
267
+"Pinstripe" = "Pinstripe";
268
+"Briefing" = "Briefing";
269
+"Quorum" = "Quorum";
270
+"Docket" = "Docket";
271
+"Conduit" = "Conduit";
272
+"Principal" = "Principal";
273
+"Charter" = "Charter";
274
+"Vertex" = "Vertex";
275
+"Linea" = "Linea";
276
+"Prism" = "Prism";
277
+"Circuit" = "Circuit";
278
+"North" = "North";
279
+"Axis" = "Axis";
280
+"Marigold" = "Marigold";
281
+"Ember" = "Ember";
282
+"Lattice" = "Lattice";
283
+"Bloom" = "Bloom";
284
+"Studio" = "Studio";
285
+"Kite" = "Kite";
286
+"Regent" = "Regent";
287
+"Monarch" = "Monarch";
288
+"Sterling" = "Sterling";
289
+"Summit" = "Summit";
290
+"Estate" = "Estate";
291
+"Chairman" = "Chairman";
292
+"Blue Ocean" = "Blue Ocean";
293
+
294
+// MARK: - CV Demo Preview Content
295
+"Sarah Johnson" = "Sarah Johnson";
296
+"Senior Product Manager" = "Senior Product Manager";
297
+"Group PM, Consumer Growth & Activation" = "Group PM, Consumer Growth & Activation";
298
+"Google · Mountain View, CA · 2019 – Present" = "Google · Mountain View, CA · 2019 – Present";
299
+"Stanford University" = "Stanford University";
300
+"M.S. Management Science & Engineering" = "M.S. Management Science & Engineering";
301
+"2014 – 2016" = "2014 – 2016";
302
+"Mountain View, CA" = "Mountain View, CA";
303
+"Product leader shipping roadmap, discovery, and analytics for high-scale consumer experiences." = "Product leader shipping roadmap, discovery, and analytics for high-scale consumer experiences.";
304
+"Defined multi-year platform strategy with exec stakeholders and quarterly OKRs." = "Defined multi-year platform strategy with exec stakeholders and quarterly OKRs.";
305
+"Partnered with engineering and design to launch experiments improving activation by 12%." = "Partnered with engineering and design to launch experiments improving activation by 12%.";
306
+"Stood up quarterly business reviews with finance and GTM, aligning spend to north-star metrics." = "Stood up quarterly business reviews with finance and GTM, aligning spend to north-star metrics.";
307
+"Presented roadmap shifts to the leadership team and translated trade-offs into clear investment asks." = "Presented roadmap shifts to the leadership team and translated trade-offs into clear investment asks.";
308
+"Figma · SQL · Amplitude · Jira · BigQuery" = "Figma · SQL · Amplitude · Jira · BigQuery";
309
+"Product Strategy" = "Product Strategy";
310
+"A/B Testing" = "A/B Testing";
311
+"Roadmapping" = "Roadmapping";
312
+"CONTACT" = "CONTACT";
313
+"SKILLS" = "SKILLS";
314
+"PROFILE" = "PROFILE";
315
+"EXPERIENCE" = "EXPERIENCE";
316
+"EDUCATION" = "EDUCATION";
317
+"SUMMARY" = "SUMMARY";
318
+"PROFESSIONAL SUMMARY" = "PROFESSIONAL SUMMARY";
319
+"SELECTED EXPERIENCE" = "SELECTED EXPERIENCE";
320
+
321
+// MARK: - Job Browser
322
+"Return to the previous screen" = "Return to the previous screen";
323
+
324
+// MARK: - Errors
325
+"We couldn't reach the server. Check your internet connection and try again." = "We couldn't reach the server. Check your internet connection and try again.";
326
+"The search was cancelled. Try again when you're ready." = "The search was cancelled. Try again when you're ready.";
327
+"Something went wrong while searching. Please try again in a moment." = "Something went wrong while searching. Please try again in a moment.";
328
+"Job search is unavailable." = "Job search is unavailable.";
329
+
330
+// MARK: - Alerts
331
+"This profile will be removed from this Mac." = "This profile will be removed from this Mac.";
332
+
333
+// MARK: - Format Strings
334
+"Loading %@" = "Loading %@";
335
+"Loading %@. %@" = "Loading %@. %@";
336
+"Starting %@…" = "Starting %@…";
337
+"%d replies left" = "%d replies left";
338
+"%d saved positions" = "%d saved positions";
339
+"“%@” will be removed from this Mac." = "“%@” will be removed from this Mac.";
340
+"%@ isn’t available yet" = "%@ isn’t available yet";
341
+"I couldn't find new matches for “%@”. Try a different angle or a more specific keyword." = "I couldn't find new matches for “%@”. Try a different angle or a more specific keyword.";
342
+"No jobs found for “%@”. Try another title, skill, company, or location." = "No jobs found for “%@”. Try another title, skill, company, or location.";
343
+"Here are %d more %@ for “%@”." = "Here are %d more %@ for “%@”.";
344
+"Found %d %@ for “%@”. Tap Apply to open the listing or Save to revisit later." = "Found %d %@ for “%@”. Tap Apply to open the listing or Save to revisit later.";
345
+"Get %@" = "Get %@";
346
+"You chose the “%@” template. Tap Build CV on a profile to preview your résumé with that layout." = "You chose the “%@” template. Tap Build CV on a profile to preview your résumé with that layout.";
347
+"Experience %d" = "Experience %d";
348
+"Education %d" = "Education %d";
349
+"Please fill in: %@." = "Please fill in: %@.";
350
+
351
+// MARK: - Multi-line
352
+"Add your Mac App Store IDs in the target’s build settings:\n• AppStoreAppID — numeric app ID from App Store Connect\n• AppStoreDeveloperID — numeric developer ID (for your other apps page)" = "Add your Mac App Store IDs in the target’s build settings:\n• AppStoreAppID — numeric app ID from App Store Connect\n• AppStoreDeveloperID — numeric developer ID (for your other apps page)";