// // AppLocalization.swift // App for Indeed // // English-first localization using Localizable.strings (same pattern as the LinkedIn app). // Add more locales by creating `.lproj/Localizable.strings` and extending AppLanguage. // import Foundation enum AppLanguage: CaseIterable { case english case frenchCanada case french case arabic case chineseSimplified case chineseTraditional var localeIdentifier: String { switch self { case .english: return "en" case .frenchCanada: return "fr-CA" case .french: return "fr" case .arabic: return "ar" case .chineseSimplified: return "zh-Hans" case .chineseTraditional: return "zh-Hant" } } static var systemLanguage: AppLanguage { let preferred = Locale.preferredLanguages.first ?? "en" 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 } if lower.hasPrefix("fr-ca") { return .frenchCanada } if lower.hasPrefix("fr") { return .french } for language in AppLanguage.allCases where preferred.hasPrefix(language.localeIdentifier) { return language } return .english } /// Settings language picker labels — fixed English names, not localized. var localizedDisplayName: String { switch self { case .english: return "English" case .frenchCanada: return "French (Canada)" case .french: return "French" case .arabic: return "Arabic" case .chineseSimplified: return "Chinese (Simplified)" case .chineseTraditional: return "Chinese (Traditional)" } } } func appLocalized(_ key: String, language: AppLanguage) -> String { guard let path = Bundle.main.path(forResource: language.localeIdentifier, ofType: "lproj"), let bundle = Bundle(path: path) else { return key } return bundle.localizedString(forKey: key, value: key, table: nil) } func currentAppLanguage() -> AppLanguage { let code = UserDefaults.standard.string(forKey: "com.appforindeed.preferredLanguage") ?? "en" return AppLanguage.allCases.first(where: { $0.localeIdentifier == code }) ?? .english } /// Resolves copy for the user’s currently selected language. func L(_ key: String) -> String { appLocalized(key, language: currentAppLanguage()) } /// 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 { 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, .french, .frenchCanada: 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 .french, .frenchCanada, .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() static let didChangeNotification = Notification.Name("AppLanguageManager.didChange") private enum UserDefaultsKey { static let preferredLanguage = "com.appforindeed.preferredLanguage" } var current: AppLanguage { currentAppLanguage() } func applyStoredPreferenceOnLaunch() { // UI copy uses `appLocalized`; do not drive layout from `AppleLanguages` (Arabic forces RTL and breaks CV templates). UserDefaults.standard.removeObject(forKey: "AppleLanguages") if UserDefaults.standard.string(forKey: UserDefaultsKey.preferredLanguage) == nil { setLanguage(AppLanguage.systemLanguage, notify: false) } } func setLanguage(_ language: AppLanguage, notify: Bool = true) { let code = language.localeIdentifier UserDefaults.standard.set(code, forKey: UserDefaultsKey.preferredLanguage) UserDefaults.standard.removeObject(forKey: "AppleLanguages") if notify { NotificationCenter.default.post(name: Self.didChangeNotification, object: self) } } func setLanguage(code: String, notify: Bool = true) { guard let language = AppLanguage.allCases.first(where: { $0.localeIdentifier == code }) else { return } setLanguage(language, notify: notify) } }