vOOice/VoiceInk/Services/WordReplacementService.swift
Beingpax 60125c316b Migrate dictionary data from UserDefaults to SwiftData
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
2025-12-28 12:09:43 +05:45

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
}
}