256 lines
12 KiB
Swift
256 lines
12 KiB
Swift
import Foundation
|
|
import TermindApplication
|
|
import TermindDomain
|
|
import TermindInfrastructure
|
|
import TermindPersistence
|
|
import TermindPresentation
|
|
import TermindRemoteInfrastructure
|
|
import Testing
|
|
|
|
@testable import Termind
|
|
|
|
struct AppLanguageTests {
|
|
@Test
|
|
func missingAndUnknownPreferencesFollowSystem() throws {
|
|
let defaults = try makeDefaults()
|
|
#expect(AppLanguage.stored(in: defaults) == .system)
|
|
|
|
defaults.set("unsupported", forKey: SettingsKey.appLanguage)
|
|
#expect(AppLanguage.stored(in: defaults) == .system)
|
|
}
|
|
|
|
@Test
|
|
func explicitLanguagesWriteCanonicalLanguageTags() throws {
|
|
let defaults = try makeDefaults()
|
|
|
|
AppLanguage.store(.english, in: defaults)
|
|
#expect(AppLanguage.stored(in: defaults) == .english)
|
|
#expect(defaults.stringArray(forKey: "AppleLanguages") == ["en"])
|
|
|
|
AppLanguage.store(.simplifiedChinese, in: defaults)
|
|
#expect(AppLanguage.stored(in: defaults) == .simplifiedChinese)
|
|
#expect(defaults.stringArray(forKey: "AppleLanguages") == ["zh-Hans"])
|
|
#expect(AppLanguage.simplifiedChinese.locale.identifier == "zh-Hans")
|
|
#expect(AppLanguage.simplifiedChinese.displayName == "简体中文")
|
|
|
|
AppLanguage.store(.traditionalChinese, in: defaults)
|
|
#expect(AppLanguage.stored(in: defaults) == .traditionalChinese)
|
|
#expect(defaults.stringArray(forKey: "AppleLanguages") == ["zh-Hant"])
|
|
#expect(AppLanguage.traditionalChinese.locale.identifier == "zh-Hant")
|
|
#expect(AppLanguage.traditionalChinese.displayName == "繁體中文")
|
|
}
|
|
|
|
@Test(arguments: ["zh-Hans-CN", "zh-CN", "zh_CN", "zh-Hans-SG", "zh"])
|
|
func legacySimplifiedChinesePreferencesMigrateToCanonicalTag(identifier: String) throws {
|
|
let defaults = try makeDefaults()
|
|
defaults.set(identifier, forKey: SettingsKey.appLanguage)
|
|
defaults.set([identifier], forKey: "AppleLanguages")
|
|
|
|
#expect(AppLanguage.stored(in: defaults) == .simplifiedChinese)
|
|
#expect(defaults.string(forKey: SettingsKey.appLanguage) == "zh-Hans")
|
|
#expect(defaults.stringArray(forKey: "AppleLanguages") == ["zh-Hans"])
|
|
}
|
|
|
|
@Test(arguments: ["zh-TW", "zh-Hant-TW", "zh-HK", "zh-MO", "unsupported"])
|
|
func unknownPreferencesContinueToFollowSystemWithoutMigration(identifier: String) throws {
|
|
let defaults = try makeDefaults()
|
|
defaults.set(identifier, forKey: SettingsKey.appLanguage)
|
|
|
|
#expect(AppLanguage.stored(in: defaults) == .system)
|
|
#expect(defaults.string(forKey: SettingsKey.appLanguage) == identifier)
|
|
}
|
|
|
|
@Test
|
|
func traditionalChineseDisplayNameRequiresTaiwanRegionAndVisibleFlag() {
|
|
#expect(
|
|
AppLanguage.traditionalChinese.displayName(deviceRegion: "TW", canDisplayTaiwanFlag: true)
|
|
== "正體中文")
|
|
#expect(
|
|
AppLanguage.traditionalChinese.displayName(deviceRegion: "TW", canDisplayTaiwanFlag: false)
|
|
== "繁體中文")
|
|
#expect(
|
|
AppLanguage.traditionalChinese.displayName(deviceRegion: "HK", canDisplayTaiwanFlag: true)
|
|
== "繁體中文")
|
|
#expect(
|
|
AppLanguage.traditionalChinese.displayName(deviceRegion: nil, canDisplayTaiwanFlag: false)
|
|
== "繁體中文")
|
|
#expect(AppLanguage.english.displayName(deviceRegion: "TW", canDisplayTaiwanFlag: true) == "English")
|
|
}
|
|
|
|
@Test(arguments: ["zh-TW", "zh-Hant-TW", "zh-HK", "zh-MO"])
|
|
func traditionalChineseSystemLocalesResolveTraditionalCatalog(identifier: String) {
|
|
#expect(AppLocalization.string("Settings", locale: Locale(identifier: identifier)) == "設定")
|
|
}
|
|
|
|
@Test
|
|
func simplifiedAndTraditionalChineseCatalogsDoNotCrossFallback() {
|
|
#expect(AppLocalization.string("Settings", locale: Locale(identifier: "zh-Hans")) == "设置")
|
|
#expect(AppLocalization.string("Settings", locale: Locale(identifier: "zh-Hant")) == "設定")
|
|
}
|
|
|
|
@Test(arguments: [("zh-Hans", "词元"), ("zh-Hant", "符元")])
|
|
func assistantTokenCountsUseModelTerminology(language: String, unit: String) {
|
|
let locale = Locale(identifier: language)
|
|
let prefix = "≈"
|
|
let count = "1.2k"
|
|
#expect(AppLocalization.string("\(prefix)\(count) tokens", locale: locale) == "≈1.2k\(unit)")
|
|
for key: String.LocalizationValue in ["Token", "Bearer Token", "Clear Token", "Generate Token"] {
|
|
#expect(!AppLocalization.string(key, locale: locale).contains(unit))
|
|
}
|
|
}
|
|
|
|
@Test(
|
|
arguments: [AppLanguage.english, .simplifiedChinese, .traditionalChinese],
|
|
[ProPurchasePlan.annual, .lifetime])
|
|
func proOfferUsesTheExplicitAppLanguage(language: AppLanguage, plan: ProPurchasePlan) throws {
|
|
let locale = language.locale
|
|
let price = "$12.34"
|
|
let offer = ProOfferPresentation(
|
|
plan: plan, annualPrice: price, billingPeriod: DateComponents(year: 1),
|
|
trialDuration: DateComponents(day: 7), canStartTrial: true, locale: locale
|
|
) { AppLocalization.string($0, locale: locale) }
|
|
let title: String.LocalizationValue = plan == .annual ? "Start Free Trial" : "Buy Lifetime Access"
|
|
#expect(offer.purchaseButtonTitle == AppLocalization.string(title, locale: locale))
|
|
#expect(offer.annualBillingUnit == AppLocalization.string("year", locale: locale))
|
|
#expect(!(try #require(offer.trialBadge)).isEmpty)
|
|
#expect(!(try #require(offer.selectedPlanDetail)).isEmpty)
|
|
if plan == .annual { #expect(offer.selectedPlanDetail?.contains(price) == true) }
|
|
if language != .english {
|
|
#expect(offer.purchaseButtonTitle != AppLocalization.string(title, locale: AppLanguage.english.locale))
|
|
}
|
|
}
|
|
|
|
@Test
|
|
func followSystemRemovesAppleLanguageOverride() throws {
|
|
let suiteName = "AppLanguageTests.\(UUID().uuidString)"
|
|
let defaults = try #require(UserDefaults(suiteName: suiteName))
|
|
defaults.removePersistentDomain(forName: suiteName)
|
|
defaults.set(["zh-Hans"], forKey: "AppleLanguages")
|
|
|
|
AppLanguage.store(.system, in: defaults)
|
|
|
|
#expect(AppLanguage.stored(in: defaults) == .system)
|
|
#expect(defaults.persistentDomain(forName: suiteName)?["AppleLanguages"] == nil)
|
|
}
|
|
|
|
@Test
|
|
func everyCatalogEntryHasFinishedChineseTranslations() throws {
|
|
let repository = URL(fileURLWithPath: #filePath)
|
|
.deletingLastPathComponent().deletingLastPathComponent()
|
|
for name in ["Localizable", "InfoPlist"] {
|
|
let url = repository.appending(path: "Frameworks/Resources/\(name).xcstrings")
|
|
let root = try #require(
|
|
JSONSerialization.jsonObject(with: Data(contentsOf: url))
|
|
as? [String: Any]
|
|
)
|
|
let strings = try #require(root["strings"] as? [String: Any])
|
|
for (key, value) in strings {
|
|
let entry = try #require(value as? [String: Any])
|
|
if entry["shouldTranslate"] as? Bool == false { continue }
|
|
let localizations = try #require(entry["localizations"] as? [String: Any])
|
|
for language in ["zh-Hans", "zh-Hant"] {
|
|
let chinese = try #require(localizations[language] as? [String: Any])
|
|
let units = localizedUnits(in: chinese)
|
|
#expect(!units.isEmpty, "No translated values for \(language): \(key)")
|
|
for unit in units {
|
|
#expect(unit["state"] as? String == "translated", "Incomplete \(language): \(key)")
|
|
#expect(!(unit["value"] as? String ?? "").isEmpty || key.isEmpty)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@Test
|
|
func forwardingTemplatesPreserveArgumentsInOrder() throws {
|
|
let repository = URL(fileURLWithPath: #filePath)
|
|
.deletingLastPathComponent().deletingLastPathComponent()
|
|
let url = repository.appending(path: "Frameworks/Resources/Localizable.xcstrings")
|
|
let root = try #require(
|
|
JSONSerialization.jsonObject(with: Data(contentsOf: url)) as? [String: Any]
|
|
)
|
|
let strings = try #require(root["strings"] as? [String: Any])
|
|
let interpolations: [String: [String]] = [
|
|
"SSH %@ → This Mac %@": ["[::1]:8080", "[2001:db8::1]:443"],
|
|
"Unable to listen on %@: %@": ["[::1]:8080", "Address already in use"],
|
|
]
|
|
|
|
for (key, arguments) in interpolations.sorted(by: { $0.key < $1.key }) {
|
|
let entry = try #require(strings[key] as? [String: Any], "Missing forwarding template: \(key)")
|
|
let localizations = try #require(entry["localizations"] as? [String: Any])
|
|
for language in ["zh-Hans", "zh-Hant"] {
|
|
let chinese = try #require(localizations[language] as? [String: Any])
|
|
let unit = try #require(chinese["stringUnit"] as? [String: Any])
|
|
let translation = try #require(unit["value"] as? String)
|
|
let placeholders = translation.matches(of: /%(?:[1-9][0-9]*\$)?@/)
|
|
#expect(placeholders.count == arguments.count, "Placeholder mismatch: \(language) \(key)")
|
|
let rendered = String(format: translation, arguments: arguments)
|
|
var remaining = rendered[...]
|
|
for argument in arguments {
|
|
let range = try #require(remaining.range(of: argument), "Lost value: \(language) \(key)")
|
|
remaining = remaining[range.upperBound...]
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@Test
|
|
func countedToolResourcesUseCatalogPluralRules() throws {
|
|
let repository = URL(fileURLWithPath: #filePath)
|
|
.deletingLastPathComponent().deletingLastPathComponent()
|
|
let url = repository.appending(path: "Frameworks/Resources/Localizable.xcstrings")
|
|
let root = try #require(
|
|
JSONSerialization.jsonObject(with: Data(contentsOf: url)) as? [String: Any]
|
|
)
|
|
let strings = try #require(root["strings"] as? [String: Any])
|
|
|
|
for key in [
|
|
// `%lld results` is not here: `web_search`'s card moved to AgentKit
|
|
// with the runtime, and so did the plural resource behind it.
|
|
"%lld commands", "%lld hosts", "%lld items", "%lld questions",
|
|
"the task list %lld times",
|
|
"%lld tool calls", "%@ — %lld tools",
|
|
] {
|
|
let entry = try #require(strings[key] as? [String: Any])
|
|
let localizations = try #require(entry["localizations"] as? [String: Any])
|
|
let english = try #require(localizations["en"] as? [String: Any])
|
|
#expect(pluralForms(in: english).keys.sorted() == ["one", "other"])
|
|
for language in ["zh-Hans", "zh-Hant"] {
|
|
let chinese = try #require(localizations[language] as? [String: Any])
|
|
#expect(pluralForms(in: chinese).keys.sorted() == ["other"])
|
|
}
|
|
}
|
|
}
|
|
|
|
@Test
|
|
func taiwanFlagColorDetectionHandlesColorGrayscaleAndMissingPixels() {
|
|
#expect(DeviceCensorship.containsColoredPixel([255, 0, 0, 255]))
|
|
#expect(!DeviceCensorship.containsColoredPixel([128, 128, 128, 255]))
|
|
#expect(!DeviceCensorship.containsColoredPixel([]))
|
|
}
|
|
|
|
private func localizedUnits(in value: [String: Any]) -> [[String: Any]] {
|
|
if let unit = value["stringUnit"] as? [String: Any] { return [unit] }
|
|
guard let variations = value["variations"] as? [String: Any] else { return [] }
|
|
return variations.values.flatMap { variation -> [[String: Any]] in
|
|
guard let forms = variation as? [String: Any] else { return [] }
|
|
return forms.values.flatMap { form -> [[String: Any]] in
|
|
guard let form = form as? [String: Any] else { return [] }
|
|
return localizedUnits(in: form)
|
|
}
|
|
}
|
|
}
|
|
|
|
private func pluralForms(in localization: [String: Any]) -> [String: Any] {
|
|
let variations = localization["variations"] as? [String: Any]
|
|
return variations?["plural"] as? [String: Any] ?? [:]
|
|
}
|
|
|
|
private func makeDefaults() throws -> UserDefaults {
|
|
let suiteName = "AppLanguageTests.\(UUID().uuidString)"
|
|
let defaults = try #require(UserDefaults(suiteName: suiteName))
|
|
defaults.removePersistentDomain(forName: suiteName)
|
|
return defaults
|
|
}
|
|
}
|