From 1796d3074778ae3070d842977ea60f6c558ad063 Mon Sep 17 00:00:00 2001 From: TIANLI0 <507249007@qq.com> Date: Sun, 8 Mar 2026 14:53:35 +0800 Subject: [PATCH 1/9] feat: add locale provider and storage permission service - Implemented LocaleProvider to manage app localization settings, including loading and setting locale codes. - Created StoragePermissionService to handle storage permission requests with user dialogs. - Updated pubspec.lock with new dependencies. --- flutter_app/.flutter-plugins-dependencies | 1 + flutter_app/android/app/build.gradle | 40 +- .../plugins/GeneratedPluginRegistrant.java | 64 ++ .../com/nxg/openclawproot/MainActivity.kt | 25 - flutter_app/android/build.gradle | 2 +- flutter_app/android/gradle.properties | 1 + .../android/gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 53636 bytes .../gradle/wrapper/gradle-wrapper.properties | 2 +- flutter_app/android/gradlew | 160 +++ flutter_app/android/gradlew.bat | 90 ++ flutter_app/android/settings.gradle | 4 +- flutter_app/lib/app.dart | 51 +- flutter_app/lib/l10n/app_localizations.dart | 528 ++++++++++ .../lib/providers/locale_provider.dart | 43 + flutter_app/lib/screens/configure_screen.dart | 94 +- flutter_app/lib/screens/dashboard_screen.dart | 83 +- flutter_app/lib/screens/logs_screen.dart | 111 ++- flutter_app/lib/screens/node_screen.dart | 184 ++-- .../lib/screens/onboarding_screen.dart | 121 ++- flutter_app/lib/screens/packages_screen.dart | 48 +- .../lib/screens/provider_detail_screen.dart | 2 +- flutter_app/lib/screens/settings_screen.dart | 210 ++-- .../lib/screens/setup_wizard_screen.dart | 213 +++- .../lib/services/preferences_service.dart | 11 + .../services/storage_permission_service.dart | 46 + flutter_app/lib/widgets/gateway_controls.dart | 38 +- flutter_app/lib/widgets/node_controls.dart | 38 +- flutter_app/pubspec.lock | 943 ++++++++++++++++++ flutter_app/pubspec.yaml | 12 +- scripts/fetch-proot-binaries.ps1 | 143 +++ 30 files changed, 2872 insertions(+), 436 deletions(-) create mode 100644 flutter_app/.flutter-plugins-dependencies create mode 100644 flutter_app/android/app/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java create mode 100644 flutter_app/android/gradle/wrapper/gradle-wrapper.jar create mode 100644 flutter_app/android/gradlew create mode 100644 flutter_app/android/gradlew.bat create mode 100644 flutter_app/lib/l10n/app_localizations.dart create mode 100644 flutter_app/lib/providers/locale_provider.dart create mode 100644 flutter_app/lib/services/storage_permission_service.dart create mode 100644 flutter_app/pubspec.lock create mode 100644 scripts/fetch-proot-binaries.ps1 diff --git a/flutter_app/.flutter-plugins-dependencies b/flutter_app/.flutter-plugins-dependencies new file mode 100644 index 0000000..d06f71b --- /dev/null +++ b/flutter_app/.flutter-plugins-dependencies @@ -0,0 +1 @@ +{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"camera_avfoundation","path":"C:\\\\Users\\\\tian5\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\camera_avfoundation-0.10.1\\\\","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"flutter_pty","path":"C:\\\\Users\\\\tian5\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\flutter_pty-0.4.2\\\\","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"geolocator_apple","path":"C:\\\\Users\\\\tian5\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\geolocator_apple-2.3.13\\\\","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false},{"name":"package_info_plus","path":"C:\\\\Users\\\\tian5\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\package_info_plus-9.0.0\\\\","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"path_provider_foundation","path":"C:\\\\Users\\\\tian5\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\path_provider_foundation-2.6.0\\\\","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"permission_handler_apple","path":"C:\\\\Users\\\\tian5\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\permission_handler_apple-9.4.7\\\\","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"shared_preferences_foundation","path":"C:\\\\Users\\\\tian5\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\shared_preferences_foundation-2.5.6\\\\","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false},{"name":"url_launcher_ios","path":"C:\\\\Users\\\\tian5\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\url_launcher_ios-6.4.1\\\\","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"webview_flutter_wkwebview","path":"C:\\\\Users\\\\tian5\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\webview_flutter_wkwebview-3.23.8\\\\","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false}],"android":[{"name":"camera_android_camerax","path":"C:\\\\Users\\\\tian5\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\camera_android_camerax-0.7.1\\\\","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"flutter_plugin_android_lifecycle","path":"C:\\\\Users\\\\tian5\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\flutter_plugin_android_lifecycle-2.0.33\\\\","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"flutter_pty","path":"C:\\\\Users\\\\tian5\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\flutter_pty-0.4.2\\\\","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"geolocator_android","path":"C:\\\\Users\\\\tian5\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\geolocator_android-5.0.2\\\\","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"package_info_plus","path":"C:\\\\Users\\\\tian5\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\package_info_plus-9.0.0\\\\","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"path_provider_android","path":"C:\\\\Users\\\\tian5\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\path_provider_android-2.2.22\\\\","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"permission_handler_android","path":"C:\\\\Users\\\\tian5\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\permission_handler_android-13.0.1\\\\","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"shared_preferences_android","path":"C:\\\\Users\\\\tian5\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\shared_preferences_android-2.4.21\\\\","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"url_launcher_android","path":"C:\\\\Users\\\\tian5\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\url_launcher_android-6.3.28\\\\","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"webview_flutter_android","path":"C:\\\\Users\\\\tian5\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\webview_flutter_android-4.10.13\\\\","native_build":true,"dependencies":[],"dev_dependency":false}],"macos":[{"name":"flutter_pty","path":"C:\\\\Users\\\\tian5\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\flutter_pty-0.4.2\\\\","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"geolocator_apple","path":"C:\\\\Users\\\\tian5\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\geolocator_apple-2.3.13\\\\","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false},{"name":"package_info_plus","path":"C:\\\\Users\\\\tian5\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\package_info_plus-9.0.0\\\\","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"path_provider_foundation","path":"C:\\\\Users\\\\tian5\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\path_provider_foundation-2.6.0\\\\","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"shared_preferences_foundation","path":"C:\\\\Users\\\\tian5\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\shared_preferences_foundation-2.5.6\\\\","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false},{"name":"url_launcher_macos","path":"C:\\\\Users\\\\tian5\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\url_launcher_macos-3.2.5\\\\","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"webview_flutter_wkwebview","path":"C:\\\\Users\\\\tian5\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\webview_flutter_wkwebview-3.23.8\\\\","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false}],"linux":[{"name":"flutter_pty","path":"C:\\\\Users\\\\tian5\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\flutter_pty-0.4.2\\\\","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"geolocator_linux","path":"C:\\\\Users\\\\tian5\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\geolocator_linux-0.2.4\\\\","native_build":false,"dependencies":["package_info_plus"],"dev_dependency":false},{"name":"package_info_plus","path":"C:\\\\Users\\\\tian5\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\package_info_plus-9.0.0\\\\","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"path_provider_linux","path":"C:\\\\Users\\\\tian5\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\path_provider_linux-2.2.1\\\\","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"shared_preferences_linux","path":"C:\\\\Users\\\\tian5\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\shared_preferences_linux-2.4.1\\\\","native_build":false,"dependencies":["path_provider_linux"],"dev_dependency":false},{"name":"url_launcher_linux","path":"C:\\\\Users\\\\tian5\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\url_launcher_linux-3.2.2\\\\","native_build":true,"dependencies":[],"dev_dependency":false}],"windows":[{"name":"flutter_pty","path":"C:\\\\Users\\\\tian5\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\flutter_pty-0.4.2\\\\","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"geolocator_windows","path":"C:\\\\Users\\\\tian5\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\geolocator_windows-0.2.5\\\\","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"package_info_plus","path":"C:\\\\Users\\\\tian5\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\package_info_plus-9.0.0\\\\","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"path_provider_windows","path":"C:\\\\Users\\\\tian5\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\path_provider_windows-2.3.0\\\\","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"permission_handler_windows","path":"C:\\\\Users\\\\tian5\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\permission_handler_windows-0.2.1\\\\","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"shared_preferences_windows","path":"C:\\\\Users\\\\tian5\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\shared_preferences_windows-2.4.1\\\\","native_build":false,"dependencies":["path_provider_windows"],"dev_dependency":false},{"name":"url_launcher_windows","path":"C:\\\\Users\\\\tian5\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\url_launcher_windows-3.1.5\\\\","native_build":true,"dependencies":[],"dev_dependency":false}],"web":[{"name":"camera_web","path":"C:\\\\Users\\\\tian5\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\camera_web-0.3.5+3\\\\","dependencies":[],"dev_dependency":false},{"name":"geolocator_web","path":"C:\\\\Users\\\\tian5\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\geolocator_web-4.1.3\\\\","dependencies":[],"dev_dependency":false},{"name":"package_info_plus","path":"C:\\\\Users\\\\tian5\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\package_info_plus-9.0.0\\\\","dependencies":[],"dev_dependency":false},{"name":"permission_handler_html","path":"C:\\\\Users\\\\tian5\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\permission_handler_html-0.1.3+5\\\\","dependencies":[],"dev_dependency":false},{"name":"shared_preferences_web","path":"C:\\\\Users\\\\tian5\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\shared_preferences_web-2.4.3\\\\","dependencies":[],"dev_dependency":false},{"name":"url_launcher_web","path":"C:\\\\Users\\\\tian5\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\url_launcher_web-2.4.2\\\\","dependencies":[],"dev_dependency":false}]},"dependencyGraph":[{"name":"camera","dependencies":["camera_android_camerax","camera_avfoundation","camera_web","flutter_plugin_android_lifecycle"]},{"name":"camera_android_camerax","dependencies":[]},{"name":"camera_avfoundation","dependencies":[]},{"name":"camera_web","dependencies":[]},{"name":"flutter_plugin_android_lifecycle","dependencies":[]},{"name":"flutter_pty","dependencies":[]},{"name":"geolocator","dependencies":["geolocator_android","geolocator_apple","geolocator_web","geolocator_windows","geolocator_linux"]},{"name":"geolocator_android","dependencies":[]},{"name":"geolocator_apple","dependencies":[]},{"name":"geolocator_linux","dependencies":["package_info_plus"]},{"name":"geolocator_web","dependencies":[]},{"name":"geolocator_windows","dependencies":[]},{"name":"package_info_plus","dependencies":[]},{"name":"path_provider","dependencies":["path_provider_android","path_provider_foundation","path_provider_linux","path_provider_windows"]},{"name":"path_provider_android","dependencies":[]},{"name":"path_provider_foundation","dependencies":[]},{"name":"path_provider_linux","dependencies":[]},{"name":"path_provider_windows","dependencies":[]},{"name":"permission_handler","dependencies":["permission_handler_android","permission_handler_apple","permission_handler_html","permission_handler_windows"]},{"name":"permission_handler_android","dependencies":[]},{"name":"permission_handler_apple","dependencies":[]},{"name":"permission_handler_html","dependencies":[]},{"name":"permission_handler_windows","dependencies":[]},{"name":"shared_preferences","dependencies":["shared_preferences_android","shared_preferences_foundation","shared_preferences_linux","shared_preferences_web","shared_preferences_windows"]},{"name":"shared_preferences_android","dependencies":[]},{"name":"shared_preferences_foundation","dependencies":[]},{"name":"shared_preferences_linux","dependencies":["path_provider_linux"]},{"name":"shared_preferences_web","dependencies":[]},{"name":"shared_preferences_windows","dependencies":["path_provider_windows"]},{"name":"url_launcher","dependencies":["url_launcher_android","url_launcher_ios","url_launcher_linux","url_launcher_macos","url_launcher_web","url_launcher_windows"]},{"name":"url_launcher_android","dependencies":[]},{"name":"url_launcher_ios","dependencies":[]},{"name":"url_launcher_linux","dependencies":[]},{"name":"url_launcher_macos","dependencies":[]},{"name":"url_launcher_web","dependencies":[]},{"name":"url_launcher_windows","dependencies":[]},{"name":"webview_flutter","dependencies":["webview_flutter_android","webview_flutter_wkwebview"]},{"name":"webview_flutter_android","dependencies":[]},{"name":"webview_flutter_wkwebview","dependencies":[]}],"date_created":"2026-03-08 14:01:41.596085","version":"3.41.4","swift_package_manager_enabled":{"ios":false,"macos":false}} \ No newline at end of file diff --git a/flutter_app/android/app/build.gradle b/flutter_app/android/app/build.gradle index fd928c9..7fac76f 100644 --- a/flutter_app/android/app/build.gradle +++ b/flutter_app/android/app/build.gradle @@ -33,18 +33,50 @@ if (keystorePropertiesFile.exists()) { def hasKeystore = keystoreProperties.containsKey('storeFile') && file(keystoreProperties['storeFile']).exists() +def requiredProotLibs = [ + 'arm64-v8a/libproot.so', + 'arm64-v8a/libprootloader.so', + 'arm64-v8a/libtalloc.so', + 'armeabi-v7a/libproot.so', + 'armeabi-v7a/libprootloader.so', + 'armeabi-v7a/libtalloc.so', + 'x86_64/libproot.so', + 'x86_64/libprootloader.so', + 'x86_64/libtalloc.so', +] + +tasks.register('verifyProotBinaries') { + doLast { + def baseDir = file('src/main/jniLibs') + def missing = requiredProotLibs.findAll { relativePath -> + !new File(baseDir, relativePath).exists() + } + + if (!missing.isEmpty()) { + throw new GradleException( + "Missing PRoot native binaries: ${missing.join(', ')}. " + + "Run scripts/fetch-proot-binaries.sh on Unix or scripts/fetch-proot-binaries.ps1 on Windows before building." + ) + } + } +} + +tasks.named('preBuild') { + dependsOn 'verifyProotBinaries' +} + android { namespace = "com.nxg.openclawproot" - compileSdk = 35 + compileSdk = 36 ndkVersion = flutter.ndkVersion compileOptions { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 } kotlinOptions { - jvmTarget = JavaVersion.VERSION_1_8 + jvmTarget = "17" } defaultConfig { diff --git a/flutter_app/android/app/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java b/flutter_app/android/app/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java new file mode 100644 index 0000000..dcec92e --- /dev/null +++ b/flutter_app/android/app/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java @@ -0,0 +1,64 @@ +package io.flutter.plugins; + +import androidx.annotation.Keep; +import androidx.annotation.NonNull; +import io.flutter.Log; + +import io.flutter.embedding.engine.FlutterEngine; + +/** + * Generated file. Do not edit. + * This file is generated by the Flutter tool based on the + * plugins that support the Android platform. + */ +@Keep +public final class GeneratedPluginRegistrant { + private static final String TAG = "GeneratedPluginRegistrant"; + public static void registerWith(@NonNull FlutterEngine flutterEngine) { + try { + flutterEngine.getPlugins().add(new io.flutter.plugins.camerax.CameraAndroidCameraxPlugin()); + } catch (Exception e) { + Log.e(TAG, "Error registering plugin camera_android_camerax, io.flutter.plugins.camerax.CameraAndroidCameraxPlugin", e); + } + try { + flutterEngine.getPlugins().add(new io.flutter.plugins.flutter_plugin_android_lifecycle.FlutterAndroidLifecyclePlugin()); + } catch (Exception e) { + Log.e(TAG, "Error registering plugin flutter_plugin_android_lifecycle, io.flutter.plugins.flutter_plugin_android_lifecycle.FlutterAndroidLifecyclePlugin", e); + } + try { + flutterEngine.getPlugins().add(new com.baseflow.geolocator.GeolocatorPlugin()); + } catch (Exception e) { + Log.e(TAG, "Error registering plugin geolocator_android, com.baseflow.geolocator.GeolocatorPlugin", e); + } + try { + flutterEngine.getPlugins().add(new dev.fluttercommunity.plus.packageinfo.PackageInfoPlugin()); + } catch (Exception e) { + Log.e(TAG, "Error registering plugin package_info_plus, dev.fluttercommunity.plus.packageinfo.PackageInfoPlugin", e); + } + try { + flutterEngine.getPlugins().add(new io.flutter.plugins.pathprovider.PathProviderPlugin()); + } catch (Exception e) { + Log.e(TAG, "Error registering plugin path_provider_android, io.flutter.plugins.pathprovider.PathProviderPlugin", e); + } + try { + flutterEngine.getPlugins().add(new com.baseflow.permissionhandler.PermissionHandlerPlugin()); + } catch (Exception e) { + Log.e(TAG, "Error registering plugin permission_handler_android, com.baseflow.permissionhandler.PermissionHandlerPlugin", e); + } + try { + flutterEngine.getPlugins().add(new io.flutter.plugins.sharedpreferences.SharedPreferencesPlugin()); + } catch (Exception e) { + Log.e(TAG, "Error registering plugin shared_preferences_android, io.flutter.plugins.sharedpreferences.SharedPreferencesPlugin", e); + } + try { + flutterEngine.getPlugins().add(new io.flutter.plugins.urllauncher.UrlLauncherPlugin()); + } catch (Exception e) { + Log.e(TAG, "Error registering plugin url_launcher_android, io.flutter.plugins.urllauncher.UrlLauncherPlugin", e); + } + try { + flutterEngine.getPlugins().add(new io.flutter.plugins.webviewflutter.WebViewFlutterPlugin()); + } catch (Exception e) { + Log.e(TAG, "Error registering plugin webview_flutter_android, io.flutter.plugins.webviewflutter.WebViewFlutterPlugin", e); + } + } +} diff --git a/flutter_app/android/app/src/main/kotlin/com/nxg/openclawproot/MainActivity.kt b/flutter_app/android/app/src/main/kotlin/com/nxg/openclawproot/MainActivity.kt index 708b09f..319bb3c 100644 --- a/flutter_app/android/app/src/main/kotlin/com/nxg/openclawproot/MainActivity.kt +++ b/flutter_app/android/app/src/main/kotlin/com/nxg/openclawproot/MainActivity.kt @@ -530,7 +530,6 @@ class MainActivity : FlutterActivity() { createUrlNotificationChannel() requestNotificationPermission() - requestStoragePermissionOnLaunch() EventChannel(flutterEngine.dartExecutor.binaryMessenger, EVENT_CHANNEL).setStreamHandler( object : EventChannel.StreamHandler { @@ -558,30 +557,6 @@ class MainActivity : FlutterActivity() { } } - private fun requestStoragePermissionOnLaunch() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - if (!Environment.isExternalStorageManager()) { - try { - val intent = Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION) - startActivity(intent) - } catch (_: Exception) {} - } - } else { - if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) - != PackageManager.PERMISSION_GRANTED - ) { - ActivityCompat.requestPermissions( - this, - arrayOf( - Manifest.permission.READ_EXTERNAL_STORAGE, - Manifest.permission.WRITE_EXTERNAL_STORAGE - ), - STORAGE_PERMISSION_REQUEST - ) - } - } - } - private fun createUrlNotificationChannel() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { val channel = NotificationChannel( diff --git a/flutter_app/android/build.gradle b/flutter_app/android/build.gradle index dfd81c4..ecd72c5 100644 --- a/flutter_app/android/build.gradle +++ b/flutter_app/android/build.gradle @@ -13,7 +13,7 @@ subprojects { // references flutter.compileSdkVersion before the Flutter tooling injects it. if (project.name != "app") { project.ext.set("flutter", [ - compileSdkVersion: 35, + compileSdkVersion: 36, minSdkVersion: 29, targetSdkVersion: 36 ]) diff --git a/flutter_app/android/gradle.properties b/flutter_app/android/gradle.properties index 2597170..86c765c 100644 --- a/flutter_app/android/gradle.properties +++ b/flutter_app/android/gradle.properties @@ -1,3 +1,4 @@ org.gradle.jvmargs=-Xmx4G -XX:MaxMetaspaceSize=2G -XX:+HeapDumpOnOutOfMemoryError android.useAndroidX=true android.enableJetifier=true +kotlin.incremental=false diff --git a/flutter_app/android/gradle/wrapper/gradle-wrapper.jar b/flutter_app/android/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..13372aef5e24af05341d49695ee84e5f9b594659 GIT binary patch literal 53636 zcmafaW0a=B^559DjdyHo$F^PVt zzd|cWgMz^T0YO0lQ8%TE1O06v|NZl~LH{LLQ58WtNjWhFP#}eWVO&eiP!jmdp!%24 z{&z-MK{-h=QDqf+S+Pgi=_wg$I{F28X*%lJ>A7Yl#$}fMhymMu?R9TEB?#6@|Q^e^AHhxcRL$z1gsc`-Q`3j+eYAd<4@z^{+?JM8bmu zSVlrVZ5-)SzLn&LU9GhXYG{{I+u(+6ES+tAtQUanYC0^6kWkks8cG;C&r1KGs)Cq}WZSd3k1c?lkzwLySimkP5z)T2Ox3pNs;PdQ=8JPDkT7#0L!cV? zzn${PZs;o7UjcCVd&DCDpFJvjI=h(KDmdByJuDYXQ|G@u4^Kf?7YkE67fWM97kj6F z973tGtv!k$k{<>jd~D&c(x5hVbJa`bILdy(00%lY5}HZ2N>)a|))3UZ&fUa5@uB`H z+LrYm@~t?g`9~@dFzW5l>=p0hG%rv0>(S}jEzqQg6-jImG%Pr%HPtqIV_Ym6yRydW z4L+)NhcyYp*g#vLH{1lK-hQQSScfvNiNx|?nSn-?cc8}-9~Z_0oxlr~(b^EiD`Mx< zlOLK)MH?nl4dD|hx!jBCIku-lI(&v~bCU#!L7d0{)h z;k4y^X+=#XarKzK*)lv0d6?kE1< zmCG^yDYrSwrKIn04tG)>>10%+ zEKzs$S*Zrl+GeE55f)QjY$ zD5hi~J17k;4VSF_`{lPFwf^Qroqg%kqM+Pdn%h#oOPIsOIwu?JR717atg~!)*CgXk zERAW?c}(66rnI+LqM^l7BW|9dH~5g1(_w$;+AAzSYlqop*=u5}=g^e0xjlWy0cUIT7{Fs2Xqx*8% zW71JB%hk%aV-wjNE0*$;E-S9hRx5|`L2JXxz4TX3nf8fMAn|523ssV;2&145zh{$V z#4lt)vL2%DCZUgDSq>)ei2I`*aeNXHXL1TB zC8I4!uq=YYVjAdcCjcf4XgK2_$y5mgsCdcn2U!VPljXHco>+%`)6W=gzJk0$e%m$xWUCs&Ju-nUJjyQ04QF_moED2(y6q4l+~fo845xm zE5Esx?~o#$;rzpCUk2^2$c3EBRNY?wO(F3Pb+<;qfq;JhMFuSYSxiMejBQ+l8(C-- zz?Xufw@7{qvh$;QM0*9tiO$nW(L>83egxc=1@=9Z3)G^+*JX-z92F((wYiK>f;6 zkc&L6k4Ua~FFp`x7EF;ef{hb*n8kx#LU|6{5n=A55R4Ik#sX{-nuQ}m7e<{pXq~8#$`~6| zi{+MIgsBRR-o{>)CE8t0Bq$|SF`M0$$7-{JqwFI1)M^!GMwq5RAWMP!o6G~%EG>$S zYDS?ux;VHhRSm*b^^JukYPVb?t0O%^&s(E7Rb#TnsWGS2#FdTRj_SR~YGjkaRFDI=d)+bw$rD;_!7&P2WEmn zIqdERAbL&7`iA^d?8thJ{(=)v>DgTF7rK-rck({PpYY$7uNY$9-Z< ze4=??I#p;$*+-Tm!q8z}k^%-gTm59^3$*ByyroqUe02Dne4?Fc%JlO>*f9Zj{++!^ zBz0FxuS&7X52o6-^CYq>jkXa?EEIfh?xdBPAkgpWpb9Tam^SXoFb3IRfLwanWfskJ zIbfU-rJ1zPmOV)|%;&NSWIEbbwj}5DIuN}!m7v4($I{Rh@<~-sK{fT|Wh?<|;)-Z; zwP{t@{uTsmnO@5ZY82lzwl4jeZ*zsZ7w%a+VtQXkigW$zN$QZnKw4F`RG`=@eWowO zFJ6RC4e>Y7Nu*J?E1*4*U0x^>GK$>O1S~gkA)`wU2isq^0nDb`);Q(FY<8V6^2R%= zDY}j+?mSj{bz2>F;^6S=OLqiHBy~7h4VVscgR#GILP!zkn68S^c04ZL3e$lnSU_(F zZm3e`1~?eu1>ys#R6>Gu$`rWZJG&#dsZ?^)4)v(?{NPt+_^Ak>Ap6828Cv^B84fa4 z_`l$0SSqkBU}`f*H#<14a)khT1Z5Z8;=ga^45{l8y*m|3Z60vgb^3TnuUKaa+zP;m zS`za@C#Y;-LOm&pW||G!wzr+}T~Q9v4U4ufu*fLJC=PajN?zN=?v^8TY}wrEeUygdgwr z7szml+(Bar;w*c^!5txLGKWZftqbZP`o;Kr1)zI}0Kb8yr?p6ZivtYL_KA<+9)XFE z=pLS5U&476PKY2aKEZh}%|Vb%!us(^qf)bKdF7x_v|Qz8lO7Ro>;#mxG0gqMaTudL zi2W!_#3@INslT}1DFJ`TsPvRBBGsODklX0`p-M6Mrgn~6&fF`kdj4K0I$<2Hp(YIA z)fFdgR&=qTl#sEFj6IHzEr1sYM6 zNfi!V!biByA&vAnZd;e_UfGg_={}Tj0MRt3SG%BQYnX$jndLG6>ssgIV{T3#=;RI% zE}b!9z#fek19#&nFgC->@!IJ*Fe8K$ZOLmg|6(g}ccsSBpc`)3;Ar8;3_k`FQ#N9&1tm>c|2mzG!!uWvelm zJj|oDZ6-m(^|dn3em(BF&3n12=hdtlb@%!vGuL*h`CXF?^=IHU%Q8;g8vABm=U!vX zT%Ma6gpKQC2c;@wH+A{)q+?dAuhetSxBDui+Z;S~6%oQq*IwSMu-UhMDy{pP z-#GB-a0`0+cJ%dZ7v0)3zfW$eV>w*mgU4Cma{P$DY3|w364n$B%cf()fZ;`VIiK_O zQ|q|(55+F$H(?opzr%r)BJLy6M&7Oq8KCsh`pA5^ohB@CDlMKoDVo5gO&{0k)R0b(UOfd>-(GZGeF}y?QI_T+GzdY$G{l!l% zHyToqa-x&X4;^(-56Lg$?(KYkgJn9W=w##)&CECqIxLe@+)2RhO*-Inpb7zd8txFG6mY8E?N8JP!kRt_7-&X{5P?$LAbafb$+hkA*_MfarZxf zXLpXmndnV3ubbXe*SYsx=eeuBKcDZI0bg&LL-a8f9>T(?VyrpC6;T{)Z{&|D5a`Aa zjP&lP)D)^YYWHbjYB6ArVs+4xvrUd1@f;;>*l zZH``*BxW+>Dd$be{`<&GN(w+m3B?~3Jjz}gB8^|!>pyZo;#0SOqWem%xeltYZ}KxOp&dS=bg|4 zY-^F~fv8v}u<7kvaZH`M$fBeltAglH@-SQres30fHC%9spF8Ld%4mjZJDeGNJR8+* zl&3Yo$|JYr2zi9deF2jzEC) zl+?io*GUGRp;^z+4?8gOFA>n;h%TJC#-st7#r&-JVeFM57P7rn{&k*z@+Y5 zc2sui8(gFATezp|Te|1-Q*e|Xi+__8bh$>%3|xNc2kAwTM!;;|KF6cS)X3SaO8^z8 zs5jV(s(4_NhWBSSJ}qUzjuYMKlkjbJS!7_)wwVsK^qDzHx1u*sC@C1ERqC#l%a zk>z>m@sZK{#GmsB_NkEM$$q@kBrgq%=NRBhL#hjDQHrI7(XPgFvP&~ZBJ@r58nLme zK4tD}Nz6xrbvbD6DaDC9E_82T{(WRQBpFc+Zb&W~jHf1MiBEqd57}Tpo8tOXj@LcF zwN8L-s}UO8%6piEtTrj@4bLH!mGpl5mH(UJR1r9bBOrSt0tSJDQ9oIjcW#elyMAxl7W^V(>8M~ss0^>OKvf{&oUG@uW{f^PtV#JDOx^APQKm& z{*Ysrz&ugt4PBUX@KERQbycxP%D+ApR%6jCx7%1RG2YpIa0~tqS6Xw6k#UN$b`^l6d$!I z*>%#Eg=n#VqWnW~MurJLK|hOQPTSy7G@29g@|g;mXC%MF1O7IAS8J^Q6D&Ra!h^+L&(IBYg2WWzZjT-rUsJMFh@E)g)YPW_)W9GF3 zMZz4RK;qcjpnat&J;|MShuPc4qAc)A| zVB?h~3TX+k#Cmry90=kdDoPYbhzs#z96}#M=Q0nC{`s{3ZLU)c(mqQQX;l~1$nf^c zFRQ~}0_!cM2;Pr6q_(>VqoW0;9=ZW)KSgV-c_-XdzEapeLySavTs5-PBsl-n3l;1jD z9^$^xR_QKDUYoeqva|O-+8@+e??(pRg@V|=WtkY!_IwTN~ z9Rd&##eWt_1w$7LL1$-ETciKFyHnNPjd9hHzgJh$J(D@3oYz}}jVNPjH!viX0g|Y9 zDD`Zjd6+o+dbAbUA( zEqA9mSoX5p|9sDVaRBFx_8)Ra4HD#xDB(fa4O8_J2`h#j17tSZOd3%}q8*176Y#ak zC?V8Ol<*X{Q?9j{Ys4Bc#sq!H;^HU$&F_`q2%`^=9DP9YV-A!ZeQ@#p=#ArloIgUH%Y-s>G!%V3aoXaY=f<UBrJTN+*8_lMX$yC=Vq+ zrjLn-pO%+VIvb~>k%`$^aJ1SevcPUo;V{CUqF>>+$c(MXxU12mxqyFAP>ki{5#;Q0 zx7Hh2zZdZzoxPY^YqI*Vgr)ip0xnpQJ+~R*UyFi9RbFd?<_l8GH@}gGmdB)~V7vHg z>Cjy78TQTDwh~+$u$|K3if-^4uY^|JQ+rLVX=u7~bLY29{lr>jWV7QCO5D0I>_1?; zx>*PxE4|wC?#;!#cK|6ivMzJ({k3bT_L3dHY#h7M!ChyTT`P#%3b=k}P(;QYTdrbe z+e{f@we?3$66%02q8p3;^th;9@y2vqt@LRz!DO(WMIk?#Pba85D!n=Ao$5NW0QVgS zoW)fa45>RkjU?H2SZ^#``zs6dG@QWj;MO4k6tIp8ZPminF`rY31dzv^e-3W`ZgN#7 z)N^%Rx?jX&?!5v`hb0-$22Fl&UBV?~cV*{hPG6%ml{k;m+a-D^XOF6DxPd$3;2VVY zT)E%m#ZrF=D=84$l}71DK3Vq^?N4``cdWn3 zqV=mX1(s`eCCj~#Nw4XMGW9tK>$?=cd$ule0Ir8UYzhi?%_u0S?c&j7)-~4LdolkgP^CUeE<2`3m)I^b ztV`K0k$OS^-GK0M0cNTLR22Y_eeT{<;G(+51Xx}b6f!kD&E4; z&Op8;?O<4D$t8PB4#=cWV9Q*i4U+8Bjlj!y4`j)^RNU#<5La6|fa4wLD!b6?RrBsF z@R8Nc^aO8ty7qzlOLRL|RUC-Bt-9>-g`2;@jfNhWAYciF{df9$n#a~28+x~@x0IWM zld=J%YjoKm%6Ea>iF){z#|~fo_w#=&&HRogJmXJDjCp&##oVvMn9iB~gyBlNO3B5f zXgp_1I~^`A0z_~oAa_YBbNZbDsnxLTy0@kkH!=(xt8|{$y<+|(wSZW7@)#|fs_?gU5-o%vpsQPRjIxq;AED^oG%4S%`WR}2(*!84Pe8Jw(snJ zq~#T7+m|w#acH1o%e<+f;!C|*&_!lL*^zRS`;E}AHh%cj1yR&3Grv&0I9k9v0*w8^ zXHEyRyCB`pDBRAxl;ockOh6$|7i$kzCBW$}wGUc|2bo3`x*7>B@eI=-7lKvI)P=gQ zf_GuA+36kQb$&{ZH)6o^x}wS}S^d&Xmftj%nIU=>&j@0?z8V3PLb1JXgHLq)^cTvB zFO6(yj1fl1Bap^}?hh<>j?Jv>RJdK{YpGjHxnY%d8x>A{k+(18J|R}%mAqq9Uzm8^Us#Ir_q^w9-S?W07YRD`w%D(n;|8N%_^RO`zp4 z@`zMAs>*x0keyE)$dJ8hR37_&MsSUMlGC*=7|wUehhKO)C85qoU}j>VVklO^TxK?! zO!RG~y4lv#W=Jr%B#sqc;HjhN={wx761vA3_$S>{j+r?{5=n3le|WLJ(2y_r>{)F_ z=v8Eo&xFR~wkw5v-{+9^JQukxf8*CXDWX*ZzjPVDc>S72uxAcY+(jtg3ns_5R zRYl2pz`B)h+e=|7SfiAAP;A zk0tR)3u1qy0{+?bQOa17SpBRZ5LRHz(TQ@L0%n5xJ21ri>^X420II1?5^FN3&bV?( zCeA)d9!3FAhep;p3?wLPs`>b5Cd}N!;}y`Hq3ppDs0+><{2ey0yq8o7m-4|oaMsWf zsLrG*aMh91drd-_QdX6t&I}t2!`-7$DCR`W2yoV%bcugue)@!SXM}fJOfG(bQQh++ zjAtF~zO#pFz})d8h)1=uhigDuFy`n*sbxZ$BA^Bt=Jdm}_KB6sCvY(T!MQnqO;TJs zVD{*F(FW=+v`6t^6{z<3-fx#|Ze~#h+ymBL^^GKS%Ve<)sP^<4*y_Y${06eD zH_n?Ani5Gs4&1z)UCL-uBvq(8)i!E@T_*0Sp5{Ddlpgke^_$gukJc_f9e=0Rfpta@ ze5~~aJBNK&OJSw!(rDRAHV0d+eW#1?PFbr==uG-$_fu8`!DWqQD~ef-Gx*ZmZx33_ zb0+I(0!hIK>r9_S5A*UwgRBKSd6!ieiYJHRigU@cogJ~FvJHY^DSysg)ac=7#wDBf zNLl!E$AiUMZC%%i5@g$WsN+sMSoUADKZ}-Pb`{7{S>3U%ry~?GVX!BDar2dJHLY|g zTJRo#Bs|u#8ke<3ohL2EFI*n6adobnYG?F3-#7eZZQO{#rmM8*PFycBR^UZKJWr(a z8cex$DPOx_PL^TO<%+f^L6#tdB8S^y#+fb|acQfD(9WgA+cb15L+LUdHKv)wE6={i zX^iY3N#U7QahohDP{g`IHS?D00eJC9DIx0V&nq!1T* z4$Bb?trvEG9JixrrNRKcjX)?KWR#Y(dh#re_<y*=5!J+-Wwb*D>jKXgr5L8_b6pvSAn3RIvI5oj!XF^m?otNA=t^dg z#V=L0@W)n?4Y@}49}YxQS=v5GsIF3%Cp#fFYm0Bm<}ey& zOfWB^vS8ye?n;%yD%NF8DvOpZqlB++#4KnUj>3%*S(c#yACIU>TyBG!GQl7{b8j#V z;lS})mrRtT!IRh2B-*T58%9;!X}W^mg;K&fb7?2#JH>JpCZV5jbDfOgOlc@wNLfHN z8O92GeBRjCP6Q9^Euw-*i&Wu=$>$;8Cktx52b{&Y^Ise-R1gTKRB9m0*Gze>$k?$N zua_0Hmbcj8qQy{ZyJ%`6v6F+yBGm>chZxCGpeL@os+v&5LON7;$tb~MQAbSZKG$k z8w`Mzn=cX4Hf~09q8_|3C7KnoM1^ZGU}#=vn1?1^Kc-eWv4x^T<|i9bCu;+lTQKr- zRwbRK!&XrWRoO7Kw!$zNQb#cJ1`iugR(f_vgmu!O)6tFH-0fOSBk6$^y+R07&&B!(V#ZV)CX42( zTC(jF&b@xu40fyb1=_2;Q|uPso&Gv9OSM1HR{iGPi@JUvmYM;rkv#JiJZ5-EFA%Lu zf;wAmbyclUM*D7>^nPatbGr%2aR5j55qSR$hR`c?d+z z`qko8Yn%vg)p=H`1o?=b9K0%Blx62gSy)q*8jWPyFmtA2a+E??&P~mT@cBdCsvFw4 zg{xaEyVZ|laq!sqN}mWq^*89$e6%sb6Thof;ml_G#Q6_0-zwf80?O}D0;La25A0C+ z3)w-xesp6?LlzF4V%yA9Ryl_Kq*wMk4eu&)Tqe#tmQJtwq`gI^7FXpToum5HP3@;N zpe4Y!wv5uMHUu`zbdtLys5)(l^C(hFKJ(T)z*PC>7f6ZRR1C#ao;R&_8&&a3)JLh* zOFKz5#F)hJqVAvcR#1)*AWPGmlEKw$sQd)YWdAs_W-ojA?Lm#wCd}uF0^X=?AA#ki zWG6oDQZJ5Tvifdz4xKWfK&_s`V*bM7SVc^=w7-m}jW6U1lQEv_JsW6W(| zkKf>qn^G!EWn~|7{G-&t0C6C%4)N{WRK_PM>4sW8^dDkFM|p&*aBuN%fg(I z^M-49vnMd%=04N95VO+?d#el>LEo^tvnQsMop70lNqq@%cTlht?e+B5L1L9R4R(_6 z!3dCLeGXb+_LiACNiqa^nOELJj%q&F^S+XbmdP}`KAep%TDop{Pz;UDc#P&LtMPgH zy+)P1jdgZQUuwLhV<89V{3*=Iu?u#v;v)LtxoOwV(}0UD@$NCzd=id{UuDdedeEp| z`%Q|Y<6T?kI)P|8c!K0Za&jxPhMSS!T`wlQNlkE(2B*>m{D#`hYYD>cgvsKrlcOcs7;SnVCeBiK6Wfho@*Ym9 zr0zNfrr}0%aOkHd)d%V^OFMI~MJp+Vg-^1HPru3Wvac@-QjLX9Dx}FL(l>Z;CkSvC zOR1MK%T1Edv2(b9$ttz!E7{x4{+uSVGz`uH&)gG`$)Vv0^E#b&JSZp#V)b6~$RWwe zzC3FzI`&`EDK@aKfeqQ4M(IEzDd~DS>GB$~ip2n!S%6sR&7QQ*=Mr(v*v-&07CO%# zMBTaD8-EgW#C6qFPPG1Ph^|0AFs;I+s|+A@WU}%@WbPI$S0+qFR^$gim+Fejs2f!$ z@Xdlb_K1BI;iiOUj`j+gOD%mjq^S~J0cZZwuqfzNH9}|(vvI6VO+9ZDA_(=EAo;( zKKzm`k!s!_sYCGOm)93Skaz+GF7eY@Ra8J$C)`X)`aPKym?7D^SI}Mnef4C@SgIEB z>nONSFl$qd;0gSZhNcRlq9VVHPkbakHlZ1gJ1y9W+@!V$TLpdsbKR-VwZrsSM^wLr zL9ob&JG)QDTaf&R^cnm5T5#*J3(pSpjM5~S1 z@V#E2syvK6wb?&h?{E)CoI~9uA(hST7hx4_6M(7!|BW3TR_9Q zLS{+uPoNgw(aK^?=1rFcDO?xPEk5Sm=|pW%-G2O>YWS^(RT)5EQ2GSl75`b}vRcD2 z|HX(x0#Qv+07*O|vMIV(0?KGjOny#Wa~C8Q(kF^IR8u|hyyfwD&>4lW=)Pa311caC zUk3aLCkAFkcidp@C%vNVLNUa#1ZnA~ZCLrLNp1b8(ndgB(0zy{Mw2M@QXXC{hTxr7 zbipeHI-U$#Kr>H4}+cu$#2fG6DgyWgq{O#8aa)4PoJ^;1z7b6t&zt zPei^>F1%8pcB#1`z`?f0EAe8A2C|}TRhzs*-vN^jf(XNoPN!tONWG=abD^=Lm9D?4 zbq4b(in{eZehKC0lF}`*7CTzAvu(K!eAwDNC#MlL2~&gyFKkhMIF=32gMFLvKsbLY z1d$)VSzc^K&!k#2Q?(f>pXn){C+g?vhQ0ijV^Z}p5#BGrGb%6n>IH-)SA$O)*z3lJ z1rtFlovL`cC*RaVG!p!4qMB+-f5j^1)ALf4Z;2X&ul&L!?`9Vdp@d(%(>O=7ZBV;l z?bbmyPen>!P{TJhSYPmLs759b1Ni1`d$0?&>OhxxqaU|}-?Z2c+}jgZ&vCSaCivx| z-&1gw2Lr<;U-_xzlg}Fa_3NE?o}R-ZRX->__}L$%2ySyiPegbnM{UuADqwDR{C2oS zPuo88%DNfl4xBogn((9j{;*YGE0>2YoL?LrH=o^SaAcgO39Ew|vZ0tyOXb509#6{7 z0<}CptRX5(Z4*}8CqCgpT@HY3Q)CvRz_YE;nf6ZFwEje^;Hkj0b1ESI*8Z@(RQrW4 z35D5;S73>-W$S@|+M~A(vYvX(yvLN(35THo!yT=vw@d(=q8m+sJyZMB7T&>QJ=jkwQVQ07*Am^T980rldC)j}}zf!gq7_z4dZ zHwHB94%D-EB<-^W@9;u|(=X33c(G>q;Tfq1F~-Lltp|+uwVzg?e$M96ndY{Lcou%w zWRkjeE`G*i)Bm*|_7bi+=MPm8by_};`=pG!DSGBP6y}zvV^+#BYx{<>p0DO{j@)(S zxcE`o+gZf8EPv1g3E1c3LIbw+`rO3N+Auz}vn~)cCm^DlEi#|Az$b z2}Pqf#=rxd!W*6HijC|u-4b~jtuQS>7uu{>wm)PY6^S5eo=?M>;tK`=DKXuArZvaU zHk(G??qjKYS9G6Du)#fn+ob=}C1Hj9d?V$_=J41ljM$CaA^xh^XrV-jzi7TR-{{9V zZZI0;aQ9YNEc`q=Xvz;@q$eqL<}+L(>HR$JA4mB6~g*YRSnpo zTofY;u7F~{1Pl=pdsDQx8Gg#|@BdoWo~J~j%DfVlT~JaC)he>he6`C`&@@#?;e(9( zgKcmoidHU$;pi{;VXyE~4>0{kJ>K3Uy6`s*1S--*mM&NY)*eOyy!7?9&osK*AQ~vi z{4qIQs)s#eN6j&0S()cD&aCtV;r>ykvAzd4O-fG^4Bmx2A2U7-kZR5{Qp-R^i4H2yfwC7?9(r3=?oH(~JR4=QMls>auMv*>^^!$}{}R z;#(gP+O;kn4G|totqZGdB~`9yzShMze{+$$?9%LJi>4YIsaPMwiJ{`gocu0U}$Q$vI5oeyKrgzz>!gI+XFt!#n z7vs9Pn`{{5w-@}FJZn?!%EQV!PdA3hw%Xa2#-;X4*B4?`WM;4@bj`R-yoAs_t4!!` zEaY5OrYi`3u3rXdY$2jZdZvufgFwVna?!>#t#DKAD2;U zqpqktqJ)8EPY*w~yj7r~#bNk|PDM>ZS?5F7T5aPFVZrqeX~5_1*zTQ%;xUHe#li?s zJ*5XZVERVfRjwX^s=0<%nXhULK+MdibMjzt%J7#fuh?NXyJ^pqpfG$PFmG!h*opyi zmMONjJY#%dkdRHm$l!DLeBm#_0YCq|x17c1fYJ#5YMpsjrFKyU=y>g5QcTgbDm28X zYL1RK)sn1@XtkGR;tNb}(kg#9L=jNSbJizqAgV-TtK2#?LZXrCIz({ zO^R|`ZDu(d@E7vE}df5`a zNIQRp&mDFbgyDKtyl@J|GcR9!h+_a$za$fnO5Ai9{)d7m@?@qk(RjHwXD}JbKRn|u z=Hy^z2vZ<1Mf{5ihhi9Y9GEG74Wvka;%G61WB*y7;&L>k99;IEH;d8-IR6KV{~(LZ zN7@V~f)+yg7&K~uLvG9MAY+{o+|JX?yf7h9FT%7ZrW7!RekjwgAA4jU$U#>_!ZC|c zA9%tc9nq|>2N1rg9uw-Qc89V}I5Y`vuJ(y`Ibc_?D>lPF0>d_mB@~pU`~)uWP48cT@fTxkWSw{aR!`K{v)v zpN?vQZZNPgs3ki9h{An4&Cap-c5sJ!LVLtRd=GOZ^bUpyDZHm6T|t#218}ZA zx*=~9PO>5IGaBD^XX-_2t7?7@WN7VfI^^#Csdz9&{1r z9y<9R?BT~-V8+W3kzWWQ^)ZSI+R zt^Lg`iN$Z~a27)sC_03jrD-%@{ArCPY#Pc*u|j7rE%}jF$LvO4vyvAw3bdL_mg&ei zXys_i=Q!UoF^Xp6^2h5o&%cQ@@)$J4l`AG09G6Uj<~A~!xG>KjKSyTX)zH*EdHMK0 zo;AV-D+bqWhtD-!^+`$*P0B`HokilLd1EuuwhJ?%3wJ~VXIjIE3tj653PExvIVhE& zFMYsI(OX-Q&W$}9gad^PUGuKElCvXxU_s*kx%dH)Bi&$*Q(+9j>(Q>7K1A#|8 zY!G!p0kW29rP*BNHe_wH49bF{K7tymi}Q!Vc_Ox2XjwtpM2SYo7n>?_sB=$c8O5^? z6as!fE9B48FcE`(ruNXP%rAZlDXrFTC7^aoXEX41k)tIq)6kJ*(sr$xVqsh_m3^?? zOR#{GJIr6E0Sz{-( z-R?4asj|!GVl0SEagNH-t|{s06Q3eG{kZOoPHL&Hs0gUkPc&SMY=&{C0&HDI)EHx9 zm#ySWluxwp+b~+K#VG%21%F65tyrt9RTPR$eG0afer6D`M zTW=y!@y6yi#I5V#!I|8IqU=@IfZo!@9*P+f{yLxGu$1MZ%xRY(gRQ2qH@9eMK0`Z> zgO`4DHfFEN8@m@dxYuljsmVv}c4SID+8{kr>d_dLzF$g>urGy9g+=`xAfTkVtz56G zrKNsP$yrDyP=kIqPN9~rVmC-wH672NF7xU>~j5M06Xr&>UJBmOV z%7Ie2d=K=u^D`~i3(U7x?n=h!SCSD1`aFe-sY<*oh+=;B>UVFBOHsF=(Xr(Cai{dL z4S7Y>PHdfG9Iav5FtKzx&UCgg)|DRLvq7!0*9VD`e6``Pgc z1O!qSaNeBBZnDXClh(Dq@XAk?Bd6+_rsFt`5(E+V2c)!Mx4X z47X+QCB4B7$B=Fw1Z1vnHg;x9oDV1YQJAR6Q3}_}BXTFg$A$E!oGG%`Rc()-Ysc%w za(yEn0fw~AaEFr}Rxi;if?Gv)&g~21UzXU9osI9{rNfH$gPTTk#^B|irEc<8W+|9$ zc~R${X2)N!npz1DFVa%nEW)cgPq`MSs)_I*Xwo<+ZK-2^hD(Mc8rF1+2v7&qV;5SET-ygMLNFsb~#u+LpD$uLR1o!ha67gPV5Q{v#PZK5X zUT4aZ{o}&*q7rs)v%*fDTl%}VFX?Oi{i+oKVUBqbi8w#FI%_5;6`?(yc&(Fed4Quy8xsswG+o&R zO1#lUiA%!}61s3jR7;+iO$;1YN;_*yUnJK=$PT_}Q%&0T@2i$ zwGC@ZE^A62YeOS9DU9me5#`(wv24fK=C)N$>!!6V#6rX3xiHehfdvwWJ>_fwz9l)o`Vw9yi z0p5BgvIM5o_ zgo-xaAkS_mya8FXo1Ke4;U*7TGSfm0!fb4{E5Ar8T3p!Z@4;FYT8m=d`C@4-LM121 z?6W@9d@52vxUT-6K_;1!SE%FZHcm0U$SsC%QB zxkTrfH;#Y7OYPy!nt|k^Lgz}uYudos9wI^8x>Y{fTzv9gfTVXN2xH`;Er=rTeAO1x znaaJOR-I)qwD4z%&dDjY)@s`LLSd#FoD!?NY~9#wQRTHpD7Vyyq?tKUHKv6^VE93U zt_&ePH+LM-+9w-_9rvc|>B!oT>_L59nipM-@ITy|x=P%Ezu@Y?N!?jpwP%lm;0V5p z?-$)m84(|7vxV<6f%rK3!(R7>^!EuvA&j@jdTI+5S1E{(a*wvsV}_)HDR&8iuc#>+ zMr^2z*@GTnfDW-QS38OJPR3h6U&mA;vA6Pr)MoT7%NvA`%a&JPi|K8NP$b1QY#WdMt8-CDA zyL0UXNpZ?x=tj~LeM0wk<0Dlvn$rtjd$36`+mlf6;Q}K2{%?%EQ+#FJy6v5cS+Q-~ ztk||Iwr$(CZQHi38QZF;lFFBNt+mg2*V_AhzkM<8#>E_S^xj8%T5tXTytD6f)vePG z^B0Ne-*6Pqg+rVW?%FGHLhl^ycQM-dhNCr)tGC|XyES*NK%*4AnZ!V+Zu?x zV2a82fs8?o?X} zjC1`&uo1Ti*gaP@E43NageV^$Xue3%es2pOrLdgznZ!_a{*`tfA+vnUv;^Ebi3cc$?-kh76PqA zMpL!y(V=4BGPQSU)78q~N}_@xY5S>BavY3Sez-+%b*m0v*tOz6zub9%*~%-B)lb}t zy1UgzupFgf?XyMa+j}Yu>102tP$^S9f7;b7N&8?_lYG$okIC`h2QCT_)HxG1V4Uv{xdA4k3-FVY)d}`cmkePsLScG&~@wE?ix2<(G7h zQ7&jBQ}Kx9mm<0frw#BDYR7_HvY7En#z?&*FurzdDNdfF znCL1U3#iO`BnfPyM@>;#m2Lw9cGn;(5*QN9$zd4P68ji$X?^=qHraP~Nk@JX6}S>2 zhJz4MVTib`OlEAqt!UYobU0-0r*`=03)&q7ubQXrt|t?^U^Z#MEZV?VEin3Nv1~?U zuwwSeR10BrNZ@*h7M)aTxG`D(By$(ZP#UmBGf}duX zhx;7y1x@j2t5sS#QjbEPIj95hV8*7uF6c}~NBl5|hgbB(}M3vnt zu_^>@s*Bd>w;{6v53iF5q7Em>8n&m&MXL#ilSzuC6HTzzi-V#lWoX zBOSBYm|ti@bXb9HZ~}=dlV+F?nYo3?YaV2=N@AI5T5LWWZzwvnFa%w%C<$wBkc@&3 zyUE^8xu<=k!KX<}XJYo8L5NLySP)cF392GK97(ylPS+&b}$M$Y+1VDrJa`GG7+%ToAsh z5NEB9oVv>as?i7f^o>0XCd%2wIaNRyejlFws`bXG$Mhmb6S&shdZKo;p&~b4wv$ z?2ZoM$la+_?cynm&~jEi6bnD;zSx<0BuCSDHGSssT7Qctf`0U!GDwG=+^|-a5%8Ty z&Q!%m%geLjBT*#}t zv1wDzuC)_WK1E|H?NZ&-xr5OX(ukXMYM~_2c;K}219agkgBte_#f+b9Al8XjL-p}1 z8deBZFjplH85+Fa5Q$MbL>AfKPxj?6Bib2pevGxIGAG=vr;IuuC%sq9x{g4L$?Bw+ zvoo`E)3#bpJ{Ij>Yn0I>R&&5B$&M|r&zxh+q>*QPaxi2{lp?omkCo~7ibow#@{0P> z&XBocU8KAP3hNPKEMksQ^90zB1&&b1Me>?maT}4xv7QHA@Nbvt-iWy7+yPFa9G0DP zP82ooqy_ku{UPv$YF0kFrrx3L=FI|AjG7*(paRLM0k1J>3oPxU0Zd+4&vIMW>h4O5G zej2N$(e|2Re z@8xQ|uUvbA8QVXGjZ{Uiolxb7c7C^nW`P(m*Jkqn)qdI0xTa#fcK7SLp)<86(c`A3 zFNB4y#NHe$wYc7V)|=uiW8gS{1WMaJhDj4xYhld;zJip&uJ{Jg3R`n+jywDc*=>bW zEqw(_+j%8LMRrH~+M*$V$xn9x9P&zt^evq$P`aSf-51`ZOKm(35OEUMlO^$>%@b?a z>qXny!8eV7cI)cb0lu+dwzGH(Drx1-g+uDX;Oy$cs+gz~?LWif;#!+IvPR6fa&@Gj zwz!Vw9@-Jm1QtYT?I@JQf%`=$^I%0NK9CJ75gA}ff@?I*xUD7!x*qcyTX5X+pS zAVy4{51-dHKs*OroaTy;U?zpFS;bKV7wb}8v+Q#z<^$%NXN(_hG}*9E_DhrRd7Jqp zr}2jKH{avzrpXj?cW{17{kgKql+R(Ew55YiKK7=8nkzp7Sx<956tRa(|yvHlW zNO7|;GvR(1q}GrTY@uC&ow0me|8wE(PzOd}Y=T+Ih8@c2&~6(nzQrK??I7DbOguA9GUoz3ASU%BFCc8LBsslu|nl>q8Ag(jA9vkQ`q2amJ5FfA7GoCdsLW znuok(diRhuN+)A&`rH{$(HXWyG2TLXhVDo4xu?}k2cH7QsoS>sPV)ylb45Zt&_+1& zT)Yzh#FHRZ-z_Q^8~IZ+G~+qSw-D<{0NZ5!J1%rAc`B23T98TMh9ylkzdk^O?W`@C??Z5U9#vi0d<(`?9fQvNN^ji;&r}geU zSbKR5Mv$&u8d|iB^qiLaZQ#@)%kx1N;Og8Js>HQD3W4~pI(l>KiHpAv&-Ev45z(vYK<>p6 z6#pU(@rUu{i9UngMhU&FI5yeRub4#u=9H+N>L@t}djC(Schr;gc90n%)qH{$l0L4T z;=R%r>CuxH!O@+eBR`rBLrT0vnP^sJ^+qE^C8ZY0-@te3SjnJ)d(~HcnQw@`|qAp|Trrs^E*n zY1!(LgVJfL?@N+u{*!Q97N{Uu)ZvaN>hsM~J?*Qvqv;sLnXHjKrtG&x)7tk?8%AHI zo5eI#`qV1{HmUf-Fucg1xn?Kw;(!%pdQ)ai43J3NP4{%x1D zI0#GZh8tjRy+2{m$HyI(iEwK30a4I36cSht3MM85UqccyUq6$j5K>|w$O3>`Ds;`0736+M@q(9$(`C6QZQ-vAKjIXKR(NAH88 zwfM6_nGWlhpy!_o56^BU``%TQ%tD4hs2^<2pLypjAZ;W9xAQRfF_;T9W-uidv{`B z{)0udL1~tMg}a!hzVM0a_$RbuQk|EG&(z*{nZXD3hf;BJe4YxX8pKX7VaIjjDP%sk zU5iOkhzZ&%?A@YfaJ8l&H;it@;u>AIB`TkglVuy>h;vjtq~o`5NfvR!ZfL8qS#LL` zD!nYHGzZ|}BcCf8s>b=5nZRYV{)KK#7$I06s<;RyYC3<~`mob_t2IfR*dkFJyL?FU zvuo-EE4U(-le)zdgtW#AVA~zjx*^80kd3A#?vI63pLnW2{j*=#UG}ISD>=ZGA$H&` z?Nd8&11*4`%MQlM64wfK`{O*ad5}vk4{Gy}F98xIAsmjp*9P=a^yBHBjF2*Iibo2H zGJAMFDjZcVd%6bZ`dz;I@F55VCn{~RKUqD#V_d{gc|Z|`RstPw$>Wu+;SY%yf1rI=>51Oolm>cnjOWHm?ydcgGs_kPUu=?ZKtQS> zKtLS-v$OMWXO>B%Z4LFUgw4MqA?60o{}-^6tf(c0{Y3|yF##+)RoXYVY-lyPhgn{1 z>}yF0Ab}D#1*746QAj5c%66>7CCWs8O7_d&=Ktu!SK(m}StvvBT1$8QP3O2a*^BNA z)HPhmIi*((2`?w}IE6Fo-SwzI_F~OC7OR}guyY!bOQfpNRg3iMvsFPYb9-;dT6T%R zhLwIjgiE^-9_4F3eMHZ3LI%bbOmWVe{SONpujQ;3C+58=Be4@yJK>3&@O>YaSdrevAdCLMe_tL zl8@F}{Oc!aXO5!t!|`I zdC`k$5z9Yf%RYJp2|k*DK1W@AN23W%SD0EdUV^6~6bPp_HZi0@dku_^N--oZv}wZA zH?Bf`knx%oKB36^L;P%|pf#}Tp(icw=0(2N4aL_Ea=9DMtF})2ay68V{*KfE{O=xL zf}tcfCL|D$6g&_R;r~1m{+)sutQPKzVv6Zw(%8w&4aeiy(qct1x38kiqgk!0^^X3IzI2ia zxI|Q)qJNEf{=I$RnS0`SGMVg~>kHQB@~&iT7+eR!Ilo1ZrDc3TVW)CvFFjHK4K}Kh z)dxbw7X%-9Ol&Y4NQE~bX6z+BGOEIIfJ~KfD}f4spk(m62#u%k<+iD^`AqIhWxtKGIm)l$7=L`=VU0Bz3-cLvy&xdHDe-_d3%*C|Q&&_-n;B`87X zDBt3O?Wo-Hg6*i?f`G}5zvM?OzQjkB8uJhzj3N;TM5dSM$C@~gGU7nt-XX_W(p0IA6$~^cP*IAnA<=@HVqNz=Dp#Rcj9_6*8o|*^YseK_4d&mBY*Y&q z8gtl;(5%~3Ehpz)bLX%)7|h4tAwx}1+8CBtu9f5%^SE<&4%~9EVn4*_!r}+{^2;} zwz}#@Iw?&|8F2LdXUIjh@kg3QH69tqxR_FzA;zVpY=E zcHnWh(3j3UXeD=4m_@)Ea4m#r?axC&X%#wC8FpJPDYR~@65T?pXuWdPzEqXP>|L`S zKYFF0I~%I>SFWF|&sDsRdXf$-TVGSoWTx7>7mtCVUrQNVjZ#;Krobgh76tiP*0(5A zs#<7EJ#J`Xhp*IXB+p5{b&X3GXi#b*u~peAD9vr0*Vd&mvMY^zxTD=e(`}ybDt=BC(4q)CIdp>aK z0c?i@vFWjcbK>oH&V_1m_EuZ;KjZSiW^i30U` zGLK{%1o9TGm8@gy+Rl=-5&z`~Un@l*2ne3e9B+>wKyxuoUa1qhf?-Pi= zZLCD-b7*(ybv6uh4b`s&Ol3hX2ZE<}N@iC+h&{J5U|U{u$XK0AJz)!TSX6lrkG?ris;y{s zv`B5Rq(~G58?KlDZ!o9q5t%^E4`+=ku_h@~w**@jHV-+cBW-`H9HS@o?YUUkKJ;AeCMz^f@FgrRi@?NvO3|J zBM^>4Z}}!vzNum!R~o0)rszHG(eeq!#C^wggTgne^2xc9nIanR$pH1*O;V>3&#PNa z7yoo?%T(?m-x_ow+M0Bk!@ow>A=skt&~xK=a(GEGIWo4AW09{U%(;CYLiQIY$bl3M zxC_FGKY%J`&oTS{R8MHVe{vghGEshWi!(EK*DWmoOv|(Ff#(bZ-<~{rc|a%}Q4-;w z{2gca97m~Nj@Nl{d)P`J__#Zgvc@)q_(yfrF2yHs6RU8UXxcU(T257}E#E_A}%2_IW?%O+7v((|iQ{H<|$S7w?;7J;iwD>xbZc$=l*(bzRXc~edIirlU0T&0E_EXfS5%yA zs0y|Sp&i`0zf;VLN=%hmo9!aoLGP<*Z7E8GT}%)cLFs(KHScNBco(uTubbxCOD_%P zD7XlHivrSWLth7jf4QR9`jFNk-7i%v4*4fC*A=;$Dm@Z^OK|rAw>*CI%E z3%14h-)|Q%_$wi9=p!;+cQ*N1(47<49TyB&B*bm_m$rs+*ztWStR~>b zE@V06;x19Y_A85N;R+?e?zMTIqdB1R8>(!4_S!Fh={DGqYvA0e-P~2DaRpCYf4$-Q z*&}6D!N_@s`$W(|!DOv%>R0n;?#(HgaI$KpHYpnbj~I5eeI(u4CS7OJajF%iKz)*V zt@8=9)tD1ML_CrdXQ81bETBeW!IEy7mu4*bnU--kK;KfgZ>oO>f)Sz~UK1AW#ZQ_ic&!ce~@(m2HT@xEh5u%{t}EOn8ET#*U~PfiIh2QgpT z%gJU6!sR2rA94u@xj3%Q`n@d}^iMH#X>&Bax+f4cG7E{g{vlJQ!f9T5wA6T`CgB%6 z-9aRjn$BmH=)}?xWm9bf`Yj-f;%XKRp@&7?L^k?OT_oZXASIqbQ#eztkW=tmRF$~% z6(&9wJuC-BlGrR*(LQKx8}jaE5t`aaz#Xb;(TBK98RJBjiqbZFyRNTOPA;fG$;~e` zsd6SBii3^(1Y`6^#>kJ77xF{PAfDkyevgox`qW`nz1F`&w*DH5Oh1idOTLES>DToi z8Qs4|?%#%>yuQO1#{R!-+2AOFznWo)e3~_D!nhoDgjovB%A8< zt%c^KlBL$cDPu!Cc`NLc_8>f?)!FGV7yudL$bKj!h;eOGkd;P~sr6>r6TlO{Wp1%xep8r1W{`<4am^(U} z+nCDP{Z*I?IGBE&*KjiaR}dpvM{ZFMW%P5Ft)u$FD373r2|cNsz%b0uk1T+mQI@4& zFF*~xDxDRew1Bol-*q>F{Xw8BUO;>|0KXf`lv7IUh%GgeLUzR|_r(TXZTbfXFE0oc zmGMwzNFgkdg><=+3MnncRD^O`m=SxJ6?}NZ8BR)=ag^b4Eiu<_bN&i0wUaCGi60W6 z%iMl&`h8G)y`gfrVw$={cZ)H4KSQO`UV#!@@cDx*hChXJB7zY18EsIo1)tw0k+8u; zg(6qLysbxVbLFbkYqKbEuc3KxTE+%j5&k>zHB8_FuDcOO3}FS|eTxoUh2~|Bh?pD| zsmg(EtMh`@s;`(r!%^xxDt(5wawK+*jLl>_Z3shaB~vdkJ!V3RnShluzmwn7>PHai z3avc`)jZSAvTVC6{2~^CaX49GXMtd|sbi*swkgoyLr=&yp!ASd^mIC^D;a|<=3pSt zM&0u%#%DGzlF4JpMDs~#kU;UCtyW+d3JwNiu`Uc7Yi6%2gfvP_pz8I{Q<#25DjM_D z(>8yI^s@_tG@c=cPoZImW1CO~`>l>rs=i4BFMZT`vq5bMOe!H@8q@sEZX<-kiY&@u3g1YFc zc@)@OF;K-JjI(eLs~hy8qOa9H1zb!3GslI!nH2DhP=p*NLHeh^9WF?4Iakt+b( z-4!;Q-8c|AX>t+5I64EKpDj4l2x*!_REy9L_9F~i{)1?o#Ws{YG#*}lg_zktt#ZlN zmoNsGm7$AXLink`GWtY*TZEH!J9Qv+A1y|@>?&(pb(6XW#ZF*}x*{60%wnt{n8Icp zq-Kb($kh6v_voqvA`8rq!cgyu;GaWZ>C2t6G5wk! zcKTlw=>KX3ldU}a1%XESW71))Z=HW%sMj2znJ;fdN${00DGGO}d+QsTQ=f;BeZ`eC~0-*|gn$9G#`#0YbT(>O(k&!?2jI z&oi9&3n6Vz<4RGR}h*1ggr#&0f%Op(6{h>EEVFNJ0C>I~~SmvqG+{RXDrexBz zw;bR@$Wi`HQ3e*eU@Cr-4Z7g`1R}>3-Qej(#Dmy|CuFc{Pg83Jv(pOMs$t(9vVJQJ zXqn2Ol^MW;DXq!qM$55vZ{JRqg!Q1^Qdn&FIug%O3=PUr~Q`UJuZ zc`_bE6i^Cp_(fka&A)MsPukiMyjG$((zE$!u>wyAe`gf-1Qf}WFfi1Y{^ zdCTTrxqpQE#2BYWEBnTr)u-qGSVRMV7HTC(x zb(0FjYH~nW07F|{@oy)rlK6CCCgyX?cB;19Z(bCP5>lwN0UBF}Ia|L0$oGHl-oSTZ zr;(u7nDjSA03v~XoF@ULya8|dzH<2G=n9A)AIkQKF0mn?!BU(ipengAE}6r`CE!jd z=EcX8exgDZZQ~~fgxR-2yF;l|kAfnjhz|i_o~cYRdhnE~1yZ{s zG!kZJ<-OVnO{s3bOJK<)`O;rk>=^Sj3M76Nqkj<_@Jjw~iOkWUCL+*Z?+_Jvdb!0cUBy=(5W9H-r4I zxAFts>~r)B>KXdQANyaeKvFheZMgoq4EVV0|^NR@>ea* zh%<78{}wsdL|9N1!jCN-)wH4SDhl$MN^f_3&qo?>Bz#?c{ne*P1+1 z!a`(2Bxy`S^(cw^dv{$cT^wEQ5;+MBctgPfM9kIQGFUKI#>ZfW9(8~Ey-8`OR_XoT zflW^mFO?AwFWx9mW2-@LrY~I1{dlX~jBMt!3?5goHeg#o0lKgQ+eZcIheq@A&dD}GY&1c%hsgo?z zH>-hNgF?Jk*F0UOZ*bs+MXO(dLZ|jzKu5xV1v#!RD+jRrHdQ z>>b){U(I@i6~4kZXn$rk?8j(eVKYJ2&k7Uc`u01>B&G@c`P#t#x@>Q$N$1aT514fK zA_H8j)UKen{k^ehe%nbTw}<JV6xN_|| z(bd-%aL}b z3VITE`N~@WlS+cV>C9TU;YfsU3;`+@hJSbG6aGvis{Gs%2K|($)(_VfpHB|DG8Nje+0tCNW%_cu3hk0F)~{-% zW{2xSu@)Xnc`Dc%AOH)+LT97ImFR*WekSnJ3OYIs#ijP4TD`K&7NZKsfZ;76k@VD3py?pSw~~r^VV$Z zuUl9lF4H2(Qga0EP_==vQ@f!FLC+Y74*s`Ogq|^!?RRt&9e9A&?Tdu=8SOva$dqgYU$zkKD3m>I=`nhx-+M;-leZgt z8TeyQFy`jtUg4Ih^JCUcq+g_qs?LXSxF#t+?1Jsr8c1PB#V+f6aOx@;ThTIR4AyF5 z3m$Rq(6R}U2S}~Bn^M0P&Aaux%D@ijl0kCCF48t)+Y`u>g?|ibOAJoQGML@;tn{%3IEMaD(@`{7ByXQ`PmDeK*;W?| zI8%%P8%9)9{9DL-zKbDQ*%@Cl>Q)_M6vCs~5rb(oTD%vH@o?Gk?UoRD=C-M|w~&vb z{n-B9>t0EORXd-VfYC>sNv5vOF_Wo5V)(Oa%<~f|EU7=npanpVX^SxPW;C!hMf#kq z*vGNI-!9&y!|>Zj0V<~)zDu=JqlQu+ii387D-_U>WI_`3pDuHg{%N5yzU zEulPN)%3&{PX|hv*rc&NKe(bJLhH=GPuLk5pSo9J(M9J3v)FxCo65T%9x<)x+&4Rr2#nu2?~Glz|{28OV6 z)H^`XkUL|MG-$XE=M4*fIPmeR2wFWd>5o*)(gG^Y>!P4(f z68RkX0cRBOFc@`W-IA(q@p@m>*2q-`LfujOJ8-h$OgHte;KY4vZKTxO95;wh#2ZDL zKi8aHkz2l54lZd81t`yY$Tq_Q2_JZ1d(65apMg}vqwx=ceNOWjFB)6m3Q!edw2<{O z4J6+Un(E8jxs-L-K_XM_VWahy zE+9fm_ZaxjNi{fI_AqLKqhc4IkqQ4`Ut$=0L)nzlQw^%i?bP~znsbMY3f}*nPWqQZ zz_CQDpZ?Npn_pEr`~SX1`OoSkS;bmzQ69y|W_4bH3&U3F7EBlx+t%2R02VRJ01cfX zo$$^ObDHK%bHQaOcMpCq@@Jp8!OLYVQO+itW1ZxlkmoG#3FmD4b61mZjn4H|pSmYi2YE;I#@jtq8Mhjdgl!6({gUsQA>IRXb#AyWVt7b=(HWGUj;wd!S+q z4S+H|y<$yPrrrTqQHsa}H`#eJFV2H5Dd2FqFMA%mwd`4hMK4722|78d(XV}rz^-GV(k zqsQ>JWy~cg_hbp0=~V3&TnniMQ}t#INg!o2lN#H4_gx8Tn~Gu&*ZF8#kkM*5gvPu^ zw?!M^05{7q&uthxOn?%#%RA_%y~1IWly7&_-sV!D=Kw3DP+W)>YYRiAqw^d7vG_Q%v;tRbE1pOBHc)c&_5=@wo4CJTJ1DeZErEvP5J(kc^GnGYX z|LqQjTkM{^gO2cO#-(g!7^di@$J0ibC(vsnVkHt3osnWL8?-;R1BW40q5Tmu_9L-s z7fNF5fiuS-%B%F$;D97N-I@!~c+J>nv%mzQ5vs?1MgR@XD*Gv`A{s8 z5Cr>z5j?|sb>n=c*xSKHpdy667QZT?$j^Doa%#m4ggM@4t5Oe%iW z@w~j_B>GJJkO+6dVHD#CkbC(=VMN8nDkz%44SK62N(ZM#AsNz1KW~3(i=)O;q5JrK z?vAVuL}Rme)OGQuLn8{3+V352UvEBV^>|-TAAa1l-T)oiYYD&}Kyxw73shz?Bn})7 z_a_CIPYK(zMp(i+tRLjy4dV#CBf3s@bdmwXo`Y)dRq9r9-c@^2S*YoNOmAX%@OYJOXs zT*->in!8Ca_$W8zMBb04@|Y)|>WZ)-QGO&S7Zga1(1#VR&)X+MD{LEPc%EJCXIMtr z1X@}oNU;_(dfQ_|kI-iUSTKiVzcy+zr72kq)TIp(GkgVyd%{8@^)$%G)pA@^Mfj71FG%d?sf(2Vm>k%X^RS`}v0LmwIQ7!_7cy$Q8pT?X1VWecA_W68u==HbrU& z@&L6pM0@8ZHL?k{6+&ewAj%grb6y@0$3oamTvXsjGmPL_$~OpIyIq%b$(uI1VKo zk_@{r>1p84UK3}B>@d?xUZ}dJk>uEd+-QhwFQ`U?rA=jj+$w8sD#{492P}~R#%z%0 z5dlltiAaiPKv9fhjmuy{*m!C22$;>#85EduvdSrFES{QO$bHpa7E@&{bWb@<7VhTF zXCFS_wB>7*MjJ3$_i4^A2XfF2t7`LOr3B@??OOUk=4fKkaHne4RhI~Lm$JrHfUU*h zgD9G66;_F?3>0W{pW2A^DR7Bq`ZUiSc${S8EM>%gFIqAw0du4~kU#vuCb=$I_PQv? zZfEY7X6c{jJZ@nF&T>4oyy(Zr_XqnMq)ZtGPASbr?IhZOnL|JKY()`eo=P5UK9(P-@ zOJKFogtk|pscVD+#$7KZs^K5l4gC}*CTd0neZ8L(^&1*bPrCp23%{VNp`4Ld*)Fly z)b|zb*bCzp?&X3_=qLT&0J+=p01&}9*xbk~^hd^@mV!Ha`1H+M&60QH2c|!Ty`RepK|H|Moc5MquD z=&$Ne3%WX+|7?iiR8=7*LW9O3{O%Z6U6`VekeF8lGr5vd)rsZu@X#5!^G1;nV60cz zW?9%HgD}1G{E(YvcLcIMQR65BP50)a;WI*tjRzL7diqRqh$3>OK{06VyC=pj6OiardshTnYfve5U>Tln@y{DC99f!B4> zCrZa$B;IjDrg}*D5l=CrW|wdzENw{q?oIj!Px^7DnqAsU7_=AzXxoA;4(YvN5^9ag zwEd4-HOlO~R0~zk>!4|_Z&&q}agLD`Nx!%9RLC#7fK=w06e zOK<>|#@|e2zjwZ5aB>DJ%#P>k4s0+xHJs@jROvoDQfSoE84l8{9y%5^POiP+?yq0> z7+Ymbld(s-4p5vykK@g<{X*!DZt1QWXKGmj${`@_R~=a!qPzB357nWW^KmhV!^G3i zsYN{2_@gtzsZH*FY!}}vNDnqq>kc(+7wK}M4V*O!M&GQ|uj>+8!Q8Ja+j3f*MzwcI z^s4FXGC=LZ?il4D+Y^f89wh!d7EU-5dZ}}>_PO}jXRQ@q^CjK-{KVnmFd_f&IDKmx zZ5;PDLF%_O);<4t`WSMN;Ec^;I#wU?Z?_R|Jg`#wbq;UM#50f@7F?b7ySi-$C-N;% zqXowTcT@=|@~*a)dkZ836R=H+m6|fynm#0Y{KVyYU=_*NHO1{=Eo{^L@wWr7 zjz9GOu8Fd&v}a4d+}@J^9=!dJRsCO@=>K6UCM)Xv6};tb)M#{(k!i}_0Rjq z2kb7wPcNgov%%q#(1cLykjrxAg)By+3QueBR>Wsep&rWQHq1wE!JP+L;q+mXts{j@ zOY@t9BFmofApO0k@iBFPeKsV3X=|=_t65QyohXMSfMRr7Jyf8~ogPVmJwbr@`nmml zov*NCf;*mT(5s4K=~xtYy8SzE66W#tW4X#RnN%<8FGCT{z#jRKy@Cy|!yR`7dsJ}R z!eZzPCF+^b0qwg(mE=M#V;Ud9)2QL~ z-r-2%0dbya)%ui_>e6>O3-}4+Q!D+MU-9HL2tH)O`cMC1^=rA=q$Pcc;Zel@@ss|K zH*WMdS^O`5Uv1qNTMhM(=;qjhaJ|ZC41i2!kt4;JGlXQ$tvvF8Oa^C@(q6(&6B^l) zNG{GaX?`qROHwL-F1WZDEF;C6Inuv~1&ZuP3j53547P38tr|iPH#3&hN*g0R^H;#) znft`cw0+^Lwe{!^kQat+xjf_$SZ05OD6~U`6njelvd+4pLZU(0ykS5&S$)u?gm!;} z+gJ8g12b1D4^2HH!?AHFAjDAP^q)Juw|hZfIv{3Ryn%4B^-rqIF2 zeWk^za4fq#@;re{z4_O|Zj&Zn{2WsyI^1%NW=2qA^iMH>u>@;GAYI>Bk~u0wWQrz* zdEf)7_pSYMg;_9^qrCzvv{FZYwgXK}6e6ceOH+i&+O=x&{7aRI(oz3NHc;UAxMJE2 zDb0QeNpm$TDcshGWs!Zy!shR$lC_Yh-PkQ`{V~z!AvUoRr&BAGS#_*ZygwI2-)6+a zq|?A;+-7f0Dk4uuht z6sWPGl&Q$bev1b6%aheld88yMmBp2j=z*egn1aAWd?zN=yEtRDGRW&nmv#%OQwuJ; zqKZ`L4DsqJwU{&2V9f>2`1QP7U}`6)$qxTNEi`4xn!HzIY?hDnnJZw+mFnVSry=bLH7ar+M(e9h?GiwnOM?9ZJcTJ08)T1-+J#cr&uHhXkiJ~}&(}wvzCo33 zLd_<%rRFQ3d5fzKYQy41<`HKk#$yn$Q+Fx-?{3h72XZrr*uN!5QjRon-qZh9-uZ$rWEKZ z!dJMP`hprNS{pzqO`Qhx`oXGd{4Uy0&RDwJ`hqLw4v5k#MOjvyt}IkLW{nNau8~XM z&XKeoVYreO=$E%z^WMd>J%tCdJx5-h+8tiawu2;s& zD7l`HV!v@vcX*qM(}KvZ#%0VBIbd)NClLBu-m2Scx1H`jyLYce;2z;;eo;ckYlU53 z9JcQS+CvCwj*yxM+e*1Vk6}+qIik2VzvUuJyWyO}piM1rEk%IvS;dsXOIR!#9S;G@ zPcz^%QTf9D<2~VA5L@Z@FGQqwyx~Mc-QFzT4Em?7u`OU!PB=MD8jx%J{<`tH$Kcxz zjIvb$x|`s!-^^Zw{hGV>rg&zb;=m?XYAU0LFw+uyp8v@Y)zmjj&Ib7Y1@r4`cfrS%cVxJiw`;*BwIU*6QVsBBL;~nw4`ZFqs z1YSgLVy=rvA&GQB4MDG+j^)X1N=T;Ty2lE-`zrg(dNq?=Q`nCM*o8~A2V~UPArX<| zF;e$5B0hPSo56=ePVy{nah#?e-Yi3g*z6iYJ#BFJ-5f0KlQ-PRiuGwe29fyk1T6>& zeo2lvb%h9Vzi&^QcVNp}J!x&ubtw5fKa|n2XSMlg#=G*6F|;p)%SpN~l8BaMREDQN z-c9O}?%U1p-ej%hzIDB!W_{`9lS}_U==fdYpAil1E3MQOFW^u#B)Cs zTE3|YB0bKpXuDKR9z&{4gNO3VHDLB!xxPES+)yaJxo<|}&bl`F21};xsQnc!*FPZA zSct2IU3gEu@WQKmY-vA5>MV?7W|{$rAEj4<8`*i)<%fj*gDz2=ApqZ&MP&0UmO1?q!GN=di+n(#bB_mHa z(H-rIOJqamMfwB%?di!TrN=x~0jOJtvb0e9uu$ZCVj(gJyK}Fa5F2S?VE30P{#n3eMy!-v7e8viCooW9cfQx%xyPNL*eDKL zB=X@jxulpkLfnar7D2EeP*0L7c9urDz{XdV;@tO;u`7DlN7#~ zAKA~uM2u8_<5FLkd}OzD9K zO5&hbK8yakUXn8r*H9RE zO9Gsipa2()=&x=1mnQtNP#4m%GXThu8Ccqx*qb;S{5}>bU*V5{SY~(Hb={cyTeaTM zMEaKedtJf^NnJrwQ^Bd57vSlJ3l@$^0QpX@_1>h^+js8QVpwOiIMOiSC_>3@dt*&| zV?0jRdlgn|FIYam0s)a@5?0kf7A|GD|dRnP1=B!{ldr;N5s)}MJ=i4XEqlC}w)LEJ}7f9~c!?It(s zu>b=YBlFRi(H-%8A!@Vr{mndRJ z_jx*?BQpK>qh`2+3cBJhx;>yXPjv>dQ0m+nd4nl(L;GmF-?XzlMK zP(Xeyh7mFlP#=J%i~L{o)*sG7H5g~bnL2Hn3y!!r5YiYRzgNTvgL<(*g5IB*gcajK z86X3LoW*5heFmkIQ-I_@I_7b!Xq#O;IzOv(TK#(4gd)rmCbv5YfA4koRfLydaIXUU z8(q?)EWy!sjsn-oyUC&uwJqEXdlM}#tmD~*Ztav=mTQyrw0^F=1I5lj*}GSQTQOW{ z=O12;?fJfXxy`)ItiDB@0sk43AZo_sRn*jc#S|(2*%tH84d|UTYN!O4R(G6-CM}84 zpiyYJ^wl|w@!*t)dwn0XJv2kuHgbfNL$U6)O-k*~7pQ?y=sQJdKk5x`1>PEAxjIWn z{H$)fZH4S}%?xzAy1om0^`Q$^?QEL}*ZVQK)NLgmnJ`(we z21c23X1&=^>k;UF-}7}@nzUf5HSLUcOYW&gsqUrj7%d$)+d8ZWwTZq)tOgc%fz95+ zl%sdl)|l|jXfqIcjKTFrX74Rbq1}osA~fXPSPE?XO=__@`7k4Taa!sHE8v-zfx(AM zXT_(7u;&_?4ZIh%45x>p!(I&xV|IE**qbqCRGD5aqLpCRvrNy@uT?iYo-FPpu`t}J zSTZ}MDrud+`#^14r`A%UoMvN;raizytxMBV$~~y3i0#m}0F}Dj_fBIz+)1RWdnctP z>^O^vd0E+jS+$V~*`mZWER~L^q?i-6RPxxufWdrW=%prbCYT{5>Vgu%vPB)~NN*2L zB?xQg2K@+Xy=sPh$%10LH!39p&SJG+3^i*lFLn=uY8Io6AXRZf;p~v@1(hWsFzeKzx99_{w>r;cypkPVJCKtLGK>?-K0GE zGH>$g?u`)U_%0|f#!;+E>?v>qghuBwYZxZ*Q*EE|P|__G+OzC-Z+}CS(XK^t!TMoT zc+QU|1C_PGiVp&_^wMxfmMAuJDQ%1p4O|x5DljN6+MJiO%8s{^ts8$uh5`N~qK46c`3WY#hRH$QI@*i1OB7qBIN*S2gK#uVd{ zik+wwQ{D)g{XTGjKV1m#kYhmK#?uy)g@idi&^8mX)Ms`^=hQGY)j|LuFr8SJGZjr| zzZf{hxYg)-I^G|*#dT9Jj)+wMfz-l7ixjmwHK9L4aPdXyD-QCW!2|Jn(<3$pq-BM; zs(6}egHAL?8l?f}2FJSkP`N%hdAeBiD{3qVlghzJe5s9ZUMd`;KURm_eFaK?d&+TyC88v zCv2R(Qg~0VS?+p+l1e(aVq`($>|0b{{tPNbi} zaZDffTZ7N|t2D5DBv~aX#X+yGagWs1JRsqbr4L8a`B`m) z1p9?T`|*8ZXHS7YD8{P1Dk`EGM`2Yjsy0=7M&U6^VO30`Gx!ZkUoqmc3oUbd&)V*iD08>dk=#G!*cs~^tOw^s8YQqYJ z!5=-4ZB7rW4mQF&YZw>T_in-c9`0NqQ_5Q}fq|)%HECgBd5KIo`miEcJ>~a1e2B@) zL_rqoQ;1MowD34e6#_U+>D`WcnG5<2Q6cnt4Iv@NC$*M+i3!c?6hqPJLsB|SJ~xo! zm>!N;b0E{RX{d*in3&0w!cmB&TBNEjhxdg!fo+}iGE*BWV%x*46rT@+cXU;leofWy zxst{S8m!_#hIhbV7wfWN#th8OI5EUr3IR_GOIzBgGW1u4J*TQxtT7PXp#U#EagTV* zehVkBFF06`@5bh!t%L)-)`p|d7D|^kED7fsht#SN7*3`MKZX};Jh0~nCREL_BGqNR zxpJ4`V{%>CAqEE#Dt95u=;Un8wLhrac$fao`XlNsOH%&Ey2tK&vAcriS1kXnntDuttcN{%YJz@!$T zD&v6ZQ>zS1`o!qT=JK-Y+^i~bZkVJpN8%<4>HbuG($h9LP;{3DJF_Jcl8CA5M~<3s^!$Sg62zLEnJtZ z0`)jwK75Il6)9XLf(64~`778D6-#Ie1IR2Ffu+_Oty%$8u+bP$?803V5W6%(+iZzp zp5<&sBV&%CJcXUIATUakP1czt$&0x$lyoLH!ueNaIpvtO z*eCijxOv^-D?JaLzH<3yhOfDENi@q#4w(#tl-19(&Yc2K%S8Y&r{3~-)P17sC1{rQ zOy>IZ6%814_UoEi+w9a4XyGXF66{rgE~UT)oT4x zg9oIx@|{KL#VpTyE=6WK@Sbd9RKEEY)5W{-%0F^6(QMuT$RQRZ&yqfyF*Z$f8>{iT zq(;UzB-Ltv;VHvh4y%YvG^UEkvpe9ugiT97ErbY0ErCEOWs4J=kflA!*Q}gMbEP`N zY#L`x9a?E)*~B~t+7c8eR}VY`t}J;EWuJ-6&}SHnNZ8i0PZT^ahA@@HXk?c0{)6rC zP}I}_KK7MjXqn1E19gOwWvJ3i9>FNxN67o?lZy4H?n}%j|Dq$p%TFLUPJBD;R|*0O z3pLw^?*$9Ax!xy<&fO@;E2w$9nMez{5JdFO^q)B0OmGwkxxaDsEU+5C#g+?Ln-Vg@ z-=z4O*#*VJa*nujGnGfK#?`a|xfZsuiO+R}7y(d60@!WUIEUt>K+KTI&I z9YQ6#hVCo}0^*>yr-#Lisq6R?uI=Ms!J7}qm@B}Zu zp%f-~1Cf!-5S0xXl`oqq&fS=tt0`%dDWI&6pW(s zJXtYiY&~t>k5I0RK3sN;#8?#xO+*FeK#=C^%{Y>{k{~bXz%(H;)V5)DZRk~(_d0b6 zV!x54fwkl`1y;%U;n|E#^Vx(RGnuN|T$oJ^R%ZmI{8(9>U-K^QpDcT?Bb@|J0NAfvHtL#wP ziYupr2E5=_KS{U@;kyW7oy*+UTOiF*e+EhYqVcV^wx~5}49tBNSUHLH1=x}6L2Fl^4X4633$k!ZHZTL50Vq+a5+ z<}uglXQ<{x&6ey)-lq6;4KLHbR)_;Oo^FodsYSw3M-)FbLaBcPI=-ao+|))T2ksKb z{c%Fu`HR1dqNw8%>e0>HI2E_zNH1$+4RWfk}p-h(W@)7LC zwVnUO17y+~kw35CxVtokT44iF$l8XxYuetp)1Br${@lb(Q^e|q*5%7JNxp5B{r<09 z-~8o#rI1(Qb9FhW-igcsC6npf5j`-v!nCrAcVx5+S&_V2D>MOWp6cV$~Olhp2`F^Td{WV`2k4J`djb#M>5D#k&5XkMu*FiO(uP{SNX@(=)|Wm`@b> z_D<~{ip6@uyd7e3Rn+qM80@}Cl35~^)7XN?D{=B-4@gO4mY%`z!kMIZizhGtCH-*7 z{a%uB4usaUoJwbkVVj%8o!K^>W=(ZzRDA&kISY?`^0YHKe!()(*w@{w7o5lHd3(Us zUm-K=z&rEbOe$ackQ3XH=An;Qyug2g&vqf;zsRBldxA+=vNGoM$Zo9yT?Bn?`Hkiq z&h@Ss--~+=YOe@~JlC`CdSHy zcO`;bgMASYi6`WSw#Z|A;wQgH@>+I3OT6(*JgZZ_XQ!LrBJfVW2RK%#02|@V|H4&8DqslU6Zj(x!tM{h zRawG+Vy63_8gP#G!Eq>qKf(C&!^G$01~baLLk#)ov-Pqx~Du>%LHMv?=WBx2p2eV zbj5fjTBhwo&zeD=l1*o}Zs%SMxEi9yokhbHhY4N!XV?t8}?!?42E-B^Rh&ABFxovs*HeQ5{{*)SrnJ%e{){Z_#JH+jvwF7>Jo zE+qzWrugBwVOZou~oFa(wc7?`wNde>~HcC@>fA^o>ll?~aj-e|Ju z+iJzZg0y1@eQ4}rm`+@hH(|=gW^;>n>ydn!8%B4t7WL)R-D>mMw<7Wz6>ulFnM7QA ze2HEqaE4O6jpVq&ol3O$46r+DW@%glD8Kp*tFY#8oiSyMi#yEpVIw3#t?pXG?+H>v z$pUwT@0ri)_Bt+H(^uzp6qx!P(AdAI_Q?b`>0J?aAKTPt>73uL2(WXws9+T|%U)Jq zP?Oy;y6?{%J>}?ZmfcnyIQHh_jL;oD$`U#!v@Bf{5%^F`UiOX%)<0DqQ^nqA5Ac!< z1DPO5C>W0%m?MN*x(k>lDT4W3;tPi=&yM#Wjwc5IFNiLkQf`7GN+J*MbB4q~HVePM zeDj8YyA*btY&n!M9$tuOxG0)2um))hsVsY+(p~JnDaT7x(s2If0H_iRSju7!z7p|8 zzI`NV!1hHWX3m)?t68k6yNKvop{Z>kl)f5GV(~1InT4%9IxqhDX-rgj)Y|NYq_NTlZgz-)=Y$=x9L7|k0=m@6WQ<4&r=BX@pW25NtCI+N{e&`RGSpR zeb^`@FHm5?pWseZ6V08{R(ki}--13S2op~9Kzz;#cPgL}Tmrqd+gs(fJLTCM8#&|S z^L+7PbAhltJDyyxAVxqf(2h!RGC3$;hX@YNz@&JRw!m5?Q)|-tZ8u0D$4we+QytG^ zj0U_@+N|OJlBHdWPN!K={a$R1Zi{2%5QD}s&s-Xn1tY1cwh)8VW z$pjq>8sj4)?76EJs6bA0E&pfr^Vq`&Xc;Tl2T!fm+MV%!H|i0o;7A=zE?dl)-Iz#P zSY7QRV`qRc6b&rON`BValC01zSLQpVemH5y%FxK8m^PeNN(Hf1(%C}KPfC*L?Nm!nMW0@J3(J=mYq3DPk;TMs%h`-amWbc%7{1Lg3$ z^e=btuqch-lydbtLvazh+fx?87Q7!YRT(=-Vx;hO)?o@f1($e5B?JB9jcRd;zM;iE zu?3EqyK`@_5Smr#^a`C#M>sRwq2^|ym)X*r;0v6AM`Zz1aK94@9Ti)Lixun2N!e-A z>w#}xPxVd9AfaF$XTTff?+#D(xwOpjZj9-&SU%7Z-E2-VF-n#xnPeQH*67J=j>TL# z<v}>AiTXrQ(fYa%82%qlH=L z6Fg8@r4p+BeTZ!5cZlu$iR?EJpYuTx>cJ~{{B7KODY#o*2seq=p2U0Rh;3mX^9sza zk^R_l7jzL5BXWlrVkhh!+LQ-Nc0I`6l1mWkp~inn)HQWqMTWl4G-TBLglR~n&6J?4 z7J)IO{wkrtT!Csntw3H$Mnj>@;QbrxC&Shqn^VVu$Ls*_c~TTY~fri6fO-=eJsC*8(3(H zSyO>=B;G`qA398OvCHRvf3mabrPZaaLhn*+jeA`qI!gP&i8Zs!*bBqMXDJpSZG$N) zx0rDLvcO>EoqCTR)|n7eOp-jmd>`#w`6`;+9+hihW2WnKVPQ20LR94h+(p)R$Y!Q zj_3ZEY+e@NH0f6VjLND)sh+Cvfo3CpcXw?`$@a^@CyLrAKIpjL8G z`;cDLqvK=ER)$q)+6vMKlxn!!SzWl>Ib9Ys9L)L0IWr*Ox;Rk#(Dpqf;wapY_EYL8 zKFrV)Q8BBKO4$r2hON%g=r@lPE;kBUVYVG`uxx~QI>9>MCXw_5vnmDsm|^KRny929 zeKx>F(LDs#K4FGU*k3~GX`A!)l8&|tyan-rBHBm6XaB5hc5sGKWwibAD7&3M-gh1n z2?eI7E2u{(^z#W~wU~dHSfy|m)%PY454NBxED)y-T3AO`CLQxklcC1I@Y`v4~SEI#Cm> z-cjqK6I?mypZapi$ZK;y&G+|#D=woItrajg69VRD+Fu8*UxG6KdfFmFLE}HvBJ~Y) zC&c-hr~;H2Idnsz7_F~MKpBZldh)>itc1AL0>4knbVy#%pUB&9vqL1Kg*^aU`k#(p z=A%lur(|$GWSqILaWZ#2xj(&lheSiA|N6DOG?A|$!aYM)?oME6ngnfLw0CA79WA+y zhUeLbMw*VB?drVE_D~3DWVaD>8x?_q>f!6;)i3@W<=kBZBSE=uIU60SW)qct?AdM zXgti8&O=}QNd|u%Fpxr172Kc`sX^@fm>Fxl8fbFalJYci_GGoIzU*~U*I!QLz? z4NYk^=JXBS*Uph@51da-v;%?))cB^(ps}y8yChu7CzyC9SX{jAq13zdnqRHRvc{ha zcPmgCUqAJ^1RChMCCz;ZN*ap{JPoE<1#8nNObDbAt6Jr}Crq#xGkK@w2mLhIUecvy z#?s~?J()H*?w9K`_;S+8TNVkHSk}#yvn+|~jcB|he}OY(zH|7%EK%-Tq=)18730)v zM3f|=oFugXq3Lqn={L!wx|u(ycZf(Te11c3?^8~aF; zNMC)gi?nQ#S$s{46yImv_7@4_qu|XXEza~);h&cr*~dO@#$LtKZa@@r$8PD^jz{D6 zk~5;IJBuQjsKk+8i0wzLJ2=toMw4@rw7(|6`7*e|V(5-#ZzRirtkXBO1oshQ&0>z&HAtSF8+871e|ni4gLs#`3v7gnG#^F zDv!w100_HwtU}B2T!+v_YDR@-9VmoGW+a76oo4yy)o`MY(a^GcIvXW+4)t{lK}I-& zl-C=(w_1Z}tsSFjFd z3iZjkO6xnjLV3!EE?ex9rb1Zxm)O-CnWPat4vw08!GtcQ3lHD+ySRB*3zQu-at$rj zzBn`S?5h=JlLXX8)~Jp%1~YS6>M8c-Mv~E%s7_RcvIYjc-ia`3r>dvjxZ6=?6=#OM zfsv}?hGnMMdi9C`J9+g)5`M9+S79ug=!xE_XcHdWnIRr&hq$!X7aX5kJV8Q(6Lq?|AE8N2H z37j{DPDY^Jw!J>~>Mwaja$g%q1sYfH4bUJFOR`x=pZQ@O(-4b#5=_Vm(0xe!LW>YF zO4w`2C|Cu%^C9q9B>NjFD{+qt)cY3~(09ma%mp3%cjFsj0_93oVHC3)AsbBPuQNBO z`+zffU~AgGrE0K{NVR}@oxB4&XWt&pJ-mq!JLhFWbnXf~H%uU?6N zWJ7oa@``Vi$pMWM#7N9=sX1%Y+1qTGnr_G&h3YfnkHPKG}p>i{fAG+(klE z(g~u_rJXF48l1D?;;>e}Ra{P$>{o`jR_!s{hV1Wk`vURz`W2c$-#r9GM7jgs2>um~ zouGlCm92rOiLITzf`jgl`v2qYw^!Lh0YwFHO1|3Krp8ztE}?#2+>c)yQlNw%5e6w5 zIm9BKZN5Q9b!tX`Zo$0RD~B)VscWp(FR|!a!{|Q$={;ZWl%10vBzfgWn}WBe!%cug z^G%;J-L4<6&aCKx@@(Grsf}dh8fuGT+TmhhA)_16uB!t{HIAK!B-7fJLe9fsF)4G- zf>(~ⅅ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 H-+%o-(+5aQ literal 0 HcmV?d00001 diff --git a/flutter_app/android/gradle/wrapper/gradle-wrapper.properties b/flutter_app/android/gradle/wrapper/gradle-wrapper.properties index 7bb2df6..efdcc4a 100644 --- a/flutter_app/android/gradle/wrapper/gradle-wrapper.properties +++ b/flutter_app/android/gradle/wrapper/gradle-wrapper.properties @@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-all.zip diff --git a/flutter_app/android/gradlew b/flutter_app/android/gradlew new file mode 100644 index 0000000..9d82f78 --- /dev/null +++ b/flutter_app/android/gradlew @@ -0,0 +1,160 @@ +#!/usr/bin/env bash + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn ( ) { + echo "$*" +} + +die ( ) { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; +esac + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules +function splitJvmOpts() { + JVM_OPTS=("$@") +} +eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS +JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" + +exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" diff --git a/flutter_app/android/gradlew.bat b/flutter_app/android/gradlew.bat new file mode 100644 index 0000000..8a0b282 --- /dev/null +++ b/flutter_app/android/gradlew.bat @@ -0,0 +1,90 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windowz variants + +if not "%OS%" == "Windows_NT" goto win9xME_args +if "%@eval[2+2]" == "4" goto 4NT_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* +goto execute + +:4NT_args +@rem Get arguments from the 4NT Shell from JP Software +set CMD_LINE_ARGS=%$ + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/flutter_app/android/settings.gradle b/flutter_app/android/settings.gradle index 54b690d..b507b94 100644 --- a/flutter_app/android/settings.gradle +++ b/flutter_app/android/settings.gradle @@ -18,8 +18,8 @@ pluginManagement { plugins { id "dev.flutter.flutter-plugin-loader" version "1.0.0" - id "com.android.application" version "8.1.0" apply false - id "org.jetbrains.kotlin.android" version "1.9.0" apply false + id "com.android.application" version "8.9.1" apply false + id "org.jetbrains.kotlin.android" version "2.1.0" apply false } include ":app" diff --git a/flutter_app/lib/app.dart b/flutter_app/lib/app.dart index 5732c6a..89cbb3b 100644 --- a/flutter_app/lib/app.dart +++ b/flutter_app/lib/app.dart @@ -1,6 +1,9 @@ import 'package:flutter/material.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:provider/provider.dart'; +import 'l10n/app_localizations.dart'; +import 'providers/locale_provider.dart'; import 'providers/setup_provider.dart'; import 'providers/gateway_provider.dart'; import 'providers/node_provider.dart'; @@ -41,6 +44,7 @@ class OpenClawApp extends StatelessWidget { Widget build(BuildContext context) { return MultiProvider( providers: [ + ChangeNotifierProvider(create: (_) => LocaleProvider()..load()), ChangeNotifierProvider(create: (_) => SetupProvider()), ChangeNotifierProvider(create: (_) => GatewayProvider()), ChangeNotifierProxyProvider( @@ -51,13 +55,38 @@ class OpenClawApp extends StatelessWidget { }, ), ], - child: MaterialApp( - title: 'OpenClaw', - debugShowCheckedModeBanner: false, - theme: _buildLightTheme(), - darkTheme: _buildDarkTheme(), - themeMode: ThemeMode.system, - home: const SplashScreen(), + child: Consumer( + builder: (context, localeProvider, _) => MaterialApp( + debugShowCheckedModeBanner: false, + onGenerateTitle: (context) => context.l10n.t('appName'), + locale: localeProvider.locale, + localeListResolutionCallback: (deviceLocales, supportedLocales) { + if (localeProvider.locale != null) { + return localeProvider.locale; + } + + for (final deviceLocale in deviceLocales ?? const []) { + for (final supportedLocale in supportedLocales) { + if (supportedLocale.languageCode == deviceLocale.languageCode) { + return supportedLocale; + } + } + } + + return supportedLocales.first; + }, + supportedLocales: AppLocalizations.supportedLocales, + localizationsDelegates: const [ + AppLocalizations.delegate, + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + theme: _buildLightTheme(), + darkTheme: _buildDarkTheme(), + themeMode: ThemeMode.system, + home: const SplashScreen(), + ), ), ); } @@ -95,7 +124,7 @@ class OpenClawApp extends StatelessWidget { color: Colors.white, ), ), - cardTheme: CardTheme( + cardTheme: CardThemeData( elevation: 0, color: AppColors.darkSurface, shape: RoundedRectangleBorder( @@ -165,7 +194,7 @@ class OpenClawApp extends StatelessWidget { color: AppColors.darkBorder, space: 1, ), - dialogTheme: DialogTheme( + dialogTheme: DialogThemeData( backgroundColor: AppColors.darkSurface, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16), @@ -226,7 +255,7 @@ class OpenClawApp extends StatelessWidget { color: const Color(0xFF0A0A0A), ), ), - cardTheme: CardTheme( + cardTheme: CardThemeData( elevation: 0, color: AppColors.lightBg, shape: RoundedRectangleBorder( @@ -296,7 +325,7 @@ class OpenClawApp extends StatelessWidget { color: AppColors.lightBorder, space: 1, ), - dialogTheme: DialogTheme( + dialogTheme: DialogThemeData( backgroundColor: AppColors.lightBg, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16), diff --git a/flutter_app/lib/l10n/app_localizations.dart b/flutter_app/lib/l10n/app_localizations.dart new file mode 100644 index 0000000..1fc85b2 --- /dev/null +++ b/flutter_app/lib/l10n/app_localizations.dart @@ -0,0 +1,528 @@ +import 'package:flutter/material.dart'; + +class AppLocalizations { + AppLocalizations(this.locale); + + final Locale locale; + + static const supportedLocales = [ + Locale('en'), + Locale('zh'), + ]; + + static const LocalizationsDelegate delegate = + _AppLocalizationsDelegate(); + + static AppLocalizations of(BuildContext context) { + final localizations = Localizations.of( + context, + AppLocalizations, + ); + assert(localizations != null, 'AppLocalizations not found in context'); + return localizations!; + } + + String t(String key, [Map params = const {}]) { + final localized = + _localizedValues[locale.languageCode] ?? _localizedValues['en']!; + final fallback = _localizedValues['en']!; + var value = localized[key] ?? fallback[key] ?? key; + for (final entry in params.entries) { + value = value.replaceAll('{${entry.key}}', '${entry.value ?? ''}'); + } + return value; + } + + static const Map> _localizedValues = { + 'en': { + 'appName': 'OpenClaw', + 'language': 'Language', + 'languageSystem': 'System default', + 'languageEnglish': 'English', + 'languageChinese': 'Simplified Chinese', + 'commonInstalled': 'Installed', + 'commonNotInstalled': 'Not installed', + 'commonCancel': 'Cancel', + 'commonCopy': 'Copy', + 'commonCopiedToClipboard': 'Copied to clipboard', + 'commonOpen': 'Open', + 'commonPaste': 'Paste', + 'commonRetry': 'Retry', + 'commonDone': 'Done', + 'commonConfigure': 'Configure', + 'commonScreenshot': 'Screenshot', + 'commonSaveFailed': 'Failed to capture screenshot', + 'commonScreenshotSaved': 'Screenshot saved: {fileName}', + 'commonNoUrlFound': 'No URL found in selection', + 'commonOpenLink': 'Open Link', + 'commonLinkCopied': 'Link copied', + 'dashboardQuickActions': 'Quick actions', + 'dashboardTerminalTitle': 'Terminal', + 'dashboardTerminalSubtitle': 'Open Ubuntu shell with OpenClaw', + 'dashboardWebDashboardTitle': 'Web Dashboard', + 'dashboardWebDashboardSubtitle': 'Open OpenClaw dashboard in browser', + 'dashboardStartGatewayFirst': 'Start gateway first', + 'dashboardOnboardingTitle': 'Onboarding', + 'dashboardOnboardingSubtitle': 'Configure API keys and binding', + 'dashboardConfigureTitle': 'Configure', + 'dashboardConfigureSubtitle': 'Manage gateway settings', + 'dashboardProvidersTitle': 'AI Providers', + 'dashboardProvidersSubtitle': 'Configure models and API keys', + 'dashboardPackagesTitle': 'Packages', + 'dashboardPackagesSubtitle': 'Install optional tools (Go, Homebrew, SSH)', + 'dashboardSshTitle': 'SSH Access', + 'dashboardSshSubtitle': 'Remote terminal access via SSH', + 'dashboardLogsTitle': 'Logs', + 'dashboardLogsSubtitle': 'View gateway output and errors', + 'dashboardSnapshotTitle': 'Snapshot', + 'dashboardSnapshotSubtitle': 'Backup or restore your config', + 'dashboardNodeTitle': 'Node', + 'dashboardNodeConnected': 'Connected to gateway', + 'dashboardNodeDisabled': 'Device capabilities for AI', + 'dashboardVersionLabel': 'OpenClaw v{version}', + 'dashboardAuthorLabel': 'by {author} | {org}', + 'gatewayTitle': 'Gateway', + 'gatewayCopyUrl': 'Copy URL', + 'gatewayUrlCopied': 'URL copied to clipboard', + 'gatewayOpenDashboard': 'Open dashboard', + 'gatewayStart': 'Start Gateway', + 'gatewayStop': 'Stop Gateway', + 'gatewayViewLogs': 'View Logs', + 'gatewayStatusRunning': 'Running', + 'gatewayStatusStarting': 'Starting', + 'gatewayStatusError': 'Error', + 'gatewayStatusStopped': 'Stopped', + 'logsTitle': 'Gateway Logs', + 'logsAutoScrollOn': 'Auto-scroll on', + 'logsAutoScrollOff': 'Auto-scroll off', + 'logsCopyAll': 'Copy all logs', + 'logsFilterHint': 'Filter logs...', + 'logsEmpty': 'No logs yet. Start the gateway.', + 'logsNoMatch': 'No matching logs.', + 'logsCopied': 'Logs copied to clipboard', + 'packagesTitle': 'Optional Packages', + 'packagesDescription': + 'Development tools you can install inside the Ubuntu environment.', + 'packagesInstall': 'Install', + 'packagesUninstall': 'Uninstall', + 'packagesUninstallTitle': 'Uninstall {name}?', + 'packagesUninstallDescription': + 'This will remove {name} from the environment.', + 'packageGoDescription': 'Go programming language compiler and tools', + 'packageBrewDescription': 'The missing package manager for Linux', + 'packageSshDescription': 'SSH client and server for secure remote access', + 'setupWizardTitle': 'Setup OpenClaw', + 'setupWizardIntroIdle': + 'This will download Ubuntu, Node.js, and OpenClaw into a self-contained environment.', + 'setupWizardIntroRunning': + 'Setting up the environment. This may take several minutes.', + 'setupWizardConfigureApiKeys': 'Configure API Keys', + 'setupWizardRetry': 'Retry Setup', + 'setupWizardBegin': 'Begin Setup', + 'setupWizardRequirements': + 'Requires ~500MB of storage and an internet connection', + 'setupWizardStorageDialogTitle': 'Grant file access before setup', + 'setupWizardStorageDialogBody': + 'OpenClaw setup needs file management access before installation starts, so the Ubuntu environment can mount shared storage correctly inside proot. The Android permission page will open next.', + 'setupWizardStoragePermissionRequired': + 'File management access is required before setup can start.', + 'setupWizardOptionalPackages': 'OPTIONAL PACKAGES', + 'setupWizardStepDownloadRootfs': 'Download Ubuntu rootfs', + 'setupWizardStepExtractRootfs': 'Extract rootfs', + 'setupWizardStepInstallNode': 'Install Node.js', + 'setupWizardStepInstallOpenClaw': 'Install OpenClaw', + 'setupWizardStepConfigureBypass': 'Configure Bionic Bypass', + 'setupWizardComplete': 'Setup complete!', + 'setupWizardStatusSetupComplete': 'Setup complete', + 'setupWizardStatusSetupRequired': 'Setup required', + 'setupWizardStatusSettingUpDirs': 'Setting up directories...', + 'setupWizardStatusDownloadingUbuntuRootfs': + 'Downloading Ubuntu rootfs...', + 'setupWizardStatusDownloadingProgress': + 'Downloading: {current} MB / {total} MB', + 'setupWizardStatusExtractingRootfs': + 'Extracting rootfs (this takes a while)...', + 'setupWizardStatusRootfsExtracted': 'Rootfs extracted', + 'setupWizardStatusFixingPermissions': 'Fixing rootfs permissions...', + 'setupWizardStatusUpdatingPackageLists': 'Updating package lists...', + 'setupWizardStatusInstallingBasePackages': 'Installing base packages...', + 'setupWizardStatusDownloadingNode': 'Downloading Node.js {version}...', + 'setupWizardStatusDownloadingNodeProgress': + 'Downloading Node.js: {current} MB / {total} MB', + 'setupWizardStatusExtractingNode': 'Extracting Node.js...', + 'setupWizardStatusVerifyingNode': 'Verifying Node.js...', + 'setupWizardStatusNodeInstalled': 'Node.js installed', + 'setupWizardStatusInstallingOpenClaw': + 'Installing OpenClaw (this may take a few minutes)...', + 'setupWizardStatusCreatingBinWrappers': 'Creating bin wrappers...', + 'setupWizardStatusVerifyingOpenClaw': 'Verifying OpenClaw...', + 'setupWizardStatusOpenClawInstalled': 'OpenClaw installed', + 'setupWizardStatusBypassConfigured': 'Bionic Bypass configured', + 'setupWizardStatusReady': 'Setup complete! Ready to start the gateway.', + 'onboardingTitle': 'OpenClaw Onboarding', + 'onboardingStarting': 'Starting onboarding...', + 'onboardingGoToDashboard': 'Go to Dashboard', + 'onboardingStartFailed': 'Failed to start onboarding: {error}', + 'configureTitle': 'OpenClaw Configure', + 'configureStarting': 'Starting configure...', + 'configureStartFailed': 'Failed to start configure: {error}', + 'nodeTitle': 'Node', + 'nodeConfigurationTitle': 'Node Configuration', + 'nodeGatewayConnection': 'Gateway connection', + 'nodeLocalGateway': 'Local Gateway', + 'nodeLocalGatewaySubtitle': 'Auto-pair with gateway on this device', + 'nodeRemoteGateway': 'Remote Gateway', + 'nodeRemoteGatewaySubtitle': 'Connect to a gateway on another device', + 'nodeGatewayHost': 'Gateway Host', + 'nodeGatewayPort': 'Gateway Port', + 'nodeGatewayToken': 'Gateway Token', + 'nodeGatewayTokenHint': 'Paste token from gateway dashboard URL', + 'nodeGatewayTokenHelper': 'Found in dashboard URL after #token=', + 'nodeConnect': 'Connect', + 'nodePairing': 'Pairing', + 'nodeApproveCode': 'Approve this code on the gateway:', + 'nodeCapabilities': 'Capabilities', + 'nodeCapabilityCameraTitle': 'Camera', + 'nodeCapabilityCameraSubtitle': 'Capture photos and video clips', + 'nodeCapabilityCanvasTitle': 'Canvas', + 'nodeCapabilityCanvasSubtitle': 'Navigate and interact with web pages', + 'nodeCapabilityLocationTitle': 'Location', + 'nodeCapabilityLocationSubtitle': 'Get device GPS coordinates', + 'nodeCapabilityScreenTitle': 'Screen Recording', + 'nodeCapabilityScreenSubtitle': + 'Record device screen (requires consent each time)', + 'nodeCapabilityFlashlightTitle': 'Flashlight', + 'nodeCapabilityFlashlightSubtitle': 'Toggle device torch on/off', + 'nodeCapabilityVibrationTitle': 'Vibration', + 'nodeCapabilityVibrationSubtitle': + 'Trigger haptic feedback and vibration patterns', + 'nodeCapabilitySensorsTitle': 'Sensors', + 'nodeCapabilitySensorsSubtitle': + 'Read accelerometer, gyroscope, magnetometer, barometer', + 'nodeDeviceInfo': 'Device info', + 'nodeDeviceId': 'Device ID', + 'nodeLogs': 'Node logs', + 'nodeNoLogs': 'No logs yet', + 'nodeConnectedTo': 'Connected to {host}:{port}', + 'nodePairingCode': 'Pairing code: ', + 'nodeEnable': 'Enable Node', + 'nodeDisable': 'Disable Node', + 'nodeReconnect': 'Reconnect', + 'nodeStatusPaired': 'Paired', + 'nodeStatusConnecting': 'Connecting', + 'nodeStatusError': 'Error', + 'nodeStatusDisabled': 'Disabled', + 'nodeStatusDisconnected': 'Disconnected', + 'settingsTitle': 'Settings', + 'settingsGeneral': 'General', + 'settingsAutoStart': 'Auto-start gateway', + 'settingsAutoStartSubtitle': 'Start the gateway when the app opens', + 'settingsBatteryOptimization': 'Battery Optimization', + 'settingsBatteryOptimized': 'Optimized (may kill background sessions)', + 'settingsBatteryUnrestricted': 'Unrestricted (recommended)', + 'settingsStorage': 'Setup Storage', + 'settingsStorageGranted': 'Granted — /sdcard accessible in proot', + 'settingsStorageMissing': 'Allow access to shared storage', + 'settingsStorageDialogTitle': 'Grant file access', + 'settingsStorageDialogBody': + 'OpenClaw needs file management access to read and write snapshot files in shared storage. You will be taken to the system settings page next.', + 'settingsStorageDialogAction': 'Continue', + 'onboardingStorageDialogTitle': 'Grant file access for onboarding', + 'onboardingStorageDialogBody': + 'OpenClaw onboarding needs file management access so Ubuntu can mount shared storage inside proot while you configure API keys and binding. The Android permission page will open next.', + 'onboardingStoragePermissionRequired': + 'File management access is required before onboarding can continue.', + 'configureStorageDialogTitle': 'Grant file access for configuration', + 'configureStorageDialogBody': + 'OpenClaw configuration needs file management access so Ubuntu can mount shared storage inside proot while you manage gateway settings. The Android permission page will open next.', + 'configureStoragePermissionRequired': + 'File management access is required before configuration can continue.', + 'settingsNode': 'Node', + 'settingsEnableNode': 'Enable Node', + 'settingsEnableNodeSubtitle': + 'Provide device capabilities to the gateway', + 'settingsNodeConfiguration': 'Node Configuration', + 'settingsNodeConfigurationSubtitle': + 'Connection, pairing, and capabilities', + 'settingsSystemInfo': 'System info', + 'settingsArchitecture': 'Architecture', + 'settingsProotPath': 'PRoot path', + 'settingsRootfs': 'Rootfs', + 'settingsNodeJs': 'Node.js', + 'settingsOpenClaw': 'OpenClaw', + 'settingsGo': 'Go (Golang)', + 'settingsHomebrew': 'Homebrew', + 'settingsOpenSsh': 'OpenSSH', + 'settingsMaintenance': 'Maintenance', + 'settingsExportSnapshot': 'Export Snapshot', + 'settingsExportSnapshotSubtitle': 'Backup config to Downloads', + 'settingsImportSnapshot': 'Import Snapshot', + 'settingsImportSnapshotSubtitle': 'Restore config from backup', + 'settingsRerunSetup': 'Re-run setup', + 'settingsRerunSetupSubtitle': 'Reinstall or repair the environment', + 'settingsAbout': 'About', + 'settingsAboutSubtitle': 'AI Gateway for Android\nVersion {version}', + 'settingsDeveloper': 'Developer', + 'settingsGithub': 'GitHub', + 'settingsContact': 'Contact', + 'settingsLicense': 'License', + 'settingsPlayStore': 'Play Store', + 'settingsEmail': 'Email', + 'settingsSnapshotSaved': 'Snapshot saved to {path}', + 'settingsExportFailed': 'Export failed: {error}', + 'settingsSnapshotMissing': 'No snapshot found at {path}', + 'settingsSnapshotRestored': + 'Snapshot restored successfully. Restart the gateway to apply.', + 'settingsImportFailed': 'Import failed: {error}', + 'statusInstalled': 'Installed', + 'statusNotInstalled': 'Not installed', + }, + 'zh': { + 'appName': 'OpenClaw', + 'language': '语言', + 'languageSystem': '跟随系统', + 'languageEnglish': '英语', + 'languageChinese': '简体中文', + 'commonInstalled': '已安装', + 'commonNotInstalled': '未安装', + 'commonCancel': '取消', + 'commonCopy': '复制', + 'commonCopiedToClipboard': '已复制到剪贴板', + 'commonOpen': '打开', + 'commonPaste': '粘贴', + 'commonRetry': '重试', + 'commonDone': '完成', + 'commonConfigure': '配置', + 'commonScreenshot': '截图', + 'commonSaveFailed': '截图失败', + 'commonScreenshotSaved': '截图已保存:{fileName}', + 'commonNoUrlFound': '所选内容中未找到 URL', + 'commonOpenLink': '打开链接', + 'commonLinkCopied': '链接已复制', + 'dashboardQuickActions': '快捷操作', + 'dashboardTerminalTitle': '终端', + 'dashboardTerminalSubtitle': '打开 Ubuntu Shell 并使用 OpenClaw', + 'dashboardWebDashboardTitle': 'Web 控制台', + 'dashboardWebDashboardSubtitle': '在浏览器中打开 OpenClaw 控制台', + 'dashboardStartGatewayFirst': '请先启动网关', + 'dashboardOnboardingTitle': '引导配置', + 'dashboardOnboardingSubtitle': '配置 API Key 和绑定信息', + 'dashboardConfigureTitle': '网关配置', + 'dashboardConfigureSubtitle': '管理网关设置', + 'dashboardProvidersTitle': 'AI 提供商', + 'dashboardProvidersSubtitle': '配置模型和 API Key', + 'dashboardPackagesTitle': '可选组件', + 'dashboardPackagesSubtitle': '安装可选工具(Go、Homebrew、SSH)', + 'dashboardSshTitle': 'SSH 访问', + 'dashboardSshSubtitle': '通过 SSH 远程访问终端', + 'dashboardLogsTitle': '日志', + 'dashboardLogsSubtitle': '查看网关输出和错误', + 'dashboardSnapshotTitle': '快照', + 'dashboardSnapshotSubtitle': '备份或恢复你的配置', + 'dashboardNodeTitle': '节点', + 'dashboardNodeConnected': '已连接到网关', + 'dashboardNodeDisabled': '为 AI 提供设备能力', + 'dashboardVersionLabel': 'OpenClaw v{version}', + 'dashboardAuthorLabel': '作者 {author} | {org}', + 'gatewayTitle': '网关', + 'gatewayCopyUrl': '复制 URL', + 'gatewayUrlCopied': 'URL 已复制到剪贴板', + 'gatewayOpenDashboard': '打开控制台', + 'gatewayStart': '启动网关', + 'gatewayStop': '停止网关', + 'gatewayViewLogs': '查看日志', + 'gatewayStatusRunning': '运行中', + 'gatewayStatusStarting': '启动中', + 'gatewayStatusError': '错误', + 'gatewayStatusStopped': '已停止', + 'logsTitle': '网关日志', + 'logsAutoScrollOn': '自动滚动已开启', + 'logsAutoScrollOff': '自动滚动已关闭', + 'logsCopyAll': '复制全部日志', + 'logsFilterHint': '筛选日志...', + 'logsEmpty': '还没有日志。请先启动网关。', + 'logsNoMatch': '没有匹配的日志。', + 'logsCopied': '日志已复制到剪贴板', + 'packagesTitle': '可选组件', + 'packagesDescription': '可在 Ubuntu 环境内安装的开发工具。', + 'packagesInstall': '安装', + 'packagesUninstall': '卸载', + 'packagesUninstallTitle': '卸载 {name}?', + 'packagesUninstallDescription': '这会将 {name} 从环境中移除。', + 'packageGoDescription': 'Go 编程语言编译器和工具链', + 'packageBrewDescription': 'Linux 上常用的缺省包管理器', + 'packageSshDescription': '用于安全远程访问的 SSH 客户端和服务端', + 'setupWizardTitle': '开始配置 OpenClaw', + 'setupWizardIntroIdle': '这会将 Ubuntu、Node.js 和 OpenClaw 下载到一个自包含环境中。', + 'setupWizardIntroRunning': '正在配置环境,可能需要几分钟。', + 'setupWizardConfigureApiKeys': '配置 API Key', + 'setupWizardRetry': '重新安装', + 'setupWizardBegin': '开始安装', + 'setupWizardRequirements': '需要约 500MB 存储空间和网络连接', + 'setupWizardStorageDialogTitle': '安装前授予文件访问权限', + 'setupWizardStorageDialogBody': + 'OpenClaw 在开始安装前需要文件管理权限,这样 Ubuntu 环境才能在 proot 中正确挂载共享存储。接下来会打开 Android 授权页面。', + 'setupWizardStoragePermissionRequired': '开始安装前,必须先授予文件管理权限。', + 'setupWizardOptionalPackages': '可选组件', + 'setupWizardStepDownloadRootfs': '下载 Ubuntu rootfs', + 'setupWizardStepExtractRootfs': '解压 rootfs', + 'setupWizardStepInstallNode': '安装 Node.js', + 'setupWizardStepInstallOpenClaw': '安装 OpenClaw', + 'setupWizardStepConfigureBypass': '配置 Bionic Bypass', + 'setupWizardComplete': '安装完成!', + 'setupWizardStatusSetupComplete': '安装完成', + 'setupWizardStatusSetupRequired': '需要安装环境', + 'setupWizardStatusSettingUpDirs': '正在准备目录...', + 'setupWizardStatusDownloadingUbuntuRootfs': '正在下载 Ubuntu rootfs...', + 'setupWizardStatusDownloadingProgress': '正在下载:{current} MB / {total} MB', + 'setupWizardStatusExtractingRootfs': '正在解压 rootfs(这会花一点时间)...', + 'setupWizardStatusRootfsExtracted': 'rootfs 已解压', + 'setupWizardStatusFixingPermissions': '正在修复 rootfs 权限...', + 'setupWizardStatusUpdatingPackageLists': '正在更新软件包列表...', + 'setupWizardStatusInstallingBasePackages': '正在安装基础软件包...', + 'setupWizardStatusDownloadingNode': '正在下载 Node.js {version}...', + 'setupWizardStatusDownloadingNodeProgress': + '正在下载 Node.js:{current} MB / {total} MB', + 'setupWizardStatusExtractingNode': '正在解压 Node.js...', + 'setupWizardStatusVerifyingNode': '正在验证 Node.js...', + 'setupWizardStatusNodeInstalled': 'Node.js 已安装', + 'setupWizardStatusInstallingOpenClaw': '正在安装 OpenClaw(这可能需要几分钟)...', + 'setupWizardStatusCreatingBinWrappers': '正在创建命令包装器...', + 'setupWizardStatusVerifyingOpenClaw': '正在验证 OpenClaw...', + 'setupWizardStatusOpenClawInstalled': 'OpenClaw 已安装', + 'setupWizardStatusBypassConfigured': 'Bionic Bypass 已配置', + 'setupWizardStatusReady': '安装完成,可以开始启动网关了。', + 'onboardingTitle': 'OpenClaw 引导配置', + 'onboardingStarting': '正在启动引导配置...', + 'onboardingGoToDashboard': '前往控制台', + 'onboardingStartFailed': '启动引导配置失败:{error}', + 'configureTitle': 'OpenClaw 配置', + 'configureStarting': '正在启动配置...', + 'configureStartFailed': '启动配置失败:{error}', + 'nodeTitle': '节点', + 'nodeConfigurationTitle': '节点配置', + 'nodeGatewayConnection': '网关连接', + 'nodeLocalGateway': '本地网关', + 'nodeLocalGatewaySubtitle': '自动配对本机上的网关', + 'nodeRemoteGateway': '远程网关', + 'nodeRemoteGatewaySubtitle': '连接到其他设备上的网关', + 'nodeGatewayHost': '网关主机', + 'nodeGatewayPort': '网关端口', + 'nodeGatewayToken': '网关令牌', + 'nodeGatewayTokenHint': '粘贴控制台 URL 中的令牌', + 'nodeGatewayTokenHelper': '可在控制台 URL 的 #token= 后找到', + 'nodeConnect': '连接', + 'nodePairing': '配对', + 'nodeApproveCode': '请在网关端确认此代码:', + 'nodeCapabilities': '能力', + 'nodeCapabilityCameraTitle': '相机', + 'nodeCapabilityCameraSubtitle': '拍摄照片和视频片段', + 'nodeCapabilityCanvasTitle': 'Canvas', + 'nodeCapabilityCanvasSubtitle': '浏览并交互网页', + 'nodeCapabilityLocationTitle': '定位', + 'nodeCapabilityLocationSubtitle': '获取设备 GPS 坐标', + 'nodeCapabilityScreenTitle': '录屏', + 'nodeCapabilityScreenSubtitle': '录制设备屏幕(每次都需授权)', + 'nodeCapabilityFlashlightTitle': '手电筒', + 'nodeCapabilityFlashlightSubtitle': '切换设备闪光灯开关', + 'nodeCapabilityVibrationTitle': '震动', + 'nodeCapabilityVibrationSubtitle': '触发触觉反馈和震动模式', + 'nodeCapabilitySensorsTitle': '传感器', + 'nodeCapabilitySensorsSubtitle': '读取加速度计、陀螺仪、磁力计、气压计', + 'nodeDeviceInfo': '设备信息', + 'nodeDeviceId': '设备 ID', + 'nodeLogs': '节点日志', + 'nodeNoLogs': '还没有日志', + 'nodeConnectedTo': '已连接到 {host}:{port}', + 'nodePairingCode': '配对码:', + 'nodeEnable': '启用节点', + 'nodeDisable': '禁用节点', + 'nodeReconnect': '重新连接', + 'nodeStatusPaired': '已配对', + 'nodeStatusConnecting': '连接中', + 'nodeStatusError': '错误', + 'nodeStatusDisabled': '已禁用', + 'nodeStatusDisconnected': '未连接', + 'settingsTitle': '设置', + 'settingsGeneral': '常规', + 'settingsAutoStart': '自动启动网关', + 'settingsAutoStartSubtitle': '应用打开时自动启动网关', + 'settingsBatteryOptimization': '电池优化', + 'settingsBatteryOptimized': '已优化(可能会杀死后台会话)', + 'settingsBatteryUnrestricted': '不受限制(推荐)', + 'settingsStorage': '存储访问', + 'settingsStorageGranted': '已授权,可在 proot 中访问 /sdcard', + 'settingsStorageMissing': '允许访问共享存储', + 'settingsStorageDialogTitle': '授予文件访问权限', + 'settingsStorageDialogBody': + 'OpenClaw 需要文件管理权限,才能在共享存储中读取和写入快照文件。接下来会跳转到系统设置页面。', + 'settingsStorageDialogAction': '继续', + 'onboardingStorageDialogTitle': '为引导配置授予文件访问权限', + 'onboardingStorageDialogBody': + 'OpenClaw 引导配置需要文件管理权限,Ubuntu 才能在 proot 中挂载共享存储,并完成 API Key 与绑定配置。接下来会打开 Android 授权页面。', + 'onboardingStoragePermissionRequired': '继续引导配置前,必须先授予文件管理权限。', + 'configureStorageDialogTitle': '为配置页面授予文件访问权限', + 'configureStorageDialogBody': + 'OpenClaw 配置页面需要文件管理权限,Ubuntu 才能在 proot 中挂载共享存储并管理网关设置。接下来会打开 Android 授权页面。', + 'configureStoragePermissionRequired': '继续配置前,必须先授予文件管理权限。', + 'settingsNode': '节点', + 'settingsEnableNode': '启用节点', + 'settingsEnableNodeSubtitle': '向网关提供设备能力', + 'settingsNodeConfiguration': '节点配置', + 'settingsNodeConfigurationSubtitle': '连接、配对和能力设置', + 'settingsSystemInfo': '系统信息', + 'settingsArchitecture': '架构', + 'settingsProotPath': 'PRoot 路径', + 'settingsRootfs': 'Rootfs', + 'settingsNodeJs': 'Node.js', + 'settingsOpenClaw': 'OpenClaw', + 'settingsGo': 'Go(Golang)', + 'settingsHomebrew': 'Homebrew', + 'settingsOpenSsh': 'OpenSSH', + 'settingsMaintenance': '维护', + 'settingsExportSnapshot': '导出快照', + 'settingsExportSnapshotSubtitle': '将配置备份到 Downloads', + 'settingsImportSnapshot': '导入快照', + 'settingsImportSnapshotSubtitle': '从备份恢复配置', + 'settingsRerunSetup': '重新运行安装', + 'settingsRerunSetupSubtitle': '重新安装或修复环境', + 'settingsAbout': '关于', + 'settingsAboutSubtitle': 'Android AI 网关\n版本 {version}', + 'settingsDeveloper': '开发者', + 'settingsGithub': 'GitHub', + 'settingsContact': '联系方式', + 'settingsLicense': '许可证', + 'settingsPlayStore': 'Play 商店', + 'settingsEmail': '邮箱', + 'settingsSnapshotSaved': '快照已保存到 {path}', + 'settingsExportFailed': '导出失败:{error}', + 'settingsSnapshotMissing': '在 {path} 未找到快照', + 'settingsSnapshotRestored': '快照已恢复。请重启网关以生效。', + 'settingsImportFailed': '导入失败:{error}', + 'statusInstalled': '已安装', + 'statusNotInstalled': '未安装', + }, + }; +} + +class _AppLocalizationsDelegate + extends LocalizationsDelegate { + const _AppLocalizationsDelegate(); + + @override + bool isSupported(Locale locale) => AppLocalizations.supportedLocales.any( + (supported) => supported.languageCode == locale.languageCode, + ); + + @override + Future load(Locale locale) async { + return AppLocalizations(locale); + } + + @override + bool shouldReload(_AppLocalizationsDelegate old) => false; +} + +extension AppLocalizationsContextExtension on BuildContext { + AppLocalizations get l10n => AppLocalizations.of(this); +} diff --git a/flutter_app/lib/providers/locale_provider.dart b/flutter_app/lib/providers/locale_provider.dart new file mode 100644 index 0000000..5547085 --- /dev/null +++ b/flutter_app/lib/providers/locale_provider.dart @@ -0,0 +1,43 @@ +import 'package:flutter/material.dart'; + +import '../services/preferences_service.dart'; + +class LocaleProvider extends ChangeNotifier { + final PreferencesService _prefs = PreferencesService(); + + Locale? _locale; + bool _initialized = false; + + Locale? get locale => _locale; + String get localeCode => _locale?.languageCode ?? 'system'; + + Future load() async { + if (_initialized) return; + await _prefs.init(); + _initialized = true; + _locale = _mapCodeToLocale(_prefs.localeCode); + notifyListeners(); + } + + Future setLocaleCode(String code) async { + if (!_initialized) { + await load(); + } + + final normalized = code == 'system' ? null : code; + _prefs.localeCode = normalized; + _locale = _mapCodeToLocale(normalized); + notifyListeners(); + } + + Locale? _mapCodeToLocale(String? code) { + switch (code) { + case 'en': + return const Locale('en'); + case 'zh': + return const Locale('zh'); + default: + return null; + } + } +} diff --git a/flutter_app/lib/screens/configure_screen.dart b/flutter_app/lib/screens/configure_screen.dart index f695700..e2bec5a 100644 --- a/flutter_app/lib/screens/configure_screen.dart +++ b/flutter_app/lib/screens/configure_screen.dart @@ -5,8 +5,10 @@ import 'package:flutter/services.dart'; import 'package:xterm/xterm.dart'; import 'package:flutter_pty/flutter_pty.dart'; import 'package:url_launcher/url_launcher.dart'; +import '../l10n/app_localizations.dart'; import '../services/native_bridge.dart'; import '../services/screenshot_service.dart'; +import '../services/storage_permission_service.dart'; import '../services/terminal_service.dart'; import '../widgets/terminal_toolbar.dart'; @@ -59,9 +61,29 @@ class _ConfigureScreenState extends State { _pty?.kill(); _pty = null; try { + final hasStoragePermission = + await StoragePermissionService.ensurePermission( + context, + dialogTitleKey: 'configureStorageDialogTitle', + dialogBodyKey: 'configureStorageDialogBody', + ); + if (!hasStoragePermission) { + if (mounted) { + setState(() { + _loading = false; + _error = context.l10n.t('configureStoragePermissionRequired'); + }); + } + return; + } + // Ensure dirs + resolv.conf exist before proot starts (#40). - try { await NativeBridge.setupDirs(); } catch (_) {} - try { await NativeBridge.writeResolv(); } catch (_) {} + try { + await NativeBridge.setupDirs(); + } catch (_) {} + try { + await NativeBridge.writeResolv(); + } catch (_) {} try { final filesDir = await NativeBridge.getFilesDir(); const resolvContent = 'nameserver 8.8.8.8\nnameserver 8.8.4.4\n'; @@ -88,12 +110,13 @@ class _ConfigureScreenState extends State { configureArgs.removeLast(); // remove '-l' configureArgs.removeLast(); // remove '/bin/bash' configureArgs.addAll([ - '/bin/bash', '-lc', + '/bin/bash', + '-lc', 'echo "=== OpenClaw Configure ===" && ' - 'echo "Manage your gateway settings." && ' - 'echo "" && ' - 'openclaw configure; ' - 'echo "" && echo "Configuration complete! You can close this screen."', + 'echo "Manage your gateway settings." && ' + 'echo "" && ' + 'openclaw configure; ' + 'echo "" && echo "Configuration complete! You can close this screen."', ]); _pty = Pty.start( @@ -140,7 +163,7 @@ class _ConfigureScreenState extends State { } catch (e) { setState(() { _loading = false; - _error = 'Failed to start configure: $e'; + _error = context.l10n.t('configureStartFailed', {'error': '$e'}); }); } } @@ -174,7 +197,8 @@ class _ConfigureScreenState extends State { } String? _extractUrl(String text) { - final clean = text.replaceAll(_boxDrawing, '').replaceAll(RegExp(r'\s+'), ''); + final clean = + text.replaceAll(_boxDrawing, '').replaceAll(RegExp(r'\s+'), ''); final parts = clean.split(RegExp(r'(?=https?://)')); String? best; for (final part in parts) { @@ -199,10 +223,10 @@ class _ConfigureScreenState extends State { if (url != null) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( - content: const Text('Copied to clipboard'), + content: Text(context.l10n.t('commonCopiedToClipboard')), duration: const Duration(seconds: 3), action: SnackBarAction( - label: 'Open', + label: context.l10n.t('commonOpen'), onPressed: () { final uri = Uri.tryParse(url); if (uri != null) { @@ -214,9 +238,9 @@ class _ConfigureScreenState extends State { ); } else { ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Copied to clipboard'), - duration: Duration(seconds: 1), + SnackBar( + content: Text(context.l10n.t('commonCopiedToClipboard')), + duration: const Duration(seconds: 1), ), ); } @@ -235,9 +259,9 @@ class _ConfigureScreenState extends State { } } ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('No URL found in selection'), - duration: Duration(seconds: 1), + SnackBar( + content: Text(context.l10n.t('commonNoUrlFound')), + duration: const Duration(seconds: 1), ), ); } @@ -250,22 +274,27 @@ class _ConfigureScreenState extends State { } Future _takeScreenshot() async { - final path = await ScreenshotService.capture(_screenshotKey, prefix: 'configure'); + final path = + await ScreenshotService.capture(_screenshotKey, prefix: 'configure'); if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(path != null - ? 'Screenshot saved: ${path.split('/').last}' - : 'Failed to capture screenshot'), + ? context.l10n.t('commonScreenshotSaved', { + 'fileName': path.split('/').last, + }) + : context.l10n.t('commonSaveFailed')), ), ); } @override Widget build(BuildContext context) { + final l10n = context.l10n; + return Scaffold( appBar: AppBar( - title: const Text('OpenClaw Configure'), + title: Text(l10n.t('configureTitle')), leading: IconButton( icon: const Icon(Icons.arrow_back), onPressed: () => Navigator.of(context).pop(), @@ -274,22 +303,22 @@ class _ConfigureScreenState extends State { actions: [ IconButton( icon: const Icon(Icons.camera_alt_outlined), - tooltip: 'Screenshot', + tooltip: l10n.t('commonScreenshot'), onPressed: _takeScreenshot, ), IconButton( icon: const Icon(Icons.copy), - tooltip: 'Copy', + tooltip: l10n.t('commonCopy'), onPressed: _copySelection, ), IconButton( icon: const Icon(Icons.open_in_browser), - tooltip: 'Open URL', + tooltip: l10n.t('commonOpen'), onPressed: _openSelection, ), IconButton( icon: const Icon(Icons.paste), - tooltip: 'Paste', + tooltip: l10n.t('commonPaste'), onPressed: _paste, ), ], @@ -297,14 +326,14 @@ class _ConfigureScreenState extends State { body: Column( children: [ if (_loading) - const Expanded( + Expanded( child: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - CircularProgressIndicator(), - SizedBox(height: 16), - Text('Starting configure...'), + const CircularProgressIndicator(), + const SizedBox(height: 16), + Text(l10n.t('configureStarting')), ], ), ), @@ -326,7 +355,8 @@ class _ConfigureScreenState extends State { Text( _error!, textAlign: TextAlign.center, - style: TextStyle(color: Theme.of(context).colorScheme.error), + style: TextStyle( + color: Theme.of(context).colorScheme.error), ), const SizedBox(height: 16), FilledButton.icon( @@ -339,7 +369,7 @@ class _ConfigureScreenState extends State { _startConfigure(); }, icon: const Icon(Icons.refresh), - label: const Text('Retry'), + label: Text(l10n.t('commonRetry')), ), ], ), @@ -376,7 +406,7 @@ class _ConfigureScreenState extends State { child: FilledButton.icon( onPressed: () => Navigator.of(context).pop(), icon: const Icon(Icons.check), - label: const Text('Done'), + label: Text(l10n.t('commonDone')), ), ), ), diff --git a/flutter_app/lib/screens/dashboard_screen.dart b/flutter_app/lib/screens/dashboard_screen.dart index e2632f2..1fa3919 100644 --- a/flutter_app/lib/screens/dashboard_screen.dart +++ b/flutter_app/lib/screens/dashboard_screen.dart @@ -1,6 +1,8 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../constants.dart'; +import '../l10n/app_localizations.dart'; +import '../models/node_state.dart'; import '../providers/gateway_provider.dart'; import '../providers/node_provider.dart'; import '../widgets/gateway_controls.dart'; @@ -22,10 +24,11 @@ class DashboardScreen extends StatelessWidget { @override Widget build(BuildContext context) { final theme = Theme.of(context); + final l10n = context.l10n; return Scaffold( appBar: AppBar( - title: const Text('OpenClaw'), + title: Text(l10n.t('appName')), actions: [ IconButton( icon: const Icon(Icons.settings), @@ -45,7 +48,7 @@ class DashboardScreen extends StatelessWidget { Padding( padding: const EdgeInsets.only(left: 4, bottom: 8), child: Text( - 'QUICK ACTIONS', + l10n.t('dashboardQuickActions'), style: theme.textTheme.labelSmall?.copyWith( color: theme.colorScheme.onSurfaceVariant, fontWeight: FontWeight.w600, @@ -54,8 +57,8 @@ class DashboardScreen extends StatelessWidget { ), ), StatusCard( - title: 'Terminal', - subtitle: 'Open Ubuntu shell with OpenClaw', + title: l10n.t('dashboardTerminalTitle'), + subtitle: l10n.t('dashboardTerminalSubtitle'), icon: Icons.terminal, trailing: const Icon(Icons.chevron_right), onTap: () => Navigator.of(context).push( @@ -65,10 +68,10 @@ class DashboardScreen extends StatelessWidget { Consumer( builder: (context, provider, _) { return StatusCard( - title: 'Web Dashboard', + title: l10n.t('dashboardWebDashboardTitle'), subtitle: provider.state.isRunning - ? 'Open OpenClaw dashboard in browser' - : 'Start gateway first', + ? l10n.t('dashboardWebDashboardSubtitle') + : l10n.t('dashboardStartGatewayFirst'), icon: Icons.dashboard, trailing: const Icon(Icons.chevron_right), onTap: provider.state.isRunning @@ -84,8 +87,8 @@ class DashboardScreen extends StatelessWidget { }, ), StatusCard( - title: 'Onboarding', - subtitle: 'Configure API keys and binding', + title: l10n.t('dashboardOnboardingTitle'), + subtitle: l10n.t('dashboardOnboardingSubtitle'), icon: Icons.vpn_key, trailing: const Icon(Icons.chevron_right), onTap: () => Navigator.of(context).push( @@ -93,8 +96,8 @@ class DashboardScreen extends StatelessWidget { ), ), StatusCard( - title: 'Configure', - subtitle: 'Manage gateway settings', + title: l10n.t('dashboardConfigureTitle'), + subtitle: l10n.t('dashboardConfigureSubtitle'), icon: Icons.tune, trailing: const Icon(Icons.chevron_right), onTap: () => Navigator.of(context).push( @@ -102,8 +105,8 @@ class DashboardScreen extends StatelessWidget { ), ), StatusCard( - title: 'AI Providers', - subtitle: 'Configure models and API keys', + title: l10n.t('dashboardProvidersTitle'), + subtitle: l10n.t('dashboardProvidersSubtitle'), icon: Icons.model_training, trailing: const Icon(Icons.chevron_right), onTap: () => Navigator.of(context).push( @@ -111,8 +114,8 @@ class DashboardScreen extends StatelessWidget { ), ), StatusCard( - title: 'Packages', - subtitle: 'Install optional tools (Go, Homebrew, SSH)', + title: l10n.t('dashboardPackagesTitle'), + subtitle: l10n.t('dashboardPackagesSubtitle'), icon: Icons.extension, trailing: const Icon(Icons.chevron_right), onTap: () => Navigator.of(context).push( @@ -120,8 +123,8 @@ class DashboardScreen extends StatelessWidget { ), ), StatusCard( - title: 'SSH Access', - subtitle: 'Remote terminal access via SSH', + title: l10n.t('dashboardSshTitle'), + subtitle: l10n.t('dashboardSshSubtitle'), icon: Icons.terminal, trailing: const Icon(Icons.chevron_right), onTap: () => Navigator.of(context).push( @@ -129,8 +132,8 @@ class DashboardScreen extends StatelessWidget { ), ), StatusCard( - title: 'Logs', - subtitle: 'View gateway output and errors', + title: l10n.t('dashboardLogsTitle'), + subtitle: l10n.t('dashboardLogsSubtitle'), icon: Icons.article_outlined, trailing: const Icon(Icons.chevron_right), onTap: () => Navigator.of(context).push( @@ -138,8 +141,8 @@ class DashboardScreen extends StatelessWidget { ), ), StatusCard( - title: 'Snapshot', - subtitle: 'Backup or restore your config', + title: l10n.t('dashboardSnapshotTitle'), + subtitle: l10n.t('dashboardSnapshotSubtitle'), icon: Icons.backup, trailing: const Icon(Icons.chevron_right), onTap: () => Navigator.of(context).push( @@ -150,12 +153,12 @@ class DashboardScreen extends StatelessWidget { builder: (context, nodeProvider, _) { final nodeState = nodeProvider.state; return StatusCard( - title: 'Node', + title: l10n.t('dashboardNodeTitle'), subtitle: nodeState.isPaired - ? 'Connected to gateway' + ? l10n.t('dashboardNodeConnected') : nodeState.isDisabled - ? 'Device capabilities for AI' - : nodeState.statusText, + ? l10n.t('dashboardNodeDisabled') + : _nodeStatusText(l10n, nodeState.status), icon: Icons.devices, trailing: const Icon(Icons.chevron_right), onTap: () => Navigator.of(context).push( @@ -169,14 +172,23 @@ class DashboardScreen extends StatelessWidget { child: Column( children: [ Text( - 'OpenClaw v${AppConstants.version}', + l10n.t( + 'dashboardVersionLabel', + {'version': AppConstants.version}, + ), style: theme.textTheme.bodySmall?.copyWith( color: theme.colorScheme.onSurfaceVariant, ), ), const SizedBox(height: 2), Text( - 'by ${AppConstants.authorName} | ${AppConstants.orgName}', + l10n.t( + 'dashboardAuthorLabel', + { + 'author': AppConstants.authorName, + 'org': AppConstants.orgName, + }, + ), style: theme.textTheme.bodySmall?.copyWith( color: theme.colorScheme.onSurfaceVariant, ), @@ -189,4 +201,21 @@ class DashboardScreen extends StatelessWidget { ), ); } + + String _nodeStatusText(AppLocalizations l10n, NodeStatus status) { + switch (status) { + case NodeStatus.disabled: + return l10n.t('nodeStatusDisabled'); + case NodeStatus.disconnected: + return l10n.t('nodeStatusDisconnected'); + case NodeStatus.connecting: + case NodeStatus.challenging: + case NodeStatus.pairing: + return l10n.t('nodeStatusConnecting'); + case NodeStatus.paired: + return l10n.t('nodeStatusPaired'); + case NodeStatus.error: + return l10n.t('nodeStatusError'); + } + } } diff --git a/flutter_app/lib/screens/logs_screen.dart b/flutter_app/lib/screens/logs_screen.dart index eca00e2..950ef09 100644 --- a/flutter_app/lib/screens/logs_screen.dart +++ b/flutter_app/lib/screens/logs_screen.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:provider/provider.dart'; import '../app.dart'; +import '../l10n/app_localizations.dart'; import '../providers/gateway_provider.dart'; import '../services/screenshot_service.dart'; @@ -29,26 +30,31 @@ class _LogsScreenState extends State { @override Widget build(BuildContext context) { final theme = Theme.of(context); + final l10n = context.l10n; return Scaffold( appBar: AppBar( - title: const Text('Gateway Logs'), + title: Text(l10n.t('logsTitle')), actions: [ IconButton( icon: const Icon(Icons.camera_alt_outlined), - tooltip: 'Screenshot', + tooltip: l10n.t('commonScreenshot'), onPressed: _takeScreenshot, ), IconButton( icon: Icon( - _autoScroll ? Icons.vertical_align_bottom : Icons.vertical_align_top, + _autoScroll + ? Icons.vertical_align_bottom + : Icons.vertical_align_top, ), - tooltip: _autoScroll ? 'Auto-scroll on' : 'Auto-scroll off', + tooltip: _autoScroll + ? l10n.t('logsAutoScrollOn') + : l10n.t('logsAutoScrollOff'), onPressed: () => setState(() => _autoScroll = !_autoScroll), ), IconButton( icon: const Icon(Icons.copy), - tooltip: 'Copy all logs', + tooltip: l10n.t('logsCopyAll'), onPressed: () => _copyLogs(context), ), ], @@ -60,7 +66,7 @@ class _LogsScreenState extends State { child: TextField( controller: _searchController, decoration: InputDecoration( - hintText: 'Filter logs...', + hintText: l10n.t('logsFilterHint'), prefixIcon: const Icon(Icons.search), isDense: true, contentPadding: const EdgeInsets.symmetric( @@ -84,50 +90,54 @@ class _LogsScreenState extends State { child: RepaintBoundary( key: _screenshotKey, child: Consumer( - builder: (context, provider, _) { - final logs = provider.state.logs; - final filtered = _filter.isEmpty - ? logs - : logs.where((l) => - l.toLowerCase().contains(_filter.toLowerCase())).toList(); + builder: (context, provider, _) { + final logs = provider.state.logs; + final filtered = _filter.isEmpty + ? logs + : logs + .where((l) => + l.toLowerCase().contains(_filter.toLowerCase())) + .toList(); - if (filtered.isEmpty) { - return Center( - child: Text( - logs.isEmpty ? 'No logs yet. Start the gateway.' : 'No matching logs.', - style: theme.textTheme.bodyMedium?.copyWith( - color: theme.colorScheme.onSurfaceVariant, + if (filtered.isEmpty) { + return Center( + child: Text( + logs.isEmpty + ? l10n.t('logsEmpty') + : l10n.t('logsNoMatch'), + style: theme.textTheme.bodyMedium?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + ), ), - ), - ); - } - - WidgetsBinding.instance.addPostFrameCallback((_) { - if (_autoScroll && _scrollController.hasClients) { - _scrollController.jumpTo( - _scrollController.position.maxScrollExtent, ); } - }); - return ListView.builder( - controller: _scrollController, - padding: const EdgeInsets.symmetric(horizontal: 8), - itemCount: filtered.length, - itemBuilder: (context, index) { - final line = filtered[index]; - return Text( - line, - style: TextStyle( - fontFamily: 'monospace', - fontSize: 12, - color: _logColor(line, theme), - ), - ); - }, - ); - }, - ), + WidgetsBinding.instance.addPostFrameCallback((_) { + if (_autoScroll && _scrollController.hasClients) { + _scrollController.jumpTo( + _scrollController.position.maxScrollExtent, + ); + } + }); + + return ListView.builder( + controller: _scrollController, + padding: const EdgeInsets.symmetric(horizontal: 8), + itemCount: filtered.length, + itemBuilder: (context, index) { + final line = filtered[index]; + return Text( + line, + style: TextStyle( + fontFamily: 'monospace', + fontSize: 12, + color: _logColor(line, theme), + ), + ); + }, + ); + }, + ), ), ), ], @@ -149,13 +159,16 @@ class _LogsScreenState extends State { } Future _takeScreenshot() async { - final path = await ScreenshotService.capture(_screenshotKey, prefix: 'logs'); + final path = + await ScreenshotService.capture(_screenshotKey, prefix: 'logs'); if (!mounted) return; + final l10n = context.l10n; ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(path != null - ? 'Screenshot saved: ${path.split('/').last}' - : 'Failed to capture screenshot'), + ? l10n + .t('commonScreenshotSaved', {'fileName': path.split('/').last}) + : l10n.t('commonSaveFailed')), ), ); } @@ -165,7 +178,7 @@ class _LogsScreenState extends State { final text = provider.state.logs.join('\n'); Clipboard.setData(ClipboardData(text: text)); ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Logs copied to clipboard')), + SnackBar(content: Text(context.l10n.t('logsCopied'))), ); } } diff --git a/flutter_app/lib/screens/node_screen.dart b/flutter_app/lib/screens/node_screen.dart index 44fca9c..c527e4d 100644 --- a/flutter_app/lib/screens/node_screen.dart +++ b/flutter_app/lib/screens/node_screen.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../app.dart'; +import '../l10n/app_localizations.dart'; import '../providers/node_provider.dart'; import '../services/preferences_service.dart'; import '../widgets/node_controls.dart'; @@ -51,9 +52,10 @@ class _NodeScreenState extends State { @override Widget build(BuildContext context) { final theme = Theme.of(context); + final l10n = context.l10n; return Scaffold( - appBar: AppBar(title: const Text('Node Configuration')), + appBar: AppBar(title: Text(l10n.t('nodeConfigurationTitle'))), body: _loading ? const Center(child: CircularProgressIndicator()) : Consumer( @@ -67,76 +69,82 @@ class _NodeScreenState extends State { const SizedBox(height: 16), // Gateway Connection - _sectionHeader(theme, 'GATEWAY CONNECTION'), + _sectionHeader(theme, l10n.t('nodeGatewayConnection')), Card( child: Padding( padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - RadioListTile( - title: const Text('Local Gateway'), - subtitle: const Text('Auto-pair with gateway on this device'), - value: true, - groupValue: _isLocal, - onChanged: (value) { - setState(() => _isLocal = value!); - }, - ), - RadioListTile( - title: const Text('Remote Gateway'), - subtitle: const Text('Connect to a gateway on another device'), - value: false, - groupValue: _isLocal, - onChanged: (value) { - setState(() => _isLocal = value!); - }, - ), - if (!_isLocal) ...[ - const SizedBox(height: 12), - TextField( - controller: _hostController, - decoration: const InputDecoration( - labelText: 'Gateway Host', - hintText: '192.168.1.100', - ), + child: RadioGroup( + groupValue: _isLocal, + onChanged: (value) { + if (value != null) { + setState(() => _isLocal = value); + } + }, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + RadioListTile( + title: Text(l10n.t('nodeLocalGateway')), + subtitle: + Text(l10n.t('nodeLocalGatewaySubtitle')), + value: true, ), - const SizedBox(height: 12), - TextField( - controller: _portController, - decoration: const InputDecoration( - labelText: 'Gateway Port', - hintText: '18789', - ), - keyboardType: TextInputType.number, + RadioListTile( + title: Text(l10n.t('nodeRemoteGateway')), + subtitle: + Text(l10n.t('nodeRemoteGatewaySubtitle')), + value: false, ), - const SizedBox(height: 12), - TextField( - controller: _tokenController, - decoration: const InputDecoration( - labelText: 'Gateway Token', - hintText: 'Paste token from gateway dashboard URL', - helperText: 'Found in dashboard URL after #token=', - prefixIcon: Icon(Icons.key), + if (!_isLocal) ...[ + const SizedBox(height: 12), + TextField( + controller: _hostController, + decoration: InputDecoration( + labelText: l10n.t('nodeGatewayHost'), + hintText: '192.168.1.100', + ), ), - obscureText: true, - ), - const SizedBox(height: 12), - FilledButton.icon( - onPressed: () { - final host = _hostController.text.trim(); - final port = int.tryParse(_portController.text.trim()) ?? 18789; - final token = _tokenController.text.trim(); - if (host.isNotEmpty) { - provider.connectRemote(host, port, - token: token.isNotEmpty ? token : null); - } - }, - icon: const Icon(Icons.link), - label: const Text('Connect'), - ), + const SizedBox(height: 12), + TextField( + controller: _portController, + decoration: InputDecoration( + labelText: l10n.t('nodeGatewayPort'), + hintText: '18789', + ), + keyboardType: TextInputType.number, + ), + const SizedBox(height: 12), + TextField( + controller: _tokenController, + decoration: InputDecoration( + labelText: l10n.t('nodeGatewayToken'), + hintText: l10n.t('nodeGatewayTokenHint'), + helperText: + l10n.t('nodeGatewayTokenHelper'), + prefixIcon: const Icon(Icons.key), + ), + obscureText: true, + ), + const SizedBox(height: 12), + FilledButton.icon( + onPressed: () { + final host = _hostController.text.trim(); + final port = int.tryParse( + _portController.text.trim()) ?? + 18789; + final token = _tokenController.text.trim(); + if (host.isNotEmpty) { + provider.connectRemote(host, port, + token: + token.isNotEmpty ? token : null); + } + }, + icon: const Icon(Icons.link), + label: Text(l10n.t('nodeConnect')), + ), + ], ], - ], + ), ), ), ), @@ -144,7 +152,7 @@ class _NodeScreenState extends State { // Pairing Status if (state.pairingCode != null) ...[ - _sectionHeader(theme, 'PAIRING'), + _sectionHeader(theme, l10n.t('nodePairing')), Card( child: Padding( padding: const EdgeInsets.all(16), @@ -153,7 +161,7 @@ class _NodeScreenState extends State { const Icon(Icons.qr_code, size: 48), const SizedBox(height: 8), Text( - 'Approve this code on the gateway:', + l10n.t('nodeApproveCode'), style: theme.textTheme.bodyMedium, ), const SizedBox(height: 8), @@ -173,59 +181,60 @@ class _NodeScreenState extends State { ], // Capabilities - _sectionHeader(theme, 'CAPABILITIES'), + _sectionHeader(theme, l10n.t('nodeCapabilities')), _capabilityTile( theme, - 'Camera', - 'Capture photos and video clips', + l10n.t('nodeCapabilityCameraTitle'), + l10n.t('nodeCapabilityCameraSubtitle'), Icons.camera_alt, ), _capabilityTile( theme, - 'Canvas', - 'Navigate and interact with web pages', + l10n.t('nodeCapabilityCanvasTitle'), + l10n.t('nodeCapabilityCanvasSubtitle'), Icons.web, ), _capabilityTile( theme, - 'Location', - 'Get device GPS coordinates', + l10n.t('nodeCapabilityLocationTitle'), + l10n.t('nodeCapabilityLocationSubtitle'), Icons.location_on, ), _capabilityTile( theme, - 'Screen Recording', - 'Record device screen (requires consent each time)', + l10n.t('nodeCapabilityScreenTitle'), + l10n.t('nodeCapabilityScreenSubtitle'), Icons.screen_share, ), _capabilityTile( theme, - 'Flashlight', - 'Toggle device torch on/off', + l10n.t('nodeCapabilityFlashlightTitle'), + l10n.t('nodeCapabilityFlashlightSubtitle'), Icons.flashlight_on, ), _capabilityTile( theme, - 'Vibration', - 'Trigger haptic feedback and vibration patterns', + l10n.t('nodeCapabilityVibrationTitle'), + l10n.t('nodeCapabilityVibrationSubtitle'), Icons.vibration, ), _capabilityTile( theme, - 'Sensors', - 'Read accelerometer, gyroscope, magnetometer, barometer', + l10n.t('nodeCapabilitySensorsTitle'), + l10n.t('nodeCapabilitySensorsSubtitle'), Icons.sensors, ), const SizedBox(height: 16), // Device Info if (state.deviceId != null) ...[ - _sectionHeader(theme, 'DEVICE INFO'), + _sectionHeader(theme, l10n.t('nodeDeviceInfo')), ListTile( - title: const Text('Device ID'), + title: Text(l10n.t('nodeDeviceId')), subtitle: SelectableText( state.deviceId!, - style: const TextStyle(fontFamily: 'monospace', fontSize: 12), + style: const TextStyle( + fontFamily: 'monospace', fontSize: 12), ), leading: const Icon(Icons.fingerprint), ), @@ -233,7 +242,7 @@ class _NodeScreenState extends State { const SizedBox(height: 16), // Logs - _sectionHeader(theme, 'NODE LOGS'), + _sectionHeader(theme, l10n.t('nodeLogs')), Card( child: Container( height: 200, @@ -241,7 +250,7 @@ class _NodeScreenState extends State { child: state.logs.isEmpty ? Center( child: Text( - 'No logs yet', + l10n.t('nodeNoLogs'), style: theme.textTheme.bodySmall?.copyWith( color: theme.colorScheme.onSurfaceVariant, ), @@ -251,7 +260,8 @@ class _NodeScreenState extends State { reverse: true, itemCount: state.logs.length, itemBuilder: (context, index) { - final log = state.logs[state.logs.length - 1 - index]; + final log = + state.logs[state.logs.length - 1 - index]; return Text( log, style: const TextStyle( diff --git a/flutter_app/lib/screens/onboarding_screen.dart b/flutter_app/lib/screens/onboarding_screen.dart index b2e409e..7c1ede6 100644 --- a/flutter_app/lib/screens/onboarding_screen.dart +++ b/flutter_app/lib/screens/onboarding_screen.dart @@ -6,8 +6,10 @@ import 'package:xterm/xterm.dart'; import 'package:flutter_pty/flutter_pty.dart'; import 'package:url_launcher/url_launcher.dart'; import '../constants.dart'; +import '../l10n/app_localizations.dart'; import '../services/native_bridge.dart'; import '../services/screenshot_service.dart'; +import '../services/storage_permission_service.dart'; import '../services/terminal_service.dart'; import '../services/preferences_service.dart'; import '../widgets/terminal_toolbar.dart'; @@ -38,8 +40,10 @@ class _OnboardingScreenState extends State { final _altNotifier = ValueNotifier(false); final _screenshotKey = GlobalKey(); static final _anyUrlRegex = RegExp(r'https?://[^\s<>\[\]"' "'" r'\)]+'); - static final _tokenUrlRegex = RegExp(r'https?://(?:localhost|127\.0\.0\.1):18789/#token=[0-9a-f]+'); + static final _tokenUrlRegex = + RegExp(r'https?://(?:localhost|127\.0\.0\.1):18789/#token=[0-9a-f]+'); static final _ansiEscape = AppConstants.ansiEscape; + /// Box-drawing and other TUI characters that break URLs when copied static final _boxDrawing = RegExp(r'[│┤├┬┴┼╮╯╰╭─╌╴╶┌┐└┘◇◆]+'); static final _completionPattern = RegExp( @@ -79,9 +83,29 @@ class _OnboardingScreenState extends State { _pty?.kill(); _pty = null; try { + final hasStoragePermission = + await StoragePermissionService.ensurePermission( + context, + dialogTitleKey: 'onboardingStorageDialogTitle', + dialogBodyKey: 'onboardingStorageDialogBody', + ); + if (!hasStoragePermission) { + if (mounted) { + setState(() { + _loading = false; + _error = context.l10n.t('onboardingStoragePermissionRequired'); + }); + } + return; + } + // Ensure dirs + resolv.conf exist before proot starts (#40). - try { await NativeBridge.setupDirs(); } catch (_) {} - try { await NativeBridge.writeResolv(); } catch (_) {} + try { + await NativeBridge.setupDirs(); + } catch (_) {} + try { + await NativeBridge.writeResolv(); + } catch (_) {} try { final filesDir = await NativeBridge.getFilesDir(); const resolvContent = 'nameserver 8.8.8.8\nnameserver 8.8.4.4\n'; @@ -112,13 +136,14 @@ class _OnboardingScreenState extends State { onboardingArgs.removeLast(); // remove '-l' onboardingArgs.removeLast(); // remove '/bin/bash' onboardingArgs.addAll([ - '/bin/bash', '-lc', + '/bin/bash', + '-lc', 'echo "=== OpenClaw Onboarding ===" && ' - 'echo "Configure your API keys and binding settings." && ' - 'echo "TIP: Select Loopback (127.0.0.1) when asked for binding!" && ' - 'echo "" && ' - 'openclaw onboard; ' - 'echo "" && echo "Onboarding complete! You can close this screen."', + 'echo "Configure your API keys and binding settings." && ' + 'echo "TIP: Select Loopback (127.0.0.1) when asked for binding!" && ' + 'echo "" && ' + 'openclaw onboard; ' + 'echo "" && echo "Onboarding complete! You can close this screen."', ]); _pty = Pty.start( @@ -191,7 +216,7 @@ class _OnboardingScreenState extends State { } catch (e) { setState(() { _loading = false; - _error = 'Failed to start onboarding: $e'; + _error = context.l10n.t('onboardingStartFailed', {'error': '$e'}); }); } } @@ -234,7 +259,8 @@ class _OnboardingScreenState extends State { /// chars and rejoining lines, but splitting on `http` boundaries /// so concatenated URLs don't merge into one. String? _extractUrl(String text) { - final clean = text.replaceAll(_boxDrawing, '').replaceAll(RegExp(r'\s+'), ''); + final clean = + text.replaceAll(_boxDrawing, '').replaceAll(RegExp(r'\s+'), ''); // Split before each http(s):// so concatenated URLs become separate final parts = clean.split(RegExp(r'(?=https?://)')); // Return the longest URL match (token URLs are longest) @@ -262,10 +288,10 @@ class _OnboardingScreenState extends State { if (url != null) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( - content: const Text('Copied to clipboard'), + content: Text(context.l10n.t('commonCopiedToClipboard')), duration: const Duration(seconds: 3), action: SnackBarAction( - label: 'Open', + label: context.l10n.t('commonOpen'), onPressed: () { final uri = Uri.tryParse(url); if (uri != null) { @@ -277,9 +303,9 @@ class _OnboardingScreenState extends State { ); } else { ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Copied to clipboard'), - duration: Duration(seconds: 1), + SnackBar( + content: Text(context.l10n.t('commonCopiedToClipboard')), + duration: const Duration(seconds: 1), ), ); } @@ -298,9 +324,9 @@ class _OnboardingScreenState extends State { } } ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('No URL found in selection'), - duration: Duration(seconds: 1), + SnackBar( + content: Text(context.l10n.t('commonNoUrlFound')), + duration: const Duration(seconds: 1), ), ); } @@ -313,13 +339,16 @@ class _OnboardingScreenState extends State { } Future _takeScreenshot() async { - final path = await ScreenshotService.capture(_screenshotKey, prefix: 'onboarding'); + final path = + await ScreenshotService.capture(_screenshotKey, prefix: 'onboarding'); if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(path != null - ? 'Screenshot saved: ${path.split('/').last}' - : 'Failed to capture screenshot'), + ? context.l10n.t('commonScreenshotSaved', { + 'fileName': path.split('/').last, + }) + : context.l10n.t('commonSaveFailed')), ), ); } @@ -364,29 +393,29 @@ class _OnboardingScreenState extends State { final shouldOpen = await showDialog( context: context, builder: (ctx) => AlertDialog( - title: const Text('Open Link'), + title: Text(context.l10n.t('commonOpenLink')), content: Text(url), actions: [ TextButton( onPressed: () => Navigator.pop(ctx, false), - child: const Text('Cancel'), + child: Text(context.l10n.t('commonCancel')), ), TextButton( onPressed: () { Clipboard.setData(ClipboardData(text: url)); ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Link copied'), - duration: Duration(seconds: 1), + SnackBar( + content: Text(context.l10n.t('commonLinkCopied')), + duration: const Duration(seconds: 1), ), ); Navigator.pop(ctx, false); }, - child: const Text('Copy'), + child: Text(context.l10n.t('commonCopy')), ), FilledButton( onPressed: () => Navigator.pop(ctx, true), - child: const Text('Open'), + child: Text(context.l10n.t('commonOpen')), ), ], ), @@ -413,9 +442,11 @@ class _OnboardingScreenState extends State { @override Widget build(BuildContext context) { + final l10n = context.l10n; + return Scaffold( appBar: AppBar( - title: const Text('OpenClaw Onboarding'), + title: Text(l10n.t('onboardingTitle')), leading: widget.isFirstRun ? null // no back button during first-run : IconButton( @@ -426,22 +457,22 @@ class _OnboardingScreenState extends State { actions: [ IconButton( icon: const Icon(Icons.camera_alt_outlined), - tooltip: 'Screenshot', + tooltip: l10n.t('commonScreenshot'), onPressed: _takeScreenshot, ), IconButton( icon: const Icon(Icons.copy), - tooltip: 'Copy', + tooltip: l10n.t('commonCopy'), onPressed: _copySelection, ), IconButton( icon: const Icon(Icons.open_in_browser), - tooltip: 'Open URL', + tooltip: l10n.t('commonOpen'), onPressed: _openSelection, ), IconButton( icon: const Icon(Icons.paste), - tooltip: 'Paste', + tooltip: l10n.t('commonPaste'), onPressed: _paste, ), ], @@ -449,14 +480,14 @@ class _OnboardingScreenState extends State { body: Column( children: [ if (_loading) - const Expanded( + Expanded( child: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - CircularProgressIndicator(), - SizedBox(height: 16), - Text('Starting onboarding...'), + const CircularProgressIndicator(), + const SizedBox(height: 16), + Text(l10n.t('onboardingStarting')), ], ), ), @@ -478,7 +509,8 @@ class _OnboardingScreenState extends State { Text( _error!, textAlign: TextAlign.center, - style: TextStyle(color: Theme.of(context).colorScheme.error), + style: TextStyle( + color: Theme.of(context).colorScheme.error), ), const SizedBox(height: 16), FilledButton.icon( @@ -491,7 +523,7 @@ class _OnboardingScreenState extends State { _startOnboarding(); }, icon: const Icon(Icons.refresh), - label: const Text('Retry'), + label: Text(l10n.t('commonRetry')), ), ], ), @@ -530,12 +562,11 @@ class _OnboardingScreenState extends State { onPressed: widget.isFirstRun ? _goToDashboard : () => Navigator.of(context).pop(), - icon: Icon(widget.isFirstRun - ? Icons.arrow_forward - : Icons.check), + icon: Icon( + widget.isFirstRun ? Icons.arrow_forward : Icons.check), label: Text(widget.isFirstRun - ? 'Go to Dashboard' - : 'Done'), + ? l10n.t('onboardingGoToDashboard') + : l10n.t('commonDone')), ), ), ), diff --git a/flutter_app/lib/screens/packages_screen.dart b/flutter_app/lib/screens/packages_screen.dart index 9324309..021fffd 100644 --- a/flutter_app/lib/screens/packages_screen.dart +++ b/flutter_app/lib/screens/packages_screen.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import '../app.dart'; +import '../l10n/app_localizations.dart'; import '../models/optional_package.dart'; import '../services/package_service.dart'; import 'package_install_screen.dart'; @@ -50,24 +51,25 @@ class _PackagesScreenState extends State { } void _confirmUninstall(OptionalPackage package) { + final l10n = context.l10n; showDialog( context: context, builder: (ctx) => AlertDialog( - title: Text('Uninstall ${package.name}?'), + title: Text(l10n.t('packagesUninstallTitle', {'name': package.name})), content: Text( - 'This will remove ${package.name} from the environment.', + l10n.t('packagesUninstallDescription', {'name': package.name}), ), actions: [ TextButton( onPressed: () => Navigator.pop(ctx), - child: const Text('Cancel'), + child: Text(l10n.t('commonCancel')), ), FilledButton( onPressed: () { Navigator.pop(ctx); _navigateToInstall(package, isUninstall: true); }, - child: const Text('Uninstall'), + child: Text(l10n.t('packagesUninstall')), ), ], ), @@ -78,29 +80,35 @@ class _PackagesScreenState extends State { Widget build(BuildContext context) { final theme = Theme.of(context); final isDark = theme.brightness == Brightness.dark; + final l10n = context.l10n; return Scaffold( - appBar: AppBar(title: const Text('Optional Packages')), + appBar: AppBar(title: Text(l10n.t('packagesTitle'))), body: _loading ? const Center(child: CircularProgressIndicator()) : ListView( padding: const EdgeInsets.all(16), children: [ Text( - 'Development tools you can install inside the Ubuntu environment.', + l10n.t('packagesDescription'), style: theme.textTheme.bodyMedium?.copyWith( color: theme.colorScheme.onSurfaceVariant, ), ), const SizedBox(height: 16), for (final pkg in OptionalPackage.all) - _buildPackageCard(theme, pkg, isDark), + _buildPackageCard(theme, l10n, pkg, isDark), ], ), ); } - Widget _buildPackageCard(ThemeData theme, OptionalPackage package, bool isDark) { + Widget _buildPackageCard( + ThemeData theme, + AppLocalizations l10n, + OptionalPackage package, + bool isDark, + ) { final installed = _statuses[package.id] ?? false; final iconBg = isDark ? AppColors.darkSurfaceAlt : const Color(0xFFF3F4F6); @@ -117,7 +125,8 @@ class _PackagesScreenState extends State { color: iconBg, borderRadius: BorderRadius.circular(12), ), - child: Icon(package.icon, color: theme.colorScheme.onSurfaceVariant), + child: + Icon(package.icon, color: theme.colorScheme.onSurfaceVariant), ), const SizedBox(width: 16), Expanded( @@ -144,7 +153,7 @@ class _PackagesScreenState extends State { borderRadius: BorderRadius.circular(12), ), child: Text( - 'Installed', + l10n.t('commonInstalled'), style: theme.textTheme.labelSmall?.copyWith( color: AppColors.statusGreen, fontWeight: FontWeight.w600, @@ -156,7 +165,7 @@ class _PackagesScreenState extends State { ), const SizedBox(height: 4), Text( - package.description, + _packageDescription(l10n, package), style: theme.textTheme.bodySmall?.copyWith( color: theme.colorScheme.onSurfaceVariant, ), @@ -175,15 +184,28 @@ class _PackagesScreenState extends State { installed ? OutlinedButton( onPressed: () => _confirmUninstall(package), - child: const Text('Uninstall'), + child: Text(l10n.t('packagesUninstall')), ) : FilledButton( onPressed: () => _navigateToInstall(package), - child: const Text('Install'), + child: Text(l10n.t('packagesInstall')), ), ], ), ), ); } + + String _packageDescription(AppLocalizations l10n, OptionalPackage package) { + switch (package.id) { + case 'go': + return l10n.t('packageGoDescription'); + case 'brew': + return l10n.t('packageBrewDescription'); + case 'ssh': + return l10n.t('packageSshDescription'); + default: + return package.description; + } + } } diff --git a/flutter_app/lib/screens/provider_detail_screen.dart b/flutter_app/lib/screens/provider_detail_screen.dart index 8601b88..30fc8a4 100644 --- a/flutter_app/lib/screens/provider_detail_screen.dart +++ b/flutter_app/lib/screens/provider_detail_screen.dart @@ -221,7 +221,7 @@ class _ProviderDetailScreenState extends State { ), const SizedBox(height: 8), DropdownButtonFormField( - value: _selectedModel, + initialValue: _selectedModel, isExpanded: true, decoration: const InputDecoration(), items: [ diff --git a/flutter_app/lib/screens/settings_screen.dart b/flutter_app/lib/screens/settings_screen.dart index a256c99..8c63451 100644 --- a/flutter_app/lib/screens/settings_screen.dart +++ b/flutter_app/lib/screens/settings_screen.dart @@ -6,6 +6,8 @@ import 'package:provider/provider.dart'; import 'package:url_launcher/url_launcher.dart'; import '../app.dart'; import '../constants.dart'; +import '../l10n/app_localizations.dart'; +import '../providers/locale_provider.dart'; import '../providers/node_provider.dart'; import '../services/native_bridge.dart'; import '../services/preferences_service.dart'; @@ -81,32 +83,61 @@ class _SettingsScreenState extends State { @override Widget build(BuildContext context) { final theme = Theme.of(context); + final l10n = context.l10n; + final localeProvider = context.watch(); return Scaffold( - appBar: AppBar(title: const Text('Settings')), + appBar: AppBar(title: Text(l10n.t('settingsTitle'))), body: _loading ? const Center(child: CircularProgressIndicator()) : ListView( children: [ - _sectionHeader(theme, 'GENERAL'), + _sectionHeader(theme, l10n.t('settingsGeneral')), SwitchListTile( - title: const Text('Auto-start gateway'), - subtitle: const Text('Start the gateway when the app opens'), + title: Text(l10n.t('settingsAutoStart')), + subtitle: Text(l10n.t('settingsAutoStartSubtitle')), value: _autoStart, onChanged: (value) { setState(() => _autoStart = value); _prefs.autoStartGateway = value; }, ), + Padding( + padding: const EdgeInsets.fromLTRB(16, 0, 16, 8), + child: DropdownButtonFormField( + initialValue: localeProvider.localeCode, + decoration: InputDecoration(labelText: l10n.t('language')), + items: [ + DropdownMenuItem( + value: 'system', + child: Text(l10n.t('languageSystem')), + ), + DropdownMenuItem( + value: 'en', + child: Text(l10n.t('languageEnglish')), + ), + DropdownMenuItem( + value: 'zh', + child: Text(l10n.t('languageChinese')), + ), + ], + onChanged: (value) { + if (value != null) { + localeProvider.setLocaleCode(value); + } + }, + ), + ), ListTile( - title: const Text('Battery Optimization'), + title: Text(l10n.t('settingsBatteryOptimization')), subtitle: Text(_batteryOptimized - ? 'Optimized (may kill background sessions)' - : 'Unrestricted (recommended)'), + ? l10n.t('settingsBatteryOptimized') + : l10n.t('settingsBatteryUnrestricted')), leading: const Icon(Icons.battery_alert), trailing: _batteryOptimized ? const Icon(Icons.warning, color: AppColors.statusAmber) - : const Icon(Icons.check_circle, color: AppColors.statusGreen), + : const Icon(Icons.check_circle, + color: AppColors.statusGreen), onTap: () async { await NativeBridge.requestBatteryOptimization(); // Refresh status after returning from settings @@ -115,15 +146,40 @@ class _SettingsScreenState extends State { }, ), ListTile( - title: const Text('Setup Storage'), + title: Text(l10n.t('settingsStorage')), subtitle: Text(_storageGranted - ? 'Granted — /sdcard accessible in proot' - : 'Allow access to shared storage'), + ? l10n.t('settingsStorageGranted') + : l10n.t('settingsStorageMissing')), leading: const Icon(Icons.sd_storage), trailing: _storageGranted - ? const Icon(Icons.check_circle, color: AppColors.statusGreen) + ? const Icon(Icons.check_circle, + color: AppColors.statusGreen) : const Icon(Icons.warning, color: AppColors.statusAmber), onTap: () async { + final shouldRequest = await showDialog( + context: context, + builder: (dialogContext) => AlertDialog( + title: Text(l10n.t('settingsStorageDialogTitle')), + content: Text(l10n.t('settingsStorageDialogBody')), + actions: [ + TextButton( + onPressed: () => + Navigator.of(dialogContext).pop(false), + child: Text(l10n.t('commonCancel')), + ), + FilledButton( + onPressed: () => + Navigator.of(dialogContext).pop(true), + child: Text(l10n.t('settingsStorageDialogAction')), + ), + ], + ), + ); + + if (shouldRequest != true) { + return; + } + await NativeBridge.requestStoragePermission(); // Refresh after returning from permission screen final granted = await NativeBridge.hasStoragePermission(); @@ -131,10 +187,10 @@ class _SettingsScreenState extends State { }, ), const Divider(), - _sectionHeader(theme, 'NODE'), + _sectionHeader(theme, l10n.t('settingsNode')), SwitchListTile( - title: const Text('Enable Node'), - subtitle: const Text('Provide device capabilities to the gateway'), + title: Text(l10n.t('settingsEnableNode')), + subtitle: Text(l10n.t('settingsEnableNodeSubtitle')), value: _nodeEnabled, onChanged: (value) { setState(() => _nodeEnabled = value); @@ -148,8 +204,8 @@ class _SettingsScreenState extends State { }, ), ListTile( - title: const Text('Node Configuration'), - subtitle: const Text('Connection, pairing, and capabilities'), + title: Text(l10n.t('settingsNodeConfiguration')), + subtitle: Text(l10n.t('settingsNodeConfigurationSubtitle')), leading: const Icon(Icons.devices), trailing: const Icon(Icons.chevron_right), onTap: () => Navigator.of(context).push( @@ -157,78 +213,78 @@ class _SettingsScreenState extends State { ), ), const Divider(), - _sectionHeader(theme, 'SYSTEM INFO'), + _sectionHeader(theme, l10n.t('settingsSystemInfo')), ListTile( - title: const Text('Architecture'), + title: Text(l10n.t('settingsArchitecture')), subtitle: Text(_arch), leading: const Icon(Icons.memory), ), ListTile( - title: const Text('PRoot path'), + title: Text(l10n.t('settingsProotPath')), subtitle: Text(_prootPath), leading: const Icon(Icons.folder), ), ListTile( - title: const Text('Rootfs'), + title: Text(l10n.t('settingsRootfs')), subtitle: Text(_status['rootfsExists'] == true - ? 'Installed' - : 'Not installed'), + ? l10n.t('statusInstalled') + : l10n.t('statusNotInstalled')), leading: const Icon(Icons.storage), ), ListTile( - title: const Text('Node.js'), + title: Text(l10n.t('settingsNodeJs')), subtitle: Text(_status['nodeInstalled'] == true - ? 'Installed' - : 'Not installed'), + ? l10n.t('statusInstalled') + : l10n.t('statusNotInstalled')), leading: const Icon(Icons.code), ), ListTile( - title: const Text('OpenClaw'), + title: Text(l10n.t('settingsOpenClaw')), subtitle: Text(_status['openclawInstalled'] == true - ? 'Installed' - : 'Not installed'), + ? l10n.t('statusInstalled') + : l10n.t('statusNotInstalled')), leading: const Icon(Icons.cloud), ), ListTile( - title: const Text('Go (Golang)'), + title: Text(l10n.t('settingsGo')), subtitle: Text(_goInstalled - ? 'Installed' - : 'Not installed'), + ? l10n.t('statusInstalled') + : l10n.t('statusNotInstalled')), leading: const Icon(Icons.integration_instructions), ), ListTile( - title: const Text('Homebrew'), + title: Text(l10n.t('settingsHomebrew')), subtitle: Text(_brewInstalled - ? 'Installed' - : 'Not installed'), + ? l10n.t('statusInstalled') + : l10n.t('statusNotInstalled')), leading: const Icon(Icons.science), ), ListTile( - title: const Text('OpenSSH'), + title: Text(l10n.t('settingsOpenSsh')), subtitle: Text(_sshInstalled - ? 'Installed' - : 'Not installed'), + ? l10n.t('statusInstalled') + : l10n.t('statusNotInstalled')), leading: const Icon(Icons.vpn_key), ), const Divider(), - _sectionHeader(theme, 'MAINTENANCE'), + _sectionHeader(theme, l10n.t('settingsMaintenance')), ListTile( - title: const Text('Export Snapshot'), - subtitle: const Text('Backup config to Downloads'), + title: Text(l10n.t('settingsExportSnapshot')), + subtitle: Text(l10n.t('settingsExportSnapshotSubtitle')), leading: const Icon(Icons.upload_file), trailing: const Icon(Icons.chevron_right), onTap: _exportSnapshot, ), ListTile( - title: const Text('Import Snapshot'), - subtitle: const Text('Restore config from backup'), + title: Text(l10n.t('settingsImportSnapshot')), + subtitle: Text(l10n.t('settingsImportSnapshotSubtitle')), leading: const Icon(Icons.download), trailing: const Icon(Icons.chevron_right), onTap: _importSnapshot, ), ListTile( - title: const Text('Re-run setup'), - subtitle: const Text('Reinstall or repair the environment'), + title: Text(l10n.t('settingsRerunSetup')), + subtitle: Text(l10n.t('settingsRerunSetupSubtitle')), leading: const Icon(Icons.build), trailing: const Icon(Icons.chevron_right), onTap: () => Navigator.of(context).pushReplacement( @@ -238,22 +294,23 @@ class _SettingsScreenState extends State { ), ), const Divider(), - _sectionHeader(theme, 'ABOUT'), - const ListTile( - title: Text('OpenClaw'), + _sectionHeader(theme, l10n.t('settingsAbout')), + ListTile( + title: Text(l10n.t('settingsOpenClaw')), subtitle: Text( - 'AI Gateway for Android\nVersion ${AppConstants.version}', + l10n.t('settingsAboutSubtitle', + {'version': AppConstants.version}), ), - leading: Icon(Icons.info_outline), + leading: const Icon(Icons.info_outline), isThreeLine: true, ), - const ListTile( - title: Text('Developer'), - subtitle: Text(AppConstants.authorName), - leading: Icon(Icons.person), + ListTile( + title: Text(l10n.t('settingsDeveloper')), + subtitle: const Text(AppConstants.authorName), + leading: const Icon(Icons.person), ), ListTile( - title: const Text('GitHub'), + title: Text(l10n.t('settingsGithub')), subtitle: const Text('mithun50/openclaw-termux'), leading: const Icon(Icons.code), trailing: const Icon(Icons.open_in_new, size: 18), @@ -263,7 +320,7 @@ class _SettingsScreenState extends State { ), ), ListTile( - title: const Text('Contact'), + title: Text(l10n.t('settingsContact')), subtitle: const Text(AppConstants.authorEmail), leading: const Icon(Icons.email), trailing: const Icon(Icons.open_in_new, size: 18), @@ -271,10 +328,10 @@ class _SettingsScreenState extends State { Uri.parse('mailto:${AppConstants.authorEmail}'), ), ), - const ListTile( - title: Text('License'), - subtitle: Text(AppConstants.license), - leading: Icon(Icons.description), + ListTile( + title: Text(l10n.t('settingsLicense')), + subtitle: const Text(AppConstants.license), + leading: const Icon(Icons.description), ), const Divider(), _sectionHeader(theme, AppConstants.orgName.toUpperCase()), @@ -299,7 +356,7 @@ class _SettingsScreenState extends State { ), ), ListTile( - title: const Text('Play Store'), + title: Text(l10n.t('settingsPlayStore')), subtitle: const Text('NextGenX Apps'), leading: const Icon(Icons.shop), trailing: const Icon(Icons.open_in_new, size: 18), @@ -309,7 +366,7 @@ class _SettingsScreenState extends State { ), ), ListTile( - title: const Text('Email'), + title: Text(l10n.t('settingsEmail')), subtitle: const Text(AppConstants.orgEmail), leading: const Icon(Icons.email_outlined), trailing: const Icon(Icons.open_in_new, size: 18), @@ -339,7 +396,8 @@ class _SettingsScreenState extends State { Future _exportSnapshot() async { try { - final openclawJson = await NativeBridge.readRootfsFile('root/.openclaw/openclaw.json'); + final openclawJson = + await NativeBridge.readRootfsFile('root/.openclaw/openclaw.json'); final snapshot = { 'version': AppConstants.version, 'timestamp': DateTime.now().toIso8601String(), @@ -351,16 +409,22 @@ class _SettingsScreenState extends State { final path = await _getSnapshotPath(); final file = File(path); - await file.writeAsString(const JsonEncoder.withIndent(' ').convert(snapshot)); + await file + .writeAsString(const JsonEncoder.withIndent(' ').convert(snapshot)); if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Snapshot saved to $path')), + SnackBar( + content: + Text(context.l10n.t('settingsSnapshotSaved', {'path': path})), + ), ); } catch (e) { if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Export failed: $e')), + SnackBar( + content: Text(context.l10n.t('settingsExportFailed', {'error': e})), + ), ); } } @@ -373,7 +437,10 @@ class _SettingsScreenState extends State { if (!await file.exists()) { if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('No snapshot found at $path')), + SnackBar( + content: + Text(context.l10n.t('settingsSnapshotMissing', {'path': path})), + ), ); return; } @@ -384,7 +451,8 @@ class _SettingsScreenState extends State { // Restore openclaw.json into rootfs final openclawConfig = snapshot['openclawConfig'] as String?; if (openclawConfig != null) { - await NativeBridge.writeRootfsFile('root/.openclaw/openclaw.json', openclawConfig); + await NativeBridge.writeRootfsFile( + 'root/.openclaw/openclaw.json', openclawConfig); } // Restore preferences @@ -403,12 +471,14 @@ class _SettingsScreenState extends State { if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Snapshot restored successfully. Restart the gateway to apply.')), + SnackBar(content: Text(context.l10n.t('settingsSnapshotRestored'))), ); } catch (e) { if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Import failed: $e')), + SnackBar( + content: Text(context.l10n.t('settingsImportFailed', {'error': e})), + ), ); } } diff --git a/flutter_app/lib/screens/setup_wizard_screen.dart b/flutter_app/lib/screens/setup_wizard_screen.dart index e3d396a..ede12c2 100644 --- a/flutter_app/lib/screens/setup_wizard_screen.dart +++ b/flutter_app/lib/screens/setup_wizard_screen.dart @@ -2,10 +2,12 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../app.dart'; import '../constants.dart'; +import '../l10n/app_localizations.dart'; import '../models/setup_state.dart'; import '../models/optional_package.dart'; import '../providers/setup_provider.dart'; import '../services/package_service.dart'; +import '../services/storage_permission_service.dart'; import '../widgets/progress_step.dart'; import 'onboarding_screen.dart'; import 'package_install_screen.dart'; @@ -20,6 +22,7 @@ class SetupWizardScreen extends StatefulWidget { class _SetupWizardScreenState extends State { bool _started = false; Map _pkgStatuses = {}; + String? _permissionError; Future _refreshPkgStatuses() async { final statuses = await PackageService.checkAllStatuses(); @@ -35,10 +38,39 @@ class _SetupWizardScreenState extends State { if (result == true) _refreshPkgStatuses(); } + Future _beginSetup(SetupProvider provider) async { + final hasStoragePermission = + await StoragePermissionService.ensurePermission( + context, + dialogTitleKey: 'setupWizardStorageDialogTitle', + dialogBodyKey: 'setupWizardStorageDialogBody', + ); + + if (!mounted) { + return; + } + + if (!hasStoragePermission) { + setState(() { + _started = false; + _permissionError = + context.l10n.t('setupWizardStoragePermissionRequired'); + }); + return; + } + + setState(() { + _started = true; + _permissionError = null; + }); + provider.runSetup(); + } + @override Widget build(BuildContext context) { final theme = Theme.of(context); final isDark = theme.brightness == Brightness.dark; + final l10n = context.l10n; return Scaffold( body: SafeArea( @@ -64,7 +96,7 @@ class _SetupWizardScreenState extends State { ), const SizedBox(height: 16), Text( - 'Setup OpenClaw', + l10n.t('setupWizardTitle'), style: theme.textTheme.headlineSmall?.copyWith( fontWeight: FontWeight.bold, ), @@ -72,17 +104,17 @@ class _SetupWizardScreenState extends State { const SizedBox(height: 8), Text( _started - ? 'Setting up the environment. This may take several minutes.' - : 'This will download Ubuntu, Node.js, and OpenClaw into a self-contained environment.', + ? l10n.t('setupWizardIntroRunning') + : l10n.t('setupWizardIntroIdle'), style: theme.textTheme.bodyMedium?.copyWith( color: theme.colorScheme.onSurfaceVariant, ), ), const SizedBox(height: 32), Expanded( - child: _buildSteps(state, theme, isDark), + child: _buildSteps(state, theme, isDark, l10n), ), - if (state.hasError) ...[ + if (state.hasError || _permissionError != null) ...[ ConstrainedBox( constraints: const BoxConstraints(maxHeight: 160), child: Container( @@ -94,13 +126,18 @@ class _SetupWizardScreenState extends State { child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Icon(Icons.error_outline, color: theme.colorScheme.error), + Icon(Icons.error_outline, + color: theme.colorScheme.error), const SizedBox(width: 8), Expanded( child: SingleChildScrollView( child: Text( - state.error ?? 'Unknown error', - style: TextStyle(color: theme.colorScheme.onErrorContainer), + _permissionError ?? + state.error ?? + 'Unknown error', + style: TextStyle( + color: + theme.colorScheme.onErrorContainer), ), ), ), @@ -116,7 +153,7 @@ class _SetupWizardScreenState extends State { child: FilledButton.icon( onPressed: () => _goToOnboarding(context), icon: const Icon(Icons.arrow_forward), - label: const Text('Configure API Keys'), + label: Text(l10n.t('setupWizardConfigureApiKeys')), ), ) else if (!_started || state.hasError) @@ -125,19 +162,20 @@ class _SetupWizardScreenState extends State { child: FilledButton.icon( onPressed: provider.isRunning ? null - : () { - setState(() => _started = true); - provider.runSetup(); - }, + : () => _beginSetup(provider), icon: const Icon(Icons.download), - label: Text(_started ? 'Retry Setup' : 'Begin Setup'), + label: Text( + _started + ? l10n.t('setupWizardRetry') + : l10n.t('setupWizardBegin'), + ), ), ), if (!_started) ...[ const SizedBox(height: 8), Center( child: Text( - 'Requires ~500MB of storage and an internet connection', + l10n.t('setupWizardRequirements'), style: theme.textTheme.bodySmall?.copyWith( color: theme.colorScheme.onSurfaceVariant, ), @@ -162,13 +200,26 @@ class _SetupWizardScreenState extends State { ); } - Widget _buildSteps(SetupState state, ThemeData theme, bool isDark) { + Widget _buildSteps( + SetupState state, + ThemeData theme, + bool isDark, + AppLocalizations l10n, + ) { final steps = [ - (1, 'Download Ubuntu rootfs', SetupStep.downloadingRootfs), - (2, 'Extract rootfs', SetupStep.extractingRootfs), - (3, 'Install Node.js', SetupStep.installingNode), - (4, 'Install OpenClaw', SetupStep.installingOpenClaw), - (5, 'Configure Bionic Bypass', SetupStep.configuringBypass), + (1, l10n.t('setupWizardStepDownloadRootfs'), SetupStep.downloadingRootfs), + (2, l10n.t('setupWizardStepExtractRootfs'), SetupStep.extractingRootfs), + (3, l10n.t('setupWizardStepInstallNode'), SetupStep.installingNode), + ( + 4, + l10n.t('setupWizardStepInstallOpenClaw'), + SetupStep.installingOpenClaw + ), + ( + 5, + l10n.t('setupWizardStepConfigureBypass'), + SetupStep.configuringBypass + ), ]; return ListView( @@ -176,23 +227,25 @@ class _SetupWizardScreenState extends State { for (final (num, label, step) in steps) ProgressStep( stepNumber: num, - label: state.step == step ? state.message : label, + label: state.step == step + ? _localizedSetupMessage(l10n, state.message) + : label, isActive: state.step == step, isComplete: state.stepNumber > step.index + 1 || state.isComplete, hasError: state.hasError && state.step == step, progress: state.step == step ? state.progress : null, ), if (state.isComplete) ...[ - const ProgressStep( + ProgressStep( stepNumber: 6, - label: 'Setup complete!', + label: l10n.t('setupWizardComplete'), isComplete: true, ), const SizedBox(height: 24), Padding( padding: const EdgeInsets.symmetric(horizontal: 8), child: Text( - 'OPTIONAL PACKAGES', + l10n.t('setupWizardOptionalPackages'), style: theme.textTheme.labelSmall?.copyWith( color: theme.colorScheme.onSurfaceVariant, fontWeight: FontWeight.w600, @@ -202,13 +255,18 @@ class _SetupWizardScreenState extends State { ), const SizedBox(height: 8), for (final pkg in OptionalPackage.all) - _buildPackageTile(theme, pkg, isDark), + _buildPackageTile(theme, l10n, pkg, isDark), ], ], ); } - Widget _buildPackageTile(ThemeData theme, OptionalPackage package, bool isDark) { + Widget _buildPackageTile( + ThemeData theme, + AppLocalizations l10n, + OptionalPackage package, + bool isDark, + ) { final installed = _pkgStatuses[package.id] ?? false; final iconBg = isDark ? AppColors.darkSurfaceAlt : const Color(0xFFF3F4F6); @@ -222,7 +280,8 @@ class _SetupWizardScreenState extends State { color: iconBg, borderRadius: BorderRadius.circular(10), ), - child: Icon(package.icon, color: theme.colorScheme.onSurfaceVariant, size: 22), + child: Icon(package.icon, + color: theme.colorScheme.onSurfaceVariant, size: 22), ), title: Row( children: [ @@ -231,13 +290,12 @@ class _SetupWizardScreenState extends State { if (installed) ...[ const SizedBox(width: 8), Container( - padding: - const EdgeInsets.symmetric(horizontal: 6, vertical: 1), + padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 1), decoration: BoxDecoration( color: AppColors.statusGreen.withAlpha(25), borderRadius: BorderRadius.circular(8), ), - child: Text('Installed', + child: Text(l10n.t('commonInstalled'), style: theme.textTheme.labelSmall?.copyWith( color: AppColors.statusGreen, fontWeight: FontWeight.w600, @@ -246,17 +304,106 @@ class _SetupWizardScreenState extends State { ], ], ), - subtitle: Text('${package.description} (${package.estimatedSize})'), + subtitle: Text( + '${_packageDescription(l10n, package)} (${package.estimatedSize})'), trailing: installed ? const Icon(Icons.check_circle, color: AppColors.statusGreen) : OutlinedButton( onPressed: () => _installPackage(package), - child: const Text('Install'), + child: Text(l10n.t('packagesInstall')), ), ), ); } + String _packageDescription(AppLocalizations l10n, OptionalPackage package) { + switch (package.id) { + case 'go': + return l10n.t('packageGoDescription'); + case 'brew': + return l10n.t('packageBrewDescription'); + case 'ssh': + return l10n.t('packageSshDescription'); + default: + return package.description; + } + } + + String _localizedSetupMessage(AppLocalizations l10n, String? message) { + if (message == null || message.isEmpty) { + return ''; + } + + final downloadProgress = + RegExp(r'^Downloading: ([0-9.]+) MB / ([0-9.]+) MB$') + .firstMatch(message); + if (downloadProgress != null) { + return l10n.t('setupWizardStatusDownloadingProgress', { + 'current': downloadProgress.group(1), + 'total': downloadProgress.group(2), + }); + } + + final nodeDownloadProgress = + RegExp(r'^Downloading Node\.js: ([0-9.]+) MB / ([0-9.]+) MB$') + .firstMatch(message); + if (nodeDownloadProgress != null) { + return l10n.t('setupWizardStatusDownloadingNodeProgress', { + 'current': nodeDownloadProgress.group(1), + 'total': nodeDownloadProgress.group(2), + }); + } + + final nodeVersionMatch = + RegExp(r'^Downloading Node\.js (.+)\.\.\.$').firstMatch(message); + if (nodeVersionMatch != null) { + return l10n.t('setupWizardStatusDownloadingNode', { + 'version': nodeVersionMatch.group(1), + }); + } + + switch (message) { + case 'Setup complete': + return l10n.t('setupWizardStatusSetupComplete'); + case 'Setup required': + return l10n.t('setupWizardStatusSetupRequired'); + case 'Setting up directories...': + return l10n.t('setupWizardStatusSettingUpDirs'); + case 'Downloading Ubuntu rootfs...': + return l10n.t('setupWizardStatusDownloadingUbuntuRootfs'); + case 'Extracting rootfs (this takes a while)...': + return l10n.t('setupWizardStatusExtractingRootfs'); + case 'Rootfs extracted': + return l10n.t('setupWizardStatusRootfsExtracted'); + case 'Fixing rootfs permissions...': + return l10n.t('setupWizardStatusFixingPermissions'); + case 'Updating package lists...': + return l10n.t('setupWizardStatusUpdatingPackageLists'); + case 'Installing base packages...': + return l10n.t('setupWizardStatusInstallingBasePackages'); + case 'Extracting Node.js...': + return l10n.t('setupWizardStatusExtractingNode'); + case 'Verifying Node.js...': + return l10n.t('setupWizardStatusVerifyingNode'); + case 'Node.js installed': + return l10n.t('setupWizardStatusNodeInstalled'); + case 'Installing OpenClaw (this may take a few minutes)...': + return l10n.t('setupWizardStatusInstallingOpenClaw'); + case 'Creating bin wrappers...': + return l10n.t('setupWizardStatusCreatingBinWrappers'); + case 'Verifying OpenClaw...': + return l10n.t('setupWizardStatusVerifyingOpenClaw'); + case 'OpenClaw installed': + return l10n.t('setupWizardStatusOpenClawInstalled'); + case 'Bionic Bypass configured': + return l10n.t('setupWizardStatusBypassConfigured'); + case 'Setup complete! Ready to start the gateway.': + return l10n.t('setupWizardStatusReady'); + default: + return message; + } + } + void _goToOnboarding(BuildContext context) { Navigator.of(context).pushReplacement( MaterialPageRoute( diff --git a/flutter_app/lib/services/preferences_service.dart b/flutter_app/lib/services/preferences_service.dart index 44c40b3..efd82ce 100644 --- a/flutter_app/lib/services/preferences_service.dart +++ b/flutter_app/lib/services/preferences_service.dart @@ -5,6 +5,7 @@ class PreferencesService { static const _keySetupComplete = 'setup_complete'; static const _keyFirstRun = 'first_run'; static const _keyDashboardUrl = 'dashboard_url'; + static const _keyLocaleCode = 'locale_code'; static const _keyNodeEnabled = 'node_enabled'; static const _keyNodeDeviceToken = 'node_device_token'; static const _keyNodeGatewayHost = 'node_gateway_host'; @@ -36,6 +37,15 @@ class PreferencesService { } } + String? get localeCode => _prefs.getString(_keyLocaleCode); + set localeCode(String? value) { + if (value != null && value.isNotEmpty) { + _prefs.setString(_keyLocaleCode, value); + } else { + _prefs.remove(_keyLocaleCode); + } + } + bool get nodeEnabled => _prefs.getBool(_keyNodeEnabled) ?? false; set nodeEnabled(bool value) => _prefs.setBool(_keyNodeEnabled, value); @@ -72,6 +82,7 @@ class PreferencesService { final val = _prefs.getInt(_keyNodeGatewayPort); return val; } + set nodeGatewayPort(int? value) { if (value != null) { _prefs.setInt(_keyNodeGatewayPort, value); diff --git a/flutter_app/lib/services/storage_permission_service.dart b/flutter_app/lib/services/storage_permission_service.dart new file mode 100644 index 0000000..4eddde2 --- /dev/null +++ b/flutter_app/lib/services/storage_permission_service.dart @@ -0,0 +1,46 @@ +import 'package:flutter/material.dart'; +import '../l10n/app_localizations.dart'; +import 'native_bridge.dart'; + +class StoragePermissionService { + static Future ensurePermission( + BuildContext context, { + required String dialogTitleKey, + required String dialogBodyKey, + }) async { + final hasPermission = await NativeBridge.hasStoragePermission(); + if (hasPermission) { + return true; + } + + if (!context.mounted) { + return false; + } + + final l10n = context.l10n; + final shouldRequest = await showDialog( + context: context, + builder: (dialogContext) => AlertDialog( + title: Text(l10n.t(dialogTitleKey)), + content: Text(l10n.t(dialogBodyKey)), + actions: [ + TextButton( + onPressed: () => Navigator.of(dialogContext).pop(false), + child: Text(l10n.t('commonCancel')), + ), + FilledButton( + onPressed: () => Navigator.of(dialogContext).pop(true), + child: Text(l10n.t('settingsStorageDialogAction')), + ), + ], + ), + ); + + if (shouldRequest != true) { + return false; + } + + await NativeBridge.requestStoragePermission(); + return await NativeBridge.hasStoragePermission(); + } +} diff --git a/flutter_app/lib/widgets/gateway_controls.dart b/flutter_app/lib/widgets/gateway_controls.dart index 52c524b..2db1568 100644 --- a/flutter_app/lib/widgets/gateway_controls.dart +++ b/flutter_app/lib/widgets/gateway_controls.dart @@ -3,6 +3,7 @@ import 'package:flutter/services.dart'; import 'package:provider/provider.dart'; import '../app.dart'; import '../constants.dart'; +import '../l10n/app_localizations.dart'; import '../models/gateway_state.dart'; import '../providers/gateway_provider.dart'; import '../screens/logs_screen.dart'; @@ -14,6 +15,7 @@ class GatewayControls extends StatelessWidget { @override Widget build(BuildContext context) { final theme = Theme.of(context); + final l10n = context.l10n; return Consumer( builder: (context, provider, _) { @@ -29,13 +31,13 @@ class GatewayControls extends StatelessWidget { children: [ Expanded( child: Text( - 'Gateway', + l10n.t('gatewayTitle'), style: theme.textTheme.titleLarge?.copyWith( fontWeight: FontWeight.w600, ), ), ), - _statusBadge(state.status, theme), + _statusBadge(context, state.status, theme), ], ), const SizedBox(height: 8), @@ -66,13 +68,14 @@ class GatewayControls extends StatelessWidget { ), IconButton( icon: const Icon(Icons.copy, size: 18), - tooltip: 'Copy URL', + tooltip: l10n.t('gatewayCopyUrl'), onPressed: () { - final url = state.dashboardUrl ?? AppConstants.gatewayUrl; + final url = + state.dashboardUrl ?? AppConstants.gatewayUrl; Clipboard.setData(ClipboardData(text: url)); ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('URL copied to clipboard'), + SnackBar( + content: Text(l10n.t('gatewayUrlCopied')), duration: Duration(seconds: 2), ), ); @@ -80,7 +83,7 @@ class GatewayControls extends StatelessWidget { ), IconButton( icon: const Icon(Icons.open_in_new, size: 18), - tooltip: 'Open dashboard', + tooltip: l10n.t('gatewayOpenDashboard'), onPressed: () { Navigator.of(context).push( MaterialPageRoute( @@ -108,20 +111,21 @@ class GatewayControls extends StatelessWidget { FilledButton.icon( onPressed: () => provider.start(), icon: const Icon(Icons.play_arrow), - label: const Text('Start Gateway'), + label: Text(l10n.t('gatewayStart')), ), - if (state.isRunning || state.status == GatewayStatus.starting) + if (state.isRunning || + state.status == GatewayStatus.starting) OutlinedButton.icon( onPressed: () => provider.stop(), icon: const Icon(Icons.stop), - label: const Text('Stop Gateway'), + label: Text(l10n.t('gatewayStop')), ), OutlinedButton.icon( onPressed: () => Navigator.of(context).push( MaterialPageRoute(builder: (_) => const LogsScreen()), ), icon: const Icon(Icons.article_outlined), - label: const Text('View Logs'), + label: Text(l10n.t('gatewayViewLogs')), ), ], ), @@ -133,7 +137,9 @@ class GatewayControls extends StatelessWidget { ); } - Widget _statusBadge(GatewayStatus status, ThemeData theme) { + Widget _statusBadge( + BuildContext context, GatewayStatus status, ThemeData theme) { + final l10n = context.l10n; Color color; String label; IconData icon; @@ -141,19 +147,19 @@ class GatewayControls extends StatelessWidget { switch (status) { case GatewayStatus.running: color = AppColors.statusGreen; - label = 'Running'; + label = l10n.t('gatewayStatusRunning'); icon = Icons.check_circle_outline; case GatewayStatus.starting: color = AppColors.statusAmber; - label = 'Starting'; + label = l10n.t('gatewayStatusStarting'); icon = Icons.hourglass_top; case GatewayStatus.error: color = AppColors.statusRed; - label = 'Error'; + label = l10n.t('gatewayStatusError'); icon = Icons.error_outline; case GatewayStatus.stopped: color = AppColors.statusGrey; - label = 'Stopped'; + label = l10n.t('gatewayStatusStopped'); icon = Icons.circle_outlined; } diff --git a/flutter_app/lib/widgets/node_controls.dart b/flutter_app/lib/widgets/node_controls.dart index 4a2b719..95eb4e6 100644 --- a/flutter_app/lib/widgets/node_controls.dart +++ b/flutter_app/lib/widgets/node_controls.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../app.dart'; +import '../l10n/app_localizations.dart'; import '../models/node_state.dart'; import '../providers/node_provider.dart'; import '../screens/node_screen.dart'; @@ -11,6 +12,7 @@ class NodeControls extends StatelessWidget { @override Widget build(BuildContext context) { final theme = Theme.of(context); + final l10n = context.l10n; return Consumer( builder: (context, provider, _) { @@ -26,19 +28,25 @@ class NodeControls extends StatelessWidget { children: [ Expanded( child: Text( - 'Node', + l10n.t('nodeTitle'), style: theme.textTheme.titleLarge?.copyWith( fontWeight: FontWeight.w600, ), ), ), - _statusBadge(state.status, theme), + _statusBadge(context, state.status, theme), ], ), const SizedBox(height: 8), if (state.isPaired) ...[ Text( - 'Connected to ${state.gatewayHost}:${state.gatewayPort}', + l10n.t( + 'nodeConnectedTo', + { + 'host': state.gatewayHost, + 'port': state.gatewayPort, + }, + ), style: theme.textTheme.bodyMedium?.copyWith( color: theme.colorScheme.onSurfaceVariant, fontFamily: 'monospace', @@ -50,7 +58,7 @@ class NodeControls extends StatelessWidget { Row( children: [ Text( - 'Pairing code: ', + l10n.t('nodePairingCode'), style: theme.textTheme.bodyMedium, ), SelectableText( @@ -78,20 +86,20 @@ class NodeControls extends StatelessWidget { FilledButton.icon( onPressed: () => provider.enable(), icon: const Icon(Icons.power_settings_new), - label: const Text('Enable Node'), + label: Text(l10n.t('nodeEnable')), ), if (!state.isDisabled) ...[ OutlinedButton.icon( onPressed: () => provider.disable(), icon: const Icon(Icons.stop), - label: const Text('Disable Node'), + label: Text(l10n.t('nodeDisable')), ), if (state.status == NodeStatus.error || state.status == NodeStatus.disconnected) OutlinedButton.icon( onPressed: () => provider.reconnect(), icon: const Icon(Icons.refresh), - label: const Text('Reconnect'), + label: Text(l10n.t('nodeReconnect')), ), ], OutlinedButton.icon( @@ -99,7 +107,7 @@ class NodeControls extends StatelessWidget { MaterialPageRoute(builder: (_) => const NodeScreen()), ), icon: const Icon(Icons.settings), - label: const Text('Configure'), + label: Text(l10n.t('commonConfigure')), ), ], ), @@ -111,7 +119,9 @@ class NodeControls extends StatelessWidget { ); } - Widget _statusBadge(NodeStatus status, ThemeData theme) { + Widget _statusBadge( + BuildContext context, NodeStatus status, ThemeData theme) { + final l10n = context.l10n; Color color; String label; IconData icon; @@ -119,25 +129,25 @@ class NodeControls extends StatelessWidget { switch (status) { case NodeStatus.paired: color = AppColors.statusGreen; - label = 'Paired'; + label = l10n.t('nodeStatusPaired'); icon = Icons.check_circle_outline; case NodeStatus.connecting: case NodeStatus.challenging: case NodeStatus.pairing: color = AppColors.statusAmber; - label = 'Connecting'; + label = l10n.t('nodeStatusConnecting'); icon = Icons.hourglass_top; case NodeStatus.error: color = AppColors.statusRed; - label = 'Error'; + label = l10n.t('nodeStatusError'); icon = Icons.error_outline; case NodeStatus.disabled: color = AppColors.statusGrey; - label = 'Disabled'; + label = l10n.t('nodeStatusDisabled'); icon = Icons.circle_outlined; case NodeStatus.disconnected: color = AppColors.statusGrey; - label = 'Disconnected'; + label = l10n.t('nodeStatusDisconnected'); icon = Icons.link_off; } diff --git a/flutter_app/pubspec.lock b/flutter_app/pubspec.lock new file mode 100644 index 0000000..f4f4f4e --- /dev/null +++ b/flutter_app/pubspec.lock @@ -0,0 +1,943 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + args: + dependency: transitive + description: + name: args + sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 + url: "https://pub.dev" + source: hosted + version: "2.7.0" + async: + dependency: transitive + description: + name: async + sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" + url: "https://pub.dev" + source: hosted + version: "2.13.0" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + camera: + dependency: "direct main" + description: + name: camera + sha256: "46f391e9bbdaa373d15e296abc5de8bfb0dd0d0c7487592dd8f20e8ef980429f" + url: "https://pub.dev" + source: hosted + version: "0.12.0" + camera_android_camerax: + dependency: transitive + description: + name: camera_android_camerax + sha256: c0be4298e3888ba6cf5c1fb1ae1203f08dcbb14d4f545ce5262f473cf8c33e28 + url: "https://pub.dev" + source: hosted + version: "0.7.1" + camera_avfoundation: + dependency: transitive + description: + name: camera_avfoundation + sha256: "90e4cc3fde331581a3b2d35d83be41dbb7393af0ab857eb27b732174289cb96d" + url: "https://pub.dev" + source: hosted + version: "0.10.1" + camera_platform_interface: + dependency: transitive + description: + name: camera_platform_interface + sha256: "98cfc9357e04bad617671b4c1f78a597f25f08003089dd94050709ae54effc63" + url: "https://pub.dev" + source: hosted + version: "2.12.0" + camera_web: + dependency: transitive + description: + name: camera_web + sha256: "57f49a635c8bf249d07fb95eb693d7e4dda6796dedb3777f9127fb54847beba7" + url: "https://pub.dev" + source: hosted + version: "0.3.5+3" + characters: + dependency: transitive + description: + name: characters + sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b + url: "https://pub.dev" + source: hosted + version: "1.4.1" + clock: + dependency: transitive + description: + name: clock + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b + url: "https://pub.dev" + source: hosted + version: "1.1.2" + code_assets: + dependency: transitive + description: + name: code_assets + sha256: "83ccdaa064c980b5596c35dd64a8d3ecc68620174ab9b90b6343b753aa721687" + url: "https://pub.dev" + source: hosted + version: "1.0.0" + collection: + dependency: transitive + description: + name: collection + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" + url: "https://pub.dev" + source: hosted + version: "1.19.1" + convert: + dependency: transitive + description: + name: convert + sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 + url: "https://pub.dev" + source: hosted + version: "3.1.2" + cross_file: + dependency: transitive + description: + name: cross_file + sha256: "28bb3ae56f117b5aec029d702a90f57d285cd975c3c5c281eaca38dbc47c5937" + url: "https://pub.dev" + source: hosted + version: "0.3.5+2" + crypto: + dependency: transitive + description: + name: crypto + sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf + url: "https://pub.dev" + source: hosted + version: "3.0.7" + cryptography: + dependency: "direct main" + description: + name: cryptography + sha256: "3eda3029d34ec9095a27a198ac9785630fe525c0eb6a49f3d575272f8e792ef0" + url: "https://pub.dev" + source: hosted + version: "2.9.0" + dbus: + dependency: transitive + description: + name: dbus + sha256: d0c98dcd4f5169878b6cf8f6e0a52403a9dff371a3e2f019697accbf6f44a270 + url: "https://pub.dev" + source: hosted + version: "0.7.12" + dio: + dependency: "direct main" + description: + name: dio + sha256: aff32c08f92787a557dd5c0145ac91536481831a01b4648136373cddb0e64f8c + url: "https://pub.dev" + source: hosted + version: "5.9.2" + dio_web_adapter: + dependency: transitive + description: + name: dio_web_adapter + sha256: "2f9e64323a7c3c7ef69567d5c800424a11f8337b8b228bad02524c9fb3c1f340" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + equatable: + dependency: transitive + description: + name: equatable + sha256: "3e0141505477fd8ad55d6eb4e7776d3fe8430be8e497ccb1521370c3f21a3e2b" + url: "https://pub.dev" + source: hosted + version: "2.0.8" + fake_async: + dependency: transitive + description: + name: fake_async + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" + url: "https://pub.dev" + source: hosted + version: "1.3.3" + ffi: + dependency: transitive + description: + name: ffi + sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45" + url: "https://pub.dev" + source: hosted + version: "2.2.0" + file: + dependency: transitive + description: + name: file + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 + url: "https://pub.dev" + source: hosted + version: "7.0.1" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be + url: "https://pub.dev" + source: hosted + version: "1.1.1" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1" + url: "https://pub.dev" + source: hosted + version: "6.0.0" + flutter_localizations: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_plugin_android_lifecycle: + dependency: transitive + description: + name: flutter_plugin_android_lifecycle + sha256: ee8068e0e1cd16c4a82714119918efdeed33b3ba7772c54b5d094ab53f9b7fd1 + url: "https://pub.dev" + source: hosted + version: "2.0.33" + flutter_pty: + dependency: "direct main" + description: + name: flutter_pty + sha256: c2f3b3160b519ac820fa3f6ef175361f2dfc52c557465643589542e9f229ad66 + url: "https://pub.dev" + source: hosted + version: "0.4.2" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + geoclue: + dependency: transitive + description: + name: geoclue + sha256: c2a998c77474fc57aa00c6baa2928e58f4b267649057a1c76738656e9dbd2a7f + url: "https://pub.dev" + source: hosted + version: "0.1.1" + geolocator: + dependency: "direct main" + description: + name: geolocator + sha256: "79939537046c9025be47ec645f35c8090ecadb6fe98eba146a0d25e8c1357516" + url: "https://pub.dev" + source: hosted + version: "14.0.2" + geolocator_android: + dependency: transitive + description: + name: geolocator_android + sha256: "179c3cb66dfa674fc9ccbf2be872a02658724d1c067634e2c427cf6df7df901a" + url: "https://pub.dev" + source: hosted + version: "5.0.2" + geolocator_apple: + dependency: transitive + description: + name: geolocator_apple + sha256: dbdd8789d5aaf14cf69f74d4925ad1336b4433a6efdf2fce91e8955dc921bf22 + url: "https://pub.dev" + source: hosted + version: "2.3.13" + geolocator_linux: + dependency: transitive + description: + name: geolocator_linux + sha256: d64112a205931926f4363bb6bd48f14cb38e7326833041d170615586cd143797 + url: "https://pub.dev" + source: hosted + version: "0.2.4" + geolocator_platform_interface: + dependency: transitive + description: + name: geolocator_platform_interface + sha256: "30cb64f0b9adcc0fb36f628b4ebf4f731a2961a0ebd849f4b56200205056fe67" + url: "https://pub.dev" + source: hosted + version: "4.2.6" + geolocator_web: + dependency: transitive + description: + name: geolocator_web + sha256: b1ae9bdfd90f861fde8fd4f209c37b953d65e92823cb73c7dee1fa021b06f172 + url: "https://pub.dev" + source: hosted + version: "4.1.3" + geolocator_windows: + dependency: transitive + description: + name: geolocator_windows + sha256: "175435404d20278ffd220de83c2ca293b73db95eafbdc8131fe8609be1421eb6" + url: "https://pub.dev" + source: hosted + version: "0.2.5" + glob: + dependency: transitive + description: + name: glob + sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de + url: "https://pub.dev" + source: hosted + version: "2.1.3" + google_fonts: + dependency: "direct main" + description: + name: google_fonts + sha256: db9df7a5898d894eeda4c78143f35c30a243558be439518972366880b80bf88e + url: "https://pub.dev" + source: hosted + version: "8.0.2" + gsettings: + dependency: transitive + description: + name: gsettings + sha256: "1b0ce661f5436d2db1e51f3c4295a49849f03d304003a7ba177d01e3a858249c" + url: "https://pub.dev" + source: hosted + version: "0.2.8" + hooks: + dependency: transitive + description: + name: hooks + sha256: e79ed1e8e1929bc6ecb6ec85f0cb519c887aa5b423705ded0d0f2d9226def388 + url: "https://pub.dev" + source: hosted + version: "1.0.2" + http: + dependency: "direct main" + description: + name: http + sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412" + url: "https://pub.dev" + source: hosted + version: "1.6.0" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" + url: "https://pub.dev" + source: hosted + version: "4.1.2" + intl: + dependency: transitive + description: + name: intl + sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5" + url: "https://pub.dev" + source: hosted + version: "0.20.2" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" + url: "https://pub.dev" + source: hosted + version: "11.0.2" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" + url: "https://pub.dev" + source: hosted + version: "3.0.10" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + lints: + dependency: transitive + description: + name: lints + sha256: "12f842a479589fea194fe5c5a3095abc7be0c1f2ddfa9a0e76aed1dbd26a87df" + url: "https://pub.dev" + source: hosted + version: "6.1.0" + logging: + dependency: transitive + description: + name: logging + sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 + url: "https://pub.dev" + source: hosted + version: "1.3.0" + matcher: + dependency: transitive + description: + name: matcher + sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861 + url: "https://pub.dev" + source: hosted + version: "0.12.19" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" + url: "https://pub.dev" + source: hosted + version: "0.13.0" + meta: + dependency: transitive + description: + name: meta + sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" + url: "https://pub.dev" + source: hosted + version: "1.17.0" + mime: + dependency: transitive + description: + name: mime + sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" + url: "https://pub.dev" + source: hosted + version: "2.0.0" + native_toolchain_c: + dependency: transitive + description: + name: native_toolchain_c + sha256: "92b2ca62c8bd2b8d2f267cdfccf9bfbdb7322f778f8f91b3ce5b5cda23a3899f" + url: "https://pub.dev" + source: hosted + version: "0.17.5" + nested: + dependency: transitive + description: + name: nested + sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" + url: "https://pub.dev" + source: hosted + version: "1.0.0" + objective_c: + dependency: transitive + description: + name: objective_c + sha256: "100a1c87616ab6ed41ec263b083c0ef3261ee6cd1dc3b0f35f8ddfa4f996fe52" + url: "https://pub.dev" + source: hosted + version: "9.3.0" + package_info_plus: + dependency: transitive + description: + name: package_info_plus + sha256: f69da0d3189a4b4ceaeb1a3defb0f329b3b352517f52bed4290f83d4f06bc08d + url: "https://pub.dev" + source: hosted + version: "9.0.0" + package_info_plus_platform_interface: + dependency: transitive + description: + name: package_info_plus_platform_interface + sha256: "202a487f08836a592a6bd4f901ac69b3a8f146af552bbd14407b6b41e1c3f086" + url: "https://pub.dev" + source: hosted + version: "3.2.1" + path: + dependency: transitive + description: + name: path + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" + url: "https://pub.dev" + source: hosted + version: "1.9.1" + path_provider: + dependency: "direct main" + description: + name: path_provider + sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" + url: "https://pub.dev" + source: hosted + version: "2.1.5" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: f2c65e21139ce2c3dad46922be8272bb5963516045659e71bb16e151c93b580e + url: "https://pub.dev" + source: hosted + version: "2.2.22" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: "2a376b7d6392d80cd3705782d2caa734ca4727776db0b6ec36ef3f1855197699" + url: "https://pub.dev" + source: hosted + version: "2.6.0" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 + url: "https://pub.dev" + source: hosted + version: "2.2.1" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 + url: "https://pub.dev" + source: hosted + version: "2.3.0" + permission_handler: + dependency: "direct main" + description: + name: permission_handler + sha256: bc917da36261b00137bbc8896bf1482169cd76f866282368948f032c8c1caae1 + url: "https://pub.dev" + source: hosted + version: "12.0.1" + permission_handler_android: + dependency: transitive + description: + name: permission_handler_android + sha256: "1e3bc410ca1bf84662104b100eb126e066cb55791b7451307f9708d4007350e6" + url: "https://pub.dev" + source: hosted + version: "13.0.1" + permission_handler_apple: + dependency: transitive + description: + name: permission_handler_apple + sha256: f000131e755c54cf4d84a5d8bd6e4149e262cc31c5a8b1d698de1ac85fa41023 + url: "https://pub.dev" + source: hosted + version: "9.4.7" + permission_handler_html: + dependency: transitive + description: + name: permission_handler_html + sha256: "38f000e83355abb3392140f6bc3030660cfaef189e1f87824facb76300b4ff24" + url: "https://pub.dev" + source: hosted + version: "0.1.3+5" + permission_handler_platform_interface: + dependency: transitive + description: + name: permission_handler_platform_interface + sha256: eb99b295153abce5d683cac8c02e22faab63e50679b937fa1bf67d58bb282878 + url: "https://pub.dev" + source: hosted + version: "4.3.0" + permission_handler_windows: + dependency: transitive + description: + name: permission_handler_windows + sha256: "1a790728016f79a41216d88672dbc5df30e686e811ad4e698bfc51f76ad91f1e" + url: "https://pub.dev" + source: hosted + version: "0.2.1" + petitparser: + dependency: transitive + description: + name: petitparser + sha256: "91bd59303e9f769f108f8df05e371341b15d59e995e6806aefab827b58336675" + url: "https://pub.dev" + source: hosted + version: "7.0.2" + platform: + dependency: transitive + description: + name: platform + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" + url: "https://pub.dev" + source: hosted + version: "3.1.6" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.dev" + source: hosted + version: "2.1.8" + provider: + dependency: "direct main" + description: + name: provider + sha256: "4e82183fa20e5ca25703ead7e05de9e4cceed1fbd1eadc1ac3cb6f565a09f272" + url: "https://pub.dev" + source: hosted + version: "6.1.5+1" + pub_semver: + dependency: transitive + description: + name: pub_semver + sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" + url: "https://pub.dev" + source: hosted + version: "2.2.0" + quiver: + dependency: transitive + description: + name: quiver + sha256: ea0b925899e64ecdfbf9c7becb60d5b50e706ade44a85b2363be2a22d88117d2 + url: "https://pub.dev" + source: hosted + version: "3.2.2" + shared_preferences: + dependency: "direct main" + description: + name: shared_preferences + sha256: "2939ae520c9024cb197fc20dee269cd8cdbf564c8b5746374ec6cacdc5169e64" + url: "https://pub.dev" + source: hosted + version: "2.5.4" + shared_preferences_android: + dependency: transitive + description: + name: shared_preferences_android + sha256: "8374d6200ab33ac99031a852eba4c8eb2170c4bf20778b3e2c9eccb45384fb41" + url: "https://pub.dev" + source: hosted + version: "2.4.21" + shared_preferences_foundation: + dependency: transitive + description: + name: shared_preferences_foundation + sha256: "4e7eaffc2b17ba398759f1151415869a34771ba11ebbccd1b0145472a619a64f" + url: "https://pub.dev" + source: hosted + version: "2.5.6" + shared_preferences_linux: + dependency: transitive + description: + name: shared_preferences_linux + sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shared_preferences_platform_interface: + dependency: transitive + description: + name: shared_preferences_platform_interface + sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shared_preferences_web: + dependency: transitive + description: + name: shared_preferences_web + sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019 + url: "https://pub.dev" + source: hosted + version: "2.4.3" + shared_preferences_windows: + dependency: transitive + description: + name: shared_preferences_windows + sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + source_span: + dependency: transitive + description: + name: source_span + sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab" + url: "https://pub.dev" + source: hosted + version: "1.10.2" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" + url: "https://pub.dev" + source: hosted + version: "1.12.1" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + stream_transform: + dependency: transitive + description: + name: stream_transform + sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871 + url: "https://pub.dev" + source: hosted + version: "2.1.1" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" + url: "https://pub.dev" + source: hosted + version: "1.4.1" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" + url: "https://pub.dev" + source: hosted + version: "1.2.2" + test_api: + dependency: transitive + description: + name: test_api + sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a" + url: "https://pub.dev" + source: hosted + version: "0.7.10" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + url_launcher: + dependency: "direct main" + description: + name: url_launcher + sha256: f6a7e5c4835bb4e3026a04793a4199ca2d14c739ec378fdfe23fc8075d0439f8 + url: "https://pub.dev" + source: hosted + version: "6.3.2" + url_launcher_android: + dependency: transitive + description: + name: url_launcher_android + sha256: "767344bf3063897b5cf0db830e94f904528e6dd50a6dfaf839f0abf509009611" + url: "https://pub.dev" + source: hosted + version: "6.3.28" + url_launcher_ios: + dependency: transitive + description: + name: url_launcher_ios + sha256: "580fe5dfb51671ae38191d316e027f6b76272b026370708c2d898799750a02b0" + url: "https://pub.dev" + source: hosted + version: "6.4.1" + url_launcher_linux: + dependency: transitive + description: + name: url_launcher_linux + sha256: d5e14138b3bc193a0f63c10a53c94b91d399df0512b1f29b94a043db7482384a + url: "https://pub.dev" + source: hosted + version: "3.2.2" + url_launcher_macos: + dependency: transitive + description: + name: url_launcher_macos + sha256: "368adf46f71ad3c21b8f06614adb38346f193f3a59ba8fe9a2fd74133070ba18" + url: "https://pub.dev" + source: hosted + version: "3.2.5" + url_launcher_platform_interface: + dependency: transitive + description: + name: url_launcher_platform_interface + sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + url_launcher_web: + dependency: transitive + description: + name: url_launcher_web + sha256: d0412fcf4c6b31ecfdb7762359b7206ffba3bbffd396c6d9f9c4616ece476c1f + url: "https://pub.dev" + source: hosted + version: "2.4.2" + url_launcher_windows: + dependency: transitive + description: + name: url_launcher_windows + sha256: "712c70ab1b99744ff066053cbe3e80c73332b38d46e5e945c98689b2e66fc15f" + url: "https://pub.dev" + source: hosted + version: "3.1.5" + uuid: + dependency: "direct main" + description: + name: uuid + sha256: "1fef9e8e11e2991bb773070d4656b7bd5d850967a2456cfc83cf47925ba79489" + url: "https://pub.dev" + source: hosted + version: "4.5.3" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b + url: "https://pub.dev" + source: hosted + version: "2.2.0" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60" + url: "https://pub.dev" + source: hosted + version: "15.0.2" + web: + dependency: transitive + description: + name: web + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" + url: "https://pub.dev" + source: hosted + version: "1.1.1" + web_socket: + dependency: transitive + description: + name: web_socket + sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c" + url: "https://pub.dev" + source: hosted + version: "1.0.1" + web_socket_channel: + dependency: "direct main" + description: + name: web_socket_channel + sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8 + url: "https://pub.dev" + source: hosted + version: "3.0.3" + webview_flutter: + dependency: "direct main" + description: + name: webview_flutter + sha256: a3da219916aba44947d3a5478b1927876a09781174b5a2b67fa5be0555154bf9 + url: "https://pub.dev" + source: hosted + version: "4.13.1" + webview_flutter_android: + dependency: transitive + description: + name: webview_flutter_android + sha256: "2a03df01df2fd30b075d1e7f24c28aee593f2e5d5ac4c3c4283c5eda63717b24" + url: "https://pub.dev" + source: hosted + version: "4.10.13" + webview_flutter_platform_interface: + dependency: transitive + description: + name: webview_flutter_platform_interface + sha256: "63d26ee3aca7256a83ccb576a50272edd7cfc80573a4305caa98985feb493ee0" + url: "https://pub.dev" + source: hosted + version: "2.14.0" + webview_flutter_wkwebview: + dependency: transitive + description: + name: webview_flutter_wkwebview + sha256: fc0af89d403e1c053f03d023d97550412fa79f35332e2939514c82e6fe633198 + url: "https://pub.dev" + source: hosted + version: "3.23.8" + win32: + dependency: transitive + description: + name: win32 + sha256: d7cb55e04cd34096cd3a79b3330245f54cb96a370a1c27adb3c84b917de8b08e + url: "https://pub.dev" + source: hosted + version: "5.15.0" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + xml: + dependency: transitive + description: + name: xml + sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025" + url: "https://pub.dev" + source: hosted + version: "6.6.1" + xterm: + dependency: "direct main" + description: + name: xterm + sha256: "168dfedca77cba33fdb6f52e2cd001e9fde216e398e89335c19b524bb22da3a2" + url: "https://pub.dev" + source: hosted + version: "4.0.0" + yaml: + dependency: transitive + description: + name: yaml + sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce + url: "https://pub.dev" + source: hosted + version: "3.1.3" + zmodem: + dependency: transitive + description: + name: zmodem + sha256: "3b7e5b29f3a7d8aee472029b05165a68438eff2f3f7766edf13daba1e297adbf" + url: "https://pub.dev" + source: hosted + version: "0.0.6" +sdks: + dart: ">=3.10.3 <4.0.0" + flutter: ">=3.38.4" diff --git a/flutter_app/pubspec.yaml b/flutter_app/pubspec.yaml index 9ba5c4d..ae54141 100644 --- a/flutter_app/pubspec.yaml +++ b/flutter_app/pubspec.yaml @@ -9,6 +9,8 @@ environment: dependencies: flutter: sdk: flutter + flutter_localizations: + sdk: flutter xterm: ^4.0.0 flutter_pty: ^0.4.2 webview_flutter: ^4.4.0 @@ -17,19 +19,19 @@ dependencies: provider: ^6.1.0 shared_preferences: ^2.2.0 path_provider: ^2.1.0 - permission_handler: ^11.3.0 + permission_handler: ^12.0.1 url_launcher: ^6.2.0 web_socket_channel: ^3.0.0 cryptography: ^2.7.0 - google_fonts: ^6.1.0 + google_fonts: ^8.0.2 uuid: ^4.2.0 - camera: ^0.11.0 - geolocator: ^12.0.0 + camera: ^0.12.0 + geolocator: ^14.0.2 dev_dependencies: flutter_test: sdk: flutter - flutter_lints: ^5.0.0 + flutter_lints: ^6.0.0 flutter: uses-material-design: true diff --git a/scripts/fetch-proot-binaries.ps1 b/scripts/fetch-proot-binaries.ps1 new file mode 100644 index 0000000..e2242e4 --- /dev/null +++ b/scripts/fetch-proot-binaries.ps1 @@ -0,0 +1,143 @@ +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path +$repoRoot = Split-Path -Parent $scriptDir +$jniLibsDir = Join-Path $repoRoot 'flutter_app\android\app\src\main\jniLibs' +$tempRoot = Join-Path ([System.IO.Path]::GetTempPath()) ("openclaw-proot-" + [System.Guid]::NewGuid().ToString('N')) +$repoBase = 'https://packages.termux.dev/apt/termux-main' +$packageIndexCache = @{} + +New-Item -ItemType Directory -Force -Path $tempRoot | Out-Null + +function Get-TermuxPackageFilename { + param( + [string]$PackageName, + [string]$DebArch + ) + + if (-not $packageIndexCache.ContainsKey($DebArch)) { + $packagesPath = Join-Path $tempRoot ("Packages-$DebArch.txt") + $packagesUrl = "$repoBase/dists/stable/main/binary-$DebArch/Packages" + & curl.exe -fsSL $packagesUrl -o $packagesPath + $packageIndexCache[$DebArch] = $packagesPath + } + + $packagesFile = $packageIndexCache[$DebArch] + $match = Select-String -Path $packagesFile -Pattern "^Package: $([regex]::Escape($PackageName))$" -Context 0,12 | Select-Object -First 1 + if ($match) { + foreach ($line in $match.Context.PostContext) { + if ($line -match '^Filename:\s+(.+)$') { + return $Matches[1].Trim() + } + } + } + + throw "Package $PackageName not found for architecture $DebArch" +} + +function Expand-DebPackage { + param( + [string]$DebPath, + [string]$Destination + ) + + New-Item -ItemType Directory -Force -Path $Destination | Out-Null + Push-Location $Destination + try { + & ar.exe x $DebPath | Out-Null + + if (Test-Path 'data.tar.xz') { + & xz.exe -d -k 'data.tar.xz' | Out-Null + tar -xf 'data.tar' | Out-Null + } elseif (Test-Path 'data.tar.gz') { + tar -xf 'data.tar.gz' | Out-Null + } elseif (Test-Path 'data.tar.zst') { + tar -xf 'data.tar.zst' | Out-Null + } else { + throw "Unsupported deb payload format in $DebPath" + } + } finally { + Pop-Location + } +} + +function Copy-FirstMatch { + param( + [string[]]$Patterns, + [string]$DestinationPath, + [switch]$AllowMissing + ) + + foreach ($pattern in $Patterns) { + $item = Get-ChildItem -Path $pattern -File -ErrorAction SilentlyContinue | Select-Object -First 1 + if ($item) { + Copy-Item -LiteralPath $item.FullName -Destination $DestinationPath -Force + return $true + } + } + + if (-not $AllowMissing) { + throw "No file matched patterns: $($Patterns -join ', ')" + } + + return $false +} + +function Fetch-ForAbi { + param( + [string]$JniAbi, + [string]$DebArch + ) + + Write-Host "[$JniAbi] Fetching binaries..." + $outDir = Join-Path $jniLibsDir $JniAbi + $extractBase = Join-Path $tempRoot $JniAbi + $prootExtract = Join-Path $extractBase 'proot' + $tallocExtract = Join-Path $extractBase 'libtalloc' + + New-Item -ItemType Directory -Force -Path $outDir | Out-Null + + $prootFilename = Get-TermuxPackageFilename -PackageName 'proot' -DebArch $DebArch + $prootDeb = Join-Path $tempRoot ("proot-$DebArch.deb") + & curl.exe -fsSL "$repoBase/$prootFilename" -o $prootDeb + Expand-DebPackage -DebPath $prootDeb -Destination $prootExtract + + $tallocFilename = Get-TermuxPackageFilename -PackageName 'libtalloc' -DebArch $DebArch + $tallocDeb = Join-Path $tempRoot ("libtalloc-$DebArch.deb") + & curl.exe -fsSL "$repoBase/$tallocFilename" -o $tallocDeb + Expand-DebPackage -DebPath $tallocDeb -Destination $tallocExtract + + Copy-FirstMatch -Patterns @( + (Join-Path $prootExtract 'data\data\com.termux\files\usr\bin\proot'), + (Join-Path $prootExtract '**\bin\proot') + ) -DestinationPath (Join-Path $outDir 'libproot.so') | Out-Null + + Copy-FirstMatch -Patterns @( + (Join-Path $prootExtract 'data\data\com.termux\files\usr\libexec\proot\loader'), + (Join-Path $prootExtract '**\proot\loader') + ) -DestinationPath (Join-Path $outDir 'libprootloader.so') | Out-Null + + Copy-FirstMatch -Patterns @( + (Join-Path $prootExtract 'data\data\com.termux\files\usr\libexec\proot\loader32'), + (Join-Path $prootExtract '**\proot\loader32') + ) -DestinationPath (Join-Path $outDir 'libprootloader32.so') -AllowMissing | Out-Null + + Copy-FirstMatch -Patterns @( + (Join-Path $tallocExtract 'data\data\com.termux\files\usr\lib\libtalloc.so.*'), + (Join-Path $tallocExtract '**\libtalloc.so.*'), + (Join-Path $tallocExtract '**\libtalloc.so') + ) -DestinationPath (Join-Path $outDir 'libtalloc.so') | Out-Null + + Get-ChildItem $outDir -File | ForEach-Object { + Write-Host "[$JniAbi] $($_.Name) ($([Math]::Round($_.Length / 1KB, 1)) KB)" + } +} + +try { + Fetch-ForAbi -JniAbi 'arm64-v8a' -DebArch 'aarch64' + Fetch-ForAbi -JniAbi 'armeabi-v7a' -DebArch 'arm' + Fetch-ForAbi -JniAbi 'x86_64' -DebArch 'x86_64' +} finally { + Remove-Item -LiteralPath $tempRoot -Recurse -Force -ErrorAction SilentlyContinue +} \ No newline at end of file From c62877cd0b9bfd0be3a53e47a16d69a800b0b000 Mon Sep 17 00:00:00 2001 From: TIANLI0 <507249007@qq.com> Date: Sun, 8 Mar 2026 18:43:39 +0800 Subject: [PATCH 2/9] feat: enhance AI provider configuration and localization support --- flutter_app/lib/l10n/app_localizations.dart | 95 ++++++++++++ flutter_app/lib/models/ai_provider.dart | 112 +++++++++++--- .../lib/screens/provider_detail_screen.dart | 137 ++++++++++++++---- flutter_app/lib/screens/providers_screen.dart | 50 ++++--- .../capabilities/location_capability.dart | 9 +- .../lib/services/provider_config_service.dart | 31 ++-- flutter_app/lib/widgets/gateway_controls.dart | 2 +- 7 files changed, 355 insertions(+), 81 deletions(-) diff --git a/flutter_app/lib/l10n/app_localizations.dart b/flutter_app/lib/l10n/app_localizations.dart index 1fc85b2..a76f080 100644 --- a/flutter_app/lib/l10n/app_localizations.dart +++ b/flutter_app/lib/l10n/app_localizations.dart @@ -68,6 +68,57 @@ class AppLocalizations { 'dashboardConfigureSubtitle': 'Manage gateway settings', 'dashboardProvidersTitle': 'AI Providers', 'dashboardProvidersSubtitle': 'Configure models and API keys', + 'providersScreenTitle': 'AI Providers', + 'providersScreenActiveModel': 'Active Model', + 'providersScreenIntro': + 'Select a provider to configure its API key, endpoint, and model.', + 'providersStatusActive': 'Active', + 'providersStatusConfigured': 'Configured', + 'providerDetailApiKey': 'API Key', + 'providerDetailApiKeyEmpty': 'API key cannot be empty', + 'providerDetailEndpoint': 'API Base URL', + 'providerDetailEndpointHelper': + 'Override the default endpoint if your account uses a custom or regional API base URL.', + 'providerDetailEndpointInvalid': 'Enter a valid absolute API base URL', + 'providerDetailModel': 'Model', + 'providerDetailModelEmpty': 'Model name cannot be empty', + 'providerDetailCustomModelAction': 'Custom...', + 'providerDetailCustomModelLabel': 'Custom model name', + 'providerDetailCustomModelHint': 'e.g. meta/llama-3.3-70b-instruct', + 'providerDetailSaveAction': 'Save & Activate', + 'providerDetailSaved': '{provider} configured and activated', + 'providerDetailSaveFailed': 'Failed to save: {error}', + 'providerDetailRemoveTitle': 'Remove {provider}?', + 'providerDetailRemoveBody': + 'This will delete the API key, endpoint, and saved model for this provider.', + 'providerDetailRemoveAction': 'Remove', + 'providerDetailRemoveConfiguration': 'Remove Configuration', + 'providerDetailRemoved': '{provider} removed', + 'providerDetailRemoveFailed': 'Failed to remove: {error}', + 'providerNameAnthropic': 'Anthropic', + 'providerDescriptionAnthropic': + 'Claude models for advanced reasoning and coding', + 'providerNameOpenai': 'OpenAI', + 'providerDescriptionOpenai': 'GPT and o-series models', + 'providerNameQwen': 'Qwen', + 'providerDescriptionQwen': + 'Alibaba Cloud Qwen models via DashScope OpenAI-compatible API', + 'providerNameMinimax': 'MiniMax', + 'providerDescriptionMinimax': + 'MiniMax chat models with editable API endpoint support', + 'providerNameDoubao': 'Doubao', + 'providerDescriptionDoubao': + 'Volcengine Ark / Doubao models with official Ark endpoint presets', + 'providerNameGoogle': 'Google Gemini', + 'providerDescriptionGoogle': 'Gemini family of multimodal models', + 'providerNameOpenrouter': 'OpenRouter', + 'providerDescriptionOpenrouter': 'Unified API for hundreds of models', + 'providerNameNvidia': 'NVIDIA NIM', + 'providerDescriptionNvidia': 'GPU-optimized inference endpoints', + 'providerNameDeepseek': 'DeepSeek', + 'providerDescriptionDeepseek': 'High-performance open models', + 'providerNameXai': 'xAI', + 'providerDescriptionXai': 'Grok models from xAI', 'dashboardPackagesTitle': 'Packages', 'dashboardPackagesSubtitle': 'Install optional tools (Go, Homebrew, SSH)', 'dashboardSshTitle': 'SSH Access', @@ -311,6 +362,50 @@ class AppLocalizations { 'dashboardConfigureSubtitle': '管理网关设置', 'dashboardProvidersTitle': 'AI 提供商', 'dashboardProvidersSubtitle': '配置模型和 API Key', + 'providersScreenTitle': 'AI 提供商', + 'providersScreenActiveModel': '当前激活模型', + 'providersScreenIntro': '选择一个提供商,配置它的 API Key、端点和模型。', + 'providersStatusActive': '已激活', + 'providersStatusConfigured': '已配置', + 'providerDetailApiKey': 'API Key', + 'providerDetailApiKeyEmpty': 'API Key 不能为空', + 'providerDetailEndpoint': 'API 基础地址', + 'providerDetailEndpointHelper': '如果你的账号使用自定义或区域专属端点,可以在这里覆盖默认地址。', + 'providerDetailEndpointInvalid': '请输入有效的绝对 API 地址', + 'providerDetailModel': '模型', + 'providerDetailModelEmpty': '模型名称不能为空', + 'providerDetailCustomModelAction': '自定义...', + 'providerDetailCustomModelLabel': '自定义模型名', + 'providerDetailCustomModelHint': '例如:meta/llama-3.3-70b-instruct', + 'providerDetailSaveAction': '保存并激活', + 'providerDetailSaved': '已配置并激活 {provider}', + 'providerDetailSaveFailed': '保存失败:{error}', + 'providerDetailRemoveTitle': '移除 {provider}?', + 'providerDetailRemoveBody': '这会删除该提供商保存的 API Key、端点和模型。', + 'providerDetailRemoveAction': '移除', + 'providerDetailRemoveConfiguration': '移除配置', + 'providerDetailRemoved': '已移除 {provider}', + 'providerDetailRemoveFailed': '移除失败:{error}', + 'providerNameAnthropic': 'Anthropic', + 'providerDescriptionAnthropic': 'Claude 系列模型,适合复杂推理与编程', + 'providerNameOpenai': 'OpenAI', + 'providerDescriptionOpenai': 'GPT 与 o 系列模型', + 'providerNameQwen': '通义千问', + 'providerDescriptionQwen': '通过 DashScope OpenAI 兼容接口接入千问模型', + 'providerNameMinimax': 'MiniMax', + 'providerDescriptionMinimax': 'MiniMax 对话模型,支持自定义 API 端点', + 'providerNameDoubao': '豆包', + 'providerDescriptionDoubao': '火山方舟 / 豆包模型,内置官方 Ark 端点预设', + 'providerNameGoogle': 'Google Gemini', + 'providerDescriptionGoogle': 'Gemini 多模态模型家族', + 'providerNameOpenrouter': 'OpenRouter', + 'providerDescriptionOpenrouter': '统一接入数百种模型的 API', + 'providerNameNvidia': 'NVIDIA NIM', + 'providerDescriptionNvidia': '面向 GPU 推理的优化端点', + 'providerNameDeepseek': 'DeepSeek', + 'providerDescriptionDeepseek': '高性能开源模型服务', + 'providerNameXai': 'xAI', + 'providerDescriptionXai': 'xAI 的 Grok 系列模型', 'dashboardPackagesTitle': '可选组件', 'dashboardPackagesSubtitle': '安装可选工具(Go、Homebrew、SSH)', 'dashboardSshTitle': 'SSH 访问', diff --git a/flutter_app/lib/models/ai_provider.dart b/flutter_app/lib/models/ai_provider.dart index 094d8a5..8beaeac 100644 --- a/flutter_app/lib/models/ai_provider.dart +++ b/flutter_app/lib/models/ai_provider.dart @@ -1,32 +1,44 @@ import 'package:flutter/material.dart'; +import '../l10n/app_localizations.dart'; /// Metadata for an AI model provider that can be configured /// to power the OpenClaw gateway. class AiProvider { final String id; - final String name; - final String description; + final String nameKey; + final String descriptionKey; final IconData icon; final Color color; final String baseUrl; final List defaultModels; final String apiKeyHint; + final bool supportsCustomBaseUrl; const AiProvider({ required this.id, - required this.name, - required this.description, + required this.nameKey, + required this.descriptionKey, required this.icon, required this.color, required this.baseUrl, required this.defaultModels, required this.apiKeyHint, + this.supportsCustomBaseUrl = false, }); + String name(AppLocalizations l10n) => l10n.t(nameKey); + + String description(AppLocalizations l10n) => l10n.t(descriptionKey); + + bool matchesModel(String model) { + return defaultModels.any((candidate) => model.contains(candidate)) || + model.contains(id); + } + static const anthropic = AiProvider( id: 'anthropic', - name: 'Anthropic', - description: 'Claude models — advanced reasoning and coding', + nameKey: 'providerNameAnthropic', + descriptionKey: 'providerDescriptionAnthropic', icon: Icons.psychology, color: Color(0xFFD97706), baseUrl: 'https://api.anthropic.com/v1', @@ -41,8 +53,8 @@ class AiProvider { static const openai = AiProvider( id: 'openai', - name: 'OpenAI', - description: 'GPT and o-series models', + nameKey: 'providerNameOpenai', + descriptionKey: 'providerDescriptionOpenai', icon: Icons.auto_awesome, color: Color(0xFF10A37F), baseUrl: 'https://api.openai.com/v1', @@ -54,12 +66,13 @@ class AiProvider { 'gpt-4-turbo', ], apiKeyHint: 'sk-...', + supportsCustomBaseUrl: true, ); static const google = AiProvider( id: 'google', - name: 'Google Gemini', - description: 'Gemini family of multimodal models', + nameKey: 'providerNameGoogle', + descriptionKey: 'providerDescriptionGoogle', icon: Icons.diamond, color: Color(0xFF4285F4), baseUrl: 'https://generativelanguage.googleapis.com/v1beta', @@ -74,8 +87,8 @@ class AiProvider { static const openrouter = AiProvider( id: 'openrouter', - name: 'OpenRouter', - description: 'Unified API for hundreds of models', + nameKey: 'providerNameOpenrouter', + descriptionKey: 'providerDescriptionOpenrouter', icon: Icons.route, color: Color(0xFF6366F1), baseUrl: 'https://openrouter.ai/api/v1', @@ -90,8 +103,8 @@ class AiProvider { static const nvidia = AiProvider( id: 'nvidia', - name: 'NVIDIA NIM', - description: 'GPU-optimized inference endpoints', + nameKey: 'providerNameNvidia', + descriptionKey: 'providerDescriptionNvidia', icon: Icons.memory, color: Color(0xFF76B900), baseUrl: 'https://integrate.api.nvidia.com/v1', @@ -107,8 +120,8 @@ class AiProvider { static const deepseek = AiProvider( id: 'deepseek', - name: 'DeepSeek', - description: 'High-performance open models', + nameKey: 'providerNameDeepseek', + descriptionKey: 'providerDescriptionDeepseek', icon: Icons.explore, color: Color(0xFF0EA5E9), baseUrl: 'https://api.deepseek.com/v1', @@ -121,8 +134,8 @@ class AiProvider { static const xai = AiProvider( id: 'xai', - name: 'xAI', - description: 'Grok models from xAI', + nameKey: 'providerNameXai', + descriptionKey: 'providerDescriptionXai', icon: Icons.bolt, color: Color(0xFFEF4444), baseUrl: 'https://api.x.ai/v1', @@ -134,6 +147,67 @@ class AiProvider { apiKeyHint: 'xai-...', ); + static const qwen = AiProvider( + id: 'qwen', + nameKey: 'providerNameQwen', + descriptionKey: 'providerDescriptionQwen', + icon: Icons.cloud, + color: Color(0xFF2563EB), + baseUrl: 'https://dashscope.aliyuncs.com/compatible-mode/v1', + defaultModels: [ + 'qwen-max', + 'qwen-plus', + 'qwen-turbo', + 'qwen3-coder-plus', + ], + apiKeyHint: 'sk-...', + supportsCustomBaseUrl: true, + ); + + static const minimax = AiProvider( + id: 'minimax', + nameKey: 'providerNameMinimax', + descriptionKey: 'providerDescriptionMinimax', + icon: Icons.forum, + color: Color(0xFFEC4899), + baseUrl: 'https://api.minimax.chat/v1', + defaultModels: [ + 'MiniMax-Text-01', + 'MiniMax-M1', + 'abab7.5-chat', + ], + apiKeyHint: 'sk-...', + supportsCustomBaseUrl: true, + ); + + static const doubao = AiProvider( + id: 'doubao', + nameKey: 'providerNameDoubao', + descriptionKey: 'providerDescriptionDoubao', + icon: Icons.token, + color: Color(0xFFEA580C), + baseUrl: 'https://ark.cn-beijing.volces.com/api/v3', + defaultModels: [ + 'doubao-seed-2-0-pro-260215', + 'doubao-seed-2-0-lite-260215', + 'doubao-seed-2-0-mini-260215', + 'doubao-seed-1-8-251228', + ], + apiKeyHint: 'ark-...', + supportsCustomBaseUrl: true, + ); + /// All available AI providers. - static const all = [anthropic, openai, google, openrouter, nvidia, deepseek, xai]; + static const all = [ + anthropic, + openai, + qwen, + minimax, + doubao, + google, + openrouter, + nvidia, + deepseek, + xai, + ]; } diff --git a/flutter_app/lib/screens/provider_detail_screen.dart b/flutter_app/lib/screens/provider_detail_screen.dart index 30fc8a4..b9d0bc5 100644 --- a/flutter_app/lib/screens/provider_detail_screen.dart +++ b/flutter_app/lib/screens/provider_detail_screen.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import '../app.dart'; +import '../l10n/app_localizations.dart'; import '../models/ai_provider.dart'; import '../services/provider_config_service.dart'; @@ -7,12 +8,14 @@ import '../services/provider_config_service.dart'; class ProviderDetailScreen extends StatefulWidget { final AiProvider provider; final String? existingApiKey; + final String? existingBaseUrl; final String? existingModel; const ProviderDetailScreen({ super.key, required this.provider, this.existingApiKey, + this.existingBaseUrl, this.existingModel, }); @@ -24,6 +27,7 @@ class _ProviderDetailScreenState extends State { static const _customModelSentinel = '__custom__'; late final TextEditingController _apiKeyController; + late final TextEditingController _baseUrlController; late final TextEditingController _customModelController; late String _selectedModel; bool _isCustomModel = false; @@ -31,19 +35,27 @@ class _ProviderDetailScreenState extends State { bool _saving = false; bool _removing = false; - bool get _isConfigured => widget.existingApiKey != null && widget.existingApiKey!.isNotEmpty; + bool get _isConfigured => + widget.existingApiKey != null && widget.existingApiKey!.isNotEmpty; /// Returns the effective model name to save. String get _effectiveModel => _isCustomModel ? _customModelController.text.trim() : _selectedModel; + bool get _supportsCustomBaseUrl => widget.provider.supportsCustomBaseUrl; + @override void initState() { super.initState(); - _apiKeyController = TextEditingController(text: widget.existingApiKey ?? ''); + _apiKeyController = + TextEditingController(text: widget.existingApiKey ?? ''); + _baseUrlController = TextEditingController( + text: widget.existingBaseUrl ?? widget.provider.baseUrl, + ); _customModelController = TextEditingController(); - final existing = widget.existingModel ?? widget.provider.defaultModels.first; + final existing = + widget.existingModel ?? widget.provider.defaultModels.first; if (widget.provider.defaultModels.contains(existing)) { _selectedModel = existing; } else { @@ -57,22 +69,36 @@ class _ProviderDetailScreenState extends State { @override void dispose() { _apiKeyController.dispose(); + _baseUrlController.dispose(); _customModelController.dispose(); super.dispose(); } + bool _isValidBaseUrl(String value) { + final uri = Uri.tryParse(value); + return uri != null && uri.hasScheme && uri.hasAuthority; + } + Future _save() async { + final l10n = context.l10n; final apiKey = _apiKeyController.text.trim(); if (apiKey.isEmpty) { ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('API key cannot be empty')), + SnackBar(content: Text(l10n.t('providerDetailApiKeyEmpty'))), + ); + return; + } + final baseUrl = _baseUrlController.text.trim(); + if (_supportsCustomBaseUrl && !_isValidBaseUrl(baseUrl)) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(l10n.t('providerDetailEndpointInvalid'))), ); return; } final model = _effectiveModel; if (model.isEmpty) { ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Model name cannot be empty')), + SnackBar(content: Text(l10n.t('providerDetailModelEmpty'))), ); return; } @@ -82,18 +108,27 @@ class _ProviderDetailScreenState extends State { await ProviderConfigService.saveProviderConfig( provider: widget.provider, apiKey: apiKey, + baseUrl: baseUrl, model: model, ); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('${widget.provider.name} configured and activated')), + SnackBar( + content: Text( + l10n.t('providerDetailSaved', { + 'provider': widget.provider.name(l10n), + }), + ), + ), ); Navigator.of(context).pop(true); } } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Failed to save: $e')), + SnackBar( + content: Text(l10n.t('providerDetailSaveFailed', {'error': '$e'})), + ), ); } } finally { @@ -102,19 +137,24 @@ class _ProviderDetailScreenState extends State { } Future _remove() async { + final l10n = context.l10n; final confirmed = await showDialog( context: context, builder: (ctx) => AlertDialog( - title: Text('Remove ${widget.provider.name}?'), - content: const Text('This will delete the API key and deactivate the model.'), + title: Text( + l10n.t('providerDetailRemoveTitle', { + 'provider': widget.provider.name(l10n), + }), + ), + content: Text(l10n.t('providerDetailRemoveBody')), actions: [ TextButton( onPressed: () => Navigator.pop(ctx, false), - child: const Text('Cancel'), + child: Text(l10n.t('commonCancel')), ), FilledButton( onPressed: () => Navigator.pop(ctx, true), - child: const Text('Remove'), + child: Text(l10n.t('providerDetailRemoveAction')), ), ], ), @@ -124,17 +164,28 @@ class _ProviderDetailScreenState extends State { setState(() => _removing = true); try { - await ProviderConfigService.removeProviderConfig(provider: widget.provider); + await ProviderConfigService.removeProviderConfig( + provider: widget.provider); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('${widget.provider.name} removed')), + SnackBar( + content: Text( + l10n.t('providerDetailRemoved', { + 'provider': widget.provider.name(l10n), + }), + ), + ), ); Navigator.of(context).pop(true); } } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Failed to remove: $e')), + SnackBar( + content: Text( + l10n.t('providerDetailRemoveFailed', {'error': '$e'}), + ), + ), ); } } finally { @@ -144,12 +195,13 @@ class _ProviderDetailScreenState extends State { @override Widget build(BuildContext context) { + final l10n = context.l10n; final theme = Theme.of(context); final isDark = theme.brightness == Brightness.dark; final iconBg = isDark ? AppColors.darkSurfaceAlt : const Color(0xFFF3F4F6); return Scaffold( - appBar: AppBar(title: Text(widget.provider.name)), + appBar: AppBar(title: Text(widget.provider.name(l10n))), body: ListView( padding: const EdgeInsets.all(16), children: [ @@ -166,7 +218,8 @@ class _ProviderDetailScreenState extends State { color: iconBg, borderRadius: BorderRadius.circular(12), ), - child: Icon(widget.provider.icon, color: widget.provider.color), + child: Icon(widget.provider.icon, + color: widget.provider.color), ), const SizedBox(width: 16), Expanded( @@ -174,14 +227,14 @@ class _ProviderDetailScreenState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - widget.provider.name, + widget.provider.name(l10n), style: theme.textTheme.titleMedium?.copyWith( fontWeight: FontWeight.w600, ), ), const SizedBox(height: 4), Text( - widget.provider.description, + widget.provider.description(l10n), style: theme.textTheme.bodySmall?.copyWith( color: theme.colorScheme.onSurfaceVariant, ), @@ -197,8 +250,9 @@ class _ProviderDetailScreenState extends State { // API Key Text( - 'API Key', - style: theme.textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w600), + l10n.t('providerDetailApiKey'), + style: theme.textTheme.titleSmall + ?.copyWith(fontWeight: FontWeight.w600), ), const SizedBox(height: 8), TextField( @@ -207,17 +261,37 @@ class _ProviderDetailScreenState extends State { decoration: InputDecoration( hintText: widget.provider.apiKeyHint, suffixIcon: IconButton( - icon: Icon(_obscureKey ? Icons.visibility_off : Icons.visibility), + icon: + Icon(_obscureKey ? Icons.visibility_off : Icons.visibility), onPressed: () => setState(() => _obscureKey = !_obscureKey), ), ), ), + if (_supportsCustomBaseUrl) ...[ + const SizedBox(height: 24), + Text( + l10n.t('providerDetailEndpoint'), + style: theme.textTheme.titleSmall?.copyWith( + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 8), + TextField( + controller: _baseUrlController, + keyboardType: TextInputType.url, + decoration: InputDecoration( + hintText: widget.provider.baseUrl, + helperText: l10n.t('providerDetailEndpointHelper'), + ), + ), + ], const SizedBox(height: 24), // Model selection Text( - 'Model', - style: theme.textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w600), + l10n.t('providerDetailModel'), + style: theme.textTheme.titleSmall + ?.copyWith(fontWeight: FontWeight.w600), ), const SizedBox(height: 8), DropdownButtonFormField( @@ -227,9 +301,9 @@ class _ProviderDetailScreenState extends State { items: [ ...widget.provider.defaultModels .map((m) => DropdownMenuItem(value: m, child: Text(m))), - const DropdownMenuItem( + DropdownMenuItem( value: _customModelSentinel, - child: Text('Custom...'), + child: Text(l10n.t('providerDetailCustomModelAction')), ), ], onChanged: (value) { @@ -245,9 +319,9 @@ class _ProviderDetailScreenState extends State { const SizedBox(height: 12), TextField( controller: _customModelController, - decoration: const InputDecoration( - hintText: 'e.g. meta/llama-3.3-70b-instruct', - labelText: 'Custom model name', + decoration: InputDecoration( + hintText: l10n.t('providerDetailCustomModelHint'), + labelText: l10n.t('providerDetailCustomModelLabel'), ), ), ], @@ -260,9 +334,10 @@ class _ProviderDetailScreenState extends State { ? const SizedBox( height: 20, width: 20, - child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white), + child: CircularProgressIndicator( + strokeWidth: 2, color: Colors.white), ) - : const Text('Save & Activate'), + : Text(l10n.t('providerDetailSaveAction')), ), if (_isConfigured) ...[ const SizedBox(height: 12), @@ -274,7 +349,7 @@ class _ProviderDetailScreenState extends State { width: 20, child: CircularProgressIndicator(strokeWidth: 2), ) - : const Text('Remove Configuration'), + : Text(l10n.t('providerDetailRemoveConfiguration')), ), ], ], diff --git a/flutter_app/lib/screens/providers_screen.dart b/flutter_app/lib/screens/providers_screen.dart index 3bbb535..96ca083 100644 --- a/flutter_app/lib/screens/providers_screen.dart +++ b/flutter_app/lib/screens/providers_screen.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import '../app.dart'; +import '../l10n/app_localizations.dart'; import '../models/ai_provider.dart'; import '../services/provider_config_service.dart'; import 'provider_detail_screen.dart'; @@ -41,7 +42,8 @@ class _ProvidersScreenState extends State { builder: (_) => ProviderDetailScreen( provider: provider, existingApiKey: providerConfig?['apiKey'] as String?, - existingModel: _activeModel, + existingBaseUrl: providerConfig?['baseUrl'] as String?, + existingModel: providerConfig?['model'] as String? ?? _activeModel, ), ), ); @@ -50,25 +52,36 @@ class _ProvidersScreenState extends State { } } - String _statusLabel(AiProvider provider) { + ({String label, bool isActive}) _statusInfo(AiProvider provider) { + final l10n = context.l10n; final isConfigured = _providers.containsKey(provider.id); - if (!isConfigured) return ''; - // Check if the active model belongs to this provider + if (!isConfigured) { + return (label: '', isActive: false); + } + + final providerConfig = _providers[provider.id] as Map?; + final configuredModel = providerConfig?['model'] as String?; if (_activeModel != null) { - final isActive = provider.defaultModels.any((m) => _activeModel!.contains(m)) || - _activeModel!.contains(provider.id); - if (isActive) return 'Active'; + final isActive = configuredModel == _activeModel || + provider.matchesModel(_activeModel!); + if (isActive) { + return (label: l10n.t('providersStatusActive'), isActive: true); + } } - return 'Configured'; + return ( + label: l10n.t('providersStatusConfigured'), + isActive: false, + ); } @override Widget build(BuildContext context) { + final l10n = context.l10n; final theme = Theme.of(context); final isDark = theme.brightness == Brightness.dark; return Scaffold( - appBar: AppBar(title: const Text('AI Providers')), + appBar: AppBar(title: Text(l10n.t('providersScreenTitle'))), body: _loading ? const Center(child: CircularProgressIndicator()) : ListView( @@ -98,7 +111,7 @@ class _ProvidersScreenState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - 'Active Model', + l10n.t('providersScreenActiveModel'), style: theme.textTheme.labelSmall?.copyWith( color: AppColors.statusGreen, fontWeight: FontWeight.w600, @@ -121,7 +134,7 @@ class _ProvidersScreenState extends State { const SizedBox(height: 16), ], Text( - 'Select a provider to configure its API key and model.', + l10n.t('providersScreenIntro'), style: theme.textTheme.bodyMedium?.copyWith( color: theme.colorScheme.onSurfaceVariant, ), @@ -136,7 +149,8 @@ class _ProvidersScreenState extends State { Widget _buildProviderCard(ThemeData theme, AiProvider provider, bool isDark) { final iconBg = isDark ? AppColors.darkSurfaceAlt : const Color(0xFFF3F4F6); - final status = _statusLabel(provider); + final status = _statusInfo(provider); + final l10n = context.l10n; return Card( margin: const EdgeInsets.only(bottom: 12), @@ -164,12 +178,12 @@ class _ProvidersScreenState extends State { Row( children: [ Text( - provider.name, + provider.name(l10n), style: theme.textTheme.titleSmall?.copyWith( fontWeight: FontWeight.w600, ), ), - if (status.isNotEmpty) ...[ + if (status.label.isNotEmpty) ...[ const SizedBox(width: 8), Container( padding: const EdgeInsets.symmetric( @@ -177,16 +191,16 @@ class _ProvidersScreenState extends State { vertical: 2, ), decoration: BoxDecoration( - color: (status == 'Active' + color: (status.isActive ? AppColors.statusGreen : AppColors.statusAmber) .withAlpha(25), borderRadius: BorderRadius.circular(12), ), child: Text( - status, + status.label, style: theme.textTheme.labelSmall?.copyWith( - color: status == 'Active' + color: status.isActive ? AppColors.statusGreen : AppColors.statusAmber, fontWeight: FontWeight.w600, @@ -198,7 +212,7 @@ class _ProvidersScreenState extends State { ), const SizedBox(height: 4), Text( - provider.description, + provider.description(l10n), style: theme.textTheme.bodySmall?.copyWith( color: theme.colorScheme.onSurfaceVariant, ), diff --git a/flutter_app/lib/services/capabilities/location_capability.dart b/flutter_app/lib/services/capabilities/location_capability.dart index 5558768..0fa5852 100644 --- a/flutter_app/lib/services/capabilities/location_capability.dart +++ b/flutter_app/lib/services/capabilities/location_capability.dart @@ -66,8 +66,10 @@ class LocationCapability extends CapabilityHandler { try { final position = await Geolocator.getCurrentPosition( - desiredAccuracy: LocationAccuracy.high, - timeLimit: const Duration(seconds: 10), + locationSettings: const LocationSettings( + accuracy: LocationAccuracy.high, + timeLimit: Duration(seconds: 10), + ), ); return _positionToFrame(position); } on TimeoutException { @@ -78,7 +80,8 @@ class LocationCapability extends CapabilityHandler { } return NodeFrame.response('', error: { 'code': 'LOCATION_TIMEOUT', - 'message': 'Could not get location within 10 seconds and no cached position available', + 'message': + 'Could not get location within 10 seconds and no cached position available', }); } } catch (e) { diff --git a/flutter_app/lib/services/provider_config_service.dart b/flutter_app/lib/services/provider_config_service.dart index 1d40411..5947d47 100644 --- a/flutter_app/lib/services/provider_config_service.dart +++ b/flutter_app/lib/services/provider_config_service.dart @@ -13,7 +13,7 @@ class ProviderConfigService { /// Read the current config and return a map with: /// - `activeModel`: the current primary model string (or null) - /// - `providers`: Map for configured providers + /// - `providers`: `Map` for configured providers static Future> readConfig() async { try { final content = await NativeBridge.readRootfsFile(_configPath); @@ -39,7 +39,8 @@ class ProviderConfigService { final providers = {}; final modelsSection = config['models'] as Map?; if (modelsSection != null) { - final providerEntries = modelsSection['providers'] as Map?; + final providerEntries = + modelsSection['providers'] as Map?; if (providerEntries != null) { for (final entry in providerEntries.entries) { providers[entry.key] = entry.value; @@ -60,10 +61,15 @@ class ProviderConfigService { required AiProvider provider, required String apiKey, required String model, + String? baseUrl, }) async { + final resolvedBaseUrl = baseUrl != null && baseUrl.trim().isNotEmpty + ? baseUrl.trim() + : provider.baseUrl; final providerJson = jsonEncode({ 'apiKey': apiKey, - 'baseUrl': provider.baseUrl, + 'baseUrl': resolvedBaseUrl, + 'model': model, }); final modelJson = jsonEncode(model); final providerIdJson = jsonEncode(provider.id); @@ -93,7 +99,7 @@ fs.writeFileSync(p, JSON.stringify(c, null, 2)); await _saveConfigDirect( providerId: provider.id, apiKey: apiKey, - baseUrl: provider.baseUrl, + baseUrl: resolvedBaseUrl, model: model, ); } @@ -118,17 +124,24 @@ fs.writeFileSync(p, JSON.stringify(c, null, 2)); // Merge provider entry config['models'] ??= {}; - (config['models'] as Map)['providers'] ??= {}; - ((config['models'] as Map)['providers'] as Map)[providerId] = { + (config['models'] as Map)['providers'] ??= + {}; + ((config['models'] as Map)['providers'] + as Map)[providerId] = { 'apiKey': apiKey, 'baseUrl': baseUrl, + 'model': model, }; // Set active model config['agents'] ??= {}; - (config['agents'] as Map)['defaults'] ??= {}; - ((config['agents'] as Map)['defaults'] as Map)['model'] ??= {}; - (((config['agents'] as Map)['defaults'] as Map)['model'] as Map)['primary'] = model; + (config['agents'] as Map)['defaults'] ??= + {}; + ((config['agents'] as Map)['defaults'] + as Map)['model'] ??= {}; + (((config['agents'] as Map)['defaults'] + as Map)['model'] + as Map)['primary'] = model; const encoder = JsonEncoder.withIndent(' '); await NativeBridge.writeRootfsFile(_configPath, encoder.convert(config)); diff --git a/flutter_app/lib/widgets/gateway_controls.dart b/flutter_app/lib/widgets/gateway_controls.dart index 2db1568..49bc834 100644 --- a/flutter_app/lib/widgets/gateway_controls.dart +++ b/flutter_app/lib/widgets/gateway_controls.dart @@ -76,7 +76,7 @@ class GatewayControls extends StatelessWidget { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(l10n.t('gatewayUrlCopied')), - duration: Duration(seconds: 2), + duration: const Duration(seconds: 2), ), ); }, From c022673af045a8a3e61f29adaa62985303e0d00f Mon Sep 17 00:00:00 2001 From: TIANLI0 <507249007@qq.com> Date: Sun, 8 Mar 2026 19:38:49 +0800 Subject: [PATCH 3/9] feat: Add localization support for Simplified and Traditional Chinese - Created app_strings_zh_hans.dart for Simplified Chinese translations. - Created app_strings_zh_hant.dart for Traditional Chinese translations. - Implemented _expand_l10n.dart script to generate localized string maps from English and Japanese sources. --- flutter_app/lib/app.dart | 15 + flutter_app/lib/l10n/app_localizations.dart | 612 ++---------------- flutter_app/lib/l10n/app_strings_en.dart | 297 +++++++++ flutter_app/lib/l10n/app_strings_ja.dart | 266 ++++++++ flutter_app/lib/l10n/app_strings_zh_hans.dart | 272 ++++++++ flutter_app/lib/l10n/app_strings_zh_hant.dart | 266 ++++++++ .../lib/providers/locale_provider.dart | 20 +- flutter_app/lib/screens/settings_screen.dart | 8 + flutter_app/scripts/_expand_l10n.dart | 61 ++ 9 files changed, 1247 insertions(+), 570 deletions(-) create mode 100644 flutter_app/lib/l10n/app_strings_en.dart create mode 100644 flutter_app/lib/l10n/app_strings_ja.dart create mode 100644 flutter_app/lib/l10n/app_strings_zh_hans.dart create mode 100644 flutter_app/lib/l10n/app_strings_zh_hant.dart create mode 100644 flutter_app/scripts/_expand_l10n.dart diff --git a/flutter_app/lib/app.dart b/flutter_app/lib/app.dart index 89cbb3b..62c9c01 100644 --- a/flutter_app/lib/app.dart +++ b/flutter_app/lib/app.dart @@ -66,6 +66,21 @@ class OpenClawApp extends StatelessWidget { } for (final deviceLocale in deviceLocales ?? const []) { + for (final supportedLocale in supportedLocales) { + if (supportedLocale.languageCode == deviceLocale.languageCode && + supportedLocale.scriptCode == deviceLocale.scriptCode && + supportedLocale.countryCode == deviceLocale.countryCode) { + return supportedLocale; + } + } + + for (final supportedLocale in supportedLocales) { + if (supportedLocale.languageCode == deviceLocale.languageCode && + supportedLocale.scriptCode == deviceLocale.scriptCode) { + return supportedLocale; + } + } + for (final supportedLocale in supportedLocales) { if (supportedLocale.languageCode == deviceLocale.languageCode) { return supportedLocale; diff --git a/flutter_app/lib/l10n/app_localizations.dart b/flutter_app/lib/l10n/app_localizations.dart index a76f080..058d41b 100644 --- a/flutter_app/lib/l10n/app_localizations.dart +++ b/flutter_app/lib/l10n/app_localizations.dart @@ -1,5 +1,10 @@ import 'package:flutter/material.dart'; +import 'app_strings_en.dart'; +import 'app_strings_ja.dart'; +import 'app_strings_zh_hans.dart'; +import 'app_strings_zh_hant.dart'; + class AppLocalizations { AppLocalizations(this.locale); @@ -8,6 +13,8 @@ class AppLocalizations { static const supportedLocales = [ Locale('en'), Locale('zh'), + Locale.fromSubtags(languageCode: 'zh', scriptCode: 'Hant'), + Locale('ja'), ]; static const LocalizationsDelegate delegate = @@ -23,9 +30,12 @@ class AppLocalizations { } String t(String key, [Map params = const {}]) { - final localized = - _localizedValues[locale.languageCode] ?? _localizedValues['en']!; + final localeKey = _localeToKey(locale); + final localized = _localizedValues[localeKey] ?? + _localizedValues[locale.languageCode] ?? + _localizedValues['en']!; final fallback = _localizedValues['en']!; + var value = localized[key] ?? fallback[key] ?? key; for (final entry in params.entries) { value = value.replaceAll('{${entry.key}}', '${entry.value ?? ''}'); @@ -33,570 +43,36 @@ class AppLocalizations { return value; } - static const Map> _localizedValues = { - 'en': { - 'appName': 'OpenClaw', - 'language': 'Language', - 'languageSystem': 'System default', - 'languageEnglish': 'English', - 'languageChinese': 'Simplified Chinese', - 'commonInstalled': 'Installed', - 'commonNotInstalled': 'Not installed', - 'commonCancel': 'Cancel', - 'commonCopy': 'Copy', - 'commonCopiedToClipboard': 'Copied to clipboard', - 'commonOpen': 'Open', - 'commonPaste': 'Paste', - 'commonRetry': 'Retry', - 'commonDone': 'Done', - 'commonConfigure': 'Configure', - 'commonScreenshot': 'Screenshot', - 'commonSaveFailed': 'Failed to capture screenshot', - 'commonScreenshotSaved': 'Screenshot saved: {fileName}', - 'commonNoUrlFound': 'No URL found in selection', - 'commonOpenLink': 'Open Link', - 'commonLinkCopied': 'Link copied', - 'dashboardQuickActions': 'Quick actions', - 'dashboardTerminalTitle': 'Terminal', - 'dashboardTerminalSubtitle': 'Open Ubuntu shell with OpenClaw', - 'dashboardWebDashboardTitle': 'Web Dashboard', - 'dashboardWebDashboardSubtitle': 'Open OpenClaw dashboard in browser', - 'dashboardStartGatewayFirst': 'Start gateway first', - 'dashboardOnboardingTitle': 'Onboarding', - 'dashboardOnboardingSubtitle': 'Configure API keys and binding', - 'dashboardConfigureTitle': 'Configure', - 'dashboardConfigureSubtitle': 'Manage gateway settings', - 'dashboardProvidersTitle': 'AI Providers', - 'dashboardProvidersSubtitle': 'Configure models and API keys', - 'providersScreenTitle': 'AI Providers', - 'providersScreenActiveModel': 'Active Model', - 'providersScreenIntro': - 'Select a provider to configure its API key, endpoint, and model.', - 'providersStatusActive': 'Active', - 'providersStatusConfigured': 'Configured', - 'providerDetailApiKey': 'API Key', - 'providerDetailApiKeyEmpty': 'API key cannot be empty', - 'providerDetailEndpoint': 'API Base URL', - 'providerDetailEndpointHelper': - 'Override the default endpoint if your account uses a custom or regional API base URL.', - 'providerDetailEndpointInvalid': 'Enter a valid absolute API base URL', - 'providerDetailModel': 'Model', - 'providerDetailModelEmpty': 'Model name cannot be empty', - 'providerDetailCustomModelAction': 'Custom...', - 'providerDetailCustomModelLabel': 'Custom model name', - 'providerDetailCustomModelHint': 'e.g. meta/llama-3.3-70b-instruct', - 'providerDetailSaveAction': 'Save & Activate', - 'providerDetailSaved': '{provider} configured and activated', - 'providerDetailSaveFailed': 'Failed to save: {error}', - 'providerDetailRemoveTitle': 'Remove {provider}?', - 'providerDetailRemoveBody': - 'This will delete the API key, endpoint, and saved model for this provider.', - 'providerDetailRemoveAction': 'Remove', - 'providerDetailRemoveConfiguration': 'Remove Configuration', - 'providerDetailRemoved': '{provider} removed', - 'providerDetailRemoveFailed': 'Failed to remove: {error}', - 'providerNameAnthropic': 'Anthropic', - 'providerDescriptionAnthropic': - 'Claude models for advanced reasoning and coding', - 'providerNameOpenai': 'OpenAI', - 'providerDescriptionOpenai': 'GPT and o-series models', - 'providerNameQwen': 'Qwen', - 'providerDescriptionQwen': - 'Alibaba Cloud Qwen models via DashScope OpenAI-compatible API', - 'providerNameMinimax': 'MiniMax', - 'providerDescriptionMinimax': - 'MiniMax chat models with editable API endpoint support', - 'providerNameDoubao': 'Doubao', - 'providerDescriptionDoubao': - 'Volcengine Ark / Doubao models with official Ark endpoint presets', - 'providerNameGoogle': 'Google Gemini', - 'providerDescriptionGoogle': 'Gemini family of multimodal models', - 'providerNameOpenrouter': 'OpenRouter', - 'providerDescriptionOpenrouter': 'Unified API for hundreds of models', - 'providerNameNvidia': 'NVIDIA NIM', - 'providerDescriptionNvidia': 'GPU-optimized inference endpoints', - 'providerNameDeepseek': 'DeepSeek', - 'providerDescriptionDeepseek': 'High-performance open models', - 'providerNameXai': 'xAI', - 'providerDescriptionXai': 'Grok models from xAI', - 'dashboardPackagesTitle': 'Packages', - 'dashboardPackagesSubtitle': 'Install optional tools (Go, Homebrew, SSH)', - 'dashboardSshTitle': 'SSH Access', - 'dashboardSshSubtitle': 'Remote terminal access via SSH', - 'dashboardLogsTitle': 'Logs', - 'dashboardLogsSubtitle': 'View gateway output and errors', - 'dashboardSnapshotTitle': 'Snapshot', - 'dashboardSnapshotSubtitle': 'Backup or restore your config', - 'dashboardNodeTitle': 'Node', - 'dashboardNodeConnected': 'Connected to gateway', - 'dashboardNodeDisabled': 'Device capabilities for AI', - 'dashboardVersionLabel': 'OpenClaw v{version}', - 'dashboardAuthorLabel': 'by {author} | {org}', - 'gatewayTitle': 'Gateway', - 'gatewayCopyUrl': 'Copy URL', - 'gatewayUrlCopied': 'URL copied to clipboard', - 'gatewayOpenDashboard': 'Open dashboard', - 'gatewayStart': 'Start Gateway', - 'gatewayStop': 'Stop Gateway', - 'gatewayViewLogs': 'View Logs', - 'gatewayStatusRunning': 'Running', - 'gatewayStatusStarting': 'Starting', - 'gatewayStatusError': 'Error', - 'gatewayStatusStopped': 'Stopped', - 'logsTitle': 'Gateway Logs', - 'logsAutoScrollOn': 'Auto-scroll on', - 'logsAutoScrollOff': 'Auto-scroll off', - 'logsCopyAll': 'Copy all logs', - 'logsFilterHint': 'Filter logs...', - 'logsEmpty': 'No logs yet. Start the gateway.', - 'logsNoMatch': 'No matching logs.', - 'logsCopied': 'Logs copied to clipboard', - 'packagesTitle': 'Optional Packages', - 'packagesDescription': - 'Development tools you can install inside the Ubuntu environment.', - 'packagesInstall': 'Install', - 'packagesUninstall': 'Uninstall', - 'packagesUninstallTitle': 'Uninstall {name}?', - 'packagesUninstallDescription': - 'This will remove {name} from the environment.', - 'packageGoDescription': 'Go programming language compiler and tools', - 'packageBrewDescription': 'The missing package manager for Linux', - 'packageSshDescription': 'SSH client and server for secure remote access', - 'setupWizardTitle': 'Setup OpenClaw', - 'setupWizardIntroIdle': - 'This will download Ubuntu, Node.js, and OpenClaw into a self-contained environment.', - 'setupWizardIntroRunning': - 'Setting up the environment. This may take several minutes.', - 'setupWizardConfigureApiKeys': 'Configure API Keys', - 'setupWizardRetry': 'Retry Setup', - 'setupWizardBegin': 'Begin Setup', - 'setupWizardRequirements': - 'Requires ~500MB of storage and an internet connection', - 'setupWizardStorageDialogTitle': 'Grant file access before setup', - 'setupWizardStorageDialogBody': - 'OpenClaw setup needs file management access before installation starts, so the Ubuntu environment can mount shared storage correctly inside proot. The Android permission page will open next.', - 'setupWizardStoragePermissionRequired': - 'File management access is required before setup can start.', - 'setupWizardOptionalPackages': 'OPTIONAL PACKAGES', - 'setupWizardStepDownloadRootfs': 'Download Ubuntu rootfs', - 'setupWizardStepExtractRootfs': 'Extract rootfs', - 'setupWizardStepInstallNode': 'Install Node.js', - 'setupWizardStepInstallOpenClaw': 'Install OpenClaw', - 'setupWizardStepConfigureBypass': 'Configure Bionic Bypass', - 'setupWizardComplete': 'Setup complete!', - 'setupWizardStatusSetupComplete': 'Setup complete', - 'setupWizardStatusSetupRequired': 'Setup required', - 'setupWizardStatusSettingUpDirs': 'Setting up directories...', - 'setupWizardStatusDownloadingUbuntuRootfs': - 'Downloading Ubuntu rootfs...', - 'setupWizardStatusDownloadingProgress': - 'Downloading: {current} MB / {total} MB', - 'setupWizardStatusExtractingRootfs': - 'Extracting rootfs (this takes a while)...', - 'setupWizardStatusRootfsExtracted': 'Rootfs extracted', - 'setupWizardStatusFixingPermissions': 'Fixing rootfs permissions...', - 'setupWizardStatusUpdatingPackageLists': 'Updating package lists...', - 'setupWizardStatusInstallingBasePackages': 'Installing base packages...', - 'setupWizardStatusDownloadingNode': 'Downloading Node.js {version}...', - 'setupWizardStatusDownloadingNodeProgress': - 'Downloading Node.js: {current} MB / {total} MB', - 'setupWizardStatusExtractingNode': 'Extracting Node.js...', - 'setupWizardStatusVerifyingNode': 'Verifying Node.js...', - 'setupWizardStatusNodeInstalled': 'Node.js installed', - 'setupWizardStatusInstallingOpenClaw': - 'Installing OpenClaw (this may take a few minutes)...', - 'setupWizardStatusCreatingBinWrappers': 'Creating bin wrappers...', - 'setupWizardStatusVerifyingOpenClaw': 'Verifying OpenClaw...', - 'setupWizardStatusOpenClawInstalled': 'OpenClaw installed', - 'setupWizardStatusBypassConfigured': 'Bionic Bypass configured', - 'setupWizardStatusReady': 'Setup complete! Ready to start the gateway.', - 'onboardingTitle': 'OpenClaw Onboarding', - 'onboardingStarting': 'Starting onboarding...', - 'onboardingGoToDashboard': 'Go to Dashboard', - 'onboardingStartFailed': 'Failed to start onboarding: {error}', - 'configureTitle': 'OpenClaw Configure', - 'configureStarting': 'Starting configure...', - 'configureStartFailed': 'Failed to start configure: {error}', - 'nodeTitle': 'Node', - 'nodeConfigurationTitle': 'Node Configuration', - 'nodeGatewayConnection': 'Gateway connection', - 'nodeLocalGateway': 'Local Gateway', - 'nodeLocalGatewaySubtitle': 'Auto-pair with gateway on this device', - 'nodeRemoteGateway': 'Remote Gateway', - 'nodeRemoteGatewaySubtitle': 'Connect to a gateway on another device', - 'nodeGatewayHost': 'Gateway Host', - 'nodeGatewayPort': 'Gateway Port', - 'nodeGatewayToken': 'Gateway Token', - 'nodeGatewayTokenHint': 'Paste token from gateway dashboard URL', - 'nodeGatewayTokenHelper': 'Found in dashboard URL after #token=', - 'nodeConnect': 'Connect', - 'nodePairing': 'Pairing', - 'nodeApproveCode': 'Approve this code on the gateway:', - 'nodeCapabilities': 'Capabilities', - 'nodeCapabilityCameraTitle': 'Camera', - 'nodeCapabilityCameraSubtitle': 'Capture photos and video clips', - 'nodeCapabilityCanvasTitle': 'Canvas', - 'nodeCapabilityCanvasSubtitle': 'Navigate and interact with web pages', - 'nodeCapabilityLocationTitle': 'Location', - 'nodeCapabilityLocationSubtitle': 'Get device GPS coordinates', - 'nodeCapabilityScreenTitle': 'Screen Recording', - 'nodeCapabilityScreenSubtitle': - 'Record device screen (requires consent each time)', - 'nodeCapabilityFlashlightTitle': 'Flashlight', - 'nodeCapabilityFlashlightSubtitle': 'Toggle device torch on/off', - 'nodeCapabilityVibrationTitle': 'Vibration', - 'nodeCapabilityVibrationSubtitle': - 'Trigger haptic feedback and vibration patterns', - 'nodeCapabilitySensorsTitle': 'Sensors', - 'nodeCapabilitySensorsSubtitle': - 'Read accelerometer, gyroscope, magnetometer, barometer', - 'nodeDeviceInfo': 'Device info', - 'nodeDeviceId': 'Device ID', - 'nodeLogs': 'Node logs', - 'nodeNoLogs': 'No logs yet', - 'nodeConnectedTo': 'Connected to {host}:{port}', - 'nodePairingCode': 'Pairing code: ', - 'nodeEnable': 'Enable Node', - 'nodeDisable': 'Disable Node', - 'nodeReconnect': 'Reconnect', - 'nodeStatusPaired': 'Paired', - 'nodeStatusConnecting': 'Connecting', - 'nodeStatusError': 'Error', - 'nodeStatusDisabled': 'Disabled', - 'nodeStatusDisconnected': 'Disconnected', - 'settingsTitle': 'Settings', - 'settingsGeneral': 'General', - 'settingsAutoStart': 'Auto-start gateway', - 'settingsAutoStartSubtitle': 'Start the gateway when the app opens', - 'settingsBatteryOptimization': 'Battery Optimization', - 'settingsBatteryOptimized': 'Optimized (may kill background sessions)', - 'settingsBatteryUnrestricted': 'Unrestricted (recommended)', - 'settingsStorage': 'Setup Storage', - 'settingsStorageGranted': 'Granted — /sdcard accessible in proot', - 'settingsStorageMissing': 'Allow access to shared storage', - 'settingsStorageDialogTitle': 'Grant file access', - 'settingsStorageDialogBody': - 'OpenClaw needs file management access to read and write snapshot files in shared storage. You will be taken to the system settings page next.', - 'settingsStorageDialogAction': 'Continue', - 'onboardingStorageDialogTitle': 'Grant file access for onboarding', - 'onboardingStorageDialogBody': - 'OpenClaw onboarding needs file management access so Ubuntu can mount shared storage inside proot while you configure API keys and binding. The Android permission page will open next.', - 'onboardingStoragePermissionRequired': - 'File management access is required before onboarding can continue.', - 'configureStorageDialogTitle': 'Grant file access for configuration', - 'configureStorageDialogBody': - 'OpenClaw configuration needs file management access so Ubuntu can mount shared storage inside proot while you manage gateway settings. The Android permission page will open next.', - 'configureStoragePermissionRequired': - 'File management access is required before configuration can continue.', - 'settingsNode': 'Node', - 'settingsEnableNode': 'Enable Node', - 'settingsEnableNodeSubtitle': - 'Provide device capabilities to the gateway', - 'settingsNodeConfiguration': 'Node Configuration', - 'settingsNodeConfigurationSubtitle': - 'Connection, pairing, and capabilities', - 'settingsSystemInfo': 'System info', - 'settingsArchitecture': 'Architecture', - 'settingsProotPath': 'PRoot path', - 'settingsRootfs': 'Rootfs', - 'settingsNodeJs': 'Node.js', - 'settingsOpenClaw': 'OpenClaw', - 'settingsGo': 'Go (Golang)', - 'settingsHomebrew': 'Homebrew', - 'settingsOpenSsh': 'OpenSSH', - 'settingsMaintenance': 'Maintenance', - 'settingsExportSnapshot': 'Export Snapshot', - 'settingsExportSnapshotSubtitle': 'Backup config to Downloads', - 'settingsImportSnapshot': 'Import Snapshot', - 'settingsImportSnapshotSubtitle': 'Restore config from backup', - 'settingsRerunSetup': 'Re-run setup', - 'settingsRerunSetupSubtitle': 'Reinstall or repair the environment', - 'settingsAbout': 'About', - 'settingsAboutSubtitle': 'AI Gateway for Android\nVersion {version}', - 'settingsDeveloper': 'Developer', - 'settingsGithub': 'GitHub', - 'settingsContact': 'Contact', - 'settingsLicense': 'License', - 'settingsPlayStore': 'Play Store', - 'settingsEmail': 'Email', - 'settingsSnapshotSaved': 'Snapshot saved to {path}', - 'settingsExportFailed': 'Export failed: {error}', - 'settingsSnapshotMissing': 'No snapshot found at {path}', - 'settingsSnapshotRestored': - 'Snapshot restored successfully. Restart the gateway to apply.', - 'settingsImportFailed': 'Import failed: {error}', - 'statusInstalled': 'Installed', - 'statusNotInstalled': 'Not installed', - }, - 'zh': { - 'appName': 'OpenClaw', - 'language': '语言', - 'languageSystem': '跟随系统', - 'languageEnglish': '英语', - 'languageChinese': '简体中文', - 'commonInstalled': '已安装', - 'commonNotInstalled': '未安装', - 'commonCancel': '取消', - 'commonCopy': '复制', - 'commonCopiedToClipboard': '已复制到剪贴板', - 'commonOpen': '打开', - 'commonPaste': '粘贴', - 'commonRetry': '重试', - 'commonDone': '完成', - 'commonConfigure': '配置', - 'commonScreenshot': '截图', - 'commonSaveFailed': '截图失败', - 'commonScreenshotSaved': '截图已保存:{fileName}', - 'commonNoUrlFound': '所选内容中未找到 URL', - 'commonOpenLink': '打开链接', - 'commonLinkCopied': '链接已复制', - 'dashboardQuickActions': '快捷操作', - 'dashboardTerminalTitle': '终端', - 'dashboardTerminalSubtitle': '打开 Ubuntu Shell 并使用 OpenClaw', - 'dashboardWebDashboardTitle': 'Web 控制台', - 'dashboardWebDashboardSubtitle': '在浏览器中打开 OpenClaw 控制台', - 'dashboardStartGatewayFirst': '请先启动网关', - 'dashboardOnboardingTitle': '引导配置', - 'dashboardOnboardingSubtitle': '配置 API Key 和绑定信息', - 'dashboardConfigureTitle': '网关配置', - 'dashboardConfigureSubtitle': '管理网关设置', - 'dashboardProvidersTitle': 'AI 提供商', - 'dashboardProvidersSubtitle': '配置模型和 API Key', - 'providersScreenTitle': 'AI 提供商', - 'providersScreenActiveModel': '当前激活模型', - 'providersScreenIntro': '选择一个提供商,配置它的 API Key、端点和模型。', - 'providersStatusActive': '已激活', - 'providersStatusConfigured': '已配置', - 'providerDetailApiKey': 'API Key', - 'providerDetailApiKeyEmpty': 'API Key 不能为空', - 'providerDetailEndpoint': 'API 基础地址', - 'providerDetailEndpointHelper': '如果你的账号使用自定义或区域专属端点,可以在这里覆盖默认地址。', - 'providerDetailEndpointInvalid': '请输入有效的绝对 API 地址', - 'providerDetailModel': '模型', - 'providerDetailModelEmpty': '模型名称不能为空', - 'providerDetailCustomModelAction': '自定义...', - 'providerDetailCustomModelLabel': '自定义模型名', - 'providerDetailCustomModelHint': '例如:meta/llama-3.3-70b-instruct', - 'providerDetailSaveAction': '保存并激活', - 'providerDetailSaved': '已配置并激活 {provider}', - 'providerDetailSaveFailed': '保存失败:{error}', - 'providerDetailRemoveTitle': '移除 {provider}?', - 'providerDetailRemoveBody': '这会删除该提供商保存的 API Key、端点和模型。', - 'providerDetailRemoveAction': '移除', - 'providerDetailRemoveConfiguration': '移除配置', - 'providerDetailRemoved': '已移除 {provider}', - 'providerDetailRemoveFailed': '移除失败:{error}', - 'providerNameAnthropic': 'Anthropic', - 'providerDescriptionAnthropic': 'Claude 系列模型,适合复杂推理与编程', - 'providerNameOpenai': 'OpenAI', - 'providerDescriptionOpenai': 'GPT 与 o 系列模型', - 'providerNameQwen': '通义千问', - 'providerDescriptionQwen': '通过 DashScope OpenAI 兼容接口接入千问模型', - 'providerNameMinimax': 'MiniMax', - 'providerDescriptionMinimax': 'MiniMax 对话模型,支持自定义 API 端点', - 'providerNameDoubao': '豆包', - 'providerDescriptionDoubao': '火山方舟 / 豆包模型,内置官方 Ark 端点预设', - 'providerNameGoogle': 'Google Gemini', - 'providerDescriptionGoogle': 'Gemini 多模态模型家族', - 'providerNameOpenrouter': 'OpenRouter', - 'providerDescriptionOpenrouter': '统一接入数百种模型的 API', - 'providerNameNvidia': 'NVIDIA NIM', - 'providerDescriptionNvidia': '面向 GPU 推理的优化端点', - 'providerNameDeepseek': 'DeepSeek', - 'providerDescriptionDeepseek': '高性能开源模型服务', - 'providerNameXai': 'xAI', - 'providerDescriptionXai': 'xAI 的 Grok 系列模型', - 'dashboardPackagesTitle': '可选组件', - 'dashboardPackagesSubtitle': '安装可选工具(Go、Homebrew、SSH)', - 'dashboardSshTitle': 'SSH 访问', - 'dashboardSshSubtitle': '通过 SSH 远程访问终端', - 'dashboardLogsTitle': '日志', - 'dashboardLogsSubtitle': '查看网关输出和错误', - 'dashboardSnapshotTitle': '快照', - 'dashboardSnapshotSubtitle': '备份或恢复你的配置', - 'dashboardNodeTitle': '节点', - 'dashboardNodeConnected': '已连接到网关', - 'dashboardNodeDisabled': '为 AI 提供设备能力', - 'dashboardVersionLabel': 'OpenClaw v{version}', - 'dashboardAuthorLabel': '作者 {author} | {org}', - 'gatewayTitle': '网关', - 'gatewayCopyUrl': '复制 URL', - 'gatewayUrlCopied': 'URL 已复制到剪贴板', - 'gatewayOpenDashboard': '打开控制台', - 'gatewayStart': '启动网关', - 'gatewayStop': '停止网关', - 'gatewayViewLogs': '查看日志', - 'gatewayStatusRunning': '运行中', - 'gatewayStatusStarting': '启动中', - 'gatewayStatusError': '错误', - 'gatewayStatusStopped': '已停止', - 'logsTitle': '网关日志', - 'logsAutoScrollOn': '自动滚动已开启', - 'logsAutoScrollOff': '自动滚动已关闭', - 'logsCopyAll': '复制全部日志', - 'logsFilterHint': '筛选日志...', - 'logsEmpty': '还没有日志。请先启动网关。', - 'logsNoMatch': '没有匹配的日志。', - 'logsCopied': '日志已复制到剪贴板', - 'packagesTitle': '可选组件', - 'packagesDescription': '可在 Ubuntu 环境内安装的开发工具。', - 'packagesInstall': '安装', - 'packagesUninstall': '卸载', - 'packagesUninstallTitle': '卸载 {name}?', - 'packagesUninstallDescription': '这会将 {name} 从环境中移除。', - 'packageGoDescription': 'Go 编程语言编译器和工具链', - 'packageBrewDescription': 'Linux 上常用的缺省包管理器', - 'packageSshDescription': '用于安全远程访问的 SSH 客户端和服务端', - 'setupWizardTitle': '开始配置 OpenClaw', - 'setupWizardIntroIdle': '这会将 Ubuntu、Node.js 和 OpenClaw 下载到一个自包含环境中。', - 'setupWizardIntroRunning': '正在配置环境,可能需要几分钟。', - 'setupWizardConfigureApiKeys': '配置 API Key', - 'setupWizardRetry': '重新安装', - 'setupWizardBegin': '开始安装', - 'setupWizardRequirements': '需要约 500MB 存储空间和网络连接', - 'setupWizardStorageDialogTitle': '安装前授予文件访问权限', - 'setupWizardStorageDialogBody': - 'OpenClaw 在开始安装前需要文件管理权限,这样 Ubuntu 环境才能在 proot 中正确挂载共享存储。接下来会打开 Android 授权页面。', - 'setupWizardStoragePermissionRequired': '开始安装前,必须先授予文件管理权限。', - 'setupWizardOptionalPackages': '可选组件', - 'setupWizardStepDownloadRootfs': '下载 Ubuntu rootfs', - 'setupWizardStepExtractRootfs': '解压 rootfs', - 'setupWizardStepInstallNode': '安装 Node.js', - 'setupWizardStepInstallOpenClaw': '安装 OpenClaw', - 'setupWizardStepConfigureBypass': '配置 Bionic Bypass', - 'setupWizardComplete': '安装完成!', - 'setupWizardStatusSetupComplete': '安装完成', - 'setupWizardStatusSetupRequired': '需要安装环境', - 'setupWizardStatusSettingUpDirs': '正在准备目录...', - 'setupWizardStatusDownloadingUbuntuRootfs': '正在下载 Ubuntu rootfs...', - 'setupWizardStatusDownloadingProgress': '正在下载:{current} MB / {total} MB', - 'setupWizardStatusExtractingRootfs': '正在解压 rootfs(这会花一点时间)...', - 'setupWizardStatusRootfsExtracted': 'rootfs 已解压', - 'setupWizardStatusFixingPermissions': '正在修复 rootfs 权限...', - 'setupWizardStatusUpdatingPackageLists': '正在更新软件包列表...', - 'setupWizardStatusInstallingBasePackages': '正在安装基础软件包...', - 'setupWizardStatusDownloadingNode': '正在下载 Node.js {version}...', - 'setupWizardStatusDownloadingNodeProgress': - '正在下载 Node.js:{current} MB / {total} MB', - 'setupWizardStatusExtractingNode': '正在解压 Node.js...', - 'setupWizardStatusVerifyingNode': '正在验证 Node.js...', - 'setupWizardStatusNodeInstalled': 'Node.js 已安装', - 'setupWizardStatusInstallingOpenClaw': '正在安装 OpenClaw(这可能需要几分钟)...', - 'setupWizardStatusCreatingBinWrappers': '正在创建命令包装器...', - 'setupWizardStatusVerifyingOpenClaw': '正在验证 OpenClaw...', - 'setupWizardStatusOpenClawInstalled': 'OpenClaw 已安装', - 'setupWizardStatusBypassConfigured': 'Bionic Bypass 已配置', - 'setupWizardStatusReady': '安装完成,可以开始启动网关了。', - 'onboardingTitle': 'OpenClaw 引导配置', - 'onboardingStarting': '正在启动引导配置...', - 'onboardingGoToDashboard': '前往控制台', - 'onboardingStartFailed': '启动引导配置失败:{error}', - 'configureTitle': 'OpenClaw 配置', - 'configureStarting': '正在启动配置...', - 'configureStartFailed': '启动配置失败:{error}', - 'nodeTitle': '节点', - 'nodeConfigurationTitle': '节点配置', - 'nodeGatewayConnection': '网关连接', - 'nodeLocalGateway': '本地网关', - 'nodeLocalGatewaySubtitle': '自动配对本机上的网关', - 'nodeRemoteGateway': '远程网关', - 'nodeRemoteGatewaySubtitle': '连接到其他设备上的网关', - 'nodeGatewayHost': '网关主机', - 'nodeGatewayPort': '网关端口', - 'nodeGatewayToken': '网关令牌', - 'nodeGatewayTokenHint': '粘贴控制台 URL 中的令牌', - 'nodeGatewayTokenHelper': '可在控制台 URL 的 #token= 后找到', - 'nodeConnect': '连接', - 'nodePairing': '配对', - 'nodeApproveCode': '请在网关端确认此代码:', - 'nodeCapabilities': '能力', - 'nodeCapabilityCameraTitle': '相机', - 'nodeCapabilityCameraSubtitle': '拍摄照片和视频片段', - 'nodeCapabilityCanvasTitle': 'Canvas', - 'nodeCapabilityCanvasSubtitle': '浏览并交互网页', - 'nodeCapabilityLocationTitle': '定位', - 'nodeCapabilityLocationSubtitle': '获取设备 GPS 坐标', - 'nodeCapabilityScreenTitle': '录屏', - 'nodeCapabilityScreenSubtitle': '录制设备屏幕(每次都需授权)', - 'nodeCapabilityFlashlightTitle': '手电筒', - 'nodeCapabilityFlashlightSubtitle': '切换设备闪光灯开关', - 'nodeCapabilityVibrationTitle': '震动', - 'nodeCapabilityVibrationSubtitle': '触发触觉反馈和震动模式', - 'nodeCapabilitySensorsTitle': '传感器', - 'nodeCapabilitySensorsSubtitle': '读取加速度计、陀螺仪、磁力计、气压计', - 'nodeDeviceInfo': '设备信息', - 'nodeDeviceId': '设备 ID', - 'nodeLogs': '节点日志', - 'nodeNoLogs': '还没有日志', - 'nodeConnectedTo': '已连接到 {host}:{port}', - 'nodePairingCode': '配对码:', - 'nodeEnable': '启用节点', - 'nodeDisable': '禁用节点', - 'nodeReconnect': '重新连接', - 'nodeStatusPaired': '已配对', - 'nodeStatusConnecting': '连接中', - 'nodeStatusError': '错误', - 'nodeStatusDisabled': '已禁用', - 'nodeStatusDisconnected': '未连接', - 'settingsTitle': '设置', - 'settingsGeneral': '常规', - 'settingsAutoStart': '自动启动网关', - 'settingsAutoStartSubtitle': '应用打开时自动启动网关', - 'settingsBatteryOptimization': '电池优化', - 'settingsBatteryOptimized': '已优化(可能会杀死后台会话)', - 'settingsBatteryUnrestricted': '不受限制(推荐)', - 'settingsStorage': '存储访问', - 'settingsStorageGranted': '已授权,可在 proot 中访问 /sdcard', - 'settingsStorageMissing': '允许访问共享存储', - 'settingsStorageDialogTitle': '授予文件访问权限', - 'settingsStorageDialogBody': - 'OpenClaw 需要文件管理权限,才能在共享存储中读取和写入快照文件。接下来会跳转到系统设置页面。', - 'settingsStorageDialogAction': '继续', - 'onboardingStorageDialogTitle': '为引导配置授予文件访问权限', - 'onboardingStorageDialogBody': - 'OpenClaw 引导配置需要文件管理权限,Ubuntu 才能在 proot 中挂载共享存储,并完成 API Key 与绑定配置。接下来会打开 Android 授权页面。', - 'onboardingStoragePermissionRequired': '继续引导配置前,必须先授予文件管理权限。', - 'configureStorageDialogTitle': '为配置页面授予文件访问权限', - 'configureStorageDialogBody': - 'OpenClaw 配置页面需要文件管理权限,Ubuntu 才能在 proot 中挂载共享存储并管理网关设置。接下来会打开 Android 授权页面。', - 'configureStoragePermissionRequired': '继续配置前,必须先授予文件管理权限。', - 'settingsNode': '节点', - 'settingsEnableNode': '启用节点', - 'settingsEnableNodeSubtitle': '向网关提供设备能力', - 'settingsNodeConfiguration': '节点配置', - 'settingsNodeConfigurationSubtitle': '连接、配对和能力设置', - 'settingsSystemInfo': '系统信息', - 'settingsArchitecture': '架构', - 'settingsProotPath': 'PRoot 路径', - 'settingsRootfs': 'Rootfs', - 'settingsNodeJs': 'Node.js', - 'settingsOpenClaw': 'OpenClaw', - 'settingsGo': 'Go(Golang)', - 'settingsHomebrew': 'Homebrew', - 'settingsOpenSsh': 'OpenSSH', - 'settingsMaintenance': '维护', - 'settingsExportSnapshot': '导出快照', - 'settingsExportSnapshotSubtitle': '将配置备份到 Downloads', - 'settingsImportSnapshot': '导入快照', - 'settingsImportSnapshotSubtitle': '从备份恢复配置', - 'settingsRerunSetup': '重新运行安装', - 'settingsRerunSetupSubtitle': '重新安装或修复环境', - 'settingsAbout': '关于', - 'settingsAboutSubtitle': 'Android AI 网关\n版本 {version}', - 'settingsDeveloper': '开发者', - 'settingsGithub': 'GitHub', - 'settingsContact': '联系方式', - 'settingsLicense': '许可证', - 'settingsPlayStore': 'Play 商店', - 'settingsEmail': '邮箱', - 'settingsSnapshotSaved': '快照已保存到 {path}', - 'settingsExportFailed': '导出失败:{error}', - 'settingsSnapshotMissing': '在 {path} 未找到快照', - 'settingsSnapshotRestored': '快照已恢复。请重启网关以生效。', - 'settingsImportFailed': '导入失败:{error}', - 'statusInstalled': '已安装', - 'statusNotInstalled': '未安装', - }, + static bool isLocaleSupported(Locale locale) { + final localeKey = _localeToKey(locale); + if (_localizedValues.containsKey(localeKey)) { + return true; + } + return _localizedValues.containsKey(locale.languageCode); + } + + static String _localeToKey(Locale locale) { + final scriptCode = locale.scriptCode?.toLowerCase(); + final countryCode = locale.countryCode?.toUpperCase(); + + if (locale.languageCode == 'zh') { + if (scriptCode == 'hant') { + return 'zh-Hant'; + } + + if (countryCode == 'TW' || countryCode == 'HK' || countryCode == 'MO') { + return 'zh-Hant'; + } + } + + return locale.languageCode; + } + + static final Map> _localizedValues = { + 'en': appStringsEn, + 'zh': appStringsZhHans, + 'zh-Hant': appStringsZhHant, + 'ja': appStringsJa, }; } @@ -605,9 +81,7 @@ class _AppLocalizationsDelegate const _AppLocalizationsDelegate(); @override - bool isSupported(Locale locale) => AppLocalizations.supportedLocales.any( - (supported) => supported.languageCode == locale.languageCode, - ); + bool isSupported(Locale locale) => AppLocalizations.isLocaleSupported(locale); @override Future load(Locale locale) async { diff --git a/flutter_app/lib/l10n/app_strings_en.dart b/flutter_app/lib/l10n/app_strings_en.dart new file mode 100644 index 0000000..27ccc70 --- /dev/null +++ b/flutter_app/lib/l10n/app_strings_en.dart @@ -0,0 +1,297 @@ +const Map appStringsEn = { + + 'appName': 'OpenClaw', + 'language': 'Language', + 'languageSystem': 'System default', + 'languageEnglish': 'English', + 'languageChinese': 'Simplified Chinese', + 'languageTraditionalChinese': 'Traditional Chinese', + 'languageJapanese': 'Japanese', + 'commonInstalled': 'Installed', + 'commonNotInstalled': 'Not installed', + 'commonCancel': 'Cancel', + 'commonCopy': 'Copy', + 'commonCopiedToClipboard': 'Copied to clipboard', + 'commonOpen': 'Open', + 'commonPaste': 'Paste', + 'commonRetry': 'Retry', + 'commonDone': 'Done', + 'commonConfigure': 'Configure', + 'commonScreenshot': 'Screenshot', + 'commonSaveFailed': 'Failed to capture screenshot', + 'commonScreenshotSaved': 'Screenshot saved: {fileName}', + 'commonNoUrlFound': 'No URL found in selection', + 'commonOpenLink': 'Open Link', + 'commonLinkCopied': 'Link copied', + 'dashboardQuickActions': 'Quick actions', + 'dashboardTerminalTitle': 'Terminal', + 'dashboardTerminalSubtitle': 'Open Ubuntu shell with OpenClaw', + 'dashboardWebDashboardTitle': 'Web Dashboard', + 'dashboardWebDashboardSubtitle': 'Open OpenClaw dashboard in browser', + 'dashboardStartGatewayFirst': 'Start gateway first', + 'dashboardOnboardingTitle': 'Onboarding', + 'dashboardOnboardingSubtitle': 'Configure API keys and binding', + 'dashboardConfigureTitle': 'Configure', + 'dashboardConfigureSubtitle': 'Manage gateway settings', + 'dashboardProvidersTitle': 'AI Providers', + 'dashboardProvidersSubtitle': 'Configure models and API keys', + 'providersScreenTitle': 'AI Providers', + 'providersScreenActiveModel': 'Active Model', + 'providersScreenIntro': + 'Select a provider to configure its API key, endpoint, and model.', + 'providersStatusActive': 'Active', + 'providersStatusConfigured': 'Configured', + 'providerDetailApiKey': 'API Key', + 'providerDetailApiKeyEmpty': 'API key cannot be empty', + 'providerDetailEndpoint': 'API Base URL', + 'providerDetailEndpointHelper': + 'Override the default endpoint if your account uses a custom or regional API base URL.', + 'providerDetailEndpointInvalid': 'Enter a valid absolute API base URL', + 'providerDetailModel': 'Model', + 'providerDetailModelEmpty': 'Model name cannot be empty', + 'providerDetailCustomModelAction': 'Custom...', + 'providerDetailCustomModelLabel': 'Custom model name', + 'providerDetailCustomModelHint': 'e.g. meta/llama-3.3-70b-instruct', + 'providerDetailSaveAction': 'Save & Activate', + 'providerDetailSaved': '{provider} configured and activated', + 'providerDetailSaveFailed': 'Failed to save: {error}', + 'providerDetailRemoveTitle': 'Remove {provider}?', + 'providerDetailRemoveBody': + 'This will delete the API key, endpoint, and saved model for this provider.', + 'providerDetailRemoveAction': 'Remove', + 'providerDetailRemoveConfiguration': 'Remove Configuration', + 'providerDetailRemoved': '{provider} removed', + 'providerDetailRemoveFailed': 'Failed to remove: {error}', + 'providerNameAnthropic': 'Anthropic', + 'providerDescriptionAnthropic': + 'Claude models for advanced reasoning and coding', + 'providerNameOpenai': 'OpenAI', + 'providerDescriptionOpenai': 'GPT and o-series models', + 'providerNameQwen': 'Qwen', + 'providerDescriptionQwen': + 'Alibaba Cloud Qwen models via DashScope OpenAI-compatible API', + 'providerNameMinimax': 'MiniMax', + 'providerDescriptionMinimax': + 'MiniMax chat models with editable API endpoint support', + 'providerNameDoubao': 'Doubao', + 'providerDescriptionDoubao': + 'Volcengine Ark / Doubao models with official Ark endpoint presets', + 'providerNameGoogle': 'Google Gemini', + 'providerDescriptionGoogle': 'Gemini family of multimodal models', + 'providerNameOpenrouter': 'OpenRouter', + 'providerDescriptionOpenrouter': 'Unified API for hundreds of models', + 'providerNameNvidia': 'NVIDIA NIM', + 'providerDescriptionNvidia': 'GPU-optimized inference endpoints', + 'providerNameDeepseek': 'DeepSeek', + 'providerDescriptionDeepseek': 'High-performance open models', + 'providerNameXai': 'xAI', + 'providerDescriptionXai': 'Grok models from xAI', + 'dashboardPackagesTitle': 'Packages', + 'dashboardPackagesSubtitle': 'Install optional tools (Go, Homebrew, SSH)', + 'dashboardSshTitle': 'SSH Access', + 'dashboardSshSubtitle': 'Remote terminal access via SSH', + 'dashboardLogsTitle': 'Logs', + 'dashboardLogsSubtitle': 'View gateway output and errors', + 'dashboardSnapshotTitle': 'Snapshot', + 'dashboardSnapshotSubtitle': 'Backup or restore your config', + 'dashboardNodeTitle': 'Node', + 'dashboardNodeConnected': 'Connected to gateway', + 'dashboardNodeDisabled': 'Device capabilities for AI', + 'dashboardVersionLabel': 'OpenClaw v{version}', + 'dashboardAuthorLabel': 'by {author} | {org}', + 'gatewayTitle': 'Gateway', + 'gatewayCopyUrl': 'Copy URL', + 'gatewayUrlCopied': 'URL copied to clipboard', + 'gatewayOpenDashboard': 'Open dashboard', + 'gatewayStart': 'Start Gateway', + 'gatewayStop': 'Stop Gateway', + 'gatewayViewLogs': 'View Logs', + 'gatewayStatusRunning': 'Running', + 'gatewayStatusStarting': 'Starting', + 'gatewayStatusError': 'Error', + 'gatewayStatusStopped': 'Stopped', + 'logsTitle': 'Gateway Logs', + 'logsAutoScrollOn': 'Auto-scroll on', + 'logsAutoScrollOff': 'Auto-scroll off', + 'logsCopyAll': 'Copy all logs', + 'logsFilterHint': 'Filter logs...', + 'logsEmpty': 'No logs yet. Start the gateway.', + 'logsNoMatch': 'No matching logs.', + 'logsCopied': 'Logs copied to clipboard', + 'packagesTitle': 'Optional Packages', + 'packagesDescription': + 'Development tools you can install inside the Ubuntu environment.', + 'packagesInstall': 'Install', + 'packagesUninstall': 'Uninstall', + 'packagesUninstallTitle': 'Uninstall {name}?', + 'packagesUninstallDescription': + 'This will remove {name} from the environment.', + 'packageGoDescription': 'Go programming language compiler and tools', + 'packageBrewDescription': 'The missing package manager for Linux', + 'packageSshDescription': 'SSH client and server for secure remote access', + 'setupWizardTitle': 'Setup OpenClaw', + 'setupWizardIntroIdle': + 'This will download Ubuntu, Node.js, and OpenClaw into a self-contained environment.', + 'setupWizardIntroRunning': + 'Setting up the environment. This may take several minutes.', + 'setupWizardConfigureApiKeys': 'Configure API Keys', + 'setupWizardRetry': 'Retry Setup', + 'setupWizardBegin': 'Begin Setup', + 'setupWizardRequirements': + 'Requires ~500MB of storage and an internet connection', + 'setupWizardStorageDialogTitle': 'Grant file access before setup', + 'setupWizardStorageDialogBody': + 'OpenClaw setup needs file management access before installation starts, so the Ubuntu environment can mount shared storage correctly inside proot. The Android permission page will open next.', + 'setupWizardStoragePermissionRequired': + 'File management access is required before setup can start.', + 'setupWizardOptionalPackages': 'OPTIONAL PACKAGES', + 'setupWizardStepDownloadRootfs': 'Download Ubuntu rootfs', + 'setupWizardStepExtractRootfs': 'Extract rootfs', + 'setupWizardStepInstallNode': 'Install Node.js', + 'setupWizardStepInstallOpenClaw': 'Install OpenClaw', + 'setupWizardStepConfigureBypass': 'Configure Bionic Bypass', + 'setupWizardComplete': 'Setup complete!', + 'setupWizardStatusSetupComplete': 'Setup complete', + 'setupWizardStatusSetupRequired': 'Setup required', + 'setupWizardStatusSettingUpDirs': 'Setting up directories...', + 'setupWizardStatusDownloadingUbuntuRootfs': + 'Downloading Ubuntu rootfs...', + 'setupWizardStatusDownloadingProgress': + 'Downloading: {current} MB / {total} MB', + 'setupWizardStatusExtractingRootfs': + 'Extracting rootfs (this takes a while)...', + 'setupWizardStatusRootfsExtracted': 'Rootfs extracted', + 'setupWizardStatusFixingPermissions': 'Fixing rootfs permissions...', + 'setupWizardStatusUpdatingPackageLists': 'Updating package lists...', + 'setupWizardStatusInstallingBasePackages': 'Installing base packages...', + 'setupWizardStatusDownloadingNode': 'Downloading Node.js {version}...', + 'setupWizardStatusDownloadingNodeProgress': + 'Downloading Node.js: {current} MB / {total} MB', + 'setupWizardStatusExtractingNode': 'Extracting Node.js...', + 'setupWizardStatusVerifyingNode': 'Verifying Node.js...', + 'setupWizardStatusNodeInstalled': 'Node.js installed', + 'setupWizardStatusInstallingOpenClaw': + 'Installing OpenClaw (this may take a few minutes)...', + 'setupWizardStatusCreatingBinWrappers': 'Creating bin wrappers...', + 'setupWizardStatusVerifyingOpenClaw': 'Verifying OpenClaw...', + 'setupWizardStatusOpenClawInstalled': 'OpenClaw installed', + 'setupWizardStatusBypassConfigured': 'Bionic Bypass configured', + 'setupWizardStatusReady': 'Setup complete! Ready to start the gateway.', + 'onboardingTitle': 'OpenClaw Onboarding', + 'onboardingStarting': 'Starting onboarding...', + 'onboardingGoToDashboard': 'Go to Dashboard', + 'onboardingStartFailed': 'Failed to start onboarding: {error}', + 'configureTitle': 'OpenClaw Configure', + 'configureStarting': 'Starting configure...', + 'configureStartFailed': 'Failed to start configure: {error}', + 'nodeTitle': 'Node', + 'nodeConfigurationTitle': 'Node Configuration', + 'nodeGatewayConnection': 'Gateway connection', + 'nodeLocalGateway': 'Local Gateway', + 'nodeLocalGatewaySubtitle': 'Auto-pair with gateway on this device', + 'nodeRemoteGateway': 'Remote Gateway', + 'nodeRemoteGatewaySubtitle': 'Connect to a gateway on another device', + 'nodeGatewayHost': 'Gateway Host', + 'nodeGatewayPort': 'Gateway Port', + 'nodeGatewayToken': 'Gateway Token', + 'nodeGatewayTokenHint': 'Paste token from gateway dashboard URL', + 'nodeGatewayTokenHelper': 'Found in dashboard URL after #token=', + 'nodeConnect': 'Connect', + 'nodePairing': 'Pairing', + 'nodeApproveCode': 'Approve this code on the gateway:', + 'nodeCapabilities': 'Capabilities', + 'nodeCapabilityCameraTitle': 'Camera', + 'nodeCapabilityCameraSubtitle': 'Capture photos and video clips', + 'nodeCapabilityCanvasTitle': 'Canvas', + 'nodeCapabilityCanvasSubtitle': 'Navigate and interact with web pages', + 'nodeCapabilityLocationTitle': 'Location', + 'nodeCapabilityLocationSubtitle': 'Get device GPS coordinates', + 'nodeCapabilityScreenTitle': 'Screen Recording', + 'nodeCapabilityScreenSubtitle': + 'Record device screen (requires consent each time)', + 'nodeCapabilityFlashlightTitle': 'Flashlight', + 'nodeCapabilityFlashlightSubtitle': 'Toggle device torch on/off', + 'nodeCapabilityVibrationTitle': 'Vibration', + 'nodeCapabilityVibrationSubtitle': + 'Trigger haptic feedback and vibration patterns', + 'nodeCapabilitySensorsTitle': 'Sensors', + 'nodeCapabilitySensorsSubtitle': + 'Read accelerometer, gyroscope, magnetometer, barometer', + 'nodeDeviceInfo': 'Device info', + 'nodeDeviceId': 'Device ID', + 'nodeLogs': 'Node logs', + 'nodeNoLogs': 'No logs yet', + 'nodeConnectedTo': 'Connected to {host}:{port}', + 'nodePairingCode': 'Pairing code: ', + 'nodeEnable': 'Enable Node', + 'nodeDisable': 'Disable Node', + 'nodeReconnect': 'Reconnect', + 'nodeStatusPaired': 'Paired', + 'nodeStatusConnecting': 'Connecting', + 'nodeStatusError': 'Error', + 'nodeStatusDisabled': 'Disabled', + 'nodeStatusDisconnected': 'Disconnected', + 'settingsTitle': 'Settings', + 'settingsGeneral': 'General', + 'settingsAutoStart': 'Auto-start gateway', + 'settingsAutoStartSubtitle': 'Start the gateway when the app opens', + 'settingsBatteryOptimization': 'Battery Optimization', + 'settingsBatteryOptimized': 'Optimized (may kill background sessions)', + 'settingsBatteryUnrestricted': 'Unrestricted (recommended)', + 'settingsStorage': 'Setup Storage', + 'settingsStorageGranted': 'Granted — /sdcard accessible in proot', + 'settingsStorageMissing': 'Allow access to shared storage', + 'settingsStorageDialogTitle': 'Grant file access', + 'settingsStorageDialogBody': + 'OpenClaw needs file management access to read and write snapshot files in shared storage. You will be taken to the system settings page next.', + 'settingsStorageDialogAction': 'Continue', + 'onboardingStorageDialogTitle': 'Grant file access for onboarding', + 'onboardingStorageDialogBody': + 'OpenClaw onboarding needs file management access so Ubuntu can mount shared storage inside proot while you configure API keys and binding. The Android permission page will open next.', + 'onboardingStoragePermissionRequired': + 'File management access is required before onboarding can continue.', + 'configureStorageDialogTitle': 'Grant file access for configuration', + 'configureStorageDialogBody': + 'OpenClaw configuration needs file management access so Ubuntu can mount shared storage inside proot while you manage gateway settings. The Android permission page will open next.', + 'configureStoragePermissionRequired': + 'File management access is required before configuration can continue.', + 'settingsNode': 'Node', + 'settingsEnableNode': 'Enable Node', + 'settingsEnableNodeSubtitle': + 'Provide device capabilities to the gateway', + 'settingsNodeConfiguration': 'Node Configuration', + 'settingsNodeConfigurationSubtitle': + 'Connection, pairing, and capabilities', + 'settingsSystemInfo': 'System info', + 'settingsArchitecture': 'Architecture', + 'settingsProotPath': 'PRoot path', + 'settingsRootfs': 'Rootfs', + 'settingsNodeJs': 'Node.js', + 'settingsOpenClaw': 'OpenClaw', + 'settingsGo': 'Go (Golang)', + 'settingsHomebrew': 'Homebrew', + 'settingsOpenSsh': 'OpenSSH', + 'settingsMaintenance': 'Maintenance', + 'settingsExportSnapshot': 'Export Snapshot', + 'settingsExportSnapshotSubtitle': 'Backup config to Downloads', + 'settingsImportSnapshot': 'Import Snapshot', + 'settingsImportSnapshotSubtitle': 'Restore config from backup', + 'settingsRerunSetup': 'Re-run setup', + 'settingsRerunSetupSubtitle': 'Reinstall or repair the environment', + 'settingsAbout': 'About', + 'settingsAboutSubtitle': 'AI Gateway for Android\nVersion {version}', + 'settingsDeveloper': 'Developer', + 'settingsGithub': 'GitHub', + 'settingsContact': 'Contact', + 'settingsLicense': 'License', + 'settingsPlayStore': 'Play Store', + 'settingsEmail': 'Email', + 'settingsSnapshotSaved': 'Snapshot saved to {path}', + 'settingsExportFailed': 'Export failed: {error}', + 'settingsSnapshotMissing': 'No snapshot found at {path}', + 'settingsSnapshotRestored': + 'Snapshot restored successfully. Restart the gateway to apply.', + 'settingsImportFailed': 'Import failed: {error}', + 'statusInstalled': 'Installed', + 'statusNotInstalled': 'Not installed', +}; diff --git a/flutter_app/lib/l10n/app_strings_ja.dart b/flutter_app/lib/l10n/app_strings_ja.dart new file mode 100644 index 0000000..85b7744 --- /dev/null +++ b/flutter_app/lib/l10n/app_strings_ja.dart @@ -0,0 +1,266 @@ +const Map appStringsJa = { + 'appName': 'OpenClaw', + 'language': '言語', + 'languageSystem': 'システム既定', + 'languageEnglish': '英語', + 'languageChinese': '簡体字中国語', + 'languageTraditionalChinese': '繁体字中国語', + 'languageJapanese': '日本語', + 'commonInstalled': 'インストール済み', + 'commonNotInstalled': '未インストール', + 'commonCancel': 'キャンセル', + 'commonCopy': 'コピー', + 'commonCopiedToClipboard': 'クリップボードにコピーしました', + 'commonOpen': '開く', + 'commonPaste': '貼り付け', + 'commonRetry': '再試行', + 'commonDone': '完了', + 'commonConfigure': '設定', + 'commonScreenshot': 'スクリーンショット', + 'commonSaveFailed': 'スクリーンショットの保存に失敗しました', + 'commonScreenshotSaved': 'スクリーンショットを保存しました: {fileName}', + 'commonNoUrlFound': '選択範囲に URL が見つかりません', + 'commonOpenLink': 'リンクを開く', + 'commonLinkCopied': 'リンクをコピーしました', + 'dashboardQuickActions': 'クイック操作', + 'dashboardTerminalTitle': 'ターミナル', + 'dashboardTerminalSubtitle': 'Ubuntu シェルで OpenClaw を開く', + 'dashboardWebDashboardTitle': 'Web ダッシュボード', + 'dashboardWebDashboardSubtitle': 'ブラウザで OpenClaw ダッシュボードを開く', + 'dashboardStartGatewayFirst': '先にゲートウェイを起動してください', + 'dashboardOnboardingTitle': 'オンボーディング', + 'dashboardOnboardingSubtitle': 'API キーとバインドを設定', + 'dashboardConfigureTitle': '設定', + 'dashboardConfigureSubtitle': 'ゲートウェイ設定を管理', + 'dashboardProvidersTitle': 'AI プロバイダー', + 'dashboardProvidersSubtitle': 'モデルと API キーを設定', + 'providersScreenTitle': 'AI プロバイダー', + 'providersScreenActiveModel': '現在のモデル', + 'providersScreenIntro': 'プロバイダーを選択し、API キー、エンドポイント、モデルを設定してください。', + 'providersStatusActive': '有効', + 'providersStatusConfigured': '設定済み', + 'providerDetailApiKey': 'API Key', + 'providerDetailApiKeyEmpty': 'API key cannot be empty', + 'providerDetailEndpoint': 'API Base URL', + 'providerDetailEndpointHelper': 'Override the default endpoint if your account uses a custom or regional API base URL.', + 'providerDetailEndpointInvalid': 'Enter a valid absolute API base URL', + 'providerDetailModel': 'Model', + 'providerDetailModelEmpty': 'Model name cannot be empty', + 'providerDetailCustomModelAction': 'Custom...', + 'providerDetailCustomModelLabel': 'Custom model name', + 'providerDetailCustomModelHint': 'e.g. meta/llama-3.3-70b-instruct', + 'providerDetailSaveAction': 'Save & Activate', + 'providerDetailSaved': '{provider} configured and activated', + 'providerDetailSaveFailed': 'Failed to save: {error}', + 'providerDetailRemoveTitle': 'Remove {provider}?', + 'providerDetailRemoveBody': 'This will delete the API key, endpoint, and saved model for this provider.', + 'providerDetailRemoveAction': 'Remove', + 'providerDetailRemoveConfiguration': 'Remove Configuration', + 'providerDetailRemoved': '{provider} removed', + 'providerDetailRemoveFailed': 'Failed to remove: {error}', + 'providerNameAnthropic': 'Anthropic', + 'providerDescriptionAnthropic': 'Claude models for advanced reasoning and coding', + 'providerNameOpenai': 'OpenAI', + 'providerDescriptionOpenai': 'GPT and o-series models', + 'providerNameQwen': 'Qwen', + 'providerDescriptionQwen': 'Alibaba Cloud Qwen models via DashScope OpenAI-compatible API', + 'providerNameMinimax': 'MiniMax', + 'providerDescriptionMinimax': 'MiniMax chat models with editable API endpoint support', + 'providerNameDoubao': 'Doubao', + 'providerDescriptionDoubao': 'Volcengine Ark / Doubao models with official Ark endpoint presets', + 'providerNameGoogle': 'Google Gemini', + 'providerDescriptionGoogle': 'Gemini family of multimodal models', + 'providerNameOpenrouter': 'OpenRouter', + 'providerDescriptionOpenrouter': 'Unified API for hundreds of models', + 'providerNameNvidia': 'NVIDIA NIM', + 'providerDescriptionNvidia': 'GPU-optimized inference endpoints', + 'providerNameDeepseek': 'DeepSeek', + 'providerDescriptionDeepseek': 'High-performance open models', + 'providerNameXai': 'xAI', + 'providerDescriptionXai': 'Grok models from xAI', + 'dashboardPackagesTitle': 'Packages', + 'dashboardPackagesSubtitle': 'Install optional tools (Go, Homebrew, SSH)', + 'dashboardSshTitle': 'SSH Access', + 'dashboardSshSubtitle': 'Remote terminal access via SSH', + 'dashboardLogsTitle': 'Logs', + 'dashboardLogsSubtitle': 'View gateway output and errors', + 'dashboardSnapshotTitle': 'Snapshot', + 'dashboardSnapshotSubtitle': 'Backup or restore your config', + 'dashboardNodeTitle': 'Node', + 'dashboardNodeConnected': 'Connected to gateway', + 'dashboardNodeDisabled': 'Device capabilities for AI', + 'dashboardVersionLabel': 'OpenClaw v{version}', + 'dashboardAuthorLabel': 'by {author} | {org}', + 'gatewayTitle': 'ゲートウェイ', + 'gatewayCopyUrl': 'URL をコピー', + 'gatewayUrlCopied': 'URL をクリップボードにコピーしました', + 'gatewayOpenDashboard': 'ダッシュボードを開く', + 'gatewayStart': 'ゲートウェイを起動', + 'gatewayStop': 'ゲートウェイを停止', + 'gatewayViewLogs': 'ログを表示', + 'gatewayStatusRunning': '稼働中', + 'gatewayStatusStarting': '起動中', + 'gatewayStatusError': 'エラー', + 'gatewayStatusStopped': '停止', + 'logsTitle': 'ゲートウェイログ', + 'logsAutoScrollOn': '自動スクロール: ON', + 'logsAutoScrollOff': '自動スクロール: OFF', + 'logsCopyAll': 'ログをすべてコピー', + 'logsFilterHint': 'ログを絞り込み...', + 'logsEmpty': 'まだログがありません。ゲートウェイを起動してください。', + 'logsNoMatch': '一致するログがありません。', + 'logsCopied': 'ログをクリップボードにコピーしました', + 'packagesTitle': 'オプションパッケージ', + 'packagesDescription': 'Ubuntu 環境内にインストールできる開発ツールです。', + 'packagesInstall': 'インストール', + 'packagesUninstall': 'アンインストール', + 'packagesUninstallTitle': 'Uninstall {name}?', + 'packagesUninstallDescription': 'This will remove {name} from the environment.', + 'packageGoDescription': 'Go programming language compiler and tools', + 'packageBrewDescription': 'The missing package manager for Linux', + 'packageSshDescription': 'SSH client and server for secure remote access', + 'setupWizardTitle': 'OpenClaw セットアップ', + 'setupWizardIntroIdle': 'This will download Ubuntu, Node.js, and OpenClaw into a self-contained environment.', + 'setupWizardIntroRunning': 'Setting up the environment. This may take several minutes.', + 'setupWizardConfigureApiKeys': 'Configure API Keys', + 'setupWizardRetry': '再試行', + 'setupWizardBegin': 'セットアップ開始', + 'setupWizardRequirements': 'Requires ~500MB of storage and an internet connection', + 'setupWizardStorageDialogTitle': 'Grant file access before setup', + 'setupWizardStorageDialogBody': 'OpenClaw setup needs file management access before installation starts, so the Ubuntu environment can mount shared storage correctly inside proot. The Android permission page will open next.', + 'setupWizardStoragePermissionRequired': 'File management access is required before setup can start.', + 'setupWizardOptionalPackages': 'OPTIONAL PACKAGES', + 'setupWizardStepDownloadRootfs': 'Download Ubuntu rootfs', + 'setupWizardStepExtractRootfs': 'Extract rootfs', + 'setupWizardStepInstallNode': 'Install Node.js', + 'setupWizardStepInstallOpenClaw': 'Install OpenClaw', + 'setupWizardStepConfigureBypass': 'Configure Bionic Bypass', + 'setupWizardComplete': 'セットアップ完了!', + 'setupWizardStatusSetupComplete': 'Setup complete', + 'setupWizardStatusSetupRequired': 'Setup required', + 'setupWizardStatusSettingUpDirs': 'Setting up directories...', + 'setupWizardStatusDownloadingUbuntuRootfs': 'Downloading Ubuntu rootfs...', + 'setupWizardStatusDownloadingProgress': 'Downloading: {current} MB / {total} MB', + 'setupWizardStatusExtractingRootfs': 'Extracting rootfs (this takes a while)...', + 'setupWizardStatusRootfsExtracted': 'Rootfs extracted', + 'setupWizardStatusFixingPermissions': 'Fixing rootfs permissions...', + 'setupWizardStatusUpdatingPackageLists': 'Updating package lists...', + 'setupWizardStatusInstallingBasePackages': 'Installing base packages...', + 'setupWizardStatusDownloadingNode': 'Downloading Node.js {version}...', + 'setupWizardStatusDownloadingNodeProgress': 'Downloading Node.js: {current} MB / {total} MB', + 'setupWizardStatusExtractingNode': 'Extracting Node.js...', + 'setupWizardStatusVerifyingNode': 'Verifying Node.js...', + 'setupWizardStatusNodeInstalled': 'Node.js installed', + 'setupWizardStatusInstallingOpenClaw': 'Installing OpenClaw (this may take a few minutes)...', + 'setupWizardStatusCreatingBinWrappers': 'Creating bin wrappers...', + 'setupWizardStatusVerifyingOpenClaw': 'Verifying OpenClaw...', + 'setupWizardStatusOpenClawInstalled': 'OpenClaw installed', + 'setupWizardStatusBypassConfigured': 'Bionic Bypass configured', + 'setupWizardStatusReady': 'Setup complete! Ready to start the gateway.', + 'onboardingTitle': 'OpenClaw オンボーディング', + 'onboardingStarting': 'Starting onboarding...', + 'onboardingGoToDashboard': 'ダッシュボードへ', + 'onboardingStartFailed': 'Failed to start onboarding: {error}', + 'configureTitle': 'OpenClaw 設定', + 'configureStarting': 'Starting configure...', + 'configureStartFailed': 'Failed to start configure: {error}', + 'nodeTitle': 'ノード', + 'nodeConfigurationTitle': 'Node Configuration', + 'nodeGatewayConnection': 'Gateway connection', + 'nodeLocalGateway': 'Local Gateway', + 'nodeLocalGatewaySubtitle': 'Auto-pair with gateway on this device', + 'nodeRemoteGateway': 'Remote Gateway', + 'nodeRemoteGatewaySubtitle': 'Connect to a gateway on another device', + 'nodeGatewayHost': 'Gateway Host', + 'nodeGatewayPort': 'Gateway Port', + 'nodeGatewayToken': 'Gateway Token', + 'nodeGatewayTokenHint': 'Paste token from gateway dashboard URL', + 'nodeGatewayTokenHelper': 'Found in dashboard URL after #token=', + 'nodeConnect': 'Connect', + 'nodePairing': 'Pairing', + 'nodeApproveCode': 'Approve this code on the gateway:', + 'nodeCapabilities': 'Capabilities', + 'nodeCapabilityCameraTitle': 'Camera', + 'nodeCapabilityCameraSubtitle': 'Capture photos and video clips', + 'nodeCapabilityCanvasTitle': 'Canvas', + 'nodeCapabilityCanvasSubtitle': 'Navigate and interact with web pages', + 'nodeCapabilityLocationTitle': 'Location', + 'nodeCapabilityLocationSubtitle': 'Get device GPS coordinates', + 'nodeCapabilityScreenTitle': 'Screen Recording', + 'nodeCapabilityScreenSubtitle': 'Record device screen (requires consent each time)', + 'nodeCapabilityFlashlightTitle': 'Flashlight', + 'nodeCapabilityFlashlightSubtitle': 'Toggle device torch on/off', + 'nodeCapabilityVibrationTitle': 'Vibration', + 'nodeCapabilityVibrationSubtitle': 'Trigger haptic feedback and vibration patterns', + 'nodeCapabilitySensorsTitle': 'Sensors', + 'nodeCapabilitySensorsSubtitle': 'Read accelerometer, gyroscope, magnetometer, barometer', + 'nodeDeviceInfo': 'Device info', + 'nodeDeviceId': 'Device ID', + 'nodeLogs': 'Node logs', + 'nodeNoLogs': 'No logs yet', + 'nodeConnectedTo': 'Connected to {host}:{port}', + 'nodePairingCode': 'Pairing code: ', + 'nodeEnable': 'Enable Node', + 'nodeDisable': 'Disable Node', + 'nodeReconnect': 'Reconnect', + 'nodeStatusPaired': 'Paired', + 'nodeStatusConnecting': 'Connecting', + 'nodeStatusError': 'Error', + 'nodeStatusDisabled': 'Disabled', + 'nodeStatusDisconnected': 'Disconnected', + 'settingsTitle': '設定', + 'settingsGeneral': '一般', + 'settingsAutoStart': 'ゲートウェイを自動起動', + 'settingsAutoStartSubtitle': 'アプリ起動時にゲートウェイを開始', + 'settingsBatteryOptimization': 'バッテリー最適化', + 'settingsBatteryOptimized': 'Optimized (may kill background sessions)', + 'settingsBatteryUnrestricted': 'Unrestricted (recommended)', + 'settingsStorage': 'ストレージ設定', + 'settingsStorageGranted': 'Granted — /sdcard accessible in proot', + 'settingsStorageMissing': 'Allow access to shared storage', + 'settingsStorageDialogTitle': 'Grant file access', + 'settingsStorageDialogBody': 'OpenClaw needs file management access to read and write snapshot files in shared storage. You will be taken to the system settings page next.', + 'settingsStorageDialogAction': 'Continue', + 'onboardingStorageDialogTitle': 'Grant file access for onboarding', + 'onboardingStorageDialogBody': 'OpenClaw onboarding needs file management access so Ubuntu can mount shared storage inside proot while you configure API keys and binding. The Android permission page will open next.', + 'onboardingStoragePermissionRequired': 'File management access is required before onboarding can continue.', + 'configureStorageDialogTitle': 'Grant file access for configuration', + 'configureStorageDialogBody': 'OpenClaw configuration needs file management access so Ubuntu can mount shared storage inside proot while you manage gateway settings. The Android permission page will open next.', + 'configureStoragePermissionRequired': 'File management access is required before configuration can continue.', + 'settingsNode': 'ノード', + 'settingsEnableNode': 'Enable Node', + 'settingsEnableNodeSubtitle': 'Provide device capabilities to the gateway', + 'settingsNodeConfiguration': 'Node Configuration', + 'settingsNodeConfigurationSubtitle': 'Connection, pairing, and capabilities', + 'settingsSystemInfo': 'システム情報', + 'settingsArchitecture': 'Architecture', + 'settingsProotPath': 'PRoot path', + 'settingsRootfs': 'Rootfs', + 'settingsNodeJs': 'Node.js', + 'settingsOpenClaw': 'OpenClaw', + 'settingsGo': 'Go (Golang)', + 'settingsHomebrew': 'Homebrew', + 'settingsOpenSsh': 'OpenSSH', + 'settingsMaintenance': 'メンテナンス', + 'settingsExportSnapshot': 'Export Snapshot', + 'settingsExportSnapshotSubtitle': 'Backup config to Downloads', + 'settingsImportSnapshot': 'Import Snapshot', + 'settingsImportSnapshotSubtitle': 'Restore config from backup', + 'settingsRerunSetup': 'Re-run setup', + 'settingsRerunSetupSubtitle': 'Reinstall or repair the environment', + 'settingsAbout': '情報', + 'settingsAboutSubtitle': 'AI Gateway for Android\nVersion {version}', + 'settingsDeveloper': '開発者', + 'settingsGithub': 'GitHub', + 'settingsContact': '連絡先', + 'settingsLicense': 'ライセンス', + 'settingsPlayStore': 'Play ストア', + 'settingsEmail': 'メール', + 'settingsSnapshotSaved': 'Snapshot saved to {path}', + 'settingsExportFailed': 'Export failed: {error}', + 'settingsSnapshotMissing': 'No snapshot found at {path}', + 'settingsSnapshotRestored': 'Snapshot restored successfully. Restart the gateway to apply.', + 'settingsImportFailed': 'Import failed: {error}', + 'statusInstalled': 'インストール済み', + 'statusNotInstalled': '未インストール', +}; diff --git a/flutter_app/lib/l10n/app_strings_zh_hans.dart b/flutter_app/lib/l10n/app_strings_zh_hans.dart new file mode 100644 index 0000000..8b9b2bc --- /dev/null +++ b/flutter_app/lib/l10n/app_strings_zh_hans.dart @@ -0,0 +1,272 @@ +const Map appStringsZhHans = { + + 'appName': 'OpenClaw', + 'language': '语言', + 'languageSystem': '跟随系统', + 'languageEnglish': '英语', + 'languageChinese': '简体中文', + 'languageTraditionalChinese': '繁体中文', + 'languageJapanese': '日语', + 'commonInstalled': '已安装', + 'commonNotInstalled': '未安装', + 'commonCancel': '取消', + 'commonCopy': '复制', + 'commonCopiedToClipboard': '已复制到剪贴板', + 'commonOpen': '打开', + 'commonPaste': '粘贴', + 'commonRetry': '重试', + 'commonDone': '完成', + 'commonConfigure': '配置', + 'commonScreenshot': '截图', + 'commonSaveFailed': '截图失败', + 'commonScreenshotSaved': '截图已保存:{fileName}', + 'commonNoUrlFound': '所选内容中未找到 URL', + 'commonOpenLink': '打开链接', + 'commonLinkCopied': '链接已复制', + 'dashboardQuickActions': '快捷操作', + 'dashboardTerminalTitle': '终端', + 'dashboardTerminalSubtitle': '打开 Ubuntu Shell 并使用 OpenClaw', + 'dashboardWebDashboardTitle': 'Web 控制台', + 'dashboardWebDashboardSubtitle': '在浏览器中打开 OpenClaw 控制台', + 'dashboardStartGatewayFirst': '请先启动网关', + 'dashboardOnboardingTitle': '引导配置', + 'dashboardOnboardingSubtitle': '配置 API Key 和绑定信息', + 'dashboardConfigureTitle': '网关配置', + 'dashboardConfigureSubtitle': '管理网关设置', + 'dashboardProvidersTitle': 'AI 提供商', + 'dashboardProvidersSubtitle': '配置模型和 API Key', + 'providersScreenTitle': 'AI 提供商', + 'providersScreenActiveModel': '当前激活模型', + 'providersScreenIntro': '选择一个提供商,配置它的 API Key、端点和模型。', + 'providersStatusActive': '已激活', + 'providersStatusConfigured': '已配置', + 'providerDetailApiKey': 'API Key', + 'providerDetailApiKeyEmpty': 'API Key 不能为空', + 'providerDetailEndpoint': 'API 基础地址', + 'providerDetailEndpointHelper': '如果你的账号使用自定义或区域专属端点,可以在这里覆盖默认地址。', + 'providerDetailEndpointInvalid': '请输入有效的绝对 API 地址', + 'providerDetailModel': '模型', + 'providerDetailModelEmpty': '模型名称不能为空', + 'providerDetailCustomModelAction': '自定义...', + 'providerDetailCustomModelLabel': '自定义模型名', + 'providerDetailCustomModelHint': '例如:meta/llama-3.3-70b-instruct', + 'providerDetailSaveAction': '保存并激活', + 'providerDetailSaved': '已配置并激活 {provider}', + 'providerDetailSaveFailed': '保存失败:{error}', + 'providerDetailRemoveTitle': '移除 {provider}?', + 'providerDetailRemoveBody': '这会删除该提供商保存的 API Key、端点和模型。', + 'providerDetailRemoveAction': '移除', + 'providerDetailRemoveConfiguration': '移除配置', + 'providerDetailRemoved': '已移除 {provider}', + 'providerDetailRemoveFailed': '移除失败:{error}', + 'providerNameAnthropic': 'Anthropic', + 'providerDescriptionAnthropic': 'Claude 系列模型,适合复杂推理与编程', + 'providerNameOpenai': 'OpenAI', + 'providerDescriptionOpenai': 'GPT 与 o 系列模型', + 'providerNameQwen': '通义千问', + 'providerDescriptionQwen': '通过 DashScope OpenAI 兼容接口接入千问模型', + 'providerNameMinimax': 'MiniMax', + 'providerDescriptionMinimax': 'MiniMax 对话模型,支持自定义 API 端点', + 'providerNameDoubao': '豆包', + 'providerDescriptionDoubao': '火山方舟 / 豆包模型,内置官方 Ark 端点预设', + 'providerNameGoogle': 'Google Gemini', + 'providerDescriptionGoogle': 'Gemini 多模态模型家族', + 'providerNameOpenrouter': 'OpenRouter', + 'providerDescriptionOpenrouter': '统一接入数百种模型的 API', + 'providerNameNvidia': 'NVIDIA NIM', + 'providerDescriptionNvidia': '面向 GPU 推理的优化端点', + 'providerNameDeepseek': 'DeepSeek', + 'providerDescriptionDeepseek': '高性能开源模型服务', + 'providerNameXai': 'xAI', + 'providerDescriptionXai': 'xAI 的 Grok 系列模型', + 'dashboardPackagesTitle': '可选组件', + 'dashboardPackagesSubtitle': '安装可选工具(Go、Homebrew、SSH)', + 'dashboardSshTitle': 'SSH 访问', + 'dashboardSshSubtitle': '通过 SSH 远程访问终端', + 'dashboardLogsTitle': '日志', + 'dashboardLogsSubtitle': '查看网关输出和错误', + 'dashboardSnapshotTitle': '快照', + 'dashboardSnapshotSubtitle': '备份或恢复你的配置', + 'dashboardNodeTitle': '节点', + 'dashboardNodeConnected': '已连接到网关', + 'dashboardNodeDisabled': '为 AI 提供设备能力', + 'dashboardVersionLabel': 'OpenClaw v{version}', + 'dashboardAuthorLabel': '作者 {author} | {org}', + 'gatewayTitle': '网关', + 'gatewayCopyUrl': '复制 URL', + 'gatewayUrlCopied': 'URL 已复制到剪贴板', + 'gatewayOpenDashboard': '打开控制台', + 'gatewayStart': '启动网关', + 'gatewayStop': '停止网关', + 'gatewayViewLogs': '查看日志', + 'gatewayStatusRunning': '运行中', + 'gatewayStatusStarting': '启动中', + 'gatewayStatusError': '错误', + 'gatewayStatusStopped': '已停止', + 'logsTitle': '网关日志', + 'logsAutoScrollOn': '自动滚动已开启', + 'logsAutoScrollOff': '自动滚动已关闭', + 'logsCopyAll': '复制全部日志', + 'logsFilterHint': '筛选日志...', + 'logsEmpty': '还没有日志。请先启动网关。', + 'logsNoMatch': '没有匹配的日志。', + 'logsCopied': '日志已复制到剪贴板', + 'packagesTitle': '可选组件', + 'packagesDescription': '可在 Ubuntu 环境内安装的开发工具。', + 'packagesInstall': '安装', + 'packagesUninstall': '卸载', + 'packagesUninstallTitle': '卸载 {name}?', + 'packagesUninstallDescription': '这会将 {name} 从环境中移除。', + 'packageGoDescription': 'Go 编程语言编译器和工具链', + 'packageBrewDescription': 'Linux 上常用的缺省包管理器', + 'packageSshDescription': '用于安全远程访问的 SSH 客户端和服务端', + 'setupWizardTitle': '开始配置 OpenClaw', + 'setupWizardIntroIdle': '这会将 Ubuntu、Node.js 和 OpenClaw 下载到一个自包含环境中。', + 'setupWizardIntroRunning': '正在配置环境,可能需要几分钟。', + 'setupWizardConfigureApiKeys': '配置 API Key', + 'setupWizardRetry': '重新安装', + 'setupWizardBegin': '开始安装', + 'setupWizardRequirements': '需要约 500MB 存储空间和网络连接', + 'setupWizardStorageDialogTitle': '安装前授予文件访问权限', + 'setupWizardStorageDialogBody': + 'OpenClaw 在开始安装前需要文件管理权限,这样 Ubuntu 环境才能在 proot 中正确挂载共享存储。接下来会打开 Android 授权页面。', + 'setupWizardStoragePermissionRequired': '开始安装前,必须先授予文件管理权限。', + 'setupWizardOptionalPackages': '可选组件', + 'setupWizardStepDownloadRootfs': '下载 Ubuntu rootfs', + 'setupWizardStepExtractRootfs': '解压 rootfs', + 'setupWizardStepInstallNode': '安装 Node.js', + 'setupWizardStepInstallOpenClaw': '安装 OpenClaw', + 'setupWizardStepConfigureBypass': '配置 Bionic Bypass', + 'setupWizardComplete': '安装完成!', + 'setupWizardStatusSetupComplete': '安装完成', + 'setupWizardStatusSetupRequired': '需要安装环境', + 'setupWizardStatusSettingUpDirs': '正在准备目录...', + 'setupWizardStatusDownloadingUbuntuRootfs': '正在下载 Ubuntu rootfs...', + 'setupWizardStatusDownloadingProgress': '正在下载:{current} MB / {total} MB', + 'setupWizardStatusExtractingRootfs': '正在解压 rootfs(这会花一点时间)...', + 'setupWizardStatusRootfsExtracted': 'rootfs 已解压', + 'setupWizardStatusFixingPermissions': '正在修复 rootfs 权限...', + 'setupWizardStatusUpdatingPackageLists': '正在更新软件包列表...', + 'setupWizardStatusInstallingBasePackages': '正在安装基础软件包...', + 'setupWizardStatusDownloadingNode': '正在下载 Node.js {version}...', + 'setupWizardStatusDownloadingNodeProgress': + '正在下载 Node.js:{current} MB / {total} MB', + 'setupWizardStatusExtractingNode': '正在解压 Node.js...', + 'setupWizardStatusVerifyingNode': '正在验证 Node.js...', + 'setupWizardStatusNodeInstalled': 'Node.js 已安装', + 'setupWizardStatusInstallingOpenClaw': '正在安装 OpenClaw(这可能需要几分钟)...', + 'setupWizardStatusCreatingBinWrappers': '正在创建命令包装器...', + 'setupWizardStatusVerifyingOpenClaw': '正在验证 OpenClaw...', + 'setupWizardStatusOpenClawInstalled': 'OpenClaw 已安装', + 'setupWizardStatusBypassConfigured': 'Bionic Bypass 已配置', + 'setupWizardStatusReady': '安装完成,可以开始启动网关了。', + 'onboardingTitle': 'OpenClaw 引导配置', + 'onboardingStarting': '正在启动引导配置...', + 'onboardingGoToDashboard': '前往控制台', + 'onboardingStartFailed': '启动引导配置失败:{error}', + 'configureTitle': 'OpenClaw 配置', + 'configureStarting': '正在启动配置...', + 'configureStartFailed': '启动配置失败:{error}', + 'nodeTitle': '节点', + 'nodeConfigurationTitle': '节点配置', + 'nodeGatewayConnection': '网关连接', + 'nodeLocalGateway': '本地网关', + 'nodeLocalGatewaySubtitle': '自动配对本机上的网关', + 'nodeRemoteGateway': '远程网关', + 'nodeRemoteGatewaySubtitle': '连接到其他设备上的网关', + 'nodeGatewayHost': '网关主机', + 'nodeGatewayPort': '网关端口', + 'nodeGatewayToken': '网关令牌', + 'nodeGatewayTokenHint': '粘贴控制台 URL 中的令牌', + 'nodeGatewayTokenHelper': '可在控制台 URL 的 #token= 后找到', + 'nodeConnect': '连接', + 'nodePairing': '配对', + 'nodeApproveCode': '请在网关端确认此代码:', + 'nodeCapabilities': '能力', + 'nodeCapabilityCameraTitle': '相机', + 'nodeCapabilityCameraSubtitle': '拍摄照片和视频片段', + 'nodeCapabilityCanvasTitle': 'Canvas', + 'nodeCapabilityCanvasSubtitle': '浏览并交互网页', + 'nodeCapabilityLocationTitle': '定位', + 'nodeCapabilityLocationSubtitle': '获取设备 GPS 坐标', + 'nodeCapabilityScreenTitle': '录屏', + 'nodeCapabilityScreenSubtitle': '录制设备屏幕(每次都需授权)', + 'nodeCapabilityFlashlightTitle': '手电筒', + 'nodeCapabilityFlashlightSubtitle': '切换设备闪光灯开关', + 'nodeCapabilityVibrationTitle': '震动', + 'nodeCapabilityVibrationSubtitle': '触发触觉反馈和震动模式', + 'nodeCapabilitySensorsTitle': '传感器', + 'nodeCapabilitySensorsSubtitle': '读取加速度计、陀螺仪、磁力计、气压计', + 'nodeDeviceInfo': '设备信息', + 'nodeDeviceId': '设备 ID', + 'nodeLogs': '节点日志', + 'nodeNoLogs': '还没有日志', + 'nodeConnectedTo': '已连接到 {host}:{port}', + 'nodePairingCode': '配对码:', + 'nodeEnable': '启用节点', + 'nodeDisable': '禁用节点', + 'nodeReconnect': '重新连接', + 'nodeStatusPaired': '已配对', + 'nodeStatusConnecting': '连接中', + 'nodeStatusError': '错误', + 'nodeStatusDisabled': '已禁用', + 'nodeStatusDisconnected': '未连接', + 'settingsTitle': '设置', + 'settingsGeneral': '常规', + 'settingsAutoStart': '自动启动网关', + 'settingsAutoStartSubtitle': '应用打开时自动启动网关', + 'settingsBatteryOptimization': '电池优化', + 'settingsBatteryOptimized': '已优化(可能会杀死后台会话)', + 'settingsBatteryUnrestricted': '不受限制(推荐)', + 'settingsStorage': '存储访问', + 'settingsStorageGranted': '已授权,可在 proot 中访问 /sdcard', + 'settingsStorageMissing': '允许访问共享存储', + 'settingsStorageDialogTitle': '授予文件访问权限', + 'settingsStorageDialogBody': + 'OpenClaw 需要文件管理权限,才能在共享存储中读取和写入快照文件。接下来会跳转到系统设置页面。', + 'settingsStorageDialogAction': '继续', + 'onboardingStorageDialogTitle': '为引导配置授予文件访问权限', + 'onboardingStorageDialogBody': + 'OpenClaw 引导配置需要文件管理权限,Ubuntu 才能在 proot 中挂载共享存储,并完成 API Key 与绑定配置。接下来会打开 Android 授权页面。', + 'onboardingStoragePermissionRequired': '继续引导配置前,必须先授予文件管理权限。', + 'configureStorageDialogTitle': '为配置页面授予文件访问权限', + 'configureStorageDialogBody': + 'OpenClaw 配置页面需要文件管理权限,Ubuntu 才能在 proot 中挂载共享存储并管理网关设置。接下来会打开 Android 授权页面。', + 'configureStoragePermissionRequired': '继续配置前,必须先授予文件管理权限。', + 'settingsNode': '节点', + 'settingsEnableNode': '启用节点', + 'settingsEnableNodeSubtitle': '向网关提供设备能力', + 'settingsNodeConfiguration': '节点配置', + 'settingsNodeConfigurationSubtitle': '连接、配对和能力设置', + 'settingsSystemInfo': '系统信息', + 'settingsArchitecture': '架构', + 'settingsProotPath': 'PRoot 路径', + 'settingsRootfs': 'Rootfs', + 'settingsNodeJs': 'Node.js', + 'settingsOpenClaw': 'OpenClaw', + 'settingsGo': 'Go(Golang)', + 'settingsHomebrew': 'Homebrew', + 'settingsOpenSsh': 'OpenSSH', + 'settingsMaintenance': '维护', + 'settingsExportSnapshot': '导出快照', + 'settingsExportSnapshotSubtitle': '将配置备份到 Downloads', + 'settingsImportSnapshot': '导入快照', + 'settingsImportSnapshotSubtitle': '从备份恢复配置', + 'settingsRerunSetup': '重新运行安装', + 'settingsRerunSetupSubtitle': '重新安装或修复环境', + 'settingsAbout': '关于', + 'settingsAboutSubtitle': 'Android AI 网关\n版本 {version}', + 'settingsDeveloper': '开发者', + 'settingsGithub': 'GitHub', + 'settingsContact': '联系方式', + 'settingsLicense': '许可证', + 'settingsPlayStore': 'Play 商店', + 'settingsEmail': '邮箱', + 'settingsSnapshotSaved': '快照已保存到 {path}', + 'settingsExportFailed': '导出失败:{error}', + 'settingsSnapshotMissing': '在 {path} 未找到快照', + 'settingsSnapshotRestored': '快照已恢复。请重启网关以生效。', + 'settingsImportFailed': '导入失败:{error}', + 'statusInstalled': '已安装', + 'statusNotInstalled': '未安装', +}; diff --git a/flutter_app/lib/l10n/app_strings_zh_hant.dart b/flutter_app/lib/l10n/app_strings_zh_hant.dart new file mode 100644 index 0000000..7deabe7 --- /dev/null +++ b/flutter_app/lib/l10n/app_strings_zh_hant.dart @@ -0,0 +1,266 @@ +const Map appStringsZhHant = { + 'appName': 'OpenClaw', + 'language': '語言', + 'languageSystem': '跟隨系統', + 'languageEnglish': '英語', + 'languageChinese': '簡體中文', + 'languageTraditionalChinese': '繁體中文', + 'languageJapanese': '日語', + 'commonInstalled': '已安裝', + 'commonNotInstalled': '未安裝', + 'commonCancel': '取消', + 'commonCopy': '複製', + 'commonCopiedToClipboard': '已複製到剪貼簿', + 'commonOpen': '打開', + 'commonPaste': '貼上', + 'commonRetry': '重試', + 'commonDone': '完成', + 'commonConfigure': '設定', + 'commonScreenshot': '截圖', + 'commonSaveFailed': '截圖失敗', + 'commonScreenshotSaved': '截圖已儲存:{fileName}', + 'commonNoUrlFound': '所選內容中未找到 URL', + 'commonOpenLink': '打開連結', + 'commonLinkCopied': '連結已複製', + 'dashboardQuickActions': '快捷操作', + 'dashboardTerminalTitle': '終端', + 'dashboardTerminalSubtitle': '打開 Ubuntu Shell 並使用 OpenClaw', + 'dashboardWebDashboardTitle': 'Web 控制台', + 'dashboardWebDashboardSubtitle': '在瀏覽器中打開 OpenClaw 控制台', + 'dashboardStartGatewayFirst': '請先啟動網關', + 'dashboardOnboardingTitle': '引導設定', + 'dashboardOnboardingSubtitle': '配置 API Key 和綁定資訊', + 'dashboardConfigureTitle': '網關設定', + 'dashboardConfigureSubtitle': '管理網關設定', + 'dashboardProvidersTitle': 'AI 供應商', + 'dashboardProvidersSubtitle': '配置模型和 API Key', + 'providersScreenTitle': 'AI 供應商', + 'providersScreenActiveModel': '目前啟用模型', + 'providersScreenIntro': '選擇一個提供商,配置它的 API Key、端點和模型。', + 'providersStatusActive': '已啟用', + 'providersStatusConfigured': '已設定', + 'providerDetailApiKey': 'API Key', + 'providerDetailApiKeyEmpty': 'API Key 不能為空', + 'providerDetailEndpoint': 'API 基礎地址', + 'providerDetailEndpointHelper': '如果你的帳號使用自定義或區域專屬端點,可以在這裡覆蓋預設地址。', + 'providerDetailEndpointInvalid': '請輸入有效的絕對 API 地址', + 'providerDetailModel': '模型', + 'providerDetailModelEmpty': '模型名稱不能為空', + 'providerDetailCustomModelAction': '自定義...', + 'providerDetailCustomModelLabel': '自定義模型名', + 'providerDetailCustomModelHint': '例如:meta/llama-3.3-70b-instruct', + 'providerDetailSaveAction': '儲存並啟用', + 'providerDetailSaved': '已配置並啟用 {provider}', + 'providerDetailSaveFailed': '儲存失敗:{error}', + 'providerDetailRemoveTitle': '移除 {provider}?', + 'providerDetailRemoveBody': '這會刪除該提供商儲存的 API Key、端點和模型。', + 'providerDetailRemoveAction': '移除', + 'providerDetailRemoveConfiguration': '移除配置', + 'providerDetailRemoved': '已移除 {provider}', + 'providerDetailRemoveFailed': '移除失敗:{error}', + 'providerNameAnthropic': 'Anthropic', + 'providerDescriptionAnthropic': 'Claude 系列模型,適合複雜推理與編程', + 'providerNameOpenai': 'OpenAI', + 'providerDescriptionOpenai': 'GPT 與 o 系列模型', + 'providerNameQwen': '通義千問', + 'providerDescriptionQwen': '通過 DashScope OpenAI 相容接口接入千問模型', + 'providerNameMinimax': 'MiniMax', + 'providerDescriptionMinimax': 'MiniMax 對話模型,支援自定義 API 端點', + 'providerNameDoubao': '豆包', + 'providerDescriptionDoubao': '火山方舟 / 豆包模型,內建官方 Ark 端點預設', + 'providerNameGoogle': 'Google Gemini', + 'providerDescriptionGoogle': 'Gemini 多模態模型家族', + 'providerNameOpenrouter': 'OpenRouter', + 'providerDescriptionOpenrouter': '統一接入數百種模型的 API', + 'providerNameNvidia': 'NVIDIA NIM', + 'providerDescriptionNvidia': '面向 GPU 推理的優化端點', + 'providerNameDeepseek': 'DeepSeek', + 'providerDescriptionDeepseek': '高性能開源模型服務', + 'providerNameXai': 'xAI', + 'providerDescriptionXai': 'xAI 的 Grok 系列模型', + 'dashboardPackagesTitle': '可選組件', + 'dashboardPackagesSubtitle': '安裝可選工具(Go、Homebrew、SSH)', + 'dashboardSshTitle': 'SSH 存取', + 'dashboardSshSubtitle': '通過 SSH 遠程存取終端', + 'dashboardLogsTitle': '日誌', + 'dashboardLogsSubtitle': '查看網關輸出和錯誤', + 'dashboardSnapshotTitle': '快照', + 'dashboardSnapshotSubtitle': '備份或恢復你的配置', + 'dashboardNodeTitle': '節點', + 'dashboardNodeConnected': '已連接到網關', + 'dashboardNodeDisabled': '為 AI 提供設備能力', + 'dashboardVersionLabel': 'OpenClaw v{version}', + 'dashboardAuthorLabel': '作者 {author} | {org}', + 'gatewayTitle': '網關', + 'gatewayCopyUrl': '複製 URL', + 'gatewayUrlCopied': 'URL 已複製到剪貼簿', + 'gatewayOpenDashboard': '打開控制台', + 'gatewayStart': '啟動網關', + 'gatewayStop': '停止網關', + 'gatewayViewLogs': '查看日誌', + 'gatewayStatusRunning': '運行中', + 'gatewayStatusStarting': '啟動中', + 'gatewayStatusError': '錯誤', + 'gatewayStatusStopped': '已停止', + 'logsTitle': '網關日誌', + 'logsAutoScrollOn': '自動滾動已開啟', + 'logsAutoScrollOff': '自動滾動已關閉', + 'logsCopyAll': '複製全部日誌', + 'logsFilterHint': '篩選日誌...', + 'logsEmpty': '尚無日誌。請先啟動網關。', + 'logsNoMatch': '沒有匹配的日誌。', + 'logsCopied': '日誌已複製到剪貼簿', + 'packagesTitle': '可選元件', + 'packagesDescription': '可在 Ubuntu 環境內安裝的開發工具。', + 'packagesInstall': '安裝', + 'packagesUninstall': '解除安裝', + 'packagesUninstallTitle': '卸載 {name}?', + 'packagesUninstallDescription': '這會將 {name} 從環境中移除。', + 'packageGoDescription': 'Go 編程語言編譯器和工具鏈', + 'packageBrewDescription': 'Linux 上常用的缺省包管理器', + 'packageSshDescription': '用於安全遠程存取的 SSH 客戶端和服務端', + 'setupWizardTitle': '開始配置 OpenClaw', + 'setupWizardIntroIdle': '這會將 Ubuntu、Node.js 和 OpenClaw 下載到一個自包含環境中。', + 'setupWizardIntroRunning': '正在配置環境,可能需要幾分鐘。', + 'setupWizardConfigureApiKeys': '配置 API Key', + 'setupWizardRetry': '重新安裝', + 'setupWizardBegin': '開始安裝', + 'setupWizardRequirements': '需要約 500MB 存儲空間和網絡連接', + 'setupWizardStorageDialogTitle': '安裝前授予檔案存取權限', + 'setupWizardStorageDialogBody': 'OpenClaw 在開始安裝前需要檔案管理權限,這樣 Ubuntu 環境才能在 proot 中正確掛載共享存儲。接下來會打開 Android 授權頁面。', + 'setupWizardStoragePermissionRequired': '開始安裝前,必須先授予檔案管理權限。', + 'setupWizardOptionalPackages': '可選組件', + 'setupWizardStepDownloadRootfs': '下載 Ubuntu rootfs', + 'setupWizardStepExtractRootfs': '解壓 rootfs', + 'setupWizardStepInstallNode': '安裝 Node.js', + 'setupWizardStepInstallOpenClaw': '安裝 OpenClaw', + 'setupWizardStepConfigureBypass': '配置 Bionic Bypass', + 'setupWizardComplete': '安裝完成!', + 'setupWizardStatusSetupComplete': '安裝完成', + 'setupWizardStatusSetupRequired': '需要安裝環境', + 'setupWizardStatusSettingUpDirs': '正在準備目錄...', + 'setupWizardStatusDownloadingUbuntuRootfs': '正在下載 Ubuntu rootfs...', + 'setupWizardStatusDownloadingProgress': '正在下載:{current} MB / {total} MB', + 'setupWizardStatusExtractingRootfs': '正在解壓 rootfs(這會花一點時間)...', + 'setupWizardStatusRootfsExtracted': 'rootfs 已解壓', + 'setupWizardStatusFixingPermissions': '正在修複 rootfs 權限...', + 'setupWizardStatusUpdatingPackageLists': '正在更新軟體包列表...', + 'setupWizardStatusInstallingBasePackages': '正在安裝基礎軟體包...', + 'setupWizardStatusDownloadingNode': '正在下載 Node.js {version}...', + 'setupWizardStatusDownloadingNodeProgress': '正在下載 Node.js:{current} MB / {total} MB', + 'setupWizardStatusExtractingNode': '正在解壓 Node.js...', + 'setupWizardStatusVerifyingNode': '正在驗證 Node.js...', + 'setupWizardStatusNodeInstalled': 'Node.js 已安裝', + 'setupWizardStatusInstallingOpenClaw': '正在安裝 OpenClaw(這可能需要幾分鐘)...', + 'setupWizardStatusCreatingBinWrappers': '正在建立命令包裝器...', + 'setupWizardStatusVerifyingOpenClaw': '正在驗證 OpenClaw...', + 'setupWizardStatusOpenClawInstalled': 'OpenClaw 已安裝', + 'setupWizardStatusBypassConfigured': 'Bionic Bypass 已配置', + 'setupWizardStatusReady': '安裝完成,可以開始啟動網關了。', + 'onboardingTitle': 'OpenClaw 引導配置', + 'onboardingStarting': '正在啟動引導配置...', + 'onboardingGoToDashboard': '前往控制台', + 'onboardingStartFailed': '啟動引導配置失敗:{error}', + 'configureTitle': 'OpenClaw 配置', + 'configureStarting': '正在啟動配置...', + 'configureStartFailed': '啟動配置失敗:{error}', + 'nodeTitle': '節點', + 'nodeConfigurationTitle': '節點配置', + 'nodeGatewayConnection': '網關連接', + 'nodeLocalGateway': '本地網關', + 'nodeLocalGatewaySubtitle': '自動配對本機上的網關', + 'nodeRemoteGateway': '遠程網關', + 'nodeRemoteGatewaySubtitle': '連接到其他設備上的網關', + 'nodeGatewayHost': '網關主機', + 'nodeGatewayPort': '網關端口', + 'nodeGatewayToken': '網關令牌', + 'nodeGatewayTokenHint': '貼上控制台 URL 中的令牌', + 'nodeGatewayTokenHelper': '可在控制台 URL 的 #token= 後找到', + 'nodeConnect': '連接', + 'nodePairing': '配對', + 'nodeApproveCode': '請在網關端確認此代碼:', + 'nodeCapabilities': '能力', + 'nodeCapabilityCameraTitle': '相機', + 'nodeCapabilityCameraSubtitle': '拍攝照片和影片片段', + 'nodeCapabilityCanvasTitle': 'Canvas', + 'nodeCapabilityCanvasSubtitle': '瀏覽並互動網頁', + 'nodeCapabilityLocationTitle': '定位', + 'nodeCapabilityLocationSubtitle': '獲取設備 GPS 坐標', + 'nodeCapabilityScreenTitle': '錄屏', + 'nodeCapabilityScreenSubtitle': '錄制設備螢幕(每次都需授權)', + 'nodeCapabilityFlashlightTitle': '手電筒', + 'nodeCapabilityFlashlightSubtitle': '切換設備閃光燈開關', + 'nodeCapabilityVibrationTitle': '震動', + 'nodeCapabilityVibrationSubtitle': '觸發觸覺回饋和震動模式', + 'nodeCapabilitySensorsTitle': '感測器', + 'nodeCapabilitySensorsSubtitle': '讀取加速度計、陀螺儀、磁力計、氣壓計', + 'nodeDeviceInfo': '設備資訊', + 'nodeDeviceId': '設備 ID', + 'nodeLogs': '節點日誌', + 'nodeNoLogs': '還沒有日誌', + 'nodeConnectedTo': '已連接到 {host}:{port}', + 'nodePairingCode': '配對碼:', + 'nodeEnable': '啟用節點', + 'nodeDisable': '禁用節點', + 'nodeReconnect': '重新連接', + 'nodeStatusPaired': '已配對', + 'nodeStatusConnecting': '連接中', + 'nodeStatusError': '錯誤', + 'nodeStatusDisabled': '已禁用', + 'nodeStatusDisconnected': '未連接', + 'settingsTitle': '設定', + 'settingsGeneral': '一般', + 'settingsAutoStart': '自動啟動網關', + 'settingsAutoStartSubtitle': '應用打開時自動啟動網關', + 'settingsBatteryOptimization': '電池最佳化', + 'settingsBatteryOptimized': '已優化(可能會終止後台會話)', + 'settingsBatteryUnrestricted': '不受限制(推薦)', + 'settingsStorage': '儲存存取', + 'settingsStorageGranted': '已授權,可在 proot 中存取 /sdcard', + 'settingsStorageMissing': '允許存取共享存儲', + 'settingsStorageDialogTitle': '授予檔案存取權限', + 'settingsStorageDialogBody': 'OpenClaw 需要檔案管理權限,才能在共享存儲中讀取和寫入快照檔案。接下來會跳轉到系統設定頁面。', + 'settingsStorageDialogAction': '繼續', + 'onboardingStorageDialogTitle': '為引導配置授予檔案存取權限', + 'onboardingStorageDialogBody': 'OpenClaw 引導配置需要檔案管理權限,Ubuntu 才能在 proot 中掛載共享存儲,並完成 API Key 與綁定配置。接下來會打開 Android 授權頁面。', + 'onboardingStoragePermissionRequired': '繼續引導配置前,必須先授予檔案管理權限。', + 'configureStorageDialogTitle': '為配置頁面授予檔案存取權限', + 'configureStorageDialogBody': 'OpenClaw 配置頁面需要檔案管理權限,Ubuntu 才能在 proot 中掛載共享存儲並管理網關設定。接下來會打開 Android 授權頁面。', + 'configureStoragePermissionRequired': '繼續配置前,必須先授予檔案管理權限。', + 'settingsNode': '節點', + 'settingsEnableNode': '啟用節點', + 'settingsEnableNodeSubtitle': '向網關提供設備能力', + 'settingsNodeConfiguration': '節點配置', + 'settingsNodeConfigurationSubtitle': '連接、配對和能力設定', + 'settingsSystemInfo': '系統資訊', + 'settingsArchitecture': '架構', + 'settingsProotPath': 'PRoot 路徑', + 'settingsRootfs': 'Rootfs', + 'settingsNodeJs': 'Node.js', + 'settingsOpenClaw': 'OpenClaw', + 'settingsGo': 'Go(Golang)', + 'settingsHomebrew': 'Homebrew', + 'settingsOpenSsh': 'OpenSSH', + 'settingsMaintenance': '維護', + 'settingsExportSnapshot': '匯出快照', + 'settingsExportSnapshotSubtitle': '將配置備份到 Downloads', + 'settingsImportSnapshot': '匯入快照', + 'settingsImportSnapshotSubtitle': '從備份恢復配置', + 'settingsRerunSetup': '重新執行安裝', + 'settingsRerunSetupSubtitle': '重新安裝或修複環境', + 'settingsAbout': '關於', + 'settingsAboutSubtitle': 'Android AI 網關\n版本 {version}', + 'settingsDeveloper': '開發者', + 'settingsGithub': 'GitHub', + 'settingsContact': '聯絡方式', + 'settingsLicense': '授權', + 'settingsPlayStore': 'Play 商店', + 'settingsEmail': '電子郵件', + 'settingsSnapshotSaved': '快照已儲存到 {path}', + 'settingsExportFailed': '匯出失敗:{error}', + 'settingsSnapshotMissing': '在 {path} 未找到快照', + 'settingsSnapshotRestored': '快照已恢復。請重啟網關以生效。', + 'settingsImportFailed': '匯入失敗:{error}', + 'statusInstalled': '已安裝', + 'statusNotInstalled': '未安裝', +}; diff --git a/flutter_app/lib/providers/locale_provider.dart b/flutter_app/lib/providers/locale_provider.dart index 5547085..b2bf74b 100644 --- a/flutter_app/lib/providers/locale_provider.dart +++ b/flutter_app/lib/providers/locale_provider.dart @@ -9,7 +9,19 @@ class LocaleProvider extends ChangeNotifier { bool _initialized = false; Locale? get locale => _locale; - String get localeCode => _locale?.languageCode ?? 'system'; + String get localeCode { + final value = _locale; + if (value == null) { + return 'system'; + } + + if (value.languageCode == 'zh' && + value.scriptCode?.toLowerCase() == 'hant') { + return 'zh-Hant'; + } + + return value.languageCode; + } Future load() async { if (_initialized) return; @@ -36,6 +48,12 @@ class LocaleProvider extends ChangeNotifier { return const Locale('en'); case 'zh': return const Locale('zh'); + case 'zh-Hant': + case 'zh_TW': + case 'zh-HK': + return const Locale.fromSubtags(languageCode: 'zh', scriptCode: 'Hant'); + case 'ja': + return const Locale('ja'); default: return null; } diff --git a/flutter_app/lib/screens/settings_screen.dart b/flutter_app/lib/screens/settings_screen.dart index 8c63451..b735e79 100644 --- a/flutter_app/lib/screens/settings_screen.dart +++ b/flutter_app/lib/screens/settings_screen.dart @@ -120,6 +120,14 @@ class _SettingsScreenState extends State { value: 'zh', child: Text(l10n.t('languageChinese')), ), + DropdownMenuItem( + value: 'zh-Hant', + child: Text(l10n.t('languageTraditionalChinese')), + ), + DropdownMenuItem( + value: 'ja', + child: Text(l10n.t('languageJapanese')), + ), ], onChanged: (value) { if (value != null) { diff --git a/flutter_app/scripts/_expand_l10n.dart b/flutter_app/scripts/_expand_l10n.dart new file mode 100644 index 0000000..e1cb961 --- /dev/null +++ b/flutter_app/scripts/_expand_l10n.dart @@ -0,0 +1,61 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:openclaw/l10n/app_strings_en.dart'; +import 'package:openclaw/l10n/app_strings_ja.dart'; +import 'package:openclaw/l10n/app_strings_zh_hans.dart'; +import 'package:openclaw/l10n/app_strings_zh_hant.dart'; + +String _escape(String value) { + return value + .replaceAll('\\', '\\\\') + .replaceAll("'", "\\'") + .replaceAll('\n', '\\n'); +} + +String _buildMapFile({ + required String variableName, + required Map base, + required Map localized, +}) { + final buffer = StringBuffer(); + buffer.writeln('const Map $variableName = {'); + + final seen = {}; + for (final key in base.keys) { + final value = localized[key] ?? base[key] ?? key; + buffer.writeln(" '$key': '${_escape(value)}',"); + seen.add(key); + } + + for (final entry in localized.entries) { + if (seen.contains(entry.key)) continue; + buffer.writeln(" '${entry.key}': '${_escape(entry.value)}',"); + } + + buffer.writeln('};'); + return buffer.toString(); +} + +void main() { + final jaContent = _buildMapFile( + variableName: 'appStringsJa', + base: appStringsEn, + localized: appStringsJa, + ); + + final zhHantContent = _buildMapFile( + variableName: 'appStringsZhHant', + base: appStringsZhHans, + localized: appStringsZhHant, + ); + + File('lib/l10n/app_strings_ja.dart').writeAsStringSync( + jaContent, + encoding: utf8, + ); + File('lib/l10n/app_strings_zh_hant.dart').writeAsStringSync( + zhHantContent, + encoding: utf8, + ); +} From d17812d232d90937359b7e743bc7f20884cb9b43 Mon Sep 17 00:00:00 2001 From: TIANLI0 <507249007@qq.com> Date: Sun, 8 Mar 2026 23:57:28 +0800 Subject: [PATCH 4/9] chore(i18n): remove non-translation and authorization changes from translation branch --- flutter_app/.flutter-plugins-dependencies | 1 - flutter_app/android/app/build.gradle | 40 +- .../plugins/GeneratedPluginRegistrant.java | 74 -- flutter_app/android/build.gradle | 2 +- flutter_app/android/gradle.properties | 1 - .../android/gradle/wrapper/gradle-wrapper.jar | Bin 53636 -> 0 bytes .../gradle/wrapper/gradle-wrapper.properties | 2 +- flutter_app/android/gradlew | 160 --- flutter_app/android/gradlew.bat | 90 -- flutter_app/android/settings.gradle | 4 +- flutter_app/lib/screens/configure_screen.dart | 17 - .../lib/screens/onboarding_screen.dart | 17 - .../lib/screens/setup_wizard_screen.dart | 21 - .../capabilities/location_capability.dart | 9 +- .../lib/services/provider_config_service.dart | 23 +- .../services/storage_permission_service.dart | 46 - flutter_app/pubspec.lock | 1015 ----------------- flutter_app/pubspec.yaml | 10 +- scripts/fetch-proot-binaries.ps1 | 143 --- 19 files changed, 23 insertions(+), 1652 deletions(-) delete mode 100644 flutter_app/.flutter-plugins-dependencies delete mode 100644 flutter_app/android/app/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java delete mode 100644 flutter_app/android/gradle/wrapper/gradle-wrapper.jar delete mode 100644 flutter_app/android/gradlew delete mode 100644 flutter_app/android/gradlew.bat delete mode 100644 flutter_app/lib/services/storage_permission_service.dart delete mode 100644 flutter_app/pubspec.lock delete mode 100644 scripts/fetch-proot-binaries.ps1 diff --git a/flutter_app/.flutter-plugins-dependencies b/flutter_app/.flutter-plugins-dependencies deleted file mode 100644 index d506c63..0000000 --- a/flutter_app/.flutter-plugins-dependencies +++ /dev/null @@ -1 +0,0 @@ -{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"camera_avfoundation","path":"C:\\\\Users\\\\tian5\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\camera_avfoundation-0.10.1\\\\","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"flutter_blue_plus_darwin","path":"C:\\\\Users\\\\tian5\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\flutter_blue_plus_darwin-7.0.3\\\\","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false},{"name":"flutter_pty","path":"C:\\\\Users\\\\tian5\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\flutter_pty-0.4.2\\\\","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"geolocator_apple","path":"C:\\\\Users\\\\tian5\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\geolocator_apple-2.3.13\\\\","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false},{"name":"package_info_plus","path":"C:\\\\Users\\\\tian5\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\package_info_plus-9.0.0\\\\","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"path_provider_foundation","path":"C:\\\\Users\\\\tian5\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\path_provider_foundation-2.6.0\\\\","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"permission_handler_apple","path":"C:\\\\Users\\\\tian5\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\permission_handler_apple-9.4.7\\\\","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"shared_preferences_foundation","path":"C:\\\\Users\\\\tian5\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\shared_preferences_foundation-2.5.6\\\\","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false},{"name":"url_launcher_ios","path":"C:\\\\Users\\\\tian5\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\url_launcher_ios-6.4.1\\\\","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"webview_flutter_wkwebview","path":"C:\\\\Users\\\\tian5\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\webview_flutter_wkwebview-3.23.8\\\\","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false}],"android":[{"name":"camera_android_camerax","path":"C:\\\\Users\\\\tian5\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\camera_android_camerax-0.7.1\\\\","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"flutter_blue_plus_android","path":"C:\\\\Users\\\\tian5\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\flutter_blue_plus_android-7.0.4\\\\","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"flutter_plugin_android_lifecycle","path":"C:\\\\Users\\\\tian5\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\flutter_plugin_android_lifecycle-2.0.33\\\\","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"flutter_pty","path":"C:\\\\Users\\\\tian5\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\flutter_pty-0.4.2\\\\","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"geolocator_android","path":"C:\\\\Users\\\\tian5\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\geolocator_android-5.0.2\\\\","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"package_info_plus","path":"C:\\\\Users\\\\tian5\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\package_info_plus-9.0.0\\\\","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"path_provider_android","path":"C:\\\\Users\\\\tian5\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\path_provider_android-2.2.22\\\\","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"permission_handler_android","path":"C:\\\\Users\\\\tian5\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\permission_handler_android-13.0.1\\\\","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"shared_preferences_android","path":"C:\\\\Users\\\\tian5\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\shared_preferences_android-2.4.21\\\\","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"url_launcher_android","path":"C:\\\\Users\\\\tian5\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\url_launcher_android-6.3.28\\\\","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"usb_serial","path":"C:\\\\Users\\\\tian5\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\usb_serial-0.5.2\\\\","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"webview_flutter_android","path":"C:\\\\Users\\\\tian5\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\webview_flutter_android-4.10.13\\\\","native_build":true,"dependencies":[],"dev_dependency":false}],"macos":[{"name":"flutter_blue_plus_darwin","path":"C:\\\\Users\\\\tian5\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\flutter_blue_plus_darwin-7.0.3\\\\","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false},{"name":"flutter_pty","path":"C:\\\\Users\\\\tian5\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\flutter_pty-0.4.2\\\\","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"geolocator_apple","path":"C:\\\\Users\\\\tian5\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\geolocator_apple-2.3.13\\\\","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false},{"name":"package_info_plus","path":"C:\\\\Users\\\\tian5\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\package_info_plus-9.0.0\\\\","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"path_provider_foundation","path":"C:\\\\Users\\\\tian5\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\path_provider_foundation-2.6.0\\\\","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"shared_preferences_foundation","path":"C:\\\\Users\\\\tian5\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\shared_preferences_foundation-2.5.6\\\\","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false},{"name":"url_launcher_macos","path":"C:\\\\Users\\\\tian5\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\url_launcher_macos-3.2.5\\\\","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"webview_flutter_wkwebview","path":"C:\\\\Users\\\\tian5\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\webview_flutter_wkwebview-3.23.8\\\\","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false}],"linux":[{"name":"flutter_blue_plus_linux","path":"C:\\\\Users\\\\tian5\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\flutter_blue_plus_linux-7.0.3\\\\","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"flutter_pty","path":"C:\\\\Users\\\\tian5\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\flutter_pty-0.4.2\\\\","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"geolocator_linux","path":"C:\\\\Users\\\\tian5\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\geolocator_linux-0.2.4\\\\","native_build":false,"dependencies":["package_info_plus"],"dev_dependency":false},{"name":"package_info_plus","path":"C:\\\\Users\\\\tian5\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\package_info_plus-9.0.0\\\\","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"path_provider_linux","path":"C:\\\\Users\\\\tian5\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\path_provider_linux-2.2.1\\\\","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"shared_preferences_linux","path":"C:\\\\Users\\\\tian5\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\shared_preferences_linux-2.4.1\\\\","native_build":false,"dependencies":["path_provider_linux"],"dev_dependency":false},{"name":"url_launcher_linux","path":"C:\\\\Users\\\\tian5\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\url_launcher_linux-3.2.2\\\\","native_build":true,"dependencies":[],"dev_dependency":false}],"windows":[{"name":"flutter_pty","path":"C:\\\\Users\\\\tian5\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\flutter_pty-0.4.2\\\\","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"geolocator_windows","path":"C:\\\\Users\\\\tian5\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\geolocator_windows-0.2.5\\\\","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"package_info_plus","path":"C:\\\\Users\\\\tian5\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\package_info_plus-9.0.0\\\\","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"path_provider_windows","path":"C:\\\\Users\\\\tian5\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\path_provider_windows-2.3.0\\\\","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"permission_handler_windows","path":"C:\\\\Users\\\\tian5\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\permission_handler_windows-0.2.1\\\\","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"shared_preferences_windows","path":"C:\\\\Users\\\\tian5\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\shared_preferences_windows-2.4.1\\\\","native_build":false,"dependencies":["path_provider_windows"],"dev_dependency":false},{"name":"url_launcher_windows","path":"C:\\\\Users\\\\tian5\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\url_launcher_windows-3.1.5\\\\","native_build":true,"dependencies":[],"dev_dependency":false}],"web":[{"name":"camera_web","path":"C:\\\\Users\\\\tian5\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\camera_web-0.3.5+3\\\\","dependencies":[],"dev_dependency":false},{"name":"flutter_blue_plus_web","path":"C:\\\\Users\\\\tian5\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\flutter_blue_plus_web-7.0.2\\\\","dependencies":[],"dev_dependency":false},{"name":"geolocator_web","path":"C:\\\\Users\\\\tian5\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\geolocator_web-4.1.3\\\\","dependencies":[],"dev_dependency":false},{"name":"package_info_plus","path":"C:\\\\Users\\\\tian5\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\package_info_plus-9.0.0\\\\","dependencies":[],"dev_dependency":false},{"name":"permission_handler_html","path":"C:\\\\Users\\\\tian5\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\permission_handler_html-0.1.3+5\\\\","dependencies":[],"dev_dependency":false},{"name":"shared_preferences_web","path":"C:\\\\Users\\\\tian5\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\shared_preferences_web-2.4.3\\\\","dependencies":[],"dev_dependency":false},{"name":"url_launcher_web","path":"C:\\\\Users\\\\tian5\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\url_launcher_web-2.4.2\\\\","dependencies":[],"dev_dependency":false}]},"dependencyGraph":[{"name":"camera","dependencies":["camera_android_camerax","camera_avfoundation","camera_web","flutter_plugin_android_lifecycle"]},{"name":"camera_android_camerax","dependencies":[]},{"name":"camera_avfoundation","dependencies":[]},{"name":"camera_web","dependencies":[]},{"name":"flutter_blue_plus","dependencies":["flutter_blue_plus_android","flutter_blue_plus_darwin","flutter_blue_plus_linux","flutter_blue_plus_web"]},{"name":"flutter_blue_plus_android","dependencies":[]},{"name":"flutter_blue_plus_darwin","dependencies":[]},{"name":"flutter_blue_plus_linux","dependencies":[]},{"name":"flutter_blue_plus_web","dependencies":[]},{"name":"flutter_plugin_android_lifecycle","dependencies":[]},{"name":"flutter_pty","dependencies":[]},{"name":"geolocator","dependencies":["geolocator_android","geolocator_apple","geolocator_web","geolocator_windows","geolocator_linux"]},{"name":"geolocator_android","dependencies":[]},{"name":"geolocator_apple","dependencies":[]},{"name":"geolocator_linux","dependencies":["package_info_plus"]},{"name":"geolocator_web","dependencies":[]},{"name":"geolocator_windows","dependencies":[]},{"name":"package_info_plus","dependencies":[]},{"name":"path_provider","dependencies":["path_provider_android","path_provider_foundation","path_provider_linux","path_provider_windows"]},{"name":"path_provider_android","dependencies":[]},{"name":"path_provider_foundation","dependencies":[]},{"name":"path_provider_linux","dependencies":[]},{"name":"path_provider_windows","dependencies":[]},{"name":"permission_handler","dependencies":["permission_handler_android","permission_handler_apple","permission_handler_html","permission_handler_windows"]},{"name":"permission_handler_android","dependencies":[]},{"name":"permission_handler_apple","dependencies":[]},{"name":"permission_handler_html","dependencies":[]},{"name":"permission_handler_windows","dependencies":[]},{"name":"shared_preferences","dependencies":["shared_preferences_android","shared_preferences_foundation","shared_preferences_linux","shared_preferences_web","shared_preferences_windows"]},{"name":"shared_preferences_android","dependencies":[]},{"name":"shared_preferences_foundation","dependencies":[]},{"name":"shared_preferences_linux","dependencies":["path_provider_linux"]},{"name":"shared_preferences_web","dependencies":[]},{"name":"shared_preferences_windows","dependencies":["path_provider_windows"]},{"name":"url_launcher","dependencies":["url_launcher_android","url_launcher_ios","url_launcher_linux","url_launcher_macos","url_launcher_web","url_launcher_windows"]},{"name":"url_launcher_android","dependencies":[]},{"name":"url_launcher_ios","dependencies":[]},{"name":"url_launcher_linux","dependencies":[]},{"name":"url_launcher_macos","dependencies":[]},{"name":"url_launcher_web","dependencies":[]},{"name":"url_launcher_windows","dependencies":[]},{"name":"usb_serial","dependencies":[]},{"name":"webview_flutter","dependencies":["webview_flutter_android","webview_flutter_wkwebview"]},{"name":"webview_flutter_android","dependencies":[]},{"name":"webview_flutter_wkwebview","dependencies":[]}],"date_created":"2026-03-08 23:35:29.580932","version":"3.41.4","swift_package_manager_enabled":{"ios":false,"macos":false}} \ No newline at end of file diff --git a/flutter_app/android/app/build.gradle b/flutter_app/android/app/build.gradle index 7fac76f..fd928c9 100644 --- a/flutter_app/android/app/build.gradle +++ b/flutter_app/android/app/build.gradle @@ -33,50 +33,18 @@ if (keystorePropertiesFile.exists()) { def hasKeystore = keystoreProperties.containsKey('storeFile') && file(keystoreProperties['storeFile']).exists() -def requiredProotLibs = [ - 'arm64-v8a/libproot.so', - 'arm64-v8a/libprootloader.so', - 'arm64-v8a/libtalloc.so', - 'armeabi-v7a/libproot.so', - 'armeabi-v7a/libprootloader.so', - 'armeabi-v7a/libtalloc.so', - 'x86_64/libproot.so', - 'x86_64/libprootloader.so', - 'x86_64/libtalloc.so', -] - -tasks.register('verifyProotBinaries') { - doLast { - def baseDir = file('src/main/jniLibs') - def missing = requiredProotLibs.findAll { relativePath -> - !new File(baseDir, relativePath).exists() - } - - if (!missing.isEmpty()) { - throw new GradleException( - "Missing PRoot native binaries: ${missing.join(', ')}. " + - "Run scripts/fetch-proot-binaries.sh on Unix or scripts/fetch-proot-binaries.ps1 on Windows before building." - ) - } - } -} - -tasks.named('preBuild') { - dependsOn 'verifyProotBinaries' -} - android { namespace = "com.nxg.openclawproot" - compileSdk = 36 + compileSdk = 35 ndkVersion = flutter.ndkVersion compileOptions { - sourceCompatibility = JavaVersion.VERSION_17 - targetCompatibility = JavaVersion.VERSION_17 + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 } kotlinOptions { - jvmTarget = "17" + jvmTarget = JavaVersion.VERSION_1_8 } defaultConfig { diff --git a/flutter_app/android/app/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java b/flutter_app/android/app/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java deleted file mode 100644 index 57d527c..0000000 --- a/flutter_app/android/app/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java +++ /dev/null @@ -1,74 +0,0 @@ -package io.flutter.plugins; - -import androidx.annotation.Keep; -import androidx.annotation.NonNull; -import io.flutter.Log; - -import io.flutter.embedding.engine.FlutterEngine; - -/** - * Generated file. Do not edit. - * This file is generated by the Flutter tool based on the - * plugins that support the Android platform. - */ -@Keep -public final class GeneratedPluginRegistrant { - private static final String TAG = "GeneratedPluginRegistrant"; - public static void registerWith(@NonNull FlutterEngine flutterEngine) { - try { - flutterEngine.getPlugins().add(new io.flutter.plugins.camerax.CameraAndroidCameraxPlugin()); - } catch (Exception e) { - Log.e(TAG, "Error registering plugin camera_android_camerax, io.flutter.plugins.camerax.CameraAndroidCameraxPlugin", e); - } - try { - flutterEngine.getPlugins().add(new com.lib.flutter_blue_plus.FlutterBluePlusPlugin()); - } catch (Exception e) { - Log.e(TAG, "Error registering plugin flutter_blue_plus_android, com.lib.flutter_blue_plus.FlutterBluePlusPlugin", e); - } - try { - flutterEngine.getPlugins().add(new io.flutter.plugins.flutter_plugin_android_lifecycle.FlutterAndroidLifecyclePlugin()); - } catch (Exception e) { - Log.e(TAG, "Error registering plugin flutter_plugin_android_lifecycle, io.flutter.plugins.flutter_plugin_android_lifecycle.FlutterAndroidLifecyclePlugin", e); - } - try { - flutterEngine.getPlugins().add(new com.baseflow.geolocator.GeolocatorPlugin()); - } catch (Exception e) { - Log.e(TAG, "Error registering plugin geolocator_android, com.baseflow.geolocator.GeolocatorPlugin", e); - } - try { - flutterEngine.getPlugins().add(new dev.fluttercommunity.plus.packageinfo.PackageInfoPlugin()); - } catch (Exception e) { - Log.e(TAG, "Error registering plugin package_info_plus, dev.fluttercommunity.plus.packageinfo.PackageInfoPlugin", e); - } - try { - flutterEngine.getPlugins().add(new io.flutter.plugins.pathprovider.PathProviderPlugin()); - } catch (Exception e) { - Log.e(TAG, "Error registering plugin path_provider_android, io.flutter.plugins.pathprovider.PathProviderPlugin", e); - } - try { - flutterEngine.getPlugins().add(new com.baseflow.permissionhandler.PermissionHandlerPlugin()); - } catch (Exception e) { - Log.e(TAG, "Error registering plugin permission_handler_android, com.baseflow.permissionhandler.PermissionHandlerPlugin", e); - } - try { - flutterEngine.getPlugins().add(new io.flutter.plugins.sharedpreferences.SharedPreferencesPlugin()); - } catch (Exception e) { - Log.e(TAG, "Error registering plugin shared_preferences_android, io.flutter.plugins.sharedpreferences.SharedPreferencesPlugin", e); - } - try { - flutterEngine.getPlugins().add(new io.flutter.plugins.urllauncher.UrlLauncherPlugin()); - } catch (Exception e) { - Log.e(TAG, "Error registering plugin url_launcher_android, io.flutter.plugins.urllauncher.UrlLauncherPlugin", e); - } - try { - flutterEngine.getPlugins().add(new dev.bessems.usbserial.UsbSerialPlugin()); - } catch (Exception e) { - Log.e(TAG, "Error registering plugin usb_serial, dev.bessems.usbserial.UsbSerialPlugin", e); - } - try { - flutterEngine.getPlugins().add(new io.flutter.plugins.webviewflutter.WebViewFlutterPlugin()); - } catch (Exception e) { - Log.e(TAG, "Error registering plugin webview_flutter_android, io.flutter.plugins.webviewflutter.WebViewFlutterPlugin", e); - } - } -} diff --git a/flutter_app/android/build.gradle b/flutter_app/android/build.gradle index ecd72c5..dfd81c4 100644 --- a/flutter_app/android/build.gradle +++ b/flutter_app/android/build.gradle @@ -13,7 +13,7 @@ subprojects { // references flutter.compileSdkVersion before the Flutter tooling injects it. if (project.name != "app") { project.ext.set("flutter", [ - compileSdkVersion: 36, + compileSdkVersion: 35, minSdkVersion: 29, targetSdkVersion: 36 ]) diff --git a/flutter_app/android/gradle.properties b/flutter_app/android/gradle.properties index 86c765c..2597170 100644 --- a/flutter_app/android/gradle.properties +++ b/flutter_app/android/gradle.properties @@ -1,4 +1,3 @@ org.gradle.jvmargs=-Xmx4G -XX:MaxMetaspaceSize=2G -XX:+HeapDumpOnOutOfMemoryError android.useAndroidX=true android.enableJetifier=true -kotlin.incremental=false diff --git a/flutter_app/android/gradle/wrapper/gradle-wrapper.jar b/flutter_app/android/gradle/wrapper/gradle-wrapper.jar deleted file mode 100644 index 13372aef5e24af05341d49695ee84e5f9b594659..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 53636 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ⅅ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 H-+%o-(+5aQ diff --git a/flutter_app/android/gradle/wrapper/gradle-wrapper.properties b/flutter_app/android/gradle/wrapper/gradle-wrapper.properties index efdcc4a..7bb2df6 100644 --- a/flutter_app/android/gradle/wrapper/gradle-wrapper.properties +++ b/flutter_app/android/gradle/wrapper/gradle-wrapper.properties @@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-all.zip diff --git a/flutter_app/android/gradlew b/flutter_app/android/gradlew deleted file mode 100644 index 9d82f78..0000000 --- a/flutter_app/android/gradlew +++ /dev/null @@ -1,160 +0,0 @@ -#!/usr/bin/env bash - -############################################################################## -## -## Gradle start up script for UN*X -## -############################################################################## - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS="" - -APP_NAME="Gradle" -APP_BASE_NAME=`basename "$0"` - -# Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD="maximum" - -warn ( ) { - echo "$*" -} - -die ( ) { - echo - echo "$*" - echo - exit 1 -} - -# OS specific support (must be 'true' or 'false'). -cygwin=false -msys=false -darwin=false -case "`uname`" in - CYGWIN* ) - cygwin=true - ;; - Darwin* ) - darwin=true - ;; - MINGW* ) - msys=true - ;; -esac - -# Attempt to set APP_HOME -# Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi -done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null - -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar - -# Determine the Java command to use to start the JVM. -if [ -n "$JAVA_HOME" ] ; then - if [ -x "$JAVA_HOME/jre/sh/java" ] ; then - # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" - else - JAVACMD="$JAVA_HOME/bin/java" - fi - if [ ! -x "$JAVACMD" ] ; then - die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." - fi -else - JAVACMD="java" - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." -fi - -# Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then - MAX_FD_LIMIT=`ulimit -H -n` - if [ $? -eq 0 ] ; then - if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then - MAX_FD="$MAX_FD_LIMIT" - fi - ulimit -n $MAX_FD - if [ $? -ne 0 ] ; then - warn "Could not set maximum file descriptor limit: $MAX_FD" - fi - else - warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" - fi -fi - -# For Darwin, add options to specify how the application appears in the dock -if $darwin; then - GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" -fi - -# For Cygwin, switch paths to Windows format before running java -if $cygwin ; then - APP_HOME=`cygpath --path --mixed "$APP_HOME"` - CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - JAVACMD=`cygpath --unix "$JAVACMD"` - - # We build the pattern for arguments to be converted via cygpath - ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` - SEP="" - for dir in $ROOTDIRSRAW ; do - ROOTDIRS="$ROOTDIRS$SEP$dir" - SEP="|" - done - OURCYGPATTERN="(^($ROOTDIRS))" - # Add a user-defined pattern to the cygpath arguments - if [ "$GRADLE_CYGPATTERN" != "" ] ; then - OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" - fi - # Now convert the arguments - kludge to limit ourselves to /bin/sh - i=0 - for arg in "$@" ; do - CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` - CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option - - if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition - eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` - else - eval `echo args$i`="\"$arg\"" - fi - i=$((i+1)) - done - case $i in - (0) set -- ;; - (1) set -- "$args0" ;; - (2) set -- "$args0" "$args1" ;; - (3) set -- "$args0" "$args1" "$args2" ;; - (4) set -- "$args0" "$args1" "$args2" "$args3" ;; - (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; - esac -fi - -# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules -function splitJvmOpts() { - JVM_OPTS=("$@") -} -eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS -JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" - -exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" diff --git a/flutter_app/android/gradlew.bat b/flutter_app/android/gradlew.bat deleted file mode 100644 index 8a0b282..0000000 --- a/flutter_app/android/gradlew.bat +++ /dev/null @@ -1,90 +0,0 @@ -@if "%DEBUG%" == "" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS= - -set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto init - -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto init - -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:init -@rem Get command-line arguments, handling Windowz variants - -if not "%OS%" == "Windows_NT" goto win9xME_args -if "%@eval[2+2]" == "4" goto 4NT_args - -:win9xME_args -@rem Slurp the command line arguments. -set CMD_LINE_ARGS= -set _SKIP=2 - -:win9xME_args_slurp -if "x%~1" == "x" goto execute - -set CMD_LINE_ARGS=%* -goto execute - -:4NT_args -@rem Get arguments from the 4NT Shell from JP Software -set CMD_LINE_ARGS=%$ - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% - -:end -@rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega diff --git a/flutter_app/android/settings.gradle b/flutter_app/android/settings.gradle index b507b94..54b690d 100644 --- a/flutter_app/android/settings.gradle +++ b/flutter_app/android/settings.gradle @@ -18,8 +18,8 @@ pluginManagement { plugins { id "dev.flutter.flutter-plugin-loader" version "1.0.0" - id "com.android.application" version "8.9.1" apply false - id "org.jetbrains.kotlin.android" version "2.1.0" apply false + id "com.android.application" version "8.1.0" apply false + id "org.jetbrains.kotlin.android" version "1.9.0" apply false } include ":app" diff --git a/flutter_app/lib/screens/configure_screen.dart b/flutter_app/lib/screens/configure_screen.dart index e2bec5a..5e3aa1d 100644 --- a/flutter_app/lib/screens/configure_screen.dart +++ b/flutter_app/lib/screens/configure_screen.dart @@ -8,7 +8,6 @@ import 'package:url_launcher/url_launcher.dart'; import '../l10n/app_localizations.dart'; import '../services/native_bridge.dart'; import '../services/screenshot_service.dart'; -import '../services/storage_permission_service.dart'; import '../services/terminal_service.dart'; import '../widgets/terminal_toolbar.dart'; @@ -61,22 +60,6 @@ class _ConfigureScreenState extends State { _pty?.kill(); _pty = null; try { - final hasStoragePermission = - await StoragePermissionService.ensurePermission( - context, - dialogTitleKey: 'configureStorageDialogTitle', - dialogBodyKey: 'configureStorageDialogBody', - ); - if (!hasStoragePermission) { - if (mounted) { - setState(() { - _loading = false; - _error = context.l10n.t('configureStoragePermissionRequired'); - }); - } - return; - } - // Ensure dirs + resolv.conf exist before proot starts (#40). try { await NativeBridge.setupDirs(); diff --git a/flutter_app/lib/screens/onboarding_screen.dart b/flutter_app/lib/screens/onboarding_screen.dart index 7c1ede6..c8cc953 100644 --- a/flutter_app/lib/screens/onboarding_screen.dart +++ b/flutter_app/lib/screens/onboarding_screen.dart @@ -9,7 +9,6 @@ import '../constants.dart'; import '../l10n/app_localizations.dart'; import '../services/native_bridge.dart'; import '../services/screenshot_service.dart'; -import '../services/storage_permission_service.dart'; import '../services/terminal_service.dart'; import '../services/preferences_service.dart'; import '../widgets/terminal_toolbar.dart'; @@ -83,22 +82,6 @@ class _OnboardingScreenState extends State { _pty?.kill(); _pty = null; try { - final hasStoragePermission = - await StoragePermissionService.ensurePermission( - context, - dialogTitleKey: 'onboardingStorageDialogTitle', - dialogBodyKey: 'onboardingStorageDialogBody', - ); - if (!hasStoragePermission) { - if (mounted) { - setState(() { - _loading = false; - _error = context.l10n.t('onboardingStoragePermissionRequired'); - }); - } - return; - } - // Ensure dirs + resolv.conf exist before proot starts (#40). try { await NativeBridge.setupDirs(); diff --git a/flutter_app/lib/screens/setup_wizard_screen.dart b/flutter_app/lib/screens/setup_wizard_screen.dart index ede12c2..b4a1631 100644 --- a/flutter_app/lib/screens/setup_wizard_screen.dart +++ b/flutter_app/lib/screens/setup_wizard_screen.dart @@ -7,7 +7,6 @@ import '../models/setup_state.dart'; import '../models/optional_package.dart'; import '../providers/setup_provider.dart'; import '../services/package_service.dart'; -import '../services/storage_permission_service.dart'; import '../widgets/progress_step.dart'; import 'onboarding_screen.dart'; import 'package_install_screen.dart'; @@ -39,26 +38,6 @@ class _SetupWizardScreenState extends State { } Future _beginSetup(SetupProvider provider) async { - final hasStoragePermission = - await StoragePermissionService.ensurePermission( - context, - dialogTitleKey: 'setupWizardStorageDialogTitle', - dialogBodyKey: 'setupWizardStorageDialogBody', - ); - - if (!mounted) { - return; - } - - if (!hasStoragePermission) { - setState(() { - _started = false; - _permissionError = - context.l10n.t('setupWizardStoragePermissionRequired'); - }); - return; - } - setState(() { _started = true; _permissionError = null; diff --git a/flutter_app/lib/services/capabilities/location_capability.dart b/flutter_app/lib/services/capabilities/location_capability.dart index 0fa5852..5558768 100644 --- a/flutter_app/lib/services/capabilities/location_capability.dart +++ b/flutter_app/lib/services/capabilities/location_capability.dart @@ -66,10 +66,8 @@ class LocationCapability extends CapabilityHandler { try { final position = await Geolocator.getCurrentPosition( - locationSettings: const LocationSettings( - accuracy: LocationAccuracy.high, - timeLimit: Duration(seconds: 10), - ), + desiredAccuracy: LocationAccuracy.high, + timeLimit: const Duration(seconds: 10), ); return _positionToFrame(position); } on TimeoutException { @@ -80,8 +78,7 @@ class LocationCapability extends CapabilityHandler { } return NodeFrame.response('', error: { 'code': 'LOCATION_TIMEOUT', - 'message': - 'Could not get location within 10 seconds and no cached position available', + 'message': 'Could not get location within 10 seconds and no cached position available', }); } } catch (e) { diff --git a/flutter_app/lib/services/provider_config_service.dart b/flutter_app/lib/services/provider_config_service.dart index cd4ce2f..34c98e2 100644 --- a/flutter_app/lib/services/provider_config_service.dart +++ b/flutter_app/lib/services/provider_config_service.dart @@ -13,7 +13,7 @@ class ProviderConfigService { /// Read the current config and return a map with: /// - `activeModel`: the current primary model string (or null) - /// - `providers`: `Map` for configured providers + /// - `providers`: `Map` for configured providers static Future> readConfig() async { try { final content = await NativeBridge.readRootfsFile(_configPath); @@ -39,8 +39,7 @@ class ProviderConfigService { final providers = {}; final modelsSection = config['models'] as Map?; if (modelsSection != null) { - final providerEntries = - modelsSection['providers'] as Map?; + final providerEntries = modelsSection['providers'] as Map?; if (providerEntries != null) { for (final entry in providerEntries.entries) { providers[entry.key] = entry.value; @@ -69,7 +68,6 @@ class ProviderConfigService { final providerJson = jsonEncode({ 'apiKey': apiKey, 'baseUrl': resolvedBaseUrl, - 'model': model, 'models': [model], }); final modelJson = jsonEncode(model); @@ -125,25 +123,18 @@ fs.writeFileSync(p, JSON.stringify(c, null, 2)); // Merge provider entry config['models'] ??= {}; - (config['models'] as Map)['providers'] ??= - {}; - ((config['models'] as Map)['providers'] - as Map)[providerId] = { + (config['models'] as Map)['providers'] ??= {}; + ((config['models'] as Map)['providers'] as Map)[providerId] = { 'apiKey': apiKey, 'baseUrl': baseUrl, - 'model': model, 'models': [model], }; // Set active model config['agents'] ??= {}; - (config['agents'] as Map)['defaults'] ??= - {}; - ((config['agents'] as Map)['defaults'] - as Map)['model'] ??= {}; - (((config['agents'] as Map)['defaults'] - as Map)['model'] - as Map)['primary'] = model; + (config['agents'] as Map)['defaults'] ??= {}; + ((config['agents'] as Map)['defaults'] as Map)['model'] ??= {}; + (((config['agents'] as Map)['defaults'] as Map)['model'] as Map)['primary'] = model; const encoder = JsonEncoder.withIndent(' '); await NativeBridge.writeRootfsFile(_configPath, encoder.convert(config)); diff --git a/flutter_app/lib/services/storage_permission_service.dart b/flutter_app/lib/services/storage_permission_service.dart deleted file mode 100644 index 4eddde2..0000000 --- a/flutter_app/lib/services/storage_permission_service.dart +++ /dev/null @@ -1,46 +0,0 @@ -import 'package:flutter/material.dart'; -import '../l10n/app_localizations.dart'; -import 'native_bridge.dart'; - -class StoragePermissionService { - static Future ensurePermission( - BuildContext context, { - required String dialogTitleKey, - required String dialogBodyKey, - }) async { - final hasPermission = await NativeBridge.hasStoragePermission(); - if (hasPermission) { - return true; - } - - if (!context.mounted) { - return false; - } - - final l10n = context.l10n; - final shouldRequest = await showDialog( - context: context, - builder: (dialogContext) => AlertDialog( - title: Text(l10n.t(dialogTitleKey)), - content: Text(l10n.t(dialogBodyKey)), - actions: [ - TextButton( - onPressed: () => Navigator.of(dialogContext).pop(false), - child: Text(l10n.t('commonCancel')), - ), - FilledButton( - onPressed: () => Navigator.of(dialogContext).pop(true), - child: Text(l10n.t('settingsStorageDialogAction')), - ), - ], - ), - ); - - if (shouldRequest != true) { - return false; - } - - await NativeBridge.requestStoragePermission(); - return await NativeBridge.hasStoragePermission(); - } -} diff --git a/flutter_app/pubspec.lock b/flutter_app/pubspec.lock deleted file mode 100644 index 635abe9..0000000 --- a/flutter_app/pubspec.lock +++ /dev/null @@ -1,1015 +0,0 @@ -# Generated by pub -# See https://dart.dev/tools/pub/glossary#lockfile -packages: - args: - dependency: transitive - description: - name: args - sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 - url: "https://pub.dev" - source: hosted - version: "2.7.0" - async: - dependency: transitive - description: - name: async - sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" - url: "https://pub.dev" - source: hosted - version: "2.13.0" - bluez: - dependency: transitive - description: - name: bluez - sha256: "61a7204381925896a374301498f2f5399e59827c6498ae1e924aaa598751b545" - url: "https://pub.dev" - source: hosted - version: "0.8.3" - boolean_selector: - dependency: transitive - description: - name: boolean_selector - sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" - url: "https://pub.dev" - source: hosted - version: "2.1.2" - camera: - dependency: "direct main" - description: - name: camera - sha256: "46f391e9bbdaa373d15e296abc5de8bfb0dd0d0c7487592dd8f20e8ef980429f" - url: "https://pub.dev" - source: hosted - version: "0.12.0" - camera_android_camerax: - dependency: transitive - description: - name: camera_android_camerax - sha256: c0be4298e3888ba6cf5c1fb1ae1203f08dcbb14d4f545ce5262f473cf8c33e28 - url: "https://pub.dev" - source: hosted - version: "0.7.1" - camera_avfoundation: - dependency: transitive - description: - name: camera_avfoundation - sha256: "90e4cc3fde331581a3b2d35d83be41dbb7393af0ab857eb27b732174289cb96d" - url: "https://pub.dev" - source: hosted - version: "0.10.1" - camera_platform_interface: - dependency: transitive - description: - name: camera_platform_interface - sha256: "98cfc9357e04bad617671b4c1f78a597f25f08003089dd94050709ae54effc63" - url: "https://pub.dev" - source: hosted - version: "2.12.0" - camera_web: - dependency: transitive - description: - name: camera_web - sha256: "57f49a635c8bf249d07fb95eb693d7e4dda6796dedb3777f9127fb54847beba7" - url: "https://pub.dev" - source: hosted - version: "0.3.5+3" - characters: - dependency: transitive - description: - name: characters - sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b - url: "https://pub.dev" - source: hosted - version: "1.4.1" - clock: - dependency: transitive - description: - name: clock - sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b - url: "https://pub.dev" - source: hosted - version: "1.1.2" - code_assets: - dependency: transitive - description: - name: code_assets - sha256: "83ccdaa064c980b5596c35dd64a8d3ecc68620174ab9b90b6343b753aa721687" - url: "https://pub.dev" - source: hosted - version: "1.0.0" - collection: - dependency: transitive - description: - name: collection - sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" - url: "https://pub.dev" - source: hosted - version: "1.19.1" - convert: - dependency: transitive - description: - name: convert - sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 - url: "https://pub.dev" - source: hosted - version: "3.1.2" - cross_file: - dependency: transitive - description: - name: cross_file - sha256: "28bb3ae56f117b5aec029d702a90f57d285cd975c3c5c281eaca38dbc47c5937" - url: "https://pub.dev" - source: hosted - version: "0.3.5+2" - crypto: - dependency: transitive - description: - name: crypto - sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf - url: "https://pub.dev" - source: hosted - version: "3.0.7" - cryptography: - dependency: "direct main" - description: - name: cryptography - sha256: "3eda3029d34ec9095a27a198ac9785630fe525c0eb6a49f3d575272f8e792ef0" - url: "https://pub.dev" - source: hosted - version: "2.9.0" - dbus: - dependency: transitive - description: - name: dbus - sha256: d0c98dcd4f5169878b6cf8f6e0a52403a9dff371a3e2f019697accbf6f44a270 - url: "https://pub.dev" - source: hosted - version: "0.7.12" - dio: - dependency: "direct main" - description: - name: dio - sha256: aff32c08f92787a557dd5c0145ac91536481831a01b4648136373cddb0e64f8c - url: "https://pub.dev" - source: hosted - version: "5.9.2" - dio_web_adapter: - dependency: transitive - description: - name: dio_web_adapter - sha256: "2f9e64323a7c3c7ef69567d5c800424a11f8337b8b228bad02524c9fb3c1f340" - url: "https://pub.dev" - source: hosted - version: "2.1.2" - equatable: - dependency: transitive - description: - name: equatable - sha256: "3e0141505477fd8ad55d6eb4e7776d3fe8430be8e497ccb1521370c3f21a3e2b" - url: "https://pub.dev" - source: hosted - version: "2.0.8" - fake_async: - dependency: transitive - description: - name: fake_async - sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" - url: "https://pub.dev" - source: hosted - version: "1.3.3" - ffi: - dependency: transitive - description: - name: ffi - sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45" - url: "https://pub.dev" - source: hosted - version: "2.2.0" - file: - dependency: transitive - description: - name: file - sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 - url: "https://pub.dev" - source: hosted - version: "7.0.1" - fixnum: - dependency: transitive - description: - name: fixnum - sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be - url: "https://pub.dev" - source: hosted - version: "1.1.1" - flutter: - dependency: "direct main" - description: flutter - source: sdk - version: "0.0.0" - flutter_blue_plus: - dependency: "direct main" - description: - name: flutter_blue_plus - sha256: "69a8c87c11fc792e8cf0f997d275484fbdb5143ac9f0ac4d424429700cb4e0ed" - url: "https://pub.dev" - source: hosted - version: "1.36.8" - flutter_blue_plus_android: - dependency: transitive - description: - name: flutter_blue_plus_android - sha256: "6f7fe7e69659c30af164a53730707edc16aa4d959e4c208f547b893d940f853d" - url: "https://pub.dev" - source: hosted - version: "7.0.4" - flutter_blue_plus_darwin: - dependency: transitive - description: - name: flutter_blue_plus_darwin - sha256: "682982862c1d964f4d54a3fb5fccc9e59a066422b93b7e22079aeecd9c0d38f8" - url: "https://pub.dev" - source: hosted - version: "7.0.3" - flutter_blue_plus_linux: - dependency: transitive - description: - name: flutter_blue_plus_linux - sha256: "56b0c45edd0a2eec8f85bd97a26ac3cd09447e10d0094fed55587bf0592e3347" - url: "https://pub.dev" - source: hosted - version: "7.0.3" - flutter_blue_plus_platform_interface: - dependency: transitive - description: - name: flutter_blue_plus_platform_interface - sha256: "84fbd180c50a40c92482f273a92069960805ce324e3673ad29c41d2faaa7c5c2" - url: "https://pub.dev" - source: hosted - version: "7.0.0" - flutter_blue_plus_web: - dependency: transitive - description: - name: flutter_blue_plus_web - sha256: a1aceee753d171d24c0e0cdadb37895b5e9124862721f25f60bb758e20b72c99 - url: "https://pub.dev" - source: hosted - version: "7.0.2" - flutter_lints: - dependency: "direct dev" - description: - name: flutter_lints - sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1" - url: "https://pub.dev" - source: hosted - version: "6.0.0" - flutter_localizations: - dependency: "direct main" - description: flutter - source: sdk - version: "0.0.0" - flutter_plugin_android_lifecycle: - dependency: transitive - description: - name: flutter_plugin_android_lifecycle - sha256: ee8068e0e1cd16c4a82714119918efdeed33b3ba7772c54b5d094ab53f9b7fd1 - url: "https://pub.dev" - source: hosted - version: "2.0.33" - flutter_pty: - dependency: "direct main" - description: - name: flutter_pty - sha256: c2f3b3160b519ac820fa3f6ef175361f2dfc52c557465643589542e9f229ad66 - url: "https://pub.dev" - source: hosted - version: "0.4.2" - flutter_test: - dependency: "direct dev" - description: flutter - source: sdk - version: "0.0.0" - flutter_web_plugins: - dependency: transitive - description: flutter - source: sdk - version: "0.0.0" - geoclue: - dependency: transitive - description: - name: geoclue - sha256: c2a998c77474fc57aa00c6baa2928e58f4b267649057a1c76738656e9dbd2a7f - url: "https://pub.dev" - source: hosted - version: "0.1.1" - geolocator: - dependency: "direct main" - description: - name: geolocator - sha256: "79939537046c9025be47ec645f35c8090ecadb6fe98eba146a0d25e8c1357516" - url: "https://pub.dev" - source: hosted - version: "14.0.2" - geolocator_android: - dependency: transitive - description: - name: geolocator_android - sha256: "179c3cb66dfa674fc9ccbf2be872a02658724d1c067634e2c427cf6df7df901a" - url: "https://pub.dev" - source: hosted - version: "5.0.2" - geolocator_apple: - dependency: transitive - description: - name: geolocator_apple - sha256: dbdd8789d5aaf14cf69f74d4925ad1336b4433a6efdf2fce91e8955dc921bf22 - url: "https://pub.dev" - source: hosted - version: "2.3.13" - geolocator_linux: - dependency: transitive - description: - name: geolocator_linux - sha256: d64112a205931926f4363bb6bd48f14cb38e7326833041d170615586cd143797 - url: "https://pub.dev" - source: hosted - version: "0.2.4" - geolocator_platform_interface: - dependency: transitive - description: - name: geolocator_platform_interface - sha256: "30cb64f0b9adcc0fb36f628b4ebf4f731a2961a0ebd849f4b56200205056fe67" - url: "https://pub.dev" - source: hosted - version: "4.2.6" - geolocator_web: - dependency: transitive - description: - name: geolocator_web - sha256: b1ae9bdfd90f861fde8fd4f209c37b953d65e92823cb73c7dee1fa021b06f172 - url: "https://pub.dev" - source: hosted - version: "4.1.3" - geolocator_windows: - dependency: transitive - description: - name: geolocator_windows - sha256: "175435404d20278ffd220de83c2ca293b73db95eafbdc8131fe8609be1421eb6" - url: "https://pub.dev" - source: hosted - version: "0.2.5" - glob: - dependency: transitive - description: - name: glob - sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de - url: "https://pub.dev" - source: hosted - version: "2.1.3" - google_fonts: - dependency: "direct main" - description: - name: google_fonts - sha256: db9df7a5898d894eeda4c78143f35c30a243558be439518972366880b80bf88e - url: "https://pub.dev" - source: hosted - version: "8.0.2" - gsettings: - dependency: transitive - description: - name: gsettings - sha256: "1b0ce661f5436d2db1e51f3c4295a49849f03d304003a7ba177d01e3a858249c" - url: "https://pub.dev" - source: hosted - version: "0.2.8" - hooks: - dependency: transitive - description: - name: hooks - sha256: e79ed1e8e1929bc6ecb6ec85f0cb519c887aa5b423705ded0d0f2d9226def388 - url: "https://pub.dev" - source: hosted - version: "1.0.2" - http: - dependency: "direct main" - description: - name: http - sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412" - url: "https://pub.dev" - source: hosted - version: "1.6.0" - http_parser: - dependency: transitive - description: - name: http_parser - sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" - url: "https://pub.dev" - source: hosted - version: "4.1.2" - intl: - dependency: transitive - description: - name: intl - sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5" - url: "https://pub.dev" - source: hosted - version: "0.20.2" - leak_tracker: - dependency: transitive - description: - name: leak_tracker - sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" - url: "https://pub.dev" - source: hosted - version: "11.0.2" - leak_tracker_flutter_testing: - dependency: transitive - description: - name: leak_tracker_flutter_testing - sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" - url: "https://pub.dev" - source: hosted - version: "3.0.10" - leak_tracker_testing: - dependency: transitive - description: - name: leak_tracker_testing - sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" - url: "https://pub.dev" - source: hosted - version: "3.0.2" - lints: - dependency: transitive - description: - name: lints - sha256: "12f842a479589fea194fe5c5a3095abc7be0c1f2ddfa9a0e76aed1dbd26a87df" - url: "https://pub.dev" - source: hosted - version: "6.1.0" - logging: - dependency: transitive - description: - name: logging - sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 - url: "https://pub.dev" - source: hosted - version: "1.3.0" - matcher: - dependency: transitive - description: - name: matcher - sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861 - url: "https://pub.dev" - source: hosted - version: "0.12.19" - material_color_utilities: - dependency: transitive - description: - name: material_color_utilities - sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" - url: "https://pub.dev" - source: hosted - version: "0.13.0" - meta: - dependency: transitive - description: - name: meta - sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" - url: "https://pub.dev" - source: hosted - version: "1.17.0" - mime: - dependency: transitive - description: - name: mime - sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" - url: "https://pub.dev" - source: hosted - version: "2.0.0" - native_toolchain_c: - dependency: transitive - description: - name: native_toolchain_c - sha256: "92b2ca62c8bd2b8d2f267cdfccf9bfbdb7322f778f8f91b3ce5b5cda23a3899f" - url: "https://pub.dev" - source: hosted - version: "0.17.5" - nested: - dependency: transitive - description: - name: nested - sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" - url: "https://pub.dev" - source: hosted - version: "1.0.0" - objective_c: - dependency: transitive - description: - name: objective_c - sha256: "100a1c87616ab6ed41ec263b083c0ef3261ee6cd1dc3b0f35f8ddfa4f996fe52" - url: "https://pub.dev" - source: hosted - version: "9.3.0" - package_info_plus: - dependency: transitive - description: - name: package_info_plus - sha256: f69da0d3189a4b4ceaeb1a3defb0f329b3b352517f52bed4290f83d4f06bc08d - url: "https://pub.dev" - source: hosted - version: "9.0.0" - package_info_plus_platform_interface: - dependency: transitive - description: - name: package_info_plus_platform_interface - sha256: "202a487f08836a592a6bd4f901ac69b3a8f146af552bbd14407b6b41e1c3f086" - url: "https://pub.dev" - source: hosted - version: "3.2.1" - path: - dependency: transitive - description: - name: path - sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" - url: "https://pub.dev" - source: hosted - version: "1.9.1" - path_provider: - dependency: "direct main" - description: - name: path_provider - sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" - url: "https://pub.dev" - source: hosted - version: "2.1.5" - path_provider_android: - dependency: transitive - description: - name: path_provider_android - sha256: f2c65e21139ce2c3dad46922be8272bb5963516045659e71bb16e151c93b580e - url: "https://pub.dev" - source: hosted - version: "2.2.22" - path_provider_foundation: - dependency: transitive - description: - name: path_provider_foundation - sha256: "2a376b7d6392d80cd3705782d2caa734ca4727776db0b6ec36ef3f1855197699" - url: "https://pub.dev" - source: hosted - version: "2.6.0" - path_provider_linux: - dependency: transitive - description: - name: path_provider_linux - sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 - url: "https://pub.dev" - source: hosted - version: "2.2.1" - path_provider_platform_interface: - dependency: transitive - description: - name: path_provider_platform_interface - sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" - url: "https://pub.dev" - source: hosted - version: "2.1.2" - path_provider_windows: - dependency: transitive - description: - name: path_provider_windows - sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 - url: "https://pub.dev" - source: hosted - version: "2.3.0" - permission_handler: - dependency: "direct main" - description: - name: permission_handler - sha256: bc917da36261b00137bbc8896bf1482169cd76f866282368948f032c8c1caae1 - url: "https://pub.dev" - source: hosted - version: "12.0.1" - permission_handler_android: - dependency: transitive - description: - name: permission_handler_android - sha256: "1e3bc410ca1bf84662104b100eb126e066cb55791b7451307f9708d4007350e6" - url: "https://pub.dev" - source: hosted - version: "13.0.1" - permission_handler_apple: - dependency: transitive - description: - name: permission_handler_apple - sha256: f000131e755c54cf4d84a5d8bd6e4149e262cc31c5a8b1d698de1ac85fa41023 - url: "https://pub.dev" - source: hosted - version: "9.4.7" - permission_handler_html: - dependency: transitive - description: - name: permission_handler_html - sha256: "38f000e83355abb3392140f6bc3030660cfaef189e1f87824facb76300b4ff24" - url: "https://pub.dev" - source: hosted - version: "0.1.3+5" - permission_handler_platform_interface: - dependency: transitive - description: - name: permission_handler_platform_interface - sha256: eb99b295153abce5d683cac8c02e22faab63e50679b937fa1bf67d58bb282878 - url: "https://pub.dev" - source: hosted - version: "4.3.0" - permission_handler_windows: - dependency: transitive - description: - name: permission_handler_windows - sha256: "1a790728016f79a41216d88672dbc5df30e686e811ad4e698bfc51f76ad91f1e" - url: "https://pub.dev" - source: hosted - version: "0.2.1" - petitparser: - dependency: transitive - description: - name: petitparser - sha256: "91bd59303e9f769f108f8df05e371341b15d59e995e6806aefab827b58336675" - url: "https://pub.dev" - source: hosted - version: "7.0.2" - platform: - dependency: transitive - description: - name: platform - sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" - url: "https://pub.dev" - source: hosted - version: "3.1.6" - plugin_platform_interface: - dependency: transitive - description: - name: plugin_platform_interface - sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" - url: "https://pub.dev" - source: hosted - version: "2.1.8" - provider: - dependency: "direct main" - description: - name: provider - sha256: "4e82183fa20e5ca25703ead7e05de9e4cceed1fbd1eadc1ac3cb6f565a09f272" - url: "https://pub.dev" - source: hosted - version: "6.1.5+1" - pub_semver: - dependency: transitive - description: - name: pub_semver - sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" - url: "https://pub.dev" - source: hosted - version: "2.2.0" - quiver: - dependency: transitive - description: - name: quiver - sha256: ea0b925899e64ecdfbf9c7becb60d5b50e706ade44a85b2363be2a22d88117d2 - url: "https://pub.dev" - source: hosted - version: "3.2.2" - rxdart: - dependency: transitive - description: - name: rxdart - sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962" - url: "https://pub.dev" - source: hosted - version: "0.28.0" - shared_preferences: - dependency: "direct main" - description: - name: shared_preferences - sha256: "2939ae520c9024cb197fc20dee269cd8cdbf564c8b5746374ec6cacdc5169e64" - url: "https://pub.dev" - source: hosted - version: "2.5.4" - shared_preferences_android: - dependency: transitive - description: - name: shared_preferences_android - sha256: "8374d6200ab33ac99031a852eba4c8eb2170c4bf20778b3e2c9eccb45384fb41" - url: "https://pub.dev" - source: hosted - version: "2.4.21" - shared_preferences_foundation: - dependency: transitive - description: - name: shared_preferences_foundation - sha256: "4e7eaffc2b17ba398759f1151415869a34771ba11ebbccd1b0145472a619a64f" - url: "https://pub.dev" - source: hosted - version: "2.5.6" - shared_preferences_linux: - dependency: transitive - description: - name: shared_preferences_linux - sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f" - url: "https://pub.dev" - source: hosted - version: "2.4.1" - shared_preferences_platform_interface: - dependency: transitive - description: - name: shared_preferences_platform_interface - sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80" - url: "https://pub.dev" - source: hosted - version: "2.4.1" - shared_preferences_web: - dependency: transitive - description: - name: shared_preferences_web - sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019 - url: "https://pub.dev" - source: hosted - version: "2.4.3" - shared_preferences_windows: - dependency: transitive - description: - name: shared_preferences_windows - sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1" - url: "https://pub.dev" - source: hosted - version: "2.4.1" - sky_engine: - dependency: transitive - description: flutter - source: sdk - version: "0.0.0" - source_span: - dependency: transitive - description: - name: source_span - sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab" - url: "https://pub.dev" - source: hosted - version: "1.10.2" - stack_trace: - dependency: transitive - description: - name: stack_trace - sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" - url: "https://pub.dev" - source: hosted - version: "1.12.1" - stream_channel: - dependency: transitive - description: - name: stream_channel - sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" - url: "https://pub.dev" - source: hosted - version: "2.1.4" - stream_transform: - dependency: transitive - description: - name: stream_transform - sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871 - url: "https://pub.dev" - source: hosted - version: "2.1.1" - string_scanner: - dependency: transitive - description: - name: string_scanner - sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" - url: "https://pub.dev" - source: hosted - version: "1.4.1" - term_glyph: - dependency: transitive - description: - name: term_glyph - sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" - url: "https://pub.dev" - source: hosted - version: "1.2.2" - test_api: - dependency: transitive - description: - name: test_api - sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a" - url: "https://pub.dev" - source: hosted - version: "0.7.10" - typed_data: - dependency: transitive - description: - name: typed_data - sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 - url: "https://pub.dev" - source: hosted - version: "1.4.0" - url_launcher: - dependency: "direct main" - description: - name: url_launcher - sha256: f6a7e5c4835bb4e3026a04793a4199ca2d14c739ec378fdfe23fc8075d0439f8 - url: "https://pub.dev" - source: hosted - version: "6.3.2" - url_launcher_android: - dependency: transitive - description: - name: url_launcher_android - sha256: "767344bf3063897b5cf0db830e94f904528e6dd50a6dfaf839f0abf509009611" - url: "https://pub.dev" - source: hosted - version: "6.3.28" - url_launcher_ios: - dependency: transitive - description: - name: url_launcher_ios - sha256: "580fe5dfb51671ae38191d316e027f6b76272b026370708c2d898799750a02b0" - url: "https://pub.dev" - source: hosted - version: "6.4.1" - url_launcher_linux: - dependency: transitive - description: - name: url_launcher_linux - sha256: d5e14138b3bc193a0f63c10a53c94b91d399df0512b1f29b94a043db7482384a - url: "https://pub.dev" - source: hosted - version: "3.2.2" - url_launcher_macos: - dependency: transitive - description: - name: url_launcher_macos - sha256: "368adf46f71ad3c21b8f06614adb38346f193f3a59ba8fe9a2fd74133070ba18" - url: "https://pub.dev" - source: hosted - version: "3.2.5" - url_launcher_platform_interface: - dependency: transitive - description: - name: url_launcher_platform_interface - sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029" - url: "https://pub.dev" - source: hosted - version: "2.3.2" - url_launcher_web: - dependency: transitive - description: - name: url_launcher_web - sha256: d0412fcf4c6b31ecfdb7762359b7206ffba3bbffd396c6d9f9c4616ece476c1f - url: "https://pub.dev" - source: hosted - version: "2.4.2" - url_launcher_windows: - dependency: transitive - description: - name: url_launcher_windows - sha256: "712c70ab1b99744ff066053cbe3e80c73332b38d46e5e945c98689b2e66fc15f" - url: "https://pub.dev" - source: hosted - version: "3.1.5" - usb_serial: - dependency: "direct main" - description: - name: usb_serial - sha256: a605a600e34e7f28d4e80851ca3999ef747e42e406138887b8a88b8c382a8b07 - url: "https://pub.dev" - source: hosted - version: "0.5.2" - uuid: - dependency: "direct main" - description: - name: uuid - sha256: "1fef9e8e11e2991bb773070d4656b7bd5d850967a2456cfc83cf47925ba79489" - url: "https://pub.dev" - source: hosted - version: "4.5.3" - vector_math: - dependency: transitive - description: - name: vector_math - sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b - url: "https://pub.dev" - source: hosted - version: "2.2.0" - vm_service: - dependency: transitive - description: - name: vm_service - sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60" - url: "https://pub.dev" - source: hosted - version: "15.0.2" - web: - dependency: transitive - description: - name: web - sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" - url: "https://pub.dev" - source: hosted - version: "1.1.1" - web_socket: - dependency: transitive - description: - name: web_socket - sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c" - url: "https://pub.dev" - source: hosted - version: "1.0.1" - web_socket_channel: - dependency: "direct main" - description: - name: web_socket_channel - sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8 - url: "https://pub.dev" - source: hosted - version: "3.0.3" - webview_flutter: - dependency: "direct main" - description: - name: webview_flutter - sha256: a3da219916aba44947d3a5478b1927876a09781174b5a2b67fa5be0555154bf9 - url: "https://pub.dev" - source: hosted - version: "4.13.1" - webview_flutter_android: - dependency: transitive - description: - name: webview_flutter_android - sha256: "2a03df01df2fd30b075d1e7f24c28aee593f2e5d5ac4c3c4283c5eda63717b24" - url: "https://pub.dev" - source: hosted - version: "4.10.13" - webview_flutter_platform_interface: - dependency: transitive - description: - name: webview_flutter_platform_interface - sha256: "63d26ee3aca7256a83ccb576a50272edd7cfc80573a4305caa98985feb493ee0" - url: "https://pub.dev" - source: hosted - version: "2.14.0" - webview_flutter_wkwebview: - dependency: transitive - description: - name: webview_flutter_wkwebview - sha256: fc0af89d403e1c053f03d023d97550412fa79f35332e2939514c82e6fe633198 - url: "https://pub.dev" - source: hosted - version: "3.23.8" - win32: - dependency: transitive - description: - name: win32 - sha256: d7cb55e04cd34096cd3a79b3330245f54cb96a370a1c27adb3c84b917de8b08e - url: "https://pub.dev" - source: hosted - version: "5.15.0" - xdg_directories: - dependency: transitive - description: - name: xdg_directories - sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" - url: "https://pub.dev" - source: hosted - version: "1.1.0" - xml: - dependency: transitive - description: - name: xml - sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025" - url: "https://pub.dev" - source: hosted - version: "6.6.1" - xterm: - dependency: "direct main" - description: - name: xterm - sha256: "168dfedca77cba33fdb6f52e2cd001e9fde216e398e89335c19b524bb22da3a2" - url: "https://pub.dev" - source: hosted - version: "4.0.0" - yaml: - dependency: transitive - description: - name: yaml - sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce - url: "https://pub.dev" - source: hosted - version: "3.1.3" - zmodem: - dependency: transitive - description: - name: zmodem - sha256: "3b7e5b29f3a7d8aee472029b05165a68438eff2f3f7766edf13daba1e297adbf" - url: "https://pub.dev" - source: hosted - version: "0.0.6" -sdks: - dart: ">=3.10.3 <4.0.0" - flutter: ">=3.38.4" diff --git a/flutter_app/pubspec.yaml b/flutter_app/pubspec.yaml index eb83b11..0afdc3d 100644 --- a/flutter_app/pubspec.yaml +++ b/flutter_app/pubspec.yaml @@ -19,21 +19,21 @@ dependencies: provider: ^6.1.0 shared_preferences: ^2.2.0 path_provider: ^2.1.0 - permission_handler: ^12.0.1 + permission_handler: ^11.3.0 url_launcher: ^6.2.0 web_socket_channel: ^3.0.0 cryptography: ^2.7.0 - google_fonts: ^8.0.2 + google_fonts: ^6.1.0 uuid: ^4.2.0 - camera: ^0.12.0 - geolocator: ^14.0.2 + camera: ^0.11.0 + geolocator: ^12.0.0 flutter_blue_plus: ^1.32.0 usb_serial: ^0.5.1 dev_dependencies: flutter_test: sdk: flutter - flutter_lints: ^6.0.0 + flutter_lints: ^5.0.0 flutter: uses-material-design: true diff --git a/scripts/fetch-proot-binaries.ps1 b/scripts/fetch-proot-binaries.ps1 deleted file mode 100644 index e2242e4..0000000 --- a/scripts/fetch-proot-binaries.ps1 +++ /dev/null @@ -1,143 +0,0 @@ -Set-StrictMode -Version Latest -$ErrorActionPreference = 'Stop' - -$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path -$repoRoot = Split-Path -Parent $scriptDir -$jniLibsDir = Join-Path $repoRoot 'flutter_app\android\app\src\main\jniLibs' -$tempRoot = Join-Path ([System.IO.Path]::GetTempPath()) ("openclaw-proot-" + [System.Guid]::NewGuid().ToString('N')) -$repoBase = 'https://packages.termux.dev/apt/termux-main' -$packageIndexCache = @{} - -New-Item -ItemType Directory -Force -Path $tempRoot | Out-Null - -function Get-TermuxPackageFilename { - param( - [string]$PackageName, - [string]$DebArch - ) - - if (-not $packageIndexCache.ContainsKey($DebArch)) { - $packagesPath = Join-Path $tempRoot ("Packages-$DebArch.txt") - $packagesUrl = "$repoBase/dists/stable/main/binary-$DebArch/Packages" - & curl.exe -fsSL $packagesUrl -o $packagesPath - $packageIndexCache[$DebArch] = $packagesPath - } - - $packagesFile = $packageIndexCache[$DebArch] - $match = Select-String -Path $packagesFile -Pattern "^Package: $([regex]::Escape($PackageName))$" -Context 0,12 | Select-Object -First 1 - if ($match) { - foreach ($line in $match.Context.PostContext) { - if ($line -match '^Filename:\s+(.+)$') { - return $Matches[1].Trim() - } - } - } - - throw "Package $PackageName not found for architecture $DebArch" -} - -function Expand-DebPackage { - param( - [string]$DebPath, - [string]$Destination - ) - - New-Item -ItemType Directory -Force -Path $Destination | Out-Null - Push-Location $Destination - try { - & ar.exe x $DebPath | Out-Null - - if (Test-Path 'data.tar.xz') { - & xz.exe -d -k 'data.tar.xz' | Out-Null - tar -xf 'data.tar' | Out-Null - } elseif (Test-Path 'data.tar.gz') { - tar -xf 'data.tar.gz' | Out-Null - } elseif (Test-Path 'data.tar.zst') { - tar -xf 'data.tar.zst' | Out-Null - } else { - throw "Unsupported deb payload format in $DebPath" - } - } finally { - Pop-Location - } -} - -function Copy-FirstMatch { - param( - [string[]]$Patterns, - [string]$DestinationPath, - [switch]$AllowMissing - ) - - foreach ($pattern in $Patterns) { - $item = Get-ChildItem -Path $pattern -File -ErrorAction SilentlyContinue | Select-Object -First 1 - if ($item) { - Copy-Item -LiteralPath $item.FullName -Destination $DestinationPath -Force - return $true - } - } - - if (-not $AllowMissing) { - throw "No file matched patterns: $($Patterns -join ', ')" - } - - return $false -} - -function Fetch-ForAbi { - param( - [string]$JniAbi, - [string]$DebArch - ) - - Write-Host "[$JniAbi] Fetching binaries..." - $outDir = Join-Path $jniLibsDir $JniAbi - $extractBase = Join-Path $tempRoot $JniAbi - $prootExtract = Join-Path $extractBase 'proot' - $tallocExtract = Join-Path $extractBase 'libtalloc' - - New-Item -ItemType Directory -Force -Path $outDir | Out-Null - - $prootFilename = Get-TermuxPackageFilename -PackageName 'proot' -DebArch $DebArch - $prootDeb = Join-Path $tempRoot ("proot-$DebArch.deb") - & curl.exe -fsSL "$repoBase/$prootFilename" -o $prootDeb - Expand-DebPackage -DebPath $prootDeb -Destination $prootExtract - - $tallocFilename = Get-TermuxPackageFilename -PackageName 'libtalloc' -DebArch $DebArch - $tallocDeb = Join-Path $tempRoot ("libtalloc-$DebArch.deb") - & curl.exe -fsSL "$repoBase/$tallocFilename" -o $tallocDeb - Expand-DebPackage -DebPath $tallocDeb -Destination $tallocExtract - - Copy-FirstMatch -Patterns @( - (Join-Path $prootExtract 'data\data\com.termux\files\usr\bin\proot'), - (Join-Path $prootExtract '**\bin\proot') - ) -DestinationPath (Join-Path $outDir 'libproot.so') | Out-Null - - Copy-FirstMatch -Patterns @( - (Join-Path $prootExtract 'data\data\com.termux\files\usr\libexec\proot\loader'), - (Join-Path $prootExtract '**\proot\loader') - ) -DestinationPath (Join-Path $outDir 'libprootloader.so') | Out-Null - - Copy-FirstMatch -Patterns @( - (Join-Path $prootExtract 'data\data\com.termux\files\usr\libexec\proot\loader32'), - (Join-Path $prootExtract '**\proot\loader32') - ) -DestinationPath (Join-Path $outDir 'libprootloader32.so') -AllowMissing | Out-Null - - Copy-FirstMatch -Patterns @( - (Join-Path $tallocExtract 'data\data\com.termux\files\usr\lib\libtalloc.so.*'), - (Join-Path $tallocExtract '**\libtalloc.so.*'), - (Join-Path $tallocExtract '**\libtalloc.so') - ) -DestinationPath (Join-Path $outDir 'libtalloc.so') | Out-Null - - Get-ChildItem $outDir -File | ForEach-Object { - Write-Host "[$JniAbi] $($_.Name) ($([Math]::Round($_.Length / 1KB, 1)) KB)" - } -} - -try { - Fetch-ForAbi -JniAbi 'arm64-v8a' -DebArch 'aarch64' - Fetch-ForAbi -JniAbi 'armeabi-v7a' -DebArch 'arm' - Fetch-ForAbi -JniAbi 'x86_64' -DebArch 'x86_64' -} finally { - Remove-Item -LiteralPath $tempRoot -Recurse -Force -ErrorAction SilentlyContinue -} \ No newline at end of file From 64a58eef4d6eb4c2aa246001c83a16dfde31611d Mon Sep 17 00:00:00 2001 From: TIANLI0 <507249007@qq.com> Date: Mon, 9 Mar 2026 00:30:29 +0800 Subject: [PATCH 5/9] fix(flutter): make UI APIs compatible with Flutter 3.24 --- flutter_app/lib/app.dart | 36 ++----------- flutter_app/lib/screens/node_screen.dart | 52 ++++++++++--------- .../lib/screens/provider_detail_screen.dart | 2 +- flutter_app/lib/screens/settings_screen.dart | 2 +- 4 files changed, 33 insertions(+), 59 deletions(-) diff --git a/flutter_app/lib/app.dart b/flutter_app/lib/app.dart index 62c9c01..c953879 100644 --- a/flutter_app/lib/app.dart +++ b/flutter_app/lib/app.dart @@ -139,15 +139,7 @@ class OpenClawApp extends StatelessWidget { color: Colors.white, ), ), - cardTheme: CardThemeData( - elevation: 0, - color: AppColors.darkSurface, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - side: const BorderSide(color: AppColors.darkBorder), - ), - margin: const EdgeInsets.symmetric(vertical: 4), - ), + cardColor: AppColors.darkSurface, filledButtonTheme: FilledButtonThemeData( style: FilledButton.styleFrom( backgroundColor: AppColors.accent, @@ -209,13 +201,7 @@ class OpenClawApp extends StatelessWidget { color: AppColors.darkBorder, space: 1, ), - dialogTheme: DialogThemeData( - backgroundColor: AppColors.darkSurface, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), - side: const BorderSide(color: AppColors.darkBorder), - ), - ), + dialogBackgroundColor: AppColors.darkSurface, snackBarTheme: SnackBarThemeData( backgroundColor: AppColors.darkSurfaceAlt, contentTextStyle: GoogleFonts.inter(color: Colors.white), @@ -270,15 +256,7 @@ class OpenClawApp extends StatelessWidget { color: const Color(0xFF0A0A0A), ), ), - cardTheme: CardThemeData( - elevation: 0, - color: AppColors.lightBg, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - side: const BorderSide(color: AppColors.lightBorder), - ), - margin: const EdgeInsets.symmetric(vertical: 4), - ), + cardColor: AppColors.lightBg, filledButtonTheme: FilledButtonThemeData( style: FilledButton.styleFrom( backgroundColor: AppColors.accent, @@ -340,13 +318,7 @@ class OpenClawApp extends StatelessWidget { color: AppColors.lightBorder, space: 1, ), - dialogTheme: DialogThemeData( - backgroundColor: AppColors.lightBg, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), - side: const BorderSide(color: AppColors.lightBorder), - ), - ), + dialogBackgroundColor: AppColors.lightBg, snackBarTheme: SnackBarThemeData( backgroundColor: const Color(0xFF0A0A0A), contentTextStyle: GoogleFonts.inter(color: Colors.white), diff --git a/flutter_app/lib/screens/node_screen.dart b/flutter_app/lib/screens/node_screen.dart index 48c2295..da6d34c 100644 --- a/flutter_app/lib/screens/node_screen.dart +++ b/flutter_app/lib/screens/node_screen.dart @@ -73,29 +73,32 @@ class _NodeScreenState extends State { Card( child: Padding( padding: const EdgeInsets.all(16), - child: RadioGroup( - groupValue: _isLocal, - onChanged: (value) { - if (value != null) { - setState(() => _isLocal = value); - } - }, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - RadioListTile( - title: Text(l10n.t('nodeLocalGateway')), - subtitle: - Text(l10n.t('nodeLocalGatewaySubtitle')), - value: true, - ), - RadioListTile( - title: Text(l10n.t('nodeRemoteGateway')), - subtitle: - Text(l10n.t('nodeRemoteGatewaySubtitle')), - value: false, - ), - if (!_isLocal) ...[ + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + RadioListTile( + title: Text(l10n.t('nodeLocalGateway')), + subtitle: Text(l10n.t('nodeLocalGatewaySubtitle')), + value: true, + groupValue: _isLocal, + onChanged: (value) { + if (value != null) { + setState(() => _isLocal = value); + } + }, + ), + RadioListTile( + title: Text(l10n.t('nodeRemoteGateway')), + subtitle: Text(l10n.t('nodeRemoteGatewaySubtitle')), + value: false, + groupValue: _isLocal, + onChanged: (value) { + if (value != null) { + setState(() => _isLocal = value); + } + }, + ), + if (!_isLocal) ...[ const SizedBox(height: 12), TextField( controller: _hostController, @@ -142,9 +145,8 @@ class _NodeScreenState extends State { icon: const Icon(Icons.link), label: Text(l10n.t('nodeConnect')), ), - ], ], - ), + ], ), ), ), diff --git a/flutter_app/lib/screens/provider_detail_screen.dart b/flutter_app/lib/screens/provider_detail_screen.dart index b9d0bc5..c00962c 100644 --- a/flutter_app/lib/screens/provider_detail_screen.dart +++ b/flutter_app/lib/screens/provider_detail_screen.dart @@ -295,7 +295,7 @@ class _ProviderDetailScreenState extends State { ), const SizedBox(height: 8), DropdownButtonFormField( - initialValue: _selectedModel, + value: _selectedModel, isExpanded: true, decoration: const InputDecoration(), items: [ diff --git a/flutter_app/lib/screens/settings_screen.dart b/flutter_app/lib/screens/settings_screen.dart index 2e6cce6..2a883f0 100644 --- a/flutter_app/lib/screens/settings_screen.dart +++ b/flutter_app/lib/screens/settings_screen.dart @@ -107,7 +107,7 @@ class _SettingsScreenState extends State { Padding( padding: const EdgeInsets.fromLTRB(16, 0, 16, 8), child: DropdownButtonFormField( - initialValue: localeProvider.localeCode, + value: localeProvider.localeCode, decoration: InputDecoration(labelText: l10n.t('language')), items: [ DropdownMenuItem( From 5472c497fb82ed76073e556eda012626bc052ce9 Mon Sep 17 00:00:00 2001 From: TIANLI0 <507249007@qq.com> Date: Mon, 9 Mar 2026 00:40:00 +0800 Subject: [PATCH 6/9] fix(i18n): complete locale coverage and async safety fixes --- flutter_app/lib/app.dart | 13 + flutter_app/lib/l10n/app_strings_en.dart | 3 + flutter_app/lib/l10n/app_strings_ja.dart | 318 +++++++++--------- flutter_app/lib/l10n/app_strings_zh_hans.dart | 2 + flutter_app/lib/l10n/app_strings_zh_hant.dart | 8 +- flutter_app/lib/screens/configure_screen.dart | 5 +- flutter_app/lib/screens/node_screen.dart | 4 +- .../lib/screens/onboarding_screen.dart | 5 +- .../lib/screens/setup_wizard_screen.dart | 10 +- 9 files changed, 196 insertions(+), 172 deletions(-) diff --git a/flutter_app/lib/app.dart b/flutter_app/lib/app.dart index c953879..7baa089 100644 --- a/flutter_app/lib/app.dart +++ b/flutter_app/lib/app.dart @@ -66,6 +66,19 @@ class OpenClawApp extends StatelessWidget { } for (final deviceLocale in deviceLocales ?? const []) { + if (deviceLocale.languageCode == 'zh' && + deviceLocale.scriptCode == null) { + final country = deviceLocale.countryCode?.toUpperCase(); + if (country == 'TW' || country == 'HK' || country == 'MO') { + for (final supportedLocale in supportedLocales) { + if (supportedLocale.languageCode == 'zh' && + supportedLocale.scriptCode == 'Hant') { + return supportedLocale; + } + } + } + } + for (final supportedLocale in supportedLocales) { if (supportedLocale.languageCode == deviceLocale.languageCode && supportedLocale.scriptCode == deviceLocale.scriptCode && diff --git a/flutter_app/lib/l10n/app_strings_en.dart b/flutter_app/lib/l10n/app_strings_en.dart index 27ccc70..99a471e 100644 --- a/flutter_app/lib/l10n/app_strings_en.dart +++ b/flutter_app/lib/l10n/app_strings_en.dart @@ -217,6 +217,9 @@ const Map appStringsEn = { 'nodeCapabilitySensorsTitle': 'Sensors', 'nodeCapabilitySensorsSubtitle': 'Read accelerometer, gyroscope, magnetometer, barometer', + 'nodeCapabilitySerialTitle': 'Serial', + 'nodeCapabilitySerialSubtitle': + 'Bluetooth and USB serial communication', 'nodeDeviceInfo': 'Device info', 'nodeDeviceId': 'Device ID', 'nodeLogs': 'Node logs', diff --git a/flutter_app/lib/l10n/app_strings_ja.dart b/flutter_app/lib/l10n/app_strings_ja.dart index 85b7744..69c184f 100644 --- a/flutter_app/lib/l10n/app_strings_ja.dart +++ b/flutter_app/lib/l10n/app_strings_ja.dart @@ -39,58 +39,58 @@ const Map appStringsJa = { 'providersScreenIntro': 'プロバイダーを選択し、API キー、エンドポイント、モデルを設定してください。', 'providersStatusActive': '有効', 'providersStatusConfigured': '設定済み', - 'providerDetailApiKey': 'API Key', - 'providerDetailApiKeyEmpty': 'API key cannot be empty', - 'providerDetailEndpoint': 'API Base URL', - 'providerDetailEndpointHelper': 'Override the default endpoint if your account uses a custom or regional API base URL.', - 'providerDetailEndpointInvalid': 'Enter a valid absolute API base URL', - 'providerDetailModel': 'Model', - 'providerDetailModelEmpty': 'Model name cannot be empty', - 'providerDetailCustomModelAction': 'Custom...', - 'providerDetailCustomModelLabel': 'Custom model name', - 'providerDetailCustomModelHint': 'e.g. meta/llama-3.3-70b-instruct', - 'providerDetailSaveAction': 'Save & Activate', - 'providerDetailSaved': '{provider} configured and activated', - 'providerDetailSaveFailed': 'Failed to save: {error}', - 'providerDetailRemoveTitle': 'Remove {provider}?', - 'providerDetailRemoveBody': 'This will delete the API key, endpoint, and saved model for this provider.', - 'providerDetailRemoveAction': 'Remove', - 'providerDetailRemoveConfiguration': 'Remove Configuration', - 'providerDetailRemoved': '{provider} removed', - 'providerDetailRemoveFailed': 'Failed to remove: {error}', + 'providerDetailApiKey': 'API キー', + 'providerDetailApiKeyEmpty': 'API キーは必須です', + 'providerDetailEndpoint': 'API ベース URL', + 'providerDetailEndpointHelper': 'アカウントがカスタムまたはリージョン専用の API ベース URL を使う場合、既定のエンドポイントを上書きできます。', + 'providerDetailEndpointInvalid': '有効な絶対 API ベース URL を入力してください', + 'providerDetailModel': 'モデル', + 'providerDetailModelEmpty': 'モデル名は必須です', + 'providerDetailCustomModelAction': 'カスタム...', + 'providerDetailCustomModelLabel': 'カスタムモデル名', + 'providerDetailCustomModelHint': '例: meta/llama-3.3-70b-instruct', + 'providerDetailSaveAction': '保存して有効化', + 'providerDetailSaved': '{provider} を設定して有効化しました', + 'providerDetailSaveFailed': '保存に失敗しました: {error}', + 'providerDetailRemoveTitle': '{provider} を削除しますか?', + 'providerDetailRemoveBody': 'このプロバイダーに保存されている API キー、エンドポイント、モデルを削除します。', + 'providerDetailRemoveAction': '削除', + 'providerDetailRemoveConfiguration': '設定を削除', + 'providerDetailRemoved': '{provider} を削除しました', + 'providerDetailRemoveFailed': '削除に失敗しました: {error}', 'providerNameAnthropic': 'Anthropic', - 'providerDescriptionAnthropic': 'Claude models for advanced reasoning and coding', + 'providerDescriptionAnthropic': '高度な推論とコーディング向けの Claude モデル', 'providerNameOpenai': 'OpenAI', - 'providerDescriptionOpenai': 'GPT and o-series models', + 'providerDescriptionOpenai': 'GPT と o シリーズのモデル', 'providerNameQwen': 'Qwen', - 'providerDescriptionQwen': 'Alibaba Cloud Qwen models via DashScope OpenAI-compatible API', + 'providerDescriptionQwen': 'DashScope の OpenAI 互換 API 経由で利用できる Alibaba Cloud Qwen モデル', 'providerNameMinimax': 'MiniMax', - 'providerDescriptionMinimax': 'MiniMax chat models with editable API endpoint support', + 'providerDescriptionMinimax': 'API エンドポイントを編集可能な MiniMax チャットモデル', 'providerNameDoubao': 'Doubao', - 'providerDescriptionDoubao': 'Volcengine Ark / Doubao models with official Ark endpoint presets', + 'providerDescriptionDoubao': '公式 Ark エンドポイントのプリセット付き Volcengine Ark / Doubao モデル', 'providerNameGoogle': 'Google Gemini', - 'providerDescriptionGoogle': 'Gemini family of multimodal models', + 'providerDescriptionGoogle': 'Gemini マルチモーダルモデル群', 'providerNameOpenrouter': 'OpenRouter', - 'providerDescriptionOpenrouter': 'Unified API for hundreds of models', + 'providerDescriptionOpenrouter': '数百のモデルを統合的に使える API', 'providerNameNvidia': 'NVIDIA NIM', - 'providerDescriptionNvidia': 'GPU-optimized inference endpoints', + 'providerDescriptionNvidia': 'GPU 推論に最適化されたエンドポイント', 'providerNameDeepseek': 'DeepSeek', - 'providerDescriptionDeepseek': 'High-performance open models', + 'providerDescriptionDeepseek': '高性能なオープンモデル', 'providerNameXai': 'xAI', - 'providerDescriptionXai': 'Grok models from xAI', - 'dashboardPackagesTitle': 'Packages', - 'dashboardPackagesSubtitle': 'Install optional tools (Go, Homebrew, SSH)', - 'dashboardSshTitle': 'SSH Access', - 'dashboardSshSubtitle': 'Remote terminal access via SSH', - 'dashboardLogsTitle': 'Logs', - 'dashboardLogsSubtitle': 'View gateway output and errors', - 'dashboardSnapshotTitle': 'Snapshot', - 'dashboardSnapshotSubtitle': 'Backup or restore your config', - 'dashboardNodeTitle': 'Node', - 'dashboardNodeConnected': 'Connected to gateway', - 'dashboardNodeDisabled': 'Device capabilities for AI', + 'providerDescriptionXai': 'xAI の Grok モデル', + 'dashboardPackagesTitle': 'パッケージ', + 'dashboardPackagesSubtitle': 'オプションツールをインストール(Go、Homebrew、SSH)', + 'dashboardSshTitle': 'SSH アクセス', + 'dashboardSshSubtitle': 'SSH 経由でリモートターミナルにアクセス', + 'dashboardLogsTitle': 'ログ', + 'dashboardLogsSubtitle': 'ゲートウェイ出力とエラーを表示', + 'dashboardSnapshotTitle': 'スナップショット', + 'dashboardSnapshotSubtitle': '設定をバックアップまたは復元', + 'dashboardNodeTitle': 'ノード', + 'dashboardNodeConnected': 'ゲートウェイに接続済み', + 'dashboardNodeDisabled': 'AI にデバイス機能を提供', 'dashboardVersionLabel': 'OpenClaw v{version}', - 'dashboardAuthorLabel': 'by {author} | {org}', + 'dashboardAuthorLabel': '{author} | {org}', 'gatewayTitle': 'ゲートウェイ', 'gatewayCopyUrl': 'URL をコピー', 'gatewayUrlCopied': 'URL をクリップボードにコピーしました', @@ -114,153 +114,155 @@ const Map appStringsJa = { 'packagesDescription': 'Ubuntu 環境内にインストールできる開発ツールです。', 'packagesInstall': 'インストール', 'packagesUninstall': 'アンインストール', - 'packagesUninstallTitle': 'Uninstall {name}?', - 'packagesUninstallDescription': 'This will remove {name} from the environment.', - 'packageGoDescription': 'Go programming language compiler and tools', - 'packageBrewDescription': 'The missing package manager for Linux', - 'packageSshDescription': 'SSH client and server for secure remote access', + 'packagesUninstallTitle': '{name} をアンインストールしますか?', + 'packagesUninstallDescription': 'これにより環境から {name} が削除されます。', + 'packageGoDescription': 'Go プログラミング言語のコンパイラとツール', + 'packageBrewDescription': 'Linux 向けの定番パッケージマネージャー', + 'packageSshDescription': '安全なリモートアクセス用の SSH クライアント/サーバー', 'setupWizardTitle': 'OpenClaw セットアップ', - 'setupWizardIntroIdle': 'This will download Ubuntu, Node.js, and OpenClaw into a self-contained environment.', - 'setupWizardIntroRunning': 'Setting up the environment. This may take several minutes.', - 'setupWizardConfigureApiKeys': 'Configure API Keys', + 'setupWizardIntroIdle': 'Ubuntu、Node.js、OpenClaw を自己完結型の環境にダウンロードします。', + 'setupWizardIntroRunning': '環境をセットアップしています。数分かかる場合があります。', + 'setupWizardConfigureApiKeys': 'API キーを設定', 'setupWizardRetry': '再試行', 'setupWizardBegin': 'セットアップ開始', - 'setupWizardRequirements': 'Requires ~500MB of storage and an internet connection', - 'setupWizardStorageDialogTitle': 'Grant file access before setup', - 'setupWizardStorageDialogBody': 'OpenClaw setup needs file management access before installation starts, so the Ubuntu environment can mount shared storage correctly inside proot. The Android permission page will open next.', - 'setupWizardStoragePermissionRequired': 'File management access is required before setup can start.', - 'setupWizardOptionalPackages': 'OPTIONAL PACKAGES', - 'setupWizardStepDownloadRootfs': 'Download Ubuntu rootfs', - 'setupWizardStepExtractRootfs': 'Extract rootfs', - 'setupWizardStepInstallNode': 'Install Node.js', - 'setupWizardStepInstallOpenClaw': 'Install OpenClaw', - 'setupWizardStepConfigureBypass': 'Configure Bionic Bypass', + 'setupWizardRequirements': '約 500MB の空き容量とインターネット接続が必要です', + 'setupWizardStorageDialogTitle': 'セットアップ前にファイルアクセスを許可', + 'setupWizardStorageDialogBody': 'OpenClaw のセットアップ開始前に、ファイル管理アクセスが必要です。これにより Ubuntu 環境が proot 内で共有ストレージを正しくマウントできます。次に Android の権限ページが開きます。', + 'setupWizardStoragePermissionRequired': 'セットアップを開始する前にファイル管理アクセスが必要です。', + 'setupWizardOptionalPackages': 'オプションパッケージ', + 'setupWizardStepDownloadRootfs': 'Ubuntu rootfs をダウンロード', + 'setupWizardStepExtractRootfs': 'rootfs を展開', + 'setupWizardStepInstallNode': 'Node.js をインストール', + 'setupWizardStepInstallOpenClaw': 'OpenClaw をインストール', + 'setupWizardStepConfigureBypass': 'Bionic Bypass を設定', 'setupWizardComplete': 'セットアップ完了!', - 'setupWizardStatusSetupComplete': 'Setup complete', - 'setupWizardStatusSetupRequired': 'Setup required', - 'setupWizardStatusSettingUpDirs': 'Setting up directories...', - 'setupWizardStatusDownloadingUbuntuRootfs': 'Downloading Ubuntu rootfs...', - 'setupWizardStatusDownloadingProgress': 'Downloading: {current} MB / {total} MB', - 'setupWizardStatusExtractingRootfs': 'Extracting rootfs (this takes a while)...', - 'setupWizardStatusRootfsExtracted': 'Rootfs extracted', - 'setupWizardStatusFixingPermissions': 'Fixing rootfs permissions...', - 'setupWizardStatusUpdatingPackageLists': 'Updating package lists...', - 'setupWizardStatusInstallingBasePackages': 'Installing base packages...', - 'setupWizardStatusDownloadingNode': 'Downloading Node.js {version}...', - 'setupWizardStatusDownloadingNodeProgress': 'Downloading Node.js: {current} MB / {total} MB', - 'setupWizardStatusExtractingNode': 'Extracting Node.js...', - 'setupWizardStatusVerifyingNode': 'Verifying Node.js...', - 'setupWizardStatusNodeInstalled': 'Node.js installed', - 'setupWizardStatusInstallingOpenClaw': 'Installing OpenClaw (this may take a few minutes)...', - 'setupWizardStatusCreatingBinWrappers': 'Creating bin wrappers...', - 'setupWizardStatusVerifyingOpenClaw': 'Verifying OpenClaw...', - 'setupWizardStatusOpenClawInstalled': 'OpenClaw installed', - 'setupWizardStatusBypassConfigured': 'Bionic Bypass configured', - 'setupWizardStatusReady': 'Setup complete! Ready to start the gateway.', + 'setupWizardStatusSetupComplete': 'セットアップ完了', + 'setupWizardStatusSetupRequired': 'セットアップが必要です', + 'setupWizardStatusSettingUpDirs': 'ディレクトリを準備中...', + 'setupWizardStatusDownloadingUbuntuRootfs': 'Ubuntu rootfs をダウンロード中...', + 'setupWizardStatusDownloadingProgress': 'ダウンロード中: {current} MB / {total} MB', + 'setupWizardStatusExtractingRootfs': 'rootfs を展開中(少し時間がかかります)...', + 'setupWizardStatusRootfsExtracted': 'rootfs を展開しました', + 'setupWizardStatusFixingPermissions': 'rootfs の権限を修正中...', + 'setupWizardStatusUpdatingPackageLists': 'パッケージリストを更新中...', + 'setupWizardStatusInstallingBasePackages': '基本パッケージをインストール中...', + 'setupWizardStatusDownloadingNode': 'Node.js {version} をダウンロード中...', + 'setupWizardStatusDownloadingNodeProgress': 'Node.js をダウンロード中: {current} MB / {total} MB', + 'setupWizardStatusExtractingNode': 'Node.js を展開中...', + 'setupWizardStatusVerifyingNode': 'Node.js を検証中...', + 'setupWizardStatusNodeInstalled': 'Node.js をインストールしました', + 'setupWizardStatusInstallingOpenClaw': 'OpenClaw をインストール中(数分かかる場合があります)...', + 'setupWizardStatusCreatingBinWrappers': 'bin ラッパーを作成中...', + 'setupWizardStatusVerifyingOpenClaw': 'OpenClaw を検証中...', + 'setupWizardStatusOpenClawInstalled': 'OpenClaw をインストールしました', + 'setupWizardStatusBypassConfigured': 'Bionic Bypass を設定しました', + 'setupWizardStatusReady': 'セットアップ完了! ゲートウェイを起動できます。', 'onboardingTitle': 'OpenClaw オンボーディング', - 'onboardingStarting': 'Starting onboarding...', + 'onboardingStarting': 'オンボーディングを開始中...', 'onboardingGoToDashboard': 'ダッシュボードへ', - 'onboardingStartFailed': 'Failed to start onboarding: {error}', + 'onboardingStartFailed': 'オンボーディングの開始に失敗しました: {error}', 'configureTitle': 'OpenClaw 設定', - 'configureStarting': 'Starting configure...', - 'configureStartFailed': 'Failed to start configure: {error}', + 'configureStarting': '設定を開始中...', + 'configureStartFailed': '設定の開始に失敗しました: {error}', 'nodeTitle': 'ノード', - 'nodeConfigurationTitle': 'Node Configuration', - 'nodeGatewayConnection': 'Gateway connection', - 'nodeLocalGateway': 'Local Gateway', - 'nodeLocalGatewaySubtitle': 'Auto-pair with gateway on this device', - 'nodeRemoteGateway': 'Remote Gateway', - 'nodeRemoteGatewaySubtitle': 'Connect to a gateway on another device', - 'nodeGatewayHost': 'Gateway Host', - 'nodeGatewayPort': 'Gateway Port', - 'nodeGatewayToken': 'Gateway Token', - 'nodeGatewayTokenHint': 'Paste token from gateway dashboard URL', - 'nodeGatewayTokenHelper': 'Found in dashboard URL after #token=', - 'nodeConnect': 'Connect', - 'nodePairing': 'Pairing', - 'nodeApproveCode': 'Approve this code on the gateway:', - 'nodeCapabilities': 'Capabilities', - 'nodeCapabilityCameraTitle': 'Camera', - 'nodeCapabilityCameraSubtitle': 'Capture photos and video clips', + 'nodeConfigurationTitle': 'ノード設定', + 'nodeGatewayConnection': 'ゲートウェイ接続', + 'nodeLocalGateway': 'ローカルゲートウェイ', + 'nodeLocalGatewaySubtitle': 'このデバイス上のゲートウェイと自動ペアリング', + 'nodeRemoteGateway': 'リモートゲートウェイ', + 'nodeRemoteGatewaySubtitle': '別のデバイス上のゲートウェイに接続', + 'nodeGatewayHost': 'ゲートウェイホスト', + 'nodeGatewayPort': 'ゲートウェイポート', + 'nodeGatewayToken': 'ゲートウェイトークン', + 'nodeGatewayTokenHint': 'ゲートウェイダッシュボード URL のトークンを貼り付け', + 'nodeGatewayTokenHelper': 'ダッシュボード URL の #token= の後にあります', + 'nodeConnect': '接続', + 'nodePairing': 'ペアリング', + 'nodeApproveCode': 'ゲートウェイで次のコードを承認してください:', + 'nodeCapabilities': '機能', + 'nodeCapabilityCameraTitle': 'カメラ', + 'nodeCapabilityCameraSubtitle': '写真と動画クリップを撮影', 'nodeCapabilityCanvasTitle': 'Canvas', - 'nodeCapabilityCanvasSubtitle': 'Navigate and interact with web pages', - 'nodeCapabilityLocationTitle': 'Location', - 'nodeCapabilityLocationSubtitle': 'Get device GPS coordinates', - 'nodeCapabilityScreenTitle': 'Screen Recording', - 'nodeCapabilityScreenSubtitle': 'Record device screen (requires consent each time)', - 'nodeCapabilityFlashlightTitle': 'Flashlight', - 'nodeCapabilityFlashlightSubtitle': 'Toggle device torch on/off', - 'nodeCapabilityVibrationTitle': 'Vibration', - 'nodeCapabilityVibrationSubtitle': 'Trigger haptic feedback and vibration patterns', - 'nodeCapabilitySensorsTitle': 'Sensors', - 'nodeCapabilitySensorsSubtitle': 'Read accelerometer, gyroscope, magnetometer, barometer', - 'nodeDeviceInfo': 'Device info', - 'nodeDeviceId': 'Device ID', - 'nodeLogs': 'Node logs', - 'nodeNoLogs': 'No logs yet', - 'nodeConnectedTo': 'Connected to {host}:{port}', - 'nodePairingCode': 'Pairing code: ', - 'nodeEnable': 'Enable Node', - 'nodeDisable': 'Disable Node', - 'nodeReconnect': 'Reconnect', - 'nodeStatusPaired': 'Paired', - 'nodeStatusConnecting': 'Connecting', - 'nodeStatusError': 'Error', - 'nodeStatusDisabled': 'Disabled', - 'nodeStatusDisconnected': 'Disconnected', + 'nodeCapabilityCanvasSubtitle': 'Web ページを閲覧し操作', + 'nodeCapabilityLocationTitle': '位置情報', + 'nodeCapabilityLocationSubtitle': 'デバイスの GPS 座標を取得', + 'nodeCapabilityScreenTitle': '画面録画', + 'nodeCapabilityScreenSubtitle': 'デバイス画面を録画(毎回の同意が必要)', + 'nodeCapabilityFlashlightTitle': 'ライト', + 'nodeCapabilityFlashlightSubtitle': 'デバイスのライトをオン/オフ', + 'nodeCapabilityVibrationTitle': 'バイブレーション', + 'nodeCapabilityVibrationSubtitle': '触覚フィードバックや振動パターンを実行', + 'nodeCapabilitySensorsTitle': 'センサー', + 'nodeCapabilitySensorsSubtitle': '加速度計、ジャイロ、磁力計、気圧計を読み取り', + 'nodeCapabilitySerialTitle': 'シリアル', + 'nodeCapabilitySerialSubtitle': 'Bluetooth と USB のシリアル通信', + 'nodeDeviceInfo': 'デバイス情報', + 'nodeDeviceId': 'デバイス ID', + 'nodeLogs': 'ノードログ', + 'nodeNoLogs': 'まだログがありません', + 'nodeConnectedTo': '{host}:{port} に接続済み', + 'nodePairingCode': 'ペアリングコード: ', + 'nodeEnable': 'ノードを有効化', + 'nodeDisable': 'ノードを無効化', + 'nodeReconnect': '再接続', + 'nodeStatusPaired': 'ペアリング済み', + 'nodeStatusConnecting': '接続中', + 'nodeStatusError': 'エラー', + 'nodeStatusDisabled': '無効', + 'nodeStatusDisconnected': '未接続', 'settingsTitle': '設定', 'settingsGeneral': '一般', 'settingsAutoStart': 'ゲートウェイを自動起動', 'settingsAutoStartSubtitle': 'アプリ起動時にゲートウェイを開始', 'settingsBatteryOptimization': 'バッテリー最適化', - 'settingsBatteryOptimized': 'Optimized (may kill background sessions)', - 'settingsBatteryUnrestricted': 'Unrestricted (recommended)', + 'settingsBatteryOptimized': '最適化済み(バックグラウンドセッションが終了する場合があります)', + 'settingsBatteryUnrestricted': '制限なし(推奨)', 'settingsStorage': 'ストレージ設定', - 'settingsStorageGranted': 'Granted — /sdcard accessible in proot', - 'settingsStorageMissing': 'Allow access to shared storage', - 'settingsStorageDialogTitle': 'Grant file access', - 'settingsStorageDialogBody': 'OpenClaw needs file management access to read and write snapshot files in shared storage. You will be taken to the system settings page next.', - 'settingsStorageDialogAction': 'Continue', - 'onboardingStorageDialogTitle': 'Grant file access for onboarding', - 'onboardingStorageDialogBody': 'OpenClaw onboarding needs file management access so Ubuntu can mount shared storage inside proot while you configure API keys and binding. The Android permission page will open next.', - 'onboardingStoragePermissionRequired': 'File management access is required before onboarding can continue.', - 'configureStorageDialogTitle': 'Grant file access for configuration', - 'configureStorageDialogBody': 'OpenClaw configuration needs file management access so Ubuntu can mount shared storage inside proot while you manage gateway settings. The Android permission page will open next.', - 'configureStoragePermissionRequired': 'File management access is required before configuration can continue.', + 'settingsStorageGranted': '許可済み - proot で /sdcard にアクセス可能', + 'settingsStorageMissing': '共有ストレージへのアクセスを許可してください', + 'settingsStorageDialogTitle': 'ファイルアクセスを許可', + 'settingsStorageDialogBody': 'OpenClaw が共有ストレージ内のスナップショットを読み書きするには、ファイル管理アクセスが必要です。次にシステム設定ページを開きます。', + 'settingsStorageDialogAction': '続行', + 'onboardingStorageDialogTitle': 'オンボーディング用にファイルアクセスを許可', + 'onboardingStorageDialogBody': 'OpenClaw のオンボーディングでは、API キーやバインド設定中に Ubuntu が proot 内で共有ストレージをマウントできるよう、ファイル管理アクセスが必要です。次に Android の権限ページを開きます。', + 'onboardingStoragePermissionRequired': 'オンボーディングを続行する前にファイル管理アクセスが必要です。', + 'configureStorageDialogTitle': '設定用にファイルアクセスを許可', + 'configureStorageDialogBody': 'OpenClaw の設定では、ゲートウェイ設定の管理中に Ubuntu が proot 内で共有ストレージをマウントできるよう、ファイル管理アクセスが必要です。次に Android の権限ページを開きます。', + 'configureStoragePermissionRequired': '設定を続行する前にファイル管理アクセスが必要です。', 'settingsNode': 'ノード', - 'settingsEnableNode': 'Enable Node', - 'settingsEnableNodeSubtitle': 'Provide device capabilities to the gateway', - 'settingsNodeConfiguration': 'Node Configuration', - 'settingsNodeConfigurationSubtitle': 'Connection, pairing, and capabilities', + 'settingsEnableNode': 'ノードを有効化', + 'settingsEnableNodeSubtitle': 'ゲートウェイにデバイス機能を提供', + 'settingsNodeConfiguration': 'ノード設定', + 'settingsNodeConfigurationSubtitle': '接続、ペアリング、機能の設定', 'settingsSystemInfo': 'システム情報', - 'settingsArchitecture': 'Architecture', - 'settingsProotPath': 'PRoot path', + 'settingsArchitecture': 'アーキテクチャ', + 'settingsProotPath': 'PRoot パス', 'settingsRootfs': 'Rootfs', 'settingsNodeJs': 'Node.js', 'settingsOpenClaw': 'OpenClaw', - 'settingsGo': 'Go (Golang)', + 'settingsGo': 'Go(Golang)', 'settingsHomebrew': 'Homebrew', 'settingsOpenSsh': 'OpenSSH', 'settingsMaintenance': 'メンテナンス', - 'settingsExportSnapshot': 'Export Snapshot', - 'settingsExportSnapshotSubtitle': 'Backup config to Downloads', - 'settingsImportSnapshot': 'Import Snapshot', - 'settingsImportSnapshotSubtitle': 'Restore config from backup', - 'settingsRerunSetup': 'Re-run setup', - 'settingsRerunSetupSubtitle': 'Reinstall or repair the environment', + 'settingsExportSnapshot': 'スナップショットをエクスポート', + 'settingsExportSnapshotSubtitle': '設定を Downloads にバックアップ', + 'settingsImportSnapshot': 'スナップショットをインポート', + 'settingsImportSnapshotSubtitle': 'バックアップから設定を復元', + 'settingsRerunSetup': 'セットアップを再実行', + 'settingsRerunSetupSubtitle': '環境を再インストールまたは修復', 'settingsAbout': '情報', - 'settingsAboutSubtitle': 'AI Gateway for Android\nVersion {version}', + 'settingsAboutSubtitle': 'Android 向け AI ゲートウェイ\nバージョン {version}', 'settingsDeveloper': '開発者', 'settingsGithub': 'GitHub', 'settingsContact': '連絡先', 'settingsLicense': 'ライセンス', 'settingsPlayStore': 'Play ストア', 'settingsEmail': 'メール', - 'settingsSnapshotSaved': 'Snapshot saved to {path}', - 'settingsExportFailed': 'Export failed: {error}', - 'settingsSnapshotMissing': 'No snapshot found at {path}', - 'settingsSnapshotRestored': 'Snapshot restored successfully. Restart the gateway to apply.', - 'settingsImportFailed': 'Import failed: {error}', + 'settingsSnapshotSaved': 'スナップショットを {path} に保存しました', + 'settingsExportFailed': 'エクスポートに失敗しました: {error}', + 'settingsSnapshotMissing': '{path} にスナップショットが見つかりません', + 'settingsSnapshotRestored': 'スナップショットを復元しました。反映するにはゲートウェイを再起動してください。', + 'settingsImportFailed': 'インポートに失敗しました: {error}', 'statusInstalled': 'インストール済み', 'statusNotInstalled': '未インストール', }; diff --git a/flutter_app/lib/l10n/app_strings_zh_hans.dart b/flutter_app/lib/l10n/app_strings_zh_hans.dart index 8b9b2bc..bde2b47 100644 --- a/flutter_app/lib/l10n/app_strings_zh_hans.dart +++ b/flutter_app/lib/l10n/app_strings_zh_hans.dart @@ -197,6 +197,8 @@ const Map appStringsZhHans = { 'nodeCapabilityVibrationSubtitle': '触发触觉反馈和震动模式', 'nodeCapabilitySensorsTitle': '传感器', 'nodeCapabilitySensorsSubtitle': '读取加速度计、陀螺仪、磁力计、气压计', + 'nodeCapabilitySerialTitle': '串口', + 'nodeCapabilitySerialSubtitle': '蓝牙和 USB 串口通信', 'nodeDeviceInfo': '设备信息', 'nodeDeviceId': '设备 ID', 'nodeLogs': '节点日志', diff --git a/flutter_app/lib/l10n/app_strings_zh_hant.dart b/flutter_app/lib/l10n/app_strings_zh_hant.dart index 7deabe7..7ce87c7 100644 --- a/flutter_app/lib/l10n/app_strings_zh_hant.dart +++ b/flutter_app/lib/l10n/app_strings_zh_hant.dart @@ -143,7 +143,7 @@ const Map appStringsZhHant = { 'setupWizardStatusDownloadingProgress': '正在下載:{current} MB / {total} MB', 'setupWizardStatusExtractingRootfs': '正在解壓 rootfs(這會花一點時間)...', 'setupWizardStatusRootfsExtracted': 'rootfs 已解壓', - 'setupWizardStatusFixingPermissions': '正在修複 rootfs 權限...', + 'setupWizardStatusFixingPermissions': '正在修復 rootfs 權限...', 'setupWizardStatusUpdatingPackageLists': '正在更新軟體包列表...', 'setupWizardStatusInstallingBasePackages': '正在安裝基礎軟體包...', 'setupWizardStatusDownloadingNode': '正在下載 Node.js {version}...', @@ -187,13 +187,15 @@ const Map appStringsZhHant = { 'nodeCapabilityLocationTitle': '定位', 'nodeCapabilityLocationSubtitle': '獲取設備 GPS 坐標', 'nodeCapabilityScreenTitle': '錄屏', - 'nodeCapabilityScreenSubtitle': '錄制設備螢幕(每次都需授權)', + 'nodeCapabilityScreenSubtitle': '錄製設備螢幕(每次都需授權)', 'nodeCapabilityFlashlightTitle': '手電筒', 'nodeCapabilityFlashlightSubtitle': '切換設備閃光燈開關', 'nodeCapabilityVibrationTitle': '震動', 'nodeCapabilityVibrationSubtitle': '觸發觸覺回饋和震動模式', 'nodeCapabilitySensorsTitle': '感測器', 'nodeCapabilitySensorsSubtitle': '讀取加速度計、陀螺儀、磁力計、氣壓計', + 'nodeCapabilitySerialTitle': '串口', + 'nodeCapabilitySerialSubtitle': '藍牙與 USB 串口通訊', 'nodeDeviceInfo': '設備資訊', 'nodeDeviceId': '設備 ID', 'nodeLogs': '節點日誌', @@ -247,7 +249,7 @@ const Map appStringsZhHant = { 'settingsImportSnapshot': '匯入快照', 'settingsImportSnapshotSubtitle': '從備份恢復配置', 'settingsRerunSetup': '重新執行安裝', - 'settingsRerunSetupSubtitle': '重新安裝或修複環境', + 'settingsRerunSetupSubtitle': '重新安裝或修復環境', 'settingsAbout': '關於', 'settingsAboutSubtitle': 'Android AI 網關\n版本 {version}', 'settingsDeveloper': '開發者', diff --git a/flutter_app/lib/screens/configure_screen.dart b/flutter_app/lib/screens/configure_screen.dart index 5e3aa1d..957bf06 100644 --- a/flutter_app/lib/screens/configure_screen.dart +++ b/flutter_app/lib/screens/configure_screen.dart @@ -142,11 +142,14 @@ class _ConfigureScreenState extends State { _pty?.resize(h, w); }; + if (!mounted) return; setState(() => _loading = false); } catch (e) { + if (!mounted) return; + final message = context.l10n.t('configureStartFailed', {'error': '$e'}); setState(() { _loading = false; - _error = context.l10n.t('configureStartFailed', {'error': '$e'}); + _error = message; }); } } diff --git a/flutter_app/lib/screens/node_screen.dart b/flutter_app/lib/screens/node_screen.dart index da6d34c..eee4f68 100644 --- a/flutter_app/lib/screens/node_screen.dart +++ b/flutter_app/lib/screens/node_screen.dart @@ -229,8 +229,8 @@ class _NodeScreenState extends State { ), _capabilityTile( theme, - 'Serial', - 'Bluetooth and USB serial communication', + l10n.t('nodeCapabilitySerialTitle'), + l10n.t('nodeCapabilitySerialSubtitle'), Icons.usb, ), const SizedBox(height: 16), diff --git a/flutter_app/lib/screens/onboarding_screen.dart b/flutter_app/lib/screens/onboarding_screen.dart index c8cc953..6627d47 100644 --- a/flutter_app/lib/screens/onboarding_screen.dart +++ b/flutter_app/lib/screens/onboarding_screen.dart @@ -195,11 +195,14 @@ class _OnboardingScreenState extends State { _pty?.resize(h, w); }; + if (!mounted) return; setState(() => _loading = false); } catch (e) { + if (!mounted) return; + final message = context.l10n.t('onboardingStartFailed', {'error': '$e'}); setState(() { _loading = false; - _error = context.l10n.t('onboardingStartFailed', {'error': '$e'}); + _error = message; }); } } diff --git a/flutter_app/lib/screens/setup_wizard_screen.dart b/flutter_app/lib/screens/setup_wizard_screen.dart index b4a1631..46eb17e 100644 --- a/flutter_app/lib/screens/setup_wizard_screen.dart +++ b/flutter_app/lib/screens/setup_wizard_screen.dart @@ -21,7 +21,6 @@ class SetupWizardScreen extends StatefulWidget { class _SetupWizardScreenState extends State { bool _started = false; Map _pkgStatuses = {}; - String? _permissionError; Future _refreshPkgStatuses() async { final statuses = await PackageService.checkAllStatuses(); @@ -40,9 +39,8 @@ class _SetupWizardScreenState extends State { Future _beginSetup(SetupProvider provider) async { setState(() { _started = true; - _permissionError = null; }); - provider.runSetup(); + await provider.runSetup(); } @override @@ -93,7 +91,7 @@ class _SetupWizardScreenState extends State { Expanded( child: _buildSteps(state, theme, isDark, l10n), ), - if (state.hasError || _permissionError != null) ...[ + if (state.hasError) ...[ ConstrainedBox( constraints: const BoxConstraints(maxHeight: 160), child: Container( @@ -111,9 +109,7 @@ class _SetupWizardScreenState extends State { Expanded( child: SingleChildScrollView( child: Text( - _permissionError ?? - state.error ?? - 'Unknown error', + state.error ?? 'Unknown error', style: TextStyle( color: theme.colorScheme.onErrorContainer), From 81791f7fc9879f91f04ef03537c912364bcaa6e8 Mon Sep 17 00:00:00 2001 From: TIANLI0 <507249007@qq.com> Date: Mon, 9 Mar 2026 01:19:10 +0800 Subject: [PATCH 7/9] i18n: localize dashboard and node serial capability text --- flutter_app/lib/l10n/app_strings_en.dart | 588 +++++++++--------- flutter_app/lib/l10n/app_strings_ja.dart | 77 ++- flutter_app/lib/l10n/app_strings_zh_hans.dart | 541 ++++++++-------- flutter_app/lib/l10n/app_strings_zh_hant.dart | 17 +- flutter_app/lib/screens/dashboard_screen.dart | 4 +- flutter_app/lib/screens/node_screen.dart | 4 +- 6 files changed, 632 insertions(+), 599 deletions(-) diff --git a/flutter_app/lib/l10n/app_strings_en.dart b/flutter_app/lib/l10n/app_strings_en.dart index 27ccc70..0c2f855 100644 --- a/flutter_app/lib/l10n/app_strings_en.dart +++ b/flutter_app/lib/l10n/app_strings_en.dart @@ -1,297 +1,295 @@ const Map appStringsEn = { - - 'appName': 'OpenClaw', - 'language': 'Language', - 'languageSystem': 'System default', - 'languageEnglish': 'English', - 'languageChinese': 'Simplified Chinese', - 'languageTraditionalChinese': 'Traditional Chinese', - 'languageJapanese': 'Japanese', - 'commonInstalled': 'Installed', - 'commonNotInstalled': 'Not installed', - 'commonCancel': 'Cancel', - 'commonCopy': 'Copy', - 'commonCopiedToClipboard': 'Copied to clipboard', - 'commonOpen': 'Open', - 'commonPaste': 'Paste', - 'commonRetry': 'Retry', - 'commonDone': 'Done', - 'commonConfigure': 'Configure', - 'commonScreenshot': 'Screenshot', - 'commonSaveFailed': 'Failed to capture screenshot', - 'commonScreenshotSaved': 'Screenshot saved: {fileName}', - 'commonNoUrlFound': 'No URL found in selection', - 'commonOpenLink': 'Open Link', - 'commonLinkCopied': 'Link copied', - 'dashboardQuickActions': 'Quick actions', - 'dashboardTerminalTitle': 'Terminal', - 'dashboardTerminalSubtitle': 'Open Ubuntu shell with OpenClaw', - 'dashboardWebDashboardTitle': 'Web Dashboard', - 'dashboardWebDashboardSubtitle': 'Open OpenClaw dashboard in browser', - 'dashboardStartGatewayFirst': 'Start gateway first', - 'dashboardOnboardingTitle': 'Onboarding', - 'dashboardOnboardingSubtitle': 'Configure API keys and binding', - 'dashboardConfigureTitle': 'Configure', - 'dashboardConfigureSubtitle': 'Manage gateway settings', - 'dashboardProvidersTitle': 'AI Providers', - 'dashboardProvidersSubtitle': 'Configure models and API keys', - 'providersScreenTitle': 'AI Providers', - 'providersScreenActiveModel': 'Active Model', - 'providersScreenIntro': - 'Select a provider to configure its API key, endpoint, and model.', - 'providersStatusActive': 'Active', - 'providersStatusConfigured': 'Configured', - 'providerDetailApiKey': 'API Key', - 'providerDetailApiKeyEmpty': 'API key cannot be empty', - 'providerDetailEndpoint': 'API Base URL', - 'providerDetailEndpointHelper': - 'Override the default endpoint if your account uses a custom or regional API base URL.', - 'providerDetailEndpointInvalid': 'Enter a valid absolute API base URL', - 'providerDetailModel': 'Model', - 'providerDetailModelEmpty': 'Model name cannot be empty', - 'providerDetailCustomModelAction': 'Custom...', - 'providerDetailCustomModelLabel': 'Custom model name', - 'providerDetailCustomModelHint': 'e.g. meta/llama-3.3-70b-instruct', - 'providerDetailSaveAction': 'Save & Activate', - 'providerDetailSaved': '{provider} configured and activated', - 'providerDetailSaveFailed': 'Failed to save: {error}', - 'providerDetailRemoveTitle': 'Remove {provider}?', - 'providerDetailRemoveBody': - 'This will delete the API key, endpoint, and saved model for this provider.', - 'providerDetailRemoveAction': 'Remove', - 'providerDetailRemoveConfiguration': 'Remove Configuration', - 'providerDetailRemoved': '{provider} removed', - 'providerDetailRemoveFailed': 'Failed to remove: {error}', - 'providerNameAnthropic': 'Anthropic', - 'providerDescriptionAnthropic': - 'Claude models for advanced reasoning and coding', - 'providerNameOpenai': 'OpenAI', - 'providerDescriptionOpenai': 'GPT and o-series models', - 'providerNameQwen': 'Qwen', - 'providerDescriptionQwen': - 'Alibaba Cloud Qwen models via DashScope OpenAI-compatible API', - 'providerNameMinimax': 'MiniMax', - 'providerDescriptionMinimax': - 'MiniMax chat models with editable API endpoint support', - 'providerNameDoubao': 'Doubao', - 'providerDescriptionDoubao': - 'Volcengine Ark / Doubao models with official Ark endpoint presets', - 'providerNameGoogle': 'Google Gemini', - 'providerDescriptionGoogle': 'Gemini family of multimodal models', - 'providerNameOpenrouter': 'OpenRouter', - 'providerDescriptionOpenrouter': 'Unified API for hundreds of models', - 'providerNameNvidia': 'NVIDIA NIM', - 'providerDescriptionNvidia': 'GPU-optimized inference endpoints', - 'providerNameDeepseek': 'DeepSeek', - 'providerDescriptionDeepseek': 'High-performance open models', - 'providerNameXai': 'xAI', - 'providerDescriptionXai': 'Grok models from xAI', - 'dashboardPackagesTitle': 'Packages', - 'dashboardPackagesSubtitle': 'Install optional tools (Go, Homebrew, SSH)', - 'dashboardSshTitle': 'SSH Access', - 'dashboardSshSubtitle': 'Remote terminal access via SSH', - 'dashboardLogsTitle': 'Logs', - 'dashboardLogsSubtitle': 'View gateway output and errors', - 'dashboardSnapshotTitle': 'Snapshot', - 'dashboardSnapshotSubtitle': 'Backup or restore your config', - 'dashboardNodeTitle': 'Node', - 'dashboardNodeConnected': 'Connected to gateway', - 'dashboardNodeDisabled': 'Device capabilities for AI', - 'dashboardVersionLabel': 'OpenClaw v{version}', - 'dashboardAuthorLabel': 'by {author} | {org}', - 'gatewayTitle': 'Gateway', - 'gatewayCopyUrl': 'Copy URL', - 'gatewayUrlCopied': 'URL copied to clipboard', - 'gatewayOpenDashboard': 'Open dashboard', - 'gatewayStart': 'Start Gateway', - 'gatewayStop': 'Stop Gateway', - 'gatewayViewLogs': 'View Logs', - 'gatewayStatusRunning': 'Running', - 'gatewayStatusStarting': 'Starting', - 'gatewayStatusError': 'Error', - 'gatewayStatusStopped': 'Stopped', - 'logsTitle': 'Gateway Logs', - 'logsAutoScrollOn': 'Auto-scroll on', - 'logsAutoScrollOff': 'Auto-scroll off', - 'logsCopyAll': 'Copy all logs', - 'logsFilterHint': 'Filter logs...', - 'logsEmpty': 'No logs yet. Start the gateway.', - 'logsNoMatch': 'No matching logs.', - 'logsCopied': 'Logs copied to clipboard', - 'packagesTitle': 'Optional Packages', - 'packagesDescription': - 'Development tools you can install inside the Ubuntu environment.', - 'packagesInstall': 'Install', - 'packagesUninstall': 'Uninstall', - 'packagesUninstallTitle': 'Uninstall {name}?', - 'packagesUninstallDescription': - 'This will remove {name} from the environment.', - 'packageGoDescription': 'Go programming language compiler and tools', - 'packageBrewDescription': 'The missing package manager for Linux', - 'packageSshDescription': 'SSH client and server for secure remote access', - 'setupWizardTitle': 'Setup OpenClaw', - 'setupWizardIntroIdle': - 'This will download Ubuntu, Node.js, and OpenClaw into a self-contained environment.', - 'setupWizardIntroRunning': - 'Setting up the environment. This may take several minutes.', - 'setupWizardConfigureApiKeys': 'Configure API Keys', - 'setupWizardRetry': 'Retry Setup', - 'setupWizardBegin': 'Begin Setup', - 'setupWizardRequirements': - 'Requires ~500MB of storage and an internet connection', - 'setupWizardStorageDialogTitle': 'Grant file access before setup', - 'setupWizardStorageDialogBody': - 'OpenClaw setup needs file management access before installation starts, so the Ubuntu environment can mount shared storage correctly inside proot. The Android permission page will open next.', - 'setupWizardStoragePermissionRequired': - 'File management access is required before setup can start.', - 'setupWizardOptionalPackages': 'OPTIONAL PACKAGES', - 'setupWizardStepDownloadRootfs': 'Download Ubuntu rootfs', - 'setupWizardStepExtractRootfs': 'Extract rootfs', - 'setupWizardStepInstallNode': 'Install Node.js', - 'setupWizardStepInstallOpenClaw': 'Install OpenClaw', - 'setupWizardStepConfigureBypass': 'Configure Bionic Bypass', - 'setupWizardComplete': 'Setup complete!', - 'setupWizardStatusSetupComplete': 'Setup complete', - 'setupWizardStatusSetupRequired': 'Setup required', - 'setupWizardStatusSettingUpDirs': 'Setting up directories...', - 'setupWizardStatusDownloadingUbuntuRootfs': - 'Downloading Ubuntu rootfs...', - 'setupWizardStatusDownloadingProgress': - 'Downloading: {current} MB / {total} MB', - 'setupWizardStatusExtractingRootfs': - 'Extracting rootfs (this takes a while)...', - 'setupWizardStatusRootfsExtracted': 'Rootfs extracted', - 'setupWizardStatusFixingPermissions': 'Fixing rootfs permissions...', - 'setupWizardStatusUpdatingPackageLists': 'Updating package lists...', - 'setupWizardStatusInstallingBasePackages': 'Installing base packages...', - 'setupWizardStatusDownloadingNode': 'Downloading Node.js {version}...', - 'setupWizardStatusDownloadingNodeProgress': - 'Downloading Node.js: {current} MB / {total} MB', - 'setupWizardStatusExtractingNode': 'Extracting Node.js...', - 'setupWizardStatusVerifyingNode': 'Verifying Node.js...', - 'setupWizardStatusNodeInstalled': 'Node.js installed', - 'setupWizardStatusInstallingOpenClaw': - 'Installing OpenClaw (this may take a few minutes)...', - 'setupWizardStatusCreatingBinWrappers': 'Creating bin wrappers...', - 'setupWizardStatusVerifyingOpenClaw': 'Verifying OpenClaw...', - 'setupWizardStatusOpenClawInstalled': 'OpenClaw installed', - 'setupWizardStatusBypassConfigured': 'Bionic Bypass configured', - 'setupWizardStatusReady': 'Setup complete! Ready to start the gateway.', - 'onboardingTitle': 'OpenClaw Onboarding', - 'onboardingStarting': 'Starting onboarding...', - 'onboardingGoToDashboard': 'Go to Dashboard', - 'onboardingStartFailed': 'Failed to start onboarding: {error}', - 'configureTitle': 'OpenClaw Configure', - 'configureStarting': 'Starting configure...', - 'configureStartFailed': 'Failed to start configure: {error}', - 'nodeTitle': 'Node', - 'nodeConfigurationTitle': 'Node Configuration', - 'nodeGatewayConnection': 'Gateway connection', - 'nodeLocalGateway': 'Local Gateway', - 'nodeLocalGatewaySubtitle': 'Auto-pair with gateway on this device', - 'nodeRemoteGateway': 'Remote Gateway', - 'nodeRemoteGatewaySubtitle': 'Connect to a gateway on another device', - 'nodeGatewayHost': 'Gateway Host', - 'nodeGatewayPort': 'Gateway Port', - 'nodeGatewayToken': 'Gateway Token', - 'nodeGatewayTokenHint': 'Paste token from gateway dashboard URL', - 'nodeGatewayTokenHelper': 'Found in dashboard URL after #token=', - 'nodeConnect': 'Connect', - 'nodePairing': 'Pairing', - 'nodeApproveCode': 'Approve this code on the gateway:', - 'nodeCapabilities': 'Capabilities', - 'nodeCapabilityCameraTitle': 'Camera', - 'nodeCapabilityCameraSubtitle': 'Capture photos and video clips', - 'nodeCapabilityCanvasTitle': 'Canvas', - 'nodeCapabilityCanvasSubtitle': 'Navigate and interact with web pages', - 'nodeCapabilityLocationTitle': 'Location', - 'nodeCapabilityLocationSubtitle': 'Get device GPS coordinates', - 'nodeCapabilityScreenTitle': 'Screen Recording', - 'nodeCapabilityScreenSubtitle': - 'Record device screen (requires consent each time)', - 'nodeCapabilityFlashlightTitle': 'Flashlight', - 'nodeCapabilityFlashlightSubtitle': 'Toggle device torch on/off', - 'nodeCapabilityVibrationTitle': 'Vibration', - 'nodeCapabilityVibrationSubtitle': - 'Trigger haptic feedback and vibration patterns', - 'nodeCapabilitySensorsTitle': 'Sensors', - 'nodeCapabilitySensorsSubtitle': - 'Read accelerometer, gyroscope, magnetometer, barometer', - 'nodeDeviceInfo': 'Device info', - 'nodeDeviceId': 'Device ID', - 'nodeLogs': 'Node logs', - 'nodeNoLogs': 'No logs yet', - 'nodeConnectedTo': 'Connected to {host}:{port}', - 'nodePairingCode': 'Pairing code: ', - 'nodeEnable': 'Enable Node', - 'nodeDisable': 'Disable Node', - 'nodeReconnect': 'Reconnect', - 'nodeStatusPaired': 'Paired', - 'nodeStatusConnecting': 'Connecting', - 'nodeStatusError': 'Error', - 'nodeStatusDisabled': 'Disabled', - 'nodeStatusDisconnected': 'Disconnected', - 'settingsTitle': 'Settings', - 'settingsGeneral': 'General', - 'settingsAutoStart': 'Auto-start gateway', - 'settingsAutoStartSubtitle': 'Start the gateway when the app opens', - 'settingsBatteryOptimization': 'Battery Optimization', - 'settingsBatteryOptimized': 'Optimized (may kill background sessions)', - 'settingsBatteryUnrestricted': 'Unrestricted (recommended)', - 'settingsStorage': 'Setup Storage', - 'settingsStorageGranted': 'Granted — /sdcard accessible in proot', - 'settingsStorageMissing': 'Allow access to shared storage', - 'settingsStorageDialogTitle': 'Grant file access', - 'settingsStorageDialogBody': - 'OpenClaw needs file management access to read and write snapshot files in shared storage. You will be taken to the system settings page next.', - 'settingsStorageDialogAction': 'Continue', - 'onboardingStorageDialogTitle': 'Grant file access for onboarding', - 'onboardingStorageDialogBody': - 'OpenClaw onboarding needs file management access so Ubuntu can mount shared storage inside proot while you configure API keys and binding. The Android permission page will open next.', - 'onboardingStoragePermissionRequired': - 'File management access is required before onboarding can continue.', - 'configureStorageDialogTitle': 'Grant file access for configuration', - 'configureStorageDialogBody': - 'OpenClaw configuration needs file management access so Ubuntu can mount shared storage inside proot while you manage gateway settings. The Android permission page will open next.', - 'configureStoragePermissionRequired': - 'File management access is required before configuration can continue.', - 'settingsNode': 'Node', - 'settingsEnableNode': 'Enable Node', - 'settingsEnableNodeSubtitle': - 'Provide device capabilities to the gateway', - 'settingsNodeConfiguration': 'Node Configuration', - 'settingsNodeConfigurationSubtitle': - 'Connection, pairing, and capabilities', - 'settingsSystemInfo': 'System info', - 'settingsArchitecture': 'Architecture', - 'settingsProotPath': 'PRoot path', - 'settingsRootfs': 'Rootfs', - 'settingsNodeJs': 'Node.js', - 'settingsOpenClaw': 'OpenClaw', - 'settingsGo': 'Go (Golang)', - 'settingsHomebrew': 'Homebrew', - 'settingsOpenSsh': 'OpenSSH', - 'settingsMaintenance': 'Maintenance', - 'settingsExportSnapshot': 'Export Snapshot', - 'settingsExportSnapshotSubtitle': 'Backup config to Downloads', - 'settingsImportSnapshot': 'Import Snapshot', - 'settingsImportSnapshotSubtitle': 'Restore config from backup', - 'settingsRerunSetup': 'Re-run setup', - 'settingsRerunSetupSubtitle': 'Reinstall or repair the environment', - 'settingsAbout': 'About', - 'settingsAboutSubtitle': 'AI Gateway for Android\nVersion {version}', - 'settingsDeveloper': 'Developer', - 'settingsGithub': 'GitHub', - 'settingsContact': 'Contact', - 'settingsLicense': 'License', - 'settingsPlayStore': 'Play Store', - 'settingsEmail': 'Email', - 'settingsSnapshotSaved': 'Snapshot saved to {path}', - 'settingsExportFailed': 'Export failed: {error}', - 'settingsSnapshotMissing': 'No snapshot found at {path}', - 'settingsSnapshotRestored': - 'Snapshot restored successfully. Restart the gateway to apply.', - 'settingsImportFailed': 'Import failed: {error}', - 'statusInstalled': 'Installed', - 'statusNotInstalled': 'Not installed', + 'appName': 'OpenClaw', + 'language': 'Language', + 'languageSystem': 'System default', + 'languageEnglish': 'English', + 'languageChinese': 'Simplified Chinese', + 'languageTraditionalChinese': 'Traditional Chinese', + 'languageJapanese': 'Japanese', + 'commonInstalled': 'Installed', + 'commonNotInstalled': 'Not installed', + 'commonCancel': 'Cancel', + 'commonCopy': 'Copy', + 'commonCopiedToClipboard': 'Copied to clipboard', + 'commonOpen': 'Open', + 'commonPaste': 'Paste', + 'commonRetry': 'Retry', + 'commonDone': 'Done', + 'commonConfigure': 'Configure', + 'commonScreenshot': 'Screenshot', + 'commonSaveFailed': 'Failed to capture screenshot', + 'commonScreenshotSaved': 'Screenshot saved: {fileName}', + 'commonNoUrlFound': 'No URL found in selection', + 'commonOpenLink': 'Open Link', + 'commonLinkCopied': 'Link copied', + 'dashboardQuickActions': 'Quick actions', + 'dashboardTerminalTitle': 'Terminal', + 'dashboardTerminalSubtitle': 'Open Ubuntu shell with OpenClaw', + 'dashboardWebDashboardTitle': 'Web Dashboard', + 'dashboardWebDashboardSubtitle': 'Open OpenClaw dashboard in browser', + 'dashboardStartGatewayFirst': 'Start gateway first', + 'dashboardOnboardingTitle': 'Onboarding', + 'dashboardOnboardingSubtitle': 'Configure API keys and binding', + 'dashboardConfigureTitle': 'Configure', + 'dashboardConfigureSubtitle': 'Manage gateway settings', + 'dashboardProvidersTitle': 'AI Providers', + 'dashboardProvidersSubtitle': 'Configure models and API keys', + 'providersScreenTitle': 'AI Providers', + 'providersScreenActiveModel': 'Active Model', + 'providersScreenIntro': + 'Select a provider to configure its API key, endpoint, and model.', + 'providersStatusActive': 'Active', + 'providersStatusConfigured': 'Configured', + 'providerDetailApiKey': 'API Key', + 'providerDetailApiKeyEmpty': 'API key cannot be empty', + 'providerDetailEndpoint': 'API Base URL', + 'providerDetailEndpointHelper': + 'Override the default endpoint if your account uses a custom or regional API base URL.', + 'providerDetailEndpointInvalid': 'Enter a valid absolute API base URL', + 'providerDetailModel': 'Model', + 'providerDetailModelEmpty': 'Model name cannot be empty', + 'providerDetailCustomModelAction': 'Custom...', + 'providerDetailCustomModelLabel': 'Custom model name', + 'providerDetailCustomModelHint': 'e.g. meta/llama-3.3-70b-instruct', + 'providerDetailSaveAction': 'Save & Activate', + 'providerDetailSaved': '{provider} configured and activated', + 'providerDetailSaveFailed': 'Failed to save: {error}', + 'providerDetailRemoveTitle': 'Remove {provider}?', + 'providerDetailRemoveBody': + 'This will delete the API key, endpoint, and saved model for this provider.', + 'providerDetailRemoveAction': 'Remove', + 'providerDetailRemoveConfiguration': 'Remove Configuration', + 'providerDetailRemoved': '{provider} removed', + 'providerDetailRemoveFailed': 'Failed to remove: {error}', + 'providerNameAnthropic': 'Anthropic', + 'providerDescriptionAnthropic': + 'Claude models for advanced reasoning and coding', + 'providerNameOpenai': 'OpenAI', + 'providerDescriptionOpenai': 'GPT and o-series models', + 'providerNameQwen': 'Qwen', + 'providerDescriptionQwen': + 'Alibaba Cloud Qwen models via DashScope OpenAI-compatible API', + 'providerNameMinimax': 'MiniMax', + 'providerDescriptionMinimax': + 'MiniMax chat models with editable API endpoint support', + 'providerNameDoubao': 'Doubao', + 'providerDescriptionDoubao': + 'Volcengine Ark / Doubao models with official Ark endpoint presets', + 'providerNameGoogle': 'Google Gemini', + 'providerDescriptionGoogle': 'Gemini family of multimodal models', + 'providerNameOpenrouter': 'OpenRouter', + 'providerDescriptionOpenrouter': 'Unified API for hundreds of models', + 'providerNameNvidia': 'NVIDIA NIM', + 'providerDescriptionNvidia': 'GPU-optimized inference endpoints', + 'providerNameDeepseek': 'DeepSeek', + 'providerDescriptionDeepseek': 'High-performance open models', + 'providerNameXai': 'xAI', + 'providerDescriptionXai': 'Grok models from xAI', + 'dashboardPackagesTitle': 'Packages', + 'dashboardPackagesSubtitle': 'Install optional tools (Go, Homebrew, SSH)', + 'dashboardSshTitle': 'SSH Access', + 'dashboardSshSubtitle': 'Remote terminal access via SSH', + 'dashboardLogsTitle': 'Logs', + 'dashboardLogsSubtitle': 'View gateway output and errors', + 'dashboardSnapshotTitle': 'Snapshot', + 'dashboardSnapshotSubtitle': 'Backup or restore your config', + 'dashboardNodeTitle': 'Node', + 'dashboardNodeConnected': 'Connected to gateway', + 'dashboardNodeDisabled': 'Device capabilities for AI', + 'dashboardVersionLabel': 'OpenClaw v{version}', + 'dashboardAuthorLabel': 'by {author} | {org}', + 'gatewayTitle': 'Gateway', + 'gatewayCopyUrl': 'Copy URL', + 'gatewayUrlCopied': 'URL copied to clipboard', + 'gatewayOpenDashboard': 'Open dashboard', + 'gatewayStart': 'Start Gateway', + 'gatewayStop': 'Stop Gateway', + 'gatewayViewLogs': 'View Logs', + 'gatewayStatusRunning': 'Running', + 'gatewayStatusStarting': 'Starting', + 'gatewayStatusError': 'Error', + 'gatewayStatusStopped': 'Stopped', + 'logsTitle': 'Gateway Logs', + 'logsAutoScrollOn': 'Auto-scroll on', + 'logsAutoScrollOff': 'Auto-scroll off', + 'logsCopyAll': 'Copy all logs', + 'logsFilterHint': 'Filter logs...', + 'logsEmpty': 'No logs yet. Start the gateway.', + 'logsNoMatch': 'No matching logs.', + 'logsCopied': 'Logs copied to clipboard', + 'packagesTitle': 'Optional Packages', + 'packagesDescription': + 'Development tools you can install inside the Ubuntu environment.', + 'packagesInstall': 'Install', + 'packagesUninstall': 'Uninstall', + 'packagesUninstallTitle': 'Uninstall {name}?', + 'packagesUninstallDescription': + 'This will remove {name} from the environment.', + 'packageGoDescription': 'Go programming language compiler and tools', + 'packageBrewDescription': 'The missing package manager for Linux', + 'packageSshDescription': 'SSH client and server for secure remote access', + 'setupWizardTitle': 'Setup OpenClaw', + 'setupWizardIntroIdle': + 'This will download Ubuntu, Node.js, and OpenClaw into a self-contained environment.', + 'setupWizardIntroRunning': + 'Setting up the environment. This may take several minutes.', + 'setupWizardConfigureApiKeys': 'Configure API Keys', + 'setupWizardRetry': 'Retry Setup', + 'setupWizardBegin': 'Begin Setup', + 'setupWizardRequirements': + 'Requires ~500MB of storage and an internet connection', + 'setupWizardStorageDialogTitle': 'Grant file access before setup', + 'setupWizardStorageDialogBody': + 'OpenClaw setup needs file management access before installation starts, so the Ubuntu environment can mount shared storage correctly inside proot. The Android permission page will open next.', + 'setupWizardStoragePermissionRequired': + 'File management access is required before setup can start.', + 'setupWizardOptionalPackages': 'OPTIONAL PACKAGES', + 'setupWizardStepDownloadRootfs': 'Download Ubuntu rootfs', + 'setupWizardStepExtractRootfs': 'Extract rootfs', + 'setupWizardStepInstallNode': 'Install Node.js', + 'setupWizardStepInstallOpenClaw': 'Install OpenClaw', + 'setupWizardStepConfigureBypass': 'Configure Bionic Bypass', + 'setupWizardComplete': 'Setup complete!', + 'setupWizardStatusSetupComplete': 'Setup complete', + 'setupWizardStatusSetupRequired': 'Setup required', + 'setupWizardStatusSettingUpDirs': 'Setting up directories...', + 'setupWizardStatusDownloadingUbuntuRootfs': 'Downloading Ubuntu rootfs...', + 'setupWizardStatusDownloadingProgress': + 'Downloading: {current} MB / {total} MB', + 'setupWizardStatusExtractingRootfs': + 'Extracting rootfs (this takes a while)...', + 'setupWizardStatusRootfsExtracted': 'Rootfs extracted', + 'setupWizardStatusFixingPermissions': 'Fixing rootfs permissions...', + 'setupWizardStatusUpdatingPackageLists': 'Updating package lists...', + 'setupWizardStatusInstallingBasePackages': 'Installing base packages...', + 'setupWizardStatusDownloadingNode': 'Downloading Node.js {version}...', + 'setupWizardStatusDownloadingNodeProgress': + 'Downloading Node.js: {current} MB / {total} MB', + 'setupWizardStatusExtractingNode': 'Extracting Node.js...', + 'setupWizardStatusVerifyingNode': 'Verifying Node.js...', + 'setupWizardStatusNodeInstalled': 'Node.js installed', + 'setupWizardStatusInstallingOpenClaw': + 'Installing OpenClaw (this may take a few minutes)...', + 'setupWizardStatusCreatingBinWrappers': 'Creating bin wrappers...', + 'setupWizardStatusVerifyingOpenClaw': 'Verifying OpenClaw...', + 'setupWizardStatusOpenClawInstalled': 'OpenClaw installed', + 'setupWizardStatusBypassConfigured': 'Bionic Bypass configured', + 'setupWizardStatusReady': 'Setup complete! Ready to start the gateway.', + 'onboardingTitle': 'OpenClaw Onboarding', + 'onboardingStarting': 'Starting onboarding...', + 'onboardingGoToDashboard': 'Go to Dashboard', + 'onboardingStartFailed': 'Failed to start onboarding: {error}', + 'configureTitle': 'OpenClaw Configure', + 'configureStarting': 'Starting configure...', + 'configureStartFailed': 'Failed to start configure: {error}', + 'nodeTitle': 'Node', + 'nodeConfigurationTitle': 'Node Configuration', + 'nodeGatewayConnection': 'Gateway connection', + 'nodeLocalGateway': 'Local Gateway', + 'nodeLocalGatewaySubtitle': 'Auto-pair with gateway on this device', + 'nodeRemoteGateway': 'Remote Gateway', + 'nodeRemoteGatewaySubtitle': 'Connect to a gateway on another device', + 'nodeGatewayHost': 'Gateway Host', + 'nodeGatewayPort': 'Gateway Port', + 'nodeGatewayToken': 'Gateway Token', + 'nodeGatewayTokenHint': 'Paste token from gateway dashboard URL', + 'nodeGatewayTokenHelper': 'Found in dashboard URL after #token=', + 'nodeConnect': 'Connect', + 'nodePairing': 'Pairing', + 'nodeApproveCode': 'Approve this code on the gateway:', + 'nodeCapabilities': 'Capabilities', + 'nodeCapabilityCameraTitle': 'Camera', + 'nodeCapabilityCameraSubtitle': 'Capture photos and video clips', + 'nodeCapabilityCanvasTitle': 'Canvas', + 'nodeCapabilityCanvasSubtitle': 'Navigate and interact with web pages', + 'nodeCapabilityLocationTitle': 'Location', + 'nodeCapabilityLocationSubtitle': 'Get device GPS coordinates', + 'nodeCapabilityScreenTitle': 'Screen Recording', + 'nodeCapabilityScreenSubtitle': + 'Record device screen (requires consent each time)', + 'nodeCapabilityFlashlightTitle': 'Flashlight', + 'nodeCapabilityFlashlightSubtitle': 'Toggle device torch on/off', + 'nodeCapabilityVibrationTitle': 'Vibration', + 'nodeCapabilityVibrationSubtitle': + 'Trigger haptic feedback and vibration patterns', + 'nodeCapabilitySensorsTitle': 'Sensors', + 'nodeCapabilitySensorsSubtitle': + 'Read accelerometer, gyroscope, magnetometer, barometer', + 'nodeCapabilitySerialTitle': 'Serial', + 'nodeCapabilitySerialSubtitle': 'Bluetooth and USB serial communication', + 'nodeDeviceInfo': 'Device info', + 'nodeDeviceId': 'Device ID', + 'nodeLogs': 'Node logs', + 'nodeNoLogs': 'No logs yet', + 'nodeConnectedTo': 'Connected to {host}:{port}', + 'nodePairingCode': 'Pairing code: ', + 'nodeEnable': 'Enable Node', + 'nodeDisable': 'Disable Node', + 'nodeReconnect': 'Reconnect', + 'nodeStatusPaired': 'Paired', + 'nodeStatusConnecting': 'Connecting', + 'nodeStatusError': 'Error', + 'nodeStatusDisabled': 'Disabled', + 'nodeStatusDisconnected': 'Disconnected', + 'settingsTitle': 'Settings', + 'settingsGeneral': 'General', + 'settingsAutoStart': 'Auto-start gateway', + 'settingsAutoStartSubtitle': 'Start the gateway when the app opens', + 'settingsBatteryOptimization': 'Battery Optimization', + 'settingsBatteryOptimized': 'Optimized (may kill background sessions)', + 'settingsBatteryUnrestricted': 'Unrestricted (recommended)', + 'settingsStorage': 'Setup Storage', + 'settingsStorageGranted': 'Granted — /sdcard accessible in proot', + 'settingsStorageMissing': 'Allow access to shared storage', + 'settingsStorageDialogTitle': 'Grant file access', + 'settingsStorageDialogBody': + 'OpenClaw needs file management access to read and write snapshot files in shared storage. You will be taken to the system settings page next.', + 'settingsStorageDialogAction': 'Continue', + 'onboardingStorageDialogTitle': 'Grant file access for onboarding', + 'onboardingStorageDialogBody': + 'OpenClaw onboarding needs file management access so Ubuntu can mount shared storage inside proot while you configure API keys and binding. The Android permission page will open next.', + 'onboardingStoragePermissionRequired': + 'File management access is required before onboarding can continue.', + 'configureStorageDialogTitle': 'Grant file access for configuration', + 'configureStorageDialogBody': + 'OpenClaw configuration needs file management access so Ubuntu can mount shared storage inside proot while you manage gateway settings. The Android permission page will open next.', + 'configureStoragePermissionRequired': + 'File management access is required before configuration can continue.', + 'settingsNode': 'Node', + 'settingsEnableNode': 'Enable Node', + 'settingsEnableNodeSubtitle': 'Provide device capabilities to the gateway', + 'settingsNodeConfiguration': 'Node Configuration', + 'settingsNodeConfigurationSubtitle': 'Connection, pairing, and capabilities', + 'settingsSystemInfo': 'System info', + 'settingsArchitecture': 'Architecture', + 'settingsProotPath': 'PRoot path', + 'settingsRootfs': 'Rootfs', + 'settingsNodeJs': 'Node.js', + 'settingsOpenClaw': 'OpenClaw', + 'settingsGo': 'Go (Golang)', + 'settingsHomebrew': 'Homebrew', + 'settingsOpenSsh': 'OpenSSH', + 'settingsMaintenance': 'Maintenance', + 'settingsExportSnapshot': 'Export Snapshot', + 'settingsExportSnapshotSubtitle': 'Backup config to Downloads', + 'settingsImportSnapshot': 'Import Snapshot', + 'settingsImportSnapshotSubtitle': 'Restore config from backup', + 'settingsRerunSetup': 'Re-run setup', + 'settingsRerunSetupSubtitle': 'Reinstall or repair the environment', + 'settingsAbout': 'About', + 'settingsAboutSubtitle': 'AI Gateway for Android\nVersion {version}', + 'settingsDeveloper': 'Developer', + 'settingsGithub': 'GitHub', + 'settingsContact': 'Contact', + 'settingsLicense': 'License', + 'settingsPlayStore': 'Play Store', + 'settingsEmail': 'Email', + 'settingsSnapshotSaved': 'Snapshot saved to {path}', + 'settingsExportFailed': 'Export failed: {error}', + 'settingsSnapshotMissing': 'No snapshot found at {path}', + 'settingsSnapshotRestored': + 'Snapshot restored successfully. Restart the gateway to apply.', + 'settingsImportFailed': 'Import failed: {error}', + 'statusInstalled': 'Installed', + 'statusNotInstalled': 'Not installed', }; diff --git a/flutter_app/lib/l10n/app_strings_ja.dart b/flutter_app/lib/l10n/app_strings_ja.dart index 85b7744..5e4e8a1 100644 --- a/flutter_app/lib/l10n/app_strings_ja.dart +++ b/flutter_app/lib/l10n/app_strings_ja.dart @@ -42,7 +42,8 @@ const Map appStringsJa = { 'providerDetailApiKey': 'API Key', 'providerDetailApiKeyEmpty': 'API key cannot be empty', 'providerDetailEndpoint': 'API Base URL', - 'providerDetailEndpointHelper': 'Override the default endpoint if your account uses a custom or regional API base URL.', + 'providerDetailEndpointHelper': + 'Override the default endpoint if your account uses a custom or regional API base URL.', 'providerDetailEndpointInvalid': 'Enter a valid absolute API base URL', 'providerDetailModel': 'Model', 'providerDetailModelEmpty': 'Model name cannot be empty', @@ -53,21 +54,26 @@ const Map appStringsJa = { 'providerDetailSaved': '{provider} configured and activated', 'providerDetailSaveFailed': 'Failed to save: {error}', 'providerDetailRemoveTitle': 'Remove {provider}?', - 'providerDetailRemoveBody': 'This will delete the API key, endpoint, and saved model for this provider.', + 'providerDetailRemoveBody': + 'This will delete the API key, endpoint, and saved model for this provider.', 'providerDetailRemoveAction': 'Remove', 'providerDetailRemoveConfiguration': 'Remove Configuration', 'providerDetailRemoved': '{provider} removed', 'providerDetailRemoveFailed': 'Failed to remove: {error}', 'providerNameAnthropic': 'Anthropic', - 'providerDescriptionAnthropic': 'Claude models for advanced reasoning and coding', + 'providerDescriptionAnthropic': + 'Claude models for advanced reasoning and coding', 'providerNameOpenai': 'OpenAI', 'providerDescriptionOpenai': 'GPT and o-series models', 'providerNameQwen': 'Qwen', - 'providerDescriptionQwen': 'Alibaba Cloud Qwen models via DashScope OpenAI-compatible API', + 'providerDescriptionQwen': + 'Alibaba Cloud Qwen models via DashScope OpenAI-compatible API', 'providerNameMinimax': 'MiniMax', - 'providerDescriptionMinimax': 'MiniMax chat models with editable API endpoint support', + 'providerDescriptionMinimax': + 'MiniMax chat models with editable API endpoint support', 'providerNameDoubao': 'Doubao', - 'providerDescriptionDoubao': 'Volcengine Ark / Doubao models with official Ark endpoint presets', + 'providerDescriptionDoubao': + 'Volcengine Ark / Doubao models with official Ark endpoint presets', 'providerNameGoogle': 'Google Gemini', 'providerDescriptionGoogle': 'Gemini family of multimodal models', 'providerNameOpenrouter': 'OpenRouter', @@ -115,20 +121,26 @@ const Map appStringsJa = { 'packagesInstall': 'インストール', 'packagesUninstall': 'アンインストール', 'packagesUninstallTitle': 'Uninstall {name}?', - 'packagesUninstallDescription': 'This will remove {name} from the environment.', + 'packagesUninstallDescription': + 'This will remove {name} from the environment.', 'packageGoDescription': 'Go programming language compiler and tools', 'packageBrewDescription': 'The missing package manager for Linux', 'packageSshDescription': 'SSH client and server for secure remote access', 'setupWizardTitle': 'OpenClaw セットアップ', - 'setupWizardIntroIdle': 'This will download Ubuntu, Node.js, and OpenClaw into a self-contained environment.', - 'setupWizardIntroRunning': 'Setting up the environment. This may take several minutes.', + 'setupWizardIntroIdle': + 'This will download Ubuntu, Node.js, and OpenClaw into a self-contained environment.', + 'setupWizardIntroRunning': + 'Setting up the environment. This may take several minutes.', 'setupWizardConfigureApiKeys': 'Configure API Keys', 'setupWizardRetry': '再試行', 'setupWizardBegin': 'セットアップ開始', - 'setupWizardRequirements': 'Requires ~500MB of storage and an internet connection', + 'setupWizardRequirements': + 'Requires ~500MB of storage and an internet connection', 'setupWizardStorageDialogTitle': 'Grant file access before setup', - 'setupWizardStorageDialogBody': 'OpenClaw setup needs file management access before installation starts, so the Ubuntu environment can mount shared storage correctly inside proot. The Android permission page will open next.', - 'setupWizardStoragePermissionRequired': 'File management access is required before setup can start.', + 'setupWizardStorageDialogBody': + 'OpenClaw setup needs file management access before installation starts, so the Ubuntu environment can mount shared storage correctly inside proot. The Android permission page will open next.', + 'setupWizardStoragePermissionRequired': + 'File management access is required before setup can start.', 'setupWizardOptionalPackages': 'OPTIONAL PACKAGES', 'setupWizardStepDownloadRootfs': 'Download Ubuntu rootfs', 'setupWizardStepExtractRootfs': 'Extract rootfs', @@ -140,18 +152,22 @@ const Map appStringsJa = { 'setupWizardStatusSetupRequired': 'Setup required', 'setupWizardStatusSettingUpDirs': 'Setting up directories...', 'setupWizardStatusDownloadingUbuntuRootfs': 'Downloading Ubuntu rootfs...', - 'setupWizardStatusDownloadingProgress': 'Downloading: {current} MB / {total} MB', - 'setupWizardStatusExtractingRootfs': 'Extracting rootfs (this takes a while)...', + 'setupWizardStatusDownloadingProgress': + 'Downloading: {current} MB / {total} MB', + 'setupWizardStatusExtractingRootfs': + 'Extracting rootfs (this takes a while)...', 'setupWizardStatusRootfsExtracted': 'Rootfs extracted', 'setupWizardStatusFixingPermissions': 'Fixing rootfs permissions...', 'setupWizardStatusUpdatingPackageLists': 'Updating package lists...', 'setupWizardStatusInstallingBasePackages': 'Installing base packages...', 'setupWizardStatusDownloadingNode': 'Downloading Node.js {version}...', - 'setupWizardStatusDownloadingNodeProgress': 'Downloading Node.js: {current} MB / {total} MB', + 'setupWizardStatusDownloadingNodeProgress': + 'Downloading Node.js: {current} MB / {total} MB', 'setupWizardStatusExtractingNode': 'Extracting Node.js...', 'setupWizardStatusVerifyingNode': 'Verifying Node.js...', 'setupWizardStatusNodeInstalled': 'Node.js installed', - 'setupWizardStatusInstallingOpenClaw': 'Installing OpenClaw (this may take a few minutes)...', + 'setupWizardStatusInstallingOpenClaw': + 'Installing OpenClaw (this may take a few minutes)...', 'setupWizardStatusCreatingBinWrappers': 'Creating bin wrappers...', 'setupWizardStatusVerifyingOpenClaw': 'Verifying OpenClaw...', 'setupWizardStatusOpenClawInstalled': 'OpenClaw installed', @@ -187,13 +203,18 @@ const Map appStringsJa = { 'nodeCapabilityLocationTitle': 'Location', 'nodeCapabilityLocationSubtitle': 'Get device GPS coordinates', 'nodeCapabilityScreenTitle': 'Screen Recording', - 'nodeCapabilityScreenSubtitle': 'Record device screen (requires consent each time)', + 'nodeCapabilityScreenSubtitle': + 'Record device screen (requires consent each time)', 'nodeCapabilityFlashlightTitle': 'Flashlight', 'nodeCapabilityFlashlightSubtitle': 'Toggle device torch on/off', 'nodeCapabilityVibrationTitle': 'Vibration', - 'nodeCapabilityVibrationSubtitle': 'Trigger haptic feedback and vibration patterns', + 'nodeCapabilityVibrationSubtitle': + 'Trigger haptic feedback and vibration patterns', 'nodeCapabilitySensorsTitle': 'Sensors', - 'nodeCapabilitySensorsSubtitle': 'Read accelerometer, gyroscope, magnetometer, barometer', + 'nodeCapabilitySensorsSubtitle': + 'Read accelerometer, gyroscope, magnetometer, barometer', + 'nodeCapabilitySerialTitle': 'シリアル', + 'nodeCapabilitySerialSubtitle': 'Bluetooth と USB シリアル通信', 'nodeDeviceInfo': 'Device info', 'nodeDeviceId': 'Device ID', 'nodeLogs': 'Node logs', @@ -219,14 +240,19 @@ const Map appStringsJa = { 'settingsStorageGranted': 'Granted — /sdcard accessible in proot', 'settingsStorageMissing': 'Allow access to shared storage', 'settingsStorageDialogTitle': 'Grant file access', - 'settingsStorageDialogBody': 'OpenClaw needs file management access to read and write snapshot files in shared storage. You will be taken to the system settings page next.', + 'settingsStorageDialogBody': + 'OpenClaw needs file management access to read and write snapshot files in shared storage. You will be taken to the system settings page next.', 'settingsStorageDialogAction': 'Continue', 'onboardingStorageDialogTitle': 'Grant file access for onboarding', - 'onboardingStorageDialogBody': 'OpenClaw onboarding needs file management access so Ubuntu can mount shared storage inside proot while you configure API keys and binding. The Android permission page will open next.', - 'onboardingStoragePermissionRequired': 'File management access is required before onboarding can continue.', + 'onboardingStorageDialogBody': + 'OpenClaw onboarding needs file management access so Ubuntu can mount shared storage inside proot while you configure API keys and binding. The Android permission page will open next.', + 'onboardingStoragePermissionRequired': + 'File management access is required before onboarding can continue.', 'configureStorageDialogTitle': 'Grant file access for configuration', - 'configureStorageDialogBody': 'OpenClaw configuration needs file management access so Ubuntu can mount shared storage inside proot while you manage gateway settings. The Android permission page will open next.', - 'configureStoragePermissionRequired': 'File management access is required before configuration can continue.', + 'configureStorageDialogBody': + 'OpenClaw configuration needs file management access so Ubuntu can mount shared storage inside proot while you manage gateway settings. The Android permission page will open next.', + 'configureStoragePermissionRequired': + 'File management access is required before configuration can continue.', 'settingsNode': 'ノード', 'settingsEnableNode': 'Enable Node', 'settingsEnableNodeSubtitle': 'Provide device capabilities to the gateway', @@ -259,7 +285,8 @@ const Map appStringsJa = { 'settingsSnapshotSaved': 'Snapshot saved to {path}', 'settingsExportFailed': 'Export failed: {error}', 'settingsSnapshotMissing': 'No snapshot found at {path}', - 'settingsSnapshotRestored': 'Snapshot restored successfully. Restart the gateway to apply.', + 'settingsSnapshotRestored': + 'Snapshot restored successfully. Restart the gateway to apply.', 'settingsImportFailed': 'Import failed: {error}', 'statusInstalled': 'インストール済み', 'statusNotInstalled': '未インストール', diff --git a/flutter_app/lib/l10n/app_strings_zh_hans.dart b/flutter_app/lib/l10n/app_strings_zh_hans.dart index 8b9b2bc..088115e 100644 --- a/flutter_app/lib/l10n/app_strings_zh_hans.dart +++ b/flutter_app/lib/l10n/app_strings_zh_hans.dart @@ -1,272 +1,273 @@ const Map appStringsZhHans = { - - 'appName': 'OpenClaw', - 'language': '语言', - 'languageSystem': '跟随系统', - 'languageEnglish': '英语', - 'languageChinese': '简体中文', - 'languageTraditionalChinese': '繁体中文', - 'languageJapanese': '日语', - 'commonInstalled': '已安装', - 'commonNotInstalled': '未安装', - 'commonCancel': '取消', - 'commonCopy': '复制', - 'commonCopiedToClipboard': '已复制到剪贴板', - 'commonOpen': '打开', - 'commonPaste': '粘贴', - 'commonRetry': '重试', - 'commonDone': '完成', - 'commonConfigure': '配置', - 'commonScreenshot': '截图', - 'commonSaveFailed': '截图失败', - 'commonScreenshotSaved': '截图已保存:{fileName}', - 'commonNoUrlFound': '所选内容中未找到 URL', - 'commonOpenLink': '打开链接', - 'commonLinkCopied': '链接已复制', - 'dashboardQuickActions': '快捷操作', - 'dashboardTerminalTitle': '终端', - 'dashboardTerminalSubtitle': '打开 Ubuntu Shell 并使用 OpenClaw', - 'dashboardWebDashboardTitle': 'Web 控制台', - 'dashboardWebDashboardSubtitle': '在浏览器中打开 OpenClaw 控制台', - 'dashboardStartGatewayFirst': '请先启动网关', - 'dashboardOnboardingTitle': '引导配置', - 'dashboardOnboardingSubtitle': '配置 API Key 和绑定信息', - 'dashboardConfigureTitle': '网关配置', - 'dashboardConfigureSubtitle': '管理网关设置', - 'dashboardProvidersTitle': 'AI 提供商', - 'dashboardProvidersSubtitle': '配置模型和 API Key', - 'providersScreenTitle': 'AI 提供商', - 'providersScreenActiveModel': '当前激活模型', - 'providersScreenIntro': '选择一个提供商,配置它的 API Key、端点和模型。', - 'providersStatusActive': '已激活', - 'providersStatusConfigured': '已配置', - 'providerDetailApiKey': 'API Key', - 'providerDetailApiKeyEmpty': 'API Key 不能为空', - 'providerDetailEndpoint': 'API 基础地址', - 'providerDetailEndpointHelper': '如果你的账号使用自定义或区域专属端点,可以在这里覆盖默认地址。', - 'providerDetailEndpointInvalid': '请输入有效的绝对 API 地址', - 'providerDetailModel': '模型', - 'providerDetailModelEmpty': '模型名称不能为空', - 'providerDetailCustomModelAction': '自定义...', - 'providerDetailCustomModelLabel': '自定义模型名', - 'providerDetailCustomModelHint': '例如:meta/llama-3.3-70b-instruct', - 'providerDetailSaveAction': '保存并激活', - 'providerDetailSaved': '已配置并激活 {provider}', - 'providerDetailSaveFailed': '保存失败:{error}', - 'providerDetailRemoveTitle': '移除 {provider}?', - 'providerDetailRemoveBody': '这会删除该提供商保存的 API Key、端点和模型。', - 'providerDetailRemoveAction': '移除', - 'providerDetailRemoveConfiguration': '移除配置', - 'providerDetailRemoved': '已移除 {provider}', - 'providerDetailRemoveFailed': '移除失败:{error}', - 'providerNameAnthropic': 'Anthropic', - 'providerDescriptionAnthropic': 'Claude 系列模型,适合复杂推理与编程', - 'providerNameOpenai': 'OpenAI', - 'providerDescriptionOpenai': 'GPT 与 o 系列模型', - 'providerNameQwen': '通义千问', - 'providerDescriptionQwen': '通过 DashScope OpenAI 兼容接口接入千问模型', - 'providerNameMinimax': 'MiniMax', - 'providerDescriptionMinimax': 'MiniMax 对话模型,支持自定义 API 端点', - 'providerNameDoubao': '豆包', - 'providerDescriptionDoubao': '火山方舟 / 豆包模型,内置官方 Ark 端点预设', - 'providerNameGoogle': 'Google Gemini', - 'providerDescriptionGoogle': 'Gemini 多模态模型家族', - 'providerNameOpenrouter': 'OpenRouter', - 'providerDescriptionOpenrouter': '统一接入数百种模型的 API', - 'providerNameNvidia': 'NVIDIA NIM', - 'providerDescriptionNvidia': '面向 GPU 推理的优化端点', - 'providerNameDeepseek': 'DeepSeek', - 'providerDescriptionDeepseek': '高性能开源模型服务', - 'providerNameXai': 'xAI', - 'providerDescriptionXai': 'xAI 的 Grok 系列模型', - 'dashboardPackagesTitle': '可选组件', - 'dashboardPackagesSubtitle': '安装可选工具(Go、Homebrew、SSH)', - 'dashboardSshTitle': 'SSH 访问', - 'dashboardSshSubtitle': '通过 SSH 远程访问终端', - 'dashboardLogsTitle': '日志', - 'dashboardLogsSubtitle': '查看网关输出和错误', - 'dashboardSnapshotTitle': '快照', - 'dashboardSnapshotSubtitle': '备份或恢复你的配置', - 'dashboardNodeTitle': '节点', - 'dashboardNodeConnected': '已连接到网关', - 'dashboardNodeDisabled': '为 AI 提供设备能力', - 'dashboardVersionLabel': 'OpenClaw v{version}', - 'dashboardAuthorLabel': '作者 {author} | {org}', - 'gatewayTitle': '网关', - 'gatewayCopyUrl': '复制 URL', - 'gatewayUrlCopied': 'URL 已复制到剪贴板', - 'gatewayOpenDashboard': '打开控制台', - 'gatewayStart': '启动网关', - 'gatewayStop': '停止网关', - 'gatewayViewLogs': '查看日志', - 'gatewayStatusRunning': '运行中', - 'gatewayStatusStarting': '启动中', - 'gatewayStatusError': '错误', - 'gatewayStatusStopped': '已停止', - 'logsTitle': '网关日志', - 'logsAutoScrollOn': '自动滚动已开启', - 'logsAutoScrollOff': '自动滚动已关闭', - 'logsCopyAll': '复制全部日志', - 'logsFilterHint': '筛选日志...', - 'logsEmpty': '还没有日志。请先启动网关。', - 'logsNoMatch': '没有匹配的日志。', - 'logsCopied': '日志已复制到剪贴板', - 'packagesTitle': '可选组件', - 'packagesDescription': '可在 Ubuntu 环境内安装的开发工具。', - 'packagesInstall': '安装', - 'packagesUninstall': '卸载', - 'packagesUninstallTitle': '卸载 {name}?', - 'packagesUninstallDescription': '这会将 {name} 从环境中移除。', - 'packageGoDescription': 'Go 编程语言编译器和工具链', - 'packageBrewDescription': 'Linux 上常用的缺省包管理器', - 'packageSshDescription': '用于安全远程访问的 SSH 客户端和服务端', - 'setupWizardTitle': '开始配置 OpenClaw', - 'setupWizardIntroIdle': '这会将 Ubuntu、Node.js 和 OpenClaw 下载到一个自包含环境中。', - 'setupWizardIntroRunning': '正在配置环境,可能需要几分钟。', - 'setupWizardConfigureApiKeys': '配置 API Key', - 'setupWizardRetry': '重新安装', - 'setupWizardBegin': '开始安装', - 'setupWizardRequirements': '需要约 500MB 存储空间和网络连接', - 'setupWizardStorageDialogTitle': '安装前授予文件访问权限', - 'setupWizardStorageDialogBody': - 'OpenClaw 在开始安装前需要文件管理权限,这样 Ubuntu 环境才能在 proot 中正确挂载共享存储。接下来会打开 Android 授权页面。', - 'setupWizardStoragePermissionRequired': '开始安装前,必须先授予文件管理权限。', - 'setupWizardOptionalPackages': '可选组件', - 'setupWizardStepDownloadRootfs': '下载 Ubuntu rootfs', - 'setupWizardStepExtractRootfs': '解压 rootfs', - 'setupWizardStepInstallNode': '安装 Node.js', - 'setupWizardStepInstallOpenClaw': '安装 OpenClaw', - 'setupWizardStepConfigureBypass': '配置 Bionic Bypass', - 'setupWizardComplete': '安装完成!', - 'setupWizardStatusSetupComplete': '安装完成', - 'setupWizardStatusSetupRequired': '需要安装环境', - 'setupWizardStatusSettingUpDirs': '正在准备目录...', - 'setupWizardStatusDownloadingUbuntuRootfs': '正在下载 Ubuntu rootfs...', - 'setupWizardStatusDownloadingProgress': '正在下载:{current} MB / {total} MB', - 'setupWizardStatusExtractingRootfs': '正在解压 rootfs(这会花一点时间)...', - 'setupWizardStatusRootfsExtracted': 'rootfs 已解压', - 'setupWizardStatusFixingPermissions': '正在修复 rootfs 权限...', - 'setupWizardStatusUpdatingPackageLists': '正在更新软件包列表...', - 'setupWizardStatusInstallingBasePackages': '正在安装基础软件包...', - 'setupWizardStatusDownloadingNode': '正在下载 Node.js {version}...', - 'setupWizardStatusDownloadingNodeProgress': - '正在下载 Node.js:{current} MB / {total} MB', - 'setupWizardStatusExtractingNode': '正在解压 Node.js...', - 'setupWizardStatusVerifyingNode': '正在验证 Node.js...', - 'setupWizardStatusNodeInstalled': 'Node.js 已安装', - 'setupWizardStatusInstallingOpenClaw': '正在安装 OpenClaw(这可能需要几分钟)...', - 'setupWizardStatusCreatingBinWrappers': '正在创建命令包装器...', - 'setupWizardStatusVerifyingOpenClaw': '正在验证 OpenClaw...', - 'setupWizardStatusOpenClawInstalled': 'OpenClaw 已安装', - 'setupWizardStatusBypassConfigured': 'Bionic Bypass 已配置', - 'setupWizardStatusReady': '安装完成,可以开始启动网关了。', - 'onboardingTitle': 'OpenClaw 引导配置', - 'onboardingStarting': '正在启动引导配置...', - 'onboardingGoToDashboard': '前往控制台', - 'onboardingStartFailed': '启动引导配置失败:{error}', - 'configureTitle': 'OpenClaw 配置', - 'configureStarting': '正在启动配置...', - 'configureStartFailed': '启动配置失败:{error}', - 'nodeTitle': '节点', - 'nodeConfigurationTitle': '节点配置', - 'nodeGatewayConnection': '网关连接', - 'nodeLocalGateway': '本地网关', - 'nodeLocalGatewaySubtitle': '自动配对本机上的网关', - 'nodeRemoteGateway': '远程网关', - 'nodeRemoteGatewaySubtitle': '连接到其他设备上的网关', - 'nodeGatewayHost': '网关主机', - 'nodeGatewayPort': '网关端口', - 'nodeGatewayToken': '网关令牌', - 'nodeGatewayTokenHint': '粘贴控制台 URL 中的令牌', - 'nodeGatewayTokenHelper': '可在控制台 URL 的 #token= 后找到', - 'nodeConnect': '连接', - 'nodePairing': '配对', - 'nodeApproveCode': '请在网关端确认此代码:', - 'nodeCapabilities': '能力', - 'nodeCapabilityCameraTitle': '相机', - 'nodeCapabilityCameraSubtitle': '拍摄照片和视频片段', - 'nodeCapabilityCanvasTitle': 'Canvas', - 'nodeCapabilityCanvasSubtitle': '浏览并交互网页', - 'nodeCapabilityLocationTitle': '定位', - 'nodeCapabilityLocationSubtitle': '获取设备 GPS 坐标', - 'nodeCapabilityScreenTitle': '录屏', - 'nodeCapabilityScreenSubtitle': '录制设备屏幕(每次都需授权)', - 'nodeCapabilityFlashlightTitle': '手电筒', - 'nodeCapabilityFlashlightSubtitle': '切换设备闪光灯开关', - 'nodeCapabilityVibrationTitle': '震动', - 'nodeCapabilityVibrationSubtitle': '触发触觉反馈和震动模式', - 'nodeCapabilitySensorsTitle': '传感器', - 'nodeCapabilitySensorsSubtitle': '读取加速度计、陀螺仪、磁力计、气压计', - 'nodeDeviceInfo': '设备信息', - 'nodeDeviceId': '设备 ID', - 'nodeLogs': '节点日志', - 'nodeNoLogs': '还没有日志', - 'nodeConnectedTo': '已连接到 {host}:{port}', - 'nodePairingCode': '配对码:', - 'nodeEnable': '启用节点', - 'nodeDisable': '禁用节点', - 'nodeReconnect': '重新连接', - 'nodeStatusPaired': '已配对', - 'nodeStatusConnecting': '连接中', - 'nodeStatusError': '错误', - 'nodeStatusDisabled': '已禁用', - 'nodeStatusDisconnected': '未连接', - 'settingsTitle': '设置', - 'settingsGeneral': '常规', - 'settingsAutoStart': '自动启动网关', - 'settingsAutoStartSubtitle': '应用打开时自动启动网关', - 'settingsBatteryOptimization': '电池优化', - 'settingsBatteryOptimized': '已优化(可能会杀死后台会话)', - 'settingsBatteryUnrestricted': '不受限制(推荐)', - 'settingsStorage': '存储访问', - 'settingsStorageGranted': '已授权,可在 proot 中访问 /sdcard', - 'settingsStorageMissing': '允许访问共享存储', - 'settingsStorageDialogTitle': '授予文件访问权限', - 'settingsStorageDialogBody': - 'OpenClaw 需要文件管理权限,才能在共享存储中读取和写入快照文件。接下来会跳转到系统设置页面。', - 'settingsStorageDialogAction': '继续', - 'onboardingStorageDialogTitle': '为引导配置授予文件访问权限', - 'onboardingStorageDialogBody': - 'OpenClaw 引导配置需要文件管理权限,Ubuntu 才能在 proot 中挂载共享存储,并完成 API Key 与绑定配置。接下来会打开 Android 授权页面。', - 'onboardingStoragePermissionRequired': '继续引导配置前,必须先授予文件管理权限。', - 'configureStorageDialogTitle': '为配置页面授予文件访问权限', - 'configureStorageDialogBody': - 'OpenClaw 配置页面需要文件管理权限,Ubuntu 才能在 proot 中挂载共享存储并管理网关设置。接下来会打开 Android 授权页面。', - 'configureStoragePermissionRequired': '继续配置前,必须先授予文件管理权限。', - 'settingsNode': '节点', - 'settingsEnableNode': '启用节点', - 'settingsEnableNodeSubtitle': '向网关提供设备能力', - 'settingsNodeConfiguration': '节点配置', - 'settingsNodeConfigurationSubtitle': '连接、配对和能力设置', - 'settingsSystemInfo': '系统信息', - 'settingsArchitecture': '架构', - 'settingsProotPath': 'PRoot 路径', - 'settingsRootfs': 'Rootfs', - 'settingsNodeJs': 'Node.js', - 'settingsOpenClaw': 'OpenClaw', - 'settingsGo': 'Go(Golang)', - 'settingsHomebrew': 'Homebrew', - 'settingsOpenSsh': 'OpenSSH', - 'settingsMaintenance': '维护', - 'settingsExportSnapshot': '导出快照', - 'settingsExportSnapshotSubtitle': '将配置备份到 Downloads', - 'settingsImportSnapshot': '导入快照', - 'settingsImportSnapshotSubtitle': '从备份恢复配置', - 'settingsRerunSetup': '重新运行安装', - 'settingsRerunSetupSubtitle': '重新安装或修复环境', - 'settingsAbout': '关于', - 'settingsAboutSubtitle': 'Android AI 网关\n版本 {version}', - 'settingsDeveloper': '开发者', - 'settingsGithub': 'GitHub', - 'settingsContact': '联系方式', - 'settingsLicense': '许可证', - 'settingsPlayStore': 'Play 商店', - 'settingsEmail': '邮箱', - 'settingsSnapshotSaved': '快照已保存到 {path}', - 'settingsExportFailed': '导出失败:{error}', - 'settingsSnapshotMissing': '在 {path} 未找到快照', - 'settingsSnapshotRestored': '快照已恢复。请重启网关以生效。', - 'settingsImportFailed': '导入失败:{error}', - 'statusInstalled': '已安装', - 'statusNotInstalled': '未安装', + 'appName': 'OpenClaw', + 'language': '语言', + 'languageSystem': '跟随系统', + 'languageEnglish': '英语', + 'languageChinese': '简体中文', + 'languageTraditionalChinese': '繁体中文', + 'languageJapanese': '日语', + 'commonInstalled': '已安装', + 'commonNotInstalled': '未安装', + 'commonCancel': '取消', + 'commonCopy': '复制', + 'commonCopiedToClipboard': '已复制到剪贴板', + 'commonOpen': '打开', + 'commonPaste': '粘贴', + 'commonRetry': '重试', + 'commonDone': '完成', + 'commonConfigure': '配置', + 'commonScreenshot': '截图', + 'commonSaveFailed': '截图失败', + 'commonScreenshotSaved': '截图已保存:{fileName}', + 'commonNoUrlFound': '所选内容中未找到 URL', + 'commonOpenLink': '打开链接', + 'commonLinkCopied': '链接已复制', + 'dashboardQuickActions': '快捷操作', + 'dashboardTerminalTitle': '终端', + 'dashboardTerminalSubtitle': '打开 Ubuntu Shell 并使用 OpenClaw', + 'dashboardWebDashboardTitle': 'Web 控制台', + 'dashboardWebDashboardSubtitle': '在浏览器中打开 OpenClaw 控制台', + 'dashboardStartGatewayFirst': '请先启动网关', + 'dashboardOnboardingTitle': '引导配置', + 'dashboardOnboardingSubtitle': '配置 API Key 和绑定信息', + 'dashboardConfigureTitle': '网关配置', + 'dashboardConfigureSubtitle': '管理网关设置', + 'dashboardProvidersTitle': 'AI 提供商', + 'dashboardProvidersSubtitle': '配置模型和 API Key', + 'providersScreenTitle': 'AI 提供商', + 'providersScreenActiveModel': '当前激活模型', + 'providersScreenIntro': '选择一个提供商,配置它的 API Key、端点和模型。', + 'providersStatusActive': '已激活', + 'providersStatusConfigured': '已配置', + 'providerDetailApiKey': 'API Key', + 'providerDetailApiKeyEmpty': 'API Key 不能为空', + 'providerDetailEndpoint': 'API 基础地址', + 'providerDetailEndpointHelper': '如果你的账号使用自定义或区域专属端点,可以在这里覆盖默认地址。', + 'providerDetailEndpointInvalid': '请输入有效的绝对 API 地址', + 'providerDetailModel': '模型', + 'providerDetailModelEmpty': '模型名称不能为空', + 'providerDetailCustomModelAction': '自定义...', + 'providerDetailCustomModelLabel': '自定义模型名', + 'providerDetailCustomModelHint': '例如:meta/llama-3.3-70b-instruct', + 'providerDetailSaveAction': '保存并激活', + 'providerDetailSaved': '已配置并激活 {provider}', + 'providerDetailSaveFailed': '保存失败:{error}', + 'providerDetailRemoveTitle': '移除 {provider}?', + 'providerDetailRemoveBody': '这会删除该提供商保存的 API Key、端点和模型。', + 'providerDetailRemoveAction': '移除', + 'providerDetailRemoveConfiguration': '移除配置', + 'providerDetailRemoved': '已移除 {provider}', + 'providerDetailRemoveFailed': '移除失败:{error}', + 'providerNameAnthropic': 'Anthropic', + 'providerDescriptionAnthropic': 'Claude 系列模型,适合复杂推理与编程', + 'providerNameOpenai': 'OpenAI', + 'providerDescriptionOpenai': 'GPT 与 o 系列模型', + 'providerNameQwen': '通义千问', + 'providerDescriptionQwen': '通过 DashScope OpenAI 兼容接口接入千问模型', + 'providerNameMinimax': 'MiniMax', + 'providerDescriptionMinimax': 'MiniMax 对话模型,支持自定义 API 端点', + 'providerNameDoubao': '豆包', + 'providerDescriptionDoubao': '火山方舟 / 豆包模型,内置官方 Ark 端点预设', + 'providerNameGoogle': 'Google Gemini', + 'providerDescriptionGoogle': 'Gemini 多模态模型家族', + 'providerNameOpenrouter': 'OpenRouter', + 'providerDescriptionOpenrouter': '统一接入数百种模型的 API', + 'providerNameNvidia': 'NVIDIA NIM', + 'providerDescriptionNvidia': '面向 GPU 推理的优化端点', + 'providerNameDeepseek': 'DeepSeek', + 'providerDescriptionDeepseek': '高性能开源模型服务', + 'providerNameXai': 'xAI', + 'providerDescriptionXai': 'xAI 的 Grok 系列模型', + 'dashboardPackagesTitle': '可选组件', + 'dashboardPackagesSubtitle': '安装可选工具(Go、Homebrew、SSH)', + 'dashboardSshTitle': 'SSH 访问', + 'dashboardSshSubtitle': '通过 SSH 远程访问终端', + 'dashboardLogsTitle': '日志', + 'dashboardLogsSubtitle': '查看网关输出和错误', + 'dashboardSnapshotTitle': '快照', + 'dashboardSnapshotSubtitle': '备份或恢复你的配置', + 'dashboardNodeTitle': '节点', + 'dashboardNodeConnected': '已连接到网关', + 'dashboardNodeDisabled': '为 AI 提供设备能力', + 'dashboardVersionLabel': 'OpenClaw v{version}', + 'dashboardAuthorLabel': '作者 {author} | {org}', + 'gatewayTitle': '网关', + 'gatewayCopyUrl': '复制 URL', + 'gatewayUrlCopied': 'URL 已复制到剪贴板', + 'gatewayOpenDashboard': '打开控制台', + 'gatewayStart': '启动网关', + 'gatewayStop': '停止网关', + 'gatewayViewLogs': '查看日志', + 'gatewayStatusRunning': '运行中', + 'gatewayStatusStarting': '启动中', + 'gatewayStatusError': '错误', + 'gatewayStatusStopped': '已停止', + 'logsTitle': '网关日志', + 'logsAutoScrollOn': '自动滚动已开启', + 'logsAutoScrollOff': '自动滚动已关闭', + 'logsCopyAll': '复制全部日志', + 'logsFilterHint': '筛选日志...', + 'logsEmpty': '还没有日志。请先启动网关。', + 'logsNoMatch': '没有匹配的日志。', + 'logsCopied': '日志已复制到剪贴板', + 'packagesTitle': '可选组件', + 'packagesDescription': '可在 Ubuntu 环境内安装的开发工具。', + 'packagesInstall': '安装', + 'packagesUninstall': '卸载', + 'packagesUninstallTitle': '卸载 {name}?', + 'packagesUninstallDescription': '这会将 {name} 从环境中移除。', + 'packageGoDescription': 'Go 编程语言编译器和工具链', + 'packageBrewDescription': 'Linux 上常用的缺省包管理器', + 'packageSshDescription': '用于安全远程访问的 SSH 客户端和服务端', + 'setupWizardTitle': '开始配置 OpenClaw', + 'setupWizardIntroIdle': '这会将 Ubuntu、Node.js 和 OpenClaw 下载到一个自包含环境中。', + 'setupWizardIntroRunning': '正在配置环境,可能需要几分钟。', + 'setupWizardConfigureApiKeys': '配置 API Key', + 'setupWizardRetry': '重新安装', + 'setupWizardBegin': '开始安装', + 'setupWizardRequirements': '需要约 500MB 存储空间和网络连接', + 'setupWizardStorageDialogTitle': '安装前授予文件访问权限', + 'setupWizardStorageDialogBody': + 'OpenClaw 在开始安装前需要文件管理权限,这样 Ubuntu 环境才能在 proot 中正确挂载共享存储。接下来会打开 Android 授权页面。', + 'setupWizardStoragePermissionRequired': '开始安装前,必须先授予文件管理权限。', + 'setupWizardOptionalPackages': '可选组件', + 'setupWizardStepDownloadRootfs': '下载 Ubuntu rootfs', + 'setupWizardStepExtractRootfs': '解压 rootfs', + 'setupWizardStepInstallNode': '安装 Node.js', + 'setupWizardStepInstallOpenClaw': '安装 OpenClaw', + 'setupWizardStepConfigureBypass': '配置 Bionic Bypass', + 'setupWizardComplete': '安装完成!', + 'setupWizardStatusSetupComplete': '安装完成', + 'setupWizardStatusSetupRequired': '需要安装环境', + 'setupWizardStatusSettingUpDirs': '正在准备目录...', + 'setupWizardStatusDownloadingUbuntuRootfs': '正在下载 Ubuntu rootfs...', + 'setupWizardStatusDownloadingProgress': '正在下载:{current} MB / {total} MB', + 'setupWizardStatusExtractingRootfs': '正在解压 rootfs(这会花一点时间)...', + 'setupWizardStatusRootfsExtracted': 'rootfs 已解压', + 'setupWizardStatusFixingPermissions': '正在修复 rootfs 权限...', + 'setupWizardStatusUpdatingPackageLists': '正在更新软件包列表...', + 'setupWizardStatusInstallingBasePackages': '正在安装基础软件包...', + 'setupWizardStatusDownloadingNode': '正在下载 Node.js {version}...', + 'setupWizardStatusDownloadingNodeProgress': + '正在下载 Node.js:{current} MB / {total} MB', + 'setupWizardStatusExtractingNode': '正在解压 Node.js...', + 'setupWizardStatusVerifyingNode': '正在验证 Node.js...', + 'setupWizardStatusNodeInstalled': 'Node.js 已安装', + 'setupWizardStatusInstallingOpenClaw': '正在安装 OpenClaw(这可能需要几分钟)...', + 'setupWizardStatusCreatingBinWrappers': '正在创建命令包装器...', + 'setupWizardStatusVerifyingOpenClaw': '正在验证 OpenClaw...', + 'setupWizardStatusOpenClawInstalled': 'OpenClaw 已安装', + 'setupWizardStatusBypassConfigured': 'Bionic Bypass 已配置', + 'setupWizardStatusReady': '安装完成,可以开始启动网关了。', + 'onboardingTitle': 'OpenClaw 引导配置', + 'onboardingStarting': '正在启动引导配置...', + 'onboardingGoToDashboard': '前往控制台', + 'onboardingStartFailed': '启动引导配置失败:{error}', + 'configureTitle': 'OpenClaw 配置', + 'configureStarting': '正在启动配置...', + 'configureStartFailed': '启动配置失败:{error}', + 'nodeTitle': '节点', + 'nodeConfigurationTitle': '节点配置', + 'nodeGatewayConnection': '网关连接', + 'nodeLocalGateway': '本地网关', + 'nodeLocalGatewaySubtitle': '自动配对本机上的网关', + 'nodeRemoteGateway': '远程网关', + 'nodeRemoteGatewaySubtitle': '连接到其他设备上的网关', + 'nodeGatewayHost': '网关主机', + 'nodeGatewayPort': '网关端口', + 'nodeGatewayToken': '网关令牌', + 'nodeGatewayTokenHint': '粘贴控制台 URL 中的令牌', + 'nodeGatewayTokenHelper': '可在控制台 URL 的 #token= 后找到', + 'nodeConnect': '连接', + 'nodePairing': '配对', + 'nodeApproveCode': '请在网关端确认此代码:', + 'nodeCapabilities': '能力', + 'nodeCapabilityCameraTitle': '相机', + 'nodeCapabilityCameraSubtitle': '拍摄照片和视频片段', + 'nodeCapabilityCanvasTitle': 'Canvas', + 'nodeCapabilityCanvasSubtitle': '浏览并交互网页', + 'nodeCapabilityLocationTitle': '定位', + 'nodeCapabilityLocationSubtitle': '获取设备 GPS 坐标', + 'nodeCapabilityScreenTitle': '录屏', + 'nodeCapabilityScreenSubtitle': '录制设备屏幕(每次都需授权)', + 'nodeCapabilityFlashlightTitle': '手电筒', + 'nodeCapabilityFlashlightSubtitle': '切换设备闪光灯开关', + 'nodeCapabilityVibrationTitle': '震动', + 'nodeCapabilityVibrationSubtitle': '触发触觉反馈和震动模式', + 'nodeCapabilitySensorsTitle': '传感器', + 'nodeCapabilitySensorsSubtitle': '读取加速度计、陀螺仪、磁力计、气压计', + 'nodeCapabilitySerialTitle': '串口', + 'nodeCapabilitySerialSubtitle': '蓝牙和 USB 串口通信', + 'nodeDeviceInfo': '设备信息', + 'nodeDeviceId': '设备 ID', + 'nodeLogs': '节点日志', + 'nodeNoLogs': '还没有日志', + 'nodeConnectedTo': '已连接到 {host}:{port}', + 'nodePairingCode': '配对码:', + 'nodeEnable': '启用节点', + 'nodeDisable': '禁用节点', + 'nodeReconnect': '重新连接', + 'nodeStatusPaired': '已配对', + 'nodeStatusConnecting': '连接中', + 'nodeStatusError': '错误', + 'nodeStatusDisabled': '已禁用', + 'nodeStatusDisconnected': '未连接', + 'settingsTitle': '设置', + 'settingsGeneral': '常规', + 'settingsAutoStart': '自动启动网关', + 'settingsAutoStartSubtitle': '应用打开时自动启动网关', + 'settingsBatteryOptimization': '电池优化', + 'settingsBatteryOptimized': '已优化(可能会杀死后台会话)', + 'settingsBatteryUnrestricted': '不受限制(推荐)', + 'settingsStorage': '存储访问', + 'settingsStorageGranted': '已授权,可在 proot 中访问 /sdcard', + 'settingsStorageMissing': '允许访问共享存储', + 'settingsStorageDialogTitle': '授予文件访问权限', + 'settingsStorageDialogBody': + 'OpenClaw 需要文件管理权限,才能在共享存储中读取和写入快照文件。接下来会跳转到系统设置页面。', + 'settingsStorageDialogAction': '继续', + 'onboardingStorageDialogTitle': '为引导配置授予文件访问权限', + 'onboardingStorageDialogBody': + 'OpenClaw 引导配置需要文件管理权限,Ubuntu 才能在 proot 中挂载共享存储,并完成 API Key 与绑定配置。接下来会打开 Android 授权页面。', + 'onboardingStoragePermissionRequired': '继续引导配置前,必须先授予文件管理权限。', + 'configureStorageDialogTitle': '为配置页面授予文件访问权限', + 'configureStorageDialogBody': + 'OpenClaw 配置页面需要文件管理权限,Ubuntu 才能在 proot 中挂载共享存储并管理网关设置。接下来会打开 Android 授权页面。', + 'configureStoragePermissionRequired': '继续配置前,必须先授予文件管理权限。', + 'settingsNode': '节点', + 'settingsEnableNode': '启用节点', + 'settingsEnableNodeSubtitle': '向网关提供设备能力', + 'settingsNodeConfiguration': '节点配置', + 'settingsNodeConfigurationSubtitle': '连接、配对和能力设置', + 'settingsSystemInfo': '系统信息', + 'settingsArchitecture': '架构', + 'settingsProotPath': 'PRoot 路径', + 'settingsRootfs': 'Rootfs', + 'settingsNodeJs': 'Node.js', + 'settingsOpenClaw': 'OpenClaw', + 'settingsGo': 'Go(Golang)', + 'settingsHomebrew': 'Homebrew', + 'settingsOpenSsh': 'OpenSSH', + 'settingsMaintenance': '维护', + 'settingsExportSnapshot': '导出快照', + 'settingsExportSnapshotSubtitle': '将配置备份到 Downloads', + 'settingsImportSnapshot': '导入快照', + 'settingsImportSnapshotSubtitle': '从备份恢复配置', + 'settingsRerunSetup': '重新运行安装', + 'settingsRerunSetupSubtitle': '重新安装或修复环境', + 'settingsAbout': '关于', + 'settingsAboutSubtitle': 'Android AI 网关\n版本 {version}', + 'settingsDeveloper': '开发者', + 'settingsGithub': 'GitHub', + 'settingsContact': '联系方式', + 'settingsLicense': '许可证', + 'settingsPlayStore': 'Play 商店', + 'settingsEmail': '邮箱', + 'settingsSnapshotSaved': '快照已保存到 {path}', + 'settingsExportFailed': '导出失败:{error}', + 'settingsSnapshotMissing': '在 {path} 未找到快照', + 'settingsSnapshotRestored': '快照已恢复。请重启网关以生效。', + 'settingsImportFailed': '导入失败:{error}', + 'statusInstalled': '已安装', + 'statusNotInstalled': '未安装', }; diff --git a/flutter_app/lib/l10n/app_strings_zh_hant.dart b/flutter_app/lib/l10n/app_strings_zh_hant.dart index 7deabe7..c8265a3 100644 --- a/flutter_app/lib/l10n/app_strings_zh_hant.dart +++ b/flutter_app/lib/l10n/app_strings_zh_hant.dart @@ -127,7 +127,8 @@ const Map appStringsZhHant = { 'setupWizardBegin': '開始安裝', 'setupWizardRequirements': '需要約 500MB 存儲空間和網絡連接', 'setupWizardStorageDialogTitle': '安裝前授予檔案存取權限', - 'setupWizardStorageDialogBody': 'OpenClaw 在開始安裝前需要檔案管理權限,這樣 Ubuntu 環境才能在 proot 中正確掛載共享存儲。接下來會打開 Android 授權頁面。', + 'setupWizardStorageDialogBody': + 'OpenClaw 在開始安裝前需要檔案管理權限,這樣 Ubuntu 環境才能在 proot 中正確掛載共享存儲。接下來會打開 Android 授權頁面。', 'setupWizardStoragePermissionRequired': '開始安裝前,必須先授予檔案管理權限。', 'setupWizardOptionalPackages': '可選組件', 'setupWizardStepDownloadRootfs': '下載 Ubuntu rootfs', @@ -147,7 +148,8 @@ const Map appStringsZhHant = { 'setupWizardStatusUpdatingPackageLists': '正在更新軟體包列表...', 'setupWizardStatusInstallingBasePackages': '正在安裝基礎軟體包...', 'setupWizardStatusDownloadingNode': '正在下載 Node.js {version}...', - 'setupWizardStatusDownloadingNodeProgress': '正在下載 Node.js:{current} MB / {total} MB', + 'setupWizardStatusDownloadingNodeProgress': + '正在下載 Node.js:{current} MB / {total} MB', 'setupWizardStatusExtractingNode': '正在解壓 Node.js...', 'setupWizardStatusVerifyingNode': '正在驗證 Node.js...', 'setupWizardStatusNodeInstalled': 'Node.js 已安裝', @@ -194,6 +196,8 @@ const Map appStringsZhHant = { 'nodeCapabilityVibrationSubtitle': '觸發觸覺回饋和震動模式', 'nodeCapabilitySensorsTitle': '感測器', 'nodeCapabilitySensorsSubtitle': '讀取加速度計、陀螺儀、磁力計、氣壓計', + 'nodeCapabilitySerialTitle': '序列埠', + 'nodeCapabilitySerialSubtitle': '藍牙和 USB 序列埠通訊', 'nodeDeviceInfo': '設備資訊', 'nodeDeviceId': '設備 ID', 'nodeLogs': '節點日誌', @@ -219,13 +223,16 @@ const Map appStringsZhHant = { 'settingsStorageGranted': '已授權,可在 proot 中存取 /sdcard', 'settingsStorageMissing': '允許存取共享存儲', 'settingsStorageDialogTitle': '授予檔案存取權限', - 'settingsStorageDialogBody': 'OpenClaw 需要檔案管理權限,才能在共享存儲中讀取和寫入快照檔案。接下來會跳轉到系統設定頁面。', + 'settingsStorageDialogBody': + 'OpenClaw 需要檔案管理權限,才能在共享存儲中讀取和寫入快照檔案。接下來會跳轉到系統設定頁面。', 'settingsStorageDialogAction': '繼續', 'onboardingStorageDialogTitle': '為引導配置授予檔案存取權限', - 'onboardingStorageDialogBody': 'OpenClaw 引導配置需要檔案管理權限,Ubuntu 才能在 proot 中掛載共享存儲,並完成 API Key 與綁定配置。接下來會打開 Android 授權頁面。', + 'onboardingStorageDialogBody': + 'OpenClaw 引導配置需要檔案管理權限,Ubuntu 才能在 proot 中掛載共享存儲,並完成 API Key 與綁定配置。接下來會打開 Android 授權頁面。', 'onboardingStoragePermissionRequired': '繼續引導配置前,必須先授予檔案管理權限。', 'configureStorageDialogTitle': '為配置頁面授予檔案存取權限', - 'configureStorageDialogBody': 'OpenClaw 配置頁面需要檔案管理權限,Ubuntu 才能在 proot 中掛載共享存儲並管理網關設定。接下來會打開 Android 授權頁面。', + 'configureStorageDialogBody': + 'OpenClaw 配置頁面需要檔案管理權限,Ubuntu 才能在 proot 中掛載共享存儲並管理網關設定。接下來會打開 Android 授權頁面。', 'configureStoragePermissionRequired': '繼續配置前,必須先授予檔案管理權限。', 'settingsNode': '節點', 'settingsEnableNode': '啟用節點', diff --git a/flutter_app/lib/screens/dashboard_screen.dart b/flutter_app/lib/screens/dashboard_screen.dart index 9bd2685..f5ec693 100644 --- a/flutter_app/lib/screens/dashboard_screen.dart +++ b/flutter_app/lib/screens/dashboard_screen.dart @@ -75,8 +75,8 @@ class DashboardScreen extends StatelessWidget { final subtitle = provider.state.isRunning ? (token != null ? 'Token: ${token.substring(0, (token.length > 8 ? 8 : token.length))}...' - : 'Open OpenClaw dashboard in browser') - : 'Start gateway first'; + : l10n.t('dashboardWebDashboardSubtitle')) + : l10n.t('dashboardStartGatewayFirst'); return StatusCard( title: l10n.t('dashboardWebDashboardTitle'), subtitle: subtitle, diff --git a/flutter_app/lib/screens/node_screen.dart b/flutter_app/lib/screens/node_screen.dart index 48c2295..72ed397 100644 --- a/flutter_app/lib/screens/node_screen.dart +++ b/flutter_app/lib/screens/node_screen.dart @@ -227,8 +227,8 @@ class _NodeScreenState extends State { ), _capabilityTile( theme, - 'Serial', - 'Bluetooth and USB serial communication', + l10n.t('nodeCapabilitySerialTitle'), + l10n.t('nodeCapabilitySerialSubtitle'), Icons.usb, ), const SizedBox(height: 16), From 228b62ff5156536836ba508207a5ad2261d0454c Mon Sep 17 00:00:00 2001 From: TIANLI0 <507249007@qq.com> Date: Mon, 9 Mar 2026 01:19:10 +0800 Subject: [PATCH 8/9] i18n: localize dashboard and node serial capability text --- flutter_app/lib/l10n/app_strings_zh_hant.dart | 15 ++++++++++----- flutter_app/lib/screens/dashboard_screen.dart | 4 ++-- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/flutter_app/lib/l10n/app_strings_zh_hant.dart b/flutter_app/lib/l10n/app_strings_zh_hant.dart index 7ce87c7..06202ee 100644 --- a/flutter_app/lib/l10n/app_strings_zh_hant.dart +++ b/flutter_app/lib/l10n/app_strings_zh_hant.dart @@ -127,7 +127,8 @@ const Map appStringsZhHant = { 'setupWizardBegin': '開始安裝', 'setupWizardRequirements': '需要約 500MB 存儲空間和網絡連接', 'setupWizardStorageDialogTitle': '安裝前授予檔案存取權限', - 'setupWizardStorageDialogBody': 'OpenClaw 在開始安裝前需要檔案管理權限,這樣 Ubuntu 環境才能在 proot 中正確掛載共享存儲。接下來會打開 Android 授權頁面。', + 'setupWizardStorageDialogBody': + 'OpenClaw 在開始安裝前需要檔案管理權限,這樣 Ubuntu 環境才能在 proot 中正確掛載共享存儲。接下來會打開 Android 授權頁面。', 'setupWizardStoragePermissionRequired': '開始安裝前,必須先授予檔案管理權限。', 'setupWizardOptionalPackages': '可選組件', 'setupWizardStepDownloadRootfs': '下載 Ubuntu rootfs', @@ -147,7 +148,8 @@ const Map appStringsZhHant = { 'setupWizardStatusUpdatingPackageLists': '正在更新軟體包列表...', 'setupWizardStatusInstallingBasePackages': '正在安裝基礎軟體包...', 'setupWizardStatusDownloadingNode': '正在下載 Node.js {version}...', - 'setupWizardStatusDownloadingNodeProgress': '正在下載 Node.js:{current} MB / {total} MB', + 'setupWizardStatusDownloadingNodeProgress': + '正在下載 Node.js:{current} MB / {total} MB', 'setupWizardStatusExtractingNode': '正在解壓 Node.js...', 'setupWizardStatusVerifyingNode': '正在驗證 Node.js...', 'setupWizardStatusNodeInstalled': 'Node.js 已安裝', @@ -221,13 +223,16 @@ const Map appStringsZhHant = { 'settingsStorageGranted': '已授權,可在 proot 中存取 /sdcard', 'settingsStorageMissing': '允許存取共享存儲', 'settingsStorageDialogTitle': '授予檔案存取權限', - 'settingsStorageDialogBody': 'OpenClaw 需要檔案管理權限,才能在共享存儲中讀取和寫入快照檔案。接下來會跳轉到系統設定頁面。', + 'settingsStorageDialogBody': + 'OpenClaw 需要檔案管理權限,才能在共享存儲中讀取和寫入快照檔案。接下來會跳轉到系統設定頁面。', 'settingsStorageDialogAction': '繼續', 'onboardingStorageDialogTitle': '為引導配置授予檔案存取權限', - 'onboardingStorageDialogBody': 'OpenClaw 引導配置需要檔案管理權限,Ubuntu 才能在 proot 中掛載共享存儲,並完成 API Key 與綁定配置。接下來會打開 Android 授權頁面。', + 'onboardingStorageDialogBody': + 'OpenClaw 引導配置需要檔案管理權限,Ubuntu 才能在 proot 中掛載共享存儲,並完成 API Key 與綁定配置。接下來會打開 Android 授權頁面。', 'onboardingStoragePermissionRequired': '繼續引導配置前,必須先授予檔案管理權限。', 'configureStorageDialogTitle': '為配置頁面授予檔案存取權限', - 'configureStorageDialogBody': 'OpenClaw 配置頁面需要檔案管理權限,Ubuntu 才能在 proot 中掛載共享存儲並管理網關設定。接下來會打開 Android 授權頁面。', + 'configureStorageDialogBody': + 'OpenClaw 配置頁面需要檔案管理權限,Ubuntu 才能在 proot 中掛載共享存儲並管理網關設定。接下來會打開 Android 授權頁面。', 'configureStoragePermissionRequired': '繼續配置前,必須先授予檔案管理權限。', 'settingsNode': '節點', 'settingsEnableNode': '啟用節點', diff --git a/flutter_app/lib/screens/dashboard_screen.dart b/flutter_app/lib/screens/dashboard_screen.dart index 9bd2685..f5ec693 100644 --- a/flutter_app/lib/screens/dashboard_screen.dart +++ b/flutter_app/lib/screens/dashboard_screen.dart @@ -75,8 +75,8 @@ class DashboardScreen extends StatelessWidget { final subtitle = provider.state.isRunning ? (token != null ? 'Token: ${token.substring(0, (token.length > 8 ? 8 : token.length))}...' - : 'Open OpenClaw dashboard in browser') - : 'Start gateway first'; + : l10n.t('dashboardWebDashboardSubtitle')) + : l10n.t('dashboardStartGatewayFirst'); return StatusCard( title: l10n.t('dashboardWebDashboardTitle'), subtitle: subtitle, From c7a96eab740e8cf151bec8965858edea1ae77a97 Mon Sep 17 00:00:00 2001 From: TIANLI0 <507249007@qq.com> Date: Mon, 9 Mar 2026 07:51:18 +0800 Subject: [PATCH 9/9] fix(i18n): correct translation for save failure message in Simplified and Traditional Chinese --- flutter_app/lib/l10n/app_strings_zh_hans.dart | 4 ++-- flutter_app/lib/l10n/app_strings_zh_hant.dart | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/flutter_app/lib/l10n/app_strings_zh_hans.dart b/flutter_app/lib/l10n/app_strings_zh_hans.dart index 088115e..1ad448a 100644 --- a/flutter_app/lib/l10n/app_strings_zh_hans.dart +++ b/flutter_app/lib/l10n/app_strings_zh_hans.dart @@ -17,7 +17,7 @@ const Map appStringsZhHans = { 'commonDone': '完成', 'commonConfigure': '配置', 'commonScreenshot': '截图', - 'commonSaveFailed': '截图失败', + 'commonSaveFailed': '保存失败', 'commonScreenshotSaved': '截图已保存:{fileName}', 'commonNoUrlFound': '所选内容中未找到 URL', 'commonOpenLink': '打开链接', @@ -117,7 +117,7 @@ const Map appStringsZhHans = { 'packagesUninstallTitle': '卸载 {name}?', 'packagesUninstallDescription': '这会将 {name} 从环境中移除。', 'packageGoDescription': 'Go 编程语言编译器和工具链', - 'packageBrewDescription': 'Linux 上常用的缺省包管理器', + 'packageBrewDescription': 'Linux 上常用的包管理器', 'packageSshDescription': '用于安全远程访问的 SSH 客户端和服务端', 'setupWizardTitle': '开始配置 OpenClaw', 'setupWizardIntroIdle': '这会将 Ubuntu、Node.js 和 OpenClaw 下载到一个自包含环境中。', diff --git a/flutter_app/lib/l10n/app_strings_zh_hant.dart b/flutter_app/lib/l10n/app_strings_zh_hant.dart index 06202ee..5e5a671 100644 --- a/flutter_app/lib/l10n/app_strings_zh_hant.dart +++ b/flutter_app/lib/l10n/app_strings_zh_hant.dart @@ -17,7 +17,7 @@ const Map appStringsZhHant = { 'commonDone': '完成', 'commonConfigure': '設定', 'commonScreenshot': '截圖', - 'commonSaveFailed': '截圖失敗', + 'commonSaveFailed': '儲存失敗', 'commonScreenshotSaved': '截圖已儲存:{fileName}', 'commonNoUrlFound': '所選內容中未找到 URL', 'commonOpenLink': '打開連結', @@ -117,7 +117,7 @@ const Map appStringsZhHant = { 'packagesUninstallTitle': '卸載 {name}?', 'packagesUninstallDescription': '這會將 {name} 從環境中移除。', 'packageGoDescription': 'Go 編程語言編譯器和工具鏈', - 'packageBrewDescription': 'Linux 上常用的缺省包管理器', + 'packageBrewDescription': 'Linux 上常用的包管理器', 'packageSshDescription': '用於安全遠程存取的 SSH 客戶端和服務端', 'setupWizardTitle': '開始配置 OpenClaw', 'setupWizardIntroIdle': '這會將 Ubuntu、Node.js 和 OpenClaw 下載到一個自包含環境中。',