codeburn/mac/Sources/CodeBurnMenubar/Data/LanguagePreference.swift
Resham Joshi 43608c2558
desktop and menu bar: six-language localization (#1493)
The desktop app and the macOS menu bar can now run in English, French, Japanese, Korean, Simplified Chinese and Traditional Chinese. A Language setting in the desktop Settings picks the language or follows the system, and the choice is shared so the menu bar switches with it, relaunching to pick up the language with a retry so a switch never leaves it down.

Every user-facing string in the desktop renderer goes through a local lookup table, 1,249 keys per language, no framework and no network. The menu bar gains four .lproj to join English and Simplified Chinese, 626 keys each. A completeness gate fails the build if any language is missing a key, in both the desktop catalogs and the menu bar coverage suite. Fonts fall back per language so Japanese, Korean and both Chinese render their correct glyph forms, and dates and numbers format for the locale with values unchanged. English output is byte-identical.

Strings produced by the CLI engine stay English until the CLI is localized, a separate effort. Validated live in all six languages: no missing text, no clipping, correct fonts, numbers unchanged, and the menu bar switching for every language.
2026-09-19 03:25:46 -07:00

60 lines
2.5 KiB
Swift

import Foundation
/// The per-app language override, written where macOS already looks for it.
///
/// System Settings > General > Language & Region > Applications writes
/// `AppleLanguages` into the application's own preferences domain, and CFBundle
/// builds its language search list from exactly that. Settings writes the same
/// key, so the two surfaces are one setting rather than two, and `L(_:)` (which
/// resolves against `Bundle.module`) follows either.
///
/// The current value is read back from the app's own persistent domain, never
/// from `UserDefaults.standard.array(forKey:)`: that read falls through to the
/// global domain and would report the *system* language as this app's override,
/// so `.system` could never be shown as selected (#1244).
enum LanguagePreference: String, CaseIterable, Identifiable, Sendable {
case system
case english = "en"
case french = "fr"
case japanese = "ja"
case korean = "ko"
case chineseSimplified = "zh-Hans"
case chineseTraditional = "zh-Hant"
var id: String { rawValue }
var displayLabel: String {
switch self {
case .system: L("System")
// Each language names itself: a picker a user opens because the UI is
// in a language they cannot read is no use in that language.
case .english: "English"
case .french: "Français"
case .japanese: "日本語"
case .korean: "한국어"
case .chineseSimplified: "简体中文"
case .chineseTraditional: "繁體中文"
}
}
static let defaultsKey = "AppleLanguages"
/// What the app's own domain currently says. Absent means "follow the
/// system", and a language the app does not ship also reads as system:
/// that is what the bundle will actually resolve to.
static func current(defaults: UserDefaults = .standard, bundleID: String? = Bundle.main.bundleIdentifier) -> LanguagePreference {
guard let bundleID,
let stored = defaults.persistentDomain(forName: bundleID)?[defaultsKey] as? [String],
let first = stored.first,
let match = allCases.first(where: { $0 != .system && $0.rawValue == first })
else { return .system }
return match
}
static func apply(_ preference: LanguagePreference, defaults: UserDefaults = .standard) {
switch preference {
case .system: defaults.removeObject(forKey: defaultsKey)
default: defaults.set([preference.rawValue], forKey: defaultsKey)
}
}
}