diff --git a/KoeApp/Koe/AppDelegate/SPAppDelegate.m b/KoeApp/Koe/AppDelegate/SPAppDelegate.m index b761d12..b78d663 100644 --- a/KoeApp/Koe/AppDelegate/SPAppDelegate.m +++ b/KoeApp/Koe/AppDelegate/SPAppDelegate.m @@ -294,6 +294,7 @@ - (void)hotkeyMonitorDidDetectHoldStart { NSLog(@"[Koe] Hold start detected"); [self stopNumberKeyMonitoring]; [self stopAnyKeyDismissMonitoring]; + [self startEscapeKeyMonitoring]; [self.overlayPanel hideTemplateButtons]; self.showingError = NO; [self cancelPendingSessionEnd]; @@ -315,6 +316,7 @@ - (void)hotkeyMonitorDidDetectHoldStart { - (void)hotkeyMonitorDidDetectHoldEnd { NSLog(@"[Koe] Hold end detected"); + [self stopEscapeKeyMonitoring]; [self.cuePlayer playStop]; // Keep recording for 300ms after Fn release to capture trailing speech, @@ -335,6 +337,7 @@ - (void)hotkeyMonitorDidDetectTapStart { NSLog(@"[Koe] Tap start detected"); [self stopNumberKeyMonitoring]; [self stopAnyKeyDismissMonitoring]; + [self startEscapeKeyMonitoring]; [self.overlayPanel hideTemplateButtons]; self.showingError = NO; [self cancelPendingSessionEnd]; @@ -355,6 +358,7 @@ - (void)hotkeyMonitorDidDetectTapStart { - (void)hotkeyMonitorDidDetectTapEnd { NSLog(@"[Koe] Tap end detected"); + [self stopEscapeKeyMonitoring]; [self.cuePlayer playStop]; // Keep recording for 300ms after tap-end to capture trailing speech, @@ -628,7 +632,9 @@ - (void)statusBarDidSelectSetupWizard { self.setupWizard.delegate = self; self.setupWizard.rustBridge = self.rustBridge; } + [NSApp activateIgnoringOtherApps:YES]; [self.setupWizard showWindow:nil]; + [self.setupWizard.window makeKeyAndOrderFront:nil]; } - (void)statusBarDidSelectCheckForUpdates { @@ -720,4 +726,33 @@ - (void)stopAnyKeyDismissMonitoring { self.hotkeyMonitor.anyKeyDismissHandler = nil; } +#pragma mark - ESC Cancel Monitoring + +- (void)startEscapeKeyMonitoring { + // Default enabled: only disable if explicitly set to "false" in config. + char *raw = sp_config_get("hotkey.escape_cancel_enabled"); + BOOL disabled = (raw && strcmp(raw, "false") == 0); + if (raw) sp_core_free_string(raw); + if (disabled) { + return; + } + __weak typeof(self) weakSelf = self; + self.hotkeyMonitor.escapeKeyHandler = ^{ + __strong typeof(weakSelf) strongSelf = weakSelf; + if (!strongSelf) return; + NSLog(@"[Koe] ESC pressed — cancelling session"); + [strongSelf stopEscapeKeyMonitoring]; + [strongSelf cancelPendingSessionEnd]; + [strongSelf.audioCaptureManager stopCapture]; + [strongSelf.rustBridge cancelSession]; + [strongSelf.hotkeyMonitor resetToIdle]; + [strongSelf.overlayPanel updateState:@"idle"]; + [strongSelf.statusBarManager updateState:@"idle"]; + }; +} + +- (void)stopEscapeKeyMonitoring { + self.hotkeyMonitor.escapeKeyHandler = nil; +} + @end diff --git a/KoeApp/Koe/Hotkey/SPHotkeyMonitor.h b/KoeApp/Koe/Hotkey/SPHotkeyMonitor.h index 620a18c..ca73a7d 100644 --- a/KoeApp/Koe/Hotkey/SPHotkeyMonitor.h +++ b/KoeApp/Koe/Hotkey/SPHotkeyMonitor.h @@ -56,4 +56,9 @@ typedef NS_ENUM(uint8_t, SPHotkeyMatchKind) { /// Used to dismiss the overlay when the user resumes typing after text insertion. @property (nonatomic, copy) void (^anyKeyDismissHandler)(void); +/// Optional block called when ESC (keyCode 53) is pressed during an active recording session. +/// The key event is NOT consumed — it passes through to the target app. +/// Use this to cancel the current session without producing output. +@property (nonatomic, copy) void (^escapeKeyHandler)(void); + @end diff --git a/KoeApp/Koe/Hotkey/SPHotkeyMonitor.m b/KoeApp/Koe/Hotkey/SPHotkeyMonitor.m index d9b775f..dfed7a9 100644 --- a/KoeApp/Koe/Hotkey/SPHotkeyMonitor.m +++ b/KoeApp/Koe/Hotkey/SPHotkeyMonitor.m @@ -104,6 +104,15 @@ static CGEventRef hotkeyEventCallback(CGEventTapProxy proxy, return monitor.canConsumeGlobalKeyEvents ? NULL : event; } + // ESC (keyCode 53) during recording → cancel session. + if (type == kCGEventKeyDown && keyCode == 53 && monitor.escapeKeyHandler) { + void (^handler)(void) = monitor.escapeKeyHandler; + dispatch_async(dispatch_get_main_queue(), ^{ + handler(); + }); + return monitor.canConsumeGlobalKeyEvents ? NULL : event; + } + // Any keyDown (not handled by number keys above) dismisses the overlay. // The event is NOT consumed — it passes through to the target app. if (type == kCGEventKeyDown && monitor.anyKeyDismissHandler) { @@ -340,6 +349,12 @@ - (BOOL)handleNSEvent:(NSEvent *)event { return YES; } + // ESC (keyCode 53) during recording → cancel session. + if (event.type == NSEventTypeKeyDown && keyCode == 53 && self.escapeKeyHandler) { + self.escapeKeyHandler(); + return YES; // consume + } + // Some macOS versions send modifier keys as keyDown/keyUp events. Keep // a direct keyDown/keyUp fallback for modifier-only triggers like Fn. BOOL shouldHandleTriggerKeyEvent = NO; diff --git a/KoeApp/Koe/Localization/SPLocalization.h b/KoeApp/Koe/Localization/SPLocalization.h index 7e6fbe4..1554c48 100644 --- a/KoeApp/Koe/Localization/SPLocalization.h +++ b/KoeApp/Koe/Localization/SPLocalization.h @@ -49,6 +49,14 @@ extern NSNotificationName const SPLocalizationLanguageDidChangeNotification; /// changes. + (void)invalidateCache; +/// Returns every language code shipped in the app bundle (each .lproj +/// folder containing Localizable.strings). E.g. @[@"en", @"zh-Hans", ...]. ++ (NSArray *)availableLanguages; + +/// Returns a human-readable endonym for a language code (e.g. "en" → +/// "English", "zh-Hans" → "简体中文"), suitable for a language picker. ++ (NSString *)displayNameForLanguage:(NSString *)code; + @end NS_ASSUME_NONNULL_END diff --git a/KoeApp/Koe/Localization/SPLocalization.m b/KoeApp/Koe/Localization/SPLocalization.m index 57a2d49..8857849 100644 --- a/KoeApp/Koe/Localization/SPLocalization.m +++ b/KoeApp/Koe/Localization/SPLocalization.m @@ -97,4 +97,34 @@ + (NSBundle *)bundleForLanguage:(NSString *)language { return [NSBundle mainBundle]; } ++ (NSArray *)availableLanguages { + NSFileManager *fm = [NSFileManager defaultManager]; + NSString *resourcePath = [NSBundle mainBundle].resourcePath; + if (!resourcePath) return @[]; + + NSArray *entries = [fm contentsOfDirectoryAtPath:resourcePath error:NULL] ?: @[]; + NSMutableArray *langs = [NSMutableArray array]; + for (NSString *entry in entries) { + if (![entry.pathExtension isEqualToString:@"lproj"]) continue; + NSString *lproj = [resourcePath stringByAppendingPathComponent:entry]; + NSString *strings = [lproj stringByAppendingPathComponent:@"Localizable.strings"]; + if (![fm fileExistsAtPath:strings]) continue; + NSString *code = [entry stringByDeletingPathExtension]; + [langs addObject:code]; + } + [langs sortUsingSelector:@selector(caseInsensitiveCompare:)]; + return langs; +} + ++ (NSString *)displayNameForLanguage:(NSString *)code { + if (!code.length) return @""; + NSLocale *targetLocale = [NSLocale localeWithLocaleIdentifier:code]; + NSString *name = [targetLocale localizedStringForLocaleIdentifier:code]; + if (name.length) { + NSString *first = [[name substringToIndex:1] uppercaseStringWithLocale:targetLocale]; + return [name stringByReplacingCharactersInRange:NSMakeRange(0, 1) withString:first]; + } + return code; +} + @end diff --git a/KoeApp/Koe/SetupWizard/SPSetupWizardWindowController.m b/KoeApp/Koe/SetupWizard/SPSetupWizardWindowController.m index 1407a2a..2c2a2bc 100644 --- a/KoeApp/Koe/SetupWizard/SPSetupWizardWindowController.m +++ b/KoeApp/Koe/SetupWizard/SPSetupWizardWindowController.m @@ -561,6 +561,7 @@ @interface SPSetupWizardWindowController () *supportedLocalProviders = [self.rustBridge supportedLocalProviders]; // Add Apple Speech (macOS 26+, no model download required; also requires the // apple-speech feature to be compiled into the Rust core — excluded on x86_64) if (@available(macOS 26.0, *)) { if ([supportedLocalProviders containsObject:@"apple-speech"]) { - [self.asrProviderPopup addItemWithTitle:@"Apple Speech (On-Device)"]; + [self.asrProviderPopup addItemWithTitle:KoeLocalizedString(@"wizard.asr.provider.appleSpeech")]; [self.asrProviderPopup lastItem].representedObject = @"apple-speech"; } } // Add local providers supported by this build (model-based) NSDictionary *localProviderLabels = @{ - @"mlx": @"MLX (Apple Silicon)", - @"sherpa-onnx": @"Sherpa-ONNX", + @"mlx": KoeLocalizedString(@"wizard.asr.provider.mlx"), + @"sherpa-onnx": KoeLocalizedString(@"wizard.asr.provider.sherpa"), }; for (NSString *provider in supportedLocalProviders) { NSString *label = localProviderLabels[provider]; @@ -827,21 +848,21 @@ - (NSView *)buildAsrPane { [pane addSubview:self.asrProviderPopup]; // Test button next to Provider - self.asrTestButton = [NSButton buttonWithTitle:@"Test" target:self action:@selector(testAsrConnection:)]; + self.asrTestButton = [NSButton buttonWithTitle:KoeLocalizedString(@"wizard.asr.test") target:self action:@selector(testAsrConnection:)]; self.asrTestButton.bezelStyle = NSBezelStyleRounded; self.asrTestButton.frame = NSMakeRect(fieldX + 208, y - 2, 70, 28); [pane addSubview:self.asrTestButton]; y -= rowH; // App Key (Doubao only) - self.asrAppKeyField = [self formTextField:NSMakeRect(fieldX, y, fieldW, 22) placeholder:@"Volcengine App ID"]; + self.asrAppKeyField = [self formTextField:NSMakeRect(fieldX, y, fieldW, 22) placeholder:KoeLocalizedString(@"wizard.asr.appKeyDoubao")]; [pane addSubview:self.asrAppKeyField]; - NSTextField *appKeyLabel = [self formLabel:@"App Key" frame:NSMakeRect(16, y, labelW, 22)]; + NSTextField *appKeyLabel = [self formLabel:KoeLocalizedString(@"wizard.asr.appKey") frame:NSMakeRect(16, y, labelW, 22)]; appKeyLabel.tag = 1001; [pane addSubview:appKeyLabel]; // Apple Speech locale popup (same row as App Key / Model, tag 1005) - NSTextField *localeLabel = [self formLabel:@"Language" frame:NSMakeRect(16, y, labelW, 22)]; + NSTextField *localeLabel = [self formLabel:KoeLocalizedString(@"wizard.asr.language") frame:NSMakeRect(16, y, labelW, 22)]; localeLabel.tag = 1005; localeLabel.hidden = YES; [pane addSubview:localeLabel]; @@ -854,7 +875,7 @@ - (NSView *)buildAsrPane { [pane addSubview:self.appleSpeechLocalePopup]; // Row 1: Model popup + Download button (Local providers, same row as App Key) - self.localModelLabel = [self formLabel:@"Model" frame:NSMakeRect(16, y, labelW, 22)]; + self.localModelLabel = [self formLabel:KoeLocalizedString(@"wizard.asr.model") frame:NSMakeRect(16, y, labelW, 22)]; self.localModelLabel.tag = 1004; self.localModelLabel.hidden = YES; [pane addSubview:self.localModelLabel]; @@ -867,7 +888,7 @@ - (NSView *)buildAsrPane { // Download button (right of model popup, same style as eye button) self.modelDownloadButton = [[NSButton alloc] initWithFrame:NSMakeRect(fieldX + fieldW - 20, y + 1, 20, 20)]; self.modelDownloadButton.image = [NSImage imageWithSystemSymbolName:@"arrow.down.circle" - accessibilityDescription:@"Download"]; + accessibilityDescription:KoeLocalizedString(@"wizard.asr.download")]; self.modelDownloadButton.bezelStyle = NSBezelStyleInline; self.modelDownloadButton.bordered = NO; self.modelDownloadButton.imageScaling = NSImageScaleProportionallyUpOrDown; @@ -891,7 +912,7 @@ - (NSView *)buildAsrPane { // Delete button (right end of status row, same style as eye button) self.modelDeleteButton = [[NSButton alloc] initWithFrame:NSMakeRect(fieldX + fieldW - 20, y + 1, 20, 20)]; self.modelDeleteButton.image = [NSImage imageWithSystemSymbolName:@"trash" - accessibilityDescription:@"Delete"]; + accessibilityDescription:KoeLocalizedString(@"wizard.asr.delete")]; self.modelDeleteButton.bezelStyle = NSBezelStyleInline; self.modelDeleteButton.bordered = NO; self.modelDeleteButton.imageScaling = NSImageScaleProportionallyUpOrDown; @@ -931,34 +952,34 @@ - (NSView *)buildAsrPane { CGFloat secFieldW = fieldW - eyeW - 4; self.asrAccessKeySecureField = [[NSSecureTextField alloc] initWithFrame:NSMakeRect(fieldX, accessKeyY, secFieldW, 22)]; - self.asrAccessKeySecureField.placeholderString = @"Volcengine Access Token"; + self.asrAccessKeySecureField.placeholderString = KoeLocalizedString(@"wizard.asr.accessKeyDoubao"); self.asrAccessKeySecureField.font = [NSFont systemFontOfSize:13]; [pane addSubview:self.asrAccessKeySecureField]; - self.asrAccessKeyField = [self formTextField:NSMakeRect(fieldX, accessKeyY, secFieldW, 22) placeholder:@"Volcengine Access Token"]; + self.asrAccessKeyField = [self formTextField:NSMakeRect(fieldX, accessKeyY, secFieldW, 22) placeholder:KoeLocalizedString(@"wizard.asr.accessKeyDoubao")]; self.asrAccessKeyField.hidden = YES; [pane addSubview:self.asrAccessKeyField]; self.asrAccessKeyToggle = [self eyeButtonWithFrame:NSMakeRect(fieldX + secFieldW + 4, accessKeyY - 1, eyeW, 24) action:@selector(toggleAsrAccessKeyVisibility:)]; [pane addSubview:self.asrAccessKeyToggle]; - NSTextField *accessKeyLabel = [self formLabel:@"Access Key" frame:NSMakeRect(16, accessKeyY, labelW, 22)]; + NSTextField *accessKeyLabel = [self formLabel:KoeLocalizedString(@"wizard.asr.accessKey") frame:NSMakeRect(16, accessKeyY, labelW, 22)]; accessKeyLabel.tag = 1002; [pane addSubview:accessKeyLabel]; // Qwen API Key — fixed at row 1 (same position as App Key) CGFloat qwenY = formStartY - rowH; self.asrQwenApiKeySecureField = [[NSSecureTextField alloc] initWithFrame:NSMakeRect(fieldX, qwenY, secFieldW, 22)]; - self.asrQwenApiKeySecureField.placeholderString = @"DashScope API Key (sk-xxx)"; + self.asrQwenApiKeySecureField.placeholderString = KoeLocalizedString(@"wizard.asr.apiKeyQwenPlaceholder"); self.asrQwenApiKeySecureField.font = [NSFont systemFontOfSize:13]; self.asrQwenApiKeySecureField.hidden = YES; [pane addSubview:self.asrQwenApiKeySecureField]; - self.asrQwenApiKeyField = [self formTextField:NSMakeRect(fieldX, qwenY, secFieldW, 22) placeholder:@"DashScope API Key (sk-xxx)"]; + self.asrQwenApiKeyField = [self formTextField:NSMakeRect(fieldX, qwenY, secFieldW, 22) placeholder:KoeLocalizedString(@"wizard.asr.apiKeyQwenPlaceholder")]; self.asrQwenApiKeyField.hidden = YES; [pane addSubview:self.asrQwenApiKeyField]; self.asrQwenApiKeyToggle = [self eyeButtonWithFrame:NSMakeRect(fieldX + secFieldW + 4, qwenY - 1, eyeW, 24) action:@selector(toggleQwenApiKeyVisibility:)]; self.asrQwenApiKeyToggle.hidden = YES; [pane addSubview:self.asrQwenApiKeyToggle]; - NSTextField *qwenKeyLabel = [self formLabel:@"API Key" frame:NSMakeRect(16, qwenY, labelW, 22)]; + NSTextField *qwenKeyLabel = [self formLabel:KoeLocalizedString(@"wizard.asr.apiKey") frame:NSMakeRect(16, qwenY, labelW, 22)]; qwenKeyLabel.tag = 1003; qwenKeyLabel.hidden = YES; [pane addSubview:qwenKeyLabel]; @@ -1005,7 +1026,7 @@ - (NSView *)buildLlmPane { CGFloat y = contentHeight - 30.0; // Description - NSTextField *desc = [self addSettingsDescriptionText:@"Configure LLM for post-correction. When disabled, raw ASR output is used directly." + NSTextField *desc = [self addSettingsDescriptionText:KoeLocalizedString(@"wizard.llm.description") toPane:pane topY:y x:contentX @@ -1015,12 +1036,12 @@ - (NSView *)buildLlmPane { // Enabled toggle self.llmEnabledCheckbox = [self settingsSwitchWithAction:@selector(llmEnabledToggled:)]; NSView *llmEnabledCard = [self settingsToggleCardWithFrame:NSMakeRect(contentX, y - 48.0, contentW, 48.0) - title:@"LLM Correction" + title:KoeLocalizedString(@"wizard.llm.correction") toggle:self.llmEnabledCheckbox]; [pane addSubview:llmEnabledCard]; y = NSMinY(llmEnabledCard.frame) - 24.0; - NSTextField *sectionTitle = [self sectionTitleLabel:@"Profiles" + NSTextField *sectionTitle = [self sectionTitleLabel:KoeLocalizedString(@"wizard.llm.profiles") frame:NSMakeRect(contentX, floor(y - 20.0), contentW, 20.0)]; [pane addSubview:sectionTitle]; y = NSMinY(sectionTitle.frame) - 16.0; @@ -1060,14 +1081,14 @@ - (NSView *)buildLlmPane { // Sidebar +/- buttons (Finder-style) self.llmAddProfileButton = [[NSButton alloc] initWithFrame:NSMakeRect(sidebarX, sidebarY, 28, 24)]; self.llmAddProfileButton.bezelStyle = NSBezelStyleSmallSquare; - self.llmAddProfileButton.image = [NSImage imageWithSystemSymbolName:@"plus" accessibilityDescription:@"Add"]; + self.llmAddProfileButton.image = [NSImage imageWithSystemSymbolName:@"plus" accessibilityDescription:KoeLocalizedString(@"wizard.llm.add")]; self.llmAddProfileButton.target = self; self.llmAddProfileButton.action = @selector(showAddLlmProfileMenu:); [pane addSubview:self.llmAddProfileButton]; self.llmDeleteProfileButton = [[NSButton alloc] initWithFrame:NSMakeRect(sidebarX + 30, sidebarY, 28, 24)]; self.llmDeleteProfileButton.bezelStyle = NSBezelStyleSmallSquare; - self.llmDeleteProfileButton.image = [NSImage imageWithSystemSymbolName:@"minus" accessibilityDescription:@"Delete"]; + self.llmDeleteProfileButton.image = [NSImage imageWithSystemSymbolName:@"minus" accessibilityDescription:KoeLocalizedString(@"wizard.llm.delete")]; self.llmDeleteProfileButton.target = self; self.llmDeleteProfileButton.action = @selector(deleteLlmProfile:); [pane addSubview:self.llmDeleteProfileButton]; @@ -1076,9 +1097,9 @@ - (NSView *)buildLlmPane { CGFloat detailY = y; // top of form area aligns with top of sidebar // Name field (editable custom display name for this profile) - NSTextField *nameLabel = [self formLabel:@"Name" frame:NSMakeRect(detailLabelX, detailY, labelW, 22)]; + NSTextField *nameLabel = [self formLabel:KoeLocalizedString(@"wizard.llm.name") frame:NSMakeRect(detailLabelX, detailY, labelW, 22)]; [pane addSubview:nameLabel]; - self.llmProfileNameField = [self formTextField:NSMakeRect(fieldX, detailY, fieldW, 22) placeholder:@"My profile"]; + self.llmProfileNameField = [self formTextField:NSMakeRect(fieldX, detailY, fieldW, 22) placeholder:KoeLocalizedString(@"wizard.llm.myProfile")]; self.llmProfileNameField.target = self; self.llmProfileNameField.action = @selector(llmProfileNameChanged:); self.llmProfileNameField.delegate = self; @@ -1086,7 +1107,7 @@ - (NSView *)buildLlmPane { detailY -= rowH; // Type (read-only label — locked at creation) - NSTextField *typeLabelLeft = [self formLabel:@"Type" frame:NSMakeRect(detailLabelX, detailY, labelW, 22)]; + NSTextField *typeLabelLeft = [self formLabel:KoeLocalizedString(@"wizard.llm.type") frame:NSMakeRect(detailLabelX, detailY, labelW, 22)]; [pane addSubview:typeLabelLeft]; self.llmProfileTypeLabel = [NSTextField labelWithString:@""]; self.llmProfileTypeLabel.frame = NSMakeRect(fieldX, detailY, fieldW, 22); @@ -1100,7 +1121,7 @@ - (NSView *)buildLlmPane { // --- OpenAI fields (tag 2001-2008 for show/hide) --- // Base URL - NSTextField *baseUrlLabel = [self formLabel:@"Base URL" frame:NSMakeRect(detailLabelX, detailY, labelW, 22)]; + NSTextField *baseUrlLabel = [self formLabel:KoeLocalizedString(@"wizard.llm.baseUrl") frame:NSMakeRect(detailLabelX, detailY, labelW, 22)]; baseUrlLabel.tag = 2001; [pane addSubview:baseUrlLabel]; self.llmBaseUrlField = [self formTextField:NSMakeRect(fieldX, detailY, fieldW, 22) placeholder:@"https://api.openai.com/v1"]; @@ -1111,15 +1132,15 @@ - (NSView *)buildLlmPane { // API Key (secure by default) CGFloat eyeW = 28; CGFloat secFieldW = fieldW - eyeW - 4; - NSTextField *apiKeyLabel = [self formLabel:@"API Key" frame:NSMakeRect(detailLabelX, detailY, labelW, 22)]; + NSTextField *apiKeyLabel = [self formLabel:KoeLocalizedString(@"wizard.llm.apiKey") frame:NSMakeRect(detailLabelX, detailY, labelW, 22)]; apiKeyLabel.tag = 2002; [pane addSubview:apiKeyLabel]; self.llmApiKeySecureField = [[NSSecureTextField alloc] initWithFrame:NSMakeRect(fieldX, detailY, secFieldW, 22)]; - self.llmApiKeySecureField.placeholderString = @"sk-... (leave empty if not required)"; + self.llmApiKeySecureField.placeholderString = KoeLocalizedString(@"wizard.llm.apiKeyPlaceholder"); self.llmApiKeySecureField.font = [NSFont systemFontOfSize:13]; self.llmApiKeySecureField.tag = 2002; [pane addSubview:self.llmApiKeySecureField]; - self.llmApiKeyField = [self formTextField:NSMakeRect(fieldX, detailY, secFieldW, 22) placeholder:@"sk-... (leave empty if not required)"]; + self.llmApiKeyField = [self formTextField:NSMakeRect(fieldX, detailY, secFieldW, 22) placeholder:KoeLocalizedString(@"wizard.llm.apiKeyPlaceholder")]; self.llmApiKeyField.hidden = YES; self.llmApiKeyField.tag = 2002; [pane addSubview:self.llmApiKeyField]; @@ -1131,13 +1152,13 @@ - (NSView *)buildLlmPane { // Model (text field for OpenAI) + Choose button (toggles remote model picker) CGFloat modelPickerButtonW = 74; CGFloat modelFieldW = fieldW - modelPickerButtonW - 6; - NSTextField *modelLabel = [self formLabel:@"Model" frame:NSMakeRect(detailLabelX, detailY, labelW, 22)]; + NSTextField *modelLabel = [self formLabel:KoeLocalizedString(@"wizard.llm.model") frame:NSMakeRect(detailLabelX, detailY, labelW, 22)]; modelLabel.tag = 2003; [pane addSubview:modelLabel]; self.llmModelField = [self formTextField:NSMakeRect(fieldX, detailY, modelFieldW, 22) placeholder:@"gpt-5.4-nano"]; self.llmModelField.tag = 2003; [pane addSubview:self.llmModelField]; - self.llmToggleModelPickerButton = [NSButton buttonWithTitle:@"Choose" + self.llmToggleModelPickerButton = [NSButton buttonWithTitle:KoeLocalizedString(@"wizard.llm.choose") target:self action:@selector(toggleLlmRemoteModelPicker:)]; self.llmToggleModelPickerButton.frame = NSMakeRect(fieldX + modelFieldW + 6, detailY - 2, modelPickerButtonW, 26); @@ -1147,17 +1168,17 @@ - (NSView *)buildLlmPane { detailY -= rowH; // Model List (OpenAI /models) — initially hidden, toggled by Choose button - NSTextField *modelListLabel = [self formLabel:@"Model List" frame:NSMakeRect(detailLabelX, detailY, labelW, 22)]; + NSTextField *modelListLabel = [self formLabel:KoeLocalizedString(@"wizard.llm.modelList") frame:NSMakeRect(detailLabelX, detailY, labelW, 22)]; modelListLabel.tag = 2004; [pane addSubview:modelListLabel]; self.llmRemoteModelPopup = [[NSPopUpButton alloc] initWithFrame:NSMakeRect(fieldX, detailY - 2, fieldW - 74, 26) pullsDown:NO]; self.llmRemoteModelPopup.tag = 2004; - [self.llmRemoteModelPopup addItemWithTitle:@"No models loaded"]; + [self.llmRemoteModelPopup addItemWithTitle:KoeLocalizedString(@"wizard.llm.noModels")]; self.llmRemoteModelPopup.enabled = NO; [self.llmRemoteModelPopup setTarget:self]; [self.llmRemoteModelPopup setAction:@selector(llmRemoteModelChanged:)]; [pane addSubview:self.llmRemoteModelPopup]; - self.llmRefreshModelsButton = [NSButton buttonWithTitle:@"Refresh" target:self action:@selector(refreshLlmRemoteModels:)]; + self.llmRefreshModelsButton = [NSButton buttonWithTitle:KoeLocalizedString(@"wizard.llm.refresh") target:self action:@selector(refreshLlmRemoteModels:)]; self.llmRefreshModelsButton.frame = NSMakeRect(fieldX + fieldW - 66, detailY - 2, 66, 26); self.llmRefreshModelsButton.bezelStyle = NSBezelStyleRounded; self.llmRefreshModelsButton.tag = 2004; @@ -1168,7 +1189,7 @@ - (NSView *)buildLlmPane { detailY -= rowH + 4; // Chat Completions Path - NSTextField *chatPathLabel = [self formLabel:@"Chat Path" frame:NSMakeRect(detailLabelX, detailY, labelW, 22)]; + NSTextField *chatPathLabel = [self formLabel:KoeLocalizedString(@"wizard.llm.chatPath") frame:NSMakeRect(detailLabelX, detailY, labelW, 22)]; chatPathLabel.tag = 2005; [pane addSubview:chatPathLabel]; self.llmChatCompletionsPathField = [self formTextField:NSMakeRect(fieldX, detailY, fieldW, 22) @@ -1178,7 +1199,7 @@ - (NSView *)buildLlmPane { detailY -= rowH; // Max Token Parameter - NSTextField *tokenParamLabel = [self formLabel:@"Token Parameter" frame:NSMakeRect(detailLabelX, detailY, labelW, 22)]; + NSTextField *tokenParamLabel = [self formLabel:KoeLocalizedString(@"wizard.llm.tokenParameter") frame:NSMakeRect(detailLabelX, detailY, labelW, 22)]; tokenParamLabel.tag = 2006; [pane addSubview:tokenParamLabel]; self.maxTokenParamPopup = [[NSPopUpButton alloc] initWithFrame:NSMakeRect(fieldX, detailY - 2, 240, 26) pullsDown:NO]; @@ -1193,14 +1214,14 @@ - (NSView *)buildLlmPane { detailY -= 42; // Hint text - NSTextField *tokenHint = [self descriptionLabel:@"GPT-4o and older models use max_tokens. GPT-5 and reasoning models (o1/o3) use max_completion_tokens."]; + NSTextField *tokenHint = [self descriptionLabel:KoeLocalizedString(@"wizard.llm.tokenParameter.hint")]; tokenHint.frame = NSMakeRect(fieldX, detailY - 2, fieldW, 32); tokenHint.tag = 2007; [pane addSubview:tokenHint]; detailY -= 44; // Test button - self.llmTestButton = [NSButton buttonWithTitle:@"Test Connection" target:self action:@selector(testLlmConnection:)]; + self.llmTestButton = [NSButton buttonWithTitle:KoeLocalizedString(@"wizard.llm.testConnection") target:self action:@selector(testLlmConnection:)]; self.llmTestButton.bezelStyle = NSBezelStyleRounded; self.llmTestButton.frame = NSMakeRect(fieldX, detailY, 130, 28); self.llmTestButton.tag = 2008; @@ -1219,7 +1240,7 @@ - (NSView *)buildLlmPane { CGFloat mlxY = providerDetailStartY; // same Y as Base URL row // MLX Model popup + Download button - NSTextField *llmModelLabel = [self formLabel:@"Model" frame:NSMakeRect(detailLabelX, mlxY, labelW, 22)]; + NSTextField *llmModelLabel = [self formLabel:KoeLocalizedString(@"wizard.llm.model") frame:NSMakeRect(detailLabelX, mlxY, labelW, 22)]; llmModelLabel.tag = 2010; llmModelLabel.hidden = YES; [pane addSubview:llmModelLabel]; @@ -1232,7 +1253,7 @@ - (NSView *)buildLlmPane { self.llmModelDownloadButton = [[NSButton alloc] initWithFrame:NSMakeRect(fieldX + fieldW - 20, mlxY + 1, 20, 20)]; self.llmModelDownloadButton.image = [NSImage imageWithSystemSymbolName:@"arrow.down.circle" - accessibilityDescription:@"Download"]; + accessibilityDescription:KoeLocalizedString(@"wizard.llm.download")]; self.llmModelDownloadButton.bezelStyle = NSBezelStyleInline; self.llmModelDownloadButton.bordered = NO; self.llmModelDownloadButton.imageScaling = NSImageScaleProportionallyUpOrDown; @@ -1257,7 +1278,7 @@ - (NSView *)buildLlmPane { self.llmModelDeleteButton = [[NSButton alloc] initWithFrame:NSMakeRect(fieldX + fieldW - 20, mlxY + 1, 20, 20)]; self.llmModelDeleteButton.image = [NSImage imageWithSystemSymbolName:@"trash" - accessibilityDescription:@"Delete"]; + accessibilityDescription:KoeLocalizedString(@"wizard.llm.delete")]; self.llmModelDeleteButton.bezelStyle = NSBezelStyleInline; self.llmModelDeleteButton.bordered = NO; self.llmModelDeleteButton.imageScaling = NSImageScaleProportionallyUpOrDown; @@ -1300,7 +1321,7 @@ - (NSView *)buildOverlayPane { CGFloat paneWidth = 600.0; CGFloat contentX = 24.0; CGFloat contentW = paneWidth - 48.0; - NSString *descriptionText = @"Adjust the bottom live transcript overlay. Choose a system font, tune text size, set the bottom distance, and decide whether long live text stays capped to a few lines or expands fully. Every change is previewed directly in the real desktop overlay position."; + NSString *descriptionText = KoeLocalizedString(@"wizard.overlay.description"); self.overlayFontFamilyPopup = [self overlayFontFamilyPopupControl]; @@ -1323,20 +1344,20 @@ - (NSView *)buildOverlayPane { self.overlayLimitVisibleLinesSwitch = [self settingsSwitchWithAction:@selector(overlayControlChanged:)]; self.overlayMaxVisibleLinesPopup = [self overlayMaxVisibleLinesPopupControl]; - NSButton *resetButton = [NSButton buttonWithTitle:@"Reset to Default" + NSButton *resetButton = [NSButton buttonWithTitle:KoeLocalizedString(@"wizard.overlay.resetButton") target:self action:@selector(resetOverlaySettings:)]; resetButton.bezelStyle = NSBezelStyleRounded; resetButton.frame = NSMakeRect(0, 0, 126.0, 28.0); - NSView *controlsCard = [self cardWithTitle:@"Overlay" + NSView *controlsCard = [self cardWithTitle:KoeLocalizedString(@"wizard.overlay.cardTitle") rows:@[ - [self cardRowWithLabel:@"Font" control:self.overlayFontFamilyPopup], - [self cardRowWithLabel:@"Text Size" control:fontSliderControl], - [self cardRowWithLabel:@"Distance from Bottom" control:bottomSliderControl], - [self cardRowWithLabel:@"Limit Visible Lines" control:self.overlayLimitVisibleLinesSwitch], - [self cardRowWithLabel:@"Max Visible Lines" control:self.overlayMaxVisibleLinesPopup], - [self cardRowWithLabel:@"Defaults" control:resetButton], + [self cardRowWithLabel:KoeLocalizedString(@"wizard.overlay.font") control:self.overlayFontFamilyPopup], + [self cardRowWithLabel:KoeLocalizedString(@"wizard.overlay.textSize") control:fontSliderControl], + [self cardRowWithLabel:KoeLocalizedString(@"wizard.overlay.distanceFromBottom") control:bottomSliderControl], + [self cardRowWithLabel:KoeLocalizedString(@"wizard.overlay.limitVisibleLines") control:self.overlayLimitVisibleLinesSwitch], + [self cardRowWithLabel:KoeLocalizedString(@"wizard.overlay.maxVisibleLines") control:self.overlayMaxVisibleLinesPopup], + [self cardRowWithLabel:KoeLocalizedString(@"wizard.overlay.defaults") control:resetButton], ] width:contentW]; CGFloat descriptionHeight = [self fittingHeightForWrappingLabel:[self descriptionLabel:descriptionText] width:contentW]; @@ -1354,7 +1375,7 @@ - (NSView *)buildOverlayPane { y = NSMinY(desc.frame) - 18.0; CGFloat controlsTitleY = y - 20.0; - NSTextField *controlsTitle = [self sectionTitleLabel:@"Style Controls" + NSTextField *controlsTitle = [self sectionTitleLabel:KoeLocalizedString(@"wizard.overlay.styleControls") frame:NSMakeRect(contentX, controlsTitleY, contentW, 20.0)]; [pane addSubview:controlsTitle]; controlsCard.frame = NSMakeRect(contentX, @@ -1445,10 +1466,10 @@ - (NSView *)buildHotkeyPane { self.hotkeyPopup = [self hotkeyPresetPopup]; self.hotkeyPopup.target = self; self.hotkeyPopup.action = @selector(triggerHotkeyChanged:); - self.recordTriggerHotkeyButton = [NSButton buttonWithTitle:@"Record" target:self action:@selector(recordTriggerHotkey:)]; + self.recordTriggerHotkeyButton = [NSButton buttonWithTitle:KoeLocalizedString(@"wizard.hotkey.recordButton") target:self action:@selector(recordTriggerHotkey:)]; self.recordTriggerHotkeyButton.bezelStyle = NSBezelStyleRounded; self.recordTriggerHotkeyButton.frame = NSMakeRect(0, 0, 70, 28); - self.resetTriggerHotkeyButton = [NSButton buttonWithTitle:@"Reset" target:self action:@selector(resetTriggerHotkey:)]; + self.resetTriggerHotkeyButton = [NSButton buttonWithTitle:KoeLocalizedString(@"wizard.hotkey.resetButton") target:self action:@selector(resetTriggerHotkey:)]; self.resetTriggerHotkeyButton.bezelStyle = NSBezelStyleRounded; self.resetTriggerHotkeyButton.frame = NSMakeRect(0, 0, 58, 28); NSView *triggerShortcutControl = [self hotkeyPickerControlWithPopup:self.hotkeyPopup @@ -1458,16 +1479,20 @@ - (NSView *)buildHotkeyPane { // ── Trigger Mode ── self.triggerModePopup = [[NSPopUpButton alloc] initWithFrame:NSMakeRect(0, 0, 220, 26) pullsDown:NO]; [self.triggerModePopup addItemsWithTitles:@[ - @"Hold (Press & Hold)", - @"Toggle (Tap to Start/Stop)", + KoeLocalizedString(@"wizard.hotkey.mode.hold"), + KoeLocalizedString(@"wizard.hotkey.mode.toggle"), ]]; [self.triggerModePopup itemAtIndex:0].representedObject = @"hold"; [self.triggerModePopup itemAtIndex:1].representedObject = @"toggle"; + // ── ESC Cancel ── + self.escapeCancelCheckbox = [self settingsSwitchWithAction:NULL]; + // ── Trigger card ── - NSView *triggerCard = [self cardWithTitle:@"Trigger" rows:@[ - [self cardRowWithLabel:@"Trigger Shortcut" control:triggerShortcutControl], - [self cardRowWithLabel:@"Trigger Mode" control:self.triggerModePopup], + NSView *triggerCard = [self cardWithTitle:KoeLocalizedString(@"wizard.hotkey.cardTitle") rows:@[ + [self cardRowWithLabel:KoeLocalizedString(@"wizard.hotkey.triggerShortcut") control:triggerShortcutControl], + [self cardRowWithLabel:KoeLocalizedString(@"wizard.hotkey.triggerMode") control:self.triggerModePopup], + [self cardRowWithLabel:KoeLocalizedString(@"wizard.hotkey.escapeCancel") control:self.escapeCancelCheckbox], ] width:cardWidth]; // ── Feedback Sounds ── @@ -1475,10 +1500,10 @@ - (NSView *)buildHotkeyPane { self.stopSoundCheckbox = [self settingsSwitchWithAction:NULL]; self.errorSoundCheckbox = [self settingsSwitchWithAction:NULL]; - NSView *feedbackCard = [self cardWithTitle:@"Feedback Sounds" rows:@[ - [self cardRowWithLabel:@"Recording starts" control:self.startSoundCheckbox], - [self cardRowWithLabel:@"Recording stops" control:self.stopSoundCheckbox], - [self cardRowWithLabel:@"Error occurs" control:self.errorSoundCheckbox], + NSView *feedbackCard = [self cardWithTitle:KoeLocalizedString(@"wizard.feedback.cardTitle") rows:@[ + [self cardRowWithLabel:KoeLocalizedString(@"wizard.feedback.startSound") control:self.startSoundCheckbox], + [self cardRowWithLabel:KoeLocalizedString(@"wizard.feedback.stopSound") control:self.stopSoundCheckbox], + [self cardRowWithLabel:KoeLocalizedString(@"wizard.feedback.errorSound") control:self.errorSoundCheckbox], ] width:cardWidth]; // ── Layout ── @@ -1711,13 +1736,13 @@ - (NSView *)buildDictionaryPane { CGFloat y = contentHeight - 30.0; // Description - NSTextField *desc = [self addSettingsDescriptionText:@"User dictionary \u2014 one term per line. These terms are prioritized during LLM correction. Lines starting with # are comments." + NSTextField *desc = [self addSettingsDescriptionText:KoeLocalizedString(@"wizard.dict.description") toPane:pane topY:y x:contentX width:contentW]; - NSTextField *sectionTitle = [self sectionTitleLabel:@"Dictionary" + NSTextField *sectionTitle = [self sectionTitleLabel:KoeLocalizedString(@"wizard.dict.title") frame:NSMakeRect(contentX, floor(NSMinY(desc.frame) - 36.0), contentW, 20)]; [pane addSubview:sectionTitle]; @@ -1767,13 +1792,13 @@ - (NSView *)buildSystemPromptPane { CGFloat y = contentHeight - 30.0; // Description - NSTextField *desc = [self addSettingsDescriptionText:@"System prompt sent to the LLM for text correction. Edit to customize behavior." + NSTextField *desc = [self addSettingsDescriptionText:KoeLocalizedString(@"wizard.prompt.description") toPane:pane topY:y x:contentX width:contentW]; - NSTextField *sectionTitle = [self sectionTitleLabel:@"System Prompt" + NSTextField *sectionTitle = [self sectionTitleLabel:KoeLocalizedString(@"wizard.prompt.title") frame:NSMakeRect(contentX, floor(NSMinY(desc.frame) - 36.0), contentW, 20)]; [pane addSubview:sectionTitle]; @@ -1824,7 +1849,7 @@ - (NSView *)buildTemplatesPane { CGFloat contentW = paneWidth - 48.0; CGFloat y = contentHeight - 30.0; - NSTextField *desc = [self addSettingsDescriptionText:@"Manage overlay templates. Reorder them, control visibility, and edit each prompt here." + NSTextField *desc = [self addSettingsDescriptionText:KoeLocalizedString(@"wizard.templates.description") toPane:pane topY:y x:contentX @@ -1833,7 +1858,7 @@ - (NSView *)buildTemplatesPane { self.templatesEnabledSwitch = [self settingsSwitchWithAction:NULL]; NSView *visibilityCard = [self settingsToggleCardWithFrame:NSMakeRect(contentX, y - 48, contentW, 48) - title:@"Show template buttons in overlay" + title:KoeLocalizedString(@"wizard.templates.showButtons") toggle:self.templatesEnabledSwitch]; [pane addSubview:visibilityCard]; @@ -1845,11 +1870,11 @@ - (NSView *)buildTemplatesPane { CGFloat editorW = contentW - listW - cardGap; CGFloat editorX = contentX + listW + cardGap; - NSTextField *listTitle = [self sectionTitleLabel:@"Template Library" + NSTextField *listTitle = [self sectionTitleLabel:KoeLocalizedString(@"wizard.templates.library") frame:NSMakeRect(contentX, sectionTitleY, listW, 20)]; [pane addSubview:listTitle]; - NSTextField *editorTitle = [self sectionTitleLabel:@"Template Editor" + NSTextField *editorTitle = [self sectionTitleLabel:KoeLocalizedString(@"wizard.templates.editor") frame:NSMakeRect(editorX, sectionTitleY, editorW, 20)]; [pane addSubview:editorTitle]; @@ -1879,13 +1904,13 @@ - (NSView *)buildTemplatesPane { [listCard addSubview:footerSeparator]; self.templatePrimaryActionsControl = [self templateActionSegmentedControlWithSymbols:@[@"plus", @"minus"] - toolTips:@[@"Add template", @"Remove selected template"] + toolTips:@[KoeLocalizedString(@"wizard.templates.addTooltip"), KoeLocalizedString(@"wizard.templates.removeTooltip")] action:@selector(handleTemplatePrimaryActions:)]; self.templatePrimaryActionsControl.frame = NSMakeRect(12, 5, 50, 24); [listCard addSubview:self.templatePrimaryActionsControl]; self.templateReorderActionsControl = [self templateActionSegmentedControlWithSymbols:@[@"arrow.up", @"arrow.down"] - toolTips:@[@"Move selected template up", @"Move selected template down"] + toolTips:@[KoeLocalizedString(@"wizard.templates.moveUpTooltip"), KoeLocalizedString(@"wizard.templates.moveDownTooltip")] action:@selector(handleTemplateReorderActions:)]; self.templateReorderActionsControl.frame = NSMakeRect(listW - 12 - 50, 5, 50, 24); [listCard addSubview:self.templateReorderActionsControl]; @@ -1919,10 +1944,10 @@ - (NSView *)buildTemplatesPane { self.templatesTableView.dataSource = (id)self; scrollView.documentView = self.templatesTableView; - NSTextField *nameLabel = [self sectionTitleLabel:@"Name" frame:NSMakeRect(16, mainCardH - 34, editorW - 32, 18)]; + NSTextField *nameLabel = [self sectionTitleLabel:KoeLocalizedString(@"wizard.templates.name") frame:NSMakeRect(16, mainCardH - 34, editorW - 32, 18)]; [editorCard addSubview:nameLabel]; - self.templateNameField = [self formTextField:NSMakeRect(16, mainCardH - 64, editorW - 32, 24) placeholder:@"Template name"]; + self.templateNameField = [self formTextField:NSMakeRect(16, mainCardH - 64, editorW - 32, 24) placeholder:KoeLocalizedString(@"wizard.templates.templateName")]; self.templateNameField.delegate = self; [editorCard addSubview:self.templateNameField]; @@ -1932,7 +1957,7 @@ - (NSView *)buildTemplatesPane { CGFloat templateItemToggleH = self.templateItemEnabledSwitch.frame.size.height; CGFloat templateVisibilityCenterY = mainCardH - 86.0; - NSTextField *templateVisibilityLabel = [self settingsRowLabelWithString:@"Visible in overlay"]; + NSTextField *templateVisibilityLabel = [self settingsRowLabelWithString:KoeLocalizedString(@"wizard.templates.visible")]; templateVisibilityLabel.frame = NSMakeRect(16, floor(templateVisibilityCenterY - 10.0), editorW - templateItemToggleW - 44.0, @@ -1945,7 +1970,7 @@ - (NSView *)buildTemplatesPane { templateItemToggleH); [editorCard addSubview:self.templateItemEnabledSwitch]; - NSTextField *promptLabel = [self sectionTitleLabel:@"Prompt" frame:NSMakeRect(16, mainCardH - 124, editorW - 32, 18)]; + NSTextField *promptLabel = [self sectionTitleLabel:KoeLocalizedString(@"wizard.templates.prompt") frame:NSMakeRect(16, mainCardH - 124, editorW - 32, 18)]; [editorCard addSubview:promptLabel]; NSScrollView *promptScroll = [[NSScrollView alloc] initWithFrame:NSMakeRect(16, 16, editorW - 32, mainCardH - 146)]; @@ -2057,7 +2082,7 @@ - (NSView *)tableView:(NSTableView *)tableView viewForTableColumn:(NSTableColumn if (row < (NSInteger)self.templatesData.count) { NSDictionary *tmpl = self.templatesData[row]; - NSString *name = tmpl[@"name"] ?: @"Untitled"; + NSString *name = tmpl[@"name"] ?: KoeLocalizedString(@"wizard.templates.untitled"); BOOL enabled = [self isTemplateEnabled:tmpl]; NSTextField *titleLabel = nil; @@ -2363,7 +2388,7 @@ - (void)reloadTemplateTableSelectingRow:(NSInteger)row { - (BOOL)validateTemplatesDataWithMessage:(NSString **)message { if (self.templatesData.count > 9) { - if (message) *message = @"You can add up to 9 prompt templates."; + if (message) *message = KoeLocalizedString(@"wizard.templates.limitMessage"); return NO; } @@ -2372,15 +2397,15 @@ - (BOOL)validateTemplatesDataWithMessage:(NSString **)message { NSNumber *shortcut = [tmpl[@"shortcut"] isKindOfClass:[NSNumber class]] ? tmpl[@"shortcut"] : nil; NSInteger value = shortcut.integerValue; if (!shortcut || value < 1 || value > 9) { - if (message) *message = @"Each prompt template needs a shortcut between 1 and 9."; + if (message) *message = KoeLocalizedString(@"wizard.templates.validation.shortcutRange"); return NO; } if ([used containsObject:@(value)]) { - if (message) *message = @"Each prompt template shortcut must be unique."; + if (message) *message = KoeLocalizedString(@"wizard.templates.validation.shortcutUnique"); return NO; } if ([self trimmedResolvedPromptTextForTemplate:tmpl].length == 0) { - if (message) *message = @"Each prompt template needs a non-empty prompt."; + if (message) *message = KoeLocalizedString(@"wizard.templates.validation.prompt"); return NO; } [used addObject:@(value)]; @@ -2391,14 +2416,14 @@ - (BOOL)validateTemplatesDataWithMessage:(NSString **)message { - (void)addTemplate:(id)sender { if (self.templatesData.count >= 9) { - [self showAlert:@"Template limit reached" - info:@"You can add up to 9 prompt templates because the overlay only supports number keys 1-9."]; + [self showAlert:KoeLocalizedString(@"wizard.templates.limitReached") + info:KoeLocalizedString(@"wizard.templates.limitMessage")]; return; } [self flushEditorToIndex:self.selectedTemplateIndex]; [self.templatesData addObject:[NSMutableDictionary dictionaryWithDictionary:@{ - @"name": @"New Template", + @"name": KoeLocalizedString(@"wizard.templates.newTemplate"), @"enabled": @YES, @"shortcut": @((NSInteger)self.templatesData.count + 1), @"system_prompt": @"", @@ -2467,7 +2492,7 @@ - (void)moveTemplateDown:(id)sender { - (NSView *)buildAboutPane { CGFloat paneWidth = 600; - CGFloat contentHeight = 308; + CGFloat contentHeight = 348; NSView *pane = [[NSView alloc] initWithFrame:NSMakeRect(0, 0, paneWidth, contentHeight)]; [self applySettingsPaneBackgroundToView:pane]; @@ -2491,14 +2516,14 @@ - (NSView *)buildAboutPane { y -= 32; // Description - NSTextField *desc = [self descriptionLabel:@"A background-first macOS voice input tool.\nPress a hotkey, speak, and the corrected text is pasted into whatever app you\u2019re using."]; + NSTextField *desc = [self descriptionLabel:KoeLocalizedString(@"wizard.about.description")]; desc.alignment = NSTextAlignmentCenter; desc.frame = NSMakeRect(60, y - 10, paneWidth - 120, 40); [pane addSubview:desc]; y -= 56; // GitHub button - NSButton *githubButton = [NSButton buttonWithTitle:@"GitHub Repository" target:self action:@selector(openGitHub:)]; + NSButton *githubButton = [NSButton buttonWithTitle:KoeLocalizedString(@"wizard.about.github") target:self action:@selector(openGitHub:)]; githubButton.bezelStyle = NSBezelStyleRounded; githubButton.image = [NSImage imageWithSystemSymbolName:@"arrow.up.right" accessibilityDescription:nil]; githubButton.imagePosition = NSImageTrailing; @@ -2507,7 +2532,7 @@ - (NSView *)buildAboutPane { y -= 40; // Documentation link - NSButton *docsButton = [NSButton buttonWithTitle:@"Documentation" target:self action:@selector(openDocs:)]; + NSButton *docsButton = [NSButton buttonWithTitle:KoeLocalizedString(@"wizard.about.docs") target:self action:@selector(openDocs:)]; docsButton.bezelStyle = NSBezelStyleRounded; docsButton.image = [NSImage imageWithSystemSymbolName:@"arrow.up.right" accessibilityDescription:nil]; docsButton.imagePosition = NSImageTrailing; @@ -2516,14 +2541,57 @@ - (NSView *)buildAboutPane { y -= 48; // License - NSTextField *license = [self descriptionLabel:@"MIT License \u00b7 Made with Rust + Objective-C"]; + NSTextField *license = [self descriptionLabel:KoeLocalizedString(@"wizard.about.license")]; license.alignment = NSTextAlignmentCenter; license.frame = NSMakeRect(24, y, paneWidth - 48, 20); [pane addSubview:license]; + y -= 40; + + // Language selector + NSTextField *langLabel = [NSTextField labelWithString:KoeLocalizedString(@"wizard.about.interfaceLanguage")]; + langLabel.font = [NSFont systemFontOfSize:13 weight:NSFontWeightMedium]; + langLabel.textColor = [NSColor secondaryLabelColor]; + langLabel.alignment = NSTextAlignmentCenter; + langLabel.frame = NSMakeRect(24, y, paneWidth - 48, 18); + [pane addSubview:langLabel]; + y -= 28; + + NSPopUpButton *langPopup = [[NSPopUpButton alloc] initWithFrame:NSMakeRect((paneWidth - 220) / 2.0, y, 220, 26) pullsDown:NO]; + [langPopup addItemWithTitle:KoeLocalizedString(@"wizard.about.language.system")]; + [langPopup itemAtIndex:0].representedObject = @"system"; + + for (NSString *code in [SPLocalization availableLanguages]) { + NSString *display = [SPLocalization displayNameForLanguage:code]; + NSMenuItem *item = [[NSMenuItem alloc] initWithTitle:display action:nil keyEquivalent:@""]; + item.representedObject = code; + [langPopup.menu addItem:item]; + } + + NSString *current = [SPLocalization isFollowingSystem] ? @"system" : [SPLocalization effectiveLanguage]; + for (NSInteger i = 0; i < langPopup.numberOfItems; i++) { + if ([[langPopup itemAtIndex:i].representedObject isEqualToString:current]) { + [langPopup selectItemAtIndex:i]; + break; + } + } + [langPopup setTarget:self]; + [langPopup setAction:@selector(interfaceLanguageChanged:)]; + [pane addSubview:langPopup]; return pane; } +- (void)interfaceLanguageChanged:(NSPopUpButton *)sender { + NSString *lang = sender.selectedItem.representedObject; + if ([lang isEqualToString:@"system"]) { + [SPLocalization setPreferredLanguage:nil]; + } else { + [SPLocalization setPreferredLanguage:lang]; + } + // setPreferredLanguage posts SPLocalizationLanguageDidChangeNotification, + // which interfaceLanguageDidChange: handles by rebuilding toolbar + pane. +} + - (void)openGitHub:(id)sender { [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:@"https://github.com/missuo/koe"]]; } @@ -2535,13 +2603,13 @@ - (void)openDocs:(id)sender { // ─── Shared button bar ────────────────────────────────────────────── - (void)addButtonsToPane:(NSView *)pane atY:(CGFloat)y width:(CGFloat)paneWidth { - NSButton *saveButton = [NSButton buttonWithTitle:@"Save" target:self action:@selector(saveConfig:)]; + NSButton *saveButton = [NSButton buttonWithTitle:KoeLocalizedString(@"wizard.button.save") target:self action:@selector(saveConfig:)]; saveButton.bezelStyle = NSBezelStyleRounded; saveButton.keyEquivalent = @"\r"; saveButton.frame = NSMakeRect(paneWidth - 32 - 80, y, 80, 28); [pane addSubview:saveButton]; - NSButton *cancelButton = [NSButton buttonWithTitle:@"Cancel" target:self action:@selector(cancelSetup:)]; + NSButton *cancelButton = [NSButton buttonWithTitle:KoeLocalizedString(@"wizard.button.cancel") target:self action:@selector(cancelSetup:)]; cancelButton.bezelStyle = NSBezelStyleRounded; cancelButton.keyEquivalent = @"\033"; cancelButton.frame = NSMakeRect(paneWidth - 32 - 80 - 88, y, 80, 28); @@ -2624,9 +2692,10 @@ - (NSPopUpButton *)overlayFontFamilyPopupControl { [popup removeAllItems]; - NSMenuItem *systemItem = [[NSMenuItem alloc] initWithTitle:kOverlayFontFamilySystemLabel action:nil keyEquivalent:@""]; + NSString *systemFontDisplay = KoeLocalizedString(@"wizard.overlay.fontSystemDefault"); + NSMenuItem *systemItem = [[NSMenuItem alloc] initWithTitle:systemFontDisplay action:nil keyEquivalent:@""]; systemItem.representedObject = kOverlayFontFamilyDefault; - systemItem.attributedTitle = [self overlayFontMenuTitleWithLabel:kOverlayFontFamilySystemLabel value:kOverlayFontFamilyDefault]; + systemItem.attributedTitle = [self overlayFontMenuTitleWithLabel:systemFontDisplay value:kOverlayFontFamilyDefault]; [popup.menu addItem:systemItem]; for (NSString *family in [self availableOverlayFontFamilies]) { @@ -3810,6 +3879,10 @@ - (void)loadValuesForPane:(NSString *)identifier { [self.triggerModePopup selectItemAtIndex:0]; } + // Load ESC cancel (default: enabled) + NSString *escapeCancel = configGet(@"hotkey.escape_cancel_enabled"); + self.escapeCancelCheckbox.state = [escapeCancel isEqualToString:@"false"] ? NSControlStateValueOff : NSControlStateValueOn; + NSString *startSound = configGet(@"feedback.start_sound"); NSString *stopSound = configGet(@"feedback.stop_sound"); NSString *errorSound = configGet(@"feedback.error_sound"); @@ -3984,6 +4057,10 @@ - (void)saveConfig:(id)sender { // Save trigger mode NSString *triggerModeValue = [self.triggerModePopup selectedItem].representedObject ?: @"hold"; saveOk &= configSet(@"hotkey.trigger_mode", triggerModeValue); + + // Save ESC cancel enabled + NSString *escapeCancelValue = self.escapeCancelCheckbox.state == NSControlStateValueOn ? @"true" : @"false"; + saveOk &= configSet(@"hotkey.escape_cancel_enabled", escapeCancelValue); } if (self.overlayFontSizeSlider) { NSString *fontFamily = [self selectedOverlayFontFamilyValue]; diff --git a/KoeApp/Koe/en.lproj/Localizable.strings b/KoeApp/Koe/en.lproj/Localizable.strings index b874a7d..a259ab1 100644 --- a/KoeApp/Koe/en.lproj/Localizable.strings +++ b/KoeApp/Koe/en.lproj/Localizable.strings @@ -89,6 +89,130 @@ "settings.language.restartMessage" = "The interface language has been changed. Some elements may require restarting Koe to update."; "settings.language.restartButton" = "OK"; +/* ─── Setup Wizard ─── */ +"wizard.window.title" = "Koe Settings"; +"wizard.toolbar.asr" = "ASR"; +"wizard.toolbar.llm" = "LLM"; +"wizard.toolbar.overlay" = "Overlay"; +"wizard.toolbar.controls" = "Controls"; +"wizard.toolbar.dictionary" = "Dictionary"; +"wizard.toolbar.prompt" = "Prompt"; +"wizard.toolbar.templates" = "Templates"; +"wizard.toolbar.about" = "About"; + +"wizard.button.save" = "Save"; +"wizard.button.cancel" = "Cancel"; + +"wizard.hotkey.cardTitle" = "Trigger"; +"wizard.hotkey.triggerShortcut" = "Trigger Shortcut"; +"wizard.hotkey.triggerMode" = "Trigger Mode"; +"wizard.hotkey.escapeCancel" = "Cancel with ESC"; +"wizard.hotkey.mode.hold" = "Hold (Press & Hold)"; +"wizard.hotkey.mode.toggle" = "Toggle (Tap to Start/Stop)"; +"wizard.hotkey.recordButton" = "Record"; +"wizard.hotkey.resetButton" = "Reset"; + +"wizard.feedback.cardTitle" = "Feedback Sounds"; +"wizard.feedback.startSound" = "Recording starts"; +"wizard.feedback.stopSound" = "Recording stops"; +"wizard.feedback.errorSound" = "Error occurs"; + +"wizard.overlay.description" = "Adjust the bottom live transcript overlay. Choose a system font, tune text size, set the bottom distance, and decide whether long live text stays capped to a few lines or expands fully. Every change is previewed directly in the real desktop overlay position."; +"wizard.overlay.styleControls" = "Style Controls"; +"wizard.overlay.cardTitle" = "Overlay"; +"wizard.overlay.font" = "Font"; +"wizard.overlay.fontSystemDefault" = "System Default"; +"wizard.overlay.textSize" = "Text Size"; +"wizard.overlay.distanceFromBottom" = "Distance from Bottom"; +"wizard.overlay.limitVisibleLines" = "Limit Visible Lines"; +"wizard.overlay.maxVisibleLines" = "Max Visible Lines"; +"wizard.overlay.defaults" = "Defaults"; +"wizard.overlay.resetButton" = "Reset to Default"; + +/* ─── ASR Pane ─── */ +"wizard.asr.description" = "Choose the ASR provider used for transcription."; +"wizard.asr.provider" = "Provider"; +"wizard.asr.provider.doubaoime" = "DoubaoIME (Built-in, Free)"; +"wizard.asr.provider.doubao" = "Doubao (ByteDance)"; +"wizard.asr.provider.qwen" = "Qwen (Alibaba Cloud)"; +"wizard.asr.provider.mlx" = "MLX (Apple Silicon)"; +"wizard.asr.provider.sherpa" = "Sherpa-ONNX"; +"wizard.asr.provider.appleSpeech" = "Apple Speech (On-Device)"; +"wizard.asr.connection" = "Connection"; +"wizard.asr.appKey" = "App Key"; +"wizard.asr.accessKey" = "Access Key"; +"wizard.asr.apiKey" = "API Key"; +"wizard.asr.appKeyDoubao" = "Volcengine App ID"; +"wizard.asr.accessKeyDoubao" = "Volcengine Access Token"; +"wizard.asr.apiKeyQwenPlaceholder" = "DashScope API Key (sk-xxx)"; +"wizard.asr.model" = "Model"; +"wizard.asr.language" = "Language"; +"wizard.asr.test" = "Test"; +"wizard.asr.download" = "Download"; +"wizard.asr.delete" = "Delete"; + +/* ─── LLM Pane ─── */ +"wizard.llm.description" = "Configure LLM for post-correction. When disabled, raw ASR output is used directly."; +"wizard.llm.correction" = "LLM Correction"; +"wizard.llm.profiles" = "Profiles"; +"wizard.llm.type" = "Type"; +"wizard.llm.name" = "Name"; +"wizard.llm.baseUrl" = "Base URL"; +"wizard.llm.apiKey" = "API Key"; +"wizard.llm.apiKeyPlaceholder" = "sk-... (leave empty if not required)"; +"wizard.llm.model" = "Model"; +"wizard.llm.modelList" = "Model List"; +"wizard.llm.chatPath" = "Chat Path"; +"wizard.llm.tokenParameter" = "Token Parameter"; +"wizard.llm.tokenParameter.hint" = "GPT-4o and older models use max_tokens. GPT-5 and reasoning models (o1/o3) use max_completion_tokens."; +"wizard.llm.testConnection" = "Test Connection"; +"wizard.llm.refresh" = "Refresh"; +"wizard.llm.choose" = "Choose"; +"wizard.llm.hideModels" = "Hide"; +"wizard.llm.noModels" = "No models loaded"; +"wizard.llm.add" = "Add"; +"wizard.llm.delete" = "Delete"; +"wizard.llm.download" = "Download"; +"wizard.llm.myProfile" = "My profile"; + +/* ─── Dictionary Pane ─── */ +"wizard.dict.title" = "Dictionary"; +"wizard.dict.description" = "User dictionary — one term per line. These terms are prioritized during LLM correction. Lines starting with # are comments."; + +/* ─── System Prompt Pane ─── */ +"wizard.prompt.title" = "System Prompt"; +"wizard.prompt.description" = "System prompt sent to the LLM for text correction. Edit to customize behavior."; + +/* ─── Templates Pane ─── */ +"wizard.templates.description" = "Manage overlay templates. Reorder them, control visibility, and edit each prompt here."; +"wizard.templates.showButtons" = "Show template buttons in overlay"; +"wizard.templates.library" = "Template Library"; +"wizard.templates.editor" = "Template Editor"; +"wizard.templates.newTemplate" = "New Template"; +"wizard.templates.untitled" = "Untitled"; +"wizard.templates.templateName" = "Template name"; +"wizard.templates.name" = "Name"; +"wizard.templates.prompt" = "Prompt"; +"wizard.templates.visible" = "Visible in overlay"; +"wizard.templates.addTooltip" = "Add template"; +"wizard.templates.removeTooltip" = "Remove selected template"; +"wizard.templates.moveUpTooltip" = "Move selected template up"; +"wizard.templates.moveDownTooltip" = "Move selected template down"; +"wizard.templates.limitReached" = "Template limit reached"; +"wizard.templates.limitMessage" = "You can add up to 9 prompt templates because the overlay only supports number keys 1-9."; +"wizard.templates.validation.shortcutRange" = "Each prompt template needs a shortcut between 1 and 9."; +"wizard.templates.validation.shortcutUnique" = "Each prompt template shortcut must be unique."; +"wizard.templates.validation.prompt" = "Each prompt template needs a non-empty prompt."; + +"wizard.about.github" = "GitHub Repository"; +"wizard.about.docs" = "Documentation"; +"wizard.about.license" = "MIT License · Made with Rust + Objective-C"; +"wizard.about.interfaceLanguage" = "Interface Language / 界面语言"; +"wizard.about.language.system" = "Follow System / 跟随系统"; +"wizard.about.language.english" = "English"; +"wizard.about.language.chinese" = "简体中文"; +"wizard.about.description" = "A background-first macOS voice input tool.\nPress a hotkey, speak, and the corrected text is pasted into whatever app you're using."; + /* ─── Edit Menu ─── */ "menu.edit" = "Edit"; "menu.edit.undo" = "Undo"; diff --git a/KoeApp/Koe/ja.lproj/Localizable.strings b/KoeApp/Koe/ja.lproj/Localizable.strings new file mode 100644 index 0000000..11e0faa --- /dev/null +++ b/KoeApp/Koe/ja.lproj/Localizable.strings @@ -0,0 +1,138 @@ +/* Koe — 日本語ローカライゼーション */ + +/* ─── ステータスバー ─── */ +"statusBar.status.ready" = "準備完了 — v%@ (%@)"; +"statusBar.status.listening" = "録音中..."; +"statusBar.status.connecting" = "接続中..."; +"statusBar.status.recognizing" = "認識中..."; +"statusBar.status.thinking" = "校正中..."; +"statusBar.status.pasting" = "貼り付け中..."; +"statusBar.status.error" = "エラー"; +"statusBar.status.working" = "処理中..."; + +"statusBar.shortcut.format" = "ショートカット:%@"; +"statusBar.menu.setupWizard" = "設定ウィザード..."; + +/* ─── 設定ウィザード ─── */ +"wizard.window.title" = "Koe 設定"; +"wizard.toolbar.asr" = "音声認識"; +"wizard.toolbar.llm" = "LLM"; +"wizard.toolbar.overlay" = "オーバーレイ"; +"wizard.toolbar.controls" = "操作"; +"wizard.toolbar.dictionary" = "辞書"; +"wizard.toolbar.prompt" = "プロンプト"; +"wizard.toolbar.templates" = "テンプレート"; +"wizard.toolbar.about" = "情報"; + +"wizard.button.save" = "保存"; +"wizard.button.cancel" = "キャンセル"; + +"wizard.hotkey.cardTitle" = "トリガーキー"; +"wizard.hotkey.triggerShortcut" = "トリガーショートカット"; +"wizard.hotkey.triggerMode" = "トリガーモード"; +"wizard.hotkey.escapeCancel" = "ESCでキャンセル"; +"wizard.hotkey.mode.hold" = "ホールド(押している間)"; +"wizard.hotkey.mode.toggle" = "トグル(タップで開始/停止)"; +"wizard.hotkey.recordButton" = "録画"; +"wizard.hotkey.resetButton" = "リセット"; + +"wizard.feedback.cardTitle" = "効果音"; +"wizard.feedback.startSound" = "録音開始時"; +"wizard.feedback.stopSound" = "録音停止時"; +"wizard.feedback.errorSound" = "エラー発生時"; + +"wizard.overlay.description" = "画面下部のライブ字幕オーバーレイを調整します。システムフォントの選択、サイズ調整、下部距離の設定、長文を数行に制限するかフル展開するかを設定できます。変更は実際のデスクトップ上で即座にプレビューされます。"; +"wizard.overlay.styleControls" = "スタイル設定"; +"wizard.overlay.cardTitle" = "オーバーレイ"; +"wizard.overlay.font" = "フォント"; +"wizard.overlay.fontSystemDefault" = "システムデフォルト"; +"wizard.overlay.textSize" = "文字サイズ"; +"wizard.overlay.distanceFromBottom" = "下端からの距離"; +"wizard.overlay.limitVisibleLines" = "表示行数を制限"; +"wizard.overlay.maxVisibleLines" = "最大行数"; +"wizard.overlay.defaults" = "デフォルト"; +"wizard.overlay.resetButton" = "デフォルトに戻す"; + +/* ─── 音声認識ペイン ─── */ +"wizard.asr.description" = "音声認識に使用するプロバイダを選択します。"; +"wizard.asr.provider" = "プロバイダ"; +"wizard.asr.provider.doubaoime" = "Doubao IME(内蔵・無料)"; +"wizard.asr.provider.doubao" = "Doubao(ByteDance)"; +"wizard.asr.provider.qwen" = "Qwen(阿里云)"; +"wizard.asr.provider.mlx" = "MLX(Apple Silicon)"; +"wizard.asr.provider.sherpa" = "Sherpa-ONNX"; +"wizard.asr.provider.appleSpeech" = "Apple Speech(オンデバイス)"; +"wizard.asr.connection" = "接続"; +"wizard.asr.appKey" = "App Key"; +"wizard.asr.accessKey" = "Access Key"; +"wizard.asr.apiKey" = "API Key"; +"wizard.asr.appKeyDoubao" = "Volcengine App ID"; +"wizard.asr.accessKeyDoubao" = "Volcengine Access Token"; +"wizard.asr.apiKeyQwenPlaceholder" = "DashScope API Key(sk-xxx)"; +"wizard.asr.model" = "モデル"; +"wizard.asr.language" = "言語"; +"wizard.asr.test" = "テスト"; +"wizard.asr.download" = "ダウンロード"; +"wizard.asr.delete" = "削除"; + +/* ─── LLMペイン ─── */ +"wizard.llm.description" = "テキスト校正用の LLM を設定します。無効化すると生の認識結果を直接使用します。"; +"wizard.llm.correction" = "LLM 校正"; +"wizard.llm.profiles" = "プロファイル"; +"wizard.llm.type" = "種類"; +"wizard.llm.name" = "名前"; +"wizard.llm.baseUrl" = "Base URL"; +"wizard.llm.apiKey" = "API Key"; +"wizard.llm.apiKeyPlaceholder" = "sk-...(不要な場合は空欄)"; +"wizard.llm.model" = "モデル"; +"wizard.llm.modelList" = "モデル一覧"; +"wizard.llm.chatPath" = "Chat パス"; +"wizard.llm.tokenParameter" = "Token パラメータ"; +"wizard.llm.tokenParameter.hint" = "GPT-4o 以前は max_tokens、GPT-5 や推論モデル(o1/o3)は max_completion_tokens を使用します。"; +"wizard.llm.testConnection" = "接続テスト"; +"wizard.llm.refresh" = "更新"; +"wizard.llm.choose" = "選択"; +"wizard.llm.hideModels" = "隠す"; +"wizard.llm.noModels" = "読み込まれたモデルがありません"; +"wizard.llm.add" = "追加"; +"wizard.llm.delete" = "削除"; +"wizard.llm.download" = "ダウンロード"; +"wizard.llm.myProfile" = "マイプロファイル"; + +/* ─── 辞書ペイン ─── */ +"wizard.dict.title" = "辞書"; +"wizard.dict.description" = "ユーザー辞書 — 1行に1つの用語。これらの用語は LLM 校正時に優先されます。# で始まる行はコメントです。"; + +/* ─── システムプロンプトペイン ─── */ +"wizard.prompt.title" = "システムプロンプト"; +"wizard.prompt.description" = "テキスト校正のために LLM に送るシステムプロンプト。編集して動作をカスタマイズできます。"; + +/* ─── テンプレートペイン ─── */ +"wizard.templates.description" = "オーバーレイテンプレートを管理。並べ替え、表示制御、プロンプト編集がここでできます。"; +"wizard.templates.showButtons" = "オーバーレイにテンプレートボタンを表示"; +"wizard.templates.library" = "テンプレートライブラリ"; +"wizard.templates.editor" = "テンプレートエディタ"; +"wizard.templates.newTemplate" = "新規テンプレート"; +"wizard.templates.untitled" = "無題"; +"wizard.templates.templateName" = "テンプレート名"; +"wizard.templates.name" = "名前"; +"wizard.templates.prompt" = "プロンプト"; +"wizard.templates.visible" = "オーバーレイに表示"; +"wizard.templates.addTooltip" = "テンプレートを追加"; +"wizard.templates.removeTooltip" = "選択したテンプレートを削除"; +"wizard.templates.moveUpTooltip" = "選択したテンプレートを上へ"; +"wizard.templates.moveDownTooltip" = "選択したテンプレートを下へ"; +"wizard.templates.limitReached" = "テンプレートの上限に達しました"; +"wizard.templates.limitMessage" = "オーバーレイは数字キー 1-9 のみ対応のため、テンプレートは最大 9 個までです。"; +"wizard.templates.validation.shortcutRange" = "各テンプレートのショートカットは 1 から 9 の範囲内にしてください。"; +"wizard.templates.validation.shortcutUnique" = "各テンプレートのショートカットは一意である必要があります。"; +"wizard.templates.validation.prompt" = "各テンプレートには空でないプロンプトが必要です。"; + +"wizard.about.github" = "GitHub リポジトリ"; +"wizard.about.docs" = "ドキュメント"; +"wizard.about.license" = "MIT ライセンス · Rust + Objective-C で作成"; +"wizard.about.interfaceLanguage" = "表示言語"; +"wizard.about.language.system" = "システムに従う"; +"wizard.about.language.english" = "English"; +"wizard.about.language.chinese" = "简体中文"; +"wizard.about.description" = "バックグラウンド優先の macOS 音声入力ツール。\nホットキーを押して話すと、校正されたテキストが現在のアプリに自動で貼り付けられます。"; diff --git a/KoeApp/Koe/zh-Hans.lproj/Localizable.strings b/KoeApp/Koe/zh-Hans.lproj/Localizable.strings index 629e66b..8405e4f 100644 --- a/KoeApp/Koe/zh-Hans.lproj/Localizable.strings +++ b/KoeApp/Koe/zh-Hans.lproj/Localizable.strings @@ -89,6 +89,130 @@ "settings.language.restartMessage" = "界面语言已更改。部分元素可能需要重启 Koe 才能更新。"; "settings.language.restartButton" = "好"; +/* ─── 设置向导 ─── */ +"wizard.window.title" = "Koe 设置"; +"wizard.toolbar.asr" = "语音识别"; +"wizard.toolbar.llm" = "大模型"; +"wizard.toolbar.overlay" = "悬浮窗"; +"wizard.toolbar.controls" = "控制"; +"wizard.toolbar.dictionary" = "词典"; +"wizard.toolbar.prompt" = "提示词"; +"wizard.toolbar.templates" = "模板"; +"wizard.toolbar.about" = "关于"; + +"wizard.button.save" = "保存"; +"wizard.button.cancel" = "取消"; + +"wizard.hotkey.cardTitle" = "触发键"; +"wizard.hotkey.triggerShortcut" = "触发快捷键"; +"wizard.hotkey.triggerMode" = "触发模式"; +"wizard.hotkey.escapeCancel" = "按 ESC 取消录音"; +"wizard.hotkey.mode.hold" = "按住(长按录音)"; +"wizard.hotkey.mode.toggle" = "切换(按一下开始/停止)"; +"wizard.hotkey.recordButton" = "录制"; +"wizard.hotkey.resetButton" = "重置"; + +"wizard.feedback.cardTitle" = "反馈音效"; +"wizard.feedback.startSound" = "开始录音时"; +"wizard.feedback.stopSound" = "停止录音时"; +"wizard.feedback.errorSound" = "出错时"; + +"wizard.overlay.description" = "调整底部实时转写悬浮窗。选择系统字体、调整字号、设置离底部的距离,并决定长文本是否限制在几行内或完整展开。每次修改都会在桌面实际位置实时预览。"; +"wizard.overlay.styleControls" = "样式控制"; +"wizard.overlay.cardTitle" = "悬浮窗"; +"wizard.overlay.font" = "字体"; +"wizard.overlay.fontSystemDefault" = "系统默认"; +"wizard.overlay.textSize" = "字号"; +"wizard.overlay.distanceFromBottom" = "距底部距离"; +"wizard.overlay.limitVisibleLines" = "限制可见行数"; +"wizard.overlay.maxVisibleLines" = "最大行数"; +"wizard.overlay.defaults" = "恢复默认"; +"wizard.overlay.resetButton" = "重置为默认值"; + +/* ─── 语音识别页 ─── */ +"wizard.asr.description" = "选择用于转写的语音识别服务。"; +"wizard.asr.provider" = "服务商"; +"wizard.asr.provider.doubaoime" = "豆包输入法(内置、免费)"; +"wizard.asr.provider.doubao" = "豆包(字节跳动)"; +"wizard.asr.provider.qwen" = "通义(阿里云)"; +"wizard.asr.provider.mlx" = "MLX(Apple Silicon)"; +"wizard.asr.provider.sherpa" = "Sherpa-ONNX"; +"wizard.asr.provider.appleSpeech" = "Apple 语音(本地)"; +"wizard.asr.connection" = "连接"; +"wizard.asr.appKey" = "App Key"; +"wizard.asr.accessKey" = "Access Key"; +"wizard.asr.apiKey" = "API Key"; +"wizard.asr.appKeyDoubao" = "火山引擎 App ID"; +"wizard.asr.accessKeyDoubao" = "火山引擎 Access Token"; +"wizard.asr.apiKeyQwenPlaceholder" = "DashScope API Key(sk-xxx)"; +"wizard.asr.model" = "模型"; +"wizard.asr.language" = "语言"; +"wizard.asr.test" = "测试"; +"wizard.asr.download" = "下载"; +"wizard.asr.delete" = "删除"; + +/* ─── 大模型页 ─── */ +"wizard.llm.description" = "配置用于校正的 LLM。禁用后将直接使用原始 ASR 输出。"; +"wizard.llm.correction" = "LLM 校正"; +"wizard.llm.profiles" = "配置档案"; +"wizard.llm.type" = "类型"; +"wizard.llm.name" = "名称"; +"wizard.llm.baseUrl" = "Base URL"; +"wizard.llm.apiKey" = "API Key"; +"wizard.llm.apiKeyPlaceholder" = "sk-...(如不需要可留空)"; +"wizard.llm.model" = "模型"; +"wizard.llm.modelList" = "模型列表"; +"wizard.llm.chatPath" = "Chat 路径"; +"wizard.llm.tokenParameter" = "Token 参数"; +"wizard.llm.tokenParameter.hint" = "GPT-4o 及更早的模型使用 max_tokens。GPT-5 和推理类模型(o1/o3)使用 max_completion_tokens。"; +"wizard.llm.testConnection" = "测试连接"; +"wizard.llm.refresh" = "刷新"; +"wizard.llm.choose" = "选择"; +"wizard.llm.hideModels" = "隐藏"; +"wizard.llm.noModels" = "没有已加载的模型"; +"wizard.llm.add" = "添加"; +"wizard.llm.delete" = "删除"; +"wizard.llm.download" = "下载"; +"wizard.llm.myProfile" = "我的配置"; + +/* ─── 词典页 ─── */ +"wizard.dict.title" = "词典"; +"wizard.dict.description" = "用户词典——每行一个词条。这些词条在 LLM 校正时会被优先考虑。以 # 开头的行为注释。"; + +/* ─── 系统提示词页 ─── */ +"wizard.prompt.title" = "系统提示词"; +"wizard.prompt.description" = "发送给 LLM 用于文本校正的系统提示词。可自行编辑以调整行为。"; + +/* ─── 模板页 ─── */ +"wizard.templates.description" = "管理悬浮窗模板。在这里调整顺序、控制可见性、编辑每个提示词。"; +"wizard.templates.showButtons" = "在悬浮窗中显示模板按钮"; +"wizard.templates.library" = "模板库"; +"wizard.templates.editor" = "模板编辑器"; +"wizard.templates.newTemplate" = "新建模板"; +"wizard.templates.untitled" = "未命名"; +"wizard.templates.templateName" = "模板名称"; +"wizard.templates.name" = "名称"; +"wizard.templates.prompt" = "提示词"; +"wizard.templates.visible" = "在悬浮窗显示"; +"wizard.templates.addTooltip" = "添加模板"; +"wizard.templates.removeTooltip" = "删除所选模板"; +"wizard.templates.moveUpTooltip" = "上移所选模板"; +"wizard.templates.moveDownTooltip" = "下移所选模板"; +"wizard.templates.limitReached" = "已达模板上限"; +"wizard.templates.limitMessage" = "最多只能添加 9 个提示词模板,因为悬浮窗只支持数字键 1-9。"; +"wizard.templates.validation.shortcutRange" = "每个模板的快捷键必须在 1 到 9 之间。"; +"wizard.templates.validation.shortcutUnique" = "每个模板的快捷键必须唯一。"; +"wizard.templates.validation.prompt" = "每个模板都需要一个非空的提示词。"; + +"wizard.about.github" = "GitHub 仓库"; +"wizard.about.docs" = "使用文档"; +"wizard.about.license" = "MIT 许可 · 基于 Rust + Objective-C"; +"wizard.about.interfaceLanguage" = "界面语言"; +"wizard.about.language.system" = "跟随系统"; +"wizard.about.language.english" = "English"; +"wizard.about.language.chinese" = "简体中文"; +"wizard.about.description" = "后台运行的 macOS 语音输入工具。\n按下热键说话,校正后的文字会自动粘贴到当前应用。"; + /* ─── 编辑菜单 ─── */ "menu.edit" = "编辑"; "menu.edit.undo" = "撤销"; diff --git a/KoeApp/Koe/zh-Hant.lproj/Localizable.strings b/KoeApp/Koe/zh-Hant.lproj/Localizable.strings new file mode 100644 index 0000000..4863e50 --- /dev/null +++ b/KoeApp/Koe/zh-Hant.lproj/Localizable.strings @@ -0,0 +1,138 @@ +/* Koe — 繁體中文在地化 */ + +/* ─── 狀態列 ─── */ +"statusBar.status.ready" = "就緒 — v%@ (%@)"; +"statusBar.status.listening" = "正在錄音..."; +"statusBar.status.connecting" = "正在連線..."; +"statusBar.status.recognizing" = "正在辨識..."; +"statusBar.status.thinking" = "正在校正..."; +"statusBar.status.pasting" = "正在貼上..."; +"statusBar.status.error" = "發生錯誤"; +"statusBar.status.working" = "處理中..."; + +"statusBar.shortcut.format" = "快捷鍵:%@"; +"statusBar.menu.setupWizard" = "設定精靈..."; + +/* ─── 設定精靈 ─── */ +"wizard.window.title" = "Koe 設定"; +"wizard.toolbar.asr" = "語音辨識"; +"wizard.toolbar.llm" = "大型模型"; +"wizard.toolbar.overlay" = "浮動視窗"; +"wizard.toolbar.controls" = "控制"; +"wizard.toolbar.dictionary" = "詞典"; +"wizard.toolbar.prompt" = "提示詞"; +"wizard.toolbar.templates" = "模板"; +"wizard.toolbar.about" = "關於"; + +"wizard.button.save" = "儲存"; +"wizard.button.cancel" = "取消"; + +"wizard.hotkey.cardTitle" = "觸發鍵"; +"wizard.hotkey.triggerShortcut" = "觸發快捷鍵"; +"wizard.hotkey.triggerMode" = "觸發模式"; +"wizard.hotkey.escapeCancel" = "按 ESC 取消錄音"; +"wizard.hotkey.mode.hold" = "按住(長按錄音)"; +"wizard.hotkey.mode.toggle" = "切換(按一下開始/停止)"; +"wizard.hotkey.recordButton" = "錄製"; +"wizard.hotkey.resetButton" = "重設"; + +"wizard.feedback.cardTitle" = "回饋音效"; +"wizard.feedback.startSound" = "開始錄音時"; +"wizard.feedback.stopSound" = "停止錄音時"; +"wizard.feedback.errorSound" = "發生錯誤時"; + +"wizard.overlay.description" = "調整底部的即時字幕浮動視窗。選擇系統字型、調整字體大小、設定距離底部的距離,並決定長文字是否限制在幾行或完全展開。每次變更都會在實際桌面位置即時預覽。"; +"wizard.overlay.styleControls" = "樣式控制"; +"wizard.overlay.cardTitle" = "浮動視窗"; +"wizard.overlay.font" = "字型"; +"wizard.overlay.fontSystemDefault" = "系統預設"; +"wizard.overlay.textSize" = "字體大小"; +"wizard.overlay.distanceFromBottom" = "距底部距離"; +"wizard.overlay.limitVisibleLines" = "限制可見行數"; +"wizard.overlay.maxVisibleLines" = "最大行數"; +"wizard.overlay.defaults" = "恢復預設"; +"wizard.overlay.resetButton" = "重設為預設值"; + +/* ─── 語音辨識頁 ─── */ +"wizard.asr.description" = "選擇用於轉寫的語音辨識服務。"; +"wizard.asr.provider" = "服務商"; +"wizard.asr.provider.doubaoime" = "豆包輸入法(內建、免費)"; +"wizard.asr.provider.doubao" = "豆包(字節跳動)"; +"wizard.asr.provider.qwen" = "通義(阿里雲)"; +"wizard.asr.provider.mlx" = "MLX(Apple Silicon)"; +"wizard.asr.provider.sherpa" = "Sherpa-ONNX"; +"wizard.asr.provider.appleSpeech" = "Apple 語音(本機)"; +"wizard.asr.connection" = "連線"; +"wizard.asr.appKey" = "App Key"; +"wizard.asr.accessKey" = "Access Key"; +"wizard.asr.apiKey" = "API Key"; +"wizard.asr.appKeyDoubao" = "火山引擎 App ID"; +"wizard.asr.accessKeyDoubao" = "火山引擎 Access Token"; +"wizard.asr.apiKeyQwenPlaceholder" = "DashScope API Key(sk-xxx)"; +"wizard.asr.model" = "模型"; +"wizard.asr.language" = "語言"; +"wizard.asr.test" = "測試"; +"wizard.asr.download" = "下載"; +"wizard.asr.delete" = "刪除"; + +/* ─── 大型模型頁 ─── */ +"wizard.llm.description" = "設定用於校正的 LLM。停用後將直接使用原始語音辨識結果。"; +"wizard.llm.correction" = "LLM 校正"; +"wizard.llm.profiles" = "設定檔"; +"wizard.llm.type" = "類型"; +"wizard.llm.name" = "名稱"; +"wizard.llm.baseUrl" = "Base URL"; +"wizard.llm.apiKey" = "API Key"; +"wizard.llm.apiKeyPlaceholder" = "sk-...(若不需要可留空)"; +"wizard.llm.model" = "模型"; +"wizard.llm.modelList" = "模型清單"; +"wizard.llm.chatPath" = "Chat 路徑"; +"wizard.llm.tokenParameter" = "Token 參數"; +"wizard.llm.tokenParameter.hint" = "GPT-4o 與更早的模型使用 max_tokens。GPT-5 及推理類模型(o1/o3)使用 max_completion_tokens。"; +"wizard.llm.testConnection" = "測試連線"; +"wizard.llm.refresh" = "重新整理"; +"wizard.llm.choose" = "選擇"; +"wizard.llm.hideModels" = "隱藏"; +"wizard.llm.noModels" = "沒有已載入的模型"; +"wizard.llm.add" = "新增"; +"wizard.llm.delete" = "刪除"; +"wizard.llm.download" = "下載"; +"wizard.llm.myProfile" = "我的設定"; + +/* ─── 詞典頁 ─── */ +"wizard.dict.title" = "詞典"; +"wizard.dict.description" = "使用者詞典——每行一個詞條。這些詞條在 LLM 校正時會被優先考慮。以 # 開頭的行為註解。"; + +/* ─── 系統提示詞頁 ─── */ +"wizard.prompt.title" = "系統提示詞"; +"wizard.prompt.description" = "傳送給 LLM 用於文字校正的系統提示詞。可自行編輯以調整行為。"; + +/* ─── 範本頁 ─── */ +"wizard.templates.description" = "管理浮動視窗範本。在此調整順序、控制顯示、編輯每個提示詞。"; +"wizard.templates.showButtons" = "在浮動視窗中顯示範本按鈕"; +"wizard.templates.library" = "範本庫"; +"wizard.templates.editor" = "範本編輯器"; +"wizard.templates.newTemplate" = "新增範本"; +"wizard.templates.untitled" = "未命名"; +"wizard.templates.templateName" = "範本名稱"; +"wizard.templates.name" = "名稱"; +"wizard.templates.prompt" = "提示詞"; +"wizard.templates.visible" = "在浮動視窗顯示"; +"wizard.templates.addTooltip" = "新增範本"; +"wizard.templates.removeTooltip" = "刪除所選範本"; +"wizard.templates.moveUpTooltip" = "上移所選範本"; +"wizard.templates.moveDownTooltip" = "下移所選範本"; +"wizard.templates.limitReached" = "已達範本上限"; +"wizard.templates.limitMessage" = "浮動視窗僅支援數字鍵 1-9,因此最多只能新增 9 個範本。"; +"wizard.templates.validation.shortcutRange" = "每個範本的快捷鍵必須在 1 到 9 之間。"; +"wizard.templates.validation.shortcutUnique" = "每個範本的快捷鍵必須唯一。"; +"wizard.templates.validation.prompt" = "每個範本都需要一個非空的提示詞。"; + +"wizard.about.github" = "GitHub 儲存庫"; +"wizard.about.docs" = "使用文件"; +"wizard.about.license" = "MIT 授權 · 使用 Rust + Objective-C 打造"; +"wizard.about.interfaceLanguage" = "介面語言"; +"wizard.about.language.system" = "跟隨系統"; +"wizard.about.language.english" = "English"; +"wizard.about.language.chinese" = "简体中文"; +"wizard.about.description" = "後台優先的 macOS 語音輸入工具。\n按下熱鍵開口說話,校正後的文字會自動貼到目前使用的應用中。"; diff --git a/KoeApp/project.yml b/KoeApp/project.yml index 8f73c42..b74847f 100644 --- a/KoeApp/project.yml +++ b/KoeApp/project.yml @@ -17,6 +17,8 @@ settings: MACOSX_DEPLOYMENT_TARGET: "14.0" ENABLE_HARDENED_RUNTIME: YES APP_UPDATE_FEED_URL: "https://raw.githubusercontent.com/missuo/koe/main/docs/update-feed.json" + CODE_SIGN_STYLE: Manual + CODE_SIGN_IDENTITY: "Koe Dev" targetTemplates: KoeApp: diff --git a/koe-core/src/config.rs b/koe-core/src/config.rs index 15e4527..1dc7643 100644 --- a/koe-core/src/config.rs +++ b/koe-core/src/config.rs @@ -469,6 +469,11 @@ pub struct HotkeySection { /// Trigger mode: "hold" (press-and-hold, default) or "toggle" (tap to start/stop). #[serde(default = "default_trigger_mode")] pub trigger_mode: String, + + /// When enabled, pressing ESC during an active recording session cancels it. + /// Default: true + #[serde(default = "default_true")] + pub escape_cancel_enabled: bool, } #[derive(Debug, Clone, Copy, PartialEq, Eq)]