From b59c8e22af0d92988d2145ea28299230f3b71180 Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Wed, 29 May 2024 07:08:46 +0300 Subject: Native translation expo module (#4098) * translation expo module * add `onClose` and `onReplacementAction` * rm onReplacementAction * make all props published * make translation api available globally w/o wrapper (#4110) * conditionally import the translation module * only use native translation if language is probably supported * open native translation via dropdown menu --------- Co-authored-by: Hailey Co-authored-by: Dan Abramov --- .../expo-bluesky-translate/expo-module.config.json | 6 +++ modules/expo-bluesky-translate/index.ts | 6 +++ .../ios/Common/UIHostingControllerCompat.swift | 20 +++++++++ .../ios/ExpoBlueskyTranslate.podspec | 21 ++++++++++ .../ios/ExpoBlueskyTranslateModule.swift | 18 ++++++++ .../ios/ExpoBlueskyTranslateView.swift | 22 ++++++++++ .../expo-bluesky-translate/ios/TranslateView.swift | 31 ++++++++++++++ .../src/ExpoBlueskyTranslate.types.ts | 3 ++ .../src/ExpoBlueskyTranslateView.ios.tsx | 48 ++++++++++++++++++++++ .../src/ExpoBlueskyTranslateView.tsx | 13 ++++++ .../src/ExpoScrollForwarderView.tsx | 2 + 11 files changed, 190 insertions(+) create mode 100644 modules/expo-bluesky-translate/expo-module.config.json create mode 100644 modules/expo-bluesky-translate/index.ts create mode 100644 modules/expo-bluesky-translate/ios/Common/UIHostingControllerCompat.swift create mode 100644 modules/expo-bluesky-translate/ios/ExpoBlueskyTranslate.podspec create mode 100644 modules/expo-bluesky-translate/ios/ExpoBlueskyTranslateModule.swift create mode 100644 modules/expo-bluesky-translate/ios/ExpoBlueskyTranslateView.swift create mode 100644 modules/expo-bluesky-translate/ios/TranslateView.swift create mode 100644 modules/expo-bluesky-translate/src/ExpoBlueskyTranslate.types.ts create mode 100644 modules/expo-bluesky-translate/src/ExpoBlueskyTranslateView.ios.tsx create mode 100644 modules/expo-bluesky-translate/src/ExpoBlueskyTranslateView.tsx (limited to 'modules') diff --git a/modules/expo-bluesky-translate/expo-module.config.json b/modules/expo-bluesky-translate/expo-module.config.json new file mode 100644 index 000000000..28c5dd878 --- /dev/null +++ b/modules/expo-bluesky-translate/expo-module.config.json @@ -0,0 +1,6 @@ +{ + "platforms": ["ios"], + "ios": { + "modules": ["ExpoBlueskyTranslateModule"] + } +} diff --git a/modules/expo-bluesky-translate/index.ts b/modules/expo-bluesky-translate/index.ts new file mode 100644 index 000000000..f3e1d3b11 --- /dev/null +++ b/modules/expo-bluesky-translate/index.ts @@ -0,0 +1,6 @@ +export { + isAvailable, + isLanguageSupported, + NativeTranslationModule, + NativeTranslationView, +} from './src/ExpoBlueskyTranslateView' diff --git a/modules/expo-bluesky-translate/ios/Common/UIHostingControllerCompat.swift b/modules/expo-bluesky-translate/ios/Common/UIHostingControllerCompat.swift new file mode 100644 index 000000000..c8ca3e027 --- /dev/null +++ b/modules/expo-bluesky-translate/ios/Common/UIHostingControllerCompat.swift @@ -0,0 +1,20 @@ +import ExpoModulesCore +import SwiftUI + +// Thanks to Andrew Levy for this code snippet +// https://github.com/andrew-levy/swiftui-react-native/blob/d3fbb2abf07601ff0d4b83055e7717bb980910d6/ios/Common/ExpoView%2BUIHostingController.swift + +extension ExpoView { + func setupHostingController(_ hostingController: UIHostingController) { + hostingController.view.translatesAutoresizingMaskIntoConstraints = false + hostingController.view.backgroundColor = .clear + + addSubview(hostingController.view) + NSLayoutConstraint.activate([ + hostingController.view.topAnchor.constraint(equalTo: self.topAnchor), + hostingController.view.bottomAnchor.constraint(equalTo: self.bottomAnchor), + hostingController.view.leftAnchor.constraint(equalTo: self.leftAnchor), + hostingController.view.rightAnchor.constraint(equalTo: self.rightAnchor), + ]) + } +} diff --git a/modules/expo-bluesky-translate/ios/ExpoBlueskyTranslate.podspec b/modules/expo-bluesky-translate/ios/ExpoBlueskyTranslate.podspec new file mode 100644 index 000000000..45f86a605 --- /dev/null +++ b/modules/expo-bluesky-translate/ios/ExpoBlueskyTranslate.podspec @@ -0,0 +1,21 @@ +Pod::Spec.new do |s| + s.name = 'ExpoBlueskyTranslate' + s.version = '1.0.0' + s.summary = 'Uses SwiftUI translation to translate text.' + s.description = 'Uses SwiftUI translation to translate text.' + s.author = '' + s.homepage = 'https://docs.expo.dev/modules/' + s.platforms = { :ios => '13.4' } + s.source = { git: '' } + s.static_framework = true + + s.dependency 'ExpoModulesCore' + + # Swift/Objective-C compatibility + s.pod_target_xcconfig = { + 'DEFINES_MODULE' => 'YES', + 'SWIFT_COMPILATION_MODE' => 'wholemodule' + } + + s.source_files = "**/*.{h,m,mm,swift,hpp,cpp}" +end diff --git a/modules/expo-bluesky-translate/ios/ExpoBlueskyTranslateModule.swift b/modules/expo-bluesky-translate/ios/ExpoBlueskyTranslateModule.swift new file mode 100644 index 000000000..afa813722 --- /dev/null +++ b/modules/expo-bluesky-translate/ios/ExpoBlueskyTranslateModule.swift @@ -0,0 +1,18 @@ +import ExpoModulesCore +import Foundation +import SwiftUI + +public class ExpoBlueskyTranslateModule: Module { + public func definition() -> ModuleDefinition { + Name("ExpoBlueskyTranslate") + + AsyncFunction("presentAsync") { (text: String) in + DispatchQueue.main.async { [weak state = TranslateViewState.shared] in + state?.isPresented = true + state?.text = text + } + } + + View(ExpoBlueskyTranslateView.self) {} + } +} diff --git a/modules/expo-bluesky-translate/ios/ExpoBlueskyTranslateView.swift b/modules/expo-bluesky-translate/ios/ExpoBlueskyTranslateView.swift new file mode 100644 index 000000000..ca6e3be69 --- /dev/null +++ b/modules/expo-bluesky-translate/ios/ExpoBlueskyTranslateView.swift @@ -0,0 +1,22 @@ +import ExpoModulesCore +import Foundation +import SwiftUI + +class TranslateViewState: ObservableObject { + static var shared = TranslateViewState() + + @Published var isPresented = false + @Published var text = "" +} + +class ExpoBlueskyTranslateView: ExpoView { + required init(appContext: AppContext? = nil) { + if #available(iOS 14.0, *) { + let hostingController = UIHostingController(rootView: TranslateView()) + super.init(appContext: appContext) + setupHostingController(hostingController) + } else { + super.init(appContext: appContext) + } + } +} diff --git a/modules/expo-bluesky-translate/ios/TranslateView.swift b/modules/expo-bluesky-translate/ios/TranslateView.swift new file mode 100644 index 000000000..e2886dc84 --- /dev/null +++ b/modules/expo-bluesky-translate/ios/TranslateView.swift @@ -0,0 +1,31 @@ +import SwiftUI +// conditionally import the Translation module +#if canImport(Translation) +import Translation +#endif + +struct TranslateView: View { + @ObservedObject var state = TranslateViewState.shared + + var body: some View { + if #available(iOS 17.4, *) { + VStack { + UIViewRepresentableWrapper(view: UIView(frame: .zero)) + } + .translationPresentation( + isPresented: $state.isPresented, + text: state.text + ) + } + } +} + +struct UIViewRepresentableWrapper: UIViewRepresentable { + let view: UIView + + func makeUIView(context: Context) -> UIView { + return view + } + + func updateUIView(_ uiView: UIView, context: Context) {} +} diff --git a/modules/expo-bluesky-translate/src/ExpoBlueskyTranslate.types.ts b/modules/expo-bluesky-translate/src/ExpoBlueskyTranslate.types.ts new file mode 100644 index 000000000..a01d4d479 --- /dev/null +++ b/modules/expo-bluesky-translate/src/ExpoBlueskyTranslate.types.ts @@ -0,0 +1,3 @@ +export type ExpoBlueskyTranslateModule = { + presentAsync: (text: string) => Promise +} diff --git a/modules/expo-bluesky-translate/src/ExpoBlueskyTranslateView.ios.tsx b/modules/expo-bluesky-translate/src/ExpoBlueskyTranslateView.ios.tsx new file mode 100644 index 000000000..daddfa028 --- /dev/null +++ b/modules/expo-bluesky-translate/src/ExpoBlueskyTranslateView.ios.tsx @@ -0,0 +1,48 @@ +import React from 'react' +import {Platform} from 'react-native' +import {requireNativeModule, requireNativeViewManager} from 'expo-modules-core' + +import {ExpoBlueskyTranslateModule} from './ExpoBlueskyTranslate.types' + +export const NativeTranslationModule = + requireNativeModule('ExpoBlueskyTranslate') + +const NativeView: React.ComponentType = requireNativeViewManager( + 'ExpoBlueskyTranslate', +) + +export function NativeTranslationView() { + return +} + +export const isAvailable = Number(Platform.Version) >= 17.4 + +// https://en.wikipedia.org/wiki/Translate_(Apple)#Languages +const SUPPORTED_LANGUAGES = [ + 'ar', + 'zh', + 'zh', + 'nl', + 'en', + 'en', + 'fr', + 'de', + 'id', + 'it', + 'ja', + 'ko', + 'pl', + 'pt', + 'ru', + 'es', + 'th', + 'tr', + 'uk', + 'vi', +] + +export function isLanguageSupported(lang?: string) { + // If the language is not provided, we assume it is supported + if (!lang) return true + return SUPPORTED_LANGUAGES.includes(lang) +} diff --git a/modules/expo-bluesky-translate/src/ExpoBlueskyTranslateView.tsx b/modules/expo-bluesky-translate/src/ExpoBlueskyTranslateView.tsx new file mode 100644 index 000000000..16ff9d600 --- /dev/null +++ b/modules/expo-bluesky-translate/src/ExpoBlueskyTranslateView.tsx @@ -0,0 +1,13 @@ +export const NativeTranslationModule = { + presentAsync: async (_: string) => {}, +} + +export function NativeTranslationView() { + return null +} + +export const isAvailable = false + +export function isLanguageSupported(_lang?: string) { + return false +} diff --git a/modules/expo-scroll-forwarder/src/ExpoScrollForwarderView.tsx b/modules/expo-scroll-forwarder/src/ExpoScrollForwarderView.tsx index 93e69333f..0f5d01c13 100644 --- a/modules/expo-scroll-forwarder/src/ExpoScrollForwarderView.tsx +++ b/modules/expo-scroll-forwarder/src/ExpoScrollForwarderView.tsx @@ -1,5 +1,7 @@ import React from 'react' + import {ExpoScrollForwarderViewProps} from './ExpoScrollForwarder.types' + export function ExpoScrollForwarderView({ children, }: React.PropsWithChildren) { -- cgit 1.4.1