Files
steipete--codexbar/Tests/CodexBarTests/LocalizationLanguageCatalogTests.swift
2026-07-13 12:22:33 +08:00

618 lines
26 KiB
Swift
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import Foundation
import Testing
@testable import CodexBar
struct LocalizationLanguageCatalogTests {
private let languageKeys = [
"language_system",
"language_english",
"language_german",
"language_spanish",
"language_catalan",
"language_chinese_simplified",
"language_chinese_traditional",
"language_portuguese_brazilian",
"language_swedish",
"language_french",
"language_dutch",
"language_ukrainian",
"language_russian",
"language_italian",
"language_vietnamese",
"language_japanese",
"language_korean",
"language_turkish",
"language_indonesian",
"language_polish",
"language_arabic",
"language_persian",
"language_thai",
"language_galician",
]
@Test
func `app language catalog includes Ukrainian`() {
#expect(AppLanguage.allCases.contains(.ukrainian))
#expect(AppLanguage.ukrainian.rawValue == "uk")
}
@Test
func `app language catalog includes Russian`() {
#expect(AppLanguage.allCases.contains(.russian))
#expect(AppLanguage.russian.rawValue == "ru")
}
@Test
func `app language catalog includes Korean`() {
#expect(AppLanguage.allCases.contains(.korean))
#expect(AppLanguage.korean.rawValue == "ko")
}
@Test
func `app language catalog includes Turkish`() {
#expect(AppLanguage.allCases.contains(.turkish))
#expect(AppLanguage.turkish.rawValue == "tr")
}
@Test
func `app language catalog includes Italian`() {
#expect(AppLanguage.allCases.contains(.italian))
#expect(AppLanguage.italian.rawValue == "it")
}
@Test
func `app language catalog includes Indonesian`() {
#expect(AppLanguage.allCases.contains(.indonesian))
#expect(AppLanguage.indonesian.rawValue == "id")
}
@Test
func `app language catalog includes Polish`() {
#expect(AppLanguage.allCases.contains(.polish))
#expect(AppLanguage.polish.rawValue == "pl")
}
@Test
func `app language catalog includes Arabic Persian and Thai`() {
#expect(AppLanguage.arabic.rawValue == "ar")
#expect(AppLanguage.persian.rawValue == "fa")
#expect(AppLanguage.thai.rawValue == "th")
}
@Test
func `app language catalog includes Galician`() {
#expect(AppLanguage.allCases.contains(.galician))
#expect(AppLanguage.galician.rawValue == "gl")
}
@Test
func `language picker labels use stable native names`() {
let expected: [AppLanguage: String] = [
.system: "System",
.english: "English",
.chineseSimplified: "简体中文",
.chineseTraditional: "繁體中文",
.japanese: "日本語",
.spanish: "Español",
.portugueseBrazilian: "Português (Brasil)",
.korean: "한국어",
.german: "Deutsch",
.french: "Français",
.arabic: "العربية",
.italian: "Italiano",
.vietnamese: "Tiếng Việt",
.dutch: "Nederlands",
.turkish: "Türkçe",
.ukrainian: "Українська",
.russian: "Русский",
.indonesian: "Bahasa Indonesia",
.polish: "Polski",
.persian: "فارسی",
.thai: "ไทย",
.galician: "Galego",
.catalan: "Català",
.swedish: "Svenska",
]
#expect(expected.count == AppLanguage.allCases.count)
let japaneseLabels = CodexBarLocalizationOverride.$appLanguage.withValue("ja") {
Dictionary(uniqueKeysWithValues: AppLanguage.allCases.map { ($0, $0.label) })
}
let arabicLabels = CodexBarLocalizationOverride.$appLanguage.withValue("ar") {
Dictionary(uniqueKeysWithValues: AppLanguage.allCases.map { ($0, $0.label) })
}
#expect(japaneseLabels == expected)
#expect(arabicLabels == expected)
}
@Test
func `system language preserves an external Apple Languages override`() {
Self.withTemporaryDefaults(for: #function) { defaults, _ in
defaults.set(["de"], forKey: "AppleLanguages")
AppLanguagePreferenceMigration.clearLegacyOverrideIfOwned(
storedAppLanguage: "",
defaults: defaults)
#expect(defaults.stringArray(forKey: "AppleLanguages") == ["de"])
}
}
@Test
func `matching legacy language override is cleared`() {
Self.withTemporaryDefaults(for: #function) { defaults, suiteName in
defaults.set(["ja"], forKey: "AppleLanguages")
AppLanguagePreferenceMigration.clearLegacyOverrideIfOwned(
storedAppLanguage: "ja",
defaults: defaults)
#expect(defaults.persistentDomain(forName: suiteName)?["AppleLanguages"] == nil)
}
}
@Test
func `unrelated external language override is preserved`() {
Self.withTemporaryDefaults(for: #function) { defaults, _ in
defaults.set(["de"], forKey: "AppleLanguages")
AppLanguagePreferenceMigration.clearLegacyOverrideIfOwned(
storedAppLanguage: "ja",
defaults: defaults)
#expect(defaults.stringArray(forKey: "AppleLanguages") == ["de"])
}
}
@Test
func `new language bundles include representative native labels`() throws {
let root = URL(fileURLWithPath: #filePath)
.deletingLastPathComponent()
.deletingLastPathComponent()
.deletingLastPathComponent()
let resourcesURL = root.appendingPathComponent("Sources/CodexBar/Resources")
let expectations: [String: [String: String]] = [
"ar": [
"language_arabic": "العربية",
"tab_general": "عام",
"quit_app": "إنهاء CodexBar",
"usage_percent_suffix_left": "متبقٍ",
],
"fa": [
"language_persian": "فارسی",
"tab_general": "عمومی",
"quit_app": "خروج از CodexBar",
"usage_percent_suffix_left": "باقی مانده",
],
"th": [
"language_thai": "ไทย",
"tab_general": "ทั่วไป",
"quit_app": "ออกจาก CodexBar",
"usage_percent_suffix_left": "คงเหลือ",
],
"ru": [
"language_russian": "Русский",
"tab_general": "Общие",
"quit_app": "Выйти из CodexBar",
"usage_percent_suffix_left": "осталось",
],
"gl": [
"language_galician": "Galego",
"tab_general": "Xeral",
"quit_app": "Saír de CodexBar",
"terminal_app_title": "Terminal predeterminado",
"terminal_app_subtitle": "Terminal usado pola acción Abrir terminal",
],
"ca": [
"A managed Codex login is already running. Wait for it to finish before adding ":
"Ja hi ha un inici de sessió gestionat de Codex en curs. Espereu que acabi abans d'afegir ",
"%@: %@": "%@: %@",
"Sign in with Claude Code...": "Inicia sessió amb Claude Code...",
"keychain_access_caption":
"Desactiveu totes les lectures i escriptures del Clauer. " +
"Feu-ho si macOS continua mostrant sol·licituds de «Chrome/Brave/Edge Safe Storage» " +
"fins i tot després de triar «Permet sempre». La importació de galetes del navegador no " +
"estarà disponible mentre aquesta opció estigui activada; enganxeu manualment les " +
"capçaleres Cookie a Proveïdors. L'OAuth de Claude/Codex mitjançant la CLI continuarà funcionant.",
"language_catalan": "Català",
"menu_bar_metric_subtitle_mistral":
"Trieu entre la despesa de l'API de Mistral i l'ús del Monthly Plan per a la barra de menús.",
"quota_warning_notifications_subtitle":
"Avisa quan la quota restant de sessió o setmanal baixa per sota dels llindars configurats.",
"refresh_on_open_subtitle":
"Obté l'ús més recent de cada proveïdor cada vegada que obriu el menú.",
],
]
for (locale, expectedValues) in expectations {
let url = resourcesURL.appendingPathComponent("\(locale).lproj/Localizable.strings")
let catalog = try #require(NSDictionary(contentsOf: url) as? [String: String])
for (key, expectedValue) in expectedValues {
#expect(catalog[key] == expectedValue, "\(locale).\(key)")
}
}
}
@Test
func `german manual action labels do not describe a handbook`() throws {
let root = URL(fileURLWithPath: #filePath)
.deletingLastPathComponent()
.deletingLastPathComponent()
.deletingLastPathComponent()
let germanURL = root.appendingPathComponent("Sources/CodexBar/Resources/de.lproj/Localizable.strings")
let german = try #require(NSDictionary(contentsOf: germanURL) as? [String: String])
#expect(german["Manual"] == "Manuell")
#expect(german["refresh_manual"] == "Manuell")
}
@Test
func `galician localization matches the English catalog`() throws {
let root = URL(fileURLWithPath: #filePath)
.deletingLastPathComponent()
.deletingLastPathComponent()
.deletingLastPathComponent()
let resourcesURL = root.appendingPathComponent("Sources/CodexBar/Resources")
let englishURL = resourcesURL.appendingPathComponent("en.lproj/Localizable.strings")
let galicianURL = resourcesURL.appendingPathComponent("gl.lproj/Localizable.strings")
let english = try #require(NSDictionary(contentsOf: englishURL) as? [String: String])
let galician = try #require(NSDictionary(contentsOf: galicianURL) as? [String: String])
#expect(Set(galician.keys) == Set(english.keys))
}
@Test
func `catalan localization matches the English catalog`() throws {
let root = URL(fileURLWithPath: #filePath)
.deletingLastPathComponent()
.deletingLastPathComponent()
.deletingLastPathComponent()
let resourcesURL = root.appendingPathComponent("Sources/CodexBar/Resources")
let englishURL = resourcesURL.appendingPathComponent("en.lproj/Localizable.strings")
let catalanURL = resourcesURL.appendingPathComponent("ca.lproj/Localizable.strings")
let english = try #require(NSDictionary(contentsOf: englishURL) as? [String: String])
let catalan = try #require(NSDictionary(contentsOf: catalanURL) as? [String: String])
#expect(Set(catalan.keys) == Set(english.keys))
let statusFormat = try #require(catalan["%@: %@"])
#expect(String(format: statusFormat, "Quota", "42") == "Quota: 42")
}
@Test
func `localized catalogs include every app language label`() throws {
#expect(self.languageKeys.count == AppLanguage.allCases.count)
let root = URL(fileURLWithPath: #filePath)
.deletingLastPathComponent()
.deletingLastPathComponent()
.deletingLastPathComponent()
let resourcesURL = root.appendingPathComponent("Sources/CodexBar/Resources")
let catalogs = try FileManager.default.contentsOfDirectory(
at: resourcesURL,
includingPropertiesForKeys: nil)
.filter { $0.pathExtension == "lproj" }
for catalogURL in catalogs {
let stringsURL = catalogURL.appendingPathComponent("Localizable.strings")
let contents = try String(contentsOf: stringsURL, encoding: .utf8)
for key in self.languageKeys {
#expect(contents.contains("\"\(key)\""), "Missing \(key) in \(catalogURL.lastPathComponent)")
}
}
}
@Test
func `localized catalogs include workday pace setting copy`() throws {
let root = URL(fileURLWithPath: #filePath)
.deletingLastPathComponent()
.deletingLastPathComponent()
.deletingLastPathComponent()
let resourcesURL = root.appendingPathComponent("Sources/CodexBar/Resources")
let catalogs = try FileManager.default.contentsOfDirectory(
at: resourcesURL,
includingPropertiesForKeys: nil)
.filter { $0.pathExtension == "lproj" }
for catalogURL in catalogs {
let stringsURL = catalogURL.appendingPathComponent("Localizable.strings")
let catalog = try #require(NSDictionary(contentsOf: stringsURL) as? [String: String])
let title = catalog["weekly_progress_work_days_title"]?.trimmingCharacters(in: .whitespacesAndNewlines)
let subtitle = catalog["weekly_progress_work_days_subtitle"]?
.trimmingCharacters(in: .whitespacesAndNewlines)
#expect(title?.isEmpty == false, "Missing workday title in \(catalogURL.lastPathComponent)")
#expect(subtitle?.isEmpty == false, "Missing workday subtitle in \(catalogURL.lastPathComponent)")
}
}
@Test
func `localized catalogs include default terminal setting copy`() throws {
let root = URL(fileURLWithPath: #filePath)
.deletingLastPathComponent()
.deletingLastPathComponent()
.deletingLastPathComponent()
let resourcesURL = root.appendingPathComponent("Sources/CodexBar/Resources")
let catalogs = try FileManager.default.contentsOfDirectory(
at: resourcesURL,
includingPropertiesForKeys: nil)
.filter { $0.pathExtension == "lproj" }
for catalogURL in catalogs {
let stringsURL = catalogURL.appendingPathComponent("Localizable.strings")
let catalog = try #require(NSDictionary(contentsOf: stringsURL) as? [String: String])
let title = catalog["terminal_app_title"]?.trimmingCharacters(in: .whitespacesAndNewlines)
let subtitle = catalog["terminal_app_subtitle"]?.trimmingCharacters(in: .whitespacesAndNewlines)
#expect(title?.isEmpty == false, "Missing default terminal title in \(catalogURL.lastPathComponent)")
#expect(subtitle?.isEmpty == false, "Missing default terminal subtitle in \(catalogURL.lastPathComponent)")
}
}
@Test
func `ukrainian localization bundle exists and contains key UI labels`() throws {
let root = URL(fileURLWithPath: #filePath)
.deletingLastPathComponent()
.deletingLastPathComponent()
.deletingLastPathComponent()
let ukURL = root.appendingPathComponent("Sources/CodexBar/Resources/uk.lproj/Localizable.strings")
let contents = try String(contentsOf: ukURL, encoding: .utf8)
let requiredKeys = [
"\"language_title\"",
"\"language_subtitle\"",
"\"language_system\"",
"\"language_ukrainian\"",
"\"tab_general\"",
"\"quit_app\"",
]
for key in requiredKeys {
#expect(contents.contains(key), "Missing localization key: \(key)")
}
}
@Test
func `korean localization bundle includes representative native labels`() throws {
let root = URL(fileURLWithPath: #filePath)
.deletingLastPathComponent()
.deletingLastPathComponent()
.deletingLastPathComponent()
let koURL = root.appendingPathComponent("Sources/CodexBar/Resources/ko.lproj/Localizable.strings")
let catalog = try #require(NSDictionary(contentsOf: koURL) as? [String: String])
#expect(catalog["language_korean"] == "한국어")
#expect(catalog["tab_general"] == "일반")
#expect(catalog["quota_warning_session"] == "세션")
#expect(catalog["quota_warning_warn_at"] == "경고 기준")
#expect(catalog["quit_app"] == "CodexBar 종료")
}
@Test
func `turkish localization matches English catalog and preserves format placeholders`() throws {
let root = URL(fileURLWithPath: #filePath)
.deletingLastPathComponent()
.deletingLastPathComponent()
.deletingLastPathComponent()
let resourcesURL = root.appendingPathComponent("Sources/CodexBar/Resources")
let enURL = resourcesURL.appendingPathComponent("en.lproj/Localizable.strings")
let trURL = resourcesURL.appendingPathComponent("tr.lproj/Localizable.strings")
let english = try #require(NSDictionary(contentsOf: enURL) as? [String: String])
let turkish = try #require(NSDictionary(contentsOf: trURL) as? [String: String])
#expect(Set(turkish.keys) == Set(english.keys))
#expect(turkish["language_turkish"] == "Türkçe")
#expect(turkish["tab_general"] == "Genel")
#expect(turkish["quit_app"] == "CodexBar'dan Çık")
#expect(turkish["display_mode_percent_desc"]?.contains("%45") == true)
#expect(turkish["session_depleted_notification_body"]?.hasPrefix("0% kaldı.") == true)
let format = try #require(turkish["quota_warning_notification_body"])
let rendered = String(
format: format,
locale: Locale(identifier: "tr_TR"),
arguments: ["%20", 15, "oturum"])
#expect(rendered.contains("15%"))
#expect(!rendered.contains("%2$d"))
let historyFormat = try #require(turkish["%@: %@%% used"])
let historyLabel = String(
format: historyFormat,
locale: Locale(identifier: "tr_TR"),
arguments: ["12 Haz", "45"])
#expect(historyLabel == "12 Haz: 45% kullanıldı")
let miniMaxFormat = try #require(turkish["minimax_used_percent_format"])
let miniMaxLabel = String(
format: miniMaxFormat,
locale: Locale(identifier: "tr_TR"),
arguments: ["45%"])
#expect(miniMaxLabel == "45% kullanıldı")
}
@Test
func `italian localization matches English catalog and includes current UI labels`() throws {
let root = URL(fileURLWithPath: #filePath)
.deletingLastPathComponent()
.deletingLastPathComponent()
.deletingLastPathComponent()
let resourcesURL = root.appendingPathComponent("Sources/CodexBar/Resources")
let enURL = resourcesURL.appendingPathComponent("en.lproj/Localizable.strings")
let itURL = resourcesURL.appendingPathComponent("it.lproj/Localizable.strings")
let english = try #require(NSDictionary(contentsOf: enURL) as? [String: String])
let italian = try #require(NSDictionary(contentsOf: itURL) as? [String: String])
#expect(Set(italian.keys) == Set(english.keys))
#expect(italian["Individual credits"] == "Crediti individuali")
#expect(italian["Workspace"] == "Spazio di lavoro")
#expect(italian["display_mode_reset_time"] == "Ora di reimpostazione")
#expect(italian["display_mode_reset_time_desc"]?.contains("↻ 15:56") == true)
#expect(italian["ory_session_…=…; csrftoken=…"] == "ory_session_…=…; csrftoken=…")
#expect(italian["quota_warning_notifications_subtitle"]?.contains("scende sotto") == true)
#expect(italian["metric_mistral_payg"] == "A consumo")
#expect(italian["metric_mistral_monthly_plan"] == "Piano mensile")
let intentionallyUnchanged: Set = [
"Account",
"Build",
"Chrome",
"Cookie: ...",
"Cookie: …",
"Deployment",
"Email",
"Endpoint",
"Gemini Flash",
"GitHub",
"Google OAuth",
"No",
"Oasis-Token",
"Password",
"Provider",
"Token",
"%@: %@",
"byte_unit_byte",
"byte_unit_gigabyte",
"byte_unit_kilobyte",
"byte_unit_megabyte",
"language_arabic",
"language_galician",
"language_italian",
"language_persian",
"language_russian",
"language_thai",
"link_email",
"link_github",
"ory_session_…=…; csrftoken=…",
"section_privacy",
"tab_menu",
]
let unchanged = Set(english.keys.filter { italian[$0] == english[$0] })
#expect(unchanged == intentionallyUnchanged)
let warningFormat = try #require(italian["quota_warning_notification_body"])
let warning = String(
format: warningFormat,
locale: Locale(identifier: "it_IT"),
arguments: ["20%", 15, "settimanale"])
#expect(warning == "Rimane 20%. Hai raggiunto la soglia di avviso del 15% per la quota settimanale.")
let titleFormat = try #require(italian["quota_warning_notification_title"])
let title = String(
format: titleFormat,
locale: Locale(identifier: "it_IT"),
arguments: ["Codex", "settimanale"])
#expect(title == "Quota settimanale di Codex quasi esaurita")
}
@Test
func `indonesian localization matches English catalog and preserves format placeholders`() throws {
let root = URL(fileURLWithPath: #filePath)
.deletingLastPathComponent()
.deletingLastPathComponent()
.deletingLastPathComponent()
let resourcesURL = root.appendingPathComponent("Sources/CodexBar/Resources")
let enURL = resourcesURL.appendingPathComponent("en.lproj/Localizable.strings")
let idURL = resourcesURL.appendingPathComponent("id.lproj/Localizable.strings")
let english = try #require(NSDictionary(contentsOf: enURL) as? [String: String])
let indonesian = try #require(NSDictionary(contentsOf: idURL) as? [String: String])
#expect(Set(indonesian.keys) == Set(english.keys))
#expect(indonesian["language_indonesian"] == "Bahasa Indonesia")
#expect(indonesian["tab_general"] == "Umum")
#expect(indonesian["quit_app"] == "Keluar CodexBar")
#expect(indonesian["30d"] == "30 hari")
#expect(indonesian["On"] == "Aktif")
#expect(indonesian["Off"] == "Nonaktif")
let warningFormat = try #require(indonesian["quota_warning_notification_body"])
let warning = String(
format: warningFormat,
locale: Locale(identifier: "id_ID"),
arguments: ["20%", 15, "sesi"])
#expect(warning.contains("15%"))
#expect(!warning.contains("%2$d"))
let historyFormat = try #require(indonesian["%@: %@%% used"])
let historyLabel = String(
format: historyFormat,
locale: Locale(identifier: "id_ID"),
arguments: ["12 Jun", "45"])
#expect(historyLabel == "12 Jun: 45% terpakai")
let daysFormat = try #require(indonesian["%dd"])
let daysLabel = String(
format: daysFormat,
locale: Locale(identifier: "id_ID"),
arguments: [30])
#expect(daysLabel == "30 hari")
}
@Test
func `polish localization matches English catalog and includes current UI labels`() throws {
let root = URL(fileURLWithPath: #filePath)
.deletingLastPathComponent()
.deletingLastPathComponent()
.deletingLastPathComponent()
let resourcesURL = root.appendingPathComponent("Sources/CodexBar/Resources")
let enURL = resourcesURL.appendingPathComponent("en.lproj/Localizable.strings")
let plURL = resourcesURL.appendingPathComponent("pl.lproj/Localizable.strings")
let english = try #require(NSDictionary(contentsOf: enURL) as? [String: String])
let polish = try #require(NSDictionary(contentsOf: plURL) as? [String: String])
#expect(Set(polish.keys) == Set(english.keys))
#expect(polish["Individual credits"] == "Kredyty indywidualne")
#expect(polish["Workspace"] == "Obszar roboczy")
#expect(polish["display_mode_reset_time"] == "Godzina resetu")
#expect(polish["display_mode_reset_time_desc"]?.contains("↻ 15:56") == true)
}
@Test
func `japanese usage chart accessibility text preserves argument meanings`() throws {
let root = URL(fileURLWithPath: #filePath)
.deletingLastPathComponent()
.deletingLastPathComponent()
.deletingLastPathComponent()
let jaURL = root.appendingPathComponent("Sources/CodexBar/Resources/ja.lproj/Localizable.strings")
let catalog = try #require(NSDictionary(contentsOf: jaURL) as? [String: String])
let format = try #require(catalog["%d days of usage data across %d services"])
let rendered = String(
format: format,
locale: Locale(identifier: "ja_JP"),
arguments: [7, 3])
#expect(rendered.contains("7日間"))
#expect(rendered.contains("3サービス"))
}
@Test
func `korean usage chart accessibility text preserves argument meanings`() throws {
let root = URL(fileURLWithPath: #filePath)
.deletingLastPathComponent()
.deletingLastPathComponent()
.deletingLastPathComponent()
let koURL = root.appendingPathComponent("Sources/CodexBar/Resources/ko.lproj/Localizable.strings")
let catalog = try #require(NSDictionary(contentsOf: koURL) as? [String: String])
let format = try #require(catalog["%d days of usage data across %d services"])
let rendered = String(
format: format,
locale: Locale(identifier: "ko_KR"),
arguments: [7, 3])
#expect(rendered.contains("7일간"))
#expect(rendered.contains("3개 서비스"))
}
private static func withTemporaryDefaults(
for testName: String,
_ body: (UserDefaults, String) -> Void)
{
let suiteName = "LocalizationLanguageCatalogTests.\(testName).\(UUID().uuidString)"
let defaults = UserDefaults(suiteName: suiteName)!
defaults.removePersistentDomain(forName: suiteName)
defer { defaults.removePersistentDomain(forName: suiteName) }
body(defaults, suiteName)
}
}