macvim icon indicating copy to clipboard operation
macvim copied to clipboard

Context menu support for translate using macOS

Open hardkrash opened this issue 8 months ago • 15 comments

I would like to enable translation of words in the text window using the native Mac translation engine.

Using the built in Mac OS translation requires no third party services and keeps my text on my system

There are various vim translation plugins but they call out to external translation services on the internet. I prefer to use the offline capable translation in macOS

hardkrash avatar Apr 25 '25 20:04 hardkrash

Interestingly I was just playing around with this recently and got a prototype working:

Image

The big caveat is that the native macOS Translation API (introduced in macOS 15 Sequoia) is, frankly, terrible. It requires SwiftUI (which MacVim does not use) and is kind of a pain to use. The prototype above was instead done using private APIs similar to how native apps like Safari work, but I need to think more about the proper way to integrate this and whether I want to do this (using private APIs is not the end of the world but something I prefer to limit the usage of).

But I agree. Being able to leverage the system built-in translation service is nice. Apple just for some reason made it difficult to properly access it. For example, the new Apple Intelligence Writing Tools "just works" on any user texts and doesn't require much integration at all. I'm not sure why they made this so much more complicated.

ychin avatar Apr 26 '25 18:04 ychin

Safari (macOS & iOS Dev Beta 2) now ships OPFS. This impacts webapps that depend on origin-private persistent files. Note: Apple’s native Translation API currently expects SwiftUI (some apps use private APIs as a workaround). Also calling out the new opfs Rust crate which includes Safari WASM workarounds, a tokio native fallback, and an in-memory test FS — useful for anyone building Dioxus/WASM webapps. Recommend pinning and adding labels: platform-support, safari, opfs, rust.

Jatkingmodern avatar Sep 14 '25 09:09 Jatkingmodern

@Jatkingmodern MacVim doesn't use WASM, Safari nor OFPS.

Could you please explain how your comment fit the issue?

eirnym avatar Sep 15 '25 03:09 eirnym

Recommended approaches (ranked)

Recommended — In-process Swift bridge + SwiftUI host

Add a tiny Swift module to the MacVim Xcode target that:

Imports Translation and SwiftUI.

Hosts a lightweight SwiftUI view (hidden/offscreen or ephemeral window) that attaches a translationTask / TranslationSession.Configuration to get a TranslationSession and translate requested strings.

Exposes Objective-C compatible wrapper methods (@objc + NSObject subclass) that MacVim can call. Results are returned via completion handlers.

Why: uses official API, keeps translations local/on-device, avoids private APIs, and is easy to call from Obj-C via the -Swift.h header. Apple Developer +1

Alternative — Helper XPC / helper process in Swift

Build a small helper (XPC or commandline helper) written in Swift that runs the Translation code and exposes a small IPC API (XPC, socket, or stdio JSON).

MacVim calls the helper to translate text. This keeps MacVim free of Swift runtime complexity and is easier to sandbox independently.

Why: fewer build changes to MacVim; useful if you don’t want to link Swift into the main process.

Not recommended for production — private APIs

You can prototype with private APIs (some folks do), but don’t ship that. Apple can reject apps that depend on private system frameworks and it’s brittle.

Concrete implementation (Recommended approach)

Below is a practical recipe plus code snippets you can paste into the MacVim project. I give a Swift side (translation host + Objective-C bridge) and an example Obj-C usage.

Notes before copy/paste

The Translation framework and the .translationTask / TranslationSession patterns are Apple’s documented path; examples and details are in the WWDC talk and Apple docs. You must test on a macOS 15+ machine (Sequoia) as the simulator or older macOS may not support on-device models. Apple Developer +1

I use NSHostingController to host the SwiftUI view inside AppKit so we can get a TranslationSession. NSHostingController is the standard way to embed SwiftUI into AppKit. Apple Developer

  1. Swift: small translation host + Obj-C bridge

Create a new Swift file in the MacVim Xcode target (e.g. MacVimTranslator.swift).

// MacVimTranslator.swift import Foundation import SwiftUI import Translation // Apple Translation framework (macOS 15+)

