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

Add Traditional Chinese localization and fix CV Maker gallery copy.

Introduce zh-Hant strings, locale detection, and template name translation for built-in and AI catalogs. Resolve the gallery subtitle staying in English after template fetch and refresh header text on language changes.

Co-authored-by: Cursor <cursoragent@cursor.com>
AhtashamShahzad1 1 месяц назад
Родитель
Сommit
f5a78d2e8f

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

@@ -103,6 +103,7 @@
 				Base,
 				ar,
 				"zh-Hans",
+				"zh-Hant",
 			);
 			mainGroup = 27D852772FB1D367008DF557;
 			minimizedProjectReferenceProxies = 1;

+ 116 - 3
App for Indeed/Services/AppLocalization.swift

@@ -12,6 +12,7 @@ enum AppLanguage: CaseIterable {
     case english
     case arabic
     case chineseSimplified
+    case chineseTraditional
 
     var localeIdentifier: String {
         switch self {
@@ -21,12 +22,22 @@ enum AppLanguage: CaseIterable {
             return "ar"
         case .chineseSimplified:
             return "zh-Hans"
+        case .chineseTraditional:
+            return "zh-Hant"
         }
     }
 
     static var systemLanguage: AppLanguage {
         let preferred = Locale.preferredLanguages.first ?? "en"
-        if preferred.lowercased().hasPrefix("zh") {
+        let lower = preferred.lowercased()
+        if lower.hasPrefix("zh-hant")
+            || lower.hasPrefix("zh-tw")
+            || lower.hasPrefix("zh-hk")
+            || lower.hasPrefix("zh-mo")
+            || lower.contains("-hant") {
+            return .chineseTraditional
+        }
+        if lower.hasPrefix("zh") {
             return .chineseSimplified
         }
         for language in AppLanguage.allCases where preferred.hasPrefix(language.localeIdentifier) {
@@ -44,6 +55,8 @@ enum AppLanguage: CaseIterable {
             return "Arabic"
         case .chineseSimplified:
             return "Chinese (Simplified)"
+        case .chineseTraditional:
+            return "Chinese (Traditional)"
         }
     }
 }
@@ -66,11 +79,111 @@ func L(_ key: String) -> String {
     appLocalized(key, language: currentAppLanguage())
 }
 
-/// Localized CV template title; `name` is always the English localization key.
+/// Localized CV template title. Built-in templates use English keys in `Localizable.strings`;
+/// AI-generated titles fall back to per-word translation when no exact key exists.
 func localizedTemplateName(_ nameKey: String) -> String {
-    L(nameKey)
+    let language = currentAppLanguage()
+    let trimmed = nameKey.trimmingCharacters(in: .whitespacesAndNewlines)
+    guard !trimmed.isEmpty else { return nameKey }
+
+    let exact = appLocalized(trimmed, language: language)
+    if exact != trimmed { return exact }
+
+    guard language != .english else { return trimmed }
+    return translateTemplateNameByTokens(trimmed, language: language)
 }
 
+private func translateTemplateNameByTokens(_ name: String, language: AppLanguage) -> String {
+    let tokens = name.split(separator: " ").map(String.init)
+    guard !tokens.isEmpty else { return name }
+
+    let translated = tokens.map { token -> String in
+        let perWord = appLocalized(token, language: language)
+        if perWord != token { return perWord }
+        return templateNameTokenTranslation(token, language: language) ?? token
+    }
+
+    switch language {
+    case .chineseSimplified, .chineseTraditional:
+        return translated.joined()
+    case .arabic, .english:
+        return translated.joined(separator: " ")
+    }
+}
+
+/// Vocabulary for AI-invented template titles (e.g. “Creative Cascade”) when no full-string key exists.
+private func templateNameTokenTranslation(_ token: String, language: AppLanguage) -> String? {
+    let key = token.trimmingCharacters(in: .whitespacesAndNewlines)
+    guard !key.isEmpty else { return nil }
+
+    switch language {
+    case .chineseTraditional:
+        return TemplateNameTokenLexicon.zhHant[key] ?? TemplateNameTokenLexicon.zhHant[key.capitalized]
+    case .chineseSimplified:
+        return TemplateNameTokenLexicon.zhHans[key] ?? TemplateNameTokenLexicon.zhHans[key.capitalized]
+    case .arabic:
+        return TemplateNameTokenLexicon.ar[key] ?? TemplateNameTokenLexicon.ar[key.capitalized]
+    case .english:
+        return nil
+    }
+}
+
+private enum TemplateNameTokenLexicon {
+    static let zhHant: [String: String] = [
+        "AI": "人工智慧", "UI": "介面", "UX": "體驗", "ATS": "ATS",
+        "Airy": "通透", "Atlas": "地圖", "Axis": "軸線", "Bloom": "綻放",
+        "Blue": "藍", "Bold": "大膽", "Briefing": "簡報", "Cascade": "層疊",
+        "Chairman": "主席", "Charter": "憲章", "Circuit": "電路", "Clear": "清晰",
+        "Conduit": "管道", "Core": "核心", "Corporate": "企業", "Craft": "工藝",
+        "Creative": "創意", "Design": "設計", "Docket": "待辦", "Dynamo": "動力",
+        "Echo": "回音", "Edge": "邊緣", "Ember": "餘燼", "Estate": "莊園",
+        "Executive": "高階", "Facet": "刻面", "Flow": "流動", "Flux": "流變",
+        "Forge": "鍛造", "Frame": "框架", "Grid": "網格", "Harbor": "港灣",
+        "Horizon": "地平線", "Impact": "影響", "Kite": "風箏", "Lattice": "格柵",
+        "Ledger": "帳本", "Lens": "鏡頭", "Linea": "線條", "Marigold": "金盞花",
+        "Mesh": "網狀", "Minimal": "簡約", "Modern": "現代", "Mono": "單色",
+        "Monarch": "君主", "Nova": "新星", "North": "北方", "Ocean": "海洋",
+        "Path": "路徑", "Peak": "峰", "Pixel": "像素", "Pinstripe": "細條紋",
+        "Prime": "首要", "Prism": "稜鏡", "Professional": "專業", "Pulse": "脈動",
+        "Pure": "純淨", "Quorum": "法定人數", "Regent": "攝政", "River": "河",
+        "Sculptor": "雕刻", "Shift": "轉換", "Slate": "石板", "Spark": "火花",
+        "Sterling": "純正", "Stone": "石", "Studio": "工作室", "Summit": "頂峰",
+        "Swiss": "瑞士", "Swift": "迅捷", "Tabular": "表格", "Vale": "谷",
+        "Vertex": "頂點", "Wave": "波浪", "White": "白"
+    ]
+
+    static let zhHans: [String: String] = [
+        "AI": "人工智能", "UI": "界面", "UX": "体验", "ATS": "ATS",
+        "Airy": "通透", "Atlas": "地图", "Axis": "轴线", "Bloom": "绽放",
+        "Blue": "蓝", "Bold": "大胆", "Briefing": "简报", "Cascade": "层叠",
+        "Chairman": "主席", "Charter": "宪章", "Circuit": "电路", "Clear": "清晰",
+        "Conduit": "管道", "Core": "核心", "Corporate": "企业", "Craft": "工艺",
+        "Creative": "创意", "Design": "设计", "Docket": "待办", "Dynamo": "动力",
+        "Echo": "回音", "Edge": "边缘", "Ember": "余烬", "Estate": "庄园",
+        "Executive": "高管", "Facet": "刻面", "Flow": "流动", "Flux": "流变",
+        "Forge": "锻造", "Frame": "框架", "Grid": "网格", "Harbor": "港湾",
+        "Horizon": "地平线", "Impact": "影响", "Kite": "风筝", "Lattice": "格栅",
+        "Ledger": "账本", "Lens": "镜头", "Linea": "线条", "Marigold": "金盏花",
+        "Mesh": "网状", "Minimal": "简约", "Modern": "现代", "Mono": "单色",
+        "Monarch": "君主", "Nova": "新星", "North": "北方", "Ocean": "海洋",
+        "Path": "路径", "Peak": "峰", "Pixel": "像素", "Pinstripe": "细条纹",
+        "Prime": "首要", "Prism": "棱镜", "Professional": "专业", "Pulse": "脉动",
+        "Pure": "纯净", "Quorum": "法定人数", "Regent": "摄政", "River": "河",
+        "Sculptor": "雕刻", "Shift": "转换", "Slate": "石板", "Spark": "火花",
+        "Sterling": "纯正", "Stone": "石", "Studio": "工作室", "Summit": "顶峰",
+        "Swiss": "瑞士", "Swift": "迅捷", "Tabular": "表格", "Vale": "谷",
+        "Vertex": "顶点", "Wave": "波浪", "White": "白"
+    ]
+
+    static let ar: [String: String] = [
+        "Creative": "إبداعي", "Cascade": "تتالي", "Design": "تصميم", "Dynamo": "ديناميكي",
+        "Modern": "عصري", "Professional": "احترافي", "Executive": "تنفيذي", "Minimal": "بسيط",
+        "Ocean": "محيط", "Blue": "أزرق", "Summit": "قمة", "Horizon": "أفق", "Harbor": "ميناء",
+        "Studio": "استوديو", "Craft": "حرفة", "Sculptor": "نحات", "UI": "واجهة"
+    ]
+}
+
+
 @MainActor
 final class AppLanguageManager {
     static let shared = AppLanguageManager()

+ 56 - 17
App for Indeed/Services/CVTemplateFetchService.swift

@@ -30,15 +30,35 @@ final class CVTemplateFetchService {
         `minimal`, `professional`, and `executive` appear under Profession-Based (ATS-friendly / corporate / leadership). \
         Output must strictly match the JSON schema — no markdown or extra keys.
         """
-        static let userInput = """
-        Generate the template catalog now. Exactly six entries per family: professional, modern, creative, minimal, executive. \
-        Use both singleColumn and twoColumn layouts across the 30 rows. For twoColumn rows vary leading vs trailing sidebars and tinted true/false. \
-        Keep modern and creative entries suitable for design-led résumés; keep minimal, professional, and executive suitable for traditional industries.
-        """
+        static func userInput(language: AppLanguage) -> String {
+            """
+            Generate the template catalog now. Exactly six entries per family: professional, modern, creative, minimal, executive. \
+            Use both singleColumn and twoColumn layouts across the 30 rows. For twoColumn rows vary leading vs trailing sidebars and tinted true/false. \
+            Keep modern and creative entries suitable for design-led résumés; keep minimal, professional, and executive suitable for traditional industries. \
+            \(displayNameRule(for: language))
+            """
+        }
+
+        /// English `name` values double as localization keys in the app; keep them ASCII words.
+        static func displayNameRule(for language: AppLanguage) -> String {
+            switch language {
+            case .english:
+                return "Each `name` must be 1–3 English words (ASCII letters and spaces only), e.g. \"Creative Cascade\"."
+            case .chineseTraditional:
+                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 Chinese characters in `name`."
+            case .chineseSimplified:
+                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`."
+            case .arabic:
+                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`."
+            }
+        }
     }
 
     /// Loads templates from OpenAI (one automatic retry on transient network / parse failures).
-    func fetchTemplates(completion: @escaping (Result<[CVTemplate], Error>) -> Void) {
+    func fetchTemplates(
+        language: AppLanguage = currentAppLanguage(),
+        completion: @escaping (Result<[CVTemplate], Error>) -> Void
+    ) {
         let apiKey = OpenAIConfiguration.apiKey
         guard OpenAIConfiguration.hasAPIKey else {
             completion(.failure(Self.missingKeyError))
@@ -52,22 +72,29 @@ final class CVTemplateFetchService {
         request.timeoutInterval = 60
 
         do {
-            request.httpBody = try Self.encodeCatalogRequestBody()
+            request.httpBody = try Self.encodeCatalogRequestBody(language: language)
         } catch {
             completion(.failure(error))
             return
         }
 
         session.dataTask(with: request) { data, response, error in
-            Self.handleFetchData(data, response: response, error: error, attempt: 0, completion: completion)
+            Self.handleFetchData(
+                data,
+                response: response,
+                error: error,
+                language: language,
+                attempt: 0,
+                completion: completion
+            )
         }.resume()
     }
 
-    private static func encodeCatalogRequestBody() throws -> Data {
+    private static func encodeCatalogRequestBody(language: AppLanguage) throws -> Data {
         let payload = CVTemplateOpenAIRequest.catalogPayload(
             model: "gpt-4o-mini",
             instructions: CVTemplateCatalogPrompt.instructions,
-            input: CVTemplateCatalogPrompt.userInput
+            input: CVTemplateCatalogPrompt.userInput(language: language)
         )
         return try JSONEncoder().encode(payload)
     }
@@ -76,13 +103,14 @@ final class CVTemplateFetchService {
         _ data: Data?,
         response: URLResponse?,
         error: Error?,
+        language: AppLanguage,
         attempt: Int,
         completion: @escaping (Result<[CVTemplate], Error>) -> Void
     ) {
         if let error {
             if attempt == 0 {
                 DispatchQueue.global(qos: .utility).asyncAfter(deadline: .now() + 2.0) {
-                    refetchSameRequest(attempt: 1, completion: completion)
+                    refetchSameRequest(language: language, attempt: 1, completion: completion)
                 }
                 return
             }
@@ -92,7 +120,7 @@ final class CVTemplateFetchService {
         guard let data else {
             if attempt == 0 {
                 DispatchQueue.global(qos: .utility).asyncAfter(deadline: .now() + 2.0) {
-                    refetchSameRequest(attempt: 1, completion: completion)
+                    refetchSameRequest(language: language, attempt: 1, completion: completion)
                 }
                 return
             }
@@ -102,7 +130,7 @@ final class CVTemplateFetchService {
         if let http = response as? HTTPURLResponse, !(200...299).contains(http.statusCode) {
             if attempt == 0, (500...599).contains(http.statusCode) || http.statusCode == 429 {
                 DispatchQueue.global(qos: .utility).asyncAfter(deadline: .now() + 2.5) {
-                    refetchSameRequest(attempt: 1, completion: completion)
+                    refetchSameRequest(language: language, attempt: 1, completion: completion)
                 }
                 return
             }
@@ -138,7 +166,7 @@ final class CVTemplateFetchService {
         } catch {
             if attempt == 0 {
                 DispatchQueue.global(qos: .utility).asyncAfter(deadline: .now() + 2.0) {
-                    refetchSameRequest(attempt: 1, completion: completion)
+                    refetchSameRequest(language: language, attempt: 1, completion: completion)
                 }
                 return
             }
@@ -146,7 +174,11 @@ final class CVTemplateFetchService {
         }
     }
 
-    private static func refetchSameRequest(attempt: Int, completion: @escaping (Result<[CVTemplate], Error>) -> Void) {
+    private static func refetchSameRequest(
+        language: AppLanguage,
+        attempt: Int,
+        completion: @escaping (Result<[CVTemplate], Error>) -> Void
+    ) {
         let apiKey = OpenAIConfiguration.apiKey
         guard OpenAIConfiguration.hasAPIKey else {
             completion(.failure(missingKeyError))
@@ -157,13 +189,20 @@ final class CVTemplateFetchService {
         request.setValue("application/json", forHTTPHeaderField: "Content-Type")
         request.setValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization")
         request.timeoutInterval = 60
-        guard let body = try? encodeCatalogRequestBody() else {
+        guard let body = try? encodeCatalogRequestBody(language: language) else {
             completion(.failure(NSError(domain: "CVTemplateFetchService", code: 11, userInfo: [NSLocalizedDescriptionKey: "Could not encode request."])))
             return
         }
         request.httpBody = body
         URLSession(configuration: .ephemeral).dataTask(with: request) { data, response, error in
-            handleFetchData(data, response: response, error: error, attempt: attempt, completion: completion)
+            handleFetchData(
+                data,
+                response: response,
+                error: error,
+                language: language,
+                attempt: attempt,
+                completion: completion
+            )
         }.resume()
     }
 

+ 66 - 14
App for Indeed/Views/CVMakerPageView.swift

@@ -583,13 +583,19 @@ final class CVMakerPageView: NSView {
 
     private var appearanceObserver: NSObjectProtocol?
     private var languageObserver: NSObjectProtocol?
+    /// AI catalog keyed by `AppLanguage.localeIdentifier` so switching language can restore without a network round-trip.
+    private var aiCatalogByLocale: [String: [CVTemplate]] = [:]
+    /// Ignores stale OpenAI responses after a newer fetch or language change started.
+    private var aiCatalogFetchGeneration = 0
 
     private let pageGradientLayer = CAGradientLayer()
     private let filterChrome = NSVisualEffectView()
     private let filterStack = NSStackView()
 
-    private let titleLabel = NSTextField(labelWithString: L("Templates"))
-    private let subtitleLabel = NSTextField(labelWithString: L("Polished layouts with live previews — pick a style that fits your story."))
+    private static let gallerySubtitleKey = "Polished layouts with live previews — pick a style that fits your story."
+
+    private let titleLabel = NSTextField(labelWithString: "")
+    private let subtitleLabel = NSTextField(labelWithString: "")
     private let groupTabsRow = NSStackView()
     private let familyChipsRow = NSStackView()
     private let scrollView = NSScrollView()
@@ -644,7 +650,6 @@ final class CVMakerPageView: NSView {
         reloadFamilyChips()
         reloadTemplateGrid()
         updateSelectedChipStates()
-        beginLoadingAICatalogIfPossible()
         appearanceObserver = NotificationCenter.default.addObserver(
             forName: AppAppearanceManager.didChangeNotification,
             object: nil,
@@ -658,9 +663,11 @@ final class CVMakerPageView: NSView {
             queue: .main
         ) { [weak self] _ in
             self?.applyLocalizedStrings()
+            self?.restoreOrLoadAICatalogForCurrentLanguage()
         }
         applyCurrentAppearance()
         applyLocalizedStrings()
+        beginLoadingAICatalogIfPossible()
     }
 
     deinit {
@@ -700,9 +707,17 @@ final class CVMakerPageView: NSView {
         updateSelectedChipStates()
     }
 
-    func applyLocalizedStrings() {
+    private func gallerySubtitleText() -> String {
+        L(Self.gallerySubtitleKey)
+    }
+
+    private func applyGalleryHeaderCopy() {
         titleLabel.stringValue = L("Templates")
-        subtitleLabel.stringValue = L("Polished layouts with live previews — pick a style that fits your story.")
+        subtitleLabel.stringValue = gallerySubtitleText()
+    }
+
+    func applyLocalizedStrings() {
+        applyGalleryHeaderCopy()
         styleCTAButton(ctaButton)
         configureGroupTabs()
         reloadFamilyChips()
@@ -755,7 +770,7 @@ final class CVMakerPageView: NSView {
         subtitleLabel.font = .systemFont(ofSize: 13, weight: .regular)
         subtitleLabel.textColor = Palette.secondaryText
         subtitleLabel.alignment = .left
-        subtitleLabel.maximumNumberOfLines = 1
+        subtitleLabel.maximumNumberOfLines = 2
 
         let headerStack = NSStackView(views: [titleLabel, subtitleLabel])
         headerStack.orientation = .vertical
@@ -1105,17 +1120,49 @@ final class CVMakerPageView: NSView {
         }
     }
 
+    private func restoreOrLoadAICatalogForCurrentLanguage() {
+        aiCatalogFetchGeneration += 1
+        applyGalleryHeaderCopy()
+        let locale = currentAppLanguage().localeIdentifier
+        if let cached = aiCatalogByLocale[locale], !cached.isEmpty {
+            activeCatalog = cached
+            configureGroupTabs()
+            reloadFamilyChips()
+            reloadTemplateGrid()
+            updateSelectedChipStates()
+            return
+        }
+        beginLoadingAICatalogIfPossible()
+    }
+
     private func beginLoadingAICatalogIfPossible() {
-        guard OpenAIConfiguration.hasAPIKey else { return }
-        let defaultSubtitle = L("Polished layouts with live previews — pick a style that fits your story.")
+        guard OpenAIConfiguration.hasAPIKey else {
+            applyGalleryHeaderCopy()
+            return
+        }
+        let language = currentAppLanguage()
+        let locale = language.localeIdentifier
+        if let cached = aiCatalogByLocale[locale], !cached.isEmpty {
+            activeCatalog = cached
+            applyGalleryHeaderCopy()
+            configureGroupTabs()
+            reloadFamilyChips()
+            reloadTemplateGrid()
+            updateSelectedChipStates()
+            return
+        }
+
+        aiCatalogFetchGeneration += 1
+        let generation = aiCatalogFetchGeneration
         subtitleLabel.stringValue = L("Fetching AI-curated templates…")
-        CVTemplateFetchService.shared.fetchTemplates { [weak self] result in
+        CVTemplateFetchService.shared.fetchTemplates(language: language) { [weak self] result in
             DispatchQueue.main.async {
-                guard let self else { return }
+                guard let self, self.aiCatalogFetchGeneration == generation else { return }
                 switch result {
                 case .success(let list):
-                    self.subtitleLabel.stringValue = defaultSubtitle
+                    self.applyGalleryHeaderCopy()
                     if !list.isEmpty {
+                        self.aiCatalogByLocale[locale] = list
                         self.activeCatalog = list
                         self.configureGroupTabs()
                         self.reloadFamilyChips()
@@ -1125,7 +1172,8 @@ final class CVMakerPageView: NSView {
                 case .failure:
                     self.subtitleLabel.stringValue = L("Couldn’t load AI templates — showing the built-in gallery.")
                     DispatchQueue.main.asyncAfter(deadline: .now() + 5.5) { [weak self] in
-                        self?.subtitleLabel.stringValue = defaultSubtitle
+                        guard let self, self.aiCatalogFetchGeneration == generation else { return }
+                        self.applyGalleryHeaderCopy()
                     }
                 }
             }
@@ -1408,7 +1456,7 @@ private final class CVTemplateCard: NSView {
         preview.translatesAutoresizingMaskIntoConstraints = false
         previewSurface.addSubview(preview)
 
-        nameLabel.stringValue = template.localizedName
+        refreshLocalizedTitle()
         nameLabel.font = .systemFont(ofSize: 14, weight: .semibold)
         nameLabel.textColor = palette.primaryText
         nameLabel.isBordered = false
@@ -1416,7 +1464,6 @@ private final class CVTemplateCard: NSView {
         nameLabel.isEditable = false
         nameLabel.isSelectable = false
 
-        categoryLabel.stringValue = "\(template.category) · \(template.layoutType.gallerySubtitle)"
         categoryLabel.font = .systemFont(ofSize: 11.5, weight: .regular)
         categoryLabel.textColor = palette.secondaryText
         categoryLabel.isBordered = false
@@ -1468,6 +1515,11 @@ private final class CVTemplateCard: NSView {
         fatalError("init(coder:) has not been implemented")
     }
 
+    func refreshLocalizedTitle() {
+        nameLabel.stringValue = template.localizedName
+        categoryLabel.stringValue = "\(template.category) · \(template.layoutType.gallerySubtitle)"
+    }
+
     override func layout() {
         super.layout()
         layer?.shadowPath = CGPath(roundedRect: bounds, cornerWidth: 24, cornerHeight: 24, transform: nil)

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

@@ -0,0 +1,353 @@
+/* Localizable.strings (繁體中文) */
+
+// MARK: - 通用
+"OK" = "確定";
+"Cancel" = "取消";
+"Delete" = "刪除";
+"Remove" = "移除";
+"Dismiss" = "關閉";
+
+// MARK: - 啟動畫面
+"AI-POWERED" = "人工智慧驅動";
+"Find your perfect job with the power of AI." = "藉助人工智慧的力量,找到您的理想工作。";
+"Starting up…" = "啟動中…";
+"Loading progress" = "載入進度";
+
+// MARK: - 啟動狀態
+"Checking your Pro subscription…" = "正在檢查您的專業版訂閱…";
+"Loading premium plans from the App Store…" = "正在從 App Store 載入高級方案…";
+"Preparing your job search workspace…" = "正在準備您的工作搜尋空間…";
+"Almost ready…" = "即將就緒…";
+
+// MARK: - 側邊欄
+"Home" = "首頁";
+"Saved Jobs" = "已儲存的工作";
+"CV Maker" = "履歷製作器";
+"Profile" = "個人資料";
+"Settings" = "設定";
+"Premium" = "高級版";
+"Indeed" = "Indeed";
+"Open Indeed to search and apply for jobs" = "開啟 Indeed 搜尋並申請工作";
+
+// MARK: - 儀表板 / 首頁
+"Welcome" = "歡迎";
+"Send" = "傳送";
+"Clear chat" = "清空聊天";
+"Remove all messages and start a new conversation" = "刪除所有訊息並開始新對話";
+"Ask for roles, skills, salary, or job descriptions..." = "詢問職位、技能、薪資或職位描述...";
+"Ask AI" = "詢問人工智慧";
+"1 reply left" = "剩餘 1 次回覆";
+"Apply" = "申請";
+"Save" = "儲存";
+"Saved" = "已儲存";
+"Remove from saved" = "從已儲存中移除";
+"Show more jobs" = "顯示更多工作";
+"This area is not available in the preview build. Use Home to search jobs." = "此區域在預覽版中不可用。請使用首頁搜尋工作。";
+"Save jobs from Home to see them here." = "從首頁儲存的工作將顯示在此處。";
+"No saved jobs yet. Search on Home, then tap Save on a listing." = "暫無已儲存的工作。在首頁搜尋,然後點擊列表上的儲存。";
+"Tell me what role you want and I will return job descriptions, key skills, and a quick fit summary." = "告訴我您想要的職位,我將返回職位描述、關鍵技能和快速配對摘要。";
+"1 saved position" = "1 個已儲存職位";
+"Delete this profile?" = "刪除此個人資料?";
+"Find roles similar to: " = "尋找類似職位:";
+"Find jobs at company: " = "尋找公司職位:";
+"Find jobs that require skill: " = "尋找需要該技能的工作:";
+"match" = "配對";
+"matches" = "配對";
+
+// MARK: - 功能捷徑
+"Role" = "職位";
+"Explore similar or better job roles" = "探索相似或更好的職位";
+"Company" = "公司";
+"Find opportunities at other companies" = "尋找其他公司的機會";
+"Skill" = "技能";
+"Match jobs that fit your skills" = "配對適合您技能的工作";
+
+// MARK: - 專業版 / 訂閱
+"Upgrade to Pro" = "升級到專業版";
+"You're on Pro" = "您正在使用專業版";
+"Unlimited AI matches, smart alerts, and interview prep—all in one place." = "無限人工智慧配對、智慧提醒和面試準備——所有這些都在一處。";
+"Manage billing, renewals, and plans in Premium." = "在高級版中管理帳單、續訂和方案。";
+"Try Pro" = "試用專業版";
+"Manage Subscription" = "管理訂閱";
+"Premium Plans" = "高級方案";
+"Unlock unlimited access to premium tools and boost your productivity." = "解鎖無限存取高級工具,提升您的生產力。";
+"Continue with free plan" = "繼續使用免費方案";
+"Restore Purchase" = "回復購買";
+"You're subscribed" = "您已訂閱";
+"Thank you — Pro features are now available." = "感謝您 — 專業版功能現已可用。";
+"Pro" = "專業版";
+"Purchases restored" = "購買已回復";
+"Your subscription is active." = "您的訂閱有效。";
+"No subscription found" = "未找到訂閱";
+"There was nothing to restore for this Apple ID." = "此 Apple ID 沒有可回復的內容。";
+"Something went wrong" = "出了點問題";
+"That subscription isn’t available from the App Store right now." = "該訂閱目前無法從 App Store 取得。";
+"Unlimited AI job search on Home" = "首頁無限人工智慧工作搜尋";
+"Save jobs & open listings in-app" = "儲存工作並在應用程式內開啟列表";
+"CV Maker, profiles & PDF export" = "履歷製作器、個人資料和可攜式文件匯出";
+"Role, company & skill shortcuts" = "職位、公司和技能捷徑";
+
+// MARK: - 付費牆方案
+"Weekly" = "每週";
+"Flexible and commitment-free" = "彈性且無承諾";
+"Monthly" = "每月";
+"Balanced for regular productivity" = "為常規生產力平衡設計";
+"Yearly" = "每年";
+"Best value for long-term users" = "長期使用者的最佳價值";
+"/ week" = "/週";
+"/ month" = "/月";
+"/ year" = "/年";
+"3 days free trial" = "3天免費試用";
+"Perfect for short-term job hunts" = "非常適合短期求職";
+"Cancel anytime" = "隨時取消";
+"Best for regular job seekers" = "最適合常規求職者";
+"Priority support" = "優先支援";
+"Lowest effective monthly cost" = "最低有效月成本";
+"Ideal for long-term use" = "非常適合長期使用";
+
+// MARK: - 付費牆信任
+"Secure Payments" = "安全付款";
+"Your payment is 100% secure." = "您的付款 100% 安全。";
+"Cancel Anytime" = "隨時取消";
+"No commitment, cancel anytime." = "無承諾,隨時取消。";
+"24/7 Support" = "24/7 支援";
+"We're here to help you anytime." = "我們隨時為您提供協助。";
+"Privacy First" = "隱私優先";
+"Your data is safe with us." = "您的資料在我們這裡很安全。";
+
+// MARK: - 設定
+"Appearance" = "外觀";
+"Theme" = "主題";
+"Language" = "語言";
+"Share App" = "分享應用程式";
+"More Apps" = "更多應用程式";
+"About" = "關於";
+"Website" = "網站";
+"Support" = "支援";
+"Terms of Use" = "使用條款";
+"Privacy Policy" = "隱私權政策";
+"System" = "跟隨系統";
+"Light" = "淺色";
+"Dark" = "深色";
+
+// MARK: - 個人資料
+"Profiles" = "個人資料";
+"Add new profile" = "新增資料";
+"Create and manage CV profiles. Each profile stores your details on this Mac." = "建立和管理履歷資料。每個資料都會將您的詳細資訊儲存在此 Mac 上。";
+"No profiles yet. Tap “Add new profile” to create your first one." = "暫無個人資料。點擊「新增資料」建立您的第一個資料。";
+"Build CV" = "製作履歷";
+"Edit" = "編輯";
+"Untitled profile" = "未命名資料";
+"No contact details yet" = "暫無聯絡方式";
+"← Profiles" = "← 個人資料";
+
+// MARK: - 資料編輯器
+"Save Profile  →" = "儲存資料 →";
+"← All profiles" = "← 所有資料";
+"New profile" = "新建資料";
+"Edit profile" = "編輯資料";
+"Profile Name *" = "資料名稱 *";
+"Marketing Director Profile" = "行銷總監資料";
+"Personal Information" = "個人資訊";
+"Full Name *" = "全名 *";
+"John Doe" = "張三";
+"Email *" = "電子郵件 *";
+"john@example.com" = "zhangsan@example.com";
+"Phone" = "電話";
+"+1 (555) 123-4567" = "+886 912 345 678";
+"Job Title *" = "職位名稱 *";
+"Software Engineer" = "軟體工程師";
+"Address" = "地址";
+"123 Main St, City, State, ZIP" = "示例街道 123 號,城市,縣/市,郵遞區號";
+"Certificates / Rewards" = "證書 / 獎項";
+"List your certificates and awards..." = "列出您的證書和獎項...";
+"Interests" = "興趣愛好";
+"List your interests and hobbies..." = "列出您的興趣和愛好...";
+"Languages" = "語言";
+"List languages you speak (e.g., English - Native, Spanish - Fluent)..." = "列出您所說的語言(例如:中文 - 母語,英文 - 流利)...";
+"Career Summary" = "職業摘要";
+"Brief overview of your professional background and key achievements..." = "您的專業背景和主要成就的簡要概述...";
+"Referral (Optional)" = "推薦人(選填)";
+"Referred by (Company/Person Name)" = "推薦人(公司/個人名稱)";
+"If someone referred you for this job, enter their name or company here" = "如果有人推薦您申請此職位,請在此輸入其姓名或公司";
+"Work Experience" = "工作經歷";
+"Education" = "教育背景";
+"+ Add Another" = "+ 新增另一個";
+"Complete required fields" = "填寫必填欄位";
+"Remove experience" = "刪除經歷";
+"Remove education" = "刪除教育";
+"Company Name *" = "公司名稱 *";
+"Duration *" = "持續時間 *";
+"Description" = "描述";
+"e.g., Software Engineer" = "例如:軟體工程師";
+"e.g., Google" = "例如:Google";
+"e.g., Jan 2020 - Present" = "例如:2020年1月 - 至今";
+"Describe your responsibilities and achievements..." = "描述您的職責和成就...";
+"Degree / program *" = "學位 / 學程 *";
+"Institution *" = "學校 / 機構 *";
+"Year *" = "年份 *";
+"e.g., BSc Computer Science" = "例如:資訊工程學士";
+"e.g., MIT" = "例如:台灣大學";
+"e.g., 2020" = "例如:2020";
+"Profile name" = "資料名稱";
+"Full Name" = "全名";
+"Email" = "電子郵件";
+"Job Title" = "職位名稱";
+
+// MARK: - 履歷製作器
+"Templates" = "範本";
+"Polished layouts with live previews — pick a style that fits your story." = "精美的佈局,即時預覽 — 選擇適合您故事的風格。";
+"Use Template & Select Profile  →" = "使用範本並選擇資料 →";
+"All" = "全部";
+"No templates yet for this category." = "此類別暫無範本。";
+"Pick a template" = "選擇範本";
+"Select a template first, then choose a profile to continue." = "先選擇範本,然後選擇個人資料以繼續。";
+"Fetching AI-curated templates…" = "正在取得人工智慧策劃的範本…";
+"Couldn’t load AI templates — showing the built-in gallery." = "無法載入人工智慧範本 — 顯示內建圖庫。";
+"Design-Based" = "基於設計";
+"Profession-Based" = "基於職業";
+"Professional" = "專業";
+"Modern" = "現代";
+"Creative" = "創意";
+"Minimal" = "簡約";
+"Executive" = "高階主管";
+"ATS layout" = "ATS 佈局";
+"Sidebar left" = "左側邊欄";
+"Sidebar right" = "右側邊欄";
+
+// MARK: - 履歷預覽
+"CV preview" = "履歷預覽";
+"Export PDF…" = "匯出可攜式文件…";
+"Layout matches the CV Maker thumbnail for this template. Export a PDF that matches what you see here (fonts, columns, colours, and rules)." = "佈局與此範本的履歷製作器縮圖匹配。匯出與您在此處看到的內容匹配的可攜式文件(字型、欄、顏色和規則)。";
+"The résumé could not be rendered to PDF (empty output). Try scrolling the preview so it lays out, then export again." = "無法將履歷轉譯為可攜式文件(輸出為空)。嘗試捲動預覽使其佈局完整,然後再次匯出。";
+"Couldn’t save PDF" = "無法儲存可攜式文件";
+"Your name" = "您的姓名";
+"Professional headline" = "專業標題";
+"Experience" = "經歷";
+"Highlights" = "亮點";
+"Summary" = "摘要";
+"Contact" = "聯絡方式";
+"Skills" = "技能";
+"Tools" = "工具";
+"Languages & more" = "語言及其他";
+"Certificates" = "證書";
+"Referrals" = "推薦";
+"Professional Summary" = "專業摘要";
+"Selected Experience" = "精選經歷";
+"Core Competencies" = "核心能力";
+"Impact" = "影響力";
+"Add contact in your profile" = "在您的個人資料中加入聯絡方式";
+"Add contact details in your profile" = "在您的個人資料中加入詳細聯絡方式";
+"Add a career summary or interests in your profile to populate this column." = "在您的個人資料中加入職業摘要或興趣愛好以填充此欄位。";
+"CV" = "履歷";
+"Open to relocation" = "願意 relocate";
+"STRENGTHS" = "優勢";
+"PORTFOLIO SNAPSHOT" = "作品集概覽";
+"Close" = "關閉";
+"/ day" = "/天";
+"/ %d days" = "/%d天";
+"/ %d weeks" = "/%d週";
+"/ %d months" = "/%d個月";
+"/ %d years" = "/%d年";
+
+// MARK: - 履歷範本名稱
+"Paper White" = "純白";
+"Swiss" = "瑞士";
+"Mono" = "單色";
+"Airy" = "通透";
+"Tabular" = "表格";
+"Facet" = "刻面";
+"Corporate" = "企業";
+"Atlas" = "地圖";
+"Ledger" = "帳本";
+"Harbor" = "港灣";
+"Clear Path" = "清晰路徑";
+"Pinstripe" = "細條紋";
+"Briefing" = "簡報";
+"Quorum" = "法定人數";
+"Docket" = "待辦";
+"Conduit" = "管道";
+"Principal" = "校長";
+"Charter" = "憲章";
+"Vertex" = "頂點";
+"Linea" = "線條";
+"Prism" = "稜鏡";
+"Circuit" = "電路";
+"North" = "北方";
+"Axis" = "軸線";
+"Marigold" = "金盞花";
+"Ember" = "餘燼";
+"Lattice" = "格柵";
+"Bloom" = "綻放";
+"Studio" = "工作室";
+"Kite" = "風箏";
+"Regent" = "攝政";
+"Monarch" = "君主";
+"Sterling" = "純正";
+"Summit" = "頂峰";
+"Estate" = "莊園";
+"Chairman" = "主席";
+"Blue Ocean" = "藍海";
+
+// MARK: - 履歷示範預覽內容
+"Sarah Johnson" = "李雅婷";
+"Senior Product Manager" = "資深產品經理";
+"Group PM, Consumer Growth & Activation" = "產品經理組負責人,消費者成長與啟用";
+"Google · Mountain View, CA · 2019 – Present" = "Google · 加州山景城 · 2019 – 至今";
+"Stanford University" = "史丹佛大學";
+"M.S. Management Science & Engineering" = "管理科學與工程碩士";
+"2014 – 2016" = "2014 – 2016";
+"Mountain View, CA" = "加州山景城";
+"Product leader shipping roadmap, discovery, and analytics for high-scale consumer experiences." = "產品負責人,負責大規模消費者體驗的產品路線圖、探索和分析。";
+"Defined multi-year platform strategy with exec stakeholders and quarterly OKRs." = "與高階利益相關者共同制定多年平台策略和季度 OKR。";
+"Partnered with engineering and design to launch experiments improving activation by 12%." = "與工程和設計團隊合作推出實驗,使啟用率提高了 12%。";
+"Stood up quarterly business reviews with finance and GTM, aligning spend to north-star metrics." = "與財務和 GTM 團隊建立季度業務檢討,使支出與北極星指標對齊。";
+"Presented roadmap shifts to the leadership team and translated trade-offs into clear investment asks." = "向領導團隊展示路線圖的轉變,並將權衡轉化為清晰的投資需求。";
+"Figma · SQL · Amplitude · Jira · BigQuery" = "Figma · SQL · Amplitude · Jira · BigQuery";
+"Product Strategy" = "產品策略";
+"A/B Testing" = "A/B 測試";
+"Roadmapping" = "路線圖規劃";
+"CONTACT" = "聯絡方式";
+"SKILLS" = "技能";
+"PROFILE" = "個人資料";
+"EXPERIENCE" = "經歷";
+"EDUCATION" = "教育背景";
+"SUMMARY" = "摘要";
+"PROFESSIONAL SUMMARY" = "專業摘要";
+"SELECTED EXPERIENCE" = "精選經歷";
+"CORE COMPETENCIES" = "核心能力";
+"TOOLS" = "工具";
+"IMPACT" = "影響力";
+
+// MARK: - 工作瀏覽器
+"Return to the previous screen" = "返回上一畫面";
+
+// MARK: - 錯誤
+"We couldn't reach the server. Check your internet connection and try again." = "無法連線到伺服器。請檢查您的網路連線後再試一次。";
+"The search was cancelled. Try again when you're ready." = "搜尋已取消。準備就緒後請再試一次。";
+"Something went wrong while searching. Please try again in a moment." = "搜尋時發生錯誤。請稍後再試。";
+"Job search is unavailable." = "工作搜尋無法使用。";
+
+// MARK: - 提示
+"This profile will be removed from this Mac." = "此個人資料將從這台 Mac 上移除。";
+
+// MARK: - 格式化字串
+"Loading %@" = "正在載入 %@";
+"Loading %@. %@" = "正在載入 %@。%@";
+"Starting %@…" = "正在啟動 %@…";
+"%d replies left" = "剩餘 %d 次回覆";
+"%d saved positions" = "%d 個已儲存職位";
+"“%@” will be removed from this Mac." = "「%@」將從這台 Mac 上移除。";
+"%@ isn’t available yet" = "%@ 尚不可用";
+"I couldn't find new matches for “%@”. Try a different angle or a more specific keyword." = "我找不到「%@」的新配對。請嘗試不同的角度或更具體的關鍵字。";
+"No jobs found for “%@”. Try another title, skill, company, or location." = "找不到「%@」的工作。請嘗試其他職位名稱、技能、公司或地點。";
+"Here are %d more %@ for “%@”." = "這是「%@」的另外 %d 個 %@。";
+"Found %d %@ for “%@”. Tap Apply to open the listing or Save to revisit later." = "找到 %d 個「%@」的 %@。點擊申請開啟列表,或點擊儲存以便稍後查看。";
+"Get %@" = "取得 %@";
+"You chose the “%@” template. Tap Build CV on a profile to preview your résumé with that layout." = "您選擇了「%@」範本。點擊個人資料上的製作履歷,使用該佈局預覽您的履歷。";
+"Experience %d" = "經歷 %d";
+"Education %d" = "教育 %d";
+"Please fill in: %@." = "請填寫:%@。";
+
+// MARK: - 多行文字
+"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)" = "在目標的建置設定中新增您的 Mac App Store ID:\n• AppStoreAppID — 來自 App Store Connect 的數字應用程式 ID\n• AppStoreDeveloperID — 數字開發者 ID(用於您的其他應用程式頁面)";