feat(typing): wire PersonalDictionary into Chinese Pinyin via Rime sidecar
Redeploy an osg_personal import table on dictionary add/delete/sync so Chinese, English typing, and ASR share one curated lexicon (next keyboard open).
This commit is contained in:
@@ -0,0 +1,131 @@
|
||||
// RimePersonalDictionaryExporter.swift
|
||||
// OSGKeyboard · Shared
|
||||
//
|
||||
// Turns PersonalDictionary into an osg_personal Rime table that the
|
||||
// main osg_pinyin dictionary imports. High weight keeps same-code
|
||||
// personal hits above the baseline lexicon.
|
||||
|
||||
import CryptoKit
|
||||
import Foundation
|
||||
|
||||
public enum RimePersonalDictionaryExporter {
|
||||
public static let dictionaryName = "osg_personal"
|
||||
/// Above any baseline `osg_pinyin` weight so same-code hits pin to top.
|
||||
public static let pinWeight = 50_000_000
|
||||
|
||||
public struct Entry: Equatable, Sendable {
|
||||
public let text: String
|
||||
public let code: String
|
||||
public let weight: Int
|
||||
|
||||
public init(text: String, code: String, weight: Int = RimePersonalDictionaryExporter.pinWeight) {
|
||||
self.text = text
|
||||
self.code = code
|
||||
self.weight = weight
|
||||
}
|
||||
}
|
||||
|
||||
/// Builds Rime rows for Chinese (pinyin-coded), English (latin-coded),
|
||||
/// and Latin aliases that should surface the canonical term.
|
||||
public static func entries(
|
||||
from dictionary: PersonalDictionary,
|
||||
annotator: RimePinyinAnnotator
|
||||
) -> [Entry] {
|
||||
var seen = Set<String>()
|
||||
var rows: [Entry] = []
|
||||
|
||||
func append(text: String, code: String) {
|
||||
let key = "\(text)\t\(code)"
|
||||
guard seen.insert(key).inserted else { return }
|
||||
rows.append(Entry(text: text, code: code))
|
||||
}
|
||||
|
||||
for entry in dictionary.effectiveEntries {
|
||||
let term = entry.term.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard !term.isEmpty else { continue }
|
||||
|
||||
if let code = annotator.code(for: term) {
|
||||
append(text: term, code: code)
|
||||
}
|
||||
|
||||
// Latin aliases → alternate codes for the canonical term
|
||||
// (e.g. brand English → 中文专名). Chinese ASR aliases stay out.
|
||||
for alias in entry.aliases {
|
||||
let trimmed = alias.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard !trimmed.isEmpty else { continue }
|
||||
guard !RimePinyinAnnotator.containsCJK(trimmed) else { continue }
|
||||
let latin = RimePinyinAnnotator.latinSpellerCode(trimmed)
|
||||
guard !latin.isEmpty else { continue }
|
||||
append(text: term, code: latin)
|
||||
}
|
||||
}
|
||||
|
||||
return rows.sorted {
|
||||
if $0.text != $1.text {
|
||||
return $0.text.localizedStandardCompare($1.text) == .orderedAscending
|
||||
}
|
||||
return $0.code < $1.code
|
||||
}
|
||||
}
|
||||
|
||||
public static func yaml(
|
||||
from dictionary: PersonalDictionary,
|
||||
annotator: RimePinyinAnnotator
|
||||
) -> String {
|
||||
yaml(entries: entries(from: dictionary, annotator: annotator))
|
||||
}
|
||||
|
||||
public static func yaml(entries: [Entry]) -> String {
|
||||
var lines: [String] = [
|
||||
"# Generated by OSGKeyboard from PersonalDictionary. Do not edit.",
|
||||
"---",
|
||||
"name: \(dictionaryName)",
|
||||
"version: \"1.0\"",
|
||||
"sort: by_weight",
|
||||
"columns:",
|
||||
" - text",
|
||||
" - code",
|
||||
" - weight",
|
||||
"..."
|
||||
]
|
||||
for entry in entries {
|
||||
lines.append("\(entry.text)\t\(entry.code)\t\(entry.weight)")
|
||||
}
|
||||
lines.append("")
|
||||
return lines.joined(separator: "\n")
|
||||
}
|
||||
|
||||
public static func fingerprint(of yaml: String) -> String {
|
||||
let digest = SHA256.hash(data: Data(yaml.utf8))
|
||||
return digest.map { String(format: "%02x", $0) }.joined()
|
||||
}
|
||||
|
||||
/// Injects `import_tables: [osg_personal]` into a baseline dict header.
|
||||
public static func injectingImportTables(into baselineYAML: String) -> String {
|
||||
if baselineYAML.contains("import_tables:") {
|
||||
return baselineYAML
|
||||
}
|
||||
let needle = "use_preset_vocabulary: false\n"
|
||||
let injection = """
|
||||
use_preset_vocabulary: false
|
||||
import_tables:
|
||||
- \(dictionaryName)
|
||||
|
||||
"""
|
||||
if let range = baselineYAML.range(of: needle) {
|
||||
return baselineYAML.replacingCharacters(in: range, with: injection)
|
||||
}
|
||||
// Fallback: insert after the `---` document start block's name line.
|
||||
let nameNeedle = "name: osg_pinyin\n"
|
||||
if let range = baselineYAML.range(of: nameNeedle) {
|
||||
let injectionAfterName = """
|
||||
name: osg_pinyin
|
||||
import_tables:
|
||||
- \(dictionaryName)
|
||||
|
||||
"""
|
||||
return baselineYAML.replacingCharacters(in: range, with: injectionAfterName)
|
||||
}
|
||||
return baselineYAML
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user