@objc public class MacVimTranslator : NSObject { @objc public static let shared = MacVimTranslator() private var hostingWindow: NSWindow? // ephemeral hosting window if needed

// Public API callable from Obj-C:
// - text: the text to translate
// - targetLocale: BCP47 (like "en", "fr", "ja-JP")
// - completion: returns translated string or error
@objc public func translate(_ text: String,
                            targetLocale: String,
                            completion: @escaping (NSString?, NSError?) -> Void) {
    if #available(macOS 15.0, *) {
        // Create a small hidden host controller that runs the SwiftUI view
        DispatchQueue.main.async {
            // Create the SwiftUI host view which will run translationTask and call back
            let host = TranslationHostViewWrapper(texts: [text],
                                                  targetLocaleIdentifier: targetLocale) { translated, err in
                if let t = translated {
                    completion(t as NSString, nil)
                } else {
                    let e = err ?? NSError(domain: "MacVimTranslator", code: 1, userInfo: [NSLocalizedDescriptionKey: "Unknown error"])
                    completion(nil, e)
                }
            }

            // Present it invisibly (offscreen window) — some translation flows may require a window.
            let hostingController = NSHostingController(rootView: host)
            let w = NSWindow(contentViewController: hostingController)
            w.setContentSize(NSSize(width: 10, height: 10))
            w.styleMask = []
            w.isReleasedWhenClosed = false
            w.orderOut(nil)       // keep it offscreen/invisible
            self.hostingWindow = w
            // Keep the hosting window around briefly to let translation complete.
            // The wrapper will call completion and we can tear the window down afterwards.
        }
    } else {
        let err = NSError(domain: "MacVimTranslator", code: -1, userInfo: [NSLocalizedDescriptionKey: "macOS 15+ is required"])
        completion(nil, err)
    }
}

}

Now add the SwiftUI host view (TranslationHostView.swift):

// TranslationHostView.swift import SwiftUI import Translation

@available(macOS 15.0, *) struct TranslationHostViewWrapper: View { let texts: [String] let targetLocaleIdentifier: String let onComplete: (String?, Error?) -> Void

@State private var config: TranslationSession.Configuration? = nil

var body: some View {
    // A transparent view — it must exist so we can attach the .translationTask
    Color.clear
        .translationTask(configuration: config) { session in
            // This closure runs when the session is available.
            Task {
                do {
                    let locale = Locale(identifier: targetLocaleIdentifier)
                    // Create a new configuration — detect source language automatically
                    let cfg = TranslationSession.Configuration(source: nil, target: locale)
                    // Set config so that translationTask is triggered
                    DispatchQueue.main.async {
                        self.config = cfg
                    }

                    // Translate each input string (batch). The API returns async sequence results.
                    for input in texts {
                        // The actual method is `translations(from:)` on the session.
                        for try await result in session.translations(from: [input]) {
                            // `result` is a TranslationResult; use result.translatedText
                            // Convert to String and return via completion
                            DispatchQueue.main.async {
                                self.onComplete(result.translatedText, nil)
                            }
                        }
                    }
                } catch {
                    DispatchQueue.main.async {
                        self.onComplete(nil, error)
                    }
                }
            }
        }
        .onAppear {
            // Kick the config once view appears
            self.config = TranslationSession.Configuration(source: nil, target: Locale(identifier: targetLocaleIdentifier))
        }
}

}

Notes:

The translationTask(configuration:action:) pattern and TranslationSession translations(from:) async sequence are the idiomatic way Apple demonstrates in WWDC and docs. This approach gets you a TranslationSession without forcing you to rewrite MacVim into SwiftUI. Apple Developer +1

The wrapper presents an invisible NSWindow with an NSHostingController so SwiftUI can attach the translationTask. In many cases the framework needs some UI context; hosting a tiny offscreen or zero-size window is a simple technique. (You can also present the SwiftUI view as a popover attached to a MacVim view if you prefer visible UI.) Apple Developer +1

  1. Objective-C side: call the Swift bridge from MacVim

After adding the Swift files to the MacVim target, Xcode will generate the MacVim-Swift.h header. In your Objective-C/ObjC++ file:

// MyMacVimController.m (or wherever you want to call translation) #import "MacVim-Swift.h" // Xcode-generated header

// Call translator: [MacVimTranslator.shared translate:@"テキストを翻訳" targetLocale:@"en" completion:^(NSString * _Nullable translated, NSError * _Nullable err) { if (translated) { dispatch_async(dispatch_get_main_queue(), ^{ // Use translated NSString in MacVim UI / status bar / popup NSLog(@"Translated: %@", translated); // e.g. show a small panel, replace selection, etc. }); } else { NSLog(@"Translation error: %@", err); } }];

