Selaa lähdekoodia

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 4 päivää sitten
vanhempi
commit
70e23561b7

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

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

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

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

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

@@ -50,6 +50,8 @@ final class CVTemplateFetchService {
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`."
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
             case .arabic:
51
             case .arabic:
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`."
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
 /// 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.
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
 private final class ProfileDualFieldRow: NSView {
172
 private final class ProfileDualFieldRow: NSView {
115
     private let leftView: NSView
173
     private let leftView: NSView
@@ -282,6 +340,8 @@ final class MyProfilePageView: NSView {
282
         backButton.title = L("← All profiles")
340
         backButton.title = L("← All profiles")
283
         saveButton.title = L("Save Profile  →")
341
         saveButton.title = L("Save Profile  →")
284
         contextLabel.stringValue = editingProfileID == nil ? L("New profile") : L("Edit profile")
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
         renumberWorkExperienceEntries()
345
         renumberWorkExperienceEntries()
286
         renumberEducationEntries()
346
         renumberEducationEntries()
287
         ProfileThemeAppearance.refreshFormSubtree(formStack)
347
         ProfileThemeAppearance.refreshFormSubtree(formStack)
@@ -454,22 +514,22 @@ final class MyProfilePageView: NSView {
454
         ])
514
         ])
455
 
515
 
456
         addFullWidthArrangedSubview(
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
         nameEmailRow = ProfileDualFieldRow(left: nameGroup, right: emailGroup, spacing: 12)
523
         nameEmailRow = ProfileDualFieldRow(left: nameGroup, right: emailGroup, spacing: 12)
464
         addFullWidthArrangedSubview(nameEmailRow)
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
         phoneJobRow = ProfileDualFieldRow(left: phoneGroup, right: jobGroup, spacing: 12)
528
         phoneJobRow = ProfileDualFieldRow(left: phoneGroup, right: jobGroup, spacing: 12)
469
         addFullWidthArrangedSubview(phoneJobRow)
529
         addFullWidthArrangedSubview(phoneJobRow)
470
 
530
 
471
         addFullWidthArrangedSubview(
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
         addFullWidthArrangedSubview(careerSummaryBlock())
534
         addFullWidthArrangedSubview(careerSummaryBlock())
475
         addFullWidthArrangedSubview(horizontalSeparator())
535
         addFullWidthArrangedSubview(horizontalSeparator())
@@ -479,8 +539,8 @@ final class MyProfilePageView: NSView {
479
         addFullWidthArrangedSubview(horizontalSeparator())
539
         addFullWidthArrangedSubview(horizontalSeparator())
480
         addFullWidthArrangedSubview(
540
         addFullWidthArrangedSubview(
481
             multilineProfileBlock(
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
                 field: certificatesField,
544
                 field: certificatesField,
485
                 minHeight: 100
545
                 minHeight: 100
486
             )
546
             )
@@ -488,8 +548,8 @@ final class MyProfilePageView: NSView {
488
         addFullWidthArrangedSubview(horizontalSeparator())
548
         addFullWidthArrangedSubview(horizontalSeparator())
489
         addFullWidthArrangedSubview(
549
         addFullWidthArrangedSubview(
490
             multilineProfileBlock(
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
                 field: interestsField,
553
                 field: interestsField,
494
                 minHeight: 100
554
                 minHeight: 100
495
             )
555
             )
@@ -497,8 +557,8 @@ final class MyProfilePageView: NSView {
497
         addFullWidthArrangedSubview(horizontalSeparator())
557
         addFullWidthArrangedSubview(horizontalSeparator())
498
         addFullWidthArrangedSubview(
558
         addFullWidthArrangedSubview(
499
             multilineProfileBlock(
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
                 field: languagesField,
562
                 field: languagesField,
503
                 minHeight: 100
563
                 minHeight: 100
504
             )
564
             )
@@ -518,6 +578,7 @@ final class MyProfilePageView: NSView {
518
 
578
 
519
     func prepareNewProfile() {
579
     func prepareNewProfile() {
520
         editingProfileID = nil
580
         editingProfileID = nil
581
+        applyLocalizedStrings()
521
         contextLabel.stringValue = L("New profile")
582
         contextLabel.stringValue = L("New profile")
522
         applyForm(
583
         applyForm(
523
             from: SavedProfile(
584
             from: SavedProfile(
@@ -537,6 +598,7 @@ final class MyProfilePageView: NSView {
537
 
598
 
538
     func loadSavedProfile(_ profile: SavedProfile) {
599
     func loadSavedProfile(_ profile: SavedProfile) {
539
         editingProfileID = profile.id
600
         editingProfileID = profile.id
601
+        applyLocalizedStrings()
540
         contextLabel.stringValue = L("Edit profile")
602
         contextLabel.stringValue = L("Edit profile")
541
         applyForm(from: profile)
603
         applyForm(from: profile)
542
     }
604
     }
@@ -658,8 +720,9 @@ final class MyProfilePageView: NSView {
658
         view.widthAnchor.constraint(equalTo: formStack.widthAnchor).isActive = true
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
         label.font = .systemFont(ofSize: 15, weight: .semibold)
726
         label.font = .systemFont(ofSize: 15, weight: .semibold)
664
         label.textColor = ProfilePagePalette.primaryText
727
         label.textColor = ProfilePagePalette.primaryText
665
         label.baseWritingDirection = .leftToRight
728
         label.baseWritingDirection = .leftToRight
@@ -685,15 +748,16 @@ final class MyProfilePageView: NSView {
685
         return row
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
         label.font = .systemFont(ofSize: 12, weight: .medium)
754
         label.font = .systemFont(ofSize: 12, weight: .medium)
691
         label.textColor = ProfilePagePalette.secondaryText
755
         label.textColor = ProfilePagePalette.secondaryText
692
         label.translatesAutoresizingMaskIntoConstraints = false
756
         label.translatesAutoresizingMaskIntoConstraints = false
693
         label.setContentHuggingPriority(.defaultLow, for: .horizontal)
757
         label.setContentHuggingPriority(.defaultLow, for: .horizontal)
694
         ProfileLayoutEnforcement.applyLeftAlignedTextField(label)
758
         ProfileLayoutEnforcement.applyLeftAlignedTextField(label)
695
 
759
 
696
-        styleSingleLineField(field, placeholder: placeholder)
760
+        styleSingleLineField(field, placeholderKey: placeholderKey)
697
         let wrap = roundedFieldChrome(containing: field, minHeight: 40)
761
         let wrap = roundedFieldChrome(containing: field, minHeight: 40)
698
 
762
 
699
         let stack = NSStackView(views: [label, wrap])
763
         let stack = NSStackView(views: [label, wrap])
@@ -715,7 +779,8 @@ final class MyProfilePageView: NSView {
715
         return stack
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
         field.translatesAutoresizingMaskIntoConstraints = false
784
         field.translatesAutoresizingMaskIntoConstraints = false
720
         field.isBordered = false
785
         field.isBordered = false
721
         field.drawsBackground = false
786
         field.drawsBackground = false
@@ -724,15 +789,7 @@ final class MyProfilePageView: NSView {
724
         field.textColor = ProfilePagePalette.primaryText
789
         field.textColor = ProfilePagePalette.primaryText
725
         field.setContentHuggingPriority(.defaultLow, for: .horizontal)
790
         field.setContentHuggingPriority(.defaultLow, for: .horizontal)
726
         field.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
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
         field.cell?.usesSingleLineMode = true
793
         field.cell?.usesSingleLineMode = true
737
         field.cell?.wraps = false
794
         field.cell?.wraps = false
738
         field.cell?.isScrollable = true
795
         field.cell?.isScrollable = true
@@ -763,6 +820,7 @@ final class MyProfilePageView: NSView {
763
 
820
 
764
     private func careerSummaryBlock() -> NSView {
821
     private func careerSummaryBlock() -> NSView {
765
         let label = NSTextField(labelWithString: L("Career Summary"))
822
         let label = NSTextField(labelWithString: L("Career Summary"))
823
+        ProfileFormLocalization.tagLabel(label, key: "Career Summary")
766
         label.font = .systemFont(ofSize: 12, weight: .medium)
824
         label.font = .systemFont(ofSize: 12, weight: .medium)
767
         label.textColor = ProfilePagePalette.secondaryText
825
         label.textColor = ProfilePagePalette.secondaryText
768
         label.translatesAutoresizingMaskIntoConstraints = false
826
         label.translatesAutoresizingMaskIntoConstraints = false
@@ -783,13 +841,13 @@ final class MyProfilePageView: NSView {
783
         careerField.stringValue = ""
841
         careerField.stringValue = ""
784
         careerField.setContentHuggingPriority(.defaultLow, for: .horizontal)
842
         careerField.setContentHuggingPriority(.defaultLow, for: .horizontal)
785
         careerField.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
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
         ProfileLayoutEnforcement.applyLeftAlignedTextField(careerField)
852
         ProfileLayoutEnforcement.applyLeftAlignedTextField(careerField)
795
 
853
 
@@ -830,13 +888,15 @@ final class MyProfilePageView: NSView {
830
         return stack
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
         label.font = .systemFont(ofSize: 12, weight: .medium)
894
         label.font = .systemFont(ofSize: 12, weight: .medium)
836
         label.textColor = ProfilePagePalette.secondaryText
895
         label.textColor = ProfilePagePalette.secondaryText
837
         label.translatesAutoresizingMaskIntoConstraints = false
896
         label.translatesAutoresizingMaskIntoConstraints = false
838
         ProfileLayoutEnforcement.applyLeftAlignedTextField(label)
897
         ProfileLayoutEnforcement.applyLeftAlignedTextField(label)
839
 
898
 
899
+        ProfileFormLocalization.tagPlaceholder(field, key: placeholderKey)
840
         field.translatesAutoresizingMaskIntoConstraints = false
900
         field.translatesAutoresizingMaskIntoConstraints = false
841
         field.isEditable = true
901
         field.isEditable = true
842
         field.isSelectable = true
902
         field.isSelectable = true
@@ -852,14 +912,7 @@ final class MyProfilePageView: NSView {
852
         field.stringValue = ""
912
         field.stringValue = ""
853
         field.setContentHuggingPriority(.defaultLow, for: .horizontal)
913
         field.setContentHuggingPriority(.defaultLow, for: .horizontal)
854
         field.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
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
         ProfileLayoutEnforcement.applyLeftAlignedTextField(field)
916
         ProfileLayoutEnforcement.applyLeftAlignedTextField(field)
864
 
917
 
865
         let wrap = NSView()
918
         let wrap = NSView()
@@ -901,12 +954,13 @@ final class MyProfilePageView: NSView {
901
 
954
 
902
     private func referralBlock() -> NSView {
955
     private func referralBlock() -> NSView {
903
         let label = NSTextField(labelWithString: L("Referral (Optional)"))
956
         let label = NSTextField(labelWithString: L("Referral (Optional)"))
957
+        ProfileFormLocalization.tagLabel(label, key: "Referral (Optional)")
904
         label.font = .systemFont(ofSize: 12, weight: .medium)
958
         label.font = .systemFont(ofSize: 12, weight: .medium)
905
         label.textColor = ProfilePagePalette.secondaryText
959
         label.textColor = ProfilePagePalette.secondaryText
906
         label.translatesAutoresizingMaskIntoConstraints = false
960
         label.translatesAutoresizingMaskIntoConstraints = false
907
         ProfileLayoutEnforcement.applyLeftAlignedTextField(label)
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
         let wrap = roundedFieldChrome(containing: referralField, minHeight: 40)
964
         let wrap = roundedFieldChrome(containing: referralField, minHeight: 40)
911
 
965
 
912
         let helper = NSTextField(wrappingLabelWithString: L("If someone referred you for this job, enter their name or company here"))
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
     private func workExperienceSection() -> NSView {
1002
     private func workExperienceSection() -> NSView {
949
         let title = NSTextField(labelWithString: L("Work Experience"))
1003
         let title = NSTextField(labelWithString: L("Work Experience"))
1004
+        ProfileFormLocalization.tagSection(title, key: "Work Experience")
950
         title.font = .systemFont(ofSize: 15, weight: .semibold)
1005
         title.font = .systemFont(ofSize: 15, weight: .semibold)
951
         title.textColor = ProfilePagePalette.primaryText
1006
         title.textColor = ProfilePagePalette.primaryText
952
         title.translatesAutoresizingMaskIntoConstraints = false
1007
         title.translatesAutoresizingMaskIntoConstraints = false
@@ -954,6 +1009,7 @@ final class MyProfilePageView: NSView {
954
         ProfileLayoutEnforcement.applyLeftAlignedTextField(title)
1009
         ProfileLayoutEnforcement.applyLeftAlignedTextField(title)
955
 
1010
 
956
         let addButton = NSButton(title: L("+ Add Another"), target: self, action: #selector(didTapAddWorkExperience))
1011
         let addButton = NSButton(title: L("+ Add Another"), target: self, action: #selector(didTapAddWorkExperience))
1012
+        ProfileFormLocalization.tagButton(addButton, key: "+ Add Another")
957
         addButton.translatesAutoresizingMaskIntoConstraints = false
1013
         addButton.translatesAutoresizingMaskIntoConstraints = false
958
         addButton.bezelStyle = .rounded
1014
         addButton.bezelStyle = .rounded
959
         addButton.isBordered = true
1015
         addButton.isBordered = true
@@ -994,6 +1050,7 @@ final class MyProfilePageView: NSView {
994
 
1050
 
995
     private func educationSection() -> NSView {
1051
     private func educationSection() -> NSView {
996
         let title = NSTextField(labelWithString: L("Education"))
1052
         let title = NSTextField(labelWithString: L("Education"))
1053
+        ProfileFormLocalization.tagSection(title, key: "Education")
997
         title.font = .systemFont(ofSize: 15, weight: .semibold)
1054
         title.font = .systemFont(ofSize: 15, weight: .semibold)
998
         title.textColor = ProfilePagePalette.primaryText
1055
         title.textColor = ProfilePagePalette.primaryText
999
         title.translatesAutoresizingMaskIntoConstraints = false
1056
         title.translatesAutoresizingMaskIntoConstraints = false
@@ -1001,6 +1058,7 @@ final class MyProfilePageView: NSView {
1001
         ProfileLayoutEnforcement.applyLeftAlignedTextField(title)
1058
         ProfileLayoutEnforcement.applyLeftAlignedTextField(title)
1002
 
1059
 
1003
         let addButton = NSButton(title: L("+ Add Another"), target: self, action: #selector(didTapAddEducation))
1060
         let addButton = NSButton(title: L("+ Add Another"), target: self, action: #selector(didTapAddEducation))
1061
+        ProfileFormLocalization.tagButton(addButton, key: "+ Add Another")
1004
         addButton.translatesAutoresizingMaskIntoConstraints = false
1062
         addButton.translatesAutoresizingMaskIntoConstraints = false
1005
         addButton.bezelStyle = .rounded
1063
         addButton.bezelStyle = .rounded
1006
         addButton.isBordered = true
1064
         addButton.isBordered = true
@@ -1259,9 +1317,11 @@ private final class WorkExperienceEntryView: NSView {
1259
         if #available(macOS 11.0, *) {
1317
         if #available(macOS 11.0, *) {
1260
             deleteButton.image = NSImage(systemSymbolName: "trash", accessibilityDescription: L("Remove experience"))
1318
             deleteButton.image = NSImage(systemSymbolName: "trash", accessibilityDescription: L("Remove experience"))
1261
             deleteButton.imagePosition = .imageOnly
1319
             deleteButton.imagePosition = .imageOnly
1320
+            ProfileFormLocalization.tagButton(deleteButton, key: "Remove experience")
1262
         } else {
1321
         } else {
1263
             deleteButton.title = L("Remove")
1322
             deleteButton.title = L("Remove")
1264
             deleteButton.font = .systemFont(ofSize: 12, weight: .medium)
1323
             deleteButton.font = .systemFont(ofSize: 12, weight: .medium)
1324
+            ProfileFormLocalization.tagButton(deleteButton, key: "Remove")
1265
         }
1325
         }
1266
 
1326
 
1267
         let headerSpacer = NSView()
1327
         let headerSpacer = NSView()
@@ -1276,15 +1336,27 @@ private final class WorkExperienceEntryView: NSView {
1276
         ProfileLayoutEnforcement.applyForcedLTR(to: headerRow)
1336
         ProfileLayoutEnforcement.applyForcedLTR(to: headerRow)
1277
         headerRow.translatesAutoresizingMaskIntoConstraints = false
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
         jobCompanyRow = ProfileDualFieldRow(left: jobGroup, right: companyGroup, spacing: 12)
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
         let descriptionGroup = Self.multilineLabeledStack(
1356
         let descriptionGroup = Self.multilineLabeledStack(
1285
-            title: L("Description"),
1357
+            labelKey: "Description",
1286
             field: descriptionField,
1358
             field: descriptionField,
1287
-            placeholder: L("Describe your responsibilities and achievements..."),
1359
+            placeholderKey: "Describe your responsibilities and achievements...",
1288
             minHeight: 120
1360
             minHeight: 120
1289
         )
1361
         )
1290
 
1362
 
@@ -1334,14 +1406,15 @@ private final class WorkExperienceEntryView: NSView {
1334
         ProfileThemeAppearance.refreshFormSubtree(self)
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
         label.font = .systemFont(ofSize: 12, weight: .medium)
1412
         label.font = .systemFont(ofSize: 12, weight: .medium)
1340
         label.textColor = ProfilePagePalette.secondaryText
1413
         label.textColor = ProfilePagePalette.secondaryText
1341
         label.translatesAutoresizingMaskIntoConstraints = false
1414
         label.translatesAutoresizingMaskIntoConstraints = false
1342
         ProfileLayoutEnforcement.applyLeftAlignedTextField(label)
1415
         ProfileLayoutEnforcement.applyLeftAlignedTextField(label)
1343
 
1416
 
1344
-        styleSingleLineField(field, placeholder: placeholder)
1417
+        styleSingleLineField(field, placeholderKey: placeholderKey)
1345
         let wrap = roundedChrome(around: field, minHeight: 40)
1418
         let wrap = roundedChrome(around: field, minHeight: 40)
1346
 
1419
 
1347
         let stack = NSStackView(views: [label, wrap])
1420
         let stack = NSStackView(views: [label, wrap])
@@ -1362,13 +1435,15 @@ private final class WorkExperienceEntryView: NSView {
1362
         return stack
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
         label.font = .systemFont(ofSize: 12, weight: .medium)
1441
         label.font = .systemFont(ofSize: 12, weight: .medium)
1368
         label.textColor = ProfilePagePalette.secondaryText
1442
         label.textColor = ProfilePagePalette.secondaryText
1369
         label.translatesAutoresizingMaskIntoConstraints = false
1443
         label.translatesAutoresizingMaskIntoConstraints = false
1370
         ProfileLayoutEnforcement.applyLeftAlignedTextField(label)
1444
         ProfileLayoutEnforcement.applyLeftAlignedTextField(label)
1371
 
1445
 
1446
+        ProfileFormLocalization.tagPlaceholder(field, key: placeholderKey)
1372
         field.translatesAutoresizingMaskIntoConstraints = false
1447
         field.translatesAutoresizingMaskIntoConstraints = false
1373
         field.isEditable = true
1448
         field.isEditable = true
1374
         field.isSelectable = true
1449
         field.isSelectable = true
@@ -1381,14 +1456,7 @@ private final class WorkExperienceEntryView: NSView {
1381
         field.cell?.wraps = true
1456
         field.cell?.wraps = true
1382
         field.cell?.isScrollable = false
1457
         field.cell?.isScrollable = false
1383
         field.cell?.usesSingleLineMode = false
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
         ProfileLayoutEnforcement.applyLeftAlignedTextField(field)
1460
         ProfileLayoutEnforcement.applyLeftAlignedTextField(field)
1393
 
1461
 
1394
         let wrap = NSView()
1462
         let wrap = NSView()
@@ -1428,21 +1496,15 @@ private final class WorkExperienceEntryView: NSView {
1428
         return stack
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
         field.translatesAutoresizingMaskIntoConstraints = false
1501
         field.translatesAutoresizingMaskIntoConstraints = false
1433
         field.isBordered = false
1502
         field.isBordered = false
1434
         field.drawsBackground = false
1503
         field.drawsBackground = false
1435
         field.focusRingType = .none
1504
         field.focusRingType = .none
1436
         field.font = .systemFont(ofSize: 14, weight: .regular)
1505
         field.font = .systemFont(ofSize: 14, weight: .regular)
1437
         field.textColor = ProfilePagePalette.primaryText
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
         field.cell?.usesSingleLineMode = true
1508
         field.cell?.usesSingleLineMode = true
1447
         field.cell?.wraps = false
1509
         field.cell?.wraps = false
1448
         field.cell?.isScrollable = true
1510
         field.cell?.isScrollable = true
@@ -1549,9 +1611,11 @@ private final class EducationEntryView: NSView {
1549
         if #available(macOS 11.0, *) {
1611
         if #available(macOS 11.0, *) {
1550
             deleteButton.image = NSImage(systemSymbolName: "trash", accessibilityDescription: L("Remove education"))
1612
             deleteButton.image = NSImage(systemSymbolName: "trash", accessibilityDescription: L("Remove education"))
1551
             deleteButton.imagePosition = .imageOnly
1613
             deleteButton.imagePosition = .imageOnly
1614
+            ProfileFormLocalization.tagButton(deleteButton, key: "Remove education")
1552
         } else {
1615
         } else {
1553
             deleteButton.title = L("Remove")
1616
             deleteButton.title = L("Remove")
1554
             deleteButton.font = .systemFont(ofSize: 12, weight: .medium)
1617
             deleteButton.font = .systemFont(ofSize: 12, weight: .medium)
1618
+            ProfileFormLocalization.tagButton(deleteButton, key: "Remove")
1555
         }
1619
         }
1556
 
1620
 
1557
         let headerSpacer = NSView()
1621
         let headerSpacer = NSView()
@@ -1567,21 +1631,21 @@ private final class EducationEntryView: NSView {
1567
         headerRow.translatesAutoresizingMaskIntoConstraints = false
1631
         headerRow.translatesAutoresizingMaskIntoConstraints = false
1568
 
1632
 
1569
         let degreeGroup = WorkExperienceEntryView.labeledFieldStack(
1633
         let degreeGroup = WorkExperienceEntryView.labeledFieldStack(
1570
-            title: L("Degree / program *"),
1634
+            labelKey: "Degree / program *",
1571
             field: degreeField,
1635
             field: degreeField,
1572
-            placeholder: L("e.g., BSc Computer Science")
1636
+            placeholderKey: "e.g., BSc Computer Science"
1573
         )
1637
         )
1574
         let institutionGroup = WorkExperienceEntryView.labeledFieldStack(
1638
         let institutionGroup = WorkExperienceEntryView.labeledFieldStack(
1575
-            title: L("Institution *"),
1639
+            labelKey: "Institution *",
1576
             field: institutionField,
1640
             field: institutionField,
1577
-            placeholder: L("e.g., MIT")
1641
+            placeholderKey: "e.g., MIT"
1578
         )
1642
         )
1579
         degreeInstitutionRow = ProfileDualFieldRow(left: degreeGroup, right: institutionGroup, spacing: 12)
1643
         degreeInstitutionRow = ProfileDualFieldRow(left: degreeGroup, right: institutionGroup, spacing: 12)
1580
 
1644
 
1581
         let yearGroup = WorkExperienceEntryView.labeledFieldStack(
1645
         let yearGroup = WorkExperienceEntryView.labeledFieldStack(
1582
-            title: L("Year *"),
1646
+            labelKey: "Year *",
1583
             field: yearField,
1647
             field: yearField,
1584
-            placeholder: L("e.g., 2020")
1648
+            placeholderKey: "e.g., 2020"
1585
         )
1649
         )
1586
 
1650
 
1587
         let inner = NSStackView(views: [headerRow, degreeInstitutionRow, yearGroup])
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)";