Migrates vocabulary words and word replacements from UserDefaults to SwiftData for better data management and persistence. Changes: - Create VocabularyWord and WordReplacement SwiftData models - Add dual ModelConfiguration setup (default.store for transcripts, dictionary.store for dictionary data) - Implement DictionaryMigrationService for one-time UserDefaults→SwiftData migration - Rename "Correct Spellings" to "Vocabulary" for clearer terminology - Update all dictionary views to use @Query instead of manager classes - Update all services to fetch from SwiftData using FetchDescriptor - Enhance word replacement duplicate detection (now checks during add AND edit) - Update import/export services to work with SwiftData - Preserve all existing functionality with improved data integrity Technical details: - Separate store files: default.store (transcripts) + dictionary.store (vocabulary + replacements) - Migration flag: "HasMigratedDictionaryToSwiftData_v2" - All CRUD operations properly implemented with duplicate detection
77 lines
2.8 KiB
Swift
77 lines
2.8 KiB
Swift
import Foundation
|
|
import SwiftData
|
|
|
|
class WordReplacementService {
|
|
static let shared = WordReplacementService()
|
|
|
|
private init() {}
|
|
|
|
func applyReplacements(to text: String, using context: ModelContext) -> String {
|
|
let descriptor = FetchDescriptor<WordReplacement>(
|
|
predicate: #Predicate { $0.isEnabled }
|
|
)
|
|
|
|
guard let replacements = try? context.fetch(descriptor), !replacements.isEmpty else {
|
|
return text // No replacements to apply
|
|
}
|
|
|
|
var modifiedText = text
|
|
|
|
// Apply replacements (case-insensitive)
|
|
for replacement in replacements {
|
|
let originalGroup = replacement.originalText
|
|
let replacementText = replacement.replacementText
|
|
|
|
// Split comma-separated originals at apply time only
|
|
let variants = originalGroup
|
|
.split(separator: ",")
|
|
.map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
|
|
.filter { !$0.isEmpty }
|
|
|
|
for original in variants {
|
|
let usesBoundaries = usesWordBoundaries(for: original)
|
|
|
|
if usesBoundaries {
|
|
// Word-boundary regex for full original string
|
|
let pattern = "\\b\(NSRegularExpression.escapedPattern(for: original))\\b"
|
|
if let regex = try? NSRegularExpression(pattern: pattern, options: .caseInsensitive) {
|
|
let range = NSRange(modifiedText.startIndex..., in: modifiedText)
|
|
modifiedText = regex.stringByReplacingMatches(
|
|
in: modifiedText,
|
|
options: [],
|
|
range: range,
|
|
withTemplate: replacementText
|
|
)
|
|
}
|
|
} else {
|
|
// Fallback substring replace for non-spaced scripts
|
|
modifiedText = modifiedText.replacingOccurrences(of: original, with: replacementText, options: .caseInsensitive)
|
|
}
|
|
}
|
|
}
|
|
|
|
return modifiedText
|
|
}
|
|
|
|
private func usesWordBoundaries(for text: String) -> Bool {
|
|
// Returns false for languages without spaces (CJK, Thai), true for spaced languages
|
|
let nonSpacedScripts: [ClosedRange<UInt32>] = [
|
|
0x3040...0x309F, // Hiragana
|
|
0x30A0...0x30FF, // Katakana
|
|
0x4E00...0x9FFF, // CJK Unified Ideographs
|
|
0xAC00...0xD7AF, // Hangul Syllables
|
|
0x0E00...0x0E7F, // Thai
|
|
]
|
|
|
|
for scalar in text.unicodeScalars {
|
|
for range in nonSpacedScripts {
|
|
if range.contains(scalar.value) {
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|
|
}
|