Extra notes, pitfalls & testing

macOS version: Translation framework requires macOS 15+ (Sequoia). Fallbacks needed for older macOS builds. Apple Developer

Simulator vs real machine: model downloads or on-device features sometimes don’t work reliably in simulator; test on a real macOS Sequoia device. Several writeups warn of simulator quirks. mszpro.com

UI requirement: the TranslationSession APIs and built-in presentation are SwiftUI-centric; creating a tiny NSHostingController and a hidden window is a pragmatic trick to acquire a session and perform translations while keeping MacVim AppKit UI intact. Many Apple docs show embedding SwiftUI with NSHostingController. Apple Developer +1

Sandbox/entitlements: if MacVim is sandboxed (or you plan to ship on Mac App Store), ensure your entitlements and packaging allow Swift runtime and the Translation framework. (If you’re shipping outside App Store, less friction.)

Alternative (XPC): If you prefer to avoid linking Swift into MacVim, build a small helper tool (SwiftXPC) that performs translation and returns results via XPC—this is robust and isolates Swift runtime. Good when you want minimal changes to the main process

Jatkingmodern avatar Sep 15 '25 10:09 Jatkingmodern

@Jatkingmodern While it's possible and seems to be the only way, please consider following details:

  • MacVim is already is Vim core + ObjectiveC GUI.
  • modern MacVim is built not only for the latest macOS versions, but for quite old as well (please read Info.plist for compatibility info)
  • using MacPosts MacVim is built for even older versions. Without major patches.
  • Swift language support is quite limited for target macOS and API differs from version to version
  • I know there's ifs with macOS compatibility and calling various APIs but I'm not sure (I haven't disassembled it) if these statements will be preserved in Runtime.
  • MacVim is often run as a privileged user.
  • XPC is quite easy to implement in an insecure way. @ychin has detailed informations how it could be

eirnym avatar Sep 15 '25 11:09 eirnym

High-level design (why this is best)

No Swift runtime in the main binary. You avoid ABI/SDK problems, old deployment targets, and weird runtime behavior inside the Vim core process.

Helper binary isolates all macOS 15+ dependencies. You can build this helper with a modern toolchain and a macOS 15+ deployment target. MacVim only calls it when running on an eligible OS.

Simple IPC (stdin/stdout JSON). Easy to implement cross-language, robust, debuggable, and firewall-free.

Privilege safety. Ensure the helper executes as the user. If MacVim is started privileged, explicitly drop privileges in the helper (or refuse to run) to avoid security risk.

Graceful fallback. When helper is unavailable or OS < 15, MacVim can show a message: “Translation unavailable — upgrade macOS or enable cloud translation.”

Implementation steps

Add version check in MacVim (Objective-C). If major >= 15, try to run helper. Otherwise fallback.

Spawn helper with NSTask, pass a JSON request (text + target locale) to stdin, read JSON response from stdout.

Helper (Swift): read JSON from stdin, use Translation.framework, write JSON result to stdout, exit.

Security: ensure helper runs as non-root. If it detects elevated privileges (UID 0), it should refuse to run or drop to a safe unprivileged UID.

Packaging: include helper binary in the MacVim app bundle Contents/Helpers/TranslationHelper. Sign it with your code signing identity, update the installer/package.

Obj-C: runtime check + spawn NSTask (robust, synchronous example) // TranslationClient.m (ObjC helper caller)

#import <Foundation/Foundation.h> #include <sys/types.h> #include <unistd.h>

static BOOL macOSAtLeastMajor(NSInteger majorVersion) { NSOperatingSystemVersion v = [[NSProcessInfo processInfo] operatingSystemVersion]; return v.majorVersion >= majorVersion; }

// Call helper and get translated text (synchronous for simplicity) NSString * runTranslationHelperSync(NSString *inputText, NSString *targetLocale, NSError **outErr) { if (!macOSAtLeastMajor(15)) { if (outErr) *outErr = [NSError errorWithDomain:@"Translation" code:2 userInfo:@{NSLocalizedDescriptionKey: @"macOS 15+ required"}]; return nil; }

NSString *helperPath = [[NSBundle mainBundle] pathForResource:@"TranslationHelper" ofType:@"" inDirectory:@"Contents/Helpers"];
if (!helperPath) {
    if (outErr) *outErr = [NSError errorWithDomain:@"Translation" code:3 userInfo:@{NSLocalizedDescriptionKey: @"Translation helper not found"}];
    return nil;
}

// Build JSON request
NSDictionary *req = @{@"text": inputText, @"target": targetLocale};
NSData *reqData = [NSJSONSerialization dataWithJSONObject:req options:0 error:outErr];
if (!reqData) return nil;

NSTask *task = [[NSTask alloc] init];
task.launchPath = helperPath;
task.arguments = @[]; // no args; use stdin JSON
NSPipe *inPipe = [NSPipe pipe];
NSPipe *outPipe = [NSPipe pipe];
task.standardInput = inPipe;
task.standardOutput = outPipe;
task.standardError = [NSPipe pipe];

// Security: unset dangerous env or sanitize PATH if needed
NSMutableDictionary *env = [[[NSProcessInfo processInfo] environment] mutableCopy];
// Optionally restrict env or set DYLD variables to avoid loading untrusted libs.
task.environment = env;

@try {
    [task launch];
} @catch (NSException *ex) {
    if (outErr) *outErr = [NSError errorWithDomain:@"Translation" code:4 userInfo:@{NSLocalizedDescriptionKey: @"Failed to launch helper"}];
    return nil;
}

// Write request
[[inPipe fileHandleForWriting] writeData:reqData];
[[inPipe fileHandleForWriting] closeFile];

// Read response (block until EOF)
NSData *respData = [[outPipe fileHandleForReading] readDataToEndOfFile];
[task waitUntilExit];
if (task.terminationStatus != 0) {
    if (outErr) *outErr = [NSError errorWithDomain:@"Translation" code:5 userInfo:@{NSLocalizedDescriptionKey: @"Translation helper failed"}];
    return nil;
}

NSDictionary *resp = [NSJSONSerialization JSONObjectWithData:respData options:0 error:outErr];
if (!resp) return nil;

NSString *translated = resp[@"translated"];
if (![translated isKindOfClass:[NSString class]]) {
    if (outErr) *outErr = [NSError errorWithDomain:@"Translation" code:6 userInfo:@{NSLocalizedDescriptionKey: @"Malformed helper response"}];
    return nil;
}

return translated;

}

