feat(dictionary): add iCloud KVS sync, cloud ASR, and lexicon expansion
Mirror the personal dictionary through iCloud Key-Value Store with deterministic merge rules, main-app-only sync UI, and App Group as the keyboard runtime cache. Add cloud-engine ASR with dictionary bias and expand the bundled custom language model lexicon.
This commit is contained in:
@@ -2,6 +2,8 @@
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.developer.ubiquity-kvstore-identifier</key>
|
||||
<string>$(TeamIdentifierPrefix)com.osgkeyboard.ios</string>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>group.com.osgkeyboard.shared</string>
|
||||
|
||||
@@ -1,21 +1,25 @@
|
||||
{
|
||||
"version": "v1",
|
||||
"name": "ai-tech-brands",
|
||||
"generated_at": "2026-07-05T11:34:08.462301+00:00",
|
||||
"generated_at": "2026-07-06T13:10:52.972532+00:00",
|
||||
"locale": "zh-Hans",
|
||||
"entry_count": 1259,
|
||||
"entry_count": 3040,
|
||||
"seed_file": "Scripts/lexicon/seeds/ai_tech_brands_seed.tsv",
|
||||
"license": "MIT (curated seed; OSGKeyboard contributors)",
|
||||
"categories": {
|
||||
"ai_brand": 169,
|
||||
"ai_model": 90,
|
||||
"ai_platform": 53,
|
||||
"ai_term": 197,
|
||||
"dev_tool": 283,
|
||||
"fintech": 7,
|
||||
"tech_company": 314,
|
||||
"tech_leader": 37,
|
||||
"tech_term": 109
|
||||
"ai_brand": 284,
|
||||
"ai_model": 152,
|
||||
"ai_platform": 116,
|
||||
"ai_term": 270,
|
||||
"athlete": 73,
|
||||
"celebrity": 251,
|
||||
"consumer_brand": 215,
|
||||
"dev_tool": 330,
|
||||
"fintech": 109,
|
||||
"game_ip": 70,
|
||||
"tech_company": 601,
|
||||
"tech_leader": 202,
|
||||
"tech_term": 367
|
||||
},
|
||||
"notes": [
|
||||
"Curated bilingual AI brands, tech companies, terminology, and hot words.",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -39,7 +39,7 @@ final class FlowSessionManager: ObservableObject {
|
||||
private var sessionASRWarmedLocaleID: String?
|
||||
private var asr: ASRService {
|
||||
if let sessionASR { return sessionASR }
|
||||
let service = ASRServiceFactory.make()
|
||||
let service = ASRServiceFactory.make(store: store)
|
||||
sessionASR = service
|
||||
return service
|
||||
}
|
||||
@@ -342,7 +342,7 @@ final class FlowSessionManager: ObservableObject {
|
||||
return
|
||||
}
|
||||
sessionASR?.cancel()
|
||||
sessionASR = ASRServiceFactory.make()
|
||||
sessionASR = ASRServiceFactory.make(store: store)
|
||||
sessionASREngineMode = engineMode
|
||||
sessionASRWarmedLocaleID = nil
|
||||
}
|
||||
|
||||
@@ -140,7 +140,8 @@ struct HomeView: View {
|
||||
}
|
||||
|
||||
private var headerGradientColors: [Color] {
|
||||
if sessionIsLive {
|
||||
// 云端引擎未配置(缺 API Key)时不算就绪,保持中性灰渐变。
|
||||
if sessionIsLive, !needsCloudSetup {
|
||||
return [
|
||||
palette.accent.opacity(0.28),
|
||||
palette.accent.opacity(0.10),
|
||||
@@ -175,7 +176,14 @@ struct HomeView: View {
|
||||
.fill(flowStatusColor)
|
||||
.frame(width: 6, height: 6)
|
||||
|
||||
if flowManager.isActive,
|
||||
if needsCloudSetup {
|
||||
// 云端引擎缺 API Key:不显示就绪 / 计时 / 结束按钮。
|
||||
Text("home.flow.notReady")
|
||||
.font(TypeStyle.caption2)
|
||||
.foregroundStyle(palette.warning)
|
||||
.lineLimit(1)
|
||||
.minimumScaleFactor(0.85)
|
||||
} else if flowManager.isActive,
|
||||
let expires = flowManager.sessionExpiresAt {
|
||||
Text("home.flow.label")
|
||||
.font(TypeStyle.caption2)
|
||||
@@ -195,7 +203,10 @@ struct HomeView: View {
|
||||
.minimumScaleFactor(0.85)
|
||||
}
|
||||
|
||||
if flowManager.isActive {
|
||||
if needsCloudSetup {
|
||||
// 无按钮:引导卡片已提示去设置填 API Key。
|
||||
EmptyView()
|
||||
} else if flowManager.isActive {
|
||||
Button {
|
||||
flowManager.endSession()
|
||||
} label: {
|
||||
@@ -306,6 +317,7 @@ struct HomeView: View {
|
||||
}
|
||||
|
||||
private var flowStatusColor: Color {
|
||||
if needsCloudSetup { return palette.warning }
|
||||
if flowManager.isActive { return palette.accent }
|
||||
if flowManager.isStarting { return palette.accent }
|
||||
if needsPermissionSetup { return palette.warning }
|
||||
|
||||
@@ -38,6 +38,10 @@ struct MainAppRoot: View {
|
||||
.onAppear {
|
||||
flowManager.setAppForeground(scenePhase == .active)
|
||||
flowManager.activateOnForeground()
|
||||
PersonalDictionaryCloudSync.shared.startObservingExternalChanges()
|
||||
Task {
|
||||
await PersonalDictionaryCloudSync.shared.pullAndMergeIfEnabled()
|
||||
}
|
||||
}
|
||||
.onReceive(NotificationCenter.default.publisher(for: .osgKeyboardOpenURL)) { notification in
|
||||
guard let url = notification.userInfo?["url"] as? URL else { return }
|
||||
@@ -50,8 +54,13 @@ struct MainAppRoot: View {
|
||||
}
|
||||
.onChange(of: scenePhase) { _, phase in
|
||||
flowManager.handleScenePhase(phase)
|
||||
guard phase == .active, config.hasCompletedOnboarding else { return }
|
||||
flowManager.activateOnForeground()
|
||||
guard phase == .active else { return }
|
||||
if config.hasCompletedOnboarding {
|
||||
flowManager.activateOnForeground()
|
||||
}
|
||||
Task {
|
||||
await PersonalDictionaryCloudSync.shared.pullAndMergeIfEnabled()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
// PersonalDictionaryICloudSyncRow.swift
|
||||
// OSGKeyboard · Main App
|
||||
//
|
||||
// Settings-row toggle for mirroring the personal dictionary through
|
||||
// iCloud Key-Value Store. Lives in Settings → 词库与润色.
|
||||
|
||||
import SwiftUI
|
||||
import OSGKeyboardShared
|
||||
|
||||
@MainActor
|
||||
struct PersonalDictionaryICloudSyncRow: View {
|
||||
@Environment(\.themePalette) private var palette: ThemePalette
|
||||
|
||||
@State private var isEnabled: Bool = AppGroupStore().personalDictionaryICloudSyncEnabled
|
||||
@State private var syncErrorMessage: String?
|
||||
@State private var isApplyingToggle = false
|
||||
|
||||
private let store = AppGroupStore()
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: Spacing.xxs) {
|
||||
Toggle(isOn: toggleBinding) {
|
||||
Text("settings.personalDictionary.iCloudSync.settingsTitle")
|
||||
.font(TypeStyle.body)
|
||||
.foregroundStyle(palette.textPrimary)
|
||||
}
|
||||
.tint(palette.accent)
|
||||
.disabled(isApplyingToggle)
|
||||
|
||||
if let syncErrorMessage {
|
||||
Text(syncErrorMessage)
|
||||
.font(TypeStyle.caption2)
|
||||
.foregroundStyle(.red)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.padding(.top, Spacing.xxs)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, Spacing.md)
|
||||
.padding(.vertical, Spacing.sm)
|
||||
.frame(minHeight: SettingsListMetrics.singleLineMinHeight, alignment: .leading)
|
||||
.onAppear { reloadFromStore() }
|
||||
.onReceive(
|
||||
NotificationCenter.default.publisher(for: .personalDictionaryDidSyncFromCloud)
|
||||
) { _ in
|
||||
reloadFromStore()
|
||||
}
|
||||
}
|
||||
|
||||
private var toggleBinding: Binding<Bool> {
|
||||
Binding(
|
||||
get: { isEnabled },
|
||||
set: { newValue in
|
||||
guard newValue != isEnabled else { return }
|
||||
if newValue {
|
||||
enableSync()
|
||||
} else {
|
||||
disableSync()
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private func reloadFromStore() {
|
||||
isEnabled = store.personalDictionaryICloudSyncEnabled
|
||||
}
|
||||
|
||||
private func enableSync() {
|
||||
isApplyingToggle = true
|
||||
syncErrorMessage = nil
|
||||
Task {
|
||||
do {
|
||||
try await PersonalDictionaryCloudSync.shared.enableSync()
|
||||
reloadFromStore()
|
||||
} catch let error as PersonalDictionaryCloudSyncError {
|
||||
isEnabled = false
|
||||
syncErrorMessage = localizedSyncError(error)
|
||||
} catch {
|
||||
isEnabled = false
|
||||
syncErrorMessage = error.localizedDescription
|
||||
}
|
||||
isApplyingToggle = false
|
||||
}
|
||||
}
|
||||
|
||||
private func disableSync() {
|
||||
PersonalDictionaryCloudSync.shared.disableSync()
|
||||
isEnabled = false
|
||||
syncErrorMessage = nil
|
||||
}
|
||||
|
||||
private func localizedSyncError(_ error: PersonalDictionaryCloudSyncError) -> String {
|
||||
switch error {
|
||||
case .payloadTooLarge:
|
||||
return AppL10n.string("settings.personalDictionary.iCloudSync.error.tooLarge")
|
||||
case .encodeFailed, .decodeFailed:
|
||||
return AppL10n.string("settings.personalDictionary.iCloudSync.error.generic")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -80,60 +80,48 @@ struct PersonalDictionaryView: View {
|
||||
saveManualEntry(term: term, editingID: editingEntry?.id)
|
||||
}
|
||||
}
|
||||
.task {
|
||||
reloadFromStore()
|
||||
await PersonalDictionaryCloudSync.shared.pullAndMergeIfEnabled()
|
||||
reloadFromStore()
|
||||
}
|
||||
.onReceive(
|
||||
NotificationCenter.default.publisher(for: .personalDictionaryDidSyncFromCloud)
|
||||
) { _ in
|
||||
reloadFromStore()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - List
|
||||
|
||||
private var list: some View {
|
||||
ScrollView {
|
||||
LazyVStack(alignment: .leading, spacing: Spacing.lg) {
|
||||
introBanner
|
||||
ForEach(filteredSections, id: \.0) { category, items in
|
||||
section(for: category, items: items)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, Spacing.lg)
|
||||
.padding(.vertical, Spacing.md)
|
||||
.tabBarScrollBottomPadding()
|
||||
}
|
||||
.searchable(text: $searchText, prompt: "settings.personalDictionary.search.prompt")
|
||||
}
|
||||
|
||||
private var introBanner: some View {
|
||||
VStack(alignment: .leading, spacing: Spacing.sm) {
|
||||
Text("settings.personalDictionary.intro.title")
|
||||
.font(TypeStyle.caption)
|
||||
.foregroundStyle(palette.textPrimary)
|
||||
Text("settings.personalDictionary.intro.body")
|
||||
.font(TypeStyle.caption2)
|
||||
.foregroundStyle(palette.textSecondary)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.padding(Spacing.md)
|
||||
.background(palette.surface, in: RoundedRectangle(cornerRadius: Radius.xl, style: .continuous))
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: Radius.xl, style: .continuous)
|
||||
.stroke(palette.divider, lineWidth: 0.5)
|
||||
)
|
||||
}
|
||||
|
||||
private func section(for _: PersonalDictionary.Entry.Category, items: [PersonalDictionary.Entry]) -> some View {
|
||||
VStack(alignment: .leading, spacing: Spacing.sm) {
|
||||
VStack(spacing: 0) {
|
||||
ForEach(Array(items.enumerated()), id: \.element.id) { index, entry in
|
||||
entryRow(entry)
|
||||
if index < items.count - 1 {
|
||||
Divider().background(palette.divider)
|
||||
List {
|
||||
ForEach(filteredSections, id: \.0) { _, items in
|
||||
Section {
|
||||
ForEach(items) { entry in
|
||||
entryRow(entry)
|
||||
.listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0))
|
||||
.listRowBackground(palette.surface)
|
||||
.listRowSeparatorTint(palette.divider)
|
||||
}
|
||||
.onDelete { offsets in
|
||||
delete(items: items, at: offsets)
|
||||
}
|
||||
}
|
||||
}
|
||||
.background(palette.surface, in: RoundedRectangle(cornerRadius: Radius.xl, style: .continuous))
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: Radius.xl, style: .continuous)
|
||||
.stroke(palette.divider, lineWidth: 0.5)
|
||||
)
|
||||
}
|
||||
.listStyle(.insetGrouped)
|
||||
.listSectionSpacing(Spacing.lg)
|
||||
.scrollContentBackground(.hidden)
|
||||
.background(palette.background)
|
||||
// 让搜索栏与首个词条之间留出呼吸空间,视觉更透气。
|
||||
.contentMargins(.top, Spacing.lg, for: .scrollContent)
|
||||
.searchable(
|
||||
text: $searchText,
|
||||
placement: .navigationBarDrawer(displayMode: .always),
|
||||
prompt: "settings.personalDictionary.search.prompt"
|
||||
)
|
||||
.tabBarScrollBottomPadding()
|
||||
}
|
||||
|
||||
private func entryRow(_ entry: PersonalDictionary.Entry) -> some View {
|
||||
@@ -187,18 +175,10 @@ struct PersonalDictionaryView: View {
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.swipeActions(edge: .trailing, allowsFullSwipe: true) {
|
||||
Button(role: .destructive) {
|
||||
delete(entry)
|
||||
} label: {
|
||||
Label("common.delete", systemImage: "trash")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var emptyState: some View {
|
||||
VStack(spacing: Spacing.sm) {
|
||||
Spacer()
|
||||
Image(systemName: "square.stack.3d.down.right.fill")
|
||||
.font(.system(size: 36, weight: .regular))
|
||||
.foregroundStyle(palette.textTertiary.opacity(0.5))
|
||||
@@ -219,8 +199,11 @@ struct PersonalDictionaryView: View {
|
||||
.buttonStyle(.borderedProminent)
|
||||
.tint(palette.accent)
|
||||
.padding(.top, Spacing.sm)
|
||||
Spacer()
|
||||
}
|
||||
// 内容按自身高度居中,再上移抵消 large title 占用的顶部空间,
|
||||
// 让空状态在整屏视觉上真正垂直居中。
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center)
|
||||
.padding(.bottom, 72)
|
||||
}
|
||||
|
||||
// MARK: - Derived data
|
||||
@@ -285,6 +268,12 @@ struct PersonalDictionaryView: View {
|
||||
persist()
|
||||
}
|
||||
|
||||
private func delete(items: [PersonalDictionary.Entry], at offsets: IndexSet) {
|
||||
for index in offsets {
|
||||
delete(items[index])
|
||||
}
|
||||
}
|
||||
|
||||
private func clearAll() {
|
||||
dictionary = .empty
|
||||
generatingAliasEntryIDs = []
|
||||
@@ -294,6 +283,13 @@ struct PersonalDictionaryView: View {
|
||||
private func persist() {
|
||||
dictionary.version += 1
|
||||
store.setPersonalDictionary(dictionary)
|
||||
Task {
|
||||
try? await PersonalDictionaryCloudSync.shared.pushLocalIfEnabled(dictionary)
|
||||
}
|
||||
}
|
||||
|
||||
private func reloadFromStore() {
|
||||
dictionary = store.personalDictionary
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -48,6 +48,10 @@ struct ProviderPickerSection: View {
|
||||
.font(TypeStyle.body)
|
||||
.foregroundStyle(palette.textPrimary)
|
||||
|
||||
if provider.supportsPersonalDictionaryCloudASR {
|
||||
personalDictionaryBadge
|
||||
}
|
||||
|
||||
Spacer(minLength: Spacing.xs)
|
||||
|
||||
if selected {
|
||||
@@ -80,6 +84,16 @@ struct ProviderPickerSection: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 通义千问 / 智谱 GLM 等支持云端 ASR 热词 API 的提供商。
|
||||
private var personalDictionaryBadge: some View {
|
||||
Text("settings.provider.personalDictionaryBadge")
|
||||
.font(TypeStyle.caption2)
|
||||
.foregroundStyle(palette.accent)
|
||||
.padding(.horizontal, Spacing.sm)
|
||||
.padding(.vertical, 4)
|
||||
.background(palette.accentMuted, in: Capsule())
|
||||
}
|
||||
}
|
||||
|
||||
enum ProviderLogo {
|
||||
|
||||
@@ -40,6 +40,7 @@ struct SettingsView: View {
|
||||
ScrollView {
|
||||
VStack(spacing: Spacing.md) {
|
||||
languageAndPolishSection
|
||||
dictionaryAndPolishSection
|
||||
flowSessionSection
|
||||
engineSection
|
||||
// v0.2.1: hide provider/api card when the
|
||||
@@ -180,15 +181,6 @@ struct SettingsView: View {
|
||||
|
||||
Divider().background(palette.divider)
|
||||
|
||||
polishIntensityPreferenceRows
|
||||
|
||||
if config.isTranslationRowVisible {
|
||||
Divider().background(palette.divider)
|
||||
TranslationPickerRow(config: config, isVisible: true)
|
||||
}
|
||||
|
||||
Divider().background(palette.divider)
|
||||
|
||||
cursorDragNavigationToggleRow
|
||||
}
|
||||
.background(palette.surface, in: RoundedRectangle(cornerRadius: Radius.xl, style: .continuous))
|
||||
@@ -220,6 +212,30 @@ struct SettingsView: View {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Dictionary & polish
|
||||
|
||||
private var dictionaryAndPolishSection: some View {
|
||||
VStack(alignment: .leading, spacing: SettingsListMetrics.sectionLabelSpacing) {
|
||||
sectionHeader("settings.dictionaryAndPolish.title")
|
||||
VStack(spacing: 0) {
|
||||
polishIntensityPreferenceRows
|
||||
|
||||
Divider().background(palette.divider)
|
||||
|
||||
TranslationPickerRow(config: config, isVisible: config.isTranslationRowVisible)
|
||||
|
||||
Divider().background(palette.divider)
|
||||
|
||||
PersonalDictionaryICloudSyncRow()
|
||||
}
|
||||
.background(palette.surface, in: RoundedRectangle(cornerRadius: Radius.xl, style: .continuous))
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: Radius.xl, style: .continuous)
|
||||
.stroke(palette.divider, lineWidth: 0.5)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// v0.2.1 follow-up: dedicated section for the local engine's
|
||||
/// settings (cloud-polish toggle + translation row). Renders only
|
||||
/// when `engineMode == "local"` so the cloud-engine user doesn't
|
||||
|
||||
@@ -98,8 +98,10 @@
|
||||
"settings.engine.local.ios26" = "Always on-device, no network.";
|
||||
"settings.engine.local.legacy" = "On-device ASR. Transcription only, no polish.";
|
||||
"settings.engine.cloud.title" = "Cloud recognition & polish";
|
||||
"settings.engine.cloud.subtitle" = "On-device ASR + polish via your API. Text is sent to the endpoint you configure.";
|
||||
"settings.engine.cloud.subtitle" = "Cloud ASR (with your dictionary) + API polish. Audio is sent to your provider.";
|
||||
"settings.engine.cloud.badge" = "Cloud engine";
|
||||
"settings.provider.title" = "Provider";
|
||||
"settings.provider.personalDictionaryBadge" = "Personal dictionary";
|
||||
"settings.provider.subtitle" = "Pick the LLM that polishes your dictation.";
|
||||
"provider.openai" = "OpenAI";
|
||||
"provider.deepseek" = "DeepSeek";
|
||||
@@ -141,6 +143,7 @@
|
||||
"settings.systemPrompt.hint" = "Used by the cloud LLM when polishing your dictation. Local engine skips this step.";
|
||||
"settings.about.title" = "About";
|
||||
"settings.preferences.title" = "Preferences";
|
||||
"settings.dictionaryAndPolish.title" = "Dictionary & polish";
|
||||
"settings.handedness.title" = "Handedness";
|
||||
"settings.handedness.left" = "Left hand";
|
||||
"settings.handedness.right" = "Right hand";
|
||||
@@ -283,6 +286,7 @@
|
||||
"home.flow.active" = "Voice session active";
|
||||
"home.flow.label" = "Ready";
|
||||
"home.flow.inactive" = "Voice session inactive";
|
||||
"home.flow.notReady" = "Not ready";
|
||||
"home.flow.hint" = "Switch to any app and tap the keyboard mic to dictate.";
|
||||
"home.setup.permission.mic" = "Microphone access is off — voice input won't work.";
|
||||
"home.setup.permission.speech" = "Speech recognition is off — voice input won't work.";
|
||||
@@ -346,7 +350,7 @@
|
||||
"settings.personalDictionary.intro.title" = "About your dictionary";
|
||||
"settings.personalDictionary.intro.body" = "Add words manually. Common speech-recognition mishearings are generated automatically after you save. Tap a word to edit.";
|
||||
"settings.personalDictionary.empty.title" = "No words yet";
|
||||
"settings.personalDictionary.empty.body" = "Add words you want protected during speech correction.";
|
||||
"settings.personalDictionary.empty.body" = "Add your personal dictionary to improve recognition accuracy\nSyncs across devices via iCloud";
|
||||
"settings.personalDictionary.search.prompt" = "Search words";
|
||||
"settings.personalDictionary.usageCount" = "%lld uses";
|
||||
"settings.personalDictionary.add.title" = "Add word";
|
||||
@@ -358,6 +362,11 @@
|
||||
"settings.personalDictionary.clearAll.confirmTitle" = "Clear all dictionary words?";
|
||||
"settings.personalDictionary.clearAll.message" = "This removes every word the LLM was protecting. This cannot be undone.";
|
||||
"settings.personalDictionary.clearAll.confirm" = "Clear all";
|
||||
"settings.personalDictionary.iCloudSync.title" = "Sync via iCloud";
|
||||
"settings.personalDictionary.iCloudSync.settingsTitle" = "Personal dictionary iCloud sync";
|
||||
"settings.personalDictionary.iCloudSync.subtitle" = "Keep your dictionary in sync across iPhone, iPad, and Mac. Data stays in your private iCloud account and is not used for training.";
|
||||
"settings.personalDictionary.iCloudSync.error.tooLarge" = "Dictionary is too large to sync via iCloud. Remove some entries and try again.";
|
||||
"settings.personalDictionary.iCloudSync.error.generic" = "Could not sync your dictionary with iCloud. Try again later.";
|
||||
|
||||
/* v0.3.0: Polish intensity */
|
||||
"settings.polishIntensity.title" = "Polish intensity";
|
||||
|
||||
@@ -98,8 +98,10 @@
|
||||
"settings.engine.local.ios26" = "全程在手机本地,不用联网";
|
||||
"settings.engine.local.legacy" = "端侧 ASR,仅转录,无润色。";
|
||||
"settings.engine.cloud.title" = "云端识别与润色";
|
||||
"settings.engine.cloud.subtitle" = "本地转写 + 你配置的 API 润色,文字发往该第三方服务";
|
||||
"settings.engine.cloud.subtitle" = "云端 ASR(含个性词库)+ API 润色,音频将发往第三方服务";
|
||||
"settings.engine.cloud.badge" = "云端引擎";
|
||||
"settings.provider.title" = "云端引擎";
|
||||
"settings.provider.personalDictionaryBadge" = "个性词库";
|
||||
"settings.provider.subtitle" = "选择 LLM 提供商。";
|
||||
"provider.openai" = "OpenAI";
|
||||
"provider.deepseek" = "DeepSeek";
|
||||
@@ -141,6 +143,7 @@
|
||||
"settings.systemPrompt.hint" = "云端润色时使用。本地识别模式不会用到此提示词。";
|
||||
"settings.about.title" = "关于";
|
||||
"settings.preferences.title" = "偏好设置";
|
||||
"settings.dictionaryAndPolish.title" = "词库与润色";
|
||||
"settings.handedness.title" = "握持偏好";
|
||||
"settings.handedness.left" = "左手";
|
||||
"settings.handedness.right" = "右手";
|
||||
@@ -282,6 +285,7 @@
|
||||
"home.flow.active" = "语音会话进行中";
|
||||
"home.flow.label" = "就绪";
|
||||
"home.flow.inactive" = "语音会话未启动";
|
||||
"home.flow.notReady" = "未就绪";
|
||||
"home.flow.hint" = "切到别的 App,点键盘麦克风就能说。";
|
||||
"home.setup.permission.mic" = "麦克风还没授权,语音输入用不了。";
|
||||
"home.setup.permission.speech" = "语音识别还没授权,语音输入用不了。";
|
||||
@@ -345,7 +349,7 @@
|
||||
"settings.personalDictionary.intro.title" = "关于词库";
|
||||
"settings.personalDictionary.intro.body" = "手动添加词条;保存后会自动生成常见误识别写法。点击词条可编辑。";
|
||||
"settings.personalDictionary.empty.title" = "还没有词条";
|
||||
"settings.personalDictionary.empty.body" = "添加希望在语音纠错时保护的词汇。";
|
||||
"settings.personalDictionary.empty.body" = "添加个性化词库,提升识别准确性\n词库将通过 iCloud 实现跨设备同步";
|
||||
"settings.personalDictionary.search.prompt" = "搜索词条";
|
||||
"settings.personalDictionary.usageCount" = "使用 %lld 次";
|
||||
"settings.personalDictionary.add.title" = "添加词条";
|
||||
@@ -357,9 +361,14 @@
|
||||
"settings.personalDictionary.clearAll.confirmTitle" = "清空全部词条?";
|
||||
"settings.personalDictionary.clearAll.message" = "这会移除所有受保护的词汇,且无法撤销。";
|
||||
"settings.personalDictionary.clearAll.confirm" = "全部清空";
|
||||
"settings.personalDictionary.iCloudSync.title" = "通过 iCloud 同步";
|
||||
"settings.personalDictionary.iCloudSync.settingsTitle" = "个性词库 iCloud 同步";
|
||||
"settings.personalDictionary.iCloudSync.subtitle" = "在 iPhone、iPad 和 Mac 之间同步词库。数据仅保存在你的私人 iCloud 账户中,不会用于训练。";
|
||||
"settings.personalDictionary.iCloudSync.error.tooLarge" = "词库过大,无法通过 iCloud 同步。请删除部分词条后重试。";
|
||||
"settings.personalDictionary.iCloudSync.error.generic" = "无法与 iCloud 同步词库,请稍后重试。";
|
||||
|
||||
/* v0.3.0: 润色档位 */
|
||||
"settings.polishIntensity.title" = "润色档位";
|
||||
/* v0.3.0: 润色强度 */
|
||||
"settings.polishIntensity.title" = "润色强度";
|
||||
|
||||
/* Flow 会话策略 */
|
||||
"settings.flow.title" = "语音会话";
|
||||
|
||||
Reference in New Issue
Block a user