From 62fd866df08407899eb3b415d00cf13072023f50 Mon Sep 17 00:00:00 2001 From: Hubert Date: Wed, 4 Feb 2026 16:34:48 +0800 Subject: [PATCH] feat(i18n): add complete internationalization support for English and Simplified Chinese - Add .lproj directory structure (en.lproj, zh-Hans.lproj) - Create comprehensive Localizable.strings for both languages (~410 entries) - Add LanguageManager for runtime language switching - Add AppLanguagePicker in Settings for user language selection - Replace hardcoded strings with L() localization helper - Fix PaddleOCRChecker to use async availability check (prevents SIGABRT) - Fix AttributeGraph cycle in SettingsView by deferring permission checks - Menu bar rebuilds automatically when language changes - Settings view refreshes in real-time on language change --- ScreenTranslate.xcodeproj/project.pbxproj | 1 + ScreenTranslate/App/AppDelegate.swift | 3 + .../Features/History/HistoryView.swift | 28 +- .../Features/MenuBar/MenuBarController.swift | 31 +- .../Features/Preview/PreviewContentView.swift | 76 ++-- .../Features/Settings/SettingsView.swift | 171 +++++--- .../Settings/SettingsWindowController.swift | 9 +- ScreenTranslate/Models/AppLanguage.swift | 186 ++++++++ ScreenTranslate/Models/OCREngineType.swift | 57 +-- .../{ => en.lproj}/Localizable.strings | 335 +++++++++++--- .../zh-Hans.lproj/Localizable.strings | 415 ++++++++++++++++++ 11 files changed, 1093 insertions(+), 219 deletions(-) create mode 100644 ScreenTranslate/Models/AppLanguage.swift rename ScreenTranslate/Resources/{ => en.lproj}/Localizable.strings (53%) create mode 100644 ScreenTranslate/Resources/zh-Hans.lproj/Localizable.strings diff --git a/ScreenTranslate.xcodeproj/project.pbxproj b/ScreenTranslate.xcodeproj/project.pbxproj index ac4b09b..7be0898 100644 --- a/ScreenTranslate.xcodeproj/project.pbxproj +++ b/ScreenTranslate.xcodeproj/project.pbxproj @@ -104,6 +104,7 @@ knownRegions = ( en, Base, + "zh-Hans", ); mainGroup = SC000004; minimizedProjectReferenceProxies = 1; diff --git a/ScreenTranslate/App/AppDelegate.swift b/ScreenTranslate/App/AppDelegate.swift index 411c594..e9f6e79 100644 --- a/ScreenTranslate/App/AppDelegate.swift +++ b/ScreenTranslate/App/AppDelegate.swift @@ -37,6 +37,9 @@ final class AppDelegate: NSObject, NSApplicationDelegate { Task { await checkFirstLaunchAndShowOnboarding() } + + // Check PaddleOCR availability in background (non-blocking) + PaddleOCRChecker.checkAvailabilityAsync() #if DEBUG print("ScreenTranslate launched - settings loaded from: \(settings.saveLocation.path)") diff --git a/ScreenTranslate/Features/History/HistoryView.swift b/ScreenTranslate/Features/History/HistoryView.swift index 6c96e5b..14c5e41 100644 --- a/ScreenTranslate/Features/History/HistoryView.swift +++ b/ScreenTranslate/Features/History/HistoryView.swift @@ -62,7 +62,7 @@ private struct SearchBar: View { Image(systemName: "magnifyingglass") .foregroundStyle(.secondary) - TextField("Search history...", text: Binding( + TextField(String(localized: "history.search.placeholder"), text: Binding( get: { store.searchQuery }, set: { store.search($0) } )) @@ -94,7 +94,7 @@ private struct SearchBar: View { .foregroundStyle(.secondary) } .buttonStyle(.plain) - .help("Clear all history") + .help(String(localized: "history.clear.all")) } } .padding(12) @@ -135,26 +135,26 @@ private struct EmptyStateView: View { .foregroundStyle(.secondary) if store.searchQuery.isEmpty { - Text("No Translation History") + Text("history.empty.title") .font(.headline) .foregroundStyle(.secondary) - Text("Your translated screenshots will appear here") + Text("history.empty.message") .font(.body) .foregroundStyle(.tertiary) } else { - Text("No Results") + Text("history.no.results.title") .font(.headline) .foregroundStyle(.secondary) - Text("No entries match your search") + Text("history.no.results.message") .font(.body) .foregroundStyle(.tertiary) Button { store.search("") } label: { - Text("Clear Search") + Text("history.clear.search") } .buttonStyle(.borderedProminent) } @@ -204,7 +204,7 @@ private struct HistoryEntryRow: View { TextSection( text: entry.sourcePreview, isTruncated: entry.isSourceTruncated, - label: "Source" + label: String(localized: "history.source") ) // Arrow separator @@ -227,7 +227,7 @@ private struct HistoryEntryRow: View { TextSection( text: entry.translatedPreview, isTruncated: entry.isTranslatedTruncated, - label: "Translation" + label: String(localized: "history.translation") ) } @@ -291,7 +291,7 @@ private struct TextSection: View { HStack(spacing: 4) { Image(systemName: "ellipsis") .font(.caption2) - Text("truncated") + Text("history.truncated") .font(.caption2) } .foregroundStyle(.tertiary) @@ -312,19 +312,19 @@ private struct EntryContextMenu: View { Button { store.copyTranslation(entry) } label: { - Label("Copy Translation", systemImage: "doc.on.doc") + Label(String(localized: "history.copy.translation"), systemImage: "doc.on.doc") } Button { store.copySource(entry) } label: { - Label("Copy Source", systemImage: "doc.on.doc") + Label(String(localized: "history.copy.source"), systemImage: "doc.on.doc") } Button { store.copyBoth(entry) } label: { - Label("Copy Both", systemImage: "doc.on.clipboard") + Label(String(localized: "history.copy.both"), systemImage: "doc.on.clipboard") } Divider() @@ -332,7 +332,7 @@ private struct EntryContextMenu: View { Button(role: .destructive) { store.remove(entry) } label: { - Label("Delete", systemImage: "trash") + Label(String(localized: "history.delete"), systemImage: "trash") } } } diff --git a/ScreenTranslate/Features/MenuBar/MenuBarController.swift b/ScreenTranslate/Features/MenuBar/MenuBarController.swift index 9d1a9fd..e67e991 100644 --- a/ScreenTranslate/Features/MenuBar/MenuBarController.swift +++ b/ScreenTranslate/Features/MenuBar/MenuBarController.swift @@ -23,6 +23,16 @@ final class MenuBarController { init(appDelegate: AppDelegate, recentCapturesStore: RecentCapturesStore) { self.appDelegate = appDelegate self.recentCapturesStore = recentCapturesStore + + NotificationCenter.default.addObserver( + forName: LanguageManager.languageDidChangeNotification, + object: nil, + queue: .main + ) { [weak self] _ in + Task { @MainActor in + self?.rebuildMenu() + } + } } // MARK: - Setup @@ -46,6 +56,11 @@ final class MenuBarController { statusItem = nil } } + + /// Rebuilds the menu when language changes + func rebuildMenu() { + statusItem?.menu = buildMenu() + } // MARK: - Menu Construction @@ -55,7 +70,7 @@ final class MenuBarController { // Capture Full Screen let fullScreenItem = NSMenuItem( - title: NSLocalizedString("menu.capture.full.screen", comment: "Capture Full Screen"), + title: NSLocalizedString("menu.capture.full.screen", tableName: "Localizable", bundle: .main, comment: "Capture Full Screen"), action: #selector(AppDelegate.captureFullScreen), keyEquivalent: "3" ) @@ -65,7 +80,7 @@ final class MenuBarController { // Capture Selection let selectionItem = NSMenuItem( - title: NSLocalizedString("menu.capture.selection", comment: "Capture Selection"), + title: NSLocalizedString("menu.capture.selection", tableName: "Localizable", bundle: .main, comment: "Capture Selection"), action: #selector(AppDelegate.captureSelection), keyEquivalent: "4" ) @@ -77,7 +92,7 @@ final class MenuBarController { // Recent Captures submenu let recentItem = NSMenuItem( - title: NSLocalizedString("menu.recent.captures", comment: "Recent Captures"), + title: NSLocalizedString("menu.recent.captures", tableName: "Localizable", bundle: .main, comment: "Recent Captures"), action: nil, keyEquivalent: "" ) @@ -89,7 +104,7 @@ final class MenuBarController { // Translation History let historyItem = NSMenuItem( - title: NSLocalizedString("menu.translation.history", comment: "Translation History"), + title: NSLocalizedString("menu.translation.history", tableName: "Localizable", bundle: .main, comment: "Translation History"), action: #selector(AppDelegate.openHistory), keyEquivalent: "h" ) @@ -101,7 +116,7 @@ final class MenuBarController { // Settings let settingsItem = NSMenuItem( - title: NSLocalizedString("menu.settings", comment: "Settings..."), + title: NSLocalizedString("menu.settings", tableName: "Localizable", bundle: .main, comment: "Settings..."), action: #selector(AppDelegate.openSettings), keyEquivalent: "," ) @@ -113,7 +128,7 @@ final class MenuBarController { // Quit let quitItem = NSMenuItem( - title: NSLocalizedString("menu.quit", comment: "Quit ScreenTranslate"), + title: NSLocalizedString("menu.quit", tableName: "Localizable", bundle: .main, comment: "Quit ScreenTranslate"), action: #selector(NSApplication.terminate(_:)), keyEquivalent: "q" ) @@ -144,7 +159,7 @@ final class MenuBarController { if captures.isEmpty { let emptyItem = NSMenuItem( - title: NSLocalizedString("menu.recent.captures.empty", comment: "No Recent Captures"), + title: NSLocalizedString("menu.recent.captures.empty", tableName: "Localizable", bundle: .main, comment: "No Recent Captures"), action: nil, keyEquivalent: "" ) @@ -161,7 +176,7 @@ final class MenuBarController { menu.addItem(NSMenuItem.separator()) let clearItem = NSMenuItem( - title: NSLocalizedString("menu.recent.captures.clear", comment: "Clear Recent"), + title: NSLocalizedString("menu.recent.captures.clear", tableName: "Localizable", bundle: .main, comment: "Clear Recent"), action: #selector(clearRecentCaptures), keyEquivalent: "" ) diff --git a/ScreenTranslate/Features/Preview/PreviewContentView.swift b/ScreenTranslate/Features/Preview/PreviewContentView.swift index f8a0abd..1981233 100644 --- a/ScreenTranslate/Features/Preview/PreviewContentView.swift +++ b/ScreenTranslate/Features/Preview/PreviewContentView.swift @@ -45,11 +45,11 @@ struct PreviewContentView: View { } } .alert( - "Error", + String(localized: "error.title"), isPresented: .constant(viewModel.errorMessage != nil), presenting: viewModel.errorMessage ) { _ in - Button("OK") { + Button(String(localized: "button.ok")) { viewModel.errorMessage = nil } } message: { message in @@ -84,7 +84,7 @@ struct PreviewContentView: View { ZStack(alignment: .topLeading) { // Base image - Image(viewModel.image, scale: 1.0, label: Text("Screenshot")) + Image(viewModel.image, scale: 1.0, label: Text("preview.screenshot")) .resizable() .aspectRatio(contentMode: .fit) .frame( @@ -321,7 +321,7 @@ struct PreviewContentView: View { y: position.y * scale ) - return TextField("Enter text", text: $viewModel.textInputContent) + return TextField(String(localized: "preview.enter.text"), text: $viewModel.textInputContent) .textFieldStyle(.plain) .font(.system(size: 14 * scale)) .foregroundColor(AppSettings.shared.strokeColor.color) @@ -357,14 +357,14 @@ struct PreviewContentView: View { .cornerRadius(6) .foregroundStyle(.secondary) .accessibilityElement(children: .combine) - .accessibilityLabel(Text("Active tool: \(tool.displayName)")) + .accessibilityLabel(Text("\(String(localized: "preview.active.tool")): \(tool.displayName)")) } /// Crop mode indicator badge private var cropModeIndicator: some View { HStack(spacing: 4) { Image(systemName: "crop") - Text("Crop") + Text("preview.crop") .font(.caption) } .padding(.horizontal, 8) @@ -373,7 +373,7 @@ struct PreviewContentView: View { .cornerRadius(6) .foregroundStyle(.secondary) .accessibilityElement(children: .combine) - .accessibilityLabel(Text("Crop mode active")) + .accessibilityLabel(Text("preview.crop.mode.active")) } /// Overlay for capturing crop selection gestures @@ -475,7 +475,7 @@ struct PreviewContentView: View { Button { viewModel.cancelCrop() } label: { - Label("Cancel", systemImage: "xmark") + Label(String(localized: "action.cancel"), systemImage: "xmark") } .buttonStyle(.bordered) .keyboardShortcut(.escape, modifiers: []) @@ -483,7 +483,7 @@ struct PreviewContentView: View { Button { viewModel.applyCrop() } label: { - Label("Apply Crop", systemImage: "checkmark") + Label(String(localized: "preview.crop.apply"), systemImage: "checkmark") } .buttonStyle(.borderedProminent) .keyboardShortcut(.return, modifiers: []) @@ -502,7 +502,7 @@ struct PreviewContentView: View { Text(viewModel.dimensionsText) .font(.system(.caption, design: .monospaced)) .foregroundStyle(.secondary) - .help("Image dimensions") + .help(String(localized: "preview.image.dimensions")) Text("•") .foregroundStyle(.tertiary) @@ -510,7 +510,7 @@ struct PreviewContentView: View { Text(viewModel.fileSizeText) .font(.system(.caption, design: .monospaced)) .foregroundStyle(.secondary) - .help("Estimated file size") + .help(String(localized: "preview.estimated.size")) } .fixedSize() @@ -584,7 +584,7 @@ struct PreviewContentView: View { HStack(spacing: 8) { // Show "Editing" label when modifying existing annotation if isEditingAnnotation { - Text("Edit:") + Text("preview.edit.label") .font(.caption) .foregroundStyle(.secondary) } @@ -667,7 +667,7 @@ struct PreviewContentView: View { : Color.clear ) .clipShape(RoundedRectangle(cornerRadius: 4)) - .help(isFilled ? "Filled (click for hollow)" : "Hollow (click for filled)") + .help(isFilled ? String(localized: "preview.shape.filled") : String(localized: "preview.shape.hollow")) Divider() .frame(height: 16) @@ -701,7 +701,7 @@ struct PreviewContentView: View { step: 0.5 ) .frame(width: 80) - .help("Stroke Width") + .help(String(localized: "settings.stroke.width")) let width = isEditingAnnotation ? Int(viewModel.selectedAnnotationStrokeWidth ?? 3) @@ -740,7 +740,7 @@ struct PreviewContentView: View { step: 1 ) .frame(width: 80) - .help("Text Size") + .help(String(localized: "settings.text.size")) let size = isEditingAnnotation ? Int(viewModel.selectedAnnotationFontSize ?? 16) @@ -764,7 +764,7 @@ struct PreviewContentView: View { .foregroundStyle(.red) } .buttonStyle(.plain) - .help("Delete selected annotation (Delete)") + .help(String(localized: "preview.tooltip.delete")) } } } @@ -789,15 +789,15 @@ struct PreviewContentView: View { /// Get accessible color name private func colorName(for color: Color) -> String { switch color { - case .red: return "Red" - case .orange: return "Orange" - case .yellow: return "Yellow" - case .green: return "Green" - case .blue: return "Blue" - case .purple: return "Purple" - case .white: return "White" - case .black: return "Black" - default: return "Custom" + case .red: return String(localized: "color.red") + case .orange: return String(localized: "color.orange") + case .yellow: return String(localized: "color.yellow") + case .green: return String(localized: "color.green") + case .blue: return String(localized: "color.blue") + case .purple: return String(localized: "color.purple") + case .white: return String(localized: "color.white") + case .black: return String(localized: "color.black") + default: return String(localized: "color.custom") } } @@ -805,7 +805,7 @@ struct PreviewContentView: View { VStack(alignment: .leading, spacing: 8) { if viewModel.hasOCRResults { VStack(alignment: .leading, spacing: 4) { - Text("Recognized Text:") + Text("preview.recognized.text") .font(.caption) .foregroundStyle(.secondary) Text(viewModel.combinedOCRText) @@ -816,7 +816,7 @@ struct PreviewContentView: View { if viewModel.hasTranslationResults { VStack(alignment: .leading, spacing: 4) { - Text("Translation:") + Text("preview.translation") .font(.caption) .foregroundStyle(.secondary) Text(viewModel.combinedTranslatedText) @@ -842,8 +842,8 @@ struct PreviewContentView: View { : Color.clear ) .clipShape(RoundedRectangle(cornerRadius: 4)) - .help("Crop (C)") - .accessibilityLabel(Text("Crop")) + .help(String(localized: "preview.tooltip.crop")) + .accessibilityLabel(Text("preview.crop")) .accessibilityHint(Text("Press C to toggle")) Divider() @@ -857,8 +857,8 @@ struct PreviewContentView: View { Image(systemName: "arrow.uturn.backward") } .disabled(!viewModel.canUndo) - .help("Undo (⌘Z)") - .accessibilityLabel(Text("Undo")) + .help(String(localized: "preview.tooltip.undo")) + .accessibilityLabel(Text("action.undo")) .accessibilityHint(Text("Command Z")) Button { @@ -867,8 +867,8 @@ struct PreviewContentView: View { Image(systemName: "arrow.uturn.forward") } .disabled(!viewModel.canRedo) - .help("Redo (⌘⇧Z)") - .accessibilityLabel(Text("Redo")) + .help(String(localized: "preview.tooltip.redo")) + .accessibilityLabel(Text("action.redo")) .accessibilityHint(Text("Command Shift Z")) Divider() @@ -894,7 +894,7 @@ struct PreviewContentView: View { } } .disabled(viewModel.isCopying) - .help("Copy to Clipboard (⌘C)") + .help(String(localized: "preview.tooltip.copy")) .accessibilityLabel(Text(viewModel.isCopying ? "Copying to clipboard" : "Copy to clipboard")) .accessibilityHint(Text("Command C")) @@ -916,7 +916,7 @@ struct PreviewContentView: View { } } .disabled(viewModel.isSaving) - .help("Save (⌘S or Enter)") + .help(String(localized: "preview.tooltip.save")) .accessibilityLabel(Text(viewModel.isSaving ? "Saving screenshot" : "Save screenshot")) .accessibilityHint(Text("Command S or Enter")) @@ -936,7 +936,7 @@ struct PreviewContentView: View { } } .disabled(viewModel.isPerformingOCR) - .help("Recognize Text (OCR)") + .help(String(localized: "preview.tooltip.ocr")) Button { viewModel.performTranslation() @@ -950,7 +950,7 @@ struct PreviewContentView: View { } } .disabled(viewModel.isPerformingTranslation || !viewModel.hasOCRResults) - .help("Translate Text") + .help(String(localized: "preview.tooltip.translate")) Divider() .frame(height: 16) @@ -962,7 +962,7 @@ struct PreviewContentView: View { } label: { Image(systemName: "xmark") } - .help("Dismiss (Escape)") + .help(String(localized: "preview.tooltip.dismiss")) .accessibilityLabel(Text("Dismiss preview")) .accessibilityHint(Text("Escape key")) } diff --git a/ScreenTranslate/Features/Settings/SettingsView.swift b/ScreenTranslate/Features/Settings/SettingsView.swift index b16ca6e..e73e834 100644 --- a/ScreenTranslate/Features/Settings/SettingsView.swift +++ b/ScreenTranslate/Features/Settings/SettingsView.swift @@ -5,6 +5,7 @@ import AppKit /// Organized into sections: General, Export, Keyboard Shortcuts, and Annotations. struct SettingsView: View { @Bindable var viewModel: SettingsViewModel + @State private var refreshID = UUID() var body: some View { Form { @@ -12,14 +13,15 @@ struct SettingsView: View { Section { PermissionRow(viewModel: viewModel) } header: { - Label("Permissions", systemImage: "lock.shield") + Label(L("settings.section.permissions"), systemImage: "lock.shield") } // General Settings Section Section { + AppLanguagePicker() SaveLocationPicker(viewModel: viewModel) } header: { - Label("General", systemImage: "gearshape") + Label(L("settings.section.general"), systemImage: "gearshape") } // Engine Settings Section @@ -28,7 +30,7 @@ struct SettingsView: View { TranslationEnginePicker(viewModel: viewModel) TranslationModePicker(viewModel: viewModel) } header: { - Label("Engines", systemImage: "engine.combustion") + Label(L("settings.section.engines"), systemImage: "engine.combustion") } // Language Settings Section @@ -36,7 +38,7 @@ struct SettingsView: View { SourceLanguagePicker(viewModel: viewModel) TargetLanguagePicker(viewModel: viewModel) } header: { - Label("Languages", systemImage: "globe") + Label(L("settings.section.languages"), systemImage: "globe") } // Export Settings Section @@ -48,13 +50,13 @@ struct SettingsView: View { HEICQualitySlider(viewModel: viewModel) } } header: { - Label("Export", systemImage: "square.and.arrow.up") + Label(L("settings.section.export"), systemImage: "square.and.arrow.up") } // Keyboard Shortcuts Section Section { ShortcutRecorder( - label: "Full Screen Capture", + label: L("settings.shortcut.fullscreen"), shortcut: viewModel.fullScreenShortcut, isRecording: viewModel.isRecordingFullScreenShortcut, onRecord: { viewModel.startRecordingFullScreenShortcut() }, @@ -62,14 +64,14 @@ struct SettingsView: View { ) ShortcutRecorder( - label: "Selection Capture", + label: L("settings.shortcut.selection"), shortcut: viewModel.selectionShortcut, isRecording: viewModel.isRecordingSelectionShortcut, onRecord: { viewModel.startRecordingSelectionShortcut() }, onReset: { viewModel.resetSelectionShortcut() } ) } header: { - Label("Keyboard Shortcuts", systemImage: "keyboard") + Label(L("settings.section.shortcuts"), systemImage: "keyboard") } // Annotation Settings Section @@ -78,7 +80,7 @@ struct SettingsView: View { StrokeWidthSlider(viewModel: viewModel) TextSizeSlider(viewModel: viewModel) } header: { - Label("Annotations", systemImage: "pencil.tip.crop.circle") + Label(L("settings.section.annotations"), systemImage: "pencil.tip.crop.circle") } // Reset Section @@ -86,7 +88,7 @@ struct SettingsView: View { Button(role: .destructive) { viewModel.resetAllToDefaults() } label: { - Label("Reset All to Defaults", systemImage: "arrow.counterclockwise") + Label(L("settings.reset.all"), systemImage: "arrow.counterclockwise") } .buttonStyle(.plain) .foregroundStyle(.red) @@ -94,8 +96,12 @@ struct SettingsView: View { } .formStyle(.grouped) .frame(minWidth: 450, minHeight: 500) - .alert("Error", isPresented: $viewModel.showErrorAlert) { - Button("OK") { + .id(refreshID) + .onReceive(NotificationCenter.default.publisher(for: LanguageManager.languageDidChangeNotification)) { _ in + refreshID = UUID() + } + .alert(L("error.title"), isPresented: $viewModel.showErrorAlert) { + Button(L("button.ok")) { viewModel.errorMessage = nil } } message: { @@ -117,8 +123,8 @@ private struct PermissionRow: View { // Screen Recording permission PermissionItem( icon: "record.circle", - title: "Screen Recording", - hint: "Required to capture screenshots", + title: L("settings.permission.screen.recording"), + hint: L("settings.permission.screen.recording.hint"), isGranted: viewModel.hasScreenRecordingPermission, isChecking: viewModel.isCheckingPermissions, onGrant: { viewModel.requestScreenRecordingPermission() } @@ -129,8 +135,8 @@ private struct PermissionRow: View { // Folder Access permission PermissionItem( icon: "folder", - title: "Save Location Access", - hint: "Required to save screenshots to the selected folder", + title: L("settings.save.location"), + hint: L("settings.save.location.message"), isGranted: viewModel.hasFolderAccessPermission, isChecking: viewModel.isCheckingPermissions, onGrant: { viewModel.requestFolderAccess() } @@ -141,7 +147,7 @@ private struct PermissionRow: View { Button { viewModel.checkPermissions() } label: { - Label("Refresh", systemImage: "arrow.clockwise") + Label(L("action.reset"), systemImage: "arrow.clockwise") } .buttonStyle(.borderless) } @@ -178,7 +184,7 @@ private struct PermissionItem: View { if isGranted { Image(systemName: "checkmark.circle.fill") .foregroundStyle(.green) - Text("Granted") + Text(L("settings.permission.granted")) .foregroundStyle(.secondary) } else { Image(systemName: "xmark.circle.fill") @@ -187,7 +193,7 @@ private struct PermissionItem: View { Button { onGrant() } label: { - Text("Grant Access") + Text(L("settings.permission.grant")) } .buttonStyle(.borderedProminent) .controlSize(.small) @@ -203,7 +209,7 @@ private struct PermissionItem: View { } } .accessibilityElement(children: .combine) - .accessibilityLabel(Text("\(title): \(isGranted ? "Granted" : "Not Granted")")) + .accessibilityLabel(Text("\(title): \(isGranted ? L("settings.permission.granted") : L("settings.permission.not.granted"))")) } } @@ -216,7 +222,7 @@ private struct SaveLocationPicker: View { var body: some View { HStack { VStack(alignment: .leading, spacing: 4) { - Text("Save Location") + Text(L("settings.save.location")) .font(.headline) Text(viewModel.saveLocationPath) .font(.caption) @@ -230,7 +236,7 @@ private struct SaveLocationPicker: View { Button { viewModel.selectSaveLocation() } label: { - Text("Choose...") + Text(L("settings.save.location.choose")) } Button { @@ -238,10 +244,10 @@ private struct SaveLocationPicker: View { } label: { Image(systemName: "folder") } - .help("Show in Finder") + .help(L("settings.save.location.reveal")) } .accessibilityElement(children: .combine) - .accessibilityLabel(Text("Save Location: \(viewModel.saveLocationPath)")) + .accessibilityLabel(Text("\(L("settings.save.location")): \(viewModel.saveLocationPath)")) } } @@ -252,13 +258,13 @@ private struct ExportFormatPicker: View { @Bindable var viewModel: SettingsViewModel var body: some View { - Picker("Default Format", selection: $viewModel.defaultFormat) { - Text("PNG").tag(ExportFormat.png) - Text("JPEG").tag(ExportFormat.jpeg) - Text("HEIC").tag(ExportFormat.heic) + Picker(L("settings.format"), selection: $viewModel.defaultFormat) { + Text(L("settings.format.png")).tag(ExportFormat.png) + Text(L("settings.format.jpeg")).tag(ExportFormat.jpeg) + Text(L("settings.format.heic")).tag(ExportFormat.heic) } .pickerStyle(.segmented) - .accessibilityLabel(Text("Export Format")) + .accessibilityLabel(Text(L("settings.format"))) } } @@ -271,7 +277,7 @@ private struct JPEGQualitySlider: View { var body: some View { VStack(alignment: .leading, spacing: 8) { HStack { - Text("JPEG Quality") + Text(L("settings.jpeg.quality")) Spacer() Text("\(Int(viewModel.jpegQualityPercentage))%") .foregroundStyle(.secondary) @@ -283,7 +289,7 @@ private struct JPEGQualitySlider: View { in: SettingsViewModel.jpegQualityRange, step: 0.05 ) { - Text("JPEG Quality") + Text(L("settings.jpeg.quality")) } minimumValueLabel: { Text("10%") .font(.caption) @@ -293,7 +299,7 @@ private struct JPEGQualitySlider: View { } .accessibilityValue(Text("\(Int(viewModel.jpegQualityPercentage)) percent")) - Text("Higher quality results in larger file sizes") + Text(L("settings.jpeg.quality.hint")) .font(.caption) .foregroundStyle(.secondary) } @@ -309,7 +315,7 @@ private struct HEICQualitySlider: View { var body: some View { VStack(alignment: .leading, spacing: 8) { HStack { - Text("HEIC Quality") + Text(L("settings.heic.quality")) Spacer() Text("\(Int(viewModel.heicQualityPercentage))%") .foregroundStyle(.secondary) @@ -321,7 +327,7 @@ private struct HEICQualitySlider: View { in: SettingsViewModel.heicQualityRange, step: 0.05 ) { - Text("HEIC Quality") + Text(L("settings.heic.quality")) } minimumValueLabel: { Text("10%") .font(.caption) @@ -331,7 +337,7 @@ private struct HEICQualitySlider: View { } .accessibilityValue(Text("\(Int(viewModel.heicQualityPercentage)) percent")) - Text("HEIC offers better compression than JPEG at similar quality") + Text(L("settings.heic.quality.hint")) .font(.caption) .foregroundStyle(.secondary) } @@ -355,7 +361,7 @@ private struct ShortcutRecorder: View { Spacer() if isRecording { - Text("Press keys...") + Text(L("settings.shortcut.recording")) .foregroundStyle(.secondary) .padding(.horizontal, 12) .padding(.vertical, 6) @@ -380,7 +386,7 @@ private struct ShortcutRecorder: View { Image(systemName: "arrow.counterclockwise") } .buttonStyle(.borderless) - .help("Reset to default") + .help(L("settings.shortcut.reset")) .disabled(isRecording) } .accessibilityElement(children: .combine) @@ -396,7 +402,7 @@ private struct StrokeColorPicker: View { var body: some View { HStack { - Text("Stroke Color") + Text(L("settings.stroke.color")) Spacer() @@ -434,7 +440,7 @@ private struct StrokeColorPicker: View { .frame(width: 30) } .accessibilityElement(children: .combine) - .accessibilityLabel(Text("Stroke Color")) + .accessibilityLabel(Text(L("settings.stroke.color"))) } /// Compare colors approximately @@ -453,16 +459,16 @@ private struct StrokeColorPicker: View { /// Get accessible color name private func colorName(for color: Color) -> String { switch color { - case .red: return "Red" - case .orange: return "Orange" - case .yellow: return "Yellow" - case .green: return "Green" - case .blue: return "Blue" - case .purple: return "Purple" - case .pink: return "Pink" - case .white: return "White" - case .black: return "Black" - default: return "Custom" + case .red: return L("color.red") + case .orange: return L("color.orange") + case .yellow: return L("color.yellow") + case .green: return L("color.green") + case .blue: return L("color.blue") + case .purple: return L("color.purple") + case .pink: return L("color.pink") + case .white: return L("color.white") + case .black: return L("color.black") + default: return L("color.custom") } } } @@ -476,7 +482,7 @@ private struct StrokeWidthSlider: View { var body: some View { VStack(alignment: .leading, spacing: 8) { HStack { - Text("Stroke Width") + Text(L("settings.stroke.width")) Spacer() Text("\(viewModel.strokeWidth, specifier: "%.1f") pt") .foregroundStyle(.secondary) @@ -489,7 +495,7 @@ private struct StrokeWidthSlider: View { in: SettingsViewModel.strokeWidthRange, step: 0.5 ) { - Text("Stroke Width") + Text(L("settings.stroke.width")) } .accessibilityValue(Text("\(viewModel.strokeWidth, specifier: "%.1f") points")) @@ -511,7 +517,7 @@ private struct TextSizeSlider: View { var body: some View { VStack(alignment: .leading, spacing: 8) { HStack { - Text("Text Size") + Text(L("settings.text.size")) Spacer() Text("\(Int(viewModel.textSize)) pt") .foregroundStyle(.secondary) @@ -524,7 +530,7 @@ private struct TextSizeSlider: View { in: SettingsViewModel.textSizeRange, step: 1 ) { - Text("Text Size") + Text(L("settings.text.size")) } .accessibilityValue(Text("\(Int(viewModel.textSize)) points")) @@ -545,7 +551,7 @@ private struct OCREnginePicker: View { @Bindable var viewModel: SettingsViewModel var body: some View { - Picker("OCR Engine", selection: $viewModel.ocrEngine) { + Picker(L("settings.ocr.engine"), selection: $viewModel.ocrEngine) { ForEach(OCREngineType.allCases, id: \.self) { engine in VStack(alignment: .leading, spacing: 4) { HStack { @@ -569,8 +575,11 @@ private struct OCREnginePicker: View { .pickerStyle(.inline) .onChange(of: viewModel.ocrEngine) { _, newValue in // If user selects an unavailable engine, show warning and revert to Vision + // Use Task to avoid setting value during update if !newValue.isAvailable { - viewModel.ocrEngine = .vision + Task { @MainActor in + viewModel.ocrEngine = .vision + } } } } @@ -600,7 +609,7 @@ private struct TranslationEnginePicker: View { @Bindable var viewModel: SettingsViewModel var body: some View { - Picker("Translation Engine", selection: $viewModel.translationEngine) { + Picker(L("settings.translation.engine"), selection: $viewModel.translationEngine) { ForEach(TranslationEngineType.allCases, id: \.self) { engine in VStack(alignment: .leading, spacing: 4) { Text(engine.localizedName) @@ -623,7 +632,7 @@ private struct TranslationModePicker: View { @Bindable var viewModel: SettingsViewModel var body: some View { - Picker("Translation Mode", selection: $viewModel.translationMode) { + Picker(L("settings.translation.mode"), selection: $viewModel.translationMode) { ForEach(TranslationMode.allCases, id: \.self) { mode in VStack(alignment: .leading, spacing: 4) { Text(mode.localizedName) @@ -645,14 +654,14 @@ private struct SourceLanguagePicker: View { @Bindable var viewModel: SettingsViewModel var body: some View { - Picker("Source Language", selection: $viewModel.translationSourceLanguage) { + Picker(L("translation.language.source"), selection: $viewModel.translationSourceLanguage) { ForEach(viewModel.availableSourceLanguages, id: \.rawValue) { language in Text(language.localizedName) .tag(language) } } .pickerStyle(.menu) - .help("The language of the text you want to translate") + .help(L("translation.language.source.hint")) } } @@ -664,7 +673,7 @@ private struct TargetLanguagePicker: View { var body: some View { HStack { - Text("Target Language") + Text(L("translation.language.target")) Spacer() @@ -673,7 +682,7 @@ private struct TargetLanguagePicker: View { viewModel.translationTargetLanguage = nil } label: { HStack { - Text("Follow System") + Text(L("translation.language.follow.system")) if viewModel.translationTargetLanguage == nil { Image(systemName: "checkmark") } @@ -709,14 +718,50 @@ private struct TargetLanguagePicker: View { .menuStyle(.borderlessButton) .fixedSize() } - .help("The language to translate the text into") + .help(L("translation.language.target.hint")) } private var targetLanguageDisplay: String { if let targetLanguage = viewModel.translationTargetLanguage { return targetLanguage.localizedName } - return NSLocalizedString("translation.language.follow.system", comment: "Follow System") + return L("translation.language.follow.system") + } +} + +// MARK: - App Language Picker + +/// Picker for selecting the application display language. +private struct AppLanguagePicker: View { + @State private var selectedLanguage: AppLanguage = .system + @State private var isInitialized = false + + var body: some View { + HStack { + Text(L("settings.language")) + + Spacer() + + Picker("", selection: $selectedLanguage) { + ForEach(AppLanguage.allCases) { language in + Text(language.displayName) + .tag(language) + } + } + .pickerStyle(.menu) + .labelsHidden() + .frame(minWidth: 120) + .onChange(of: selectedLanguage) { _, newValue in + guard isInitialized else { return } + Task { @MainActor in + LanguageManager.shared.currentLanguage = newValue + } + } + } + .onAppear { + selectedLanguage = LanguageManager.shared.currentLanguage + isInitialized = true + } } } diff --git a/ScreenTranslate/Features/Settings/SettingsWindowController.swift b/ScreenTranslate/Features/Settings/SettingsWindowController.swift index 8860b35..4e9e25b 100644 --- a/ScreenTranslate/Features/Settings/SettingsWindowController.swift +++ b/ScreenTranslate/Features/Settings/SettingsWindowController.swift @@ -43,9 +43,6 @@ final class SettingsWindowController: NSObject { let viewModel = SettingsViewModel(settings: AppSettings.shared, appDelegate: appDelegate) self.viewModel = viewModel - // Check permissions before creating the view to avoid state changes during view update - viewModel.checkPermissions() - // Create the SwiftUI view let settingsView = SettingsView(viewModel: viewModel) @@ -79,6 +76,12 @@ final class SettingsWindowController: NSObject { // Show the window window.makeKeyAndOrderFront(nil) NSApp.activate(ignoringOtherApps: true) + + // Check permissions after window is shown to avoid state changes during view initialization + Task { @MainActor in + try? await Task.sleep(for: .milliseconds(100)) + viewModel.checkPermissions() + } } /// Closes the settings window if open. diff --git a/ScreenTranslate/Models/AppLanguage.swift b/ScreenTranslate/Models/AppLanguage.swift new file mode 100644 index 0000000..b6934d2 --- /dev/null +++ b/ScreenTranslate/Models/AppLanguage.swift @@ -0,0 +1,186 @@ +import Foundation +import SwiftUI + +/// Supported application display languages. +enum AppLanguage: String, CaseIterable, Identifiable, Sendable { + /// Follow system language (fallback to English if unsupported) + case system = "system" + /// English + case english = "en" + /// Simplified Chinese + case simplifiedChinese = "zh-Hans" + + var id: String { rawValue } + + /// The display name for this language option + var displayName: String { + switch self { + case .system: + return String(localized: "settings.language.system") + case .english: + return "English" + case .simplifiedChinese: + return "简体中文" + } + } + + /// The locale identifier for this language + var localeIdentifier: String? { + switch self { + case .system: + return nil + case .english: + return "en" + case .simplifiedChinese: + return "zh-Hans" + } + } + + /// All supported language codes (excluding system) + static var supportedLanguageCodes: [String] { + allCases.compactMap { $0.localeIdentifier } + } +} + +/// Manages application language settings and provides runtime language switching. +@MainActor +@Observable +final class LanguageManager { + // MARK: - Singleton + + static let shared = LanguageManager() + + // MARK: - Properties + + /// The currently selected language + var currentLanguage: AppLanguage { + didSet { + if oldValue != currentLanguage { + applyLanguage() + saveLanguage() + } + } + } + + /// The active bundle for localized strings + private(set) var bundle: Bundle = .main + + /// Notification name for language change + static let languageDidChangeNotification = Notification.Name("LanguageDidChange") + + // MARK: - UserDefaults Key + + private let languageKey = "ScreenCapture.appLanguage" + + // MARK: - Initialization + + private init() { + // Load saved language preference + if let savedLanguage = UserDefaults.standard.string(forKey: languageKey), + let language = AppLanguage(rawValue: savedLanguage) { + currentLanguage = language + } else { + currentLanguage = .system + } + + applyLanguage() + } + + // MARK: - Public Methods + + /// Returns a localized string for the given key + func localizedString(_ key: String, comment: String = "") -> String { + NSLocalizedString(key, tableName: "Localizable", bundle: bundle, comment: comment) + } + + /// Returns the effective locale identifier (resolves system to actual language) + var effectiveLocaleIdentifier: String { + if let localeId = currentLanguage.localeIdentifier { + return localeId + } + + // For system, detect the preferred language + let preferredLanguages = Locale.preferredLanguages + for preferred in preferredLanguages { + // Check if we support this language + if preferred.hasPrefix("zh-Hans") || preferred.hasPrefix("zh_Hans") || preferred == "zh-CN" { + return "zh-Hans" + } + if preferred.hasPrefix("en") { + return "en" + } + } + + // Default to English + return "en" + } + + // MARK: - Private Methods + + private func applyLanguage() { + let localeId = effectiveLocaleIdentifier + + // Find the bundle for this language + if let path = Bundle.main.path(forResource: localeId, ofType: "lproj"), + let languageBundle = Bundle(path: path) { + bundle = languageBundle + } else { + // Fallback to main bundle (English) + bundle = .main + } + + // Apply to UserDefaults for system-level settings + UserDefaults.standard.set([localeId], forKey: "AppleLanguages") + + // Post notification for views to refresh + NotificationCenter.default.post(name: Self.languageDidChangeNotification, object: nil) + } + + private func saveLanguage() { + UserDefaults.standard.set(currentLanguage.rawValue, forKey: languageKey) + } +} + +// MARK: - String Extension for Localization + +extension String { + /// Returns a localized version of this string using the current app language + @MainActor + var localized: String { + LanguageManager.shared.localizedString(self) + } + + /// Returns a localized string with format arguments + @MainActor + func localized(with arguments: CVarArg...) -> String { + String(format: localized, arguments: arguments) + } +} + +// MARK: - SwiftUI LocalizedText View + +/// A Text view that automatically updates when app language changes +struct LocalizedText: View { + private let key: String + @State private var refreshID = UUID() + + init(_ key: String) { + self.key = key + } + + var body: some View { + Text(LanguageManager.shared.localizedString(key)) + .id(refreshID) + .onReceive(NotificationCenter.default.publisher(for: LanguageManager.languageDidChangeNotification)) { _ in + refreshID = UUID() + } + } +} + +// MARK: - Localized String Helper Function + +/// Returns a localized string using the current app language bundle +@MainActor +func L(_ key: String) -> String { + LanguageManager.shared.localizedString(key) +} diff --git a/ScreenTranslate/Models/OCREngineType.swift b/ScreenTranslate/Models/OCREngineType.swift index b4b0bf9..0bb7a1d 100644 --- a/ScreenTranslate/Models/OCREngineType.swift +++ b/ScreenTranslate/Models/OCREngineType.swift @@ -51,36 +51,41 @@ enum OCREngineType: String, CaseIterable, Sendable, Codable { /// Helper to check if PaddleOCR is available on the system enum PaddleOCRChecker { /// Cached availability status (nonisolated(unsafe) for singleton cache) - private nonisolated(unsafe) static var _isAvailable: Bool? + private nonisolated(unsafe) static var _isAvailable: Bool? = false - /// Check if PaddleOCR command is available + /// Check if PaddleOCR command is available (returns cached value, never blocks) static var isAvailable: Bool { - if let cached = _isAvailable { - return cached + return _isAvailable ?? false + } + + /// Async check and cache PaddleOCR availability + static func checkAvailabilityAsync() { + Task.detached(priority: .background) { + let result = await checkPaddleOCRAsync() + _isAvailable = result } - - let result = checkPaddleOCR() - _isAvailable = result - return result } - /// Perform actual check for PaddleOCR availability - private static func checkPaddleOCR() -> Bool { - let task = Process() - task.launchPath = "/usr/bin/which" - task.arguments = ["paddleocr"] - - let pipe = Pipe() - task.standardOutput = pipe - task.standardError = Pipe() - - do { - try task.run() - task.waitUntilExit() - - return task.terminationStatus == 0 - } catch { - return false + /// Perform actual check for PaddleOCR availability (async, off main thread) + private static func checkPaddleOCRAsync() async -> Bool { + await withCheckedContinuation { continuation in + DispatchQueue.global(qos: .background).async { + let task = Process() + task.executableURL = URL(fileURLWithPath: "/usr/bin/which") + task.arguments = ["paddleocr"] + + let pipe = Pipe() + task.standardOutput = pipe + task.standardError = Pipe() + + do { + try task.run() + task.waitUntilExit() + continuation.resume(returning: task.terminationStatus == 0) + } catch { + continuation.resume(returning: false) + } + } } } @@ -94,7 +99,7 @@ enum PaddleOCRChecker { guard isAvailable else { return nil } let task = Process() - task.launchPath = "/usr/local/bin/paddleocr" + task.executableURL = URL(fileURLWithPath: "/usr/local/bin/paddleocr") task.arguments = ["--version"] let pipe = Pipe() diff --git a/ScreenTranslate/Resources/Localizable.strings b/ScreenTranslate/Resources/en.lproj/Localizable.strings similarity index 53% rename from ScreenTranslate/Resources/Localizable.strings rename to ScreenTranslate/Resources/en.lproj/Localizable.strings index 3725fff..7659ecf 100644 --- a/ScreenTranslate/Resources/Localizable.strings +++ b/ScreenTranslate/Resources/en.lproj/Localizable.strings @@ -1,68 +1,228 @@ -/* Error Messages */ +/* + Localizable.strings (English) + ScreenTranslate +*/ + +/* ======================================== + Error Messages + ======================================== */ + +/* Permission Errors */ "error.permission.denied" = "Screen recording permission is required to capture screenshots."; "error.permission.denied.recovery" = "Open System Settings to grant permission."; +/* Display Errors */ "error.display.not.found" = "The selected display is no longer available."; "error.display.not.found.recovery" = "Please select a different display."; - "error.display.disconnected" = "The display '%@' was disconnected during capture."; "error.display.disconnected.recovery" = "Please reconnect the display and try again."; +/* Capture Errors */ "error.capture.failed" = "Failed to capture the screen."; "error.capture.failed.recovery" = "Please try again."; +/* Save Errors */ "error.save.location.invalid" = "The save location is not accessible."; "error.save.location.invalid.recovery" = "Choose a different save location in Settings."; - +"error.save.location.invalid.detail" = "Cannot save to %@. The location is not accessible."; +"error.save.unknown" = "An unexpected error occurred while saving."; "error.disk.full" = "There is not enough disk space to save the screenshot."; "error.disk.full.recovery" = "Free up disk space and try again."; +/* Export Errors */ "error.export.encoding.failed" = "Failed to encode the image."; "error.export.encoding.failed.recovery" = "Try a different format in Settings."; "error.export.encoding.failed.detail" = "Failed to encode the image as %@."; -"error.save.location.invalid.detail" = "Cannot save to %@. The location is not accessible."; -"error.save.unknown" = "An unexpected error occurred while saving."; - +/* Clipboard Errors */ "error.clipboard.write.failed" = "Failed to copy the screenshot to clipboard."; "error.clipboard.write.failed.recovery" = "Please try again."; +/* Hotkey Errors */ "error.hotkey.registration.failed" = "Failed to register the keyboard shortcut."; "error.hotkey.registration.failed.recovery" = "The shortcut may conflict with another app. Try a different shortcut."; - "error.hotkey.conflict" = "This keyboard shortcut conflicts with another application."; "error.hotkey.conflict.recovery" = "Choose a different keyboard shortcut."; -/* Menu Items */ +/* OCR Errors */ +"error.ocr.failed" = "Text recognition failed."; +"error.ocr.failed.recovery" = "Please try again with a clearer image."; +"error.ocr.no.text" = "No text was recognized in the image."; +"error.ocr.no.text.recovery" = "Try capturing an area with visible text."; +"error.ocr.cancelled" = "Text recognition was cancelled."; +"error.ocr.server.unreachable" = "Cannot connect to OCR server."; +"error.ocr.server.unreachable.recovery" = "Check server address and network connection."; + +/* Translation Errors */ +"error.translation.in.progress" = "A translation is already in progress"; +"error.translation.in.progress.recovery" = "Please wait for the current translation to complete"; +"error.translation.empty.input" = "No text to translate"; +"error.translation.empty.input.recovery" = "Please select some text first"; +"error.translation.timeout" = "Translation timed out"; +"error.translation.timeout.recovery" = "Please try again"; +"error.translation.unsupported.pair" = "Translation from %@ to %@ is not supported"; +"error.translation.unsupported.pair.recovery" = "Please select different languages"; +"error.translation.failed" = "Translation failed"; +"error.translation.failed.recovery" = "Please try again"; +"error.translation.language.not.installed" = "Translation language '%@' is not installed"; +"error.translation.language.download.instructions" = "Go to System Settings > General > Language & Region > Translation Languages, then download the required language."; + +/* Generic Error UI */ +"error.title" = "Error"; +"error.ok" = "OK"; +"error.dismiss" = "Dismiss"; +"error.retry.capture" = "Retry"; +"error.permission.open.settings" = "Open System Settings"; + + +/* ======================================== + Menu Items + ======================================== */ + "menu.capture.full.screen" = "Capture Full Screen"; "menu.capture.fullscreen" = "Capture Full Screen"; "menu.capture.selection" = "Capture Selection"; "menu.recent.captures" = "Recent Captures"; "menu.recent.captures.empty" = "No Recent Captures"; "menu.recent.captures.clear" = "Clear Recent"; +"menu.translation.history" = "Translation History"; "menu.settings" = "Settings..."; -"menu.quit" = "Quit ScreenCapture"; +"menu.quit" = "Quit ScreenTranslate"; + + +/* ======================================== + Display Selector + ======================================== */ + +"display.selector.title" = "Select Display"; +"display.selector.header" = "Choose display to capture:"; +"display.selector.cancel" = "Cancel"; -/* Preview Window */ + +/* ======================================== + Preview Window + ======================================== */ + +"preview.window.title" = "Screenshot Preview"; "preview.title" = "Screenshot Preview"; "preview.dimensions" = "%d x %d pixels"; "preview.file.size" = "~%@ %@"; +"preview.screenshot" = "Screenshot"; +"preview.enter.text" = "Enter text"; +"preview.image.dimensions" = "Image dimensions"; +"preview.estimated.size" = "Estimated file size"; +"preview.edit.label" = "Edit:"; +"preview.active.tool" = "Active tool"; +"preview.crop.mode.active" = "Crop mode active"; + +/* Crop */ +"preview.crop" = "Crop"; +"preview.crop.cancel" = "Cancel"; +"preview.crop.apply" = "Apply Crop"; + +/* Recognized Text */ +"preview.recognized.text" = "Recognized Text:"; +"preview.translation" = "Translation:"; + +/* Toolbar Tooltips */ +"preview.tooltip.crop" = "Crop (C)"; +"preview.tooltip.undo" = "Undo (⌘Z)"; +"preview.tooltip.redo" = "Redo (⌘⇧Z)"; +"preview.tooltip.copy" = "Copy to Clipboard (⌘C)"; +"preview.tooltip.save" = "Save (⌘S or Enter)"; +"preview.tooltip.ocr" = "Recognize Text (OCR)"; +"preview.tooltip.translate" = "Translate Text"; +"preview.tooltip.dismiss" = "Dismiss (Escape)"; +"preview.tooltip.delete" = "Delete selected annotation"; + +/* Shape Toggle */ +"preview.shape.filled" = "Filled"; +"preview.shape.hollow" = "Hollow"; +"preview.shape.toggle.hint" = "Click to toggle between filled and hollow"; + + +/* ======================================== + Annotation Tools + ======================================== */ -/* Annotation Tools */ "tool.rectangle" = "Rectangle"; "tool.freehand" = "Freehand"; "tool.text" = "Text"; +"tool.arrow" = "Arrow"; +"tool.ellipse" = "Ellipse"; +"tool.highlight" = "Highlight"; + + +/* ======================================== + Colors + ======================================== */ + +"color.red" = "Red"; +"color.orange" = "Orange"; +"color.yellow" = "Yellow"; +"color.green" = "Green"; +"color.blue" = "Blue"; +"color.purple" = "Purple"; +"color.pink" = "Pink"; +"color.white" = "White"; +"color.black" = "Black"; +"color.custom" = "Custom"; + -/* Settings Window */ -"settings.window.title" = "ScreenCapture Settings"; -"settings.title" = "ScreenCapture Settings"; +/* ======================================== + Actions + ======================================== */ -/* Settings Sections */ +"action.save" = "Save"; +"action.copy" = "Copy"; +"action.cancel" = "Cancel"; +"action.undo" = "Undo"; +"action.redo" = "Redo"; +"action.delete" = "Delete"; +"action.clear" = "Clear"; +"action.reset" = "Reset"; +"action.close" = "Close"; +"action.done" = "Done"; + +/* Buttons */ +"button.ok" = "OK"; +"button.cancel" = "Cancel"; +"button.clear" = "Clear"; +"button.reset" = "Reset"; +"button.save" = "Save"; +"button.delete" = "Delete"; + + +/* ======================================== + Settings Window + ======================================== */ + +"settings.window.title" = "ScreenTranslate Settings"; +"settings.title" = "ScreenTranslate Settings"; + +/* Settings Tabs/Sections */ +"settings.section.permissions" = "Permissions"; "settings.section.general" = "General"; +"settings.section.engines" = "Engines"; +"settings.section.languages" = "Languages"; "settings.section.export" = "Export"; "settings.section.shortcuts" = "Keyboard Shortcuts"; "settings.section.annotations" = "Annotations"; +/* Language Settings */ +"settings.language" = "Language"; +"settings.language.system" = "System Default"; +"settings.language.restart.hint" = "Some changes may require restart"; + +/* Permissions */ +"settings.permission.screen.recording" = "Screen Recording"; +"settings.permission.screen.recording.hint" = "Required to capture screenshots"; +"settings.permission.accessibility" = "Accessibility"; +"settings.permission.accessibility.hint" = "Required for global shortcuts"; +"settings.permission.granted" = "Granted"; +"settings.permission.not.granted" = "Not Granted"; +"settings.permission.grant" = "Grant Access"; + /* Save Location */ "settings.save.location" = "Save Location"; "settings.save.location.choose" = "Choose..."; @@ -72,8 +232,13 @@ /* Export Format */ "settings.format" = "Default Format"; +"settings.format.png" = "PNG"; +"settings.format.jpeg" = "JPEG"; +"settings.format.heic" = "HEIC"; "settings.jpeg.quality" = "JPEG Quality"; "settings.jpeg.quality.hint" = "Higher quality results in larger file sizes"; +"settings.heic.quality" = "HEIC Quality"; +"settings.heic.quality.hint" = "HEIC offers better compression"; /* Keyboard Shortcuts */ "settings.shortcuts" = "Keyboard Shortcuts"; @@ -90,6 +255,11 @@ "settings.stroke.width" = "Stroke Width"; "settings.text.size" = "Text Size"; +/* Engines */ +"settings.ocr.engine" = "OCR Engine"; +"settings.translation.engine" = "Translation Engine"; +"settings.translation.mode" = "Translation Mode"; + /* Reset */ "settings.reset.all" = "Reset All to Defaults"; @@ -97,73 +267,94 @@ "settings.error.title" = "Error"; "settings.error.ok" = "OK"; -/* Actions */ -"action.save" = "Save"; -"action.copy" = "Copy"; -"action.cancel" = "Cancel"; -"action.undo" = "Undo"; -"action.redo" = "Redo"; - -/* Display Selector */ -"display.selector.title" = "Select Display"; -"display.selector.header" = "Choose display to capture:"; -"display.selector.cancel" = "Cancel"; -/* Preview Window */ -"preview.window.title" = "Screenshot Preview"; +/* ======================================== + OCR Engines + ======================================== */ -/* Error Buttons */ -"error.permission.open.settings" = "Open System Settings"; -"error.dismiss" = "Dismiss"; -"error.ok" = "OK"; -"error.retry.capture" = "Retry"; +"ocr.engine.vision" = "Apple Vision"; +"ocr.engine.vision.description" = "Built-in macOS Vision framework, fast and private"; +"ocr.engine.paddleocr" = "PaddleOCR"; +"ocr.engine.paddleocr.description" = "Self-hosted OCR server for better accuracy"; -/* Permission Prompt */ -"permission.prompt.title" = "Screen Recording Permission Required"; -"permission.prompt.message" = "ScreenCapture needs permission to capture your screen. This is required to take screenshots.\n\nAfter clicking Continue, macOS will ask you to grant Screen Recording permission. You can grant it in System Settings > Privacy & Security > Screen Recording."; -"permission.prompt.continue" = "Continue"; -"permission.prompt.later" = "Later"; -/* Translation Settings */ -"translation.auto" = "Auto Detect"; -"translation.auto.detected" = "Auto Detected"; -"translation.language.follow.system" = "Follow System"; -"translation.language.source" = "Source Language"; -"translation.language.target" = "Target Language"; -"translation.language.source.hint" = "The language of the text you want to translate"; -"translation.language.target.hint" = "The language to translate the text into"; +/* ======================================== + Translation Engines + ======================================== */ -/* Translation Engines */ "translation.engine.apple" = "Apple Translation"; "translation.engine.apple.description" = "Built-in macOS translation, no setup required"; "translation.engine.mtran" = "MTranServer"; "translation.engine.mtran.description" = "Self-hosted translation server"; -/* Translation Modes */ + +/* ======================================== + Translation Modes + ======================================== */ + "translation.mode.inline" = "In-place Replacement"; "translation.mode.inline.description" = "Replace original text with translation"; "translation.mode.below" = "Below Original"; "translation.mode.below.description" = "Show translation below original text"; -/* Translation Errors */ -"error.translation.in.progress" = "A translation is already in progress"; -"error.translation.in.progress.recovery" = "Please wait for the current translation to complete"; -"error.translation.empty.input" = "No text to translate"; -"error.translation.empty.input.recovery" = "Please select some text first"; -"error.translation.timeout" = "Translation timed out"; -"error.translation.timeout.recovery" = "Please try again"; -"error.translation.unsupported.pair" = "Translation from %@ to %@ is not supported"; -"error.translation.unsupported.pair.recovery" = "Please select different languages"; -"error.translation.failed" = "Translation failed"; -"error.translation.failed.recovery" = "Please try again"; -"error.translation.language.not.installed" = "Translation language '%@' is not installed"; -"error.translation.language.download.instructions" = "Go to System Settings > General > Language & Region > Translation Languages, then download the required language."; -/* Onboarding */ -"onboarding.window.title" = "Welcome to ScreenCapture"; +/* ======================================== + Translation Settings + ======================================== */ + +"translation.auto" = "Auto Detect"; +"translation.auto.detected" = "Auto Detected"; +"translation.language.follow.system" = "Follow System"; +"translation.language.source" = "Source Language"; +"translation.language.target" = "Target Language"; +"translation.language.source.hint" = "The language of the text you want to translate"; +"translation.language.target.hint" = "The language to translate the text into"; + + +/* ======================================== + History View + ======================================== */ + +"history.title" = "Translation History"; +"history.search.placeholder" = "Search history..."; +"history.clear.all" = "Clear all history"; +"history.empty.title" = "No Translation History"; +"history.empty.message" = "Your translated screenshots will appear here"; +"history.no.results.title" = "No Results"; +"history.no.results.message" = "No entries match your search"; +"history.clear.search" = "Clear Search"; + +"history.source" = "Source"; +"history.translation" = "Translation"; +"history.truncated" = "truncated"; + +"history.copy.translation" = "Copy Translation"; +"history.copy.source" = "Copy Source"; +"history.copy.both" = "Copy Both"; +"history.delete" = "Delete"; + +"history.clear.alert.title" = "Clear History"; +"history.clear.alert.message" = "Are you sure you want to delete all translation history? This action cannot be undone."; + + +/* ======================================== + Permission Prompt + ======================================== */ + +"permission.prompt.title" = "Screen Recording Permission Required"; +"permission.prompt.message" = "ScreenTranslate needs permission to capture your screen. This is required to take screenshots.\n\nAfter clicking Continue, macOS will ask you to grant Screen Recording permission. You can grant it in System Settings > Privacy & Security > Screen Recording."; +"permission.prompt.continue" = "Continue"; +"permission.prompt.later" = "Later"; + + +/* ======================================== + Onboarding + ======================================== */ + +"onboarding.window.title" = "Welcome to ScreenTranslate"; /* Onboarding - Welcome Step */ -"onboarding.welcome.title" = "Welcome to ScreenCapture"; +"onboarding.welcome.title" = "Welcome to ScreenTranslate"; "onboarding.welcome.message" = "Let's get you set up with screen capture and translation features. This will only take a minute."; "onboarding.feature.local.ocr.title" = "Local OCR"; @@ -175,7 +366,7 @@ /* Onboarding - Permissions Step */ "onboarding.permissions.title" = "Permissions"; -"onboarding.permissions.message" = "ScreenCapture needs a few permissions to work properly. Please grant the following permissions:"; +"onboarding.permissions.message" = "ScreenTranslate needs a few permissions to work properly. Please grant the following permissions:"; "onboarding.permissions.hint" = "After granting permissions, the status will update automatically."; "onboarding.permission.screen.recording" = "Screen Recording"; @@ -201,14 +392,24 @@ /* Onboarding - Complete Step */ "onboarding.complete.title" = "You're All Set!"; -"onboarding.complete.message" = "ScreenCapture is now ready to use. Here's how to get started:"; +"onboarding.complete.message" = "ScreenTranslate is now ready to use. Here's how to get started:"; "onboarding.complete.shortcuts" = "Use ⌘⇧F to capture the full screen"; "onboarding.complete.selection" = "Use ⌘⇧A to capture a selection and translate"; "onboarding.complete.settings" = "Open Settings from the menu bar to customize options"; -"onboarding.complete.start" = "Start Using ScreenCapture"; +"onboarding.complete.start" = "Start Using ScreenTranslate"; /* Onboarding - Navigation */ "onboarding.back" = "Back"; "onboarding.continue" = "Continue"; "onboarding.next" = "Next"; "onboarding.skip" = "Skip"; + + +/* ======================================== + Accessibility Labels + ======================================== */ + +"accessibility.close.button" = "Close"; +"accessibility.settings.button" = "Settings"; +"accessibility.capture.button" = "Capture"; +"accessibility.translate.button" = "Translate"; diff --git a/ScreenTranslate/Resources/zh-Hans.lproj/Localizable.strings b/ScreenTranslate/Resources/zh-Hans.lproj/Localizable.strings new file mode 100644 index 0000000..f8123b8 --- /dev/null +++ b/ScreenTranslate/Resources/zh-Hans.lproj/Localizable.strings @@ -0,0 +1,415 @@ +/* + Localizable.strings (简体中文) + ScreenTranslate +*/ + +/* ======================================== + 错误消息 + ======================================== */ + +/* 权限错误 */ +"error.permission.denied" = "需要屏幕录制权限才能截取屏幕。"; +"error.permission.denied.recovery" = "请打开系统设置授予权限。"; + +/* 显示器错误 */ +"error.display.not.found" = "所选显示器已不可用。"; +"error.display.not.found.recovery" = "请选择其他显示器。"; +"error.display.disconnected" = "显示器 '%@' 在截图过程中断开连接。"; +"error.display.disconnected.recovery" = "请重新连接显示器后再试。"; + +/* 截图错误 */ +"error.capture.failed" = "截图失败。"; +"error.capture.failed.recovery" = "请重试。"; + +/* 保存错误 */ +"error.save.location.invalid" = "保存位置不可访问。"; +"error.save.location.invalid.recovery" = "请在设置中选择其他保存位置。"; +"error.save.location.invalid.detail" = "无法保存到 %@,该位置不可访问。"; +"error.save.unknown" = "保存时发生未知错误。"; +"error.disk.full" = "磁盘空间不足,无法保存截图。"; +"error.disk.full.recovery" = "请释放磁盘空间后重试。"; + +/* 导出错误 */ +"error.export.encoding.failed" = "图像编码失败。"; +"error.export.encoding.failed.recovery" = "请在设置中尝试其他格式。"; +"error.export.encoding.failed.detail" = "无法将图像编码为 %@ 格式。"; + +/* 剪贴板错误 */ +"error.clipboard.write.failed" = "复制截图到剪贴板失败。"; +"error.clipboard.write.failed.recovery" = "请重试。"; + +/* 快捷键错误 */ +"error.hotkey.registration.failed" = "注册键盘快捷键失败。"; +"error.hotkey.registration.failed.recovery" = "该快捷键可能与其他应用冲突,请尝试其他快捷键。"; +"error.hotkey.conflict" = "此键盘快捷键与其他应用程序冲突。"; +"error.hotkey.conflict.recovery" = "请选择其他键盘快捷键。"; + +/* OCR 错误 */ +"error.ocr.failed" = "文字识别失败。"; +"error.ocr.failed.recovery" = "请使用更清晰的图像重试。"; +"error.ocr.no.text" = "图像中未识别到文字。"; +"error.ocr.no.text.recovery" = "请尝试截取包含可见文字的区域。"; +"error.ocr.cancelled" = "文字识别已取消。"; +"error.ocr.server.unreachable" = "无法连接到 OCR 服务器。"; +"error.ocr.server.unreachable.recovery" = "请检查服务器地址和网络连接。"; + +/* 翻译错误 */ +"error.translation.in.progress" = "翻译正在进行中"; +"error.translation.in.progress.recovery" = "请等待当前翻译完成"; +"error.translation.empty.input" = "没有可翻译的文本"; +"error.translation.empty.input.recovery" = "请先选择一些文本"; +"error.translation.timeout" = "翻译超时"; +"error.translation.timeout.recovery" = "请重试"; +"error.translation.unsupported.pair" = "不支持从 %@ 翻译到 %@"; +"error.translation.unsupported.pair.recovery" = "请选择其他语言"; +"error.translation.failed" = "翻译失败"; +"error.translation.failed.recovery" = "请重试"; +"error.translation.language.not.installed" = "翻译语言 '%@' 未安装"; +"error.translation.language.download.instructions" = "请前往系统设置 > 通用 > 语言与地区 > 翻译语言,下载所需语言。"; + +/* 通用错误 UI */ +"error.title" = "错误"; +"error.ok" = "好的"; +"error.dismiss" = "关闭"; +"error.retry.capture" = "重试"; +"error.permission.open.settings" = "打开系统设置"; + + +/* ======================================== + 菜单项 + ======================================== */ + +"menu.capture.full.screen" = "全屏截图"; +"menu.capture.fullscreen" = "全屏截图"; +"menu.capture.selection" = "区域截图"; +"menu.recent.captures" = "最近截图"; +"menu.recent.captures.empty" = "没有最近截图"; +"menu.recent.captures.clear" = "清除最近截图"; +"menu.translation.history" = "翻译历史"; +"menu.settings" = "设置..."; +"menu.quit" = "退出 ScreenTranslate"; + + +/* ======================================== + 显示器选择 + ======================================== */ + +"display.selector.title" = "选择显示器"; +"display.selector.header" = "选择要截取的显示器:"; +"display.selector.cancel" = "取消"; + + +/* ======================================== + 预览窗口 + ======================================== */ + +"preview.window.title" = "截图预览"; +"preview.title" = "截图预览"; +"preview.dimensions" = "%d × %d 像素"; +"preview.file.size" = "约 %@ %@"; +"preview.screenshot" = "截图"; +"preview.enter.text" = "输入文本"; +"preview.image.dimensions" = "图片尺寸"; +"preview.estimated.size" = "预估文件大小"; +"preview.edit.label" = "编辑:"; +"preview.active.tool" = "当前工具"; +"preview.crop.mode.active" = "裁剪模式已激活"; + +/* 裁剪 */ +"preview.crop" = "裁剪"; +"preview.crop.cancel" = "取消"; +"preview.crop.apply" = "应用裁剪"; + +/* 识别文本 */ +"preview.recognized.text" = "识别文本:"; +"preview.translation" = "翻译结果:"; + +/* 工具栏提示 */ +"preview.tooltip.crop" = "裁剪 (C)"; +"preview.tooltip.undo" = "撤销 (⌘Z)"; +"preview.tooltip.redo" = "重做 (⌘⇧Z)"; +"preview.tooltip.copy" = "复制到剪贴板 (⌘C)"; +"preview.tooltip.save" = "保存 (⌘S 或 回车)"; +"preview.tooltip.ocr" = "识别文字 (OCR)"; +"preview.tooltip.translate" = "翻译文本"; +"preview.tooltip.dismiss" = "关闭 (Escape)"; +"preview.tooltip.delete" = "删除选中的标注"; + +/* 形状切换 */ +"preview.shape.filled" = "填充"; +"preview.shape.hollow" = "空心"; +"preview.shape.toggle.hint" = "点击切换填充/空心"; + + +/* ======================================== + 标注工具 + ======================================== */ + +"tool.rectangle" = "矩形"; +"tool.freehand" = "画笔"; +"tool.text" = "文本"; +"tool.arrow" = "箭头"; +"tool.ellipse" = "椭圆"; +"tool.highlight" = "高亮"; + + +/* ======================================== + 颜色 + ======================================== */ + +"color.red" = "红色"; +"color.orange" = "橙色"; +"color.yellow" = "黄色"; +"color.green" = "绿色"; +"color.blue" = "蓝色"; +"color.purple" = "紫色"; +"color.pink" = "粉色"; +"color.white" = "白色"; +"color.black" = "黑色"; +"color.custom" = "自定义"; + + +/* ======================================== + 操作 + ======================================== */ + +"action.save" = "保存"; +"action.copy" = "复制"; +"action.cancel" = "取消"; +"action.undo" = "撤销"; +"action.redo" = "重做"; +"action.delete" = "删除"; +"action.clear" = "清除"; +"action.reset" = "重置"; +"action.close" = "关闭"; +"action.done" = "完成"; + +/* 按钮 */ +"button.ok" = "好的"; +"button.cancel" = "取消"; +"button.clear" = "清除"; +"button.reset" = "重置"; +"button.save" = "保存"; +"button.delete" = "删除"; + + +/* ======================================== + 设置窗口 + ======================================== */ + +"settings.window.title" = "ScreenTranslate 设置"; +"settings.title" = "ScreenTranslate 设置"; + +/* 设置标签/分区 */ +"settings.section.permissions" = "权限"; +"settings.section.general" = "通用"; +"settings.section.engines" = "引擎"; +"settings.section.languages" = "语言"; +"settings.section.export" = "导出"; +"settings.section.shortcuts" = "键盘快捷键"; +"settings.section.annotations" = "标注"; + +/* 语言设置 */ +"settings.language" = "语言"; +"settings.language.system" = "跟随系统"; +"settings.language.restart.hint" = "部分更改可能需要重启应用"; + +/* 权限 */ +"settings.permission.screen.recording" = "屏幕录制"; +"settings.permission.screen.recording.hint" = "截图功能需要此权限"; +"settings.permission.accessibility" = "辅助功能"; +"settings.permission.accessibility.hint" = "全局快捷键需要此权限"; +"settings.permission.granted" = "已授权"; +"settings.permission.not.granted" = "未授权"; +"settings.permission.grant" = "授权"; + +/* 保存位置 */ +"settings.save.location" = "保存位置"; +"settings.save.location.choose" = "选择..."; +"settings.save.location.select" = "选择"; +"settings.save.location.message" = "选择截图的默认保存位置"; +"settings.save.location.reveal" = "在访达中显示"; + +/* 导出格式 */ +"settings.format" = "默认格式"; +"settings.format.png" = "PNG"; +"settings.format.jpeg" = "JPEG"; +"settings.format.heic" = "HEIC"; +"settings.jpeg.quality" = "JPEG 质量"; +"settings.jpeg.quality.hint" = "质量越高,文件越大"; +"settings.heic.quality" = "HEIC 质量"; +"settings.heic.quality.hint" = "HEIC 提供更好的压缩率"; + +/* 键盘快捷键 */ +"settings.shortcuts" = "键盘快捷键"; +"settings.shortcut.fullscreen" = "全屏截图"; +"settings.shortcut.selection" = "区域截图"; +"settings.shortcut.recording" = "按下快捷键..."; +"settings.shortcut.reset" = "恢复默认"; +"settings.shortcut.error.no.modifier" = "快捷键必须包含 Command、Control 或 Option"; +"settings.shortcut.error.conflict" = "此快捷键已被使用"; + +/* 标注 */ +"settings.annotations" = "标注默认设置"; +"settings.stroke.color" = "描边颜色"; +"settings.stroke.width" = "描边宽度"; +"settings.text.size" = "文字大小"; + +/* 引擎 */ +"settings.ocr.engine" = "OCR 引擎"; +"settings.translation.engine" = "翻译引擎"; +"settings.translation.mode" = "翻译模式"; + +/* 重置 */ +"settings.reset.all" = "恢复所有默认设置"; + +/* 错误 */ +"settings.error.title" = "错误"; +"settings.error.ok" = "好的"; + + +/* ======================================== + OCR 引擎 + ======================================== */ + +"ocr.engine.vision" = "Apple Vision"; +"ocr.engine.vision.description" = "内置 macOS Vision 框架,快速且隐私安全"; +"ocr.engine.paddleocr" = "PaddleOCR"; +"ocr.engine.paddleocr.description" = "自托管 OCR 服务器,识别更准确"; + + +/* ======================================== + 翻译引擎 + ======================================== */ + +"translation.engine.apple" = "Apple 翻译"; +"translation.engine.apple.description" = "内置 macOS 翻译,无需配置"; +"translation.engine.mtran" = "MTranServer"; +"translation.engine.mtran.description" = "自托管翻译服务器"; + + +/* ======================================== + 翻译模式 + ======================================== */ + +"translation.mode.inline" = "原地替换"; +"translation.mode.inline.description" = "用译文替换原文"; +"translation.mode.below" = "原文下方显示"; +"translation.mode.below.description" = "在原文下方显示译文"; + + +/* ======================================== + 翻译设置 + ======================================== */ + +"translation.auto" = "自动检测"; +"translation.auto.detected" = "自动检测"; +"translation.language.follow.system" = "跟随系统"; +"translation.language.source" = "源语言"; +"translation.language.target" = "目标语言"; +"translation.language.source.hint" = "要翻译的文本的语言"; +"translation.language.target.hint" = "翻译的目标语言"; + + +/* ======================================== + 历史记录视图 + ======================================== */ + +"history.title" = "翻译历史"; +"history.search.placeholder" = "搜索历史记录..."; +"history.clear.all" = "清除所有历史"; +"history.empty.title" = "没有翻译历史"; +"history.empty.message" = "您的翻译截图将显示在这里"; +"history.no.results.title" = "无结果"; +"history.no.results.message" = "没有匹配的记录"; +"history.clear.search" = "清除搜索"; + +"history.source" = "原文"; +"history.translation" = "译文"; +"history.truncated" = "已截断"; + +"history.copy.translation" = "复制译文"; +"history.copy.source" = "复制原文"; +"history.copy.both" = "复制全部"; +"history.delete" = "删除"; + +"history.clear.alert.title" = "清除历史"; +"history.clear.alert.message" = "确定要删除所有翻译历史吗?此操作无法撤销。"; + + +/* ======================================== + 权限提示 + ======================================== */ + +"permission.prompt.title" = "需要屏幕录制权限"; +"permission.prompt.message" = "ScreenTranslate 需要权限来截取您的屏幕。这是截图功能所必需的。\n\n点击继续后,macOS 将要求您授予屏幕录制权限。您可以在系统设置 > 隐私与安全性 > 屏幕录制中授权。"; +"permission.prompt.continue" = "继续"; +"permission.prompt.later" = "稍后"; + + +/* ======================================== + 引导页面 + ======================================== */ + +"onboarding.window.title" = "欢迎使用 ScreenTranslate"; + +/* 引导 - 欢迎步骤 */ +"onboarding.welcome.title" = "欢迎使用 ScreenTranslate"; +"onboarding.welcome.message" = "让我们为您设置屏幕截图和翻译功能。只需一分钟即可完成。"; + +"onboarding.feature.local.ocr.title" = "本地 OCR"; +"onboarding.feature.local.ocr.description" = "使用 macOS Vision 框架,快速且隐私安全的文字识别"; +"onboarding.feature.local.translation.title" = "本地翻译"; +"onboarding.feature.local.translation.description" = "使用 Apple 翻译,即时离线翻译"; +"onboarding.feature.shortcuts.title" = "全局快捷键"; +"onboarding.feature.shortcuts.description" = "随时随地使用键盘快捷键截图和翻译"; + +/* 引导 - 权限步骤 */ +"onboarding.permissions.title" = "权限"; +"onboarding.permissions.message" = "ScreenTranslate 需要一些权限才能正常工作。请授予以下权限:"; +"onboarding.permissions.hint" = "授权后,状态将自动更新。"; + +"onboarding.permission.screen.recording" = "屏幕录制"; +"onboarding.permission.accessibility" = "辅助功能"; +"onboarding.permission.granted" = "已授权"; +"onboarding.permission.not.granted" = "未授权"; +"onboarding.permission.grant" = "授权"; + +/* 引导 - 配置步骤 */ +"onboarding.configuration.title" = "可选配置"; +"onboarding.configuration.message" = "您的本地 OCR 和翻译功能已启用。可选择配置外部服务:"; +"onboarding.configuration.paddleocr" = "PaddleOCR 服务器地址"; +"onboarding.configuration.paddleocr.hint" = "留空则使用 macOS Vision OCR"; +"onboarding.configuration.mtran" = "MTranServer 地址"; +"onboarding.configuration.mtran.hint" = "留空则使用 Apple 翻译"; +"onboarding.configuration.placeholder" = "http://localhost:8080"; +"onboarding.configuration.placeholder.address" = "localhost"; +"onboarding.configuration.test" = "测试翻译"; +"onboarding.configuration.test.button" = "测试翻译"; +"onboarding.configuration.testing" = "测试中..."; +"onboarding.test.success" = "翻译测试成功:\"%@\" → \"%@\""; +"onboarding.test.failed" = "翻译测试失败:%@"; + +/* 引导 - 完成步骤 */ +"onboarding.complete.title" = "设置完成!"; +"onboarding.complete.message" = "ScreenTranslate 已准备就绪。以下是使用方法:"; +"onboarding.complete.shortcuts" = "使用 ⌘⇧F 全屏截图"; +"onboarding.complete.selection" = "使用 ⌘⇧A 区域截图并翻译"; +"onboarding.complete.settings" = "从菜单栏打开设置自定义选项"; +"onboarding.complete.start" = "开始使用 ScreenTranslate"; + +/* 引导 - 导航 */ +"onboarding.back" = "上一步"; +"onboarding.continue" = "继续"; +"onboarding.next" = "下一步"; +"onboarding.skip" = "跳过"; + + +/* ======================================== + 无障碍标签 + ======================================== */ + +"accessibility.close.button" = "关闭"; +"accessibility.settings.button" = "设置"; +"accessibility.capture.button" = "截图"; +"accessibility.translate.button" = "翻译";