Notes:

This is synchronous for clarity; in UI code you’d dispatch the call to a background queue and update UI on the main queue.

Use secure environment sanitization if required (clear DYLD_* env vars when launching untrusted code).

Swift helper skeleton (macOS 15+, uses Translation.framework)

Create TranslationHelper.swift and compile it into a standalone command line tool (macOS 15+).

// TranslationHelper.swift // Build target macOS 15+, swift tool import Foundation import Translation

struct Request: Codable { let text: String let target: String }

struct Response: Codable { let translated: String? let error: String? }

func readAllStdin() -> Data? { return FileHandle.standardInput.readDataToEndOfFile() }

func main() { // Security: refuse to run as root if getuid() == 0 { let resp = Response(translated: nil, error: "Refusing to run as root") if let d = try? JSONEncoder().encode(resp) { FileHandle.standardOutput.write(d) } exit(1) }

guard let inputData = readAllStdin(),
      let req = try? JSONDecoder().decode(Request.self, from: inputData) else {
    let resp = Response(translated: nil, error: "Bad request")
    if let d = try? JSONEncoder().encode(resp) {
        FileHandle.standardOutput.write(d)
    }
    exit(2)
}

if #available(macOS 15.0, *) {
    let sem = DispatchSemaphore(value: 0)
    var outText: String? = nil
    var outErr: String? = nil

    // Use a Task to call async Translation APIs
    Task {
        do {
            let targetLocale = Locale(identifier: req.target)
            // Create configuration and session via translationTask API
            // We'll use an ephemeral in-memory session approach
            // Note: Real code must follow session lifecycle patterns from docs
            let cfg = TranslationSession.Configuration(source: nil, target: targetLocale)
            // Start a session — example pattern:
            try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in
                // Use a minimal SwiftUI trick is not available here; but many TranslationSession APIs can be started directly.
                // If a UI context is required, consider using NSApplicationMain in helper or use session provider APIs.
                continuation.resume(returning: ())
            }

            // For brevity: fallback to a simple API call placeholder.
            // Replace with actual `TranslationSession` usage per WWDC sample:
            // e.g. let session = try await TranslationSession(configuration: cfg)
            // then for result in session.translations(from: [req.text]) { ... }
            outText = "[translated: placeholder] " + req.text // placeholder - implement real call
        } catch {
            outErr = "\(error)"
        }
        sem.signal()
    }

    // Wait for the Task
    _ = sem.wait(timeout: .now() + 10.0)
    let resp = Response(translated: outText, error: outErr)
    if let d = try? JSONEncoder().encode(resp) {
        FileHandle.standardOutput.write(d)
    }
    exit(outText != nil ? 0 : 3)
} else {
    let resp = Response(translated: nil, error: "macOS 15+ required")
    if let d = try? JSONEncoder().encode(resp) {
        FileHandle.standardOutput.write(d)
    }
    exit(2)
}

}

