From fae673d17c0d7c4f9acb076218c7de291bf507ea Mon Sep 17 00:00:00 2001
From: Ariver <shanghai3168@gmail.com>
Date: Tue, 09 Jun 2026 18:26:01 +0800
Subject: [PATCH] Release 7.8.25 multilingual app names
---
src/Apptag/DataLayer.swift | 217 ++++++++++++++++++++++++++++++++++++++++++++----------
1 files changed, 177 insertions(+), 40 deletions(-)
diff --git a/src/Apptag/DataLayer.swift b/src/Apptag/DataLayer.swift
index 17a0605..d6cd436 100644
--- a/src/Apptag/DataLayer.swift
+++ b/src/Apptag/DataLayer.swift
@@ -12,46 +12,18 @@
let bundleIdentifier: String?
let localizedNames: [String]
let localizedNamesByLanguage: [String: String]
+ let systemDisplayNames: [String]
+ let bundleDisplayNames: [String]
let icon: NSImage // Pre-loaded during background scan
var isUncommon: Bool = false
var note: String? = nil
var displayName: String {
- localizedDisplayName(for: L10n.currentCode)
+ AppDisplayNameResolver.displayName(for: self, languageCode: L10n.currentCode)
}
func localizedDisplayName(for languageCode: String) -> String {
- let candidates = AppInfo.displayLanguageFallbacks(for: languageCode)
- for code in candidates {
- if let localizedName = localizedNamesByLanguage[code],
- !localizedName.isEmpty {
- return localizedName
- }
- }
- if let localizedName = localizedNames.first, !localizedName.isEmpty {
- return localizedName
- }
- return name
- }
-
- private static func displayLanguageFallbacks(for languageCode: String) -> [String] {
- var candidates = [languageCode]
- switch languageCode {
- case "zh-Hans":
- candidates.append("zh-Hant")
- case "zh-Hant":
- candidates.append("zh-Hans")
- case "nb":
- candidates.append("no")
- case "nn":
- candidates.append("no")
- case "no":
- candidates.append(contentsOf: ["nb", "nn"])
- default:
- break
- }
- candidates.append("en")
- return uniqueLanguageCodes(candidates)
+ AppDisplayNameResolver.displayName(for: self, languageCode: languageCode)
}
fileprivate static func uniqueLanguageCodes(_ codes: [String]) -> [String] {
@@ -67,6 +39,141 @@
func hash(into hasher: inout Hasher) { hasher.combine(path) }
static func == (lhs: AppInfo, rhs: AppInfo) -> Bool { lhs.path == rhs.path }
+}
+
+enum AppDisplayNameResolver {
+ static func displayName(for app: AppInfo, languageCode: String) -> String {
+ let languageFallbacks = displayLanguageFallbacks(for: languageCode, includeEnglish: false)
+
+ if let localizedName = firstLocalizedName(
+ for: languageFallbacks,
+ in: app.localizedNamesByLanguage
+ ) {
+ return localizedName
+ }
+
+ if languageCode == "en",
+ let englishBaseName = firstDisplayCandidate([app.name] + app.bundleDisplayNames) {
+ return englishBaseName
+ }
+
+ if let systemDisplayName = firstDisplayCandidate(app.systemDisplayNames) {
+ return systemDisplayName
+ }
+
+ if let englishName = firstLocalizedName(for: ["en"], in: app.localizedNamesByLanguage) {
+ return englishName
+ }
+
+ if let bundleDisplayName = firstDisplayCandidate(app.bundleDisplayNames) {
+ return bundleDisplayName
+ }
+
+ let skippedLanguageCodes = Set(languageFallbacks + ["en"])
+ let remainingLocalizedNames = L10n.supported.compactMap { language -> String? in
+ guard !skippedLanguageCodes.contains(language.code) else { return nil }
+ return app.localizedNamesByLanguage[language.code]
+ }
+ if let localizedName = firstDisplayCandidate(remainingLocalizedNames) {
+ return localizedName
+ }
+
+ if let alias = firstDisplayCandidate(app.localizedNames) {
+ return alias
+ }
+
+ return app.name
+ }
+
+ static func searchAliases(for app: AppInfo) -> [String] {
+ uniqueDisplayNames(
+ L10n.supported.compactMap { app.localizedNamesByLanguage[$0.code] }
+ + app.systemDisplayNames
+ + app.bundleDisplayNames
+ + app.localizedNames
+ + [app.displayName],
+ excluding: app.name
+ )
+ }
+
+ private static func displayLanguageFallbacks(
+ for languageCode: String,
+ includeEnglish: Bool
+ ) -> [String] {
+ var candidates = [
+ languageCode,
+ languageCode.replacingOccurrences(of: "_", with: "-")
+ ]
+
+ switch languageCode {
+ case "zh-Hans":
+ candidates.append("zh-Hant")
+ case "zh-Hant":
+ candidates.append("zh-Hans")
+ case "pt-BR":
+ candidates.append("pt")
+ case "sr-Cyrl":
+ candidates.append("sr")
+ case "ar-Najdi":
+ candidates.append("ar")
+ case "nb":
+ candidates.append(contentsOf: ["no", "nn"])
+ case "nn":
+ candidates.append(contentsOf: ["no", "nb"])
+ case "no":
+ candidates.append(contentsOf: ["nb", "nn"])
+ default:
+ if let base = languageCode.split(separator: "-").first.map(String.init) {
+ candidates.append(base)
+ }
+ }
+
+ if includeEnglish {
+ candidates.append("en")
+ }
+ return AppInfo.uniqueLanguageCodes(candidates)
+ }
+
+ private static func firstLocalizedName(
+ for languageCodes: [String],
+ in localizedNamesByLanguage: [String: String]
+ ) -> String? {
+ firstDisplayCandidate(languageCodes.compactMap { localizedNamesByLanguage[$0] })
+ }
+
+ private static func firstDisplayCandidate(_ values: [String]) -> String? {
+ values.lazy.compactMap(normalizedDisplayName).first
+ }
+
+ private static func uniqueDisplayNames(_ values: [String], excluding excludedValue: String) -> [String] {
+ let normalizedExcluded = normalizedKey(excludedValue)
+ var seen = Set<String>()
+ var result: [String] = []
+
+ for value in values {
+ guard let trimmed = normalizedDisplayName(value) else { continue }
+ let normalized = normalizedKey(trimmed)
+ guard normalized != normalizedExcluded,
+ seen.insert(normalized).inserted
+ else { continue }
+ result.append(trimmed)
+ }
+ return result
+ }
+
+ private static func normalizedDisplayName(_ value: String) -> String? {
+ let trimmed = value
+ .replacingOccurrences(of: ".app", with: "")
+ .trimmingCharacters(in: .whitespacesAndNewlines)
+ return trimmed.isEmpty ? nil : trimmed
+ }
+
+ private static func normalizedKey(_ value: String) -> String {
+ value
+ .folding(options: [.diacriticInsensitive, .caseInsensitive], locale: .current)
+ .lowercased()
+ .trimmingCharacters(in: .whitespacesAndNewlines)
+ }
}
struct TagGroup: Identifiable {
@@ -282,10 +389,13 @@
}
}
}
+ let systemDisplayNames = systemDisplayNames(for: displayURL, fallbackName: name)
+ let bundleDisplayNames = bundleDisplayNames(bundle: bundle, fallbackName: name)
let localizedNames = localizedAppNames(
- for: displayURL,
fallbackName: name,
- localizedNamesByLanguage: localizedNamesByLanguage
+ localizedNamesByLanguage: localizedNamesByLanguage,
+ systemDisplayNames: systemDisplayNames,
+ bundleDisplayNames: bundleDisplayNames
)
apps.append(AppInfo(
@@ -295,6 +405,8 @@
bundleIdentifier: bundleId,
localizedNames: localizedNames,
localizedNamesByLanguage: localizedNamesByLanguage,
+ systemDisplayNames: systemDisplayNames,
+ bundleDisplayNames: bundleDisplayNames,
icon: icon
))
}
@@ -325,16 +437,39 @@
}
private static func localizedAppNames(
- for appURL: URL,
fallbackName: String,
- localizedNamesByLanguage: [String: String]
+ localizedNamesByLanguage: [String: String],
+ systemDisplayNames: [String],
+ bundleDisplayNames: [String]
) -> [String] {
var values = L10n.supported.map { localizedNamesByLanguage[$0.code] }
- values.append(resourceLocalizedName(for: appURL))
- values.append(spotlightDisplayName(for: appURL))
- values.append(FileManager.default.displayName(atPath: appURL.path).replacingOccurrences(of: ".app", with: ""))
+ values.append(contentsOf: systemDisplayNames.map { Optional($0) })
+ values.append(contentsOf: bundleDisplayNames.map { Optional($0) })
return uniqueLocalizedNames(values, excluding: fallbackName)
+ }
+
+ private static func systemDisplayNames(for appURL: URL, fallbackName: String) -> [String] {
+ uniqueLocalizedNames(
+ [
+ resourceLocalizedName(for: appURL),
+ spotlightDisplayName(for: appURL),
+ FileManager.default.displayName(atPath: appURL.path)
+ ],
+ excluding: fallbackName
+ )
+ }
+
+ private static func bundleDisplayNames(bundle: Bundle?, fallbackName: String) -> [String] {
+ uniqueLocalizedNames(
+ [
+ bundle?.localizedInfoDictionary?["CFBundleDisplayName"] as? String,
+ bundle?.localizedInfoDictionary?["CFBundleName"] as? String,
+ bundle?.infoDictionary?["CFBundleDisplayName"] as? String,
+ bundle?.infoDictionary?["CFBundleName"] as? String
+ ],
+ excluding: fallbackName
+ )
}
private static func localizedAppName(
@@ -488,7 +623,7 @@
.trimmingCharacters(in: .whitespacesAndNewlines)
}
- private static func isNestedInsideAppBundle(_ url: URL) -> Bool {
+ static func isNestedInsideAppBundle(_ url: URL) -> Bool {
let components = url.standardizedFileURL.pathComponents
guard let lastAppIndex = components.lastIndex(where: { $0.lowercased().hasSuffix(".app") }) else {
return false
@@ -1325,6 +1460,8 @@
bundleIdentifier: app.bundleIdentifier,
localizedNames: app.localizedNames,
localizedNamesByLanguage: app.localizedNamesByLanguage,
+ systemDisplayNames: app.systemDisplayNames,
+ bundleDisplayNames: app.bundleDisplayNames,
icon: app.icon,
isUncommon: uncommonPaths.contains(app.path.path),
note: store.appNotes[app.path.path]
--
Gitblit v1.9.3