Skip to content

Add Translate to iOS selection context menu - #180021

Closed
LouiseHsu wants to merge 40 commits into
flutter:masterfrom
LouiseHsu:translation-redux
Closed

LouiseHsu wants to merge 40 commits into
flutter:masterfrom
LouiseHsu:translation-redux

Conversation

@LouiseHsu

@LouiseHsu LouiseHsu commented Dec 17, 2025 •

Copy link
Copy Markdown
Contributor

Fixes #150392, part of #107578

This PR adds a "Translate" action to the ios selection context menu using this swiftui translate api announce during WWDC 2024.
Includes the ipad implementation as well.

Simulator.Screen.Recording.-.iPad.Air.11-inch.M3.-.2025-05-28.at.14.11.58.mp4
Simulator.Screen.Recording.-.iPhone.16.Plus.-.2025-05-28.at.14.09.13.mp4

Pre-launch Checklist

  • I read the [Contributor Guide] and followed the process outlined there for submitting PRs.
  • 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.

@github-actions github-actions Bot added a: text input Entering text in a text field or keyboard related problems platform-ios iOS applications specifically framework flutter/packages/flutter repository. See also f: labels. engine flutter/engine related. See also e: labels. p: material_ui material_ui package in flutter/packages p: cupertino_ui cupertino_ui package in flutter/packages team-ios Owned by iOS platform team labels Dec 17, 2025
@LouiseHsu LouiseHsu changed the title Translation redux Add Translate to iOS selection context menu Dec 17, 2025
@github-actions github-actions Bot added the a: internationalization Supporting other languages or locales. (aka i18n) label Jan 7, 2026
@LouiseHsu
LouiseHsu marked this pull request as ready for review January 9, 2026 00:44
@LouiseHsu
LouiseHsu requested a review from a team as a code owner January 9, 2026 00:44

@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.

This is exciting!

@LouiseHsu LouiseHsu added the CICD Run CI/CD label Apr 17, 2026
/// Label for "search web" edit buttons and menu items.
String get searchWebButtonLabel;

/// The term used for presenting a translation menu for a selection.

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.

nit: consider changing to Label for "translate" edit buttons and menu items. similar to the other context menu button labels documentation.


return !widget.obscureText &&
!textEditingValue.selection.isCollapsed &&
textEditingValue.selection.textInside(textEditingValue.text).trim() != '';

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 tried on a physical iPhone and was able to get the translate button to show up on a native app (iMessage) if the only text selected was whitespace.

I would usually lean towards matching native even though I don't think translating whitespace adds value here. Definetely not a blocker.

expect(find.text('Search Web'), findsOneWidget);

// Adding Translate pushes both Share and Translate to the next page
// So just check that they exist on the tool bar.

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.

nit: tool bar -> toolbar.

expect(find.text('Search Web'), findsOneWidget);

// Adding Translate pushes both Share and Translate to the next page
// So just check that they exist on the tool bar.

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.

nit: tool bar -> toolbar.

@github-actions github-actions Bot removed the CICD Run CI/CD label Apr 17, 2026

// Adding Translate pushes both Share and Translate to the next page
// So just check that they exist on the tool bar.
expect(cupertinoToolbarHasText('Share...'), isTrue);

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.

Consider using find.text('Translate', skipOffstage: false) instead of using cupertinoToolbarHasText.

expect(findOverflowNextButton(), findsOneWidget);

// Tapping the next button again shows the last page and the Share button
// Tapping the next button again shows the Share button

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.

nit: end comments with a ., here and in the comment below.