main()

Important:

The helper skeleton above contains placeholders. The actual TranslationSession setup often expects a SwiftUI translationTask context; in many cases an ephemeral SwiftUI host is necessary even inside the helper. But keeping this logic in a separate binary lets you do that (the helper can be a small Cocoa/SwiftUI app that runs headlessly, or a commandline tool that creates an NSApplication runloop and an offscreen NSWindow with NSHostingController like earlier). This isolation is acceptable since it doesn't modify MacVim main process.

Packaging & code signing

Put the helper in Contents/Helpers/TranslationHelper inside the .app bundle.

Code sign the helper and the main app consistently. If you ship via notarization/AppStore, ensure entitlements and notarization are correct.

If MacVim runs privileged (sudo), do not run the helper as root — either refuse or drop privileges. The helper must refuse root to avoid giving translation the ability to access privileged resources.

Fallback strategies (if helper not available or user privacy concerns)

Ask user to upgrade to macOS 15+ and enable feature.

Offer an opt-in cloud translation (Google/Microsoft/OpenAI) if user consents — requires network and API keys; good for users on older macOS.

Offer a manual copy/paste UI that opens text in a separate modern app (like a small GUI tool you ship) that runs with SwiftUI on macOS 15+.

Summary of recommendations (actionable)

Implement the helper binary that uses the Translation framework.

In MacVim:

Runtime check: only attempt helper when major >= 15.

Use NSTask + stdin/stdout JSON to call helper asynchronously.

If helper fails or is absent, fall back to cloud or show a graceful message.

Ensure helper refuses to run as root or drops privileges.

Package and sign helper inside the app bundle.

Test on real macOS 15+ hardware (not just simulator).

Jatkingmodern avatar Sep 15 '25 11:09 Jatkingmodern

This looks like a plugin it's possible to implement even for a standard Vim using any Language Server implementation available.

@ychin is it possible to implement such bindings for a plugin? This potentially would be a great feature, as Apple translation/definition is not the only potential candidate for this.

PS: Other examples can include popups with documentation from a Language Server.

eirnym avatar Sep 15 '25 13:09 eirnym

assign me and I'll Try to build the Plugin By October end.

Jatkingmodern avatar Sep 15 '25 19:09 Jatkingmodern

@Jatkingmodern Is the response you wrote written by AI/LLM? Please make sure you actually understand the content first. I don't care if you use LLMs under the hood if you can actually back up what you typed in. For example "Apple can reject apps that depend on private system frameworks" as a sentence makes no sense given the context of how we release MacVim and seems like generic recommendations that an LLM spits out. Keep in mind that we actually need to read the content spit out by an AI if we want to engage with it and provide feedbacks. More content is not better content if it's just a bunch of texts.

But no, we don't want to build a plugin because that's overkill for a niche minor feature that a small number of our users will use. It's possible to use SwiftUI for this since that's how the official APIs work but I would prefer if we just stick with Objective C. More dependencies is more stuff to manage and more backwards compatibility issues to worry. I think we should just use the private API framework, which is used by Safari and therefore we know it's going to keep working unless Safari switches to the SwiftUI APIs.

ychin avatar Sep 15 '25 21:09 ychin

