Procházet zdrojové kódy

Add French localization and refresh profile form labels on language change.

Users can select French in Settings; profile editor field labels and placeholders now update when the locale changes instead of staying in the previously selected language.

Co-authored-by: Cursor <cursoragent@cursor.com>
AhtashamShahzad1 před 4 dny
rodič
revize
70e23561b7

+ 1 - 0
App for Indeed.xcodeproj/project.pbxproj

@@ -104,6 +104,7 @@
104 104
 				ar,
105 105
 				"zh-Hans",
106 106
 				"zh-Hant",
107
+				fr,
107 108
 			);
108 109
 			mainGroup = 27D852772FB1D367008DF557;
109 110
 			minimizedProjectReferenceProxies = 1;

+ 7 - 2
App for Indeed/Services/AppLocalization.swift

@@ -10,6 +10,7 @@ import Foundation
10 10
 
11 11
 enum AppLanguage: CaseIterable {
12 12
     case english
13
+    case french
13 14
     case arabic
14 15
     case chineseSimplified
15 16
     case chineseTraditional
@@ -18,6 +19,8 @@ enum AppLanguage: CaseIterable {
18 19
         switch self {
19 20
         case .english:
20 21
             return "en"
22
+        case .french:
23
+            return "fr"
21 24
         case .arabic:
22 25
             return "ar"
23 26
         case .chineseSimplified:
@@ -51,6 +54,8 @@ enum AppLanguage: CaseIterable {
51 54
         switch self {
52 55
         case .english:
53 56
             return "English"
57
+        case .french:
58
+            return "French"
54 59
         case .arabic:
55 60
             return "Arabic"
56 61
         case .chineseSimplified:
@@ -106,7 +111,7 @@ private func translateTemplateNameByTokens(_ name: String, language: AppLanguage
106 111
     switch language {
107 112
     case .chineseSimplified, .chineseTraditional:
108 113
         return translated.joined()
109
-    case .arabic, .english:
114
+    case .arabic, .english, .french:
110 115
         return translated.joined(separator: " ")
111 116
     }
112 117
 }
@@ -123,7 +128,7 @@ private func templateNameTokenTranslation(_ token: String, language: AppLanguage
123 128
         return TemplateNameTokenLexicon.zhHans[key] ?? TemplateNameTokenLexicon.zhHans[key.capitalized]
124 129
     case .arabic:
125 130
         return TemplateNameTokenLexicon.ar[key] ?? TemplateNameTokenLexicon.ar[key.capitalized]
126
-    case .english:
131
+    case .french, .english:
127 132
         return nil
128 133
     }
129 134
 }

+ 2 - 0
App for Indeed/Services/CVTemplateFetchService.swift

@@ -50,6 +50,8 @@ final class CVTemplateFetchService {
50 50
                 return "Each `name` must be 1–3 English words (ASCII letters and spaces only) suitable as localization keys, e.g. \"Design Dynamo\" — do not use Chinese characters in `name`."
51 51
             case .arabic:
52 52
                 return "Each `name` must be 1–3 English words (ASCII letters and spaces only) suitable as localization keys, e.g. \"UI Sculptor\" — do not use Arabic script in `name`."
53
+            case .french:
54
+                return "Each `name` must be 1–3 English words (ASCII letters and spaces only) suitable as localization keys, e.g. \"Creative Cascade\" — do not use French characters in `name`."
53 55
             }
54 56
         }
55 57
     }

+ 143 - 79
App for Indeed/Views/MyProfilePageView.swift

@@ -110,6 +110,64 @@ private extension NSStackView {
110 110
     }
111 111
 }
112 112
 
113
+/// Tags profile form controls with English localization keys so labels and placeholders refresh when the user changes language.
114
+private enum ProfileFormLocalization {
115
+    static let labelPrefix = "profileForm.label."
116
+    static let sectionPrefix = "profileForm.section."
117
+    static let placeholderPrefix = "profileForm.placeholder."
118
+    static let buttonPrefix = "profileForm.button."
119
+
120
+    static func tagLabel(_ label: NSTextField, key: String) {
121
+        label.identifier = NSUserInterfaceItemIdentifier(labelPrefix + key)
122
+    }
123
+
124
+    static func tagSection(_ label: NSTextField, key: String) {
125
+        label.identifier = NSUserInterfaceItemIdentifier(sectionPrefix + key)
126
+    }
127
+
128
+    static func tagPlaceholder(_ field: NSTextField, key: String) {
129
+        field.identifier = NSUserInterfaceItemIdentifier(placeholderPrefix + key)
130
+    }
131
+
132
+    static func tagButton(_ button: NSButton, key: String) {
133
+        button.identifier = NSUserInterfaceItemIdentifier(buttonPrefix + key)
134
+    }
135
+
136
+    static func placeholderAttributes() -> [NSAttributedString.Key: Any] {
137
+        [
138
+            .foregroundColor: ProfilePagePalette.secondaryText,
139
+            .font: NSFont.systemFont(ofSize: 14, weight: .regular),
140
+            .paragraphStyle: ProfileLayoutEnforcement.leftAlignedParagraphStyle()
141
+        ]
142
+    }
143
+
144
+    static func applyPlaceholder(_ text: String, to field: NSTextField) {
145
+        field.placeholderAttributedString = NSAttributedString(string: text, attributes: placeholderAttributes())
146
+    }
147
+
148
+    static func refresh(in root: NSView) {
149
+        for view in root.profileSubviewsRecursive() {
150
+            guard let rawID = view.identifier?.rawValue else { continue }
151
+            if let label = view as? NSTextField, !label.isEditable {
152
+                if rawID.hasPrefix(labelPrefix) {
153
+                    label.stringValue = L(String(rawID.dropFirst(labelPrefix.count)))
154
+                } else if rawID.hasPrefix(sectionPrefix) {
155
+                    label.stringValue = L(String(rawID.dropFirst(sectionPrefix.count)))
156
+                }
157
+            } else if let field = view as? NSTextField, field.isEditable, rawID.hasPrefix(placeholderPrefix) {
158
+                applyPlaceholder(L(String(rawID.dropFirst(placeholderPrefix.count))), to: field)
159
+            } else if let button = view as? NSButton, rawID.hasPrefix(buttonPrefix) {
160
+                let key = String(rawID.dropFirst(buttonPrefix.count))
161
+                if button.image != nil {
162
+                    button.image = NSImage(systemSymbolName: "trash", accessibilityDescription: L(key))
163
+                } else {
164
+                    button.title = L(key)
165
+                }
166
+            }
167
+        }
168
+    }
169
+}
170
+
113 171
 /// Two fields side‑by‑side with a true 50/50 split, or stacked full‑width when compact. Avoids `NSStackView` collapsing paired columns to a narrow strip on the trailing edge.
114 172
 private final class ProfileDualFieldRow: NSView {
115 173
     private let leftView: NSView
@@ -282,6 +340,8 @@ final class MyProfilePageView: NSView {
282 340
         backButton.title = L("← All profiles")
283 341
         saveButton.title = L("Save Profile  →")
284 342
         contextLabel.stringValue = editingProfileID == nil ? L("New profile") : L("Edit profile")
343
+        ProfileFormLocalization.refresh(in: formStack)
344
+        referralHelperLabel?.stringValue = L("If someone referred you for this job, enter their name or company here")
285 345
         renumberWorkExperienceEntries()
286 346
         renumberEducationEntries()
287 347
         ProfileThemeAppearance.refreshFormSubtree(formStack)
@@ -454,22 +514,22 @@ final class MyProfilePageView: NSView {
454 514
         ])
455 515
 
456 516
         addFullWidthArrangedSubview(
457
-            labeledGroup(title: L("Profile Name *"), field: profileNameField, placeholder: L("Marketing Director Profile"))
517
+            labeledGroup(labelKey: "Profile Name *", field: profileNameField, placeholderKey: "Marketing Director Profile")
458 518
         )
459
-        addFullWidthArrangedSubview(sectionHeading(L("Personal Information")))
519
+        addFullWidthArrangedSubview(sectionHeading("Personal Information"))
460 520
 
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"))
521
+        let nameGroup = labeledGroup(labelKey: "Full Name *", field: fullNameField, placeholderKey: "John Doe")
522
+        let emailGroup = labeledGroup(labelKey: "Email *", field: emailField, placeholderKey: "john@example.com")
463 523
         nameEmailRow = ProfileDualFieldRow(left: nameGroup, right: emailGroup, spacing: 12)
464 524
         addFullWidthArrangedSubview(nameEmailRow)
465 525
 
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"))
526
+        let phoneGroup = labeledGroup(labelKey: "Phone", field: phoneField, placeholderKey: "+1 (555) 123-4567")
527
+        let jobGroup = labeledGroup(labelKey: "Job Title *", field: jobTitleField, placeholderKey: "Software Engineer")
468 528
         phoneJobRow = ProfileDualFieldRow(left: phoneGroup, right: jobGroup, spacing: 12)
469 529
         addFullWidthArrangedSubview(phoneJobRow)
470 530
 
471 531
         addFullWidthArrangedSubview(
472
-            labeledGroup(title: L("Address"), field: addressField, placeholder: L("123 Main St, City, State, ZIP"))
532
+            labeledGroup(labelKey: "Address", field: addressField, placeholderKey: "123 Main St, City, State, ZIP")
473 533
         )
474 534
         addFullWidthArrangedSubview(careerSummaryBlock())
475 535
         addFullWidthArrangedSubview(horizontalSeparator())
@@ -479,8 +539,8 @@ final class MyProfilePageView: NSView {
479 539
         addFullWidthArrangedSubview(horizontalSeparator())
480 540
         addFullWidthArrangedSubview(
481 541
             multilineProfileBlock(
482
-                title: L("Certificates / Rewards"),
483
-                placeholder: L("List your certificates and awards..."),
542
+                labelKey: "Certificates / Rewards",
543
+                placeholderKey: "List your certificates and awards...",
484 544
                 field: certificatesField,
485 545
                 minHeight: 100
486 546
             )
@@ -488,8 +548,8 @@ final class MyProfilePageView: NSView {
488 548
         addFullWidthArrangedSubview(horizontalSeparator())
489 549
         addFullWidthArrangedSubview(
490 550
             multilineProfileBlock(
491
-                title: L("Interests"),
492
-                placeholder: L("List your interests and hobbies..."),
551
+                labelKey: "Interests",
552
+                placeholderKey: "List your interests and hobbies...",
493 553
                 field: interestsField,
494 554
                 minHeight: 100
495 555
             )
@@ -497,8 +557,8 @@ final class MyProfilePageView: NSView {
497 557
         addFullWidthArrangedSubview(horizontalSeparator())
498 558
         addFullWidthArrangedSubview(
499 559
             multilineProfileBlock(
500
-                title: L("Languages"),
501
-                placeholder: L("List languages you speak (e.g., English - Native, Spanish - Fluent)..."),
560
+                labelKey: "Languages",
561
+                placeholderKey: "List languages you speak (e.g., English - Native, Spanish - Fluent)...",
502 562
                 field: languagesField,
503 563
                 minHeight: 100
504 564
             )
@@ -518,6 +578,7 @@ final class MyProfilePageView: NSView {
518 578
 
519 579
     func prepareNewProfile() {
520 580
         editingProfileID = nil
581
+        applyLocalizedStrings()
521 582
         contextLabel.stringValue = L("New profile")
522 583
         applyForm(
523 584
             from: SavedProfile(
@@ -537,6 +598,7 @@ final class MyProfilePageView: NSView {
537 598
 
538 599
     func loadSavedProfile(_ profile: SavedProfile) {
539 600
         editingProfileID = profile.id
601
+        applyLocalizedStrings()
540 602
         contextLabel.stringValue = L("Edit profile")
541 603
         applyForm(from: profile)
542 604
     }
@@ -658,8 +720,9 @@ final class MyProfilePageView: NSView {
658 720
         view.widthAnchor.constraint(equalTo: formStack.widthAnchor).isActive = true
659 721
     }
660 722
 
661
-    private func sectionHeading(_ text: String) -> NSView {
662
-        let label = NSTextField(labelWithString: text)
723
+    private func sectionHeading(_ key: String) -> NSView {
724
+        let label = NSTextField(labelWithString: L(key))
725
+        ProfileFormLocalization.tagSection(label, key: key)
663 726
         label.font = .systemFont(ofSize: 15, weight: .semibold)
664 727
         label.textColor = ProfilePagePalette.primaryText
665 728
         label.baseWritingDirection = .leftToRight
@@ -685,15 +748,16 @@ final class MyProfilePageView: NSView {
685 748
         return row
686 749
     }
687 750
 
688
-    private func labeledGroup(title: String, field: NSTextField, placeholder: String) -> NSView {
689
-        let label = NSTextField(labelWithString: title)
751
+    private func labeledGroup(labelKey: String, field: NSTextField, placeholderKey: String) -> NSView {
752
+        let label = NSTextField(labelWithString: L(labelKey))
753
+        ProfileFormLocalization.tagLabel(label, key: labelKey)
690 754
         label.font = .systemFont(ofSize: 12, weight: .medium)
691 755
         label.textColor = ProfilePagePalette.secondaryText
692 756
         label.translatesAutoresizingMaskIntoConstraints = false
693 757
         label.setContentHuggingPriority(.defaultLow, for: .horizontal)
694 758
         ProfileLayoutEnforcement.applyLeftAlignedTextField(label)
695 759
 
696
-        styleSingleLineField(field, placeholder: placeholder)
760
+        styleSingleLineField(field, placeholderKey: placeholderKey)
697 761
         let wrap = roundedFieldChrome(containing: field, minHeight: 40)
698 762
 
699 763
         let stack = NSStackView(views: [label, wrap])
@@ -715,7 +779,8 @@ final class MyProfilePageView: NSView {
715 779
         return stack
716 780
     }
717 781
 
718
-    private func styleSingleLineField(_ field: NSTextField, placeholder: String) {
782
+    private func styleSingleLineField(_ field: NSTextField, placeholderKey: String) {
783
+        ProfileFormLocalization.tagPlaceholder(field, key: placeholderKey)
719 784
         field.translatesAutoresizingMaskIntoConstraints = false
720 785
         field.isBordered = false
721 786
         field.drawsBackground = false
@@ -724,15 +789,7 @@ final class MyProfilePageView: NSView {
724 789
         field.textColor = ProfilePagePalette.primaryText
725 790
         field.setContentHuggingPriority(.defaultLow, for: .horizontal)
726 791
         field.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
727
-        let paragraph = ProfileLayoutEnforcement.leftAlignedParagraphStyle()
728
-        field.placeholderAttributedString = NSAttributedString(
729
-            string: placeholder,
730
-            attributes: [
731
-                .foregroundColor: ProfilePagePalette.secondaryText,
732
-                .font: NSFont.systemFont(ofSize: 14, weight: .regular),
733
-                .paragraphStyle: paragraph
734
-            ]
735
-        )
792
+        ProfileFormLocalization.applyPlaceholder(L(placeholderKey), to: field)
736 793
         field.cell?.usesSingleLineMode = true
737 794
         field.cell?.wraps = false
738 795
         field.cell?.isScrollable = true
@@ -763,6 +820,7 @@ final class MyProfilePageView: NSView {
763 820
 
764 821
     private func careerSummaryBlock() -> NSView {
765 822
         let label = NSTextField(labelWithString: L("Career Summary"))
823
+        ProfileFormLocalization.tagLabel(label, key: "Career Summary")
766 824
         label.font = .systemFont(ofSize: 12, weight: .medium)
767 825
         label.textColor = ProfilePagePalette.secondaryText
768 826
         label.translatesAutoresizingMaskIntoConstraints = false
@@ -783,13 +841,13 @@ final class MyProfilePageView: NSView {
783 841
         careerField.stringValue = ""
784 842
         careerField.setContentHuggingPriority(.defaultLow, for: .horizontal)
785 843
         careerField.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
786
-        careerField.placeholderAttributedString = NSAttributedString(
787
-            string: L("Brief overview of your professional background and key achievements..."),
788
-            attributes: [
789
-                .foregroundColor: ProfilePagePalette.secondaryText,
790
-                .font: NSFont.systemFont(ofSize: 14, weight: .regular),
791
-                .paragraphStyle: ProfileLayoutEnforcement.leftAlignedParagraphStyle()
792
-            ]
844
+        ProfileFormLocalization.tagPlaceholder(
845
+            careerField,
846
+            key: "Brief overview of your professional background and key achievements..."
847
+        )
848
+        ProfileFormLocalization.applyPlaceholder(
849
+            L("Brief overview of your professional background and key achievements..."),
850
+            to: careerField
793 851
         )
794 852
         ProfileLayoutEnforcement.applyLeftAlignedTextField(careerField)
795 853
 
@@ -830,13 +888,15 @@ final class MyProfilePageView: NSView {
830 888
         return stack
831 889
     }
832 890
 
833
-    private func multilineProfileBlock(title: String, placeholder: String, field: NSTextField, minHeight: CGFloat) -> NSView {
834
-        let label = NSTextField(labelWithString: title)
891
+    private func multilineProfileBlock(labelKey: String, placeholderKey: String, field: NSTextField, minHeight: CGFloat) -> NSView {
892
+        let label = NSTextField(labelWithString: L(labelKey))
893
+        ProfileFormLocalization.tagLabel(label, key: labelKey)
835 894
         label.font = .systemFont(ofSize: 12, weight: .medium)
836 895
         label.textColor = ProfilePagePalette.secondaryText
837 896
         label.translatesAutoresizingMaskIntoConstraints = false
838 897
         ProfileLayoutEnforcement.applyLeftAlignedTextField(label)
839 898
 
899
+        ProfileFormLocalization.tagPlaceholder(field, key: placeholderKey)
840 900
         field.translatesAutoresizingMaskIntoConstraints = false
841 901
         field.isEditable = true
842 902
         field.isSelectable = true
@@ -852,14 +912,7 @@ final class MyProfilePageView: NSView {
852 912
         field.stringValue = ""
853 913
         field.setContentHuggingPriority(.defaultLow, for: .horizontal)
854 914
         field.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
855
-        field.placeholderAttributedString = NSAttributedString(
856
-            string: placeholder,
857
-            attributes: [
858
-                .foregroundColor: ProfilePagePalette.secondaryText,
859
-                .font: NSFont.systemFont(ofSize: 14, weight: .regular),
860
-                .paragraphStyle: ProfileLayoutEnforcement.leftAlignedParagraphStyle()
861
-            ]
862
-        )
915
+        ProfileFormLocalization.applyPlaceholder(L(placeholderKey), to: field)
863 916
         ProfileLayoutEnforcement.applyLeftAlignedTextField(field)
864 917
 
865 918
         let wrap = NSView()
@@ -901,12 +954,13 @@ final class MyProfilePageView: NSView {
901 954
 
902 955
     private func referralBlock() -> NSView {
903 956
         let label = NSTextField(labelWithString: L("Referral (Optional)"))
957
+        ProfileFormLocalization.tagLabel(label, key: "Referral (Optional)")
904 958
         label.font = .systemFont(ofSize: 12, weight: .medium)
905 959
         label.textColor = ProfilePagePalette.secondaryText
906 960
         label.translatesAutoresizingMaskIntoConstraints = false
907 961
         ProfileLayoutEnforcement.applyLeftAlignedTextField(label)
908 962
 
909
-        styleSingleLineField(referralField, placeholder: L("Referred by (Company/Person Name)"))
963
+        styleSingleLineField(referralField, placeholderKey: "Referred by (Company/Person Name)")
910 964
         let wrap = roundedFieldChrome(containing: referralField, minHeight: 40)
911 965
 
912 966
         let helper = NSTextField(wrappingLabelWithString: L("If someone referred you for this job, enter their name or company here"))
@@ -947,6 +1001,7 @@ final class MyProfilePageView: NSView {
947 1001
 
948 1002
     private func workExperienceSection() -> NSView {
949 1003
         let title = NSTextField(labelWithString: L("Work Experience"))
1004
+        ProfileFormLocalization.tagSection(title, key: "Work Experience")
950 1005
         title.font = .systemFont(ofSize: 15, weight: .semibold)
951 1006
         title.textColor = ProfilePagePalette.primaryText
952 1007
         title.translatesAutoresizingMaskIntoConstraints = false
@@ -954,6 +1009,7 @@ final class MyProfilePageView: NSView {
954 1009
         ProfileLayoutEnforcement.applyLeftAlignedTextField(title)
955 1010
 
956 1011
         let addButton = NSButton(title: L("+ Add Another"), target: self, action: #selector(didTapAddWorkExperience))
1012
+        ProfileFormLocalization.tagButton(addButton, key: "+ Add Another")
957 1013
         addButton.translatesAutoresizingMaskIntoConstraints = false
958 1014
         addButton.bezelStyle = .rounded
959 1015
         addButton.isBordered = true
@@ -994,6 +1050,7 @@ final class MyProfilePageView: NSView {
994 1050
 
995 1051
     private func educationSection() -> NSView {
996 1052
         let title = NSTextField(labelWithString: L("Education"))
1053
+        ProfileFormLocalization.tagSection(title, key: "Education")
997 1054
         title.font = .systemFont(ofSize: 15, weight: .semibold)
998 1055
         title.textColor = ProfilePagePalette.primaryText
999 1056
         title.translatesAutoresizingMaskIntoConstraints = false
@@ -1001,6 +1058,7 @@ final class MyProfilePageView: NSView {
1001 1058
         ProfileLayoutEnforcement.applyLeftAlignedTextField(title)
1002 1059
 
1003 1060
         let addButton = NSButton(title: L("+ Add Another"), target: self, action: #selector(didTapAddEducation))
1061
+        ProfileFormLocalization.tagButton(addButton, key: "+ Add Another")
1004 1062
         addButton.translatesAutoresizingMaskIntoConstraints = false
1005 1063
         addButton.bezelStyle = .rounded
1006 1064
         addButton.isBordered = true
@@ -1259,9 +1317,11 @@ private final class WorkExperienceEntryView: NSView {
1259 1317
         if #available(macOS 11.0, *) {
1260 1318
             deleteButton.image = NSImage(systemSymbolName: "trash", accessibilityDescription: L("Remove experience"))
1261 1319
             deleteButton.imagePosition = .imageOnly
1320
+            ProfileFormLocalization.tagButton(deleteButton, key: "Remove experience")
1262 1321
         } else {
1263 1322
             deleteButton.title = L("Remove")
1264 1323
             deleteButton.font = .systemFont(ofSize: 12, weight: .medium)
1324
+            ProfileFormLocalization.tagButton(deleteButton, key: "Remove")
1265 1325
         }
1266 1326
 
1267 1327
         let headerSpacer = NSView()
@@ -1276,15 +1336,27 @@ private final class WorkExperienceEntryView: NSView {
1276 1336
         ProfileLayoutEnforcement.applyForcedLTR(to: headerRow)
1277 1337
         headerRow.translatesAutoresizingMaskIntoConstraints = false
1278 1338
 
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"))
1339
+        let jobGroup = Self.labeledFieldStack(
1340
+            labelKey: "Job Title *",
1341
+            field: jobTitleField,
1342
+            placeholderKey: "e.g., Software Engineer"
1343
+        )
1344
+        let companyGroup = Self.labeledFieldStack(
1345
+            labelKey: "Company Name *",
1346
+            field: companyField,
1347
+            placeholderKey: "e.g., Google"
1348
+        )
1281 1349
         jobCompanyRow = ProfileDualFieldRow(left: jobGroup, right: companyGroup, spacing: 12)
1282 1350
 
1283
-        let durationGroup = Self.labeledFieldStack(title: L("Duration *"), field: durationField, placeholder: L("e.g., Jan 2020 - Present"))
1351
+        let durationGroup = Self.labeledFieldStack(
1352
+            labelKey: "Duration *",
1353
+            field: durationField,
1354
+            placeholderKey: "e.g., Jan 2020 - Present"
1355
+        )
1284 1356
         let descriptionGroup = Self.multilineLabeledStack(
1285
-            title: L("Description"),
1357
+            labelKey: "Description",
1286 1358
             field: descriptionField,
1287
-            placeholder: L("Describe your responsibilities and achievements..."),
1359
+            placeholderKey: "Describe your responsibilities and achievements...",
1288 1360
             minHeight: 120
1289 1361
         )
1290 1362
 
@@ -1334,14 +1406,15 @@ private final class WorkExperienceEntryView: NSView {
1334 1406
         ProfileThemeAppearance.refreshFormSubtree(self)
1335 1407
     }
1336 1408
 
1337
-    fileprivate static func labeledFieldStack(title: String, field: NSTextField, placeholder: String) -> NSView {
1338
-        let label = NSTextField(labelWithString: title)
1409
+    fileprivate static func labeledFieldStack(labelKey: String, field: NSTextField, placeholderKey: String) -> NSView {
1410
+        let label = NSTextField(labelWithString: L(labelKey))
1411
+        ProfileFormLocalization.tagLabel(label, key: labelKey)
1339 1412
         label.font = .systemFont(ofSize: 12, weight: .medium)
1340 1413
         label.textColor = ProfilePagePalette.secondaryText
1341 1414
         label.translatesAutoresizingMaskIntoConstraints = false
1342 1415
         ProfileLayoutEnforcement.applyLeftAlignedTextField(label)
1343 1416
 
1344
-        styleSingleLineField(field, placeholder: placeholder)
1417
+        styleSingleLineField(field, placeholderKey: placeholderKey)
1345 1418
         let wrap = roundedChrome(around: field, minHeight: 40)
1346 1419
 
1347 1420
         let stack = NSStackView(views: [label, wrap])
@@ -1362,13 +1435,15 @@ private final class WorkExperienceEntryView: NSView {
1362 1435
         return stack
1363 1436
     }
1364 1437
 
1365
-    private static func multilineLabeledStack(title: String, field: NSTextField, placeholder: String, minHeight: CGFloat) -> NSView {
1366
-        let label = NSTextField(labelWithString: title)
1438
+    private static func multilineLabeledStack(labelKey: String, field: NSTextField, placeholderKey: String, minHeight: CGFloat) -> NSView {
1439
+        let label = NSTextField(labelWithString: L(labelKey))
1440
+        ProfileFormLocalization.tagLabel(label, key: labelKey)
1367 1441
         label.font = .systemFont(ofSize: 12, weight: .medium)
1368 1442
         label.textColor = ProfilePagePalette.secondaryText
1369 1443
         label.translatesAutoresizingMaskIntoConstraints = false
1370 1444
         ProfileLayoutEnforcement.applyLeftAlignedTextField(label)
1371 1445
 
1446
+        ProfileFormLocalization.tagPlaceholder(field, key: placeholderKey)
1372 1447
         field.translatesAutoresizingMaskIntoConstraints = false
1373 1448
         field.isEditable = true
1374 1449
         field.isSelectable = true
@@ -1381,14 +1456,7 @@ private final class WorkExperienceEntryView: NSView {
1381 1456
         field.cell?.wraps = true
1382 1457
         field.cell?.isScrollable = false
1383 1458
         field.cell?.usesSingleLineMode = false
1384
-        field.placeholderAttributedString = NSAttributedString(
1385
-            string: placeholder,
1386
-            attributes: [
1387
-                .foregroundColor: ProfilePagePalette.secondaryText,
1388
-                .font: NSFont.systemFont(ofSize: 14, weight: .regular),
1389
-                .paragraphStyle: ProfileLayoutEnforcement.leftAlignedParagraphStyle()
1390
-            ]
1391
-        )
1459
+        ProfileFormLocalization.applyPlaceholder(L(placeholderKey), to: field)
1392 1460
         ProfileLayoutEnforcement.applyLeftAlignedTextField(field)
1393 1461
 
1394 1462
         let wrap = NSView()
@@ -1428,21 +1496,15 @@ private final class WorkExperienceEntryView: NSView {
1428 1496
         return stack
1429 1497
     }
1430 1498
 
1431
-    private static func styleSingleLineField(_ field: NSTextField, placeholder: String) {
1499
+    private static func styleSingleLineField(_ field: NSTextField, placeholderKey: String) {
1500
+        ProfileFormLocalization.tagPlaceholder(field, key: placeholderKey)
1432 1501
         field.translatesAutoresizingMaskIntoConstraints = false
1433 1502
         field.isBordered = false
1434 1503
         field.drawsBackground = false
1435 1504
         field.focusRingType = .none
1436 1505
         field.font = .systemFont(ofSize: 14, weight: .regular)
1437 1506
         field.textColor = ProfilePagePalette.primaryText
1438
-        field.placeholderAttributedString = NSAttributedString(
1439
-            string: placeholder,
1440
-            attributes: [
1441
-                .foregroundColor: ProfilePagePalette.secondaryText,
1442
-                .font: NSFont.systemFont(ofSize: 14, weight: .regular),
1443
-                .paragraphStyle: ProfileLayoutEnforcement.leftAlignedParagraphStyle()
1444
-            ]
1445
-        )
1507
+        ProfileFormLocalization.applyPlaceholder(L(placeholderKey), to: field)
1446 1508
         field.cell?.usesSingleLineMode = true
1447 1509
         field.cell?.wraps = false
1448 1510
         field.cell?.isScrollable = true
@@ -1549,9 +1611,11 @@ private final class EducationEntryView: NSView {
1549 1611
         if #available(macOS 11.0, *) {
1550 1612
             deleteButton.image = NSImage(systemSymbolName: "trash", accessibilityDescription: L("Remove education"))
1551 1613
             deleteButton.imagePosition = .imageOnly
1614
+            ProfileFormLocalization.tagButton(deleteButton, key: "Remove education")
1552 1615
         } else {
1553 1616
             deleteButton.title = L("Remove")
1554 1617
             deleteButton.font = .systemFont(ofSize: 12, weight: .medium)
1618
+            ProfileFormLocalization.tagButton(deleteButton, key: "Remove")
1555 1619
         }
1556 1620
 
1557 1621
         let headerSpacer = NSView()
@@ -1567,21 +1631,21 @@ private final class EducationEntryView: NSView {
1567 1631
         headerRow.translatesAutoresizingMaskIntoConstraints = false
1568 1632
 
1569 1633
         let degreeGroup = WorkExperienceEntryView.labeledFieldStack(
1570
-            title: L("Degree / program *"),
1634
+            labelKey: "Degree / program *",
1571 1635
             field: degreeField,
1572
-            placeholder: L("e.g., BSc Computer Science")
1636
+            placeholderKey: "e.g., BSc Computer Science"
1573 1637
         )
1574 1638
         let institutionGroup = WorkExperienceEntryView.labeledFieldStack(
1575
-            title: L("Institution *"),
1639
+            labelKey: "Institution *",
1576 1640
             field: institutionField,
1577
-            placeholder: L("e.g., MIT")
1641
+            placeholderKey: "e.g., MIT"
1578 1642
         )
1579 1643
         degreeInstitutionRow = ProfileDualFieldRow(left: degreeGroup, right: institutionGroup, spacing: 12)
1580 1644
 
1581 1645
         let yearGroup = WorkExperienceEntryView.labeledFieldStack(
1582
-            title: L("Year *"),
1646
+            labelKey: "Year *",
1583 1647
             field: yearField,
1584
-            placeholder: L("e.g., 2020")
1648
+            placeholderKey: "e.g., 2020"
1585 1649
         )
1586 1650
 
1587 1651
         let inner = NSStackView(views: [headerRow, degreeInstitutionRow, yearGroup])

+ 353 - 0
App for Indeed/fr.lproj/Localizable.strings

@@ -0,0 +1,353 @@
1
+/* Localizable.strings (Français) */
2
+
3
+// MARK: - Général
4
+"OK" = "OK";
5
+"Cancel" = "Annuler";
6
+"Delete" = "Supprimer";
7
+"Remove" = "Retirer";
8
+"Dismiss" = "Ignorer";
9
+
10
+// MARK: - Écran de lancement
11
+"AI-POWERED" = "ALIMENTÉ PAR L'IA";
12
+"Find your perfect job with the power of AI." = "Trouvez l'emploi idéal grâce à la puissance de l'IA.";
13
+"Starting up…" = "Démarrage…";
14
+"Loading progress" = "Chargement en cours";
15
+
16
+// MARK: - Statut de lancement
17
+"Checking your Pro subscription…" = "Vérification de votre abonnement Pro…";
18
+"Loading premium plans from the App Store…" = "Chargement des offres premium depuis l'App Store…";
19
+"Preparing your job search workspace…" = "Préparation de votre espace de recherche d'emploi…";
20
+"Almost ready…" = "Presque prêt…";
21
+
22
+// MARK: - Barre latérale
23
+"Home" = "Accueil";
24
+"Saved Jobs" = "Emplois enregistrés";
25
+"CV Maker" = "Créateur de CV";
26
+"Profile" = "Profil";
27
+"Settings" = "Paramètres";
28
+"Premium" = "Premium";
29
+"Indeed" = "Indeed";
30
+"Open Indeed to search and apply for jobs" = "Ouvrez Indeed pour rechercher et postuler à des emplois";
31
+
32
+// MARK: - Tableau de bord / Accueil
33
+"Welcome" = "Bienvenue";
34
+"Send" = "Envoyer";
35
+"Clear chat" = "Effacer le chat";
36
+"Remove all messages and start a new conversation" = "Supprimer tous les messages et démarrer une nouvelle conversation";
37
+"Ask for roles, skills, salary, or job descriptions..." = "Demandez des rôles, compétences, salaires ou descriptions de poste…";
38
+"Ask AI" = "Interroger l'IA";
39
+"1 reply left" = "1 réponse restante";
40
+"Apply" = "Postuler";
41
+"Save" = "Enregistrer";
42
+"Saved" = "Enregistré";
43
+"Remove from saved" = "Retirer des enregistrements";
44
+"Show more jobs" = "Afficher plus d'emplois";
45
+"This area is not available in the preview build. Use Home to search jobs." = "Cette zone n'est pas disponible dans la version préliminaire. Utilisez Accueil pour rechercher des emplois.";
46
+"Save jobs from Home to see them here." = "Enregistrez des emplois depuis Accueil pour les voir ici.";
47
+"No saved jobs yet. Search on Home, then tap Save on a listing." = "Aucun emploi enregistré pour l'instant. Effectuez une recherche sur Accueil, puis appuyez sur Enregistrer sur une annonce.";
48
+"Tell me what role you want and I will return job descriptions, key skills, and a quick fit summary." = "Dites-moi quel rôle vous voulez et je vous fournirai des descriptions de poste, les compétences clés et un résumé rapide de la correspondance.";
49
+"1 saved position" = "1 poste enregistré";
50
+"Delete this profile?" = "Supprimer ce profil ?";
51
+"Find roles similar to: " = "Trouver des rôles similaires à : ";
52
+"Find jobs at company: " = "Trouver des emplois dans l'entreprise : ";
53
+"Find jobs that require skill: " = "Trouver des emplois qui exigent la compétence : ";
54
+"match" = "correspondance";
55
+"matches" = "correspondances";
56
+
57
+// MARK: - Raccourcis des fonctionnalités
58
+"Role" = "Rôle";
59
+"Explore similar or better job roles" = "Explorez des rôles similaires ou meilleurs";
60
+"Company" = "Entreprise";
61
+"Find opportunities at other companies" = "Trouvez des opportunités dans d'autres entreprises";
62
+"Skill" = "Compétence";
63
+"Match jobs that fit your skills" = "Trouvez des emplois qui correspondent à vos compétences";
64
+
65
+// MARK: - Pro / Abonnement
66
+"Upgrade to Pro" = "Passer à Pro";
67
+"You're on Pro" = "Vous êtes sur Pro";
68
+"Unlimited AI matches, smart alerts, and interview prep—all in one place." = "Correspondances IA illimitées, alertes intelligentes et préparation aux entretiens — tout au même endroit.";
69
+"Manage billing, renewals, and plans in Premium." = "Gérez la facturation, les renouvellements et les forfaits dans Premium.";
70
+"Try Pro" = "Essayer Pro";
71
+"Manage Subscription" = "Gérer l'abonnement";
72
+"Premium Plans" = "Forfaits Premium";
73
+"Unlock unlimited access to premium tools and boost your productivity." = "Débloquez un accès illimité aux outils premium et boostez votre productivité.";
74
+"Continue with free plan" = "Continuer avec le forfait gratuit";
75
+"Restore Purchase" = "Restaurer l'achat";
76
+"You're subscribed" = "Vous êtes abonné";
77
+"Thank you — Pro features are now available." = "Merci — les fonctionnalités Pro sont maintenant disponibles.";
78
+"Pro" = "Pro";
79
+"Purchases restored" = "Achats restaurés";
80
+"Your subscription is active." = "Votre abonnement est actif.";
81
+"No subscription found" = "Aucun abonnement trouvé";
82
+"There was nothing to restore for this Apple ID." = "Il n'y avait rien à restaurer pour cet identifiant Apple.";
83
+"Something went wrong" = "Une erreur s'est produite";
84
+"That subscription isn’t available from the App Store right now." = "Cet abonnement n'est pas disponible sur l'App Store pour le moment.";
85
+"Unlimited AI job search on Home" = "Recherche d'emploi IA illimitée sur Accueil";
86
+"Save jobs & open listings in-app" = "Enregistrez des emplois et ouvrez les annonces dans l'application";
87
+"CV Maker, profiles & PDF export" = "Créateur de CV, profils et export PDF";
88
+"Role, company & skill shortcuts" = "Raccourcis de rôle, entreprise et compétence";
89
+
90
+// MARK: - Offres du paywall
91
+"Weekly" = "Hebdomadaire";
92
+"Flexible and commitment-free" = "Flexible et sans engagement";
93
+"Monthly" = "Mensuel";
94
+"Balanced for regular productivity" = "Équilibré pour une productivité régulière";
95
+"Yearly" = "Annuel";
96
+"Best value for long-term users" = "Meilleur rapport qualité-prix pour les utilisateurs à long terme";
97
+"/ week" = "/ semaine";
98
+"/ month" = "/ mois";
99
+"/ year" = "/ an";
100
+"3 days free trial" = "3 jours d'essai gratuit";
101
+"Perfect for short-term job hunts" = "Parfait pour les recherches d'emploi à court terme";
102
+"Cancel anytime" = "Annulation à tout moment";
103
+"Best for regular job seekers" = "Idéal pour les chercheurs d'emploi réguliers";
104
+"Priority support" = "Assistance prioritaire";
105
+"Lowest effective monthly cost" = "Coût mensuel effectif le plus bas";
106
+"Ideal for long-term use" = "Idéal pour une utilisation à long terme";
107
+
108
+// MARK: - Confiance du paywall
109
+"Secure Payments" = "Paiements sécurisés";
110
+"Your payment is 100% secure." = "Votre paiement est 100 % sécurisé.";
111
+"Cancel Anytime" = "Annulez à tout moment";
112
+"No commitment, cancel anytime." = "Sans engagement, annulez à tout moment.";
113
+"24/7 Support" = "Assistance 24/7";
114
+"We're here to help you anytime." = "Nous sommes là pour vous aider à tout moment.";
115
+"Privacy First" = "Confidentialité d'abord";
116
+"Your data is safe with us." = "Vos données sont en sécurité avec nous.";
117
+
118
+// MARK: - Paramètres
119
+"Appearance" = "Apparence";
120
+"Theme" = "Thème";
121
+"Language" = "Langue";
122
+"Share App" = "Partager l'application";
123
+"More Apps" = "Plus d'applications";
124
+"About" = "À propos";
125
+"Website" = "Site Web";
126
+"Support" = "Assistance";
127
+"Terms of Use" = "Conditions d'utilisation";
128
+"Privacy Policy" = "Politique de confidentialité";
129
+"System" = "Système";
130
+"Light" = "Clair";
131
+"Dark" = "Sombre";
132
+
133
+// MARK: - Profils
134
+"Profiles" = "Profils";
135
+"Add new profile" = "Ajouter un profil";
136
+"Create and manage CV profiles. Each profile stores your details on this Mac." = "Créez et gérez des profils de CV. Chaque profil stocke vos informations sur ce Mac.";
137
+"No profiles yet. Tap “Add new profile” to create your first one." = "Aucun profil pour l'instant. Appuyez sur « Ajouter un profil » pour créer votre premier profil.";
138
+"Build CV" = "Créer un CV";
139
+"Edit" = "Modifier";
140
+"Untitled profile" = "Profil sans titre";
141
+"No contact details yet" = "Aucune coordonnée pour l'instant";
142
+"← Profiles" = "← Profils";
143
+
144
+// MARK: - Éditeur de profil
145
+"Save Profile  →" = "Enregistrer le profil →";
146
+"← All profiles" = "← Tous les profils";
147
+"New profile" = "Nouveau profil";
148
+"Edit profile" = "Modifier le profil";
149
+"Profile Name *" = "Nom du profil *";
150
+"Marketing Director Profile" = "Profil de Directeur Marketing";
151
+"Personal Information" = "Informations personnelles";
152
+"Full Name *" = "Nom complet *";
153
+"John Doe" = "Jean Dupont";
154
+"Email *" = "E-mail *";
155
+"john@example.com" = "jean.dupont@exemple.com";
156
+"Phone" = "Téléphone";
157
+"+1 (555) 123-4567" = "+33 6 12 34 56 78";
158
+"Job Title *" = "Titre du poste *";
159
+"Software Engineer" = "Ingénieur logiciel";
160
+"Address" = "Adresse";
161
+"123 Main St, City, State, ZIP" = "123 Rue Principale, Ville, Code Postal";
162
+"Certificates / Rewards" = "Certificats / Récompenses";
163
+"List your certificates and awards..." = "Listez vos certificats et récompenses…";
164
+"Interests" = "Centres d'intérêt";
165
+"List your interests and hobbies..." = "Listez vos centres d'intérêt et loisirs…";
166
+"Languages" = "Langues";
167
+"List languages you speak (e.g., English - Native, Spanish - Fluent)..." = "Listez les langues que vous parlez (ex : Français - Natif, Anglais - Courant)…";
168
+"Career Summary" = "Résumé de carrière";
169
+"Brief overview of your professional background and key achievements..." = "Aperçu bref de votre parcours professionnel et de vos principales réalisations…";
170
+"Referral (Optional)" = "Recommandation (optionnel)";
171
+"Referred by (Company/Person Name)" = "Recommandé par (Nom de l'entreprise/de la personne)";
172
+"If someone referred you for this job, enter their name or company here" = "Si quelqu'un vous a recommandé pour ce poste, entrez son nom ou son entreprise ici";
173
+"Work Experience" = "Expérience professionnelle";
174
+"Education" = "Formation";
175
+"+ Add Another" = "+ Ajouter un autre";
176
+"Complete required fields" = "Remplissez les champs obligatoires";
177
+"Remove experience" = "Supprimer l'expérience";
178
+"Remove education" = "Supprimer la formation";
179
+"Company Name *" = "Nom de l'entreprise *";
180
+"Duration *" = "Durée *";
181
+"Description" = "Description";
182
+"e.g., Software Engineer" = "ex : Ingénieur logiciel";
183
+"e.g., Google" = "ex : Google";
184
+"e.g., Jan 2020 - Present" = "ex : janv. 2020 - Présent";
185
+"Describe your responsibilities and achievements..." = "Décrivez vos responsabilités et réalisations…";
186
+"Degree / program *" = "Diplôme / programme *";
187
+"Institution *" = "Établissement *";
188
+"Year *" = "Année *";
189
+"e.g., BSc Computer Science" = "ex : Licence en Informatique";
190
+"e.g., MIT" = "ex : Sorbonne Université";
191
+"e.g., 2020" = "ex : 2020";
192
+"Profile name" = "Nom du profil";
193
+"Full Name" = "Nom complet";
194
+"Email" = "E-mail";
195
+"Job Title" = "Titre du poste";
196
+
197
+// MARK: - Créateur de CV
198
+"Templates" = "Modèles";
199
+"Polished layouts with live previews — pick a style that fits your story." = "Des mises en page soignées avec aperçus en direct — choisissez un style qui correspond à votre histoire.";
200
+"Use Template & Select Profile  →" = "Utiliser le modèle et sélectionner le profil →";
201
+"All" = "Tous";
202
+"No templates yet for this category." = "Aucun modèle pour cette catégorie pour l'instant.";
203
+"Pick a template" = "Choisissez un modèle";
204
+"Select a template first, then choose a profile to continue." = "Sélectionnez d'abord un modèle, puis choisissez un profil pour continuer.";
205
+"Fetching AI-curated templates…" = "Récupération des modèles organisés par l'IA…";
206
+"Couldn’t load AI templates — showing the built-in gallery." = "Impossible de charger les modèles IA — affichage de la galerie intégrée.";
207
+"Design-Based" = "Basé sur le design";
208
+"Profession-Based" = "Basé sur la profession";
209
+"Professional" = "Professionnel";
210
+"Modern" = "Moderne";
211
+"Creative" = "Créatif";
212
+"Minimal" = "Minimaliste";
213
+"Executive" = "Cadre dirigeant";
214
+"ATS layout" = "Mise en page ATS";
215
+"Sidebar left" = "Barre latérale gauche";
216
+"Sidebar right" = "Barre latérale droite";
217
+
218
+// MARK: - Aperçu du CV
219
+"CV preview" = "Aperçu du CV";
220
+"Export PDF…" = "Exporter en PDF…";
221
+"Layout matches the CV Maker thumbnail for this template. Export a PDF that matches what you see here (fonts, columns, colours, and rules)." = "La mise en page correspond à la miniature du Créateur de CV pour ce modèle. Exportez un PDF qui correspond à ce que vous voyez ici (polices, colonnes, couleurs et règles).";
222
+"The résumé could not be rendered to PDF (empty output). Try scrolling the preview so it lays out, then export again." = "Le CV n'a pas pu être converti en PDF (sortie vide). Essayez de faire défiler l'aperçu pour qu'il se mette en page, puis exportez à nouveau.";
223
+"Couldn’t save PDF" = "Impossible d'enregistrer le PDF";
224
+"Your name" = "Votre nom";
225
+"Professional headline" = "Titre professionnel";
226
+"Experience" = "Expérience";
227
+"Highlights" = "Points forts";
228
+"Summary" = "Résumé";
229
+"Contact" = "Contact";
230
+"Skills" = "Compétences";
231
+"Tools" = "Outils";
232
+"Languages & more" = "Langues et plus";
233
+"Certificates" = "Certificats";
234
+"Referrals" = "Recommandations";
235
+"Professional Summary" = "Résumé professionnel";
236
+"Selected Experience" = "Expérience sélectionnée";
237
+"Core Competencies" = "Compétences clés";
238
+"Impact" = "Impact";
239
+"Add contact in your profile" = "Ajoutez un contact dans votre profil";
240
+"Add contact details in your profile" = "Ajoutez des coordonnées dans votre profil";
241
+"Add a career summary or interests in your profile to populate this column." = "Ajoutez un résumé de carrière ou des centres d'intérêt dans votre profil pour remplir cette colonne.";
242
+"CV" = "CV";
243
+"Open to relocation" = "Prêt à déménager";
244
+"STRENGTHS" = "FORCES";
245
+"PORTFOLIO SNAPSHOT" = "APERÇU DU PORTFOLIO";
246
+"Close" = "Fermer";
247
+"/ day" = "/ jour";
248
+"/ %d days" = "/ %d jours";
249
+"/ %d weeks" = "/ %d semaines";
250
+"/ %d months" = "/ %d mois";
251
+"/ %d years" = "/ %d ans";
252
+
253
+// MARK: - Noms des modèles de CV
254
+"Paper White" = "Blanc Pur";
255
+"Swiss" = "Suisse";
256
+"Mono" = "Mono";
257
+"Airy" = "Aérien";
258
+"Tabular" = "Tabulaire";
259
+"Facet" = "Facette";
260
+"Corporate" = "Corporate";
261
+"Atlas" = "Atlas";
262
+"Ledger" = "Registre";
263
+"Harbor" = "Havre";
264
+"Clear Path" = "Chemin Clair";
265
+"Pinstripe" = "Rayures Fines";
266
+"Briefing" = "Briefing";
267
+"Quorum" = "Quorum";
268
+"Docket" = "Rôle";
269
+"Conduit" = "Conduit";
270
+"Principal" = "Principal";
271
+"Charter" = "Charte";
272
+"Vertex" = "Sommet";
273
+"Linea" = "Ligne";
274
+"Prism" = "Prisme";
275
+"Circuit" = "Circuit";
276
+"North" = "Nord";
277
+"Axis" = "Axe";
278
+"Marigold" = "Œillet d'Inde";
279
+"Ember" = "Braise";
280
+"Lattice" = "Treillis";
281
+"Bloom" = "Floraison";
282
+"Studio" = "Studio";
283
+"Kite" = "Cerf-volant";
284
+"Regent" = "Régent";
285
+"Monarch" = "Monarque";
286
+"Sterling" = "Sterling";
287
+"Summit" = "Sommet";
288
+"Estate" = "Domaine";
289
+"Chairman" = "Président";
290
+"Blue Ocean" = "Océan Bleu";
291
+
292
+// MARK: - Contenu de l'aperçu de démonstration du CV
293
+"Sarah Johnson" = "Sarah Johnson";
294
+"Senior Product Manager" = "Chef de produit senior";
295
+"Group PM, Consumer Growth & Activation" = "Responsable de groupe produit, Croissance et Activation des consommateurs";
296
+"Google · Mountain View, CA · 2019 – Present" = "Google · Mountain View, Californie · 2019 – Présent";
297
+"Stanford University" = "Université de Stanford";
298
+"M.S. Management Science & Engineering" = "Master en sciences du management et ingénierie";
299
+"2014 – 2016" = "2014 – 2016";
300
+"Mountain View, CA" = "Mountain View, Californie";
301
+"Product leader shipping roadmap, discovery, and analytics for high-scale consumer experiences." = "Responsable produit, pilotant la feuille de route, la découverte et les analyses pour des expériences consommateurs à grande échelle.";
302
+"Defined multi-year platform strategy with exec stakeholders and quarterly OKRs." = "Définition d'une stratégie de plateforme pluriannuelle avec les parties prenantes exécutives et les OKR trimestriels.";
303
+"Partnered with engineering and design to launch experiments improving activation by 12%." = "Partenariat avec l'ingénierie et le design pour lancer des expériences améliorant l'activation de 12 %.";
304
+"Stood up quarterly business reviews with finance and GTM, aligning spend to north-star metrics." = "Mise en place de revues commerciales trimestrielles avec les finances et le GTM, alignant les dépenses sur les métriques clés.";
305
+"Presented roadmap shifts to the leadership team and translated trade-offs into clear investment asks." = "Présentation des changements de feuille de route à l'équipe de direction et traduction des compromis en demandes d'investissement claires.";
306
+"Figma · SQL · Amplitude · Jira · BigQuery" = "Figma · SQL · Amplitude · Jira · BigQuery";
307
+"Product Strategy" = "Stratégie produit";
308
+"A/B Testing" = "Tests A/B";
309
+"Roadmapping" = "Feuille de route";
310
+"CONTACT" = "CONTACT";
311
+"SKILLS" = "COMPÉTENCES";
312
+"PROFILE" = "PROFIL";
313
+"EXPERIENCE" = "EXPÉRIENCE";
314
+"EDUCATION" = "FORMATION";
315
+"SUMMARY" = "RÉSUMÉ";
316
+"PROFESSIONAL SUMMARY" = "RÉSUMÉ PROFESSIONNEL";
317
+"SELECTED EXPERIENCE" = "EXPÉRIENCE SÉLECTIONNÉE";
318
+"CORE COMPETENCIES" = "COMPÉTENCES CLÉS";
319
+"TOOLS" = "OUTILS";
320
+"IMPACT" = "IMPACT";
321
+
322
+// MARK: - Navigateur d'emplois
323
+"Return to the previous screen" = "Revenir à l'écran précédent";
324
+
325
+// MARK: - Erreurs
326
+"We couldn't reach the server. Check your internet connection and try again." = "Nous ne pouvons pas joindre le serveur. Vérifiez votre connexion Internet et réessayez.";
327
+"The search was cancelled. Try again when you're ready." = "La recherche a été annulée. Réessayez quand vous êtes prêt.";
328
+"Something went wrong while searching. Please try again in a moment." = "Une erreur s'est produite lors de la recherche. Veuillez réessayer dans un instant.";
329
+"Job search is unavailable." = "La recherche d'emploi n'est pas disponible.";
330
+
331
+// MARK: - Alertes
332
+"This profile will be removed from this Mac." = "Ce profil sera supprimé de ce Mac.";
333
+
334
+// MARK: - Chaînes de format
335
+"Loading %@" = "Chargement de %@";
336
+"Loading %@. %@" = "Chargement de %@. %@";
337
+"Starting %@…" = "Démarrage de %@…";
338
+"%d replies left" = "%d réponses restantes";
339
+"%d saved positions" = "%d postes enregistrés";
340
+"“%@” will be removed from this Mac." = "« %@ » sera supprimé de ce Mac.";
341
+"%@ isn’t available yet" = "%@ n'est pas encore disponible";
342
+"I couldn't find new matches for “%@”. Try a different angle or a more specific keyword." = "Je n'ai pas trouvé de nouvelles correspondances pour « %@ ». Essayez un autre angle ou un mot-clé plus spécifique.";
343
+"No jobs found for “%@”. Try another title, skill, company, or location." = "Aucun emploi trouvé pour « %@ ». Essayez un autre titre, compétence, entreprise ou lieu.";
344
+"Here are %d more %@ for “%@”." = "Voici %d %@ supplémentaires pour « %@ ».";
345
+"Found %d %@ for “%@”. Tap Apply to open the listing or Save to revisit later." = "%d %@ trouvé(s) pour « %@ ». Appuyez sur Postuler pour ouvrir l'annonce ou sur Enregistrer pour y revenir plus tard.";
346
+"Get %@" = "Obtenir %@";
347
+"You chose the “%@” template. Tap Build CV on a profile to preview your résumé with that layout." = "Vous avez choisi le modèle « %@ ». Appuyez sur Créer un CV sur un profil pour prévisualiser votre CV avec cette mise en page.";
348
+"Experience %d" = "Expérience %d";
349
+"Education %d" = "Formation %d";
350
+"Please fill in: %@." = "Veuillez remplir : %@.";
351
+
352
+// MARK: - Multiligne
353
+"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)" = "Ajoutez vos identifiants Mac App Store dans les paramètres de build de la cible :\n• AppStoreAppID — identifiant numérique de l'application depuis App Store Connect\n• AppStoreDeveloperID — identifiant numérique du développeur (pour votre page d'autres applications)";