@available(iOS 17.4, *)
fileprivate struct ContentView: View {
fileprivate let termToTranslate: String
fileprivate let ipadBounds: CGRect?

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.

these 2 are not accessed outside of ContentView so should be private rather than fileprivate

fileprivate let termToTranslate: String
fileprivate let ipadBounds: CGRect?

fileprivate var onDismiss: (() -> Void)?

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.

it's file for this to be default (i.e. internal), because the struct itself is already fileprivate

CGRect lastRect = [(FlutterTextInputView*)_textInputPlugin.textInputView
caretRectForPosition:(FlutterTextPosition*)range.end];
CGRect transformedLastRect = [(FlutterTextInputView*)_textInputPlugin.textInputView
localRectFromFrameworkTransform:lastRect];

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.

Is this trying to get the bounding boxes of the current selection? So the assumption here is "term" is the same as the selected text? In that case why does the framework have to send "term" over if we know it's going to be the selected text?

CGRect transformedLastRect = [(FlutterTextInputView*)_textInputPlugin.textInputView
localRectFromFrameworkTransform:lastRect];

// In case of RTL Language, get the minimum x coordinate too

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 don't think this works in bidi text (I'm not sure if there's a valid use case for selecting across bidi regions, but the UI may look out of place if the user actually does that).


swiftUIViewController.view.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
swiftUIViewController.view.topAnchor.constraint(equalTo: view.topAnchor),

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.

So swiftUIViewController can use a different coordinate system even from FlutterView (if topAnchor is not 0 for example), and iPadBounds is calculated in the textInputView's coordinates?

termToTranslate: termToTranslate,
ipadBounds: ipadBounds
)
contentView.onDismiss = { [weak self] in

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.

Would it be possible to make onDismiss a let variable? Looks like it's not changing.

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.

Why's this method @objc and internal? It seems it's only called in viewDidLoad?

/// Currently this is only implemented for iOS.
///
/// When 'obscureText' is true or the selection is empty, this function will not do anything.
Future<void> translateSelection(SelectionChangedCause cause) async {

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.

Why does this take a SelectionChangedCause? Doesn't seem to be used anywhere.

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.

Not very familiar with context menus, but it looks like this is going to be called to trigger translation in a Flutter rendered context menu while the new logic in system_context_menu.dart is going to be used when the app is using an iOS rendered context menu? If that's the case, how does the app developer determine whether translation is available (i.e. the app is running on iOS 17.4+) and hide the button in the translationEnabled implementation (to prevent a "translate" button that does nothing from showing up).

}
}

/// Launch a translation interface for the selected text.

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.

nit:

Suggested change
/// Launch a translation interface for the selected text.
/// Launches a translation interface for the selected text.


/// Launch a translation interface for the selected text.
///
/// Currently this is only implemented for iOS.

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.

And only for iOS 17.4+ I think?

#import <OCMock/OCMock.h>
#import <XCTest/XCTest.h>

#import "flutter/shell/platform/darwin/common/InternalFlutterSwiftCommon/InternalFlutterSwiftCommon.h"

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 thought this was the bridging header? Why is the import needed?

var body: some View {
Color.clear
.frame(maxWidth: .infinity, maxHeight: .infinity)
.translationPresentation(

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.

Just curious, is this API only available in SwiftUI?

@LouiseHsu
LouiseHsu marked this pull request as draft May 14, 2026 20:59
@Piinks Piinks added the Decoupling: Not ready to port yet Instructions will be provided when this is ready to move to flutter/packages. label Jun 24, 2026
@Piinks

Piinks commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

I've marked this PR as not ready to port to flutter/packages yet.
We'll provide instructions to move this change over to material_ui/cupertino_ui once ready to receive PRs. Thank you!

The changes not in the material or cupertino libraries will remain here.

@Piinks Piinks added the Decoupling: Split PR The PR will need to be split to separate Material & Cupertino changes label Jun 24, 2026
@Piinks

Piinks commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

Advice for PR splitting: Since this touches material/cupertino along with other parts of flutter/flutter, it will need to be split up.

If the material/cupertino changes are dependent on the other non-material/cupertino changes in this PR, then the material/cupertino changes will need to wait for them to roll to stable before they can be ported over to material_ui/cupertino_ui in flutter/packages (which uses the stable branch).

  • If that is the case here, consider opening a new PR with the non-material/cupertino changes to move forward with so that waiting period can be reduced. Up to you!

@Piinks Piinks added Decoupling: Port to flutter/packages This PR is ready to be ported to flutter/packages. We will provide instructions to do so. and removed Decoupling: Not ready to port yet Instructions will be provided when this is ready to move to flutter/packages. labels Aug 18, 2026
@Piinks

Piinks commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

material_ui and cupertino_ui are now accepting contributions. :)

The portions of this PR that affect the material and cupertino libraries can be ported over to flutter/packages, although they may have to wait for the changes in the core SDK to reach stable first.

Guidance and instructions for easily porting over can be found in #188444

@jmagman

jmagman commented Sep 8, 2026

Copy link
Copy Markdown
Member

Closing in favor of #189878

@jmagman jmagman closed this Sep 8, 2026
pull Bot pushed a commit to edisplay/flutter that referenced this pull request Sep 16, 2026
Split out engine portion of
flutter#180021 b/c of cupertino/material
freeze.

## Pre-launch Checklist

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

Labels

a: internationalization Supporting other languages or locales. (aka i18n) a: text input Entering text in a text field or keyboard related problems Decoupling: Port to flutter/packages This PR is ready to be ported to flutter/packages. We will provide instructions to do so. Decoupling: Split PR The PR will need to be split to separate Material & Cupertino changes engine flutter/engine related. See also e: labels. framework flutter/packages/flutter repository. See also f: labels. p: cupertino_ui cupertino_ui package in flutter/packages p: material_ui material_ui package in flutter/packages platform-ios iOS applications specifically team-ios Owned by iOS platform team

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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