auto-detect composer language from keyboard
adds a new expo-keyboard-language native module that reads the current keyboard language on iOS (via first responder's textInputMode) and Android (via InputMethodManager). iOS also gets real-time updates when the keyboard language changes. users opt in via Settings > Languages > "Automatically detect post language from keyboard" (native only, marked experimental). when enabled, the composer language tracks the keyboard language. users can override per-session by selecting a language manually, and re-enable auto mode via the "Automatic" item in the language menu. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,39 @@
|
||||
apply plugin: 'com.android.library'
|
||||
|
||||
group = 'expo.modules.keyboardlanguage'
|
||||
version = '1.0.0'
|
||||
|
||||
def expoModulesCorePlugin = new File(project(":expo-modules-core").projectDir.absolutePath, "ExpoModulesCorePlugin.gradle")
|
||||
apply from: expoModulesCorePlugin
|
||||
applyKotlinExpoModulesCorePlugin()
|
||||
useCoreDependencies()
|
||||
useExpoPublishing()
|
||||
|
||||
def useManagedAndroidSdkVersions = false
|
||||
if (useManagedAndroidSdkVersions) {
|
||||
useDefaultAndroidSdkVersions()
|
||||
} else {
|
||||
buildscript {
|
||||
ext.safeExtGet = { prop, fallback ->
|
||||
rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
|
||||
}
|
||||
}
|
||||
project.android {
|
||||
compileSdkVersion safeExtGet("compileSdkVersion", 34)
|
||||
defaultConfig {
|
||||
minSdkVersion safeExtGet("minSdkVersion", 21)
|
||||
targetSdkVersion safeExtGet("targetSdkVersion", 34)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
android {
|
||||
namespace "expo.modules.keyboardlanguage"
|
||||
defaultConfig {
|
||||
versionCode 1
|
||||
versionName "1.0.0"
|
||||
}
|
||||
lintOptions {
|
||||
abortOnError false
|
||||
}
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
package expo.modules.keyboardlanguage
|
||||
|
||||
import android.os.Build
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import androidx.core.content.getSystemService
|
||||
import expo.modules.kotlin.modules.Module
|
||||
import expo.modules.kotlin.modules.ModuleDefinition
|
||||
|
||||
class ExpoKeyboardLanguageModule : Module() {
|
||||
override fun definition() =
|
||||
ModuleDefinition {
|
||||
Name("ExpoKeyboardLanguage")
|
||||
|
||||
Function("getCurrentKeyboardLanguage") {
|
||||
val context = appContext.reactContext ?: return@Function null
|
||||
val imm = context.getSystemService<InputMethodManager>() ?: return@Function null
|
||||
val subtype = imm.currentInputMethodSubtype ?: return@Function null
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
val tag = subtype.languageTag
|
||||
if (tag.isNotEmpty()) return@Function tag
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
val locale = subtype.locale
|
||||
if (locale.isNotEmpty()) return@Function locale
|
||||
|
||||
return@Function null
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"platforms": ["ios", "android"],
|
||||
"ios": {
|
||||
"modules": ["ExpoKeyboardLanguageModule"]
|
||||
},
|
||||
"android": {
|
||||
"modules": ["expo.modules.keyboardlanguage.ExpoKeyboardLanguageModule"]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export {
|
||||
addKeyboardLanguageListener,
|
||||
getCurrentKeyboardLanguage,
|
||||
} from './src/index'
|
||||
@@ -0,0 +1,20 @@
|
||||
Pod::Spec.new do |s|
|
||||
s.name = 'ExpoKeyboardLanguage'
|
||||
s.version = '1.0.0'
|
||||
s.summary = 'Expo module to detect the current keyboard language'
|
||||
s.description = 'Expo module to detect the current keyboard language'
|
||||
s.author = ''
|
||||
s.homepage = 'https://github.com/bluesky-social/social-app'
|
||||
s.platforms = { :ios => '13.4' }
|
||||
s.source = { git: '' }
|
||||
s.static_framework = true
|
||||
|
||||
s.dependency 'ExpoModulesCore'
|
||||
|
||||
s.pod_target_xcconfig = {
|
||||
'DEFINES_MODULE' => 'YES',
|
||||
'SWIFT_COMPILATION_MODE' => 'wholemodule'
|
||||
}
|
||||
|
||||
s.source_files = "**/*.{h,m,mm,swift,hpp,cpp}"
|
||||
end
|
||||
@@ -0,0 +1,75 @@
|
||||
import ExpoModulesCore
|
||||
|
||||
public class ExpoKeyboardLanguageModule: Module {
|
||||
public func definition() -> ModuleDefinition {
|
||||
Name("ExpoKeyboardLanguage")
|
||||
|
||||
Events("onKeyboardLanguageChange")
|
||||
|
||||
Function("getCurrentKeyboardLanguage") {
|
||||
return self.currentKeyboardLanguage()
|
||||
}
|
||||
|
||||
OnStartObserving {
|
||||
NotificationCenter.default.addObserver(
|
||||
self,
|
||||
selector: #selector(self.onInputModeOrResponderChange),
|
||||
name: UITextInputMode.currentInputModeDidChangeNotification,
|
||||
object: nil
|
||||
)
|
||||
NotificationCenter.default.addObserver(
|
||||
self,
|
||||
selector: #selector(self.onInputModeOrResponderChange),
|
||||
name: UIResponder.keyboardDidShowNotification,
|
||||
object: nil
|
||||
)
|
||||
}
|
||||
|
||||
OnStopObserving {
|
||||
NotificationCenter.default.removeObserver(
|
||||
self,
|
||||
name: UITextInputMode.currentInputModeDidChangeNotification,
|
||||
object: nil
|
||||
)
|
||||
NotificationCenter.default.removeObserver(
|
||||
self,
|
||||
name: UIResponder.keyboardDidShowNotification,
|
||||
object: nil
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private var lastLanguage: String?
|
||||
|
||||
@objc
|
||||
private func onInputModeOrResponderChange() {
|
||||
let language = currentKeyboardLanguage()
|
||||
if language != lastLanguage {
|
||||
lastLanguage = language
|
||||
sendEvent("onKeyboardLanguageChange", [
|
||||
"language": language as Any
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
private func currentKeyboardLanguage() -> String? {
|
||||
let keyWindow = UIApplication.shared.connectedScenes
|
||||
.compactMap { $0 as? UIWindowScene }
|
||||
.flatMap { $0.windows }
|
||||
.first { $0.isKeyWindow }
|
||||
|
||||
return keyWindow?.findFirstResponder()?.textInputMode?.primaryLanguage
|
||||
}
|
||||
}
|
||||
|
||||
private extension UIView {
|
||||
func findFirstResponder() -> UIView? {
|
||||
if isFirstResponder { return self }
|
||||
for subview in subviews {
|
||||
if let found = subview.findFirstResponder() {
|
||||
return found
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import {type EventSubscription} from 'expo-modules-core'
|
||||
import {requireNativeModule} from 'expo-modules-core'
|
||||
|
||||
type ExpoKeyboardLanguageModule = {
|
||||
getCurrentKeyboardLanguage(): string | null
|
||||
addListener(
|
||||
eventName: 'onKeyboardLanguageChange',
|
||||
listener: (event: {language: string | null}) => void,
|
||||
): EventSubscription
|
||||
}
|
||||
|
||||
const NativeModule = requireNativeModule<ExpoKeyboardLanguageModule>(
|
||||
'ExpoKeyboardLanguage',
|
||||
)
|
||||
|
||||
export function getCurrentKeyboardLanguage(): string | null {
|
||||
return NativeModule.getCurrentKeyboardLanguage()
|
||||
}
|
||||
|
||||
export function addKeyboardLanguageListener(
|
||||
cb: (language: string | null) => void,
|
||||
): EventSubscription {
|
||||
return NativeModule.addListener('onKeyboardLanguageChange', event => {
|
||||
cb(event.language)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import {type EventSubscription} from 'expo-modules-core'
|
||||
|
||||
export function getCurrentKeyboardLanguage(): string | null {
|
||||
return null
|
||||
}
|
||||
|
||||
export function addKeyboardLanguageListener(
|
||||
_cb: (language: string | null) => void,
|
||||
): EventSubscription {
|
||||
return {remove() {}}
|
||||
}
|
||||
Reference in New Issue
Block a user