Skip to content

[iOS] Add Translate API support to Engine - #189878

Merged
auto-submit[bot] merged 14 commits into
flutter:masterfrom
LouiseHsu:translation-redux-engine
Sep 15, 2026
Merged

auto-submit[bot] merged 14 commits into
flutter:masterfrom
LouiseHsu:translation-redux-engine

Conversation

@LouiseHsu

@LouiseHsu LouiseHsu commented Jul 22, 2026 •

Copy link
Copy Markdown
Contributor

Split out engine portion of #180021 b/c of cupertino/material freeze.

Pre-launch Checklist

  • I read the [Contributor Guide] and followed the process outlined there for submitting PRs.
  • I read the [AI contribution guidelines] and understand my responsibilities, or I am not using AI tools.
  • I read the [Tree Hygiene] wiki page, which explains my responsibilities.
  • I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement].
  • I signed the [CLA].
  • I listed at least one issue that this PR fixes in the description above.
  • I updated/added relevant documentation (doc comments with ///).
  • I added new tests to check the change I am making, or this PR is [test-exempt].
  • I followed the [breaking change policy] and added [Data Driven Fixes] where supported.
  • All existing and new tests are passing.

LouiseHsu and others added 2 commits July 22, 2026 11:23
Specifies support for iOS 17.4+ translation API, adding TranslateViewController, platform plugin handling, delegate integration, and unit/edit-menu tests.
@github-actions github-actions Bot added a: text input Entering text in a text field or keyboard related problems platform-ios iOS applications specifically engine flutter/engine related. See also e: labels. team-ios Owned by iOS platform team labels Jul 22, 2026
@LouiseHsu LouiseHsu added the CICD Run CI/CD label Jul 22, 2026
@LouiseHsu
LouiseHsu marked this pull request as ready for review July 30, 2026 20:13
@LouiseHsu
LouiseHsu requested a review from a team as a code owner July 30, 2026 20:13

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request integrates the iOS 17.4+ translation API into the Flutter iOS platform plugin by introducing TranslateViewController and adding a 'translate' action to the text input edit menu. Feedback on these changes identifies potential runtime crashes if term or flutterViewController are nil, precision loss from using abs() instead of fabs() on CGFloat coordinates, and potential UIKit hierarchy issues when dismissing the hosting controller synchronously. Additionally, suggestions are provided to improve Swift encapsulation, formatting, and consistency in platform-check casing.

CGRect ipadBounds =
CGRectMake(fmin(transformedFirstRect.origin.x, transformedLastRect.origin.x),
transformedFirstRect.origin.y,
abs(transformedLastRect.origin.x - transformedFirstRect.origin.x),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Using abs() on floating-point values (CGFloat) will cause an implicit conversion to int, truncating the fractional part. This can lead to precision loss and layout bugs for the popover bounds. Use fabs() instead.

Suggested change
abs(transformedLastRect.origin.x - transformedFirstRect.origin.x),
fabs(transformedLastRect.origin.x - transformedFirstRect.origin.x),

swiftUIViewController.didMove(toParent: self)
}

@objc func makeTranslateHostingController(termToTranslate: String) -> UIViewController {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Since makeTranslateHostingController is only called internally within TranslateViewController, it should be marked private and the @objc attribute should be removed to improve encapsulation and avoid unnecessary Objective-C runtime exposure.

Suggested change
@objc func makeTranslateHostingController(termToTranslate: String) -> UIViewController {
private func makeTranslateHostingController(termToTranslate: String) -> UIViewController {

Comment on lines +68 to +73
contentView.onDismiss = { [weak self] in
guard let self = self else { return }
self.willMove(toParent: nil)
self.view.removeFromSuperview()
self.removeFromParent()
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Modifying the UIKit view hierarchy (removing the hosting controller's view and parent-child relationship) synchronously inside the onDismiss closure (which is triggered by the SwiftUI binding's set block during a view update) can cause undefined behavior or crashes. It is safer to dispatch this to the main queue asynchronously.

Suggested change
contentView.onDismiss = { [weak self] in
guard let self = self else { return }
self.willMove(toParent: nil)
self.view.removeFromSuperview()
self.removeFromParent()
}
contentView.onDismiss = { [weak self] in
DispatchQueue.main.async {
guard let self = self else { return }
self.willMove(toParent: nil)
self.view.removeFromSuperview()
self.removeFromParent()
}
}

Color.clear
.frame(maxWidth: .infinity, maxHeight: .infinity)
.translationPresentation(
isPresented: Binding( get: { true }, set: { shown in if !shown { onDismiss?() } } ),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Cramming the get and set closures with control flow on a single line is hard to read and violates standard Swift formatting guidelines. Formatting it with proper line breaks improves readability.

        isPresented: Binding(
          get: { true },
          set: { shown in
            if !shown {
              onDismiss?()
            }
          }
        ),

suggestedMenu:suggestedMenu];
}
} else if ([type isEqualToString:@"translate"]) {
if (@available(ios 17.4, *)) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Use the standard uppercase iOS instead of lowercase ios in the @available check for consistency with the rest of the codebase.

      if (@available(iOS 17.4, *)) {

hellohuanlin
hellohuanlin previously approved these changes Jul 31, 2026

@hellohuanlin hellohuanlin left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think i reviewed this in the previous PR before? So carrying over the LGTM.

// firstRectForRange cannot be used here as its current implementation does
// not always return the full rect of the range.
CGRect firstRect = [(FlutterTextInputView*)_textInputPlugin.textInputView
caretRectForPosition:(FlutterTextPosition*)range.start];

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I probably worth extracting a local:

FlutterTextInputView* plugin = (FlutterTextInputView*)_textInputPlugin.textInputView;
// ... stuff ...

}

- (void)showTranslateViewControllerForTerm:(NSString*)term {
if (term.length == 0) {

@cbracken cbracken Aug 5, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should wrap the content of this method in an if (@available(iOS 17.4, *)) which it looks like you've done elsewhere, otherwise we risk runtime crashes.

That kind of raises the question -- does the framework need to know whether this worked or not? This method doesn't have a return so on iOS < 17.4 you won't be able to tell. Depending on how this is implemented in the framework, you could also have guards in the plugin message handler return FlutterMethodNotImplemented... or some other signal. But probably depends a lot on how the framework wires things up.

[mockPlugin handleMethodCall:methodCall result:result];
[self waitForExpectationsWithTimeout:1 handler:nil];
}
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This covers the happy path. It might be worth adding a test for the unhappy path where iOS 17.4 isn't available.

/// be imported directly into Objective-C.
@available(iOS 17.4, *)
@objc(FLTTranslateViewController)
class TranslateViewController: UIViewController {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since the class is TranslateViewController, we should name the file TranslateViewController.swift.

/// This is necessary because UIHostingController is a generic class and cannot
/// be imported directly into Objective-C.
@available(iOS 17.4, *)
@objc(FLTTranslateViewController)

@cbracken cbracken Aug 5, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be:

@objc(FlutterTranslateViewController)

for consistency with the other framework classes.

}

@available(iOS 17.4, *)
fileprivate struct ContentView: View {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems like a pretty generic name. Maybe TranslateContentView or TranslatePresentationView?

if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) {
// On iPad, the translate screen is presented in a popover view, and requires a rect to use as
// bounds
FlutterTextInputPlugin* _textInputPlugin = [self.engine textInputPlugin];

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This shouldn't have a leading underscore since that's used for ivars.

// firstRectForRange cannot be used here as its current implementation does
// not always return the full rect of the range.
CGRect firstRect = [(FlutterTextInputView*)_textInputPlugin.textInputView
caretRectForPosition:(FlutterTextPosition*)range.start];

@cbracken cbracken Aug 5, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here and a few other places: I could be wrong, but you shouldn't need a FlutterTextPosition*, the default UITextPosition* should be all that's necessary and doesn't ened a cast. UIKit APIs don't care about the underlying type; they use our own code to manipulate these.

[flutterViewController addChildViewController:translateViewController];
[flutterViewController.view addSubview:translateViewController.view];
[translateViewController didMoveToParentViewController:flutterViewController];
}

@cbracken cbracken Aug 5, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if we extracted a helper (CGRect)popoverBoundsForCurrentSelection and have showShareViewController, showLookUpViewController, and showTranslateViewControllerForTerm all call it?

@cbracken cbracken Aug 5, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thinking about it, you probably don't need to dedupe out the other (existing) code in the other methods this PR if you want to keep it focused on translate, but you could create the helper and use it your new method in this patch and then go back to clean up the others in a followup patch.

@cbracken

cbracken commented Aug 5, 2026 •

Copy link
Copy Markdown
Member

Overall this looks good! Mostly just small nits.

@flutter-dashboard

Copy link
Copy Markdown

Golden file changes have been found for this pull request. Click here to view and triage (e.g. because this is an intentional change).

If you are still iterating on this change and are not ready to resolve the images on the Flutter Gold dashboard, consider marking this PR as a draft pull request above. You will still be able to view image results on the dashboard, commenting will be silenced, and the check will not try to resolve itself until marked ready for review.

For more guidance, visit Writing a golden file test for package:flutter.

Reviewers: Read the Tree Hygiene page and make sure this patch meets those guidelines before LGTMing.

Changes reported for pull request #189878 at sha 7ac0d23

@flutter-dashboard flutter-dashboard Bot added the will affect goldens Changes to golden files label Aug 24, 2026

@cbracken cbracken left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is looking really good. Mostly just nits and then one kind of open question around the edge-case where we fail to actually present the translation view.

@LouiseHsu
LouiseHsu requested a review from cbracken September 2, 2026 20:15
@jmagman jmagman linked an issue Sep 8, 2026 that may be closed by this pull request

@cbracken cbracken left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM stamp from a Japanese personal seal

Looks great! Ship it! 🚀

@LouiseHsu LouiseHsu added the autosubmit Merge PR when tree becomes green via auto submit App label Sep 10, 2026
@auto-submit auto-submit Bot removed the autosubmit Merge PR when tree becomes green via auto submit App label Sep 10, 2026
@auto-submit

auto-submit Bot commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

autosubmit label was removed for flutter/flutter/189878, because The base commit of the PR is older than 7 days and can not be merged. Please merge the latest changes from the main into this branch and resubmit the PR.

@auto-submit

auto-submit Bot commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

autosubmit label was removed for flutter/flutter/189878, because - The status or check suite Dashboard Checks has failed. Please fix the issues identified (or deflake) before re-applying this label.

@LouiseHsu LouiseHsu added the autosubmit Merge PR when tree becomes green via auto submit App label Sep 10, 2026
@auto-submit auto-submit Bot removed the autosubmit Merge PR when tree becomes green via auto submit App label Sep 10, 2026
@auto-submit

auto-submit Bot commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

autosubmit label was removed for flutter/flutter/189878, because - The status or check suite Google testing has failed. Please fix the issues identified (or deflake) before re-applying this label.

  • The status or check suite Dashboard Checks has failed. Please fix the issues identified (or deflake) before re-applying this label.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

a: text input Entering text in a text field or keyboard related problems CICD Run CI/CD engine flutter/engine related. See also e: labels. platform-ios iOS applications specifically team-ios Owned by iOS platform team will affect goldens Changes to golden files

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[iOS] Add Translate button to text fields' context menu

3 participants