Regarding the validity of a Vim plugin, the whole point here is you want a popup dialog box using the native macOS translation UI. If you just want Vim to get access to translated texts there are tons of third-party services that could do that but it's not what this thread is about. The native macOS translation UI needs to be integrated into the app.

ychin avatar Sep 15 '25 21:09 ychin

Proposal: Use private macOS Translation API (Objective-C) instead of SwiftUI or plugin

We don’t want to build a Vim plugin here — that’s overkill for a niche feature only a small subset of users will use.
The requirement is specifically to show the native macOS translation popup dialog UI, not just to get translated text.

  • SwiftUI path: possible, since that’s what the official APIs expose, but it adds unnecessary dependencies and long-term backwards-compatibility issues we’d have to manage.
  • Plugin approach: not valid for this use case. If we just needed translated text, third-party services already exist. The point is integrating the native macOS translation UI directly into the app.
  • Private API path: preferred. Safari itself relies on the private framework, which strongly suggests Apple will keep it working (unless Safari itself switches to SwiftUI APIs). Using this keeps our implementation lean and minimizes new dependencies.

Action
Proceed with the private API Objective-C integration for now. Document the dependency clearly so if Apple does switch to SwiftUI in Safari, we can re-evaluate.

Jatkingmodern avatar Sep 16 '25 11:09 Jatkingmodern

#import <Cocoa/Cocoa.h>

NS_ASSUME_NONNULL_BEGIN

typedef void (^TranslationCompletion)(NSString * _Nullable translatedText, NSError * _Nullable error);

@interface TranslationPrivate : NSObject

  • (instancetype)sharedManager;

/// Returns YES if a candidate private translation class was found.

  • (BOOL)isPrivateTranslationAvailable;

/// Show the native translation popup UI (if available) anchored to anchorView. /// The completion block will be called if the underlying API provides a completion/callback parameter, /// or immediately with an error if the API isn't available.

  • (void)showTranslationPopupForText:(NSString *)text fromLanguage:(nullable NSString *)fromLang toLanguage:(nullable NSString *)toLang inView:(NSView *)anchorView completion:(TranslationCompletion)completion;

/// Try to get a translated string programmatically (no UI). This attempts other call variants.

  • (void)translateTextSilently:(NSString *)text fromLanguage:(nullable NSString *)fromLang toLanguage:(nullable NSString *)toLang completion:(TranslationCompletion)completion;

@end

NS_ASSUME_NONNULL_END #import "TranslationPrivate.h" #import <objc/runtime.h>

@implementation TranslationPrivate

  • (instancetype)sharedManager { static TranslationPrivate *g; static dispatch_once_t once; dispatch_once(&once, ^{ g = [[TranslationPrivate alloc] init]; }); return g; }

/// List of candidate private class names to try. Add names you discover with class-dump / runtime introspection.

  • (NSArray<NSString *> *)candidateClassNames { return @[ @"TTSTranslationViewController", // example name (may or may not exist) @"_TTS_TranslationController", // speculative variants @"TranslationViewController", @"TranslationController" ]; }

/// Candidate selectors to show a popup. Each entry is a selector name string. We attempt them in order.

  • (NSArray<NSString *> *)candidatePopupSelectors { return @[ @"showTranslationPopoverForString:fromLanguage:toLanguage:inView:completion:", // example (with completion) @"showTranslationPopoverForString:fromLanguage:toLanguage:inView:", // no completion @"presentTranslationForString:inView:source:target:completion:", // alternative @"presentTranslationForText:from:to:anchor:completion:" ]; }

/// Candidate selectors that may return translated text synchronously or via a completion.

  • (NSArray<NSString *> *)candidateSilentSelectors { return @[ @"translateString:fromLanguage:toLanguage:completion:", @"translateText:from:to:completion:", @"translationForString:from:to:error:" // returns value + error pointer ]; }

  • (BOOL)isPrivateTranslationAvailable { for (NSString *cname in [self candidateClassNames]) { Class cls = NSClassFromString(cname); if (cls) return YES; } return NO; }

/// Helper: try to instantiate a candidate controller class

  • (id _Nullable)instantiatePrivateController { for (NSString *cname in [self candidateClassNames]) { Class cls = NSClassFromString(cname); if (!cls) continue; // Prefer plain init; try common initializers if needed. id instance = nil; if ([cls respondsToSelector:@selector(new)]) { instance = [cls new]; } else if ([cls instancesRespondToSelector:@selector(init)]) { instance = [[cls alloc] init]; } else if ([cls instancesRespondToSelector:@selector(initWithNibName:bundle:)]) { instance = [[cls alloc] initWithNibName:nil bundle:nil]; }

      if (instance) {
          return instance;
      }
    

    } return nil; }

/// Core invoker: call a selector with up to 6 object arguments using NSInvocation. /// This function inspects the method signature and tries to set arguments safely.

  • (BOOL)invokeSelector:(SEL)sel onObj:(id)target withArgs:(NSArray *)args completion:(void (^)(NSInvocation *inv, BOOL invoked))done { if (!target || !sel) { if (done) done(nil, NO); return NO; }

    if (![target respondsToSelector:sel]) { if (done) done(nil, NO); return NO; }

    NSMethodSignature *sig = [target methodSignatureForSelector:sel]; if (!sig) { if (done) done(nil, NO); return NO; }

    NSInvocation *inv = [NSInvocation invocationWithMethodSignature:sig]; inv.target = target; inv.selector = sel;

    NSUInteger expected = sig.numberOfArguments - 2; // exclude self/_cmd NSUInteger toSet = MIN(expected, args.count);

    // Set object arguments; unsupported primitive args will be skipped (private APIs may vary) for (NSUInteger i = 0; i < toSet; ++i) { id obj = args[i]; // Check argument type to ensure it's an object const char *argType = [sig getArgumentTypeAtIndex:(2 + i)]; if (argType[0] == '@') { [inv setArgument:&obj atIndex:2 + i]; } else { // For primitive or block argument types we still try to set common ones: // - block (object representation) if (argType[0] == '^') { // assume block can be passed as object pointer [inv setArgument:&obj atIndex:2 + i]; } else { // can't handle primitive types generically; skip with a warning NSLog(@"[TranslationPrivate] Skipping non-object arg at index %lu (type %s)", (unsigned long)i, argType); } } }

    @try { [inv invoke]; if (done) done(inv, YES); return YES; } @catch (NSException *ex) { NSLog(@"[TranslationPrivate] Exception invoking %@ on %@: %@", NSStringFromSelector(sel), target, ex); if (done) done(inv, NO); return NO; } }

/// Show native translation popup using heuristics to find the right class/selector.

  • (void)showTranslationPopupForText:(NSString *)text fromLanguage:(NSString *)fromLang toLanguage:(NSString *)toLang inView:(NSView *)anchorView completion:(TranslationCompletion)completion { id controller = [self instantiatePrivateController]; if (!controller) { NSError *err = [NSError errorWithDomain:@"TranslationPrivate" code:1 userInfo:@{NSLocalizedDescriptionKey: @"No private translation controller class found."}]; if (completion) completion(nil, err); return; }

    // Try each popup selector variant for (NSString *selName in [self candidatePopupSelectors]) { SEL sel = sel_registerName(selName.UTF8String); if (![controller respondsToSelector:sel]) continue;

      // Prepare arguments array. We'll pass nil for missing optional args.
      NSMutableArray *args = [NSMutableArray array];
      [args addObject:(text ?: @"")];
      [args addObject:(fromLang ?: [NSNull null])];
      [args addObject:(toLang ?: [NSNull null])];
      [args addObject:(anchorView ?: [NSNull null])];
    
      // If the signature expects a completion block as the last arg, we set a bridging block.
      __block BOOL invokedOk = NO;
      BOOL called = [self invokeSelector:sel onObj:controller withArgs:args completion:^(NSInvocation *inv, BOOL invoked) {
          invokedOk = invoked;
          // Try to detect whether the last argument is a completion block by inspecting signature
          if (!invoked) {
              // invocation failed — we'll continue to next candidate
              return;
          }
          // If the method had a completion argument, the private implementation will call it when done.
          // We cannot reliably intercept that callback unless we provide our own block argument earlier.
      }];
    
      if (called) {
          // One of the selector attempts succeeded — notify caller. We can't always obtain a translated string,
          // because some APIs only surface the UI without programmatic callback. So we resolve with nil (UI shown).
          if (completion) completion(nil, nil);
          return;
      }
    

    }

    // If we reached here, none of the popup selectors worked NSError *err = [NSError errorWithDomain:@"TranslationPrivate" code:2 userInfo:@{NSLocalizedDescriptionKey: @"No matching popup selector found on private translation controller."}]; if (completion) completion(nil, err); }

/// Try to translate programmatically (no UI). This attempts selectors that return a string or provide a completion.

  • (void)translateTextSilently:(NSString *)text fromLanguage:(NSString *)fromLang toLanguage:(NSString *)toLang completion:(TranslationCompletion)completion { id controller = [self instantiatePrivateController]; if (!controller) { NSError *err = [NSError errorWithDomain:@"TranslationPrivate" code:10 userInfo:@{NSLocalizedDescriptionKey: @"No private translation controller found."}]; if (completion) completion(nil, err); return; }

    for (NSString *selName in [self candidateSilentSelectors]) { SEL sel = sel_registerName(selName.UTF8String); if (![controller respondsToSelector:sel]) continue;

      // Inspect signature
      NSMethodSignature *sig = [controller methodSignatureForSelector:sel];
      if (!sig) continue;
    
      // If the last parameter is a block-like type or there are 4 arguments and last is object, attempt to send block
      NSUInteger nargs = sig.numberOfArguments - 2;
      NSMutableArray *args = [NSMutableArray arrayWithObjects:
                              (text ?: @""),
                              (fromLang ?: [NSNull null]),
                              (toLang ?: [NSNull null]), nil];
    
      __block BOOL invoked = NO;
      // Prepare a block that matches expected form void (^)(NSString *, NSError *)
      void (^callbackBlock)(NSString *, NSError *) = ^(NSString *translated, NSError *err){
          if (completion) completion(translated, err);
      };
    
      // Append block if signature expects it
      if (nargs >= 4) {
          [args addObject:callbackBlock];
      }
    
      BOOL called = [self invokeSelector:sel onObj:controller withArgs:args completion:^(NSInvocation *inv, BOOL ok) {
          invoked = ok;
          if (!ok) return;
          // If method returned an object, we can try to read it
          const char *retType = [inv.methodSignature methodReturnType];
          if (retType && retType[0] == '@') {
              __unsafe_unretained id retObj = nil;
              [inv getReturnValue:&retObj];
              if ([retObj isKindOfClass:[NSString class]]) {
                  if (completion) completion((NSString *)retObj, nil);
                  return;
              }
          }
          // If we can't read a return value synchronously, rely on the block we passed
          // and assume the block will be called by the private framework.
      }];
    
      if (called) {
          // We either already called completion above, or the block will call it asynchronously.
          // If the call doesn't call the block for some reason, the caller may never get completion.
          return;
      }
    

    }

    NSError *err = [NSError errorWithDomain:@"TranslationPrivate" code:11 userInfo:@{NSLocalizedDescriptionKey: @"No matching translation API found (silent)."}]; if (completion) completion(nil, err); }

@end #import "TranslationPrivate.h"

// Show popup anchored to window content view [[TranslationPrivate sharedManager] showTranslationPopupForText:@"Hello, world" fromLanguage:@"en" toLanguage:@"fr" inView:myWindow.contentView completion:^(NSString * _Nullable translatedText, NSError * _Nullable error) { if (error) { NSLog(@"Translation popup failed: %@", error); } else { NSLog(@"Translation popup shown (UI). Translated text (if any): %@", translatedText); } }];

// Silent translation [[TranslationPrivate sharedManager] translateTextSilently:@"Good morning" fromLanguage:@"en" toLanguage:@"de" completion:^(NSString * _Nullable translatedText, NSError * _Nullable error) { if (error) { NSLog(@"Silent translation failed: %@", error); } else { NSLog(@"Translated text: %@", translatedText); } }];

Jatkingmodern avatar Sep 16 '25 11:09 Jatkingmodern

Here is your Required Objective -C/SwiftUI code for Private Translation API

Jatkingmodern avatar Sep 16 '25 11:09 Jatkingmodern

https://developer.apple.com/documentation/translation/translating-text-within-your-app

Jatkingmodern avatar Sep 16 '25 12:09 Jatkingmodern

This is clearly AI slop with incorrect code and approach and @Jatkingmodern did not answer my questions regarding usage of LLM. Please do not spam this thread further or you will be blocked from commenting in the future @Jatkingmodern.

ychin avatar Sep 16 '25 19:09 ychin