浏览代码

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 天之前
父节点
当前提交
75f5cafd97

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

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

+ 1 - 0
App for Indeed/AppDelegate.swift

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

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

@@ -31,9 +31,10 @@ final class IndeedJobBrowserViewController: NSViewController, WKNavigationDelega
31 31
     private let backButton = NSButton()
32 32
     private let forwardButton = NSButton()
33 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 35
     private let toolbarContainer = NSView()
36 36
     private var appearanceObserver: NSObjectProtocol?
37
+    private var languageObserver: NSObjectProtocol?
37 38
 
38 39
     override func loadView() {
39 40
         view = NSView(frame: NSRect(x: 0, y: 0, width: 920, height: 720))
@@ -55,7 +56,7 @@ final class IndeedJobBrowserViewController: NSViewController, WKNavigationDelega
55 56
         dismissEmbeddedButton.isBordered = true
56 57
         dismissEmbeddedButton.target = self
57 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 61
         toolbarContainer.translatesAutoresizingMaskIntoConstraints = false
61 62
         toolbarContainer.wantsLayer = true
@@ -112,6 +113,13 @@ final class IndeedJobBrowserViewController: NSViewController, WKNavigationDelega
112 113
         ) { [weak self] _ in
113 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 124
         if let pendingURL {
117 125
             webView.load(URLRequest(url: pendingURL))
@@ -123,6 +131,9 @@ final class IndeedJobBrowserViewController: NSViewController, WKNavigationDelega
123 131
         if let appearanceObserver {
124 132
             NotificationCenter.default.removeObserver(appearanceObserver)
125 133
         }
134
+        if let languageObserver {
135
+            NotificationCenter.default.removeObserver(languageObserver)
136
+        }
126 137
     }
127 138
 
128 139
     func loadPage(_ url: URL) {
@@ -166,6 +177,11 @@ final class IndeedJobBrowserViewController: NSViewController, WKNavigationDelega
166 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 185
     private func updateNavigationButtons() {
170 186
         backButton.isEnabled = webView.canGoBack
171 187
         forwardButton.isEnabled = webView.canGoForward
@@ -272,7 +288,7 @@ final class IndeedJobBrowserViewController: NSViewController, WKNavigationDelega
272 288
     ) {
273 289
         let alert = NSAlert()
274 290
         alert.messageText = message
275
-        alert.addButton(withTitle: NSLocalizedString("OK", comment: "Web alert dismiss"))
291
+        alert.addButton(withTitle: L("OK"))
276 292
         alert.runModal()
277 293
         completionHandler()
278 294
     }
@@ -285,8 +301,8 @@ final class IndeedJobBrowserViewController: NSViewController, WKNavigationDelega
285 301
     ) {
286 302
         let alert = NSAlert()
287 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 306
         completionHandler(alert.runModal() == .alertFirstButtonReturn)
291 307
     }
292 308
 
@@ -299,8 +315,8 @@ final class IndeedJobBrowserViewController: NSViewController, WKNavigationDelega
299 315
     ) {
300 316
         let alert = NSAlert()
301 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 320
         let field = NSTextField(frame: NSRect(x: 0, y: 0, width: 280, height: 24))
305 321
         field.stringValue = defaultText ?? ""
306 322
         alert.accessoryView = field

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

@@ -8,7 +8,7 @@ final class PremiumPlansWindowController: NSWindowController {
8 8
     init() {
9 9
         let viewController = PremiumPlansViewController()
10 10
         let window = NSWindow(contentViewController: viewController)
11
-        window.title = "Premium Plans"
11
+        window.title = L("Premium Plans")
12 12
         // Borderless avoids titled-window chrome: its rounded titlebar frame often leaves dark wedges at
13 13
         // the corners when combined with a custom full-bleed paywall (this window is only shown as a sheet).
14 14
         window.styleMask = [.borderless, .closable, .resizable]
@@ -293,7 +293,7 @@ private final class PremiumPlansViewController: NSViewController {
293 293
             wantsLayer = true
294 294
             layer?.cornerRadius = 15
295 295
             bezelStyle = .regularSquare
296
-            image = NSImage(systemSymbolName: "xmark", accessibilityDescription: "Close")
296
+            image = NSImage(systemSymbolName: "xmark", accessibilityDescription: L("Close"))
297 297
             imageScaling = .scaleProportionallyDown
298 298
             focusRingType = .none
299 299
             translatesAutoresizingMaskIntoConstraints = false
@@ -480,6 +480,7 @@ private final class PremiumPlansViewController: NSViewController {
480 480
         let featureLabels: [NSTextField]
481 481
         let featureIcons: [NSImageView]
482 482
         let purchaseButton: PlanPurchaseHoverButton
483
+        let billedPillLabel: NSTextField?
483 484
     }
484 485
 
485 486
     private enum FeatureListMetrics {
@@ -497,68 +498,73 @@ private final class PremiumPlansViewController: NSViewController {
497 498
     private var premiumCloseButton: PremiumCloseHoverButton?
498 499
     private var subscriptionStatusObservation: NSObjectProtocol?
499 500
     private var appearanceObserver: NSObjectProtocol?
501
+    private var languageObserver: NSObjectProtocol?
500 502
 
501 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 569
     private let pageGradient = CAGradientLayer()
564 570
     private var premiumTitleLabel: NSTextField?
@@ -574,6 +580,9 @@ private final class PremiumPlansViewController: NSViewController {
574 580
         if let appearanceObserver {
575 581
             NotificationCenter.default.removeObserver(appearanceObserver)
576 582
         }
583
+        if let languageObserver {
584
+            NotificationCenter.default.removeObserver(languageObserver)
585
+        }
577 586
     }
578 587
 
579 588
     override func viewDidLoad() {
@@ -585,6 +594,13 @@ private final class PremiumPlansViewController: NSViewController {
585 594
         ) { [weak self] _ in
586 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 604
         subscriptionStatusObservation = NotificationCenter.default.addObserver(
589 605
             forName: .subscriptionStatusDidChange,
590 606
             object: nil,
@@ -627,13 +643,13 @@ private final class PremiumPlansViewController: NSViewController {
627 643
         crownIcon.image = NSImage(systemSymbolName: "crown.fill", accessibilityDescription: nil)
628 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 647
         title.font = .systemFont(ofSize: 40, weight: .semibold)
632 648
         title.textColor = Theme.primaryText
633 649
         title.alignment = .center
634 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 653
         subtitle.font = .systemFont(ofSize: 14, weight: .regular)
638 654
         subtitle.textColor = Theme.secondaryText
639 655
         subtitle.alignment = .center
@@ -800,7 +816,7 @@ private final class PremiumPlansViewController: NSViewController {
800 816
 
801 817
         let selectButton = PlanPurchaseHoverButton(
802 818
             planId: plan.id,
803
-            title: "Get \(plan.title)",
819
+            title: String(format: L("Get %@"), plan.title),
804 820
             isPrimaryStyle: plan.highlight,
805 821
             target: self,
806 822
             action: #selector(didTapSelectPlan)
@@ -830,7 +846,8 @@ private final class PremiumPlansViewController: NSViewController {
830 846
                 divider: divider,
831 847
                 featureLabels: featureLabels,
832 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 983
     private func makeTrustRow() -> NSView {
967 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 990
         badges.orientation = .horizontal
974 991
         badges.alignment = .centerY
@@ -997,10 +1014,10 @@ private final class PremiumPlansViewController: NSViewController {
997 1014
         subscriptionPrimaryFooterButton = primary.button
998 1015
 
999 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 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 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 1073
     private func subscriptionPrimaryFooterTitle() -> String {
@@ -1163,10 +1180,10 @@ private final class PremiumPlansViewController: NSViewController {
1163 1180
     private func periodSuffix(for period: Product.SubscriptionPeriod) -> String {
1164 1181
         let value = period.value
1165 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 1187
         @unknown default: return ""
1171 1188
         }
1172 1189
     }
@@ -1185,10 +1202,10 @@ private final class PremiumPlansViewController: NSViewController {
1185 1202
             guard completed else { return }
1186 1203
             AppRatingCoordinator.shared.scheduleReviewAfterSubscriptionPurchase()
1187 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 1207
             alert.alertStyle = .informational
1191
-            alert.addButton(withTitle: "OK")
1208
+            alert.addButton(withTitle: L("OK"))
1192 1209
             if let window = view.window {
1193 1210
                 alert.beginSheetModal(for: window) { [weak self] _ in
1194 1211
                     self?.dismissPremiumSheetFromParentIfNeeded()
@@ -1212,14 +1229,14 @@ private final class PremiumPlansViewController: NSViewController {
1212 1229
             let active = subscriptionStore.isProActive
1213 1230
             let alert = NSAlert()
1214 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 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 1238
             alert.alertStyle = .informational
1222
-            alert.addButton(withTitle: "OK")
1239
+            alert.addButton(withTitle: L("OK"))
1223 1240
             if let window = view.window {
1224 1241
                 alert.beginSheetModal(for: window) { [weak self] _ in
1225 1242
                     if active {
@@ -1241,7 +1258,7 @@ private final class PremiumPlansViewController: NSViewController {
1241 1258
 
1242 1259
     private func presentPurchaseError(_ error: Error) {
1243 1260
         let alert = NSAlert()
1244
-        alert.messageText = "Something went wrong"
1261
+        alert.messageText = L("Something went wrong")
1245 1262
         if let localized = error as? LocalizedError {
1246 1263
             var parts: [String] = []
1247 1264
             if let description = localized.errorDescription {
@@ -1255,7 +1272,7 @@ private final class PremiumPlansViewController: NSViewController {
1255 1272
             alert.informativeText = error.localizedDescription
1256 1273
         }
1257 1274
         alert.alertStyle = .warning
1258
-        alert.addButton(withTitle: "OK")
1275
+        alert.addButton(withTitle: L("OK"))
1259 1276
         if let window = view.window {
1260 1277
             alert.beginSheetModal(for: window)
1261 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 1336
     private func dismissPremiumSheetFromParentIfNeeded() {
1267 1337
         guard let sheet = view.window, let parent = sheet.sheetParent else { return }
1268 1338
         parent.endSheet(sheet)

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

@@ -30,13 +30,13 @@ protocol DashboardDataProviding {
30 30
 final class MockDashboardDataProvider: DashboardDataProviding {
31 31
     func loadDashboardData() -> DashboardData {
32 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 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 41
             jobListings: [
42 42
                 JobListing(

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

@@ -14,19 +14,19 @@ enum AppLaunchCoordinator {
14 14
 
15 15
     /// Subscription refresh and product catalog load before the dashboard appears.
16 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 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 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 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 27
         NotificationCenter.default.post(name: .subscriptionStatusDidChange, object: nil)
28 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 11
                  .cannotFindHost,
12 12
                  .cannotConnectToHost,
13 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 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 17
             default:
18 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 151
     var errorDescription: String? {
152 152
         switch self {
153 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 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 112
     private let scrollView = NSScrollView()
113 113
     private let documentView = CVPreviewFlippedDocumentView()
114 114
     private let contentStack = NSStackView()
@@ -119,6 +119,7 @@ final class CVFilledPreviewPageView: NSView {
119 119
 
120 120
     private let subtitleLabel = NSTextField(wrappingLabelWithString: "")
121 121
     private var appearanceObserver: NSObjectProtocol?
122
+    private var languageObserver: NSObjectProtocol?
122 123
 
123 124
     override init(frame frameRect: NSRect) {
124 125
         super.init(frame: frameRect)
@@ -138,7 +139,7 @@ final class CVFilledPreviewPageView: NSView {
138 139
         exportButton.target = self
139 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 143
         subtitleLabel.font = .systemFont(ofSize: 12, weight: .regular)
143 144
         subtitleLabel.maximumNumberOfLines = 0
144 145
 
@@ -200,13 +201,24 @@ final class CVFilledPreviewPageView: NSView {
200 201
         ) { [weak self] _ in
201 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 211
         applyCurrentAppearance()
212
+        applyLocalizedStrings()
204 213
     }
205 214
 
206 215
     deinit {
207 216
         if let appearanceObserver {
208 217
             NotificationCenter.default.removeObserver(appearanceObserver)
209 218
         }
219
+        if let languageObserver {
220
+            NotificationCenter.default.removeObserver(languageObserver)
221
+        }
210 222
     }
211 223
 
212 224
     @available(*, unavailable)
@@ -227,6 +239,16 @@ final class CVFilledPreviewPageView: NSView {
227 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 252
     func configure(profile: SavedProfile, template: CVTemplate) {
231 253
         lastProfile = profile
232 254
         lastTemplate = template
@@ -237,8 +259,8 @@ final class CVFilledPreviewPageView: NSView {
237 259
         let doc = CVProfileDocumentView(profile: profile, template: template)
238 260
         profileDocumentView = doc
239 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 266
     @objc private func didTapBack() {
@@ -263,14 +285,14 @@ final class CVFilledPreviewPageView: NSView {
263 285
         // text. Rasterising what is actually drawn on screen preserves the full layout.
264 286
         let data = doc.pdfDataMatchingScreenAppearance() ?? doc.dataWithPDF(inside: bounds)
265 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 289
             return
268 290
         }
269 291
 
270 292
         let panel = NSSavePanel()
271 293
         panel.canCreateDirectories = true
272 294
         panel.allowedContentTypes = [.pdf]
273
-        let base = lastTemplate?.name ?? "CV"
295
+        let base = lastTemplate?.name ?? L("CV")
274 296
         let safe = base.replacingOccurrences(of: "/", with: "-")
275 297
         panel.nameFieldStringValue = "\(safe).pdf"
276 298
 
@@ -300,10 +322,10 @@ final class CVFilledPreviewPageView: NSView {
300 322
 
301 323
     private func presentExportError(_ message: String) {
302 324
         let alert = NSAlert()
303
-        alert.messageText = "Couldn’t save PDF"
325
+        alert.messageText = L("Couldn't save PDF")
304 326
         alert.informativeText = message
305 327
         alert.alertStyle = .warning
306
-        alert.addButton(withTitle: "OK")
328
+        alert.addButton(withTitle: L("OK"))
307 329
         if let window {
308 330
             alert.beginSheetModal(for: window, completionHandler: nil)
309 331
         } else {

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

@@ -18,8 +18,8 @@ enum CVCategoryGroup: Hashable {
18 18
 
19 19
     var title: String {
20 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 30
     var title: String {
31 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 47
     var gallerySubtitle: String {
48 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 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 127
     /// Optional bundle image name; `nil` means render a live vector/text preview.
125 128
     var previewImageAssetName: String? { nil }
126 129
 
@@ -579,19 +582,20 @@ final class CVMakerPageView: NSView {
579 582
     }
580 583
 
581 584
     private var appearanceObserver: NSObjectProtocol?
585
+    private var languageObserver: NSObjectProtocol?
582 586
 
583 587
     private let pageGradientLayer = CAGradientLayer()
584 588
     private let filterChrome = NSVisualEffectView()
585 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 593
     private let groupTabsRow = NSStackView()
590 594
     private let familyChipsRow = NSStackView()
591 595
     private let scrollView = NSScrollView()
592 596
     private let gridDocument = TopFlippedView()
593 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 600
     private var selectedGroup: CVCategoryGroup = .professionBased
597 601
     private var selectedFamily: CVDesignFamily? = nil // nil == "All"
@@ -648,13 +652,24 @@ final class CVMakerPageView: NSView {
648 652
         ) { [weak self] _ in
649 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 662
         applyCurrentAppearance()
663
+        applyLocalizedStrings()
652 664
     }
653 665
 
654 666
     deinit {
655 667
         if let appearanceObserver {
656 668
             NotificationCenter.default.removeObserver(appearanceObserver)
657 669
         }
670
+        if let languageObserver {
671
+            NotificationCenter.default.removeObserver(languageObserver)
672
+        }
658 673
     }
659 674
 
660 675
     @available(*, unavailable)
@@ -685,6 +700,16 @@ final class CVMakerPageView: NSView {
685 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 713
     // MARK: Setup
689 714
 
690 715
     private func configureLayout() {
@@ -850,7 +875,7 @@ final class CVMakerPageView: NSView {
850 875
         familyChipButtons.removeAll()
851 876
 
852 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 879
         allChip.translatesAutoresizingMaskIntoConstraints = false
855 880
         allChip.onSelect = { [weak self] in self?.didSelectFamily(nil) }
856 881
         familyChipsRow.addArrangedSubview(allChip)
@@ -904,7 +929,7 @@ final class CVMakerPageView: NSView {
904 929
 
905 930
         let templates = visibleTemplates
906 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 933
             empty.font = .systemFont(ofSize: 13)
909 934
             empty.textColor = Palette.secondaryText
910 935
             gridStack.addArrangedSubview(empty)
@@ -1044,7 +1069,7 @@ final class CVMakerPageView: NSView {
1044 1069
             chosen = nil
1045 1070
         }
1046 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 1073
             return
1049 1074
         }
1050 1075
         onContinueToProfileSelection?(template)
@@ -1060,7 +1085,7 @@ final class CVMakerPageView: NSView {
1060 1085
     }
1061 1086
 
1062 1087
     private func styleCTAButton(_ button: CVHoverableButton) {
1063
-        button.title = "Use Template & Select Profile  →"
1088
+        button.title = L("Use Template & Select Profile  →")
1064 1089
         button.font = .systemFont(ofSize: 14, weight: .semibold)
1065 1090
         button.isBordered = false
1066 1091
         button.bezelStyle = .rounded
@@ -1082,8 +1107,8 @@ final class CVMakerPageView: NSView {
1082 1107
 
1083 1108
     private func beginLoadingAICatalogIfPossible() {
1084 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 1112
         CVTemplateFetchService.shared.fetchTemplates { [weak self] result in
1088 1113
             DispatchQueue.main.async {
1089 1114
                 guard let self else { return }
@@ -1098,7 +1123,7 @@ final class CVMakerPageView: NSView {
1098 1123
                         self.updateSelectedChipStates()
1099 1124
                     }
1100 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 1127
                     DispatchQueue.main.asyncAfter(deadline: .now() + 5.5) { [weak self] in
1103 1128
                         self?.subtitleLabel.stringValue = defaultSubtitle
1104 1129
                     }
@@ -1112,7 +1137,7 @@ final class CVMakerPageView: NSView {
1112 1137
         alert.messageText = title
1113 1138
         alert.informativeText = message
1114 1139
         alert.alertStyle = .informational
1115
-        alert.addButton(withTitle: "OK")
1140
+        alert.addButton(withTitle: L("OK"))
1116 1141
         if let window {
1117 1142
             alert.beginSheetModal(for: window)
1118 1143
         } else {
@@ -1383,7 +1408,7 @@ private final class CVTemplateCard: NSView {
1383 1408
         preview.translatesAutoresizingMaskIntoConstraints = false
1384 1409
         previewSurface.addSubview(preview)
1385 1410
 
1386
-        nameLabel.stringValue = template.name
1411
+        nameLabel.stringValue = template.localizedName
1387 1412
         nameLabel.font = .systemFont(ofSize: 14, weight: .semibold)
1388 1413
         nameLabel.textColor = palette.primaryText
1389 1414
         nameLabel.isBordered = false

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

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

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

@@ -31,28 +31,28 @@ struct CVTemplateCardPalette {
31 31
 // MARK: - Demo résumé content
32 32
 
33 33
 fileprivate enum CVPreviewDemoContent {
34
-    static let fullName = "Sarah Johnson"
34
+    static var fullName: String { L("Sarah Johnson") }
35 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 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 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 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 58
 // MARK: - Mini preview
@@ -273,11 +273,11 @@ final class CVTemplatePreviewView: NSView {
273 273
         inner.orientation = .vertical
274 274
         inner.spacing = 5
275 275
         inner.alignment = .leading
276
-        inner.addArrangedSubview(sectionHeading("CONTACT"))
276
+        inner.addArrangedSubview(sectionHeading(L("CONTACT")))
277 277
         inner.addArrangedSubview(makeLabel(CVPreviewDemoContent.email, font: .systemFont(ofSize: 6.2), color: palette.previewInk, alignment: .left, maxLines: 2))
278 278
         inner.addArrangedSubview(makeLabel(CVPreviewDemoContent.phone, font: .systemFont(ofSize: 6.2), color: palette.previewMuted, alignment: .left, maxLines: 1))
279 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 281
         if variant % 5 == 2 {
282 282
             inner.addArrangedSubview(tagRow(theme: template.themeColor))
283 283
         } else {
@@ -286,7 +286,7 @@ final class CVTemplatePreviewView: NSView {
286 286
             }
287 287
         }
288 288
         if variant % 7 == 3 {
289
-            inner.addArrangedSubview(sectionHeading("TOOLS"))
289
+            inner.addArrangedSubview(sectionHeading(L("TOOLS")))
290 290
             inner.addArrangedSubview(makeLabel(CVPreviewDemoContent.toolsLine, font: .systemFont(ofSize: 5.9), color: palette.previewMuted, alignment: .left, maxLines: 1))
291 291
         }
292 292
         box.addArrangedSubview(inner)
@@ -301,11 +301,11 @@ final class CVTemplatePreviewView: NSView {
301 301
         let sp: CGFloat = compact ? 6.2 : 6.5
302 302
 
303 303
         let profileBlock: () -> Void = {
304
-            stack.addArrangedSubview(self.sectionHeading("PROFILE"))
304
+            stack.addArrangedSubview(self.sectionHeading(L("PROFILE")))
305 305
             stack.addArrangedSubview(self.makeLabel(CVPreviewDemoContent.summary, font: .systemFont(ofSize: sp), color: self.palette.previewInk, alignment: .left, maxLines: 3))
306 306
         }
307 307
         let experienceBlock: () -> Void = {
308
-            stack.addArrangedSubview(self.sectionHeading("EXPERIENCE"))
308
+            stack.addArrangedSubview(self.sectionHeading(L("EXPERIENCE")))
309 309
             stack.addArrangedSubview(self.makeLabel(CVPreviewDemoContent.experienceRole, font: .systemFont(ofSize: 6.8, weight: .semibold), color: self.palette.previewInk, alignment: .left, maxLines: 1))
310 310
             stack.addArrangedSubview(self.makeLabel(CVPreviewDemoContent.companyLine, font: .systemFont(ofSize: 6.2, weight: .medium), color: self.template.themeColor, alignment: .left, maxLines: 1))
311 311
             stack.addArrangedSubview(self.bulletRow(CVPreviewDemoContent.bullet1, size: sp))
@@ -320,7 +320,7 @@ final class CVTemplatePreviewView: NSView {
320 320
             profileBlock()
321 321
             experienceBlock()
322 322
         }
323
-        stack.addArrangedSubview(sectionHeading("EDUCATION"))
323
+        stack.addArrangedSubview(sectionHeading(L("EDUCATION")))
324 324
         stack.addArrangedSubview(makeLabel(CVPreviewDemoContent.university, font: .systemFont(ofSize: 6.6, weight: .semibold), color: palette.previewInk, alignment: .left, maxLines: 1))
325 325
         stack.addArrangedSubview(makeLabel("\(CVPreviewDemoContent.degree) · \(CVPreviewDemoContent.educationYears)", font: .systemFont(ofSize: 6.2), color: palette.previewMuted, alignment: .left, maxLines: 2))
326 326
         return stack
@@ -467,7 +467,7 @@ final class CVTemplatePreviewView: NSView {
467 467
         let onW = NSColor.white
468 468
         right.addArrangedSubview(makeLabel(CVPreviewDemoContent.email, font: .systemFont(ofSize: 5.9, weight: .medium), color: onW.withAlphaComponent(0.95), alignment: .left, maxLines: 2))
469 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 472
         let top = NSStackView(views: [left, right])
473 473
         top.orientation = .horizontal
@@ -493,9 +493,9 @@ final class CVTemplatePreviewView: NSView {
493 493
             box.layer?.cornerRadius = 4
494 494
         }
495 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 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 499
         box.addArrangedSubview(makeLabel(CVPreviewDemoContent.careerHighlights, font: .systemFont(ofSize: 6), color: palette.previewMuted, alignment: .left, maxLines: 3))
500 500
         return box
501 501
     }
@@ -520,11 +520,11 @@ final class CVTemplatePreviewView: NSView {
520 520
         stack.orientation = .vertical
521 521
         stack.spacing = 5
522 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 524
         stack.addArrangedSubview(makeLabel("\(CVPreviewDemoContent.experienceRole) — \(CVPreviewDemoContent.company)", font: .systemFont(ofSize: 6.6, weight: .semibold), color: palette.previewInk, alignment: .left, maxLines: 2))
525 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 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 528
         stack.addArrangedSubview(makeLabel("\(CVPreviewDemoContent.university), \(CVPreviewDemoContent.degree) (\(CVPreviewDemoContent.educationYears))", font: .systemFont(ofSize: 6.2), color: palette.previewInk, alignment: .left, maxLines: 2))
529 529
         return stack
530 530
     }
@@ -623,15 +623,15 @@ final class CVTemplatePreviewView: NSView {
623 623
         stack.alignment = .leading
624 624
 
625 625
         let edu: () -> Void = {
626
-            stack.addArrangedSubview(self.sectionHeading("EDUCATION"))
626
+            stack.addArrangedSubview(self.sectionHeading(L("EDUCATION")))
627 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 629
         let prof: () -> Void = {
630
-            stack.addArrangedSubview(self.sectionHeading("PROFILE"))
630
+            stack.addArrangedSubview(self.sectionHeading(L("PROFILE")))
631 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 633
         let exp: () -> Void = {
634
-            stack.addArrangedSubview(self.sectionHeading("EXPERIENCE"))
634
+            stack.addArrangedSubview(self.sectionHeading(L("EXPERIENCE")))
635 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 636
             stack.addArrangedSubview(self.makeLabel(CVPreviewDemoContent.bullet1, font: .systemFont(ofSize: 6, weight: .light), color: self.palette.previewMuted, alignment: .left, maxLines: 2))
637 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 654
         stack.orientation = .vertical
655 655
         stack.spacing = 6
656 656
         stack.alignment = .leading
657
-        stack.addArrangedSubview(sectionHeading("SKILLS"))
657
+        stack.addArrangedSubview(sectionHeading(L("SKILLS")))
658 658
         for (i, s) in CVPreviewDemoContent.skillsList.enumerated() {
659 659
             let prefix = numbered ? "\(i + 1). " : "·  "
660 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 728
         stack.orientation = .vertical
729 729
         stack.spacing = (compact ? 4 : 5) - (tightLeading ? 1 : 0)
730 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 732
         stack.addArrangedSubview(sectionHeading(sumTitle))
733 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 735
         let jobFont = NSFontManager.shared.convert(serif, toHaveTrait: .boldFontMask)
736 736
         stack.addArrangedSubview(makeLabel("\(CVPreviewDemoContent.experienceRole), \(CVPreviewDemoContent.company)", font: jobFont, color: ink, alignment: .left, maxLines: 2))
737 737
         stack.addArrangedSubview(makeLabel("2019 – Present", font: serif, color: theme, alignment: .left, maxLines: 1))
738 738
         stack.addArrangedSubview(makeLabel(CVPreviewDemoContent.bullet1, font: serif, color: muted, alignment: .left, maxLines: 2))
739 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 741
         stack.addArrangedSubview(makeLabel("\(CVPreviewDemoContent.university), \(CVPreviewDemoContent.degree) · \(CVPreviewDemoContent.educationYears)", font: serif, color: ink, alignment: .left, maxLines: 2))
742 742
         return stack
743 743
     }
@@ -756,14 +756,14 @@ final class CVTemplatePreviewView: NSView {
756 756
             stack.layer?.cornerRadius = layoutVariant % 4 == 2 ? 5 : 3
757 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 760
         for s in CVPreviewDemoContent.skillsList {
761 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 764
         stack.addArrangedSubview(makeLabel(CVPreviewDemoContent.toolsLine, font: serif, color: palette.previewMuted, alignment: .left, maxLines: 2))
765 765
         if showMetrics {
766
-            stack.addArrangedSubview(sectionHeading("IMPACT"))
766
+            stack.addArrangedSubview(sectionHeading(L("IMPACT")))
767 767
             stack.addArrangedSubview(makeLabel("+12% activation · $4.2M ARR influenced", font: serif, color: palette.previewInk, alignment: .left, maxLines: 2))
768 768
         }
769 769
         return stack
@@ -791,7 +791,7 @@ final class CVTemplatePreviewView: NSView {
791 791
         sidebar.addArrangedSubview(makeLabel(CVPreviewDemoContent.title, font: .systemFont(ofSize: 6.5, weight: .medium), color: onSidebar.withAlphaComponent(0.85), alignment: .left, maxLines: 2))
792 792
         sidebar.addArrangedSubview(makeLabel(CVPreviewDemoContent.email, font: .systemFont(ofSize: 5.8), color: onSidebar.withAlphaComponent(0.8), alignment: .left, maxLines: 1))
793 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 795
         for s in CVPreviewDemoContent.skillsList.prefix(5) {
796 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 801
         main.spacing = 4 + CGFloat(layoutVariant % 3)
802 802
         main.alignment = .leading
803 803
         main.addArrangedSubview(creativeMainHeader(theme: theme))
804
-        main.addArrangedSubview(sectionHeading("PROFILE"))
804
+        main.addArrangedSubview(sectionHeading(L("PROFILE")))
805 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 807
         main.addArrangedSubview(makeLabel("\(CVPreviewDemoContent.company) — \(CVPreviewDemoContent.experienceRole)", font: .systemFont(ofSize: 6.6, weight: .heavy), color: palette.previewInk, alignment: .left, maxLines: 2))
808 808
         let bMark = (layoutVariant % 2 == 0) ? "—  " : "▸  "
809 809
         main.addArrangedSubview(makeLabel("\(bMark)\(CVPreviewDemoContent.bullet1)", font: .systemFont(ofSize: 6), color: palette.previewMuted, alignment: .left, maxLines: 2))
810 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 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 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 120
     private let findJobsButton = HoverableButton()
121 121
     private let findJobsCTAPill = HoverableView()
122 122
     private let sendIconView = NSImageView()
123
-    private let sendLabel = NSTextField(labelWithString: "Send")
123
+    private let sendLabel = NSTextField(labelWithString: L("Send"))
124 124
     private let sendContentStack = NSStackView()
125 125
     private let findJobsCTAHost = NSView()
126 126
     private let welcomeHeroHost = NSView()
@@ -129,7 +129,7 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
129 129
     private let welcomeLogoView = IndeedLogoView(displayHeight: 40, variant: .compact)
130 130
     private let featureCardsRow = NSStackView()
131 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 133
     private let chatScrollView = NSScrollView()
134 134
     private let chatDocumentView = JobListingsDocumentView()
135 135
     private let chatStack = NSStackView()
@@ -141,7 +141,7 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
141 141
     private let nonHomeTitleLabel = NSTextField(labelWithString: "")
142 142
     private let nonHomeSubtitleLabel = NSTextField(wrappingLabelWithString: "")
143 143
     private let savedJobsPageContainer = NSView()
144
-    private let savedJobsPageTitleLabel = NSTextField(labelWithString: "Saved Jobs")
144
+    private let savedJobsPageTitleLabel = NSTextField(labelWithString: L("Saved Jobs"))
145 145
     private let savedJobsPageSubtitleLabel = NSTextField(wrappingLabelWithString: "")
146 146
     private let savedJobsScrollView = NSScrollView()
147 147
     private let savedJobsDocumentView = JobListingsDocumentView()
@@ -193,6 +193,7 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
193 193
     private weak var sidebarUpgradeButton: HoverableButton?
194 194
     private var subscriptionObserver: NSObjectProtocol?
195 195
     private var appearanceObserver: NSObjectProtocol?
196
+    private var languageObserver: NSObjectProtocol?
196 197
     /// Retains the system share picker until the user picks a destination or dismisses the menu.
197 198
     private var appSharePicker: NSSharingServicePicker?
198 199
 
@@ -226,6 +227,9 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
226 227
         if let appearanceObserver {
227 228
             NotificationCenter.default.removeObserver(appearanceObserver)
228 229
         }
230
+        if let languageObserver {
231
+            NotificationCenter.default.removeObserver(languageObserver)
232
+        }
229 233
     }
230 234
 
231 235
     override func viewDidChangeEffectiveAppearance() {
@@ -241,6 +245,103 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
241 245
         ) { [weak self] _ in
242 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 347
     override func viewDidMoveToWindow() {
@@ -262,7 +363,7 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
262 363
 
263 364
     func render(_ data: DashboardData) {
264 365
         dismissIndeedJobBrowserEmbedded()
265
-        greetingLabel.stringValue = "Welcome"
366
+        greetingLabel.stringValue = L("Welcome")
266 367
         subtitleLabel.stringValue = data.subtitle
267 368
         currentSidebarItems = data.sidebarItems
268 369
         if selectedSidebarIndex >= currentSidebarItems.count {
@@ -303,7 +404,7 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
303 404
         searchCard.layer?.borderColor = (searchHovering ? Theme.searchBarBorderHover : Theme.searchBarBorder).cgColor
304 405
         jobKeywordsField.textColor = Theme.primaryText
305 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 408
             attributes: [
308 409
                 .foregroundColor: Theme.secondaryText,
309 410
                 .font: NSFont.systemFont(ofSize: 14, weight: .regular)
@@ -318,11 +419,11 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
318 419
 
319 420
         appearanceModeSegment?.selectedSegment = AppAppearanceManager.shared.mode.segmentIndex
320 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 424
                 langPopUp.selectItem(at: index)
324 425
             }
325
-            langPopUp.isEnabled = !Self.supportedLanguages.isEmpty
426
+            langPopUp.isEnabled = !AppLanguage.allCases.isEmpty
326 427
         }
327 428
         cvMakerPageView.applyCurrentAppearance()
328 429
         profilesListPageView.applyCurrentAppearance()
@@ -340,7 +441,8 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
340 441
 
341 442
     private func refreshSettingsPageAppearance(in root: NSView) {
342 443
         for view in root.subviewsRecursive() {
343
-            switch view.identifier?.rawValue {
444
+            guard let rawID = view.identifier?.rawValue else { continue }
445
+            switch rawID {
344 446
             case SettingsAppearanceID.section:
345 447
                 view.layer?.backgroundColor = Theme.settingsGroupBackground.cgColor
346 448
                 view.layer?.borderColor = Theme.border.cgColor
@@ -351,12 +453,12 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
351 453
                 for case let icon as NSImageView in view.subviews {
352 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 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 626
         clearChatButton.font = .systemFont(ofSize: 12, weight: .medium)
525 627
         clearChatButton.target = self
526 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 630
         clearChatButton.setContentHuggingPriority(.required, for: .horizontal)
529 631
         chatHeaderRow.addArrangedSubview(chatHeaderLeadingSpacer)
530 632
         chatHeaderRow.addArrangedSubview(clearChatButton)
@@ -589,6 +691,7 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
589 691
             welcomeHeroContent.widthAnchor.constraint(equalTo: mainOverlay.widthAnchor, constant: -32)
590 692
         ])
591 693
         registerSubscriptionObserverOnce()
694
+        refreshLocalizedStrings()
592 695
     }
593 696
 
594 697
     private func registerSubscriptionObserverOnce() {
@@ -612,15 +715,15 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
612 715
 
613 716
         let descriptionWidth: CGFloat = 158
614 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 720
             upgradeDescription.preferredMaxLayoutWidth = descriptionWidth
618
-            upgradeButton.title = "Manage Subscription"
721
+            upgradeButton.title = L("Manage Subscription")
619 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 725
             upgradeDescription.preferredMaxLayoutWidth = descriptionWidth
623
-            upgradeButton.title = "Try Pro"
726
+            upgradeButton.title = L("Try Pro")
624 727
         }
625 728
         updateFreeJobSearchQuotaLabel()
626 729
     }
@@ -635,8 +738,8 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
635 738
         let remaining = FreeTierJobSearchQuota.remainingUserMessages(isProActive: false)
636 739
         freeJobSearchQuotaLabel.isHidden = false
637 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 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 799
         featureCardsRow.alignment = .top
697 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 803
             ("briefcase", "Role", "Explore similar or better job roles", #selector(didTapFeatureRole)),
701 804
             ("building.2", "Company", "Find opportunities at other companies", #selector(didTapFeatureCompany)),
702 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 807
         for spec in specs {
705 808
             let card = FeatureShortcutCardView(
706 809
                 symbolName: spec.symbol,
707
-                title: spec.title,
708
-                subtitle: spec.subtitle,
810
+                title: L(spec.titleKey),
811
+                subtitle: L(spec.subtitleKey),
709 812
                 target: self,
710 813
                 action: spec.action
711 814
             )
@@ -841,7 +944,7 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
841 944
 
842 945
     private func jobListingHostSubtitle(_ job: JobListing) -> String {
843 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 949
         if host.hasPrefix("www.") {
847 950
             return String(host.dropFirst(4))
@@ -943,7 +1046,7 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
943 1046
         descriptionField.tag = 502
944 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 1050
         applyButton.jobPayload = job
948 1051
         applyButton.cardContext = context
949 1052
         applyButton.isBordered = false
@@ -962,7 +1065,7 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
962 1065
         applyButton.setContentCompressionResistancePriority(.required, for: .horizontal)
963 1066
 
964 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 1069
         savedButton.jobPayload = job
967 1070
         savedButton.cardContext = context
968 1071
         savedButton.setButtonType(.toggle)
@@ -986,7 +1089,7 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
986 1089
         let dismissButton = JobPayloadButton()
987 1090
         dismissButton.jobPayload = job
988 1091
         dismissButton.cardContext = context
989
-        dismissButton.image = NSImage(systemSymbolName: "xmark", accessibilityDescription: "Dismiss")
1092
+        dismissButton.image = NSImage(systemSymbolName: "xmark", accessibilityDescription: L("Dismiss"))
990 1093
         dismissButton.imagePosition = .imageOnly
991 1094
         dismissButton.imageScaling = .scaleProportionallyDown
992 1095
         dismissButton.symbolConfiguration = NSImage.SymbolConfiguration(pointSize: 12, weight: .semibold)
@@ -995,7 +1098,7 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
995 1098
         dismissButton.contentTintColor = Theme.secondaryText
996 1099
         dismissButton.target = self
997 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 1102
         dismissButton.focusRingType = .none
1000 1103
         dismissButton.wantsLayer = true
1001 1104
         dismissButton.layer?.cornerRadius = 8
@@ -1199,7 +1302,7 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
1199 1302
         let willSave = !isJobSaved(job)
1200 1303
         applySavedState(willSave, for: job)
1201 1304
         sender.state = willSave ? .on : .off
1202
-        sender.title = willSave ? "Saved" : "Save"
1305
+        sender.title = willSave ? L("Saved") : L("Save")
1203 1306
         sender.image = NSImage(systemSymbolName: willSave ? "heart.fill" : "heart", accessibilityDescription: nil)
1204 1307
         styleJobSavedButton(sender)
1205 1308
         if isSavedJobsSidebarIndex(selectedSidebarIndex) {
@@ -1313,10 +1416,10 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
1313 1416
 
1314 1417
         jobSearchIcon.translatesAutoresizingMaskIntoConstraints = false
1315 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 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 1424
         let ctaHeight: CGFloat = 42
1322 1425
         let ctaCorner = ctaHeight / 2
@@ -1366,7 +1469,7 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
1366 1469
         findJobsButton.pointerCursor = true
1367 1470
         findJobsButton.target = self
1368 1471
         findJobsButton.action = #selector(didSubmitSearch)
1369
-        findJobsButton.setAccessibilityLabel("Send")
1472
+        findJobsButton.setAccessibilityLabel(L("Send"))
1370 1473
         findJobsButton.hoverHandler = { [weak self] hovering in
1371 1474
             self?.findJobsCTAPill.layer?.backgroundColor = (hovering ? Theme.brandBlueHover : Theme.brandBlue).cgColor
1372 1475
         }
@@ -1503,7 +1606,7 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
1503 1606
         nonHomeSubtitleLabel.textColor = Theme.secondaryText
1504 1607
         nonHomeSubtitleLabel.alignment = .center
1505 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 1611
         let genericStack = NSStackView(views: [nonHomeTitleLabel, nonHomeSubtitleLabel])
1509 1612
         genericStack.orientation = .vertical
@@ -1613,7 +1716,7 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
1613 1716
 
1614 1717
     /// Switches the main panel to **Profile** so the user can pick a saved CV profile after choosing a template in CV Maker.
1615 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 1720
         selectSidebarItem(at: index)
1618 1721
     }
1619 1722
 
@@ -1719,13 +1822,13 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
1719 1822
     private func confirmDeleteProfile(id: UUID) {
1720 1823
         let displayName = SavedProfilesStore.profile(id: id)?.profileDisplayName ?? ""
1721 1824
         let alert = NSAlert()
1722
-        alert.messageText = "Delete this profile?"
1825
+        alert.messageText = L("Delete this profile?")
1723 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 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 1832
         guard let window = window else {
1730 1833
             let response = alert.runModal()
1731 1834
             if response == .alertSecondButtonReturn {
@@ -1758,19 +1861,19 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
1758 1861
         contentStack.alignment = .leading
1759 1862
         contentStack.translatesAutoresizingMaskIntoConstraints = false
1760 1863
 
1761
-        let appearanceTitle = NSTextField(labelWithString: "Appearance")
1864
+        let appearanceTitle = NSTextField(labelWithString: L("Appearance"))
1762 1865
         appearanceTitle.font = .systemFont(ofSize: 12, weight: .semibold)
1763 1866
         appearanceTitle.textColor = Theme.secondaryText
1764 1867
         appearanceTitle.alignment = .left
1765
-        appearanceTitle.identifier = NSUserInterfaceItemIdentifier(SettingsAppearanceID.sectionHeader)
1868
+        appearanceTitle.identifier = NSUserInterfaceItemIdentifier("\(SettingsAppearanceID.sectionHeader).Appearance")
1766 1869
 
1767 1870
         let themeSegment = makeAppearanceModeSegment()
1768 1871
         appearanceModeSegment = themeSegment
1769 1872
         let langPopUp = makeLanguagePopUp()
1770 1873
         languagePopUp = langPopUp
1771 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 1879
         let appearanceStack = NSStackView(views: [appearanceTitle, appearanceSection])
@@ -1780,21 +1883,21 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
1780 1883
         appearanceStack.translatesAutoresizingMaskIntoConstraints = false
1781 1884
 
1782 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 1891
         aboutTitle.font = .systemFont(ofSize: 12, weight: .semibold)
1789 1892
         aboutTitle.textColor = Theme.secondaryText
1790 1893
         aboutTitle.alignment = .left
1791
-        aboutTitle.identifier = NSUserInterfaceItemIdentifier(SettingsAppearanceID.sectionHeader)
1894
+        aboutTitle.identifier = NSUserInterfaceItemIdentifier("\(SettingsAppearanceID.sectionHeader).About")
1792 1895
 
1793 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 1903
         let aboutStack = NSStackView(views: [aboutTitle, aboutSection])
@@ -1823,7 +1926,7 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
1823 1926
 
1824 1927
     private func makeAppearanceModeSegment() -> NSSegmentedControl {
1825 1928
         let segment = NSSegmentedControl(
1826
-            labels: ["System", "Light", "Dark"],
1929
+            labels: [L("System"), L("Light"), L("Dark")],
1827 1930
             trackingMode: .selectOne,
1828 1931
             target: self,
1829 1932
             action: #selector(appearanceModeChanged(_:))
@@ -1841,30 +1944,24 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
1841 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 1947
     private func makeLanguagePopUp() -> NSPopUpButton {
1851 1948
         let popup = NSPopUpButton(frame: .zero, pullsDown: false)
1852 1949
         popup.translatesAutoresizingMaskIntoConstraints = false
1853 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 1959
             popup.selectItem(at: index)
1863 1960
         }
1864 1961
 
1865 1962
         popup.target = self
1866 1963
         popup.action = #selector(languageChanged(_:))
1867
-        popup.isEnabled = !Self.supportedLanguages.isEmpty
1964
+        popup.isEnabled = !AppLanguage.allCases.isEmpty
1868 1965
         popup.setContentHuggingPriority(.required, for: .horizontal)
1869 1966
         popup.setContentCompressionResistancePriority(.required, for: .horizontal)
1870 1967
         return popup
@@ -1872,8 +1969,7 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
1872 1969
 
1873 1970
     @objc private func languageChanged(_ sender: NSPopUpButton) {
1874 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 1975
     private func makeSettingsSection(rows: [NSView]) -> NSView {
@@ -1912,7 +2008,7 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
1912 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 2012
         let row = NSView()
1917 2013
         row.translatesAutoresizingMaskIntoConstraints = false
1918 2014
         row.wantsLayer = true
@@ -1927,14 +2023,14 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
1927 2023
         let icon = NSImageView()
1928 2024
         icon.translatesAutoresizingMaskIntoConstraints = false
1929 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 2027
         icon.contentTintColor = Theme.brandBlue
1932 2028
 
1933
-        let titleLabel = NSTextField(labelWithString: title)
2029
+        let titleLabel = NSTextField(labelWithString: L(localizationKey))
1934 2030
         titleLabel.font = .systemFont(ofSize: 14, weight: .medium)
1935 2031
         titleLabel.textColor = Theme.primaryText
1936 2032
         titleLabel.alignment = .left
1937
-        titleLabel.identifier = NSUserInterfaceItemIdentifier(SettingsAppearanceID.rowTitle)
2033
+        titleLabel.identifier = NSUserInterfaceItemIdentifier("\(SettingsAppearanceID.rowTitle).\(localizationKey)")
1938 2034
 
1939 2035
         let rowStack = NSStackView()
1940 2036
         rowStack.orientation = .horizontal
@@ -1997,8 +2093,8 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
1997 2093
             $0.removeFromSuperview()
1998 2094
         }
1999 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 2098
             empty.font = .systemFont(ofSize: 14, weight: .regular)
2003 2099
             empty.textColor = Theme.secondaryText
2004 2100
             empty.alignment = .left
@@ -2008,7 +2104,9 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
2008 2104
             empty.widthAnchor.constraint(equalTo: savedJobsStack.widthAnchor).isActive = true
2009 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 2110
         for job in savedJobOrder {
2013 2111
             let card = makeJobListingCard(job, context: .savedJobsPage)
2014 2112
             savedJobsStack.addArrangedSubview(card)
@@ -2018,27 +2116,27 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
2018 2116
 
2019 2117
     private func isSavedJobsSidebarIndex(_ index: Int) -> Bool {
2020 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 2122
     private func isHomeSidebarIndex(_ index: Int) -> Bool {
2025 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 2127
     private func isSettingsSidebarIndex(_ index: Int) -> Bool {
2030 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 2132
     private func isCVMakerSidebarIndex(_ index: Int) -> Bool {
2035 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 2137
     private func isProfileSidebarIndex(_ index: Int) -> Bool {
2040 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 2142
     private func updateMainContentVisibility() {
@@ -2132,17 +2230,17 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
2132 2230
 
2133 2231
     @objc private func didTapFeatureRole() {
2134 2232
         selectFeatureShortcut(.role)
2135
-        focusSearchField(seed: "Find roles similar to: ")
2233
+        focusSearchField(seed: L("Find roles similar to: "))
2136 2234
     }
2137 2235
 
2138 2236
     @objc private func didTapFeatureCompany() {
2139 2237
         selectFeatureShortcut(.company)
2140
-        focusSearchField(seed: "Find jobs at company: ")
2238
+        focusSearchField(seed: L("Find jobs at company: "))
2141 2239
     }
2142 2240
 
2143 2241
     @objc private func didTapFeatureSkill() {
2144 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 2246
     private func selectFeatureShortcut(_ shortcut: FeatureShortcut) {
@@ -2196,7 +2294,7 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
2196 2294
 
2197 2295
     @objc private func didTapMoreApps() {
2198 2296
         guard let url = AppMarketingLinks.developerAppsURL else {
2199
-            presentAppMarketingConfigurationAlert(feature: "More Apps")
2297
+            presentAppMarketingConfigurationAlert(feature: L("More Apps"))
2200 2298
             return
2201 2299
         }
2202 2300
         NSWorkspace.shared.open(url)
@@ -2204,14 +2302,10 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
2204 2302
 
2205 2303
     private func presentAppMarketingConfigurationAlert(feature: String) {
2206 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 2307
         alert.alertStyle = .informational
2214
-        alert.addButton(withTitle: "OK")
2308
+        alert.addButton(withTitle: L("OK"))
2215 2309
         if let window {
2216 2310
             alert.beginSheetModal(for: window)
2217 2311
         } else {
@@ -2246,7 +2340,7 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
2246 2340
 
2247 2341
     @objc private func didTapLoadMoreJobs() {
2248 2342
         guard ensureProAccessForJobSearch() else { return }
2249
-        let prompt = "Show more jobs"
2343
+        let prompt = L("Show more jobs")
2250 2344
         guard !isAwaitingResponse, isContinuationPrompt(prompt) else { return }
2251 2345
         if anchorUserJobQuery(excludingLatestUserMessage: prompt) == nil { return }
2252 2346
         appendChatBubble(text: prompt, isUser: true)
@@ -2309,15 +2403,15 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
2309 2403
     private func makeAssistantSearchReply(query: String, newJobsCount: Int, isContinuation: Bool) -> String {
2310 2404
         if newJobsCount == 0 {
2311 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 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 2417
     private func resolvedSearchQuery(for prompt: String) -> String {
@@ -2347,6 +2441,10 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
2347 2441
 
2348 2442
     private func isContinuationPrompt(_ prompt: String) -> Bool {
2349 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 2448
         let continuationPhrases: Set<String> = [
2351 2449
             "more",
2352 2450
             "show more",
@@ -2423,7 +2521,7 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
2423 2521
         chatMessages.removeAll()
2424 2522
         lastSearchResults.removeAll()
2425 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 2525
         chatMessages.append(ChatMessage(role: "assistant", content: welcome))
2428 2526
         appendChatBubble(text: welcome, isUser: false)
2429 2527
     }
@@ -2620,7 +2718,7 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
2620 2718
         row.translatesAutoresizingMaskIntoConstraints = false
2621 2719
         let button = HoverableButton()
2622 2720
         button.pointerCursor = true
2623
-        button.title = "Show more jobs"
2721
+        button.title = L("Show more jobs")
2624 2722
         button.font = .systemFont(ofSize: 12, weight: .semibold)
2625 2723
         button.bezelStyle = .rounded
2626 2724
         button.controlSize = .regular
@@ -2730,10 +2828,10 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
2730 2828
         rowHost.layer?.cornerRadius = 8
2731 2829
         rowHost.restingBackgroundColor = isSelected ? Theme.selectionFill : nil
2732 2830
         rowHost.hoverBackgroundColor = isSelected ? Theme.selectionFillHover : Theme.sidebarRowHoverFill
2733
-        rowHost.setAccessibilityLabel("Indeed")
2831
+        rowHost.setAccessibilityLabel(L("Indeed"))
2734 2832
         rowHost.setAccessibilityRole(.button)
2735 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 2836
         let row = NSStackView()
2739 2837
         row.orientation = .horizontal
@@ -2749,7 +2847,7 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
2749 2847
         icon.widthAnchor.constraint(equalToConstant: Self.sidebarNavIconSize).isActive = true
2750 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 2851
         text.font = .systemFont(ofSize: 14, weight: .medium)
2754 2852
         text.textColor = isSelected ? Theme.brandBlue : Theme.secondaryText
2755 2853
         text.refusesFirstResponder = true
@@ -2859,7 +2957,7 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
2859 2957
             let sidebarHorizontalInset = sidebar.edgeInsets.left + sidebar.edgeInsets.right
2860 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 2961
                 addIndeedSidebarLaunchRow()
2864 2962
             }
2865 2963
         }
@@ -2896,7 +2994,7 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
2896 2994
         proIcon.image = NSImage(systemSymbolName: "sparkles", accessibilityDescription: nil)
2897 2995
         proIcon.contentTintColor = Theme.proAccent
2898 2996
 
2899
-        let proEyebrow = NSTextField(labelWithString: "Premium")
2997
+        let proEyebrow = NSTextField(labelWithString: L("Premium"))
2900 2998
         proEyebrow.font = .systemFont(ofSize: 11, weight: .heavy)
2901 2999
         proEyebrow.textColor = Theme.proAccent
2902 3000
         proEyebrow.alignment = .center
@@ -2906,12 +3004,12 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
2906 3004
         eyebrowRow.spacing = 6
2907 3005
         eyebrowRow.alignment = .centerY
2908 3006
 
2909
-        let headline = NSTextField(labelWithString: "Upgrade to Pro")
3007
+        let headline = NSTextField(labelWithString: L("Upgrade to Pro"))
2910 3008
         headline.font = .systemFont(ofSize: 16, weight: .bold)
2911 3009
         headline.textColor = Theme.primaryText
2912 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 3013
         upgradeDescription.font = .systemFont(ofSize: 12, weight: .regular)
2916 3014
         upgradeDescription.textColor = Theme.secondaryText
2917 3015
         upgradeDescription.alignment = .center
@@ -2920,7 +3018,7 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
2920 3018
         let innerContentWidth = cardWidth - 28
2921 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 3022
         upgradeButton.isBordered = false
2925 3023
         upgradeButton.bezelStyle = .rounded
2926 3024
         upgradeButton.font = .systemFont(ofSize: 13, weight: .bold)
@@ -3051,7 +3149,7 @@ private final class OpenAIJobSearchService {
3051 3149
             completion(.failure(NSError(
3052 3150
                 domain: "OpenAIJobSearchService",
3053 3151
                 code: 1,
3054
-                userInfo: [NSLocalizedDescriptionKey: "Job search is unavailable."]
3152
+                userInfo: [NSLocalizedDescriptionKey: L("Job search is unavailable.")]
3055 3153
             )))
3056 3154
             return
3057 3155
         }

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

@@ -29,14 +29,14 @@ final class LoadingView: NSView {
29 29
     private let iconWell = NSView()
30 30
     private let logoView = IndeedLogoView(displayHeight: 40, variant: .compact)
31 31
     private let aiBadgeHost = NSView()
32
-    private let aiBadgeLabel = NSTextField(labelWithString: "AI-POWERED")
32
+    private let aiBadgeLabel = NSTextField(labelWithString: L("AI-POWERED"))
33 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 36
     private let progressBar = LoadingProgressBarView()
37 37
     private let thinkingIndicator = ChatThinkingIndicatorView(
38 38
         compact: false,
39
-        accessibilityLabel: "Loading \(AppMarketingLinks.displayName)"
39
+        accessibilityLabel: String(format: L("Loading %@"), AppMarketingLinks.displayName)
40 40
     )
41 41
 
42 42
     override init(frame frameRect: NSRect) {
@@ -87,7 +87,7 @@ final class LoadingView: NSView {
87 87
     func setStatus(_ message: String, progress: CGFloat) {
88 88
         statusLabel.stringValue = message
89 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 93
     func startAnimating() {
@@ -236,7 +236,7 @@ final class LoadingView: NSView {
236 236
         installPageGradient()
237 237
         setAccessibilityElement(true)
238 238
         setAccessibilityRole(.group)
239
-        setAccessibilityLabel("Loading \(AppMarketingLinks.displayName)")
239
+        setAccessibilityLabel(String(format: L("Loading %@"), AppMarketingLinks.displayName))
240 240
     }
241 241
 
242 242
     private func installPageGradient() {
@@ -449,7 +449,7 @@ private final class LoadingProgressBarView: NSView {
449 449
 
450 450
         setAccessibilityElement(true)
451 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 196
     private let interestsField = NSTextField()
197 197
     private let languagesField = NSTextField()
198 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 201
     private var nameEmailRow: ProfileDualFieldRow!
202 202
     private var phoneJobRow: ProfileDualFieldRow!
203 203
 
204 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 206
     private let contextLabel = NSTextField(labelWithString: "")
207 207
     private var editingProfileID: UUID?
208 208
 
@@ -218,6 +218,7 @@ final class MyProfilePageView: NSView {
218 218
 
219 219
     private var referralHelperLabel: NSTextField?
220 220
     private var appearanceObserver: NSObjectProtocol?
221
+    private var languageObserver: NSObjectProtocol?
221 222
 
222 223
     /// Force left-to-right geometry so profile fields span the full width even when the window uses RTL layout.
223 224
     override var userInterfaceLayoutDirection: NSUserInterfaceLayoutDirection {
@@ -235,13 +236,24 @@ final class MyProfilePageView: NSView {
235 236
         ) { [weak self] _ in
236 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 246
         applyCurrentAppearance()
247
+        applyLocalizedStrings()
239 248
     }
240 249
 
241 250
     deinit {
242 251
         if let appearanceObserver {
243 252
             NotificationCenter.default.removeObserver(appearanceObserver)
244 253
         }
254
+        if let languageObserver {
255
+            NotificationCenter.default.removeObserver(languageObserver)
256
+        }
245 257
     }
246 258
 
247 259
     required init?(coder: NSCoder) {
@@ -266,6 +278,15 @@ final class MyProfilePageView: NSView {
266 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 290
     override func viewDidMoveToWindow() {
270 291
         super.viewDidMoveToWindow()
271 292
         guard window != nil else { return }
@@ -381,7 +402,7 @@ final class MyProfilePageView: NSView {
381 402
         contextLabel.translatesAutoresizingMaskIntoConstraints = false
382 403
         contextLabel.font = .systemFont(ofSize: 15, weight: .semibold)
383 404
         contextLabel.textColor = ProfilePagePalette.primaryText
384
-        contextLabel.stringValue = "New profile"
405
+        contextLabel.stringValue = L("New profile")
385 406
         contextLabel.backgroundColor = .clear
386 407
         contextLabel.isBordered = false
387 408
         contextLabel.isEditable = false
@@ -433,22 +454,22 @@ final class MyProfilePageView: NSView {
433 454
         ])
434 455
 
435 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 463
         nameEmailRow = ProfileDualFieldRow(left: nameGroup, right: emailGroup, spacing: 12)
443 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 468
         phoneJobRow = ProfileDualFieldRow(left: phoneGroup, right: jobGroup, spacing: 12)
448 469
         addFullWidthArrangedSubview(phoneJobRow)
449 470
 
450 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 474
         addFullWidthArrangedSubview(careerSummaryBlock())
454 475
         addFullWidthArrangedSubview(horizontalSeparator())
@@ -458,8 +479,8 @@ final class MyProfilePageView: NSView {
458 479
         addFullWidthArrangedSubview(horizontalSeparator())
459 480
         addFullWidthArrangedSubview(
460 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 484
                 field: certificatesField,
464 485
                 minHeight: 100
465 486
             )
@@ -467,8 +488,8 @@ final class MyProfilePageView: NSView {
467 488
         addFullWidthArrangedSubview(horizontalSeparator())
468 489
         addFullWidthArrangedSubview(
469 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 493
                 field: interestsField,
473 494
                 minHeight: 100
474 495
             )
@@ -476,8 +497,8 @@ final class MyProfilePageView: NSView {
476 497
         addFullWidthArrangedSubview(horizontalSeparator())
477 498
         addFullWidthArrangedSubview(
478 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 502
                 field: languagesField,
482 503
                 minHeight: 100
483 504
             )
@@ -497,7 +518,7 @@ final class MyProfilePageView: NSView {
497 518
 
498 519
     func prepareNewProfile() {
499 520
         editingProfileID = nil
500
-        contextLabel.stringValue = "New profile"
521
+        contextLabel.stringValue = L("New profile")
501 522
         applyForm(
502 523
             from: SavedProfile(
503 524
                 id: UUID(),
@@ -516,7 +537,7 @@ final class MyProfilePageView: NSView {
516 537
 
517 538
     func loadSavedProfile(_ profile: SavedProfile) {
518 539
         editingProfileID = profile.id
519
-        contextLabel.stringValue = "Edit profile"
540
+        contextLabel.stringValue = L("Edit profile")
520 541
         applyForm(from: profile)
521 542
     }
522 543
 
@@ -741,7 +762,7 @@ final class MyProfilePageView: NSView {
741 762
     }
742 763
 
743 764
     private func careerSummaryBlock() -> NSView {
744
-        let label = NSTextField(labelWithString: "Career Summary")
765
+        let label = NSTextField(labelWithString: L("Career Summary"))
745 766
         label.font = .systemFont(ofSize: 12, weight: .medium)
746 767
         label.textColor = ProfilePagePalette.secondaryText
747 768
         label.translatesAutoresizingMaskIntoConstraints = false
@@ -763,7 +784,7 @@ final class MyProfilePageView: NSView {
763 784
         careerField.setContentHuggingPriority(.defaultLow, for: .horizontal)
764 785
         careerField.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
765 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 788
             attributes: [
768 789
                 .foregroundColor: ProfilePagePalette.secondaryText,
769 790
                 .font: NSFont.systemFont(ofSize: 14, weight: .regular),
@@ -879,16 +900,16 @@ final class MyProfilePageView: NSView {
879 900
     }
880 901
 
881 902
     private func referralBlock() -> NSView {
882
-        let label = NSTextField(labelWithString: "Referral (Optional)")
903
+        let label = NSTextField(labelWithString: L("Referral (Optional)"))
883 904
         label.font = .systemFont(ofSize: 12, weight: .medium)
884 905
         label.textColor = ProfilePagePalette.secondaryText
885 906
         label.translatesAutoresizingMaskIntoConstraints = false
886 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 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 913
         helper.font = .systemFont(ofSize: 11, weight: .regular)
893 914
         helper.textColor = ProfilePagePalette.secondaryText
894 915
         helper.translatesAutoresizingMaskIntoConstraints = false
@@ -925,14 +946,14 @@ final class MyProfilePageView: NSView {
925 946
     }
926 947
 
927 948
     private func workExperienceSection() -> NSView {
928
-        let title = NSTextField(labelWithString: "Work Experience")
949
+        let title = NSTextField(labelWithString: L("Work Experience"))
929 950
         title.font = .systemFont(ofSize: 15, weight: .semibold)
930 951
         title.textColor = ProfilePagePalette.primaryText
931 952
         title.translatesAutoresizingMaskIntoConstraints = false
932 953
         title.setContentHuggingPriority(.defaultLow, for: .horizontal)
933 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 957
         addButton.translatesAutoresizingMaskIntoConstraints = false
937 958
         addButton.bezelStyle = .rounded
938 959
         addButton.isBordered = true
@@ -972,14 +993,14 @@ final class MyProfilePageView: NSView {
972 993
     }
973 994
 
974 995
     private func educationSection() -> NSView {
975
-        let title = NSTextField(labelWithString: "Education")
996
+        let title = NSTextField(labelWithString: L("Education"))
976 997
         title.font = .systemFont(ofSize: 15, weight: .semibold)
977 998
         title.textColor = ProfilePagePalette.primaryText
978 999
         title.translatesAutoresizingMaskIntoConstraints = false
979 1000
         title.setContentHuggingPriority(.defaultLow, for: .horizontal)
980 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 1004
         addButton.translatesAutoresizingMaskIntoConstraints = false
984 1005
         addButton.bezelStyle = .rounded
985 1006
         addButton.isBordered = true
@@ -1128,16 +1149,16 @@ final class MyProfilePageView: NSView {
1128 1149
         window?.makeFirstResponder(nil)
1129 1150
         let profile = captureSavedProfileForSave()
1130 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 1156
         guard missing.isEmpty else {
1136 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 1160
             alert.alertStyle = .informational
1140
-            alert.addButton(withTitle: "OK")
1161
+            alert.addButton(withTitle: L("OK"))
1141 1162
             if let window = window {
1142 1163
                 alert.beginSheetModal(for: window) { _ in }
1143 1164
             } else {
@@ -1161,7 +1182,7 @@ private enum ProfileEntryCardLayout {
1161 1182
 private final class WorkExperienceEntryView: NSView {
1162 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 1186
     private let deleteButton = NSButton()
1166 1187
     private let jobTitleField = NSTextField()
1167 1188
     private let companyField = NSTextField()
@@ -1179,7 +1200,7 @@ private final class WorkExperienceEntryView: NSView {
1179 1200
     }
1180 1201
 
1181 1202
     func setExperienceIndex(_ index: Int) {
1182
-        subtitleLabel.stringValue = "Experience \(index)"
1203
+        subtitleLabel.stringValue = String(format: L("Experience %d"), index)
1183 1204
     }
1184 1205
 
1185 1206
     func setDeleteHidden(_ hidden: Bool) {
@@ -1236,10 +1257,10 @@ private final class WorkExperienceEntryView: NSView {
1236 1257
         deleteButton.target = self
1237 1258
         deleteButton.action = #selector(didTapDelete)
1238 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 1261
             deleteButton.imagePosition = .imageOnly
1241 1262
         } else {
1242
-            deleteButton.title = "Remove"
1263
+            deleteButton.title = L("Remove")
1243 1264
             deleteButton.font = .systemFont(ofSize: 12, weight: .medium)
1244 1265
         }
1245 1266
 
@@ -1255,15 +1276,15 @@ private final class WorkExperienceEntryView: NSView {
1255 1276
         ProfileLayoutEnforcement.applyForcedLTR(to: headerRow)
1256 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 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 1284
         let descriptionGroup = Self.multilineLabeledStack(
1264
-            title: "Description",
1285
+            title: L("Description"),
1265 1286
             field: descriptionField,
1266
-            placeholder: "Describe your responsibilities and achievements...",
1287
+            placeholder: L("Describe your responsibilities and achievements..."),
1267 1288
             minHeight: 120
1268 1289
         )
1269 1290
 
@@ -1454,7 +1475,7 @@ private final class WorkExperienceEntryView: NSView {
1454 1475
 private final class EducationEntryView: NSView {
1455 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 1479
     private let deleteButton = NSButton()
1459 1480
     private let degreeField = NSTextField()
1460 1481
     private let institutionField = NSTextField()
@@ -1471,7 +1492,7 @@ private final class EducationEntryView: NSView {
1471 1492
     }
1472 1493
 
1473 1494
     func setEducationIndex(_ index: Int) {
1474
-        subtitleLabel.stringValue = "Education \(index)"
1495
+        subtitleLabel.stringValue = String(format: L("Education %d"), index)
1475 1496
     }
1476 1497
 
1477 1498
     func setDeleteHidden(_ hidden: Bool) {
@@ -1526,10 +1547,10 @@ private final class EducationEntryView: NSView {
1526 1547
         deleteButton.target = self
1527 1548
         deleteButton.action = #selector(didTapDelete)
1528 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 1551
             deleteButton.imagePosition = .imageOnly
1531 1552
         } else {
1532
-            deleteButton.title = "Remove"
1553
+            deleteButton.title = L("Remove")
1533 1554
             deleteButton.font = .systemFont(ofSize: 12, weight: .medium)
1534 1555
         }
1535 1556
 
@@ -1546,21 +1567,21 @@ private final class EducationEntryView: NSView {
1546 1567
         headerRow.translatesAutoresizingMaskIntoConstraints = false
1547 1568
 
1548 1569
         let degreeGroup = WorkExperienceEntryView.labeledFieldStack(
1549
-            title: "Degree / program *",
1570
+            title: L("Degree / program *"),
1550 1571
             field: degreeField,
1551
-            placeholder: "e.g., BSc Computer Science"
1572
+            placeholder: L("e.g., BSc Computer Science")
1552 1573
         )
1553 1574
         let institutionGroup = WorkExperienceEntryView.labeledFieldStack(
1554
-            title: "Institution *",
1575
+            title: L("Institution *"),
1555 1576
             field: institutionField,
1556
-            placeholder: "e.g., MIT"
1577
+            placeholder: L("e.g., MIT")
1557 1578
         )
1558 1579
         degreeInstitutionRow = ProfileDualFieldRow(left: degreeGroup, right: institutionGroup, spacing: 12)
1559 1580
 
1560 1581
         let yearGroup = WorkExperienceEntryView.labeledFieldStack(
1561
-            title: "Year *",
1582
+            title: L("Year *"),
1562 1583
             field: yearField,
1563
-            placeholder: "e.g., 2020"
1584
+            placeholder: L("e.g., 2020")
1564 1585
         )
1565 1586
 
1566 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 33
     private let scrollView = NSScrollView()
34 34
     private let documentView = ProfilesListDocumentView()
35 35
     private let contentStack = NSStackView()
36
-    private let titleLabel = NSTextField(labelWithString: "Profiles")
36
+    private let titleLabel = NSTextField(labelWithString: L("Profiles"))
37 37
     private let subtitleLabel = NSTextField(wrappingLabelWithString: "")
38 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 40
     private let pendingFlowLabel = NSTextField(wrappingLabelWithString: "")
41 41
     private var appearanceObserver: NSObjectProtocol?
42
+    private var languageObserver: NSObjectProtocol?
42 43
     /// Non-`nil` after **Use Template & Select Profile** until the dashboard clears the flow (e.g. leaving Profile).
43 44
     private var pendingCVTemplateDisplayName: String?
44 45
 
@@ -57,13 +58,24 @@ final class ProfilesListPageView: NSView {
57 58
         ) { [weak self] _ in
58 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 68
         applyCurrentAppearance()
69
+        applyLocalizedStrings()
61 70
     }
62 71
 
63 72
     deinit {
64 73
         if let appearanceObserver {
65 74
             NotificationCenter.default.removeObserver(appearanceObserver)
66 75
         }
76
+        if let languageObserver {
77
+            NotificationCenter.default.removeObserver(languageObserver)
78
+        }
67 79
     }
68 80
 
69 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 117
     func reloadFromStore() {
92 118
         for row in contentStack.arrangedSubviews {
93 119
             contentStack.removeArrangedSubview(row)
@@ -113,7 +139,10 @@ final class ProfilesListPageView: NSView {
113 139
     func setPendingCVTemplateDisplayName(_ name: String?) {
114 140
         pendingCVTemplateDisplayName = name
115 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 146
             pendingFlowLabel.isHidden = false
118 147
         } else {
119 148
             pendingFlowLabel.isHidden = true
@@ -128,11 +157,11 @@ final class ProfilesListPageView: NSView {
128 157
 
129 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 161
         subtitleLabel.font = .systemFont(ofSize: 13, weight: .regular)
133 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 165
         emptyStateLabel.font = .systemFont(ofSize: 13, weight: .regular)
137 166
         emptyStateLabel.textColor = ProfilesListPalette.secondaryText
138 167
         emptyStateLabel.isHidden = true
@@ -237,9 +266,9 @@ private final class ProfileListRowView: NSView {
237 266
     private let profileID: UUID
238 267
     private let nameLabel = NSTextField(labelWithString: "")
239 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 273
     init(profile: SavedProfile, showBuildCV: Bool) {
245 274
         self.profileID = profile.id
@@ -254,11 +283,11 @@ private final class ProfileListRowView: NSView {
254 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 287
         nameLabel.font = .systemFont(ofSize: 15, weight: .semibold)
259 288
 
260 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 291
         detailLabel.font = .systemFont(ofSize: 12, weight: .regular)
263 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)";