From 5f7ad88e0d1f9cd2bffd28d86ee2ecef1e88e7f8 Mon Sep 17 00:00:00 2001
From: Ariver <shanghai3168@gmail.com>
Date: Fri, 19 Jun 2026 21:49:07 +0800
Subject: [PATCH] Fix Apple default note localization migration

---
 src/Apptag/AppleDefaultAppCatalog.swift |  126 ++++++++++++++++++++++++++++++++++++++---
 1 files changed, 115 insertions(+), 11 deletions(-)

diff --git a/src/Apptag/AppleDefaultAppCatalog.swift b/src/Apptag/AppleDefaultAppCatalog.swift
index 120c2a4..9ef452c 100644
--- a/src/Apptag/AppleDefaultAppCatalog.swift
+++ b/src/Apptag/AppleDefaultAppCatalog.swift
@@ -108,33 +108,78 @@
 
     @discardableResult
     static func relocalizeDefaultNotesForCurrentLanguage(apps: [AppInfo]) -> Bool {
+        guard let base = loadBaseSnapshot() else { return false }
+
         var store = TagDatabase.load()
+        let originalStore = store
         var changed = false
+        var adoptedLegacyDefaultNote = false
 
         for app in apps where app.isAppleApp {
             let path = app.path.path
-            guard let currentNote = normalizedNote(store.appNotes[path]) else { continue }
-            guard let metadata = store.appNoteMetadata[path],
-                  metadata.origin == .appleDefault
+            guard let bundleIdentifier = normalizedBundleIdentifier(app.bundleIdentifier),
+                  base.byBundleIdentifier[bundleIdentifier] != nil,
+                  let currentNote = normalizedNote(store.appNotes[path], limit: base.noteLimit),
+                  let localizedNote = localizedNote(forBundleIdentifier: bundleIdentifier)
             else { continue }
+
+            let metadata = store.appNoteMetadata[path]
+            if metadata?.origin == .manual {
+                continue
+            }
 
             let currentFingerprint = TagDatabase.noteFingerprint(currentNote)
-            let expectedFingerprint = metadata.apple?.noteFingerprint ?? metadata.noteFingerprint
-            guard expectedFingerprint == currentFingerprint else { continue }
-            guard let defaultNote = Self.defaultNote(for: app),
-                  defaultNote.note != currentNote
-            else { continue }
+            let matchesKnownAppleDefault = noteMatchesKnownDefault(
+                currentNote,
+                forBundleIdentifier: bundleIdentifier,
+                base: base
+            )
 
-            store.appNotes[path] = defaultNote.note
+            let canRelocalize: Bool
+            switch metadata?.origin {
+            case .some(.manual):
+                canRelocalize = false
+            case .some(.appleDefault):
+                let expectedFingerprint = metadata?.apple?.noteFingerprint ?? metadata?.noteFingerprint
+                canRelocalize = expectedFingerprint == currentFingerprint || matchesKnownAppleDefault
+                if expectedFingerprint != currentFingerprint && matchesKnownAppleDefault {
+                    adoptedLegacyDefaultNote = true
+                }
+            case .some(.catalogDefault):
+                canRelocalize = matchesKnownAppleDefault
+                if canRelocalize {
+                    adoptedLegacyDefaultNote = true
+                }
+            case .none:
+                canRelocalize = matchesKnownAppleDefault
+                if canRelocalize {
+                    adoptedLegacyDefaultNote = true
+                }
+            }
+
+            guard canRelocalize else { continue }
+
+            let metadataMatchesLocalizedNote = metadata?.origin == .appleDefault
+                && metadata?.apple?.entryID == localizedNote.provenance.entryID
+                && metadata?.apple?.languageCode == localizedNote.provenance.languageCode
+                && metadata?.apple?.notesVersion == localizedNote.provenance.notesVersion
+                && metadata?.apple?.noteFingerprint == localizedNote.provenance.noteFingerprint
+                && metadata?.noteFingerprint == localizedNote.provenance.noteFingerprint
+            guard localizedNote.note != currentNote || !metadataMatchesLocalizedNote else { continue }
+
+            store.appNotes[path] = localizedNote.note
             store.appNoteMetadata[path] = TagDatabase.AppNoteMetadata(
                 origin: .appleDefault,
-                apple: defaultNote.provenance,
-                noteFingerprint: defaultNote.provenance.noteFingerprint
+                apple: localizedNote.provenance,
+                noteFingerprint: localizedNote.provenance.noteFingerprint
             )
             changed = true
         }
 
         if changed {
+            if adoptedLegacyDefaultNote {
+                _ = TagDatabase.backup(originalStore, reason: "apple-default-note-migration")
+            }
             TagDatabase.save(store)
         }
         return changed
@@ -168,6 +213,65 @@
         return nil
     }
 
+    private static func noteMatchesKnownDefault(
+        _ note: String,
+        forBundleIdentifier bundleIdentifier: String,
+        base: BaseSnapshot
+    ) -> Bool {
+        let fingerprint = TagDatabase.noteFingerprint(note)
+        return knownDefaultNoteFingerprints(
+            forBundleIdentifier: bundleIdentifier,
+            base: base
+        ).contains(fingerprint)
+    }
+
+    private static func knownDefaultNoteFingerprints(
+        forBundleIdentifier bundleIdentifier: String,
+        base: BaseSnapshot
+    ) -> Set<String> {
+        var result = Set<String>()
+        for languageCode in base.supportedLanguages {
+            guard let snapshot = loadLocalizationSnapshot(languageCode: languageCode),
+                  let note = normalizedNote(snapshot.notes[bundleIdentifier], limit: base.noteLimit)
+            else { continue }
+            for variant in legacyDefaultNoteVariants(note, limit: base.noteLimit) {
+                result.insert(TagDatabase.noteFingerprint(variant))
+            }
+        }
+        return result
+    }
+
+    private static func legacyDefaultNoteVariants(_ note: String, limit: Int) -> [String] {
+        let trimmed = String(note.trimmingCharacters(in: .whitespacesAndNewlines).prefix(limit))
+        guard !trimmed.isEmpty else { return [] }
+
+        var variants = [trimmed]
+        let stripped = noteByRemovingTrailingSentencePunctuation(trimmed)
+        if stripped != trimmed {
+            variants.append(stripped)
+        }
+
+        for suffix in [".", "。"] {
+            let punctuated = String((stripped + suffix).prefix(limit))
+            if !punctuated.isEmpty {
+                variants.append(punctuated)
+            }
+        }
+
+        var seen = Set<String>()
+        return variants.filter { seen.insert($0).inserted }
+    }
+
+    private static func noteByRemovingTrailingSentencePunctuation(_ value: String) -> String {
+        var result = value.trimmingCharacters(in: .whitespacesAndNewlines)
+        let punctuation = CharacterSet(charactersIn: ".。.!!??")
+        while let scalar = result.unicodeScalars.last,
+              punctuation.contains(scalar) {
+            result.removeLast()
+        }
+        return result
+    }
+
     private static func loadBaseSnapshot() -> BaseSnapshot? {
         cacheLock.lock()
         if let snapshot = cachedBaseSnapshot {

--
Gitblit v1.9.3