diff --git a/.agent-artifacts/ui-evidence/onb-m-firstcapture/001-mobile-ios-first-capture-choice.png b/.agent-artifacts/ui-evidence/onb-m-firstcapture/001-mobile-ios-first-capture-choice.png deleted file mode 100644 index d4a450897a1..00000000000 Binary files a/.agent-artifacts/ui-evidence/onb-m-firstcapture/001-mobile-ios-first-capture-choice.png and /dev/null differ diff --git a/.agent-artifacts/ui-evidence/onb-m-firstcapture/002-mobile-ios-first-capture-recording-5s.png b/.agent-artifacts/ui-evidence/onb-m-firstcapture/002-mobile-ios-first-capture-recording-5s.png deleted file mode 100644 index 873ff4e5323..00000000000 Binary files a/.agent-artifacts/ui-evidence/onb-m-firstcapture/002-mobile-ios-first-capture-recording-5s.png and /dev/null differ diff --git a/.agent-artifacts/ui-evidence/onb-m-firstcapture/003-mobile-ios-first-capture-recording-30s.png b/.agent-artifacts/ui-evidence/onb-m-firstcapture/003-mobile-ios-first-capture-recording-30s.png deleted file mode 100644 index 18d07197b4a..00000000000 Binary files a/.agent-artifacts/ui-evidence/onb-m-firstcapture/003-mobile-ios-first-capture-recording-30s.png and /dev/null differ diff --git a/.agent-artifacts/ui-evidence/onb-m-firstcapture/004-mobile-ios-first-capture-processing.png b/.agent-artifacts/ui-evidence/onb-m-firstcapture/004-mobile-ios-first-capture-processing.png deleted file mode 100644 index 930090d467a..00000000000 Binary files a/.agent-artifacts/ui-evidence/onb-m-firstcapture/004-mobile-ios-first-capture-processing.png and /dev/null differ diff --git a/.agent-artifacts/ui-evidence/onb-m-firstcapture/005-mobile-ios-first-capture-result.png b/.agent-artifacts/ui-evidence/onb-m-firstcapture/005-mobile-ios-first-capture-result.png deleted file mode 100644 index 35e9ed6b3ed..00000000000 Binary files a/.agent-artifacts/ui-evidence/onb-m-firstcapture/005-mobile-ios-first-capture-result.png and /dev/null differ diff --git a/.agent-artifacts/ui-evidence/onb-m-firstcapture/006-mobile-ios-first-capture-empty.png b/.agent-artifacts/ui-evidence/onb-m-firstcapture/006-mobile-ios-first-capture-empty.png deleted file mode 100644 index f135afec570..00000000000 Binary files a/.agent-artifacts/ui-evidence/onb-m-firstcapture/006-mobile-ios-first-capture-empty.png and /dev/null differ diff --git a/.agent-artifacts/ui-evidence/onb-m-firstcapture/007-mobile-ios-first-capture-omi-handoff.png b/.agent-artifacts/ui-evidence/onb-m-firstcapture/007-mobile-ios-first-capture-omi-handoff.png deleted file mode 100644 index 44b6ff333e8..00000000000 Binary files a/.agent-artifacts/ui-evidence/onb-m-firstcapture/007-mobile-ios-first-capture-omi-handoff.png and /dev/null differ diff --git a/.agent-artifacts/ui-evidence/onb-m-firstcapture/008-mobile-ios-first-capture-watch-instructions.png b/.agent-artifacts/ui-evidence/onb-m-firstcapture/008-mobile-ios-first-capture-watch-instructions.png deleted file mode 100644 index 56f146eeb78..00000000000 Binary files a/.agent-artifacts/ui-evidence/onb-m-firstcapture/008-mobile-ios-first-capture-watch-instructions.png and /dev/null differ diff --git a/.agent-artifacts/ui-evidence/onb-m-firstcapture/009-mobile-android-first-capture-choice.png b/.agent-artifacts/ui-evidence/onb-m-firstcapture/009-mobile-android-first-capture-choice.png deleted file mode 100644 index e401687a333..00000000000 Binary files a/.agent-artifacts/ui-evidence/onb-m-firstcapture/009-mobile-android-first-capture-choice.png and /dev/null differ diff --git a/.agent-artifacts/ui-evidence/onb-m-firstcapture/010-mobile-android-first-capture-recording-5s.png b/.agent-artifacts/ui-evidence/onb-m-firstcapture/010-mobile-android-first-capture-recording-5s.png deleted file mode 100644 index 48b61c4e7f0..00000000000 Binary files a/.agent-artifacts/ui-evidence/onb-m-firstcapture/010-mobile-android-first-capture-recording-5s.png and /dev/null differ diff --git a/.agent-artifacts/ui-evidence/onb-m-firstcapture/011-mobile-android-first-capture-recording-30s.png b/.agent-artifacts/ui-evidence/onb-m-firstcapture/011-mobile-android-first-capture-recording-30s.png deleted file mode 100644 index bd3244924ce..00000000000 Binary files a/.agent-artifacts/ui-evidence/onb-m-firstcapture/011-mobile-android-first-capture-recording-30s.png and /dev/null differ diff --git a/.agent-artifacts/ui-evidence/onb-m-firstcapture/012-mobile-android-first-capture-processing.png b/.agent-artifacts/ui-evidence/onb-m-firstcapture/012-mobile-android-first-capture-processing.png deleted file mode 100644 index e842bacfa77..00000000000 Binary files a/.agent-artifacts/ui-evidence/onb-m-firstcapture/012-mobile-android-first-capture-processing.png and /dev/null differ diff --git a/.agent-artifacts/ui-evidence/onb-m-firstcapture/013-mobile-android-first-capture-result.png b/.agent-artifacts/ui-evidence/onb-m-firstcapture/013-mobile-android-first-capture-result.png deleted file mode 100644 index f965468d32f..00000000000 Binary files a/.agent-artifacts/ui-evidence/onb-m-firstcapture/013-mobile-android-first-capture-result.png and /dev/null differ diff --git a/.agent-artifacts/ui-evidence/onb-m-firstcapture/014-mobile-android-first-capture-empty.png b/.agent-artifacts/ui-evidence/onb-m-firstcapture/014-mobile-android-first-capture-empty.png deleted file mode 100644 index c3d5ccb9e62..00000000000 Binary files a/.agent-artifacts/ui-evidence/onb-m-firstcapture/014-mobile-android-first-capture-empty.png and /dev/null differ diff --git a/.agent-artifacts/ui-evidence/onb-m-firstcapture/015-mobile-android-first-capture-omi-handoff.png b/.agent-artifacts/ui-evidence/onb-m-firstcapture/015-mobile-android-first-capture-omi-handoff.png deleted file mode 100644 index 2f523dda6e1..00000000000 Binary files a/.agent-artifacts/ui-evidence/onb-m-firstcapture/015-mobile-android-first-capture-omi-handoff.png and /dev/null differ diff --git a/.agent-artifacts/ui-evidence/onb-m-firstcapture/016-mobile-ios-first-capture-delayed.png b/.agent-artifacts/ui-evidence/onb-m-firstcapture/016-mobile-ios-first-capture-delayed.png deleted file mode 100644 index 526503764a5..00000000000 Binary files a/.agent-artifacts/ui-evidence/onb-m-firstcapture/016-mobile-ios-first-capture-delayed.png and /dev/null differ diff --git a/.agent-artifacts/ui-evidence/onb-m-firstcapture/017-mobile-android-first-capture-delayed.png b/.agent-artifacts/ui-evidence/onb-m-firstcapture/017-mobile-android-first-capture-delayed.png deleted file mode 100644 index 5e591901ba3..00000000000 Binary files a/.agent-artifacts/ui-evidence/onb-m-firstcapture/017-mobile-android-first-capture-delayed.png and /dev/null differ diff --git a/.agent-artifacts/ui-evidence/onb-m-firstcapture/018-mobile-ios-first-capture-short-warning.png b/.agent-artifacts/ui-evidence/onb-m-firstcapture/018-mobile-ios-first-capture-short-warning.png deleted file mode 100644 index f4a171b540a..00000000000 Binary files a/.agent-artifacts/ui-evidence/onb-m-firstcapture/018-mobile-ios-first-capture-short-warning.png and /dev/null differ diff --git a/.agent-artifacts/ui-evidence/onb-m-firstcapture/019-mobile-android-first-capture-short-warning.png b/.agent-artifacts/ui-evidence/onb-m-firstcapture/019-mobile-android-first-capture-short-warning.png deleted file mode 100644 index 7e951577e6a..00000000000 Binary files a/.agent-artifacts/ui-evidence/onb-m-firstcapture/019-mobile-android-first-capture-short-warning.png and /dev/null differ diff --git a/.agent-artifacts/ui-evidence/onb-m-firstcapture/020-mobile-ios-first-capture-back-confirmation.png b/.agent-artifacts/ui-evidence/onb-m-firstcapture/020-mobile-ios-first-capture-back-confirmation.png deleted file mode 100644 index 1c6d39c6395..00000000000 Binary files a/.agent-artifacts/ui-evidence/onb-m-firstcapture/020-mobile-ios-first-capture-back-confirmation.png and /dev/null differ diff --git a/.agent-artifacts/ui-evidence/onb-m-firstcapture/021-mobile-android-first-capture-back-confirmation.png b/.agent-artifacts/ui-evidence/onb-m-firstcapture/021-mobile-android-first-capture-back-confirmation.png deleted file mode 100644 index 005177e2129..00000000000 Binary files a/.agent-artifacts/ui-evidence/onb-m-firstcapture/021-mobile-android-first-capture-back-confirmation.png and /dev/null differ diff --git a/.agent-artifacts/ui-evidence/onb-m-firstcapture/022-mobile-ios-first-capture-omi-failure.png b/.agent-artifacts/ui-evidence/onb-m-firstcapture/022-mobile-ios-first-capture-omi-failure.png deleted file mode 100644 index f248902f010..00000000000 Binary files a/.agent-artifacts/ui-evidence/onb-m-firstcapture/022-mobile-ios-first-capture-omi-failure.png and /dev/null differ diff --git a/.agent-artifacts/ui-evidence/onb-m-firstcapture/023-mobile-android-first-capture-omi-failure.png b/.agent-artifacts/ui-evidence/onb-m-firstcapture/023-mobile-android-first-capture-omi-failure.png deleted file mode 100644 index 08adfdd0912..00000000000 Binary files a/.agent-artifacts/ui-evidence/onb-m-firstcapture/023-mobile-android-first-capture-omi-failure.png and /dev/null differ diff --git a/.agent-artifacts/ui-evidence/onb-m-firstcapture/evidence.json b/.agent-artifacts/ui-evidence/onb-m-firstcapture/evidence.json deleted file mode 100644 index 23d69e4e726..00000000000 --- a/.agent-artifacts/ui-evidence/onb-m-firstcapture/evidence.json +++ /dev/null @@ -1,179 +0,0 @@ -{ - "lane": "onb-m-firstcapture", - "viewport_logical": [ - 393, - 852 - ], - "pixel_ratio": 3, - "theme": "light", - "fonts": "Real SF Pro Display and SDK Roboto; explicit themed controls", - "source": "app/test/widgets/first_capture_test.dart", - "command": "FIRST_CAPTURE_RENDER=1 flutter test --no-pub test/widgets/first_capture_test.dart", - "fixtures": "Hermetic phone IO; real ConnectDevicePage with inert BLE providers; synthetic conversation", - "limits": "Widget rendering; no physical-device, production API, BLE, or Watch end-to-end acceptance.", - "version": 1, - "images": [ - { - "file": "001-mobile-ios-first-capture-choice.png", - "platform": "mobile-ios", - "description": "iOS first capture: choice", - "captured_by": "agent", - "source": "visual-audit" - }, - { - "file": "002-mobile-ios-first-capture-recording-5s.png", - "platform": "mobile-ios", - "description": "iOS first capture: recording-5s", - "captured_by": "agent", - "source": "visual-audit" - }, - { - "file": "003-mobile-ios-first-capture-recording-30s.png", - "platform": "mobile-ios", - "description": "iOS first capture: recording-30s", - "captured_by": "agent", - "source": "visual-audit" - }, - { - "file": "004-mobile-ios-first-capture-processing.png", - "platform": "mobile-ios", - "description": "iOS first capture: processing", - "captured_by": "agent", - "source": "visual-audit" - }, - { - "file": "005-mobile-ios-first-capture-result.png", - "platform": "mobile-ios", - "description": "iOS first capture: result", - "captured_by": "agent", - "source": "visual-audit" - }, - { - "file": "006-mobile-ios-first-capture-empty.png", - "platform": "mobile-ios", - "description": "iOS first capture: empty", - "captured_by": "agent", - "source": "visual-audit" - }, - { - "file": "007-mobile-ios-first-capture-omi-handoff.png", - "platform": "mobile-ios", - "description": "iOS first capture: omi-handoff", - "captured_by": "agent", - "source": "visual-audit" - }, - { - "file": "008-mobile-ios-first-capture-watch-instructions.png", - "platform": "mobile-ios", - "description": "iOS first capture: watch-instructions", - "captured_by": "agent", - "source": "visual-audit" - }, - { - "file": "009-mobile-android-first-capture-choice.png", - "platform": "mobile-android", - "description": "android first capture: choice", - "captured_by": "agent", - "source": "visual-audit" - }, - { - "file": "010-mobile-android-first-capture-recording-5s.png", - "platform": "mobile-android", - "description": "android first capture: recording-5s", - "captured_by": "agent", - "source": "visual-audit" - }, - { - "file": "011-mobile-android-first-capture-recording-30s.png", - "platform": "mobile-android", - "description": "android first capture: recording-30s", - "captured_by": "agent", - "source": "visual-audit" - }, - { - "file": "012-mobile-android-first-capture-processing.png", - "platform": "mobile-android", - "description": "android first capture: processing", - "captured_by": "agent", - "source": "visual-audit" - }, - { - "file": "013-mobile-android-first-capture-result.png", - "platform": "mobile-android", - "description": "android first capture: result", - "captured_by": "agent", - "source": "visual-audit" - }, - { - "file": "014-mobile-android-first-capture-empty.png", - "platform": "mobile-android", - "description": "android first capture: empty", - "captured_by": "agent", - "source": "visual-audit" - }, - { - "file": "015-mobile-android-first-capture-omi-handoff.png", - "platform": "mobile-android", - "description": "android first capture: omi-handoff", - "captured_by": "agent", - "source": "visual-audit" - }, - { - "file": "016-mobile-ios-first-capture-delayed.png", - "platform": "mobile-ios", - "description": "016-mobile-ios-first-capture-delayed", - "captured_by": "agent", - "source": "visual-audit" - }, - { - "file": "017-mobile-android-first-capture-delayed.png", - "platform": "mobile-android", - "description": "017-mobile-android-first-capture-delayed", - "captured_by": "agent", - "source": "visual-audit" - }, - { - "file": "018-mobile-ios-first-capture-short-warning.png", - "platform": "mobile-ios", - "description": "018-mobile-ios-first-capture-short-warning", - "captured_by": "agent", - "source": "visual-audit" - }, - { - "file": "019-mobile-android-first-capture-short-warning.png", - "platform": "mobile-android", - "description": "019-mobile-android-first-capture-short-warning", - "captured_by": "agent", - "source": "visual-audit" - }, - { - "file": "020-mobile-ios-first-capture-back-confirmation.png", - "platform": "mobile-ios", - "description": "020-mobile-ios-first-capture-back-confirmation", - "captured_by": "agent", - "source": "visual-audit" - }, - { - "file": "021-mobile-android-first-capture-back-confirmation.png", - "platform": "mobile-android", - "description": "021-mobile-android-first-capture-back-confirmation", - "captured_by": "agent", - "source": "visual-audit" - }, - { - "file": "022-mobile-ios-first-capture-omi-failure.png", - "platform": "mobile-ios", - "description": "ios pairing failure retains Connect and How to Pair", - "captured_by": "agent", - "source": "visual-audit" - }, - { - "file": "023-mobile-android-first-capture-omi-failure.png", - "platform": "mobile-android", - "description": "android pairing failure retains Connect and How to Pair", - "captured_by": "agent", - "source": "visual-audit" - } - ], - "review_revision": "Thumb-zone actions; terminal-only empty explanation; iOS App Shortcuts tip on result" -} diff --git a/.agent-artifacts/ui-evidence/yinka-onboarding-superwall/001-mobile-ios-onboarding-sign-in.png b/.agent-artifacts/ui-evidence/yinka-onboarding-superwall/001-mobile-ios-onboarding-sign-in.png new file mode 100644 index 00000000000..3e49fd2ccbb Binary files /dev/null and b/.agent-artifacts/ui-evidence/yinka-onboarding-superwall/001-mobile-ios-onboarding-sign-in.png differ diff --git a/.agent-artifacts/ui-evidence/yinka-onboarding-superwall/002-mobile-ios-onboarding-data-privacy.png b/.agent-artifacts/ui-evidence/yinka-onboarding-superwall/002-mobile-ios-onboarding-data-privacy.png new file mode 100644 index 00000000000..2fd205114ee Binary files /dev/null and b/.agent-artifacts/ui-evidence/yinka-onboarding-superwall/002-mobile-ios-onboarding-data-privacy.png differ diff --git a/.agent-artifacts/ui-evidence/yinka-onboarding-superwall/003-mobile-ios-onboarding-name.png b/.agent-artifacts/ui-evidence/yinka-onboarding-superwall/003-mobile-ios-onboarding-name.png new file mode 100644 index 00000000000..7215124a385 Binary files /dev/null and b/.agent-artifacts/ui-evidence/yinka-onboarding-superwall/003-mobile-ios-onboarding-name.png differ diff --git a/.agent-artifacts/ui-evidence/yinka-onboarding-superwall/004-mobile-ios-onboarding-permissions.png b/.agent-artifacts/ui-evidence/yinka-onboarding-superwall/004-mobile-ios-onboarding-permissions.png new file mode 100644 index 00000000000..cc30af52ea8 Binary files /dev/null and b/.agent-artifacts/ui-evidence/yinka-onboarding-superwall/004-mobile-ios-onboarding-permissions.png differ diff --git a/.agent-artifacts/ui-evidence/yinka-onboarding-superwall/005-mobile-ios-onboarding-language.png b/.agent-artifacts/ui-evidence/yinka-onboarding-superwall/005-mobile-ios-onboarding-language.png new file mode 100644 index 00000000000..67e32f87850 Binary files /dev/null and b/.agent-artifacts/ui-evidence/yinka-onboarding-superwall/005-mobile-ios-onboarding-language.png differ diff --git a/.agent-artifacts/ui-evidence/yinka-onboarding-superwall/006-mobile-ios-connect-omi.png b/.agent-artifacts/ui-evidence/yinka-onboarding-superwall/006-mobile-ios-connect-omi.png new file mode 100644 index 00000000000..d9cc1783da1 Binary files /dev/null and b/.agent-artifacts/ui-evidence/yinka-onboarding-superwall/006-mobile-ios-connect-omi.png differ diff --git a/.agent-artifacts/ui-evidence/yinka-onboarding-superwall/007-mobile-ios-activation-intro.png b/.agent-artifacts/ui-evidence/yinka-onboarding-superwall/007-mobile-ios-activation-intro.png new file mode 100644 index 00000000000..3ab4bbcf7a5 Binary files /dev/null and b/.agent-artifacts/ui-evidence/yinka-onboarding-superwall/007-mobile-ios-activation-intro.png differ diff --git a/.agent-artifacts/ui-evidence/yinka-onboarding-superwall/008-mobile-ios-activation-microphone.png b/.agent-artifacts/ui-evidence/yinka-onboarding-superwall/008-mobile-ios-activation-microphone.png new file mode 100644 index 00000000000..08cb7ec54e9 Binary files /dev/null and b/.agent-artifacts/ui-evidence/yinka-onboarding-superwall/008-mobile-ios-activation-microphone.png differ diff --git a/.agent-artifacts/ui-evidence/yinka-onboarding-superwall/009-mobile-ios-activation-dictation-live.png b/.agent-artifacts/ui-evidence/yinka-onboarding-superwall/009-mobile-ios-activation-dictation-live.png new file mode 100644 index 00000000000..853582c4f09 Binary files /dev/null and b/.agent-artifacts/ui-evidence/yinka-onboarding-superwall/009-mobile-ios-activation-dictation-live.png differ diff --git a/.agent-artifacts/ui-evidence/yinka-onboarding-superwall/010-mobile-ios-activation-summarizing.png b/.agent-artifacts/ui-evidence/yinka-onboarding-superwall/010-mobile-ios-activation-summarizing.png new file mode 100644 index 00000000000..4d098bb863a Binary files /dev/null and b/.agent-artifacts/ui-evidence/yinka-onboarding-superwall/010-mobile-ios-activation-summarizing.png differ diff --git a/.agent-artifacts/ui-evidence/yinka-onboarding-superwall/011-mobile-ios-activation-summary.png b/.agent-artifacts/ui-evidence/yinka-onboarding-superwall/011-mobile-ios-activation-summary.png new file mode 100644 index 00000000000..30b1bca0044 Binary files /dev/null and b/.agent-artifacts/ui-evidence/yinka-onboarding-superwall/011-mobile-ios-activation-summary.png differ diff --git a/.agent-artifacts/ui-evidence/yinka-onboarding-superwall/012-mobile-ios-activation-todos.png b/.agent-artifacts/ui-evidence/yinka-onboarding-superwall/012-mobile-ios-activation-todos.png new file mode 100644 index 00000000000..e118cec1fae Binary files /dev/null and b/.agent-artifacts/ui-evidence/yinka-onboarding-superwall/012-mobile-ios-activation-todos.png differ diff --git a/.agent-artifacts/ui-evidence/yinka-onboarding-superwall/013-mobile-ios-activation-all-set.png b/.agent-artifacts/ui-evidence/yinka-onboarding-superwall/013-mobile-ios-activation-all-set.png new file mode 100644 index 00000000000..03639ca8adc Binary files /dev/null and b/.agent-artifacts/ui-evidence/yinka-onboarding-superwall/013-mobile-ios-activation-all-set.png differ diff --git a/.agent-artifacts/ui-evidence/yinka-onboarding-superwall/014-mobile-ios-activation-rating.png b/.agent-artifacts/ui-evidence/yinka-onboarding-superwall/014-mobile-ios-activation-rating.png new file mode 100644 index 00000000000..339bf60c207 Binary files /dev/null and b/.agent-artifacts/ui-evidence/yinka-onboarding-superwall/014-mobile-ios-activation-rating.png differ diff --git a/.agent-artifacts/ui-evidence/yinka-onboarding-superwall/015-mobile-ios-superwall-paywall-end.png b/.agent-artifacts/ui-evidence/yinka-onboarding-superwall/015-mobile-ios-superwall-paywall-end.png new file mode 100644 index 00000000000..47e5ee2796b Binary files /dev/null and b/.agent-artifacts/ui-evidence/yinka-onboarding-superwall/015-mobile-ios-superwall-paywall-end.png differ diff --git a/.agent-artifacts/ui-evidence/yinka-onboarding-superwall/016-mobile-ios-home-after-onboarding.png b/.agent-artifacts/ui-evidence/yinka-onboarding-superwall/016-mobile-ios-home-after-onboarding.png new file mode 100644 index 00000000000..bcb146aacf8 Binary files /dev/null and b/.agent-artifacts/ui-evidence/yinka-onboarding-superwall/016-mobile-ios-home-after-onboarding.png differ diff --git a/.agent-artifacts/ui-evidence/yinka-onboarding-superwall/017-mobile-ios-chat-welcome-message.png b/.agent-artifacts/ui-evidence/yinka-onboarding-superwall/017-mobile-ios-chat-welcome-message.png new file mode 100644 index 00000000000..f2b60573802 Binary files /dev/null and b/.agent-artifacts/ui-evidence/yinka-onboarding-superwall/017-mobile-ios-chat-welcome-message.png differ diff --git a/.agent-artifacts/ui-evidence/yinka-onboarding-superwall/018-mobile-ios-home-live-pill.png b/.agent-artifacts/ui-evidence/yinka-onboarding-superwall/018-mobile-ios-home-live-pill.png new file mode 100644 index 00000000000..6f8d7559b52 Binary files /dev/null and b/.agent-artifacts/ui-evidence/yinka-onboarding-superwall/018-mobile-ios-home-live-pill.png differ diff --git a/.agent-artifacts/ui-evidence/yinka-onboarding-superwall/019-mobile-ios-activation-control-ending.png b/.agent-artifacts/ui-evidence/yinka-onboarding-superwall/019-mobile-ios-activation-control-ending.png new file mode 100644 index 00000000000..d5cecbfc3fc Binary files /dev/null and b/.agent-artifacts/ui-evidence/yinka-onboarding-superwall/019-mobile-ios-activation-control-ending.png differ diff --git a/.agent-artifacts/ui-evidence/yinka-onboarding-superwall/020-mobile-ios-activation-outline-paywall.png b/.agent-artifacts/ui-evidence/yinka-onboarding-superwall/020-mobile-ios-activation-outline-paywall.png new file mode 100644 index 00000000000..2dc74b07490 Binary files /dev/null and b/.agent-artifacts/ui-evidence/yinka-onboarding-superwall/020-mobile-ios-activation-outline-paywall.png differ diff --git a/.agent-artifacts/ui-evidence/yinka-onboarding-superwall/021-mobile-ios-activation-control-plans-sheet.png b/.agent-artifacts/ui-evidence/yinka-onboarding-superwall/021-mobile-ios-activation-control-plans-sheet.png new file mode 100644 index 00000000000..404332070f0 Binary files /dev/null and b/.agent-artifacts/ui-evidence/yinka-onboarding-superwall/021-mobile-ios-activation-control-plans-sheet.png differ diff --git a/.agent-artifacts/ui-evidence/yinka-onboarding-superwall/022-mobile-ios-paywall-reask-on-resume.png b/.agent-artifacts/ui-evidence/yinka-onboarding-superwall/022-mobile-ios-paywall-reask-on-resume.png new file mode 100644 index 00000000000..9221f078cd9 Binary files /dev/null and b/.agent-artifacts/ui-evidence/yinka-onboarding-superwall/022-mobile-ios-paywall-reask-on-resume.png differ diff --git a/.agent-artifacts/ui-evidence/yinka-onboarding-superwall/023-mobile-ios-settings-upgrade-outline.png b/.agent-artifacts/ui-evidence/yinka-onboarding-superwall/023-mobile-ios-settings-upgrade-outline.png new file mode 100644 index 00000000000..9221f078cd9 Binary files /dev/null and b/.agent-artifacts/ui-evidence/yinka-onboarding-superwall/023-mobile-ios-settings-upgrade-outline.png differ diff --git a/.agent-artifacts/ui-evidence/yinka-onboarding-superwall/evidence.json b/.agent-artifacts/ui-evidence/yinka-onboarding-superwall/evidence.json new file mode 100644 index 00000000000..b4ff4c6658d --- /dev/null +++ b/.agent-artifacts/ui-evidence/yinka-onboarding-superwall/evidence.json @@ -0,0 +1,166 @@ +{ + "version": 1, + "images": [ + { + "file": "001-mobile-ios-onboarding-sign-in.png", + "platform": "mobile-ios", + "description": "Sign-in over the launch film, no Get Started screen (OMI_ONBOARDING_HERO=video; local test build shows one local sign-in button where store builds show Apple/Google)", + "captured_by": "agent", + "source": "screencapture" + }, + { + "file": "002-mobile-ios-onboarding-data-privacy.png", + "platform": "mobile-ios", + "description": "Data & Privacy over the launch film", + "captured_by": "agent", + "source": "screencapture" + }, + { + "file": "003-mobile-ios-onboarding-name.png", + "platform": "mobile-ios", + "description": "Name, focused and ready to type", + "captured_by": "agent", + "source": "screencapture" + }, + { + "file": "004-mobile-ios-onboarding-permissions.png", + "platform": "mobile-ios", + "description": "Permissions as simple toggle cards, both answered", + "captured_by": "agent", + "source": "screencapture" + }, + { + "file": "005-mobile-ios-onboarding-language.png", + "platform": "mobile-ios", + "description": "Language, pre-selected from the phone", + "captured_by": "agent", + "source": "screencapture" + }, + { + "file": "006-mobile-ios-connect-omi.png", + "platform": "mobile-ios", + "description": "Connect Omi with I've ordered mine / Not ordered yet", + "captured_by": "agent", + "source": "screencapture" + }, + { + "file": "007-mobile-ios-activation-intro.png", + "platform": "mobile-ios", + "description": "Activation intro: Let's try Omi", + "captured_by": "agent", + "source": "screencapture" + }, + { + "file": "008-mobile-ios-activation-microphone.png", + "platform": "mobile-ios", + "description": "Microphone ask: the mic control the dictation uses", + "captured_by": "agent", + "source": "screencapture" + }, + { + "file": "009-mobile-ios-activation-dictation-live.png", + "platform": "mobile-ios", + "description": "Dictation: the user's words appear live", + "captured_by": "agent", + "source": "screencapture" + }, + { + "file": "010-mobile-ios-activation-summarizing.png", + "platform": "mobile-ios", + "description": "Summarizing, with the user's words", + "captured_by": "agent", + "source": "screencapture" + }, + { + "file": "011-mobile-ios-activation-summary.png", + "platform": "mobile-ios", + "description": "The first conversation's summary", + "captured_by": "agent", + "source": "screencapture" + }, + { + "file": "012-mobile-ios-activation-todos.png", + "platform": "mobile-ios", + "description": "To-dos found in it, saved to Tasks", + "captured_by": "agent", + "source": "screencapture" + }, + { + "file": "013-mobile-ios-activation-all-set.png", + "platform": "mobile-ios", + "description": "You're all set", + "captured_by": "agent", + "source": "screencapture" + }, + { + "file": "014-mobile-ios-activation-rating.png", + "platform": "mobile-ios", + "description": "Rating ask with the App Store prompt", + "captured_by": "agent", + "source": "screencapture" + }, + { + "file": "015-mobile-ios-superwall-paywall-end.png", + "platform": "mobile-ios", + "description": "Superwall paywall at the end of onboarding (iOS debug pilot; mock products, so prices are blank)", + "captured_by": "agent", + "source": "screencapture" + }, + { + "file": "016-mobile-ios-home-after-onboarding.png", + "platform": "mobile-ios", + "description": "Home after onboarding: the first conversation and a dot on Tasks", + "captured_by": "agent", + "source": "screencapture" + }, + { + "file": "017-mobile-ios-chat-welcome-message.png", + "platform": "mobile-ios", + "description": "Omi's first chat message in an empty chat", + "captured_by": "agent", + "source": "screencapture" + }, + { + "file": "018-mobile-ios-home-live-pill.png", + "platform": "mobile-ios", + "description": "Bottom-bar live pill while the phone records (local server has no transcription, so the Reconnecting card also shows)", + "captured_by": "agent", + "source": "screencapture" + }, + { + "file": "019-mobile-ios-activation-control-ending.png", + "platform": "mobile-ios", + "description": "Control arm: today's guided voice introduction, unchanged", + "captured_by": "agent", + "source": "screencapture" + }, + { + "file": "020-mobile-ios-activation-outline-paywall.png", + "platform": "mobile-ios", + "description": "End of the activation through #21450's router, Outline arm (OMI_PAYWALL_VARIANT=outline; local server has no plan catalog, so prices don't load)", + "captured_by": "agent", + "source": "screencapture" + }, + { + "file": "021-mobile-ios-activation-control-plans-sheet.png", + "platform": "mobile-ios", + "description": "End of the activation, control arm (no overrides): the onboarding plans sheet; closing it goes straight to Home, no second paywall", + "captured_by": "agent", + "source": "screencapture" + }, + { + "file": "022-mobile-ios-paywall-reask-on-resume.png", + "platform": "mobile-ios", + "description": "Paywall re-ask on returning to the app (OMI_PAYWALL_REASK=test, OMI_PAYWALL_REASK_NOW=true to make it due now)", + "captured_by": "agent", + "source": "screencapture" + }, + { + "file": "023-mobile-ios-settings-upgrade-outline.png", + "platform": "mobile-ios", + "description": "Settings > Upgrade through the router, Outline arm", + "captured_by": "agent", + "source": "screencapture" + } + ] +} diff --git a/app/assets/images/omi_pendant_glow.png b/app/assets/images/omi_pendant_glow.png new file mode 100644 index 00000000000..0f3a8f6a604 Binary files /dev/null and b/app/assets/images/omi_pendant_glow.png differ diff --git a/app/assets/images/onboarding.mp4 b/app/assets/images/onboarding.mp4 deleted file mode 100644 index 5a00d2f11d0..00000000000 Binary files a/app/assets/images/onboarding.mp4 and /dev/null differ diff --git a/app/assets/videos/onboarding_launch_loop.mp4 b/app/assets/videos/onboarding_launch_loop.mp4 new file mode 100644 index 00000000000..bf06a90049c Binary files /dev/null and b/app/assets/videos/onboarding_launch_loop.mp4 differ diff --git a/app/changelog/unreleased/20261013-first-capture-onboarding.json b/app/changelog/unreleased/20261013-first-capture-onboarding.json deleted file mode 100644 index 4e349fe04bb..00000000000 --- a/app/changelog/unreleased/20261013-first-capture-onboarding.json +++ /dev/null @@ -1 +0,0 @@ -{"change":"Choose how you capture during setup and turn your first phone recording into a conversation."} diff --git a/app/changelog/unreleased/20261013-onboarding-redesign-first-conversation.json b/app/changelog/unreleased/20261013-onboarding-redesign-first-conversation.json new file mode 100644 index 00000000000..1d37e4cb1da --- /dev/null +++ b/app/changelog/unreleased/20261013-onboarding-redesign-first-conversation.json @@ -0,0 +1,3 @@ +{ + "change": "A simpler first-run setup that helps you connect your Omi, plus a cleaner Home with a New Recording button" +} diff --git a/app/changelog/unreleased/20261014-onboarding-polish.json b/app/changelog/unreleased/20261014-onboarding-polish.json deleted file mode 100644 index 0ee17514c9a..00000000000 --- a/app/changelog/unreleased/20261014-onboarding-polish.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "change": "Clearer recording instructions, actionable pairing errors, and easier onboarding choices." -} diff --git a/app/contracts/addressability/unkeyed-baseline.json b/app/contracts/addressability/unkeyed-baseline.json index 189cfb146dd..256ab5b3088 100644 --- a/app/contracts/addressability/unkeyed-baseline.json +++ b/app/contracts/addressability/unkeyed-baseline.json @@ -98,10 +98,8 @@ "app/lib/pages/onboarding/apple_watch_permission_page.dart": 9, "app/lib/pages/onboarding/auth.dart": 6, "app/lib/pages/onboarding/complete_screen.dart": 2, - "app/lib/pages/onboarding/device_selection.dart": 2, "app/lib/pages/onboarding/find_device/found_devices.dart": 1, "app/lib/pages/onboarding/find_device/page.dart": 2, - "app/lib/pages/onboarding/found_omi/found_omi_widget.dart": 4, "app/lib/pages/onboarding/interactive_device_onboarding/interactive_device_onboarding_wrapper.dart": 1, "app/lib/pages/onboarding/interactive_device_onboarding/steps/double_press_config_step.dart": 1, "app/lib/pages/onboarding/interactive_device_onboarding/widgets/onboarding_intro_screen.dart": 2, diff --git a/app/docs/superwall-pilot.md b/app/docs/superwall-pilot.md new file mode 100644 index 00000000000..966144eb067 --- /dev/null +++ b/app/docs/superwall-pilot.md @@ -0,0 +1,29 @@ +# Superwall paywall pilot + +Upgrade entry points (end of onboarding, locked conversations and memories, chat quota, Settings, Usage) go through the paywall experiment (`PaywallRouter`, flag `paywall-variant`): its arm's page, or Omi's existing plans sheet and Stripe checkout for control, paid readers and any failure. On **iOS debug builds** with `--dart-define=SUPERWALL_TEST_MODE=true`, free users get a **Superwall** paywall first instead, using the pilot's own Superwall test project (public SDK key built in as the default); `--dart-define=SUPERWALL_API_KEY_IOS=` points at another project. Store builds and Android never configure Superwall. + +## Before production + +- Omi sets up its own Superwall project (paywall design, campaigns for the placements below) and replaces the test key. +- The plans are created as in-app purchase subscriptions in **App Store Connect** and attached to the paywall's product slots (the test project uses mock products, so prices show blank). +- Purchases must be reconciled to Omi subscriptions on the backend (Superwall webhooks or StoreKit server notifications), with restore and manage-subscription paths, before the pilot gate is removed. + +## Dashboard setup + +1. Use the public iOS SDK key from the Superwall project's Settings → API Keys. Never use a webhook secret or store private key in a `--dart-define` or commit it. +2. Attach the Calorie Tracker (Template) paywall to an isolated campaign for placement `omi_usage_upgrade_test`. Its sample purchase buttons are only for visual testing until sandbox products are configured. +3. Run the app in the dev flavor with the local profile. Example for iOS: + + ```sh + flutter run --flavor dev --dart-define=OMI_APP_PROFILE=local_dev --dart-define=SUPERWALL_TEST_MODE=true --dart-define=SUPERWALL_API_KEY_IOS=YOUR_PUBLIC_TEST_KEY + ``` + + The current app setup and local auth emulator must also be running; see `app/AGENTS.md` and `app/setup.sh`. + +4. Sign in, open Settings → Usage, and tap Upgrade. The pilot identifies the signed-in Firebase UID before registering the placement. Signing out resets the Superwall identity. + +## Boundaries + +- A configured paywall can offer a store purchase. Use sandbox/test store products and accounts only. This branch does not ingest Superwall purchase webhooks, reconcile App Store/Play purchases to Omi subscriptions, or grant Omi entitlements. A successful store purchase must not be treated as a successful Omi subscription until that backend path exists. +- The app's iOS deployment target (15) meets the SDK's documented minimum. +- Before a production rollout, decide the billing/catalog policy, add verified purchase lifecycle handling, reconcile Omi UID with Superwall's original user ID, add restore/manage behavior, and run store sandbox purchase and entitlement tests on devices. diff --git a/app/docs/ux-contract.md b/app/docs/ux-contract.md index 20d8162f1f1..4030a7bb82b 100644 --- a/app/docs/ux-contract.md +++ b/app/docs/ux-contract.md @@ -304,6 +304,11 @@ guidance. Open Settings) — then the system prompt. A screen's Continue asks, in row order, for every permission still askable (`requestMissingOnboardingPermissions`), then moves on whatever the reader chose. +- First-run exception (`pages/onboarding/permissions/permissions_widget.dart`): one switch card per + permission; toggling a card shows its system prompt. Continue unlocks once every card is + **answered**, never only when granted: a "no" turns the card orange (notifications red with + "Tap to fix", which opens Settings) and the reader moves on. Bluetooth is asked as the app + opens, so its card appears only when it was not allowed. - Permanently denied → the row says so and offers **Open Settings** (`l10n.openSettings`). - Ask when a feature needs the permission ("Always" location only when a feature requires it). diff --git a/app/e2e/SKILL.md b/app/e2e/SKILL.md index 6ca38a1f489..2a82546d369 100644 --- a/app/e2e/SKILL.md +++ b/app/e2e/SKILL.md @@ -123,13 +123,15 @@ Onboarding (wrapper.dart) — step wizard ├── 0: Auth (auth.dart) — Google/Apple sign-in ├── 1: AI Consent ├── 2: Name (name_widget.dart) -├── 3: Primary Language (primary_language_widget.dart) -├── 4: Found Omi (found_omi_widget.dart) -├── 5: Permissions (permissions_widget.dart) -├── 6: User Review (user_review_page.dart) -├── 9: Speech Profile (speech_profile_widget.dart) -├── 10: Knowledge Graph (knowledge_graph_step.dart) -└── 11: Complete (complete_screen.dart) → Home +├── 3: Permissions (permissions_widget.dart) +├── 4: Primary Language (primary_language_widget.dart) +├── 5: Connect Omi (connect_omi/connect_omi_step.dart) +├── 6: Ending, A/B by flag `onboarding-activation-flow`: +│ control → Speech Profile (speech_profile_widget.dart); +│ treatment → Activation (activation/activation_flow.dart) → Home +├── 7: Knowledge Graph (knowledge_graph_step.dart, control only) +├── 8: Setup (setup_page.dart, control only, flag-gated) +└── 9: Complete (complete_screen.dart, control only) → Home Home (home/page.dart) — main app after auth, 4-slot bottom nav ├── ["Ask Omi" input bar] → Chat (chat/page.dart) — full-width bar above bottom nav, not a tab diff --git a/app/e2e/flows/onboarding.yaml b/app/e2e/flows/onboarding.yaml index 819c4779270..a6c316ea30f 100644 --- a/app/e2e/flows/onboarding.yaml +++ b/app/e2e/flows/onboarding.yaml @@ -13,7 +13,8 @@ covers: - app/lib/pages/onboarding/auth.dart - app/lib/pages/onboarding/name/name_widget.dart - app/lib/pages/onboarding/primary_language/primary_language_widget.dart - - app/lib/pages/onboarding/found_omi/found_omi_widget.dart + - app/lib/pages/onboarding/connect_omi/connect_omi_step.dart + - app/lib/pages/onboarding/activation/activation_flow.dart - app/lib/pages/onboarding/permissions/permissions_widget.dart - app/lib/pages/onboarding/speech_profile_widget.dart - app/lib/pages/onboarding/knowledge_graph_step.dart diff --git a/app/integration_test/app_performance_test.dart b/app/integration_test/app_performance_test.dart index b0384e1edab..2dc301ff134 100644 --- a/app/integration_test/app_performance_test.dart +++ b/app/integration_test/app_performance_test.dart @@ -95,7 +95,7 @@ void main() { debugPrint('[4/7] Navigating to CHAT screen...'); // Find and tap the "Ask Omi" button - final askOmiButton = find.text('Ask Omi'); + final askOmiButton = find.byKey(const ValueKey('home_ask_omi_button')); if (askOmiButton.evaluate().isNotEmpty) { await tester.tap(askOmiButton); await _pumpFor(tester, 2000); @@ -185,7 +185,7 @@ Future _handleOnboardingIfNeeded(WidgetTester tester) async { debugPrint('[2/7] Checking for onboarding...'); // Check if we're already on home screen - final askOmi = find.text('Ask Omi'); + final askOmi = find.byKey(const ValueKey('home_ask_omi_button')); if (askOmi.evaluate().isNotEmpty) { debugPrint(' Already logged in - on home screen'); return; @@ -275,7 +275,7 @@ Future _handleAuthFlow(WidgetTester tester) async { await tester.pump(const Duration(milliseconds: 500)); // Check if we've reached home screen - final askOmi = find.text('Ask Omi'); + final askOmi = find.byKey(const ValueKey('home_ask_omi_button')); if (askOmi.evaluate().isNotEmpty) { debugPrint(' Sign-in complete - reached home screen'); return; @@ -377,7 +377,7 @@ Future _handleUserReviewScreen(WidgetTester tester) async { } // Check if we've already passed to home screen - final askOmi = find.text('Ask Omi'); + final askOmi = find.byKey(const ValueKey('home_ask_omi_button')); if (askOmi.evaluate().isNotEmpty) { debugPrint(' Already on home screen - skipping user review'); return; @@ -484,7 +484,7 @@ Future _handleSpeechProfileScreen(WidgetTester tester) async { await _pumpFor(tester, 500); // Check if we're already on home screen - final askOmi = find.text('Ask Omi'); + final askOmi = find.byKey(const ValueKey('home_ask_omi_button')); if (askOmi.evaluate().isNotEmpty) { debugPrint(' Onboarding complete - reached home screen'); return; diff --git a/app/integration_test/onboarding_header_capture.dart b/app/integration_test/onboarding_header_capture.dart index 3466c0ce895..1251992c10e 100644 --- a/app/integration_test/onboarding_header_capture.dart +++ b/app/integration_test/onboarding_header_capture.dart @@ -84,7 +84,7 @@ class _CaptureAppState extends State<_CaptureApp> { child: Scaffold( body: OnboardingStepLayout( reserveHeader: _fixed, - progress: const OnboardingProgressDots(current: 4, total: 6), + progress: const OnboardingProgressLine(current: 4, total: 6), onBack: () {}, child: SpeechProfileWidget(controller: _flow, goNext: () {}, onSkip: () {}), ), diff --git a/app/integration_test/visual_audit/scenarios/capture.dart b/app/integration_test/visual_audit/scenarios/capture.dart index 425cd289d0c..c794d1eccc8 100644 --- a/app/integration_test/visual_audit/scenarios/capture.dart +++ b/app/integration_test/visual_audit/scenarios/capture.dart @@ -1,7 +1,7 @@ // Home's capture surfaces where the app really draws them: the live card at the top of Home, the -// record button to the right of the floating Ask Omi bar, the device chip in the header, -// the sheets they open, and the live page. The frame mirrors HomePage's layout; its Ask Omi bar -// is a copy of HomePage._buildChatBar (private there). +// record pill beside the Ask Omi button at the bottom ("+ New", "Stop", or the capturing pendant), +// the device chip in the header, the sheets they open, and the live page. The frame mirrors +// HomePage's layout. import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; @@ -14,7 +14,9 @@ import 'package:omi/backend/schema/phone_call.dart'; import 'package:omi/backend/schema/transcript_segment.dart'; import 'package:omi/pages/conversation_capturing/page.dart'; import 'package:omi/pages/home/home_content.dart'; +import 'package:omi/pages/action_items/widgets/tasks_more_menu.dart'; import 'package:omi/pages/home/widgets/battery_info_widget.dart'; +import 'package:omi/pages/home/widgets/home_bottom_controls.dart'; import 'package:omi/pages/home/widgets/home_daily_recaps.dart'; import 'package:omi/pages/home/widgets/home_tab_switcher.dart'; import 'package:omi/backend/schema/daily_summary.dart'; @@ -206,7 +208,18 @@ class HomeFrame extends StatelessWidget { ), body: Stack(children: [ Column(children: [ - HomeTabSwitcher(onTabTap: (_, __) {}), + // The selected page's one action beside the switcher, as HomePage draws it. + HomeTabSwitcher( + onTabTap: (_, __) {}, + trailing: tasks != null + ? const TasksMoreMenu() + : OmiIconButton( + icon: const FaIcon(FontAwesomeIcons.calendar, size: 18), + label: 'Filter by date', + color: OmiColors.textSecondary, + onPressed: () {}, + ), + ), Expanded(child: tasks ?? HomeContentPage(loadRecaps: recaps)), ]), if (tasks == null) const HomeChatBarBackdrop(), @@ -215,40 +228,17 @@ class HomeFrame extends StatelessWidget { left: 16, right: 16, bottom: homeChatBarOffset(context), - child: const Row(children: [Expanded(child: _AskOmiBar()), SizedBox(width: 10), HomeRecordButton()]), + child: Row(children: [ + HomeChatButton(onTap: () {}, onLongPress: () {}), + const SizedBox(width: 10), + const Expanded(child: HomeRecordButton()), + ]), ), ]), ); } } -/// Copy of HomePage._buildChatBar's look. -class _AskOmiBar extends StatelessWidget { - const _AskOmiBar(); - - @override - Widget build(BuildContext context) => Container( - height: kHomeChatBarHeight, - decoration: BoxDecoration( - color: OmiColors.surface1, - borderRadius: OmiRadius.pillAll, - border: Border.all(color: OmiColors.border, width: 1), - ), - child: Row(children: [ - const SizedBox(width: 18), - Expanded(child: Text('Ask Omi', style: OmiType.subhead.copyWith(color: OmiColors.textTertiary))), - Container( - width: 42, - height: 42, - margin: const EdgeInsets.only(right: 6), - alignment: Alignment.center, - decoration: BoxDecoration(color: OmiColors.accent, shape: BoxShape.circle), - child: FaIcon(FontAwesomeIcons.microphone, size: 15, color: OmiColors.onAccent), - ), - ]), - ); -} - Future _runHome( AuditRun a, AuditLive live, { @@ -387,11 +377,11 @@ final captureScenarios = [ ), AuditScenario( id: 'home-capture-pendant-tap', - title: 'Tap the record button while the pendant records', + title: 'Hold the pendant pill while the pendant records', page: _home, - state: 'The pendant streams; the record button is tapped', + state: 'The pendant streams; its pill (in place of + New) is held', run: (a) => _runHome(a, AuditLive.pendant, - pendantConnected: true, tap: find.byType(HomeRecordButton), action: 'Tap the record button'), + pendantConnected: true, longPress: find.byType(HomeRecordButton), action: 'Hold the pendant pill'), ), AuditScenario( id: 'home-capture-phone-after-pendant', @@ -418,9 +408,9 @@ final captureScenarios = [ id: 'home-capture-options', title: 'Other ways to record', page: _home, - state: 'Nothing recording; the record button is held (the ⌄ badge opens the same sheet)', + state: 'Nothing recording; + New is held', run: (a) async { - await _runHome(a, AuditLive.idle, longPress: find.byType(HomeRecordButton), action: 'Hold the record button'); + await _runHome(a, AuditLive.idle, longPress: find.byType(HomeRecordButton), action: 'Hold + New'); }, ), AuditScenario( diff --git a/app/integration_test/visual_audit/scenarios/onboarding.dart b/app/integration_test/visual_audit/scenarios/onboarding.dart index 0c6b15f985b..b8d2cf01b29 100644 --- a/app/integration_test/visual_audit/scenarios/onboarding.dart +++ b/app/integration_test/visual_audit/scenarios/onboarding.dart @@ -5,7 +5,6 @@ import 'package:provider/provider.dart'; import 'package:omi/pages/onboarding/ai_consent_widget.dart'; import 'package:omi/pages/onboarding/find_device/page.dart'; -import 'package:omi/pages/onboarding/found_omi/found_omi_widget.dart'; import 'package:omi/pages/onboarding/name/name_widget.dart'; import 'package:omi/pages/onboarding/permissions/onboarding_permissions_panel.dart'; import 'package:omi/pages/onboarding/permissions/permissions_widget.dart'; @@ -28,16 +27,6 @@ final onboardingScenarios = [ await a.shot('AI consent: Agree & Continue and Use a Different Account'); }, ), - AuditScenario( - id: 'onboarding-found-omi', - title: '"Where did you hear about Omi" survey', - page: 'lib/pages/onboarding/found_omi/found_omi_widget.dart (FoundOmiWidget)', - state: 'No answer chosen yet', - run: (a) async { - await a.pump(FoundOmiWidget(goNext: () {})); - await a.shot('The survey with its Skip affordance'); - }, - ), AuditScenario( id: 'onboarding-name', title: 'Name step', @@ -71,10 +60,10 @@ final onboardingScenarios = [ AuditScenario( id: 'onboarding-progress-dots', title: 'Onboarding progress dots', - page: 'lib/pages/onboarding/wrapper.dart (OnboardingProgressDots)', + page: 'lib/pages/onboarding/wrapper.dart (OnboardingProgressLine)', state: 'Step 2 of 6', run: (a) async { - await a.pump(const Center(child: OnboardingProgressDots(current: 1, total: 6))); + await a.pump(const Center(child: OnboardingProgressLine(current: 1, total: 6))); await a.shot('Progress dots on step 2 of 6'); }, ), diff --git a/app/ios/Podfile.lock b/app/ios/Podfile.lock index 2b74e5026f3..f9efb1faf50 100644 --- a/app/ios/Podfile.lock +++ b/app/ios/Podfile.lock @@ -270,6 +270,12 @@ PODS: - sqflite_darwin (0.0.4): - Flutter - FlutterMacOS + - Superscript (1.0.15) + - SuperwallKit (4.17.0): + - Superscript (= 1.0.15) + - superwallkit_flutter (0.0.1): + - Flutter + - SuperwallKit (= 4.17.0) - SwiftCBOR (0.4.4) - SwiftProtobuf (1.36.1) - SwiftyGif (5.4.5) @@ -330,6 +336,7 @@ DEPENDENCIES: - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) - sign_in_with_apple (from `.symlinks/plugins/sign_in_with_apple/darwin`) - sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`) + - superwallkit_flutter (from `.symlinks/plugins/superwallkit_flutter/ios`) - TwilioVoice (~> 6.13) - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) - video_player_avfoundation (from `.symlinks/plugins/video_player_avfoundation/darwin`) @@ -371,6 +378,8 @@ SPEC REPOS: - PromisesSwift - RecaptchaInterop - SDWebImage + - Superscript + - SuperwallKit - SwiftCBOR - SwiftProtobuf - SwiftyGif @@ -464,6 +473,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/sign_in_with_apple/darwin" sqflite_darwin: :path: ".symlinks/plugins/sqflite_darwin/darwin" + superwallkit_flutter: + :path: ".symlinks/plugins/superwallkit_flutter/ios" url_launcher_ios: :path: ".symlinks/plugins/url_launcher_ios/ios" video_player_avfoundation: @@ -501,7 +512,7 @@ SPEC CHECKSUMS: FirebaseMessaging: 2b9f56aa4ed286e1f0ce2ee1d413aabb8f9f5cb9 FirebaseRemoteConfigInterop: 1c6135e8a094cc6368949f5faeeca7ee8948b8aa FirebaseSessions: 9b3b30947b97a15370e0902ee7a90f50ef60ead6 - Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467 + Flutter: 71a624a5bc0c04062bf19101d501e466baf2fb47 flutter_archive: ad8edfd7f7d1bb12058d05424ba93e27d9930efe flutter_background_service_ios: 00d31bdff7b4bfe06d32375df358abe0329cf87e flutter_contacts: 2ec1d6b62ca2869c3bbb3871bd74d4e3ef157135 @@ -550,6 +561,9 @@ SPEC CHECKSUMS: shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb sign_in_with_apple: 5e34677a4e74c5f42760f41b3bbe05343e84d171 sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 + Superscript: 55de63e7dc445d0f095c44f25d446889c7ba851d + SuperwallKit: 3e013cb58ac8c1f37b5311b77cedd32b7938a0cc + superwallkit_flutter: 66adf57e7bc55ca535c243a1652cb7d03abb7528 SwiftCBOR: ce5354ec8b660da2d6fc754462881119dbe1f963 SwiftProtobuf: 9e106a71456f4d3f6a3b0c8fd87ef0be085efc38 SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4 diff --git a/app/lib/backend/preferences.dart b/app/lib/backend/preferences.dart index 86ea9708abe..d045153b4db 100644 --- a/app/lib/backend/preferences.dart +++ b/app/lib/backend/preferences.dart @@ -995,6 +995,11 @@ class SharedPreferencesUtil { set hasSpeakerProfile(bool value) => saveBool('hasSpeakerProfile', value); + /// The reader closed Home's "Teach Omi your voice" card; Settings > Voice Profile still has it. + bool get voiceProfileCardDismissed => getBool('voiceProfileCardDismissed'); + + set voiceProfileCardDismissed(bool value) => saveBool('voiceProfileCardDismissed', value); + bool get showDiscardedMemories => getBool('showDiscardedMemories', defaultValue: false); set showDiscardedMemories(bool value) => saveBool('showDiscardedMemories', value); diff --git a/app/lib/coordinators/provider_capture_external_actions.dart b/app/lib/coordinators/provider_capture_external_actions.dart index e50f55fb9fe..ce093d768a6 100644 --- a/app/lib/coordinators/provider_capture_external_actions.dart +++ b/app/lib/coordinators/provider_capture_external_actions.dart @@ -38,6 +38,7 @@ class ProviderCaptureExternalActions implements CaptureExternalActions { List> data, { required VoidCallback onFirstChunkRecived, required Future Function() onNoSpeech, + Future Function()? onFailed, required BleAudioCodec codec, required bool playResponseAudio, }) { @@ -45,6 +46,7 @@ class ProviderCaptureExternalActions implements CaptureExternalActions { data, onFirstChunkRecived: onFirstChunkRecived, onNoSpeech: onNoSpeech, + onFailed: onFailed, codec: codec, playResponseAudio: playResponseAudio, ); diff --git a/app/lib/gen/assets.gen.dart b/app/lib/gen/assets.gen.dart index 0bde56a9f1c..b145aba1b5c 100644 --- a/app/lib/gen/assets.gen.dart +++ b/app/lib/gen/assets.gen.dart @@ -249,6 +249,10 @@ class $AssetsImagesGen { AssetGenImage get omiWithoutRope => const AssetGenImage('assets/images/omi-without-rope.webp'); + /// File path: assets/images/omi_pendant_glow.png + AssetGenImage get omiPendantGlow => + const AssetGenImage('assets/images/omi_pendant_glow.png'); + /// File path: assets/images/onboarding-bg-1.webp AssetGenImage get onboardingBg1 => const AssetGenImage('assets/images/onboarding-bg-1.webp'); @@ -277,9 +281,6 @@ class $AssetsImagesGen { AssetGenImage get onboardingBg6 => const AssetGenImage('assets/images/onboarding-bg-6.webp'); - /// File path: assets/images/onboarding.mp4 - String get onboarding => 'assets/images/onboarding.mp4'; - /// File path: assets/images/plaud_note_pin.webp AssetGenImage get plaudNotePin => const AssetGenImage('assets/images/plaud_note_pin.webp'); @@ -387,6 +388,7 @@ class $AssetsImagesGen { omiWithoutRopeGreenCharging, omiWithoutRopeTurnedOff, omiWithoutRope, + omiPendantGlow, onboardingBg1, onboardingBg2, onboardingBg3, @@ -394,7 +396,6 @@ class $AssetsImagesGen { onboardingBg51, onboardingBg52, onboardingBg6, - onboarding, plaudNotePin, raybanMeta, recordingGreenCircleIcon, @@ -505,6 +506,16 @@ class $AssetsTestGen { List get values => [ttsProgressiveProbe]; } +class $AssetsVideosGen { + const $AssetsVideosGen(); + + /// File path: assets/videos/onboarding_launch_loop.mp4 + String get onboardingLaunchLoop => 'assets/videos/onboarding_launch_loop.mp4'; + + /// List of all assets + List get values => [onboardingLaunchLoop]; +} + abstract final class Assets { static const $AssetsCompetitorLogosGen competitorLogos = $AssetsCompetitorLogosGen(); @@ -514,6 +525,7 @@ abstract final class Assets { $AssetsIntegrationAppLogosGen(); static const $AssetsModelsGen models = $AssetsModelsGen(); static const $AssetsTestGen test = $AssetsTestGen(); + static const $AssetsVideosGen videos = $AssetsVideosGen(); static const String shorebird = 'shorebird.yaml'; /// List of all assets diff --git a/app/lib/l10n/app_ar.arb b/app/lib/l10n/app_ar.arb index db38ea9562c..d80d470c805 100644 --- a/app/lib/l10n/app_ar.arb +++ b/app/lib/l10n/app_ar.arb @@ -520,7 +520,7 @@ "dontShowAgain": "عدم الإظهار مجدداً", "iUnderstand": "أفهم", "enableBluetooth": "تمكين البلوتوث", - "bluetoothNeeded": "يحتاج Omi إلى البلوتوث للاتصال بجهازك القابل للارتداء. يرجى تمكين البلوتوث والمحاولة مرة أخرى.", + "bluetoothNeeded": "يتصل Omi بهاتفك عبر البلوتوث. شغّله للمتابعة.", "contactSupport": "الاتصال بالدعم؟", "connectLater": "الاتصال لاحقاً", "grantPermissions": "منح الأذونات", @@ -1512,7 +1512,7 @@ "durationSeconds": "المدة: {seconds} ثانية", "additionalSpeechSampleRemoved": "تمت إزالة عينة الكلام الإضافية", "consentDataMessage": "بالمتابعة، سيتم تخزين محادثاتك وتسجيلاتك ومعلوماتك الشخصية بشكل آمن على خوادمنا. تتم معالجة تسجيلاتك الصوتية ونصوصك بواسطة خدمات ذكاء اصطناعي تابعة لجهات خارجية (بما في ذلك Deepgram للنسخ و OpenAI للتحليل) لتزويدك برؤى مدعومة بالذكاء الاصطناعي وتمكين جميع ميزات التطبيق.", - "tasksEmptyStateMessage": "ابدأ محادثة لإنشاء مهمة.", + "tasksEmptyStateMessage": "ستظهر هنا المهام من محادثاتك.", "clearChatAction": "مسح المحادثة", "enableApps": "تفعيل التطبيقات", "omiAppName": "Omi", @@ -4004,6 +4004,49 @@ "reviewProposalSuggested": "مقترح", "reviewProposalConflict": "تغيّر هذا منذ أن اقترحه Dream، لذا لم يُطبَّق أي شيء.", "itemLinkListTitle": "{count, plural, =0{لا توجد عناصر} =1{عنصر واحد} other{{count} عناصر}}", + "onboardingPermissionTapToFix": "اضغط للإصلاح", + "onboardingNoOmiYet": "ليس لديك Omi بعد؟", + "onboardingOmiOrdered": "لقد طلبته", + "onboardingOmiNotOrdered": "لم أطلبه بعد", + "onboardingConnectOmi": "اتصل بـ Omi", + "consentShortMessage": "يستخدم Omi الذكاء الاصطناعي لتحويل محادثاتك إلى ملاحظات. يمكنك حذفها في أي وقت.", + "consentAiProviders": "تتم المعالجة بواسطة {providers}.", + "onboardingActIntro": "لنجرّب Omi.", + "onboardingActMicTitle": "يحتاج Omi إلى الميكروفون", + "onboardingActMicBody": "ليسمعك Omi عندما تتحدث إليه.", + "onboardingActMicAllow": "السماح بالميكروفون", + "onboardingActDictateTitle": "أخبر Omi عن يومك", + "onboardingActDictateBody": "قلها بطريقتك، وOmi يتولى الباقي.", + "onboardingActDictateExample": "تغدّيت للتو مع سام، ستنتقل إلى لشبونة في يونيو! ذكّرني بإرسال العرض إلى فريقي قبل الجمعة، وبشراء الحليب في طريق العودة.", + "onboardingActDictateHint": "اضغط على علامة الصح عند الانتهاء", + "onboardingActPhoneMic": "ميكروفون الهاتف", + "onboardingActWriting": "جارٍ التلخيص…", + "onboardingActRemembers": "يتذكّر Omi عنك حتى لا تضطر أنت", + "onboardingActNotesLabel": "يومك، باختصار", + "onboardingActTasksLabel": "التقط Omi مهامك", + "onboardingActYourWords": "هذا ما قلته", + "onboardingActCongratsTitle": "كل شيء جاهز يا {name}", + "onboardingActRateTitle": "هل يعجبك Omi حتى الآن؟", + "onboardingActRateYes": "قيّم Omi", + "onboardingWelcomeCardTitle": "مرحبًا بك في Omi", + "onboardingWelcomeCardBody": "ارتدِ Omi وتحدّث كعادتك. تتحول كل محادثة إلى ملاحظات ومهام وذكريات هنا.", + "onboardingActDemoTitle": "غداء مع سام", + "onboardingActDemoSummary": "غداء مع سام التي ستنتقل إلى لشبونة في يونيو. تريد إرسال العرض التقديمي إلى فريقك بحلول الجمعة وشراء الحليب في طريق العودة.", + "onboardingActDemoTask1": "إرسال العرض إلى الفريق قبل الجمعة", + "onboardingActDemoTask2": "شراء الحليب في طريق العودة", + "onboardingActJustNow": "الآن", + "onboardingActSavedToTasks": "تم الحفظ في مهامك", + "onboardingConnectTapToConnect": "اضغط على Omi للاتصال.", + "onboardingActMicFallbackBody": "لا يرسل Omi الصوت بعد. استخدم هاتفك الآن.", + "onboardingActNoAudioOmi": "لا أسمع Omi. تأكد من أنه يعمل وقريب منك.", + "onboardingActNoAudioPhone": "لا أسمعك بعد. جرّب التحدث بصوت أعلى قليلاً.", + "onboardingActDictateBodyOmi": "تحدّث فقط. Omi يستمع.", + "voiceProfileCardDismissed": "يمكنك إعداد بصمة صوتك في أي وقت من الإعدادات.", + "homeNewRecordingButton": "تسجيل جديد", + "chatWelcomeMessage": "مرحبًا، أنا Omi. اسألني عن أي شيء تحدثت عنه: ما قاله شخص ما، أو ما وعدت به، أو ملخص يومك. يمكنني أيضًا إضافة مهام وتذكّر الأشياء لك.", + "onboardingStarterTaskAsk": "اسأل Omi عن يومك", + "onboardingStarterTaskRecord": "سجّل محادثة مع Omi", + "onboardingStarterTaskCheck": "ضع علامة على هذه المهمة لإنهائها", "paywallRecommended": "Recommended", "paywallNoPlans": "Plans aren't available right now. Try again in a minute.", "paywallOtherPlans": "See all plans", @@ -4014,21 +4057,6 @@ "paywallManageBilling": "Manage billing", "paywallLoadError": "Couldn't load plans. Check your connection and try again.", "paywallPlans": "Plans", - "firstCaptureTitle": "How will you capture?", - "firstCaptureDescription": "Choose how you want to capture your first conversation.", - "firstCaptureOmi": "I Have an Omi", - "firstCapturePhone": "This Phone", - "firstCaptureWatch": "Apple Watch", - "firstCaptureSkip": "Skip for Now", - "firstCapturePrompt": "Tell Omi what you're working on today, or what's on your mind. Talk for about 30 seconds.", - "firstCaptureKeepGoing": "Keep going… 30 seconds is ideal.", - "firstCaptureReady": "You can finish now, or keep talking.", - "firstCaptureStart": "Start Recording", - "firstCaptureFinish": "Finish Recording", - "firstCaptureListening": "Listening… your words will appear here.", - "firstCaptureProcessing": "Creating your first conversation…", - "firstCaptureResult": "Your first conversation", - "firstCaptureEmpty": "I didn't catch enough speech, try again with a few sentences", "watchCaptureTitle": "Record from your Apple Watch", "watchCaptureOpen": "Open Omi on your Apple Watch.", "watchCaptureRecord": "Tap Record. Allow microphone access if asked.", @@ -4040,17 +4068,6 @@ "pairingNotFoundOrDropped": "Couldn't reach your Omi. Make sure it's charged, close to your phone, and its light is blinking, then try again.", "pairingBondRecovery": "Forget Omi in your phone’s Bluetooth settings, then try pairing again.", "pairingHowTo": "How to pair", - "firstCaptureWatchInstructions": "Open Omi on your Apple Watch and tap Record. Allow microphone access if asked. Your capture will sync to Omi on this phone.", - "firstCaptureWatchInstall": "In the Watch app on your iPhone, install Omi on your Apple Watch. Then open Omi on your watch and tap Record.", - "firstCaptureWatchPair": "Pair your Apple Watch with this iPhone using the Watch app, then install and open Omi on your watch.", - "firstCaptureUnavailable": "Recording could not start. Check microphone access and your connection, then try again.", - "firstCaptureDelayed": "Your recording is taking longer to process. You can check your conversations on Home or try again.", - "firstCaptureEmptyTitle": "Let's try that again", - "firstCaptureShortTitle": "Finish This Recording?", - "firstCaptureShortWarning": "A few more sentences help Omi understand you. About 30 seconds is ideal.", - "firstCaptureCancelTitle": "Leave This Recording?", - "firstCaptureCancelWarning": "Going back stops this recording. Keep talking to give Omi a few more sentences.", - "firstCaptureSiriTip": "Tip: Enable Omi in Shortcuts → Siri.", "paywallOutlineHeadline": "لا تفوّت كلمة واحدة بعد الآن.", "paywallCancelAnytime": "يمكنك الإلغاء في أي وقت.", "connectAnAgent": "ربط وكيل", diff --git a/app/lib/l10n/app_be.arb b/app/lib/l10n/app_be.arb index d06f931f1e5..bdc655e07dd 100644 --- a/app/lib/l10n/app_be.arb +++ b/app/lib/l10n/app_be.arb @@ -1586,7 +1586,7 @@ "@enableBluetooth": { "description": "Title for dialog prompting user to enable Bluetooth" }, - "bluetoothNeeded": "Omi трэба Bluetooth для падлучэння да вашага нашпігальнага прыстроя. Калі ласка, уключыце Bluetooth і спробуйце яшчэ раз.", + "bluetoothNeeded": "Omi падключаецца да тэлефона праз Bluetooth. Уключыце яго, каб працягнуць.", "@bluetoothNeeded": { "description": "Explanation text for why Bluetooth is required" }, @@ -5782,7 +5782,7 @@ "@consentDataMessage": { "description": "Consent message explaining how user data will be stored and used" }, - "tasksEmptyStateMessage": "Пачніце размову, каб стварыць задачу.", + "tasksEmptyStateMessage": "Справы з вашых размоў з'явяцца тут.", "@tasksEmptyStateMessage": { "description": "Empty state message shown when there are no tasks, with instruction to tap + button" }, @@ -11430,6 +11430,49 @@ "reviewProposalSuggested": "Прапанавана", "reviewProposalConflict": "Гэта змянілася пасля прапановы Dream, таму нічога не ўжыта.", "itemLinkListTitle": "{count, plural, =0{Няма элементаў} =1{1 элемент} other{{count} элементаў}}", + "onboardingPermissionTapToFix": "Націсніце, каб выправіць", + "onboardingNoOmiYet": "Яшчэ няма Omi?", + "onboardingOmiOrdered": "Я ўжо замовіў", + "onboardingOmiNotOrdered": "Яшчэ не замовіў", + "onboardingConnectOmi": "Падключыце Omi", + "consentShortMessage": "Omi з дапамогай ШІ ператварае вашы размовы ў нататкі. Іх можна выдаліць у любы момант.", + "consentAiProviders": "Апрацоўка: {providers}.", + "onboardingActIntro": "Паспрабуем Omi.", + "onboardingActMicTitle": "Omi патрэбны мікрафон", + "onboardingActMicBody": "Каб Omi чуў вас, калі вы з ім размаўляеце.", + "onboardingActMicAllow": "Дазволіць мікрафон", + "onboardingActDictateTitle": "Раскажыце Omi пра свой дзень", + "onboardingActDictateBody": "Кажыце па-свойму. Астатняе зробіць Omi.", + "onboardingActDictateExample": "Толькі што паабедаў з Сэм, яна пераязджае ў Лісабон у чэрвені! Нагадай мне адправіць прэзентацыю камандзе да пятніцы і купіць малако па дарозе дадому.", + "onboardingActDictateHint": "Націсніце галачку, калі скончыце", + "onboardingActPhoneMic": "Мікрафон тэлефона", + "onboardingActWriting": "Падсумоўваем…", + "onboardingActRemembers": "Omi памятае, каб вам не трэба было", + "onboardingActNotesLabel": "Ваш дзень коратка", + "onboardingActTasksLabel": "Omi заўважыў вашы справы", + "onboardingActYourWords": "Вось што вы сказалі", + "onboardingActCongratsTitle": "Усё гатова, {name}", + "onboardingActRateTitle": "Як вам Omi?", + "onboardingActRateYes": "Ацаніць Omi", + "onboardingWelcomeCardTitle": "Вітаем у Omi", + "onboardingWelcomeCardBody": "Насіце Omi і размаўляйце як звычайна. Кожная размова ператворыцца тут у нататкі, задачы і ўспаміны.", + "onboardingActDemoTitle": "Абед з Сэм", + "onboardingActDemoSummary": "Абед з Сэм, якая ў чэрвені пераязджае ў Лісабон. Вы хочаце адправіць прэзентацыю камандзе да пятніцы і купіць малако па дарозе дадому.", + "onboardingActDemoTask1": "Адправіць прэзентацыю камандзе да пятніцы", + "onboardingActDemoTask2": "Купіць малако па дарозе дадому", + "onboardingActJustNow": "Толькі што", + "onboardingActSavedToTasks": "Захавана ў вашыя справы", + "onboardingConnectTapToConnect": "Націсніце на Omi, каб падключыць.", + "onboardingActMicFallbackBody": "Omi яшчэ не перадае гук. Пакуль скарыстайцеся тэлефонам.", + "onboardingActNoAudioOmi": "Не чуваць Omi. Пераканайцеся, што ён уключаны і побач.", + "onboardingActNoAudioPhone": "Пакуль вас не чуваць. Паспрабуйце гаварыць крыху гучней.", + "onboardingActDictateBodyOmi": "Проста гаварыце. Omi слухае.", + "voiceProfileCardDismissed": "Профіль голасу заўсёды можна наладзіць у Наладах.", + "homeNewRecordingButton": "Новы запіс", + "chatWelcomeMessage": "Прывітанне, я Omi. Пытайцеся пра ўсё, пра што вы размаўлялі: што хтосьці сказаў, што вы паабяцалі, ці як прайшоў ваш дзень. Я таксама магу дадаваць задачы і запамінаць рэчы за вас.", + "onboardingStarterTaskAsk": "Спытайце Omi пра свой дзень", + "onboardingStarterTaskRecord": "Запішыце размову з Omi", + "onboardingStarterTaskCheck": "Адзначце гэтую задачу, каб завяршыць яе", "paywallRecommended": "Recommended", "paywallNoPlans": "Plans aren't available right now. Try again in a minute.", "paywallOtherPlans": "See all plans", @@ -11440,21 +11483,6 @@ "paywallManageBilling": "Manage billing", "paywallLoadError": "Couldn't load plans. Check your connection and try again.", "paywallPlans": "Plans", - "firstCaptureTitle": "How will you capture?", - "firstCaptureDescription": "Choose how you want to capture your first conversation.", - "firstCaptureOmi": "I Have an Omi", - "firstCapturePhone": "This Phone", - "firstCaptureWatch": "Apple Watch", - "firstCaptureSkip": "Skip for Now", - "firstCapturePrompt": "Tell Omi what you're working on today, or what's on your mind. Talk for about 30 seconds.", - "firstCaptureKeepGoing": "Keep going… 30 seconds is ideal.", - "firstCaptureReady": "You can finish now, or keep talking.", - "firstCaptureStart": "Start Recording", - "firstCaptureFinish": "Finish Recording", - "firstCaptureListening": "Listening… your words will appear here.", - "firstCaptureProcessing": "Creating your first conversation…", - "firstCaptureResult": "Your first conversation", - "firstCaptureEmpty": "I didn't catch enough speech, try again with a few sentences", "watchCaptureTitle": "Record from your Apple Watch", "watchCaptureOpen": "Open Omi on your Apple Watch.", "watchCaptureRecord": "Tap Record. Allow microphone access if asked.", @@ -11466,17 +11494,6 @@ "pairingNotFoundOrDropped": "Couldn't reach your Omi. Make sure it's charged, close to your phone, and its light is blinking, then try again.", "pairingBondRecovery": "Forget Omi in your phone’s Bluetooth settings, then try pairing again.", "pairingHowTo": "How to pair", - "firstCaptureWatchInstructions": "Open Omi on your Apple Watch and tap Record. Allow microphone access if asked. Your capture will sync to Omi on this phone.", - "firstCaptureWatchInstall": "In the Watch app on your iPhone, install Omi on your Apple Watch. Then open Omi on your watch and tap Record.", - "firstCaptureWatchPair": "Pair your Apple Watch with this iPhone using the Watch app, then install and open Omi on your watch.", - "firstCaptureUnavailable": "Recording could not start. Check microphone access and your connection, then try again.", - "firstCaptureDelayed": "Your recording is taking longer to process. You can check your conversations on Home or try again.", - "firstCaptureEmptyTitle": "Let's try that again", - "firstCaptureShortTitle": "Finish This Recording?", - "firstCaptureShortWarning": "A few more sentences help Omi understand you. About 30 seconds is ideal.", - "firstCaptureCancelTitle": "Leave This Recording?", - "firstCaptureCancelWarning": "Going back stops this recording. Keep talking to give Omi a few more sentences.", - "firstCaptureSiriTip": "Tip: Enable Omi in Shortcuts → Siri.", "paywallOutlineHeadline": "Больш не прапусціце ніводнага слова.", "paywallCancelAnytime": "Адмяняйце ў любы час.", "connectAnAgent": "Падключыць агента", diff --git a/app/lib/l10n/app_bg.arb b/app/lib/l10n/app_bg.arb index 8cfd98a40b5..ba069ab0a4c 100644 --- a/app/lib/l10n/app_bg.arb +++ b/app/lib/l10n/app_bg.arb @@ -520,7 +520,7 @@ "dontShowAgain": "Не показвай отново", "iUnderstand": "Разбирам", "enableBluetooth": "Активирай Bluetooth", - "bluetoothNeeded": "Omi се нуждае от Bluetooth, за да се свърже с вашето носимо устройство. Моля, активирайте Bluetooth и опитайте отново.", + "bluetoothNeeded": "Omi се свързва с телефона ви чрез Bluetooth. Включете го, за да продължите.", "contactSupport": "Свържете се с поддръжката?", "connectLater": "Свържи по-късно", "grantPermissions": "Предоставяне на разрешения", @@ -1501,7 +1501,7 @@ "durationSeconds": "Продължителност: {seconds} секунди", "additionalSpeechSampleRemoved": "Допълнителната гласова проба е премахната", "consentDataMessage": "С продължаването вашите разговори, записи и лична информация ще бъдат сигурно съхранени на нашите сървъри. Вашите аудиозаписи и транскрипции се обработват от AI услуги на трети страни (включително Deepgram за транскрипция и OpenAI за анализ), за да ви предоставят прозрения, базирани на AI, и да активират всички функции на приложението.", - "tasksEmptyStateMessage": "Започнете разговор, за да създадете задача.", + "tasksEmptyStateMessage": "Задачите от разговорите ви ще се появят тук.", "clearChatAction": "Изчисти чата", "enableApps": "Активиране на приложения", "omiAppName": "Omi", @@ -4004,6 +4004,49 @@ "reviewProposalSuggested": "Предложено", "reviewProposalConflict": "Това се промени, след като Dream го предложи, затова нищо не беше приложено.", "itemLinkListTitle": "{count, plural, =0{Няма елементи} =1{1 елемент} other{{count} елемента}}", + "onboardingPermissionTapToFix": "Докоснете, за да поправите", + "onboardingNoOmiYet": "Все още нямате Omi?", + "onboardingOmiOrdered": "Поръчал съм го", + "onboardingOmiNotOrdered": "Още не съм поръчал", + "onboardingConnectOmi": "Свържете Omi", + "consentShortMessage": "Omi използва ИИ, за да превърне разговорите ви в бележки. Можете да ги изтриете по всяко време.", + "consentAiProviders": "Обработва се с {providers}.", + "onboardingActIntro": "Да пробваме Omi.", + "onboardingActMicTitle": "Omi се нуждае от микрофона ви", + "onboardingActMicBody": "За да ви чува Omi, когато говорите с него.", + "onboardingActMicAllow": "Разреши микрофона", + "onboardingActDictateTitle": "Разкажете на Omi за деня си", + "onboardingActDictateBody": "Кажете го по своя начин. Omi ще се погрижи за останалото.", + "onboardingActDictateExample": "Току-що обядвах със Сам, мести се в Лисабон през юни! Напомни ми да пратя презентацията на екипа до петък и да купя мляко на връщане.", + "onboardingActDictateHint": "Докоснете отметката, когато сте готови", + "onboardingActPhoneMic": "Микрофон на телефона", + "onboardingActWriting": "Обобщаваме…", + "onboardingActRemembers": "Omi помни вместо вас", + "onboardingActNotesLabel": "Денят ви накратко", + "onboardingActTasksLabel": "Omi улови задачите ви", + "onboardingActYourWords": "Ето какво казахте", + "onboardingActCongratsTitle": "Всичко е готово, {name}", + "onboardingActRateTitle": "Харесва ли ви Omi досега?", + "onboardingActRateYes": "Оценете Omi", + "onboardingWelcomeCardTitle": "Добре дошли в Omi", + "onboardingWelcomeCardBody": "Носете Omi и говорете както обикновено. Всеки разговор се превръща тук в бележки, задачи и спомени.", + "onboardingActDemoTitle": "Обяд със Сам", + "onboardingActDemoSummary": "Обяд със Сам, която се мести в Лисабон през юни. Искате да изпратите презентацията на екипа до петък и да вземете мляко на път за вкъщи.", + "onboardingActDemoTask1": "Да пратя презентацията на екипа до петък", + "onboardingActDemoTask2": "Да купя мляко на връщане", + "onboardingActJustNow": "Току-що", + "onboardingActSavedToTasks": "Запазено в задачите ви", + "onboardingConnectTapToConnect": "Докоснете Omi, за да се свържете.", + "onboardingActMicFallbackBody": "Omi все още не изпраща звук. Засега използвайте телефона си.", + "onboardingActNoAudioOmi": "Не чуваме Omi. Уверете се, че е включен и наблизо.", + "onboardingActNoAudioPhone": "Още не ви чуваме. Опитайте да говорите малко по-силно.", + "onboardingActDictateBodyOmi": "Просто говорете. Omi слуша.", + "voiceProfileCardDismissed": "Можете да настроите гласовия профил по всяко време от Настройки.", + "homeNewRecordingButton": "Нов запис", + "chatWelcomeMessage": "Здравей, аз съм Omi. Питай ме за всичко, за което си говорил: какво е казал някой, какво си обещал или обобщение на деня ти. Мога и да добавям задачи и да запомням неща вместо теб.", + "onboardingStarterTaskAsk": "Попитай Omi за деня си", + "onboardingStarterTaskRecord": "Запиши разговор с Omi", + "onboardingStarterTaskCheck": "Отметни тази задача, за да я завършиш", "paywallRecommended": "Recommended", "paywallNoPlans": "Plans aren't available right now. Try again in a minute.", "paywallOtherPlans": "See all plans", @@ -4014,21 +4057,6 @@ "paywallManageBilling": "Manage billing", "paywallLoadError": "Couldn't load plans. Check your connection and try again.", "paywallPlans": "Plans", - "firstCaptureTitle": "How will you capture?", - "firstCaptureDescription": "Choose how you want to capture your first conversation.", - "firstCaptureOmi": "I Have an Omi", - "firstCapturePhone": "This Phone", - "firstCaptureWatch": "Apple Watch", - "firstCaptureSkip": "Skip for Now", - "firstCapturePrompt": "Tell Omi what you're working on today, or what's on your mind. Talk for about 30 seconds.", - "firstCaptureKeepGoing": "Keep going… 30 seconds is ideal.", - "firstCaptureReady": "You can finish now, or keep talking.", - "firstCaptureStart": "Start Recording", - "firstCaptureFinish": "Finish Recording", - "firstCaptureListening": "Listening… your words will appear here.", - "firstCaptureProcessing": "Creating your first conversation…", - "firstCaptureResult": "Your first conversation", - "firstCaptureEmpty": "I didn't catch enough speech, try again with a few sentences", "watchCaptureTitle": "Record from your Apple Watch", "watchCaptureOpen": "Open Omi on your Apple Watch.", "watchCaptureRecord": "Tap Record. Allow microphone access if asked.", @@ -4040,17 +4068,6 @@ "pairingNotFoundOrDropped": "Couldn't reach your Omi. Make sure it's charged, close to your phone, and its light is blinking, then try again.", "pairingBondRecovery": "Forget Omi in your phone’s Bluetooth settings, then try pairing again.", "pairingHowTo": "How to pair", - "firstCaptureWatchInstructions": "Open Omi on your Apple Watch and tap Record. Allow microphone access if asked. Your capture will sync to Omi on this phone.", - "firstCaptureWatchInstall": "In the Watch app on your iPhone, install Omi on your Apple Watch. Then open Omi on your watch and tap Record.", - "firstCaptureWatchPair": "Pair your Apple Watch with this iPhone using the Watch app, then install and open Omi on your watch.", - "firstCaptureUnavailable": "Recording could not start. Check microphone access and your connection, then try again.", - "firstCaptureDelayed": "Your recording is taking longer to process. You can check your conversations on Home or try again.", - "firstCaptureEmptyTitle": "Let's try that again", - "firstCaptureShortTitle": "Finish This Recording?", - "firstCaptureShortWarning": "A few more sentences help Omi understand you. About 30 seconds is ideal.", - "firstCaptureCancelTitle": "Leave This Recording?", - "firstCaptureCancelWarning": "Going back stops this recording. Keep talking to give Omi a few more sentences.", - "firstCaptureSiriTip": "Tip: Enable Omi in Shortcuts → Siri.", "paywallOutlineHeadline": "Никога повече не пропускайте дума.", "paywallCancelAnytime": "Отказ по всяко време.", "connectAnAgent": "Свържете агент", diff --git a/app/lib/l10n/app_bn.arb b/app/lib/l10n/app_bn.arb index 1f915f0f311..85047ad114d 100644 --- a/app/lib/l10n/app_bn.arb +++ b/app/lib/l10n/app_bn.arb @@ -1586,7 +1586,7 @@ "@enableBluetooth": { "description": "Title for dialog prompting user to enable Bluetooth" }, - "bluetoothNeeded": "আপনার পরিধানযোগ্যের সাথে সংযোগ করতে Omi এর Bluetooth দরকার। Bluetooth সক্ষম করুন এবং আবার চেষ্টা করুন।", + "bluetoothNeeded": "Omi আপনার ফোনের সাথে Bluetooth-এ সংযুক্ত হয়। চালিয়ে যেতে এটি চালু করুন।", "@bluetoothNeeded": { "description": "Explanation text for why Bluetooth is required" }, @@ -5782,7 +5782,7 @@ "@consentDataMessage": { "description": "Consent message explaining how user data will be stored and used" }, - "tasksEmptyStateMessage": "একটি কাজ তৈরি করতে কথোপকথন শুরু করুন।", + "tasksEmptyStateMessage": "আপনার কথোপকথনের করণীয়গুলো এখানে দেখা যাবে।", "@tasksEmptyStateMessage": { "description": "Empty state message shown when there are no tasks, with instruction to tap + button" }, @@ -11430,6 +11430,49 @@ "reviewProposalSuggested": "প্রস্তাবিত", "reviewProposalConflict": "Dream প্রস্তাব দেওয়ার পর এটি বদলে গেছে, তাই কিছুই প্রয়োগ করা হয়নি।", "itemLinkListTitle": "{count, plural, =0{কোনো আইটেম নেই} =1{১টি আইটেম} other{{count}টি আইটেম}}", + "onboardingPermissionTapToFix": "ঠিক করতে ট্যাপ করুন", + "onboardingNoOmiYet": "এখনও Omi নেই?", + "onboardingOmiOrdered": "আমি অর্ডার করেছি", + "onboardingOmiNotOrdered": "এখনও অর্ডার করিনি", + "onboardingConnectOmi": "Omi সংযুক্ত করুন", + "consentShortMessage": "Omi AI ব্যবহার করে আপনার কথোপকথনকে নোটে পরিণত করে। যেকোনো সময় মুছে ফেলতে পারেন।", + "consentAiProviders": "{providers} দিয়ে প্রক্রিয়া করা হয়।", + "onboardingActIntro": "চলুন Omi চেষ্টা করি।", + "onboardingActMicTitle": "Omi-এর আপনার মাইক্রোফোন দরকার", + "onboardingActMicBody": "যাতে আপনি কথা বললে Omi শুনতে পায়।", + "onboardingActMicAllow": "মাইক্রোফোন অনুমতি দিন", + "onboardingActDictateTitle": "Omi-কে আপনার দিনের কথা বলুন", + "onboardingActDictateBody": "নিজের মতো করে বলুন। বাকিটা Omi সামলাবে।", + "onboardingActDictateExample": "এইমাত্র স্যামের সাথে লাঞ্চ করলাম, ও জুনে লিসবনে চলে যাচ্ছে! মনে করিয়ে দিও শুক্রবারের মধ্যে টিমকে ডেকটা পাঠাতে, আর ফেরার পথে দুধ কিনতে।", + "onboardingActDictateHint": "শেষ হলে টিক চিহ্নে ট্যাপ করুন", + "onboardingActPhoneMic": "ফোনের মাইক্রোফোন", + "onboardingActWriting": "সারাংশ তৈরি হচ্ছে…", + "onboardingActRemembers": "Omi মনে রাখে, যাতে আপনাকে রাখতে না হয়", + "onboardingActNotesLabel": "আপনার দিন, সংক্ষেপে", + "onboardingActTasksLabel": "Omi আপনার কাজগুলো ধরে ফেলেছে", + "onboardingActYourWords": "আপনি যা বললেন", + "onboardingActCongratsTitle": "সব তৈরি, {name}", + "onboardingActRateTitle": "এখন পর্যন্ত Omi কেমন লাগছে?", + "onboardingActRateYes": "Omi-কে রেটিং দিন", + "onboardingWelcomeCardTitle": "Omi-তে স্বাগতম", + "onboardingWelcomeCardBody": "Omi পরে স্বাভাবিকভাবে কথা বলুন। প্রতিটি কথোপকথন এখানেই নোট, কাজ আর স্মৃতিতে পরিণত হবে।", + "onboardingActDemoTitle": "স্যামের সাথে লাঞ্চ", + "onboardingActDemoSummary": "স্যামের সঙ্গে দুপুরের খাবার, যিনি জুনে লিসবনে চলে যাচ্ছেন। শুক্রবারের মধ্যে টিমকে ডেক পাঠাতে আর বাড়ি ফেরার পথে দুধ কিনতে হবে।", + "onboardingActDemoTask1": "শুক্রবারের মধ্যে টিমকে ডেক পাঠানো", + "onboardingActDemoTask2": "ফেরার পথে দুধ কেনা", + "onboardingActJustNow": "এইমাত্র", + "onboardingActSavedToTasks": "আপনার কাজের তালিকায় সংরক্ষিত", + "onboardingConnectTapToConnect": "সংযোগ করতে আপনার Omi-তে ট্যাপ করুন।", + "onboardingActMicFallbackBody": "Omi এখনও অডিও পাঠাচ্ছে না। আপাতত আপনার ফোন ব্যবহার করুন।", + "onboardingActNoAudioOmi": "Omi শোনা যাচ্ছে না। এটি চালু আছে ও কাছে আছে কিনা দেখুন।", + "onboardingActNoAudioPhone": "এখনও আপনাকে শোনা যাচ্ছে না। একটু জোরে বলে দেখুন।", + "onboardingActDictateBodyOmi": "শুধু কথা বলুন। আপনার Omi শুনছে।", + "voiceProfileCardDismissed": "সেটিংসে যেকোনো সময় ভয়েস প্রোফাইল সেট করতে পারেন।", + "homeNewRecordingButton": "নতুন রেকর্ডিং", + "chatWelcomeMessage": "হাই, আমি Omi। আপনি যা নিয়ে কথা বলেছেন তার যেকোনো কিছু জিজ্ঞেস করুন: কেউ কী বলেছে, আপনি কী প্রতিশ্রুতি দিয়েছেন, বা আপনার দিনের সারাংশ। আমি কাজ যোগ করতে এবং আপনার জন্য জিনিস মনে রাখতেও পারি।", + "onboardingStarterTaskAsk": "Omi-কে আপনার দিন সম্পর্কে জিজ্ঞেস করুন", + "onboardingStarterTaskRecord": "Omi দিয়ে একটি কথোপকথন রেকর্ড করুন", + "onboardingStarterTaskCheck": "শেষ করতে এই কাজটিতে টিক দিন", "paywallRecommended": "Recommended", "paywallNoPlans": "Plans aren't available right now. Try again in a minute.", "paywallOtherPlans": "See all plans", @@ -11440,21 +11483,6 @@ "paywallManageBilling": "Manage billing", "paywallLoadError": "Couldn't load plans. Check your connection and try again.", "paywallPlans": "Plans", - "firstCaptureTitle": "How will you capture?", - "firstCaptureDescription": "Choose how you want to capture your first conversation.", - "firstCaptureOmi": "I Have an Omi", - "firstCapturePhone": "This Phone", - "firstCaptureWatch": "Apple Watch", - "firstCaptureSkip": "Skip for Now", - "firstCapturePrompt": "Tell Omi what you're working on today, or what's on your mind. Talk for about 30 seconds.", - "firstCaptureKeepGoing": "Keep going… 30 seconds is ideal.", - "firstCaptureReady": "You can finish now, or keep talking.", - "firstCaptureStart": "Start Recording", - "firstCaptureFinish": "Finish Recording", - "firstCaptureListening": "Listening… your words will appear here.", - "firstCaptureProcessing": "Creating your first conversation…", - "firstCaptureResult": "Your first conversation", - "firstCaptureEmpty": "I didn't catch enough speech, try again with a few sentences", "watchCaptureTitle": "Record from your Apple Watch", "watchCaptureOpen": "Open Omi on your Apple Watch.", "watchCaptureRecord": "Tap Record. Allow microphone access if asked.", @@ -11466,17 +11494,6 @@ "pairingNotFoundOrDropped": "Couldn't reach your Omi. Make sure it's charged, close to your phone, and its light is blinking, then try again.", "pairingBondRecovery": "Forget Omi in your phone’s Bluetooth settings, then try pairing again.", "pairingHowTo": "How to pair", - "firstCaptureWatchInstructions": "Open Omi on your Apple Watch and tap Record. Allow microphone access if asked. Your capture will sync to Omi on this phone.", - "firstCaptureWatchInstall": "In the Watch app on your iPhone, install Omi on your Apple Watch. Then open Omi on your watch and tap Record.", - "firstCaptureWatchPair": "Pair your Apple Watch with this iPhone using the Watch app, then install and open Omi on your watch.", - "firstCaptureUnavailable": "Recording could not start. Check microphone access and your connection, then try again.", - "firstCaptureDelayed": "Your recording is taking longer to process. You can check your conversations on Home or try again.", - "firstCaptureEmptyTitle": "Let's try that again", - "firstCaptureShortTitle": "Finish This Recording?", - "firstCaptureShortWarning": "A few more sentences help Omi understand you. About 30 seconds is ideal.", - "firstCaptureCancelTitle": "Leave This Recording?", - "firstCaptureCancelWarning": "Going back stops this recording. Keep talking to give Omi a few more sentences.", - "firstCaptureSiriTip": "Tip: Enable Omi in Shortcuts → Siri.", "paywallOutlineHeadline": "আর কখনো একটি শব্দও মিস করবেন না।", "paywallCancelAnytime": "যেকোনো সময় বাতিল করুন।", "connectAnAgent": "একটি এজেন্ট সংযুক্ত করুন", diff --git a/app/lib/l10n/app_bs.arb b/app/lib/l10n/app_bs.arb index 5176d43f79a..dc29fb29637 100644 --- a/app/lib/l10n/app_bs.arb +++ b/app/lib/l10n/app_bs.arb @@ -1586,7 +1586,7 @@ "@enableBluetooth": { "description": "Title for dialog prompting user to enable Bluetooth" }, - "bluetoothNeeded": "Omi koristi Bluetooth da bi se povezao sa vašim nosivim uređajem. Molimo omogućite Bluetooth i pokušajte ponovo.", + "bluetoothNeeded": "Omi se s telefonom povezuje putem Bluetootha. Uključite ga da nastavite.", "@bluetoothNeeded": { "description": "Explanation text for why Bluetooth is required" }, @@ -5782,7 +5782,7 @@ "@consentDataMessage": { "description": "Consent message explaining how user data will be stored and used" }, - "tasksEmptyStateMessage": "Započnite razgovor da kreirate zadatak.", + "tasksEmptyStateMessage": "Zadaci iz vaših razgovora pojavit će se ovdje.", "@tasksEmptyStateMessage": { "description": "Empty state message shown when there are no tasks, with instruction to tap + button" }, @@ -11430,6 +11430,49 @@ "reviewProposalSuggested": "Predloženo", "reviewProposalConflict": "Ovo se promijenilo nakon što je Dream to predložio, pa ništa nije primijenjeno.", "itemLinkListTitle": "{count, plural, =0{Nema stavki} =1{1 stavka} other{{count} stavki}}", + "onboardingPermissionTapToFix": "Dodirnite da popravite", + "onboardingNoOmiYet": "Još nemate Omi?", + "onboardingOmiOrdered": "Naručio/la sam ga", + "onboardingOmiNotOrdered": "Još nisam naručio/la", + "onboardingConnectOmi": "Povežite Omi", + "consentShortMessage": "Omi koristi AI da vaše razgovore pretvori u bilješke. Možete ih izbrisati bilo kada.", + "consentAiProviders": "Obrađuje se pomoću {providers}.", + "onboardingActIntro": "Isprobajmo Omi.", + "onboardingActMicTitle": "Omi treba vaš mikrofon", + "onboardingActMicBody": "Da vas Omi čuje kad mu se obratite.", + "onboardingActMicAllow": "Dozvoli mikrofon", + "onboardingActDictateTitle": "Ispričajte Omiju svoj dan", + "onboardingActDictateBody": "Recite to na svoj način. Omi će se pobrinuti za ostalo.", + "onboardingActDictateExample": "Upravo sam ručao sa Sam, seli se u Lisabon u junu! Podsjeti me da pošaljem prezentaciju timu do petka i da kupim mlijeko na putu kući.", + "onboardingActDictateHint": "Dodirnite kvačicu kad završite", + "onboardingActPhoneMic": "Mikrofon telefona", + "onboardingActWriting": "Sažimamo…", + "onboardingActRemembers": "Omi pamti da vi ne morate", + "onboardingActNotesLabel": "Vaš dan, ukratko", + "onboardingActTasksLabel": "Omi je uhvatio vaše zadatke", + "onboardingActYourWords": "Evo šta ste rekli", + "onboardingActCongratsTitle": "Sve je spremno, {name}", + "onboardingActRateTitle": "Sviđa li vam se Omi zasad?", + "onboardingActRateYes": "Ocijenite Omi", + "onboardingWelcomeCardTitle": "Dobro došli u Omi", + "onboardingWelcomeCardBody": "Nosite Omi i pričajte kao i inače. Svaki razgovor ovdje postaje bilješke, zadaci i sjećanja.", + "onboardingActDemoTitle": "Ručak sa Sam", + "onboardingActDemoSummary": "Ručak sa Sam, koja se u junu seli u Lisabon. Želite poslati prezentaciju timu do petka i kupiti mlijeko na putu kući.", + "onboardingActDemoTask1": "Poslati prezentaciju timu do petka", + "onboardingActDemoTask2": "Kupiti mlijeko na putu kući", + "onboardingActJustNow": "Upravo sada", + "onboardingActSavedToTasks": "Sačuvano u vaše zadatke", + "onboardingConnectTapToConnect": "Dodirnite svoj Omi da se povežete.", + "onboardingActMicFallbackBody": "Omi još ne šalje zvuk. Za sada koristite telefon.", + "onboardingActNoAudioOmi": "Ne čujemo Omi. Provjerite je li uključen i blizu.", + "onboardingActNoAudioPhone": "Još vas ne čujemo. Pokušajte govoriti malo glasnije.", + "onboardingActDictateBodyOmi": "Samo pričajte. Vaš Omi sluša.", + "voiceProfileCardDismissed": "Glasovni profil možete podesiti bilo kada u Postavkama.", + "homeNewRecordingButton": "Novo snimanje", + "chatWelcomeMessage": "Zdravo, ja sam Omi. Pitaj me bilo šta o čemu si pričao: šta je neko rekao, šta si obećao ili sažetak tvog dana. Mogu i dodavati zadatke i pamtiti stvari za tebe.", + "onboardingStarterTaskAsk": "Pitaj Omi o svom danu", + "onboardingStarterTaskRecord": "Snimi razgovor uz Omi", + "onboardingStarterTaskCheck": "Označi ovaj zadatak da ga završiš", "paywallRecommended": "Recommended", "paywallNoPlans": "Plans aren't available right now. Try again in a minute.", "paywallOtherPlans": "See all plans", @@ -11440,21 +11483,6 @@ "paywallManageBilling": "Manage billing", "paywallLoadError": "Couldn't load plans. Check your connection and try again.", "paywallPlans": "Plans", - "firstCaptureTitle": "How will you capture?", - "firstCaptureDescription": "Choose how you want to capture your first conversation.", - "firstCaptureOmi": "I Have an Omi", - "firstCapturePhone": "This Phone", - "firstCaptureWatch": "Apple Watch", - "firstCaptureSkip": "Skip for Now", - "firstCapturePrompt": "Tell Omi what you're working on today, or what's on your mind. Talk for about 30 seconds.", - "firstCaptureKeepGoing": "Keep going… 30 seconds is ideal.", - "firstCaptureReady": "You can finish now, or keep talking.", - "firstCaptureStart": "Start Recording", - "firstCaptureFinish": "Finish Recording", - "firstCaptureListening": "Listening… your words will appear here.", - "firstCaptureProcessing": "Creating your first conversation…", - "firstCaptureResult": "Your first conversation", - "firstCaptureEmpty": "I didn't catch enough speech, try again with a few sentences", "watchCaptureTitle": "Record from your Apple Watch", "watchCaptureOpen": "Open Omi on your Apple Watch.", "watchCaptureRecord": "Tap Record. Allow microphone access if asked.", @@ -11466,17 +11494,6 @@ "pairingNotFoundOrDropped": "Couldn't reach your Omi. Make sure it's charged, close to your phone, and its light is blinking, then try again.", "pairingBondRecovery": "Forget Omi in your phone’s Bluetooth settings, then try pairing again.", "pairingHowTo": "How to pair", - "firstCaptureWatchInstructions": "Open Omi on your Apple Watch and tap Record. Allow microphone access if asked. Your capture will sync to Omi on this phone.", - "firstCaptureWatchInstall": "In the Watch app on your iPhone, install Omi on your Apple Watch. Then open Omi on your watch and tap Record.", - "firstCaptureWatchPair": "Pair your Apple Watch with this iPhone using the Watch app, then install and open Omi on your watch.", - "firstCaptureUnavailable": "Recording could not start. Check microphone access and your connection, then try again.", - "firstCaptureDelayed": "Your recording is taking longer to process. You can check your conversations on Home or try again.", - "firstCaptureEmptyTitle": "Let's try that again", - "firstCaptureShortTitle": "Finish This Recording?", - "firstCaptureShortWarning": "A few more sentences help Omi understand you. About 30 seconds is ideal.", - "firstCaptureCancelTitle": "Leave This Recording?", - "firstCaptureCancelWarning": "Going back stops this recording. Keep talking to give Omi a few more sentences.", - "firstCaptureSiriTip": "Tip: Enable Omi in Shortcuts → Siri.", "paywallOutlineHeadline": "Nikad više ne propustite nijednu riječ.", "paywallCancelAnytime": "Otkažite bilo kada.", "connectAnAgent": "Povežite agenta", diff --git a/app/lib/l10n/app_ca.arb b/app/lib/l10n/app_ca.arb index 2892fa636a1..e31d85b4702 100644 --- a/app/lib/l10n/app_ca.arb +++ b/app/lib/l10n/app_ca.arb @@ -520,7 +520,7 @@ "dontShowAgain": "No ho tornis a mostrar", "iUnderstand": "Ho entenc", "enableBluetooth": "Activar Bluetooth", - "bluetoothNeeded": "Omi necessita Bluetooth per connectar-se al vostre dispositiu portàtil. Activeu Bluetooth i torneu-ho a provar.", + "bluetoothNeeded": "L'Omi es connecta al telèfon per Bluetooth. Activa'l per continuar.", "contactSupport": "Contactar amb suport?", "connectLater": "Connectar més tard", "grantPermissions": "Concedir permisos", @@ -1501,7 +1501,7 @@ "durationSeconds": "Durada: {seconds} segons", "additionalSpeechSampleRemoved": "S'ha eliminat la mostra de veu addicional", "consentDataMessage": "Continuant, les vostres converses, enregistraments i informació personal s'emmagatzemaran de manera segura als nostres servidors. Els vostres enregistraments d'àudio i transcripcions són processats per serveis d'IA de tercers (incloent Deepgram per a la transcripció i OpenAI per a l'anàlisi) per proporcionar-vos informació impulsada per IA i habilitar totes les funcions de l'aplicació.", - "tasksEmptyStateMessage": "Inicia una conversa per crear una tasca.", + "tasksEmptyStateMessage": "Les tasques de les teves converses apareixeran aquí.", "clearChatAction": "Esborrar el xat", "enableApps": "Activar aplicacions", "omiAppName": "Omi", @@ -4004,6 +4004,49 @@ "reviewProposalSuggested": "Suggerit", "reviewProposalConflict": "Això ha canviat des que Dream ho va suggerir, així que no s'ha aplicat res.", "itemLinkListTitle": "{count, plural, =0{Cap element} =1{1 element} other{{count} elements}}", + "onboardingPermissionTapToFix": "Toca per solucionar-ho", + "onboardingNoOmiYet": "Encara no tens l'Omi?", + "onboardingOmiOrdered": "Ja l'he demanat", + "onboardingOmiNotOrdered": "Encara no l'he demanat", + "onboardingConnectOmi": "Connecta l'Omi", + "consentShortMessage": "Omi fa servir la IA per convertir les teves converses en notes. Les pots esborrar quan vulguis.", + "consentAiProviders": "Processat amb {providers}.", + "onboardingActIntro": "Provem Omi.", + "onboardingActMicTitle": "Omi necessita el micròfon", + "onboardingActMicBody": "Perquè Omi et senti quan li parlis.", + "onboardingActMicAllow": "Permet el micròfon", + "onboardingActDictateTitle": "Explica-li a Omi el teu dia", + "onboardingActDictateBody": "Digues-ho a la teva manera. Omi s'encarrega de la resta.", + "onboardingActDictateExample": "Acabo de dinar amb la Sam, se'n va a viure a Lisboa al juny! Recorda'm d'enviar la presentació a l'equip abans de divendres i de comprar llet de tornada a casa.", + "onboardingActDictateHint": "Toca la marca quan acabis", + "onboardingActPhoneMic": "Micròfon del telèfon", + "onboardingActWriting": "Resumint…", + "onboardingActRemembers": "L'Omi ho recorda perquè tu no ho hagis de fer", + "onboardingActNotesLabel": "El teu dia, resumit", + "onboardingActTasksLabel": "Omi ha trobat les teves tasques", + "onboardingActYourWords": "Això és el que has dit", + "onboardingActCongratsTitle": "Tot a punt, {name}", + "onboardingActRateTitle": "T'agrada Omi de moment?", + "onboardingActRateYes": "Valora Omi", + "onboardingWelcomeCardTitle": "Et donem la benvinguda a Omi", + "onboardingWelcomeCardBody": "Porta l'Omi i parla com sempre. Cada conversa es converteix aquí en notes, tasques i records.", + "onboardingActDemoTitle": "Dinar amb la Sam", + "onboardingActDemoSummary": "Dinar amb en Sam, que es trasllada a Lisboa al juny. Vols enviar la presentació a l'equip abans de divendres i comprar llet de camí a casa.", + "onboardingActDemoTask1": "Enviar la presentació a l'equip abans de divendres", + "onboardingActDemoTask2": "Comprar llet de tornada a casa", + "onboardingActJustNow": "Ara mateix", + "onboardingActSavedToTasks": "Desat a les teves tasques", + "onboardingConnectTapToConnect": "Toca el teu Omi per connectar-lo.", + "onboardingActMicFallbackBody": "L'Omi encara no envia àudio. De moment, fes servir el telèfon.", + "onboardingActNoAudioOmi": "No sentim l'Omi. Comprova que estigui encès i a prop.", + "onboardingActNoAudioPhone": "Encara no et sentim. Prova de parlar una mica més fort.", + "onboardingActDictateBodyOmi": "Només parla. El teu Omi t'escolta.", + "voiceProfileCardDismissed": "Pots configurar el perfil de veu quan vulguis a Configuració.", + "homeNewRecordingButton": "Nova gravació", + "chatWelcomeMessage": "Hola, soc l'Omi. Pregunta'm qualsevol cosa del que has parlat: què va dir algú, què vas prometre o un resum del teu dia. També puc afegir tasques i recordar coses per tu.", + "onboardingStarterTaskAsk": "Pregunta a l'Omi pel teu dia", + "onboardingStarterTaskRecord": "Grava una conversa amb l'Omi", + "onboardingStarterTaskCheck": "Marca aquesta tasca per completar-la", "paywallRecommended": "Recommended", "paywallNoPlans": "Plans aren't available right now. Try again in a minute.", "paywallOtherPlans": "See all plans", @@ -4014,21 +4057,6 @@ "paywallManageBilling": "Manage billing", "paywallLoadError": "Couldn't load plans. Check your connection and try again.", "paywallPlans": "Plans", - "firstCaptureTitle": "How will you capture?", - "firstCaptureDescription": "Choose how you want to capture your first conversation.", - "firstCaptureOmi": "I Have an Omi", - "firstCapturePhone": "This Phone", - "firstCaptureWatch": "Apple Watch", - "firstCaptureSkip": "Skip for Now", - "firstCapturePrompt": "Tell Omi what you're working on today, or what's on your mind. Talk for about 30 seconds.", - "firstCaptureKeepGoing": "Keep going… 30 seconds is ideal.", - "firstCaptureReady": "You can finish now, or keep talking.", - "firstCaptureStart": "Start Recording", - "firstCaptureFinish": "Finish Recording", - "firstCaptureListening": "Listening… your words will appear here.", - "firstCaptureProcessing": "Creating your first conversation…", - "firstCaptureResult": "Your first conversation", - "firstCaptureEmpty": "I didn't catch enough speech, try again with a few sentences", "watchCaptureTitle": "Record from your Apple Watch", "watchCaptureOpen": "Open Omi on your Apple Watch.", "watchCaptureRecord": "Tap Record. Allow microphone access if asked.", @@ -4040,17 +4068,6 @@ "pairingNotFoundOrDropped": "Couldn't reach your Omi. Make sure it's charged, close to your phone, and its light is blinking, then try again.", "pairingBondRecovery": "Forget Omi in your phone’s Bluetooth settings, then try pairing again.", "pairingHowTo": "How to pair", - "firstCaptureWatchInstructions": "Open Omi on your Apple Watch and tap Record. Allow microphone access if asked. Your capture will sync to Omi on this phone.", - "firstCaptureWatchInstall": "In the Watch app on your iPhone, install Omi on your Apple Watch. Then open Omi on your watch and tap Record.", - "firstCaptureWatchPair": "Pair your Apple Watch with this iPhone using the Watch app, then install and open Omi on your watch.", - "firstCaptureUnavailable": "Recording could not start. Check microphone access and your connection, then try again.", - "firstCaptureDelayed": "Your recording is taking longer to process. You can check your conversations on Home or try again.", - "firstCaptureEmptyTitle": "Let's try that again", - "firstCaptureShortTitle": "Finish This Recording?", - "firstCaptureShortWarning": "A few more sentences help Omi understand you. About 30 seconds is ideal.", - "firstCaptureCancelTitle": "Leave This Recording?", - "firstCaptureCancelWarning": "Going back stops this recording. Keep talking to give Omi a few more sentences.", - "firstCaptureSiriTip": "Tip: Enable Omi in Shortcuts → Siri.", "paywallOutlineHeadline": "No et perdis mai més ni una paraula.", "paywallCancelAnytime": "Cancel·la quan vulguis.", "connectAnAgent": "Connecta un agent", diff --git a/app/lib/l10n/app_cs.arb b/app/lib/l10n/app_cs.arb index 6c24a8d96bf..cb1bd9661c8 100644 --- a/app/lib/l10n/app_cs.arb +++ b/app/lib/l10n/app_cs.arb @@ -520,7 +520,7 @@ "dontShowAgain": "Znovu nezobrazovat", "iUnderstand": "Rozumím", "enableBluetooth": "Povolit Bluetooth", - "bluetoothNeeded": "Omi potřebuje Bluetooth pro připojení k vašemu nositelného zařízení. Povolte prosím Bluetooth a zkuste to znovu.", + "bluetoothNeeded": "Omi se k telefonu připojuje přes Bluetooth. Zapněte ho a pokračujte.", "contactSupport": "Kontaktovat podporu?", "connectLater": "Připojit později", "grantPermissions": "Udělit oprávnění", @@ -1501,7 +1501,7 @@ "durationSeconds": "Délka: {seconds} sekund", "additionalSpeechSampleRemoved": "Další hlasový vzorek byl odstraněn", "consentDataMessage": "Pokračováním budou vaše konverzace, nahrávky a osobní údaje bezpečně uloženy na našich serverech. Vaše audio nahrávky a přepisy jsou zpracovávány AI službami třetích stran (včetně Deepgram pro přepis a OpenAI pro analýzu), aby vám poskytly poznatky založené na AI a umožnily všechny funkce aplikace.", - "tasksEmptyStateMessage": "Začněte konverzaci a vytvořte úkol.", + "tasksEmptyStateMessage": "Úkoly z vašich konverzací se zobrazí tady.", "clearChatAction": "Vymazat chat", "enableApps": "Povolit aplikace", "omiAppName": "Omi", @@ -4004,6 +4004,49 @@ "reviewProposalSuggested": "Navrženo", "reviewProposalConflict": "Od návrhu Dream se to změnilo, takže se nic nepoužilo.", "itemLinkListTitle": "{count, plural, =0{Žádné položky} =1{1 položka} other{{count} položek}}", + "onboardingPermissionTapToFix": "Klepnutím opravíte", + "onboardingNoOmiYet": "Ještě nemáte Omi?", + "onboardingOmiOrdered": "Už jsem ho objednal/a", + "onboardingOmiNotOrdered": "Ještě neobjednáno", + "onboardingConnectOmi": "Připojte Omi", + "consentShortMessage": "Omi pomocí AI mění vaše konverzace v poznámky. Kdykoli je můžete smazat.", + "consentAiProviders": "Zpracovává {providers}.", + "onboardingActIntro": "Vyzkoušejme Omi.", + "onboardingActMicTitle": "Omi potřebuje mikrofon", + "onboardingActMicBody": "Aby vás Omi slyšel, když na něj mluvíte.", + "onboardingActMicAllow": "Povolit mikrofon", + "onboardingActDictateTitle": "Řekněte Omi o svém dni", + "onboardingActDictateBody": "Řekněte to po svém. Zbytek zařídí Omi.", + "onboardingActDictateExample": "Právě jsem obědval se Sam, v červnu se stěhuje do Lisabonu! Připomeň mi poslat prezentaci týmu do pátku a koupit cestou domů mléko.", + "onboardingActDictateHint": "Až budete hotovi, klepněte na fajfku", + "onboardingActPhoneMic": "Mikrofon telefonu", + "onboardingActWriting": "Shrnujeme…", + "onboardingActRemembers": "Omi si to pamatuje, abyste nemuseli vy", + "onboardingActNotesLabel": "Váš den ve zkratce", + "onboardingActTasksLabel": "Omi zachytil vaše úkoly", + "onboardingActYourWords": "Tohle jste řekli", + "onboardingActCongratsTitle": "Vše je připraveno, {name}", + "onboardingActRateTitle": "Líbí se vám zatím Omi?", + "onboardingActRateYes": "Ohodnotit Omi", + "onboardingWelcomeCardTitle": "Vítejte v Omi", + "onboardingWelcomeCardBody": "Noste Omi a mluvte jako obvykle. Každá konverzace se tu promění v poznámky, úkoly a vzpomínky.", + "onboardingActDemoTitle": "Oběd se Sam", + "onboardingActDemoSummary": "Oběd se Sam, která se v červnu stěhuje do Lisabonu. Chcete do pátku poslat týmu prezentaci a cestou domů koupit mléko.", + "onboardingActDemoTask1": "Poslat prezentaci týmu do pátku", + "onboardingActDemoTask2": "Koupit cestou domů mléko", + "onboardingActJustNow": "Právě teď", + "onboardingActSavedToTasks": "Uloženo do vašich úkolů", + "onboardingConnectTapToConnect": "Klepnutím na Omi se připojíte.", + "onboardingActMicFallbackBody": "Omi zatím neposílá zvuk. Zatím použijte telefon.", + "onboardingActNoAudioOmi": "Omi neslyšíme. Zkontrolujte, že je zapnuté a blízko.", + "onboardingActNoAudioPhone": "Zatím vás neslyšíme. Zkuste mluvit trochu hlasitěji.", + "onboardingActDictateBodyOmi": "Prostě mluvte. Vaše Omi poslouchá.", + "voiceProfileCardDismissed": "Hlasový profil můžete kdykoli nastavit v Nastavení.", + "homeNewRecordingButton": "Nový záznam", + "chatWelcomeMessage": "Ahoj, jsem Omi. Zeptej se mě na cokoli, o čem jsi mluvil: co kdo řekl, co jsi slíbil nebo shrnutí tvého dne. Umím také přidávat úkoly a pamatovat si věci za tebe.", + "onboardingStarterTaskAsk": "Zeptej se Omi na svůj den", + "onboardingStarterTaskRecord": "Nahraj rozhovor s Omi", + "onboardingStarterTaskCheck": "Odškrtni tento úkol a dokonči ho", "paywallRecommended": "Recommended", "paywallNoPlans": "Plans aren't available right now. Try again in a minute.", "paywallOtherPlans": "See all plans", @@ -4014,21 +4057,6 @@ "paywallManageBilling": "Manage billing", "paywallLoadError": "Couldn't load plans. Check your connection and try again.", "paywallPlans": "Plans", - "firstCaptureTitle": "How will you capture?", - "firstCaptureDescription": "Choose how you want to capture your first conversation.", - "firstCaptureOmi": "I Have an Omi", - "firstCapturePhone": "This Phone", - "firstCaptureWatch": "Apple Watch", - "firstCaptureSkip": "Skip for Now", - "firstCapturePrompt": "Tell Omi what you're working on today, or what's on your mind. Talk for about 30 seconds.", - "firstCaptureKeepGoing": "Keep going… 30 seconds is ideal.", - "firstCaptureReady": "You can finish now, or keep talking.", - "firstCaptureStart": "Start Recording", - "firstCaptureFinish": "Finish Recording", - "firstCaptureListening": "Listening… your words will appear here.", - "firstCaptureProcessing": "Creating your first conversation…", - "firstCaptureResult": "Your first conversation", - "firstCaptureEmpty": "I didn't catch enough speech, try again with a few sentences", "watchCaptureTitle": "Record from your Apple Watch", "watchCaptureOpen": "Open Omi on your Apple Watch.", "watchCaptureRecord": "Tap Record. Allow microphone access if asked.", @@ -4040,17 +4068,6 @@ "pairingNotFoundOrDropped": "Couldn't reach your Omi. Make sure it's charged, close to your phone, and its light is blinking, then try again.", "pairingBondRecovery": "Forget Omi in your phone’s Bluetooth settings, then try pairing again.", "pairingHowTo": "How to pair", - "firstCaptureWatchInstructions": "Open Omi on your Apple Watch and tap Record. Allow microphone access if asked. Your capture will sync to Omi on this phone.", - "firstCaptureWatchInstall": "In the Watch app on your iPhone, install Omi on your Apple Watch. Then open Omi on your watch and tap Record.", - "firstCaptureWatchPair": "Pair your Apple Watch with this iPhone using the Watch app, then install and open Omi on your watch.", - "firstCaptureUnavailable": "Recording could not start. Check microphone access and your connection, then try again.", - "firstCaptureDelayed": "Your recording is taking longer to process. You can check your conversations on Home or try again.", - "firstCaptureEmptyTitle": "Let's try that again", - "firstCaptureShortTitle": "Finish This Recording?", - "firstCaptureShortWarning": "A few more sentences help Omi understand you. About 30 seconds is ideal.", - "firstCaptureCancelTitle": "Leave This Recording?", - "firstCaptureCancelWarning": "Going back stops this recording. Keep talking to give Omi a few more sentences.", - "firstCaptureSiriTip": "Tip: Enable Omi in Shortcuts → Siri.", "paywallOutlineHeadline": "Už vám neunikne ani slovo.", "paywallCancelAnytime": "Zrušte kdykoli.", "connectAnAgent": "Připojit agenta", diff --git a/app/lib/l10n/app_da.arb b/app/lib/l10n/app_da.arb index 8e5e8aa6d04..e6a847842ae 100644 --- a/app/lib/l10n/app_da.arb +++ b/app/lib/l10n/app_da.arb @@ -520,7 +520,7 @@ "dontShowAgain": "Vis ikke igen", "iUnderstand": "Jeg forstår", "enableBluetooth": "Aktivér Bluetooth", - "bluetoothNeeded": "Bluetooth påkrævet", + "bluetoothNeeded": "Omi forbinder til din telefon via Bluetooth. Slå det til for at fortsætte.", "contactSupport": "Kontakt support", "connectLater": "Forbind senere", "grantPermissions": "Giv tilladelser", @@ -1501,7 +1501,7 @@ "durationSeconds": "Varighed: {seconds} sekunder", "additionalSpeechSampleRemoved": "Yderligere stemmeprøve fjernet", "consentDataMessage": "Ved at fortsætte vil dine samtaler, optagelser og personlige oplysninger blive sikkert gemt på vores servere. Dine lydoptagelser og udskrifter behandles af tredjeparts AI-tjenester (herunder Deepgram til transskription og OpenAI til analyse) for at give dig AI-drevne indsigter og aktivere alle appfunktioner.", - "tasksEmptyStateMessage": "Start en samtale for at oprette en opgave.", + "tasksEmptyStateMessage": "Opgaver fra dine samtaler vises her.", "clearChatAction": "Ryd chat", "enableApps": "Aktivér apps", "omiAppName": "Omi", @@ -4046,6 +4046,49 @@ "reviewProposalSuggested": "Foreslået", "reviewProposalConflict": "Dette er ændret, siden Dream foreslog det, så intet blev anvendt.", "itemLinkListTitle": "{count, plural, =0{Ingen elementer} =1{1 element} other{{count} elementer}}", + "onboardingPermissionTapToFix": "Tryk for at rette", + "onboardingNoOmiYet": "Har du ikke din Omi endnu?", + "onboardingOmiOrdered": "Jeg har bestilt den", + "onboardingOmiNotOrdered": "Ikke bestilt endnu", + "onboardingConnectOmi": "Forbind Omi", + "consentShortMessage": "Omi bruger AI til at gøre dine samtaler til noter. Du kan slette dem når som helst.", + "consentAiProviders": "Behandles med {providers}.", + "onboardingActIntro": "Lad os prøve Omi.", + "onboardingActMicTitle": "Omi skal bruge din mikrofon", + "onboardingActMicBody": "Så Omi kan høre dig, når du taler til den.", + "onboardingActMicAllow": "Tillad mikrofon", + "onboardingActDictateTitle": "Fortæl Omi om din dag", + "onboardingActDictateBody": "Sig det på din måde. Omi klarer resten.", + "onboardingActDictateExample": "Har lige spist frokost med Sam, hun flytter til Lissabon i juni! Mind mig om at sende præsentationen til holdet inden fredag og købe mælk på vej hjem.", + "onboardingActDictateHint": "Tryk på fluebenet, når du er færdig", + "onboardingActPhoneMic": "Telefonens mikrofon", + "onboardingActWriting": "Opsummerer…", + "onboardingActRemembers": "Omi husker det, så du ikke behøver", + "onboardingActNotesLabel": "Din dag, opsummeret", + "onboardingActTasksLabel": "Omi fangede dine to-dos", + "onboardingActYourWords": "Det her sagde du", + "onboardingActCongratsTitle": "Du er klar, {name}", + "onboardingActRateTitle": "Kan du lide Omi indtil videre?", + "onboardingActRateYes": "Bedøm Omi", + "onboardingWelcomeCardTitle": "Velkommen til Omi", + "onboardingWelcomeCardBody": "Bær Omi og tal som normalt. Hver samtale bliver her til noter, opgaver og minder.", + "onboardingActDemoTitle": "Frokost med Sam", + "onboardingActDemoSummary": "Frokost med Sam, der flytter til Lissabon i juni. Du vil sende præsentationen til teamet inden fredag og købe mælk på vejen hjem.", + "onboardingActDemoTask1": "Send præsentationen til holdet inden fredag", + "onboardingActDemoTask2": "Køb mælk på vej hjem", + "onboardingActJustNow": "Lige nu", + "onboardingActSavedToTasks": "Gemt i dine opgaver", + "onboardingConnectTapToConnect": "Tryk på din Omi for at forbinde.", + "onboardingActMicFallbackBody": "Din Omi sender ikke lyd endnu. Brug din telefon indtil videre.", + "onboardingActNoAudioOmi": "Kan ikke høre din Omi. Tjek, at den er tændt og tæt på.", + "onboardingActNoAudioPhone": "Kan ikke høre dig endnu. Prøv at tale lidt højere.", + "onboardingActDictateBodyOmi": "Bare tal. Din Omi lytter.", + "voiceProfileCardDismissed": "Du kan altid oprette din stemmeprofil under Indstillinger.", + "homeNewRecordingButton": "Ny optagelse", + "chatWelcomeMessage": "Hej, jeg er Omi. Spørg mig om alt, du har talt om: hvad nogen sagde, hvad du lovede, eller et overblik over din dag. Jeg kan også tilføje opgaver og huske ting for dig.", + "onboardingStarterTaskAsk": "Spørg Omi om din dag", + "onboardingStarterTaskRecord": "Optag en samtale med Omi", + "onboardingStarterTaskCheck": "Sæt flueben ved denne opgave for at fuldføre den", "paywallRecommended": "Recommended", "paywallNoPlans": "Plans aren't available right now. Try again in a minute.", "paywallOtherPlans": "See all plans", @@ -4056,21 +4099,6 @@ "paywallManageBilling": "Manage billing", "paywallLoadError": "Couldn't load plans. Check your connection and try again.", "paywallPlans": "Plans", - "firstCaptureTitle": "How will you capture?", - "firstCaptureDescription": "Choose how you want to capture your first conversation.", - "firstCaptureOmi": "I Have an Omi", - "firstCapturePhone": "This Phone", - "firstCaptureWatch": "Apple Watch", - "firstCaptureSkip": "Skip for Now", - "firstCapturePrompt": "Tell Omi what you're working on today, or what's on your mind. Talk for about 30 seconds.", - "firstCaptureKeepGoing": "Keep going… 30 seconds is ideal.", - "firstCaptureReady": "You can finish now, or keep talking.", - "firstCaptureStart": "Start Recording", - "firstCaptureFinish": "Finish Recording", - "firstCaptureListening": "Listening… your words will appear here.", - "firstCaptureProcessing": "Creating your first conversation…", - "firstCaptureResult": "Your first conversation", - "firstCaptureEmpty": "I didn't catch enough speech, try again with a few sentences", "watchCaptureTitle": "Record from your Apple Watch", "watchCaptureOpen": "Open Omi on your Apple Watch.", "watchCaptureRecord": "Tap Record. Allow microphone access if asked.", @@ -4082,17 +4110,6 @@ "pairingNotFoundOrDropped": "Couldn't reach your Omi. Make sure it's charged, close to your phone, and its light is blinking, then try again.", "pairingBondRecovery": "Forget Omi in your phone’s Bluetooth settings, then try pairing again.", "pairingHowTo": "How to pair", - "firstCaptureWatchInstructions": "Open Omi on your Apple Watch and tap Record. Allow microphone access if asked. Your capture will sync to Omi on this phone.", - "firstCaptureWatchInstall": "In the Watch app on your iPhone, install Omi on your Apple Watch. Then open Omi on your watch and tap Record.", - "firstCaptureWatchPair": "Pair your Apple Watch with this iPhone using the Watch app, then install and open Omi on your watch.", - "firstCaptureUnavailable": "Recording could not start. Check microphone access and your connection, then try again.", - "firstCaptureDelayed": "Your recording is taking longer to process. You can check your conversations on Home or try again.", - "firstCaptureEmptyTitle": "Let's try that again", - "firstCaptureShortTitle": "Finish This Recording?", - "firstCaptureShortWarning": "A few more sentences help Omi understand you. About 30 seconds is ideal.", - "firstCaptureCancelTitle": "Leave This Recording?", - "firstCaptureCancelWarning": "Going back stops this recording. Keep talking to give Omi a few more sentences.", - "firstCaptureSiriTip": "Tip: Enable Omi in Shortcuts → Siri.", "paywallOutlineHeadline": "Gå aldrig glip af et ord igen.", "paywallCancelAnytime": "Opsig når som helst.", "connectAnAgent": "Tilslut en agent", diff --git a/app/lib/l10n/app_de.arb b/app/lib/l10n/app_de.arb index 8829b0b302c..c1a66a9ecbb 100644 --- a/app/lib/l10n/app_de.arb +++ b/app/lib/l10n/app_de.arb @@ -520,7 +520,7 @@ "dontShowAgain": "Nicht erneut anzeigen", "iUnderstand": "Ich verstehe", "enableBluetooth": "Bluetooth aktivieren", - "bluetoothNeeded": "Omi benötigt Bluetooth, um sich mit Ihrem Wearable zu verbinden. Bitte aktivieren Sie Bluetooth und versuchen Sie es erneut.", + "bluetoothNeeded": "Omi verbindet sich über Bluetooth mit deinem Handy. Schalte es ein, um fortzufahren.", "contactSupport": "Support kontaktieren?", "connectLater": "Später verbinden", "grantPermissions": "Berechtigungen erteilen", @@ -1501,7 +1501,7 @@ "durationSeconds": "Dauer: {seconds} Sekunden", "additionalSpeechSampleRemoved": "Zusätzliche Sprachprobe entfernt", "consentDataMessage": "Durch Fortfahren werden Ihre Gespräche, Aufnahmen und persönlichen Daten sicher auf unseren Servern gespeichert. Ihre Audioaufnahmen und Transkripte werden von KI-Diensten Dritter verarbeitet (einschließlich Deepgram für die Transkription und OpenAI für die Analyse), um Ihnen KI-gestützte Erkenntnisse zu liefern und alle App-Funktionen zu ermöglichen.", - "tasksEmptyStateMessage": "Starte ein Gespräch, um eine Aufgabe zu erstellen.", + "tasksEmptyStateMessage": "To-dos aus deinen Gesprächen erscheinen hier.", "clearChatAction": "Chat löschen", "enableApps": "Apps aktivieren", "omiAppName": "Omi", @@ -4004,6 +4004,49 @@ "reviewProposalSuggested": "Vorschlag", "reviewProposalConflict": "Das hat sich geändert, seit Dream es vorgeschlagen hat. Es wurde nichts übernommen.", "itemLinkListTitle": "{count, plural, =0{Keine Elemente} =1{1 Element} other{{count} Elemente}}", + "onboardingPermissionTapToFix": "Zum Beheben tippen", + "onboardingNoOmiYet": "Hast du dein Omi noch nicht?", + "onboardingOmiOrdered": "Ich habe es bestellt", + "onboardingOmiNotOrdered": "Noch nicht bestellt", + "onboardingConnectOmi": "Omi verbinden", + "consentShortMessage": "Omi macht mit KI aus deinen Gesprächen Notizen. Du kannst sie jederzeit löschen.", + "consentAiProviders": "Verarbeitet mit {providers}.", + "onboardingActIntro": "Probier Omi aus.", + "onboardingActMicTitle": "Omi braucht dein Mikrofon", + "onboardingActMicBody": "Damit Omi dich hört, wenn du mit ihm sprichst.", + "onboardingActMicAllow": "Mikrofon erlauben", + "onboardingActDictateTitle": "Erzähl Omi von deinem Tag", + "onboardingActDictateBody": "Sag es auf deine Art. Omi erledigt den Rest.", + "onboardingActDictateExample": "Hatte gerade Mittagessen mit Sam, sie zieht im Juni nach Lissabon! Erinnere mich, die Präsentation bis Freitag ans Team zu schicken und auf dem Heimweg Milch zu holen.", + "onboardingActDictateHint": "Tippe auf den Haken, wenn du fertig bist", + "onboardingActPhoneMic": "Handymikrofon", + "onboardingActWriting": "Wird zusammengefasst…", + "onboardingActRemembers": "Omi merkt es sich, damit du es nicht musst", + "onboardingActNotesLabel": "Dein Tag, zusammengefasst", + "onboardingActTasksLabel": "Omi hat deine To-dos erkannt", + "onboardingActYourWords": "Das hast du gesagt", + "onboardingActCongratsTitle": "Alles bereit, {name}", + "onboardingActRateTitle": "Gefällt dir Omi bisher?", + "onboardingActRateYes": "Omi bewerten", + "onboardingWelcomeCardTitle": "Willkommen bei Omi", + "onboardingWelcomeCardBody": "Trag Omi und sprich wie gewohnt. Jedes Gespräch wird hier zu Notizen, Aufgaben und Erinnerungen.", + "onboardingActDemoTitle": "Mittagessen mit Sam", + "onboardingActDemoSummary": "Mittagessen mit Sam, der im Juni nach Lissabon zieht. Du willst die Präsentation bis Freitag ans Team schicken und auf dem Heimweg Milch holen.", + "onboardingActDemoTask1": "Präsentation bis Freitag ans Team schicken", + "onboardingActDemoTask2": "Auf dem Heimweg Milch holen", + "onboardingActJustNow": "Gerade eben", + "onboardingActSavedToTasks": "In deinen Aufgaben gespeichert", + "onboardingConnectTapToConnect": "Tippe auf dein Omi, um es zu verbinden.", + "onboardingActMicFallbackBody": "Dein Omi sendet noch keinen Ton. Nutze vorerst dein Handy.", + "onboardingActNoAudioOmi": "Dein Omi ist nicht zu hören. Ist es an und in der Nähe?", + "onboardingActNoAudioPhone": "Wir hören dich noch nicht. Sprich etwas lauter.", + "onboardingActDictateBodyOmi": "Sprich einfach. Dein Omi hört zu.", + "voiceProfileCardDismissed": "Dein Stimmprofil kannst du jederzeit in den Einstellungen einrichten.", + "homeNewRecordingButton": "Neue Aufnahme", + "chatWelcomeMessage": "Hi, ich bin Omi. Frag mich alles, worüber du gesprochen hast: was jemand gesagt hat, was du versprochen hast oder eine Zusammenfassung deines Tages. Ich kann auch Aufgaben hinzufügen und mir Dinge für dich merken.", + "onboardingStarterTaskAsk": "Frag Omi nach deinem Tag", + "onboardingStarterTaskRecord": "Nimm ein Gespräch mit Omi auf", + "onboardingStarterTaskCheck": "Hake diese Aufgabe ab, um sie zu erledigen", "paywallRecommended": "Recommended", "paywallNoPlans": "Plans aren't available right now. Try again in a minute.", "paywallOtherPlans": "See all plans", @@ -4014,21 +4057,6 @@ "paywallManageBilling": "Manage billing", "paywallLoadError": "Couldn't load plans. Check your connection and try again.", "paywallPlans": "Plans", - "firstCaptureTitle": "How will you capture?", - "firstCaptureDescription": "Choose how you want to capture your first conversation.", - "firstCaptureOmi": "I Have an Omi", - "firstCapturePhone": "This Phone", - "firstCaptureWatch": "Apple Watch", - "firstCaptureSkip": "Skip for Now", - "firstCapturePrompt": "Tell Omi what you're working on today, or what's on your mind. Talk for about 30 seconds.", - "firstCaptureKeepGoing": "Keep going… 30 seconds is ideal.", - "firstCaptureReady": "You can finish now, or keep talking.", - "firstCaptureStart": "Start Recording", - "firstCaptureFinish": "Finish Recording", - "firstCaptureListening": "Listening… your words will appear here.", - "firstCaptureProcessing": "Creating your first conversation…", - "firstCaptureResult": "Your first conversation", - "firstCaptureEmpty": "I didn't catch enough speech, try again with a few sentences", "watchCaptureTitle": "Record from your Apple Watch", "watchCaptureOpen": "Open Omi on your Apple Watch.", "watchCaptureRecord": "Tap Record. Allow microphone access if asked.", @@ -4040,17 +4068,6 @@ "pairingNotFoundOrDropped": "Couldn't reach your Omi. Make sure it's charged, close to your phone, and its light is blinking, then try again.", "pairingBondRecovery": "Forget Omi in your phone’s Bluetooth settings, then try pairing again.", "pairingHowTo": "How to pair", - "firstCaptureWatchInstructions": "Open Omi on your Apple Watch and tap Record. Allow microphone access if asked. Your capture will sync to Omi on this phone.", - "firstCaptureWatchInstall": "In the Watch app on your iPhone, install Omi on your Apple Watch. Then open Omi on your watch and tap Record.", - "firstCaptureWatchPair": "Pair your Apple Watch with this iPhone using the Watch app, then install and open Omi on your watch.", - "firstCaptureUnavailable": "Recording could not start. Check microphone access and your connection, then try again.", - "firstCaptureDelayed": "Your recording is taking longer to process. You can check your conversations on Home or try again.", - "firstCaptureEmptyTitle": "Let's try that again", - "firstCaptureShortTitle": "Finish This Recording?", - "firstCaptureShortWarning": "A few more sentences help Omi understand you. About 30 seconds is ideal.", - "firstCaptureCancelTitle": "Leave This Recording?", - "firstCaptureCancelWarning": "Going back stops this recording. Keep talking to give Omi a few more sentences.", - "firstCaptureSiriTip": "Tip: Enable Omi in Shortcuts → Siri.", "paywallOutlineHeadline": "Verpasse nie wieder ein Wort.", "paywallCancelAnytime": "Jederzeit kündbar.", "connectAnAgent": "Agent verbinden", diff --git a/app/lib/l10n/app_el.arb b/app/lib/l10n/app_el.arb index e74aaa79987..bb4957715a5 100644 --- a/app/lib/l10n/app_el.arb +++ b/app/lib/l10n/app_el.arb @@ -520,7 +520,7 @@ "dontShowAgain": "Να μην εμφανιστεί ξανά", "iUnderstand": "Κατανοώ", "enableBluetooth": "Ενεργοποίηση Bluetooth", - "bluetoothNeeded": "Το Omi χρειάζεται Bluetooth για σύνδεση με τη φορητή σας συσκευή. Παρακαλώ ενεργοποιήστε το Bluetooth και δοκιμάστε ξανά.", + "bluetoothNeeded": "Το Omi συνδέεται με το τηλέφωνό σας μέσω Bluetooth. Ενεργοποιήστε το για να συνεχίσετε.", "contactSupport": "Επικοινωνία με Υποστήριξη;", "connectLater": "Σύνδεση Αργότερα", "grantPermissions": "Χορήγηση αδειών", @@ -1501,7 +1501,7 @@ "durationSeconds": "Διάρκεια: {seconds} δευτερόλεπτα", "additionalSpeechSampleRemoved": "Το πρόσθετο δείγμα ομιλίας αφαιρέθηκε", "consentDataMessage": "Συνεχίζοντας, οι συνομιλίες, οι εγγραφές και τα προσωπικά σας στοιχεία θα αποθηκευτούν με ασφάλεια στους διακομιστές μας. Οι ηχογραφήσεις και τα μεταγραφές σας επεξεργάζονται από υπηρεσίες τεχνητής νοημοσύνης τρίτων (συμπεριλαμβανομένων των Deepgram για μεταγραφή και OpenAI για ανάλυση) για να σας παρέχουν γνώσεις βασισμένες σε AI και να ενεργοποιήσουν όλες τις λειτουργίες της εφαρμογής.", - "tasksEmptyStateMessage": "Ξεκινήστε μια συνομιλία για να δημιουργήσετε μια εργασία.", + "tasksEmptyStateMessage": "Οι εργασίες από τις συζητήσεις σας θα εμφανίζονται εδώ.", "clearChatAction": "Διαγραφή συνομιλίας", "enableApps": "Ενεργοποίηση εφαρμογών", "omiAppName": "Omi", @@ -4004,6 +4004,49 @@ "reviewProposalSuggested": "Προτεινόμενο", "reviewProposalConflict": "Αυτό άλλαξε αφότου το πρότεινε το Dream, οπότε δεν εφαρμόστηκε τίποτα.", "itemLinkListTitle": "{count, plural, =0{Κανένα στοιχείο} =1{1 στοιχείο} other{{count} στοιχεία}}", + "onboardingPermissionTapToFix": "Πατήστε για διόρθωση", + "onboardingNoOmiYet": "Δεν έχετε ακόμα το Omi σας;", + "onboardingOmiOrdered": "Το έχω παραγγείλει", + "onboardingOmiNotOrdered": "Δεν το έχω παραγγείλει ακόμα", + "onboardingConnectOmi": "Συνδέστε το Omi", + "consentShortMessage": "Το Omi χρησιμοποιεί AI για να μετατρέπει τις συνομιλίες σου σε σημειώσεις. Μπορείς να τις διαγράψεις οποιαδήποτε στιγμή.", + "consentAiProviders": "Επεξεργασία μέσω {providers}.", + "onboardingActIntro": "Ας δοκιμάσουμε το Omi.", + "onboardingActMicTitle": "Το Omi χρειάζεται το μικρόφωνό σας", + "onboardingActMicBody": "Για να σας ακούει το Omi όταν του μιλάτε.", + "onboardingActMicAllow": "Να επιτρέπεται το μικρόφωνο", + "onboardingActDictateTitle": "Πείτε στο Omi για τη μέρα σας", + "onboardingActDictateBody": "Πείτε το με τον τρόπο σας. Το Omi φροντίζει τα υπόλοιπα.", + "onboardingActDictateExample": "Μόλις έφαγα με τη Σαμ, μετακομίζει στη Λισαβόνα τον Ιούνιο! Θύμισέ μου να στείλω την παρουσίαση στην ομάδα ως την Παρασκευή και να πάρω γάλα στον δρόμο.", + "onboardingActDictateHint": "Πατήστε το τικ όταν τελειώσετε", + "onboardingActPhoneMic": "Μικρόφωνο τηλεφώνου", + "onboardingActWriting": "Γίνεται σύνοψη…", + "onboardingActRemembers": "Το Omi θυμάται, για να μη χρειάζεται εσύ", + "onboardingActNotesLabel": "Η μέρα σου, σε σύνοψη", + "onboardingActTasksLabel": "Το Omi εντόπισε τις εκκρεμότητές σου", + "onboardingActYourWords": "Αυτά είπατε", + "onboardingActCongratsTitle": "Όλα έτοιμα, {name}", + "onboardingActRateTitle": "Σας αρέσει το Omi μέχρι τώρα;", + "onboardingActRateYes": "Αξιολογήστε το Omi", + "onboardingWelcomeCardTitle": "Καλώς ήρθατε στο Omi", + "onboardingWelcomeCardBody": "Φορέστε το Omi και μιλήστε όπως πάντα. Κάθε συνομιλία γίνεται εδώ σημειώσεις, εργασίες και αναμνήσεις.", + "onboardingActDemoTitle": "Γεύμα με τη Σαμ", + "onboardingActDemoSummary": "Μεσημεριανό με τον Σαμ, που μετακομίζει στη Λισαβόνα τον Ιούνιο. Θέλεις να στείλεις την παρουσίαση στην ομάδα ως την Παρασκευή και να πάρεις γάλα στον δρόμο για το σπίτι.", + "onboardingActDemoTask1": "Αποστολή της παρουσίασης στην ομάδα ως την Παρασκευή", + "onboardingActDemoTask2": "Γάλα στον δρόμο για το σπίτι", + "onboardingActJustNow": "Μόλις τώρα", + "onboardingActSavedToTasks": "Αποθηκεύτηκε στις εργασίες σου", + "onboardingConnectTapToConnect": "Πάτησε το Omi σου για σύνδεση.", + "onboardingActMicFallbackBody": "Το Omi δεν στέλνει ακόμα ήχο. Χρησιμοποίησε προς το παρόν το τηλέφωνό σου.", + "onboardingActNoAudioOmi": "Δεν ακούμε το Omi. Βεβαιώσου ότι είναι ανοιχτό και κοντά σου.", + "onboardingActNoAudioPhone": "Δεν σε ακούμε ακόμα. Δοκίμασε να μιλήσεις λίγο πιο δυνατά.", + "onboardingActDictateBodyOmi": "Απλώς μίλα. Το Omi σου ακούει.", + "voiceProfileCardDismissed": "Μπορείτε να ρυθμίσετε το προφίλ φωνής ανά πάσα στιγμή από τις Ρυθμίσεις.", + "homeNewRecordingButton": "Νέα ηχογράφηση", + "chatWelcomeMessage": "Γεια, είμαι το Omi. Ρώτα με οτιδήποτε έχεις συζητήσει: τι είπε κάποιος, τι υποσχέθηκες ή μια σύνοψη της μέρας σου. Μπορώ επίσης να προσθέτω εργασίες και να θυμάμαι πράγματα για σένα.", + "onboardingStarterTaskAsk": "Ρώτα το Omi για τη μέρα σου", + "onboardingStarterTaskRecord": "Ηχογράφησε μια συζήτηση με το Omi", + "onboardingStarterTaskCheck": "Σημείωσε αυτή την εργασία για να την ολοκληρώσεις", "paywallRecommended": "Recommended", "paywallNoPlans": "Plans aren't available right now. Try again in a minute.", "paywallOtherPlans": "See all plans", @@ -4014,21 +4057,6 @@ "paywallManageBilling": "Manage billing", "paywallLoadError": "Couldn't load plans. Check your connection and try again.", "paywallPlans": "Plans", - "firstCaptureTitle": "How will you capture?", - "firstCaptureDescription": "Choose how you want to capture your first conversation.", - "firstCaptureOmi": "I Have an Omi", - "firstCapturePhone": "This Phone", - "firstCaptureWatch": "Apple Watch", - "firstCaptureSkip": "Skip for Now", - "firstCapturePrompt": "Tell Omi what you're working on today, or what's on your mind. Talk for about 30 seconds.", - "firstCaptureKeepGoing": "Keep going… 30 seconds is ideal.", - "firstCaptureReady": "You can finish now, or keep talking.", - "firstCaptureStart": "Start Recording", - "firstCaptureFinish": "Finish Recording", - "firstCaptureListening": "Listening… your words will appear here.", - "firstCaptureProcessing": "Creating your first conversation…", - "firstCaptureResult": "Your first conversation", - "firstCaptureEmpty": "I didn't catch enough speech, try again with a few sentences", "watchCaptureTitle": "Record from your Apple Watch", "watchCaptureOpen": "Open Omi on your Apple Watch.", "watchCaptureRecord": "Tap Record. Allow microphone access if asked.", @@ -4040,17 +4068,6 @@ "pairingNotFoundOrDropped": "Couldn't reach your Omi. Make sure it's charged, close to your phone, and its light is blinking, then try again.", "pairingBondRecovery": "Forget Omi in your phone’s Bluetooth settings, then try pairing again.", "pairingHowTo": "How to pair", - "firstCaptureWatchInstructions": "Open Omi on your Apple Watch and tap Record. Allow microphone access if asked. Your capture will sync to Omi on this phone.", - "firstCaptureWatchInstall": "In the Watch app on your iPhone, install Omi on your Apple Watch. Then open Omi on your watch and tap Record.", - "firstCaptureWatchPair": "Pair your Apple Watch with this iPhone using the Watch app, then install and open Omi on your watch.", - "firstCaptureUnavailable": "Recording could not start. Check microphone access and your connection, then try again.", - "firstCaptureDelayed": "Your recording is taking longer to process. You can check your conversations on Home or try again.", - "firstCaptureEmptyTitle": "Let's try that again", - "firstCaptureShortTitle": "Finish This Recording?", - "firstCaptureShortWarning": "A few more sentences help Omi understand you. About 30 seconds is ideal.", - "firstCaptureCancelTitle": "Leave This Recording?", - "firstCaptureCancelWarning": "Going back stops this recording. Keep talking to give Omi a few more sentences.", - "firstCaptureSiriTip": "Tip: Enable Omi in Shortcuts → Siri.", "paywallOutlineHeadline": "Μη χάσετε ποτέ ξανά ούτε λέξη.", "paywallCancelAnytime": "Ακύρωση ανά πάσα στιγμή.", "connectAnAgent": "Σύνδεση πράκτορα", diff --git a/app/lib/l10n/app_en.arb b/app/lib/l10n/app_en.arb index 7fd38c6a7ca..c9d22e59a34 100644 --- a/app/lib/l10n/app_en.arb +++ b/app/lib/l10n/app_en.arb @@ -1604,7 +1604,7 @@ "@enableBluetooth": { "description": "Title for dialog prompting user to enable Bluetooth" }, - "bluetoothNeeded": "Omi needs Bluetooth to connect to your wearable. Please enable Bluetooth and try again.", + "bluetoothNeeded": "Omi connects to your phone over Bluetooth. Turn it on to continue.", "@bluetoothNeeded": { "description": "Explanation text for why Bluetooth is required" }, @@ -5829,7 +5829,7 @@ "@consentDataMessage": { "description": "Consent message explaining how user data will be stored and used" }, - "tasksEmptyStateMessage": "Start a conversation to create a task.", + "tasksEmptyStateMessage": "To-dos from your conversations will show up here.", "@tasksEmptyStateMessage": { "description": "Guidance on the empty Tasks tab: start a conversation to create a task." }, @@ -15745,6 +15745,190 @@ } } }, + "onboardingPermissionTapToFix": "Tap to fix", + "@onboardingPermissionTapToFix": { + "description": "Onboarding permissions: shown on a denied permission card; tapping opens Settings" + }, + "onboardingNoOmiYet": "Don't have your Omi yet?", + "@onboardingNoOmiYet": { + "description": "Onboarding connect step: small prompt for people without their Omi device" + }, + "onboardingOmiOrdered": "I've ordered mine", + "@onboardingOmiOrdered": { + "description": "Onboarding connect step: choice for people whose Omi device is on its way" + }, + "onboardingOmiNotOrdered": "Not ordered yet", + "@onboardingOmiNotOrdered": { + "description": "Onboarding connect step: choice for people who have not ordered an Omi device" + }, + "onboardingConnectOmi": "Connect Omi", + "@onboardingConnectOmi": { + "description": "Onboarding connect step title; Omi is the brand name of the wearable" + }, + "consentShortMessage": "Omi uses AI to turn your conversations into notes. You can delete them anytime.", + "@consentShortMessage": { + "description": "Onboarding data & privacy consent: one-line summary of storage and AI processing" + }, + "consentAiProviders": "Processed with {providers}.", + "@consentAiProviders": { + "description": "Onboarding consent: names the third-party AI providers (brand names, not translated)", + "placeholders": { + "providers": { + "type": "String", + "example": "Deepgram, OpenAI" + } + } + }, + "onboardingActIntro": "Let's try Omi.", + "@onboardingActIntro": { + "description": "Activation: animated line before the demo slides" + }, + "onboardingActMicTitle": "Omi needs your microphone", + "@onboardingActMicTitle": { + "description": "Activation: microphone pre-permission title (phone users)" + }, + "onboardingActMicBody": "So Omi can hear you when you talk to it.", + "@onboardingActMicBody": { + "description": "Activation: why the microphone is needed" + }, + "onboardingActMicAllow": "Allow Microphone", + "@onboardingActMicAllow": { + "description": "Activation: button that shows the microphone system prompt" + }, + "onboardingActDictateTitle": "Tell Omi about your day", + "@onboardingActDictateTitle": { + "description": "Activation: dictation step title" + }, + "onboardingActDictateBody": "Say it your way. Omi handles the rest.", + "@onboardingActDictateBody": { + "description": "Activation: line under the dictation title" + }, + "onboardingActDictateExample": "Just had lunch with Sam, she's moving to Lisbon in June! Remind me to send the deck to my team by Friday, and to grab milk on the way home.", + "@onboardingActDictateExample": { + "description": "Activation: faded example in the dictation card (casual tone; keep 'Remind me' so Omi keeps it; Sam and Lisbon are example names)" + }, + "onboardingActDictateHint": "Tap the check when you're done", + "@onboardingActDictateHint": { + "description": "Activation: hint in the dictation bar next to the check button" + }, + "onboardingActPhoneMic": "Phone microphone", + "@onboardingActPhoneMic": { + "description": "Activation: which microphone is listening (phone)" + }, + "onboardingActWriting": "Summarizing…", + "@onboardingActWriting": { + "description": "Activation: shown while Omi processes the dictation" + }, + "onboardingActRemembers": "Omi remembers so you don't have to", + "@onboardingActRemembers": { + "description": "Line above the summary title at the end of the onboarding dictation" + }, + "onboardingActNotesLabel": "Your day, summarized", + "@onboardingActNotesLabel": { + "description": "Activation: small label above the generated summary" + }, + "onboardingActTasksLabel": "Omi caught your to-dos", + "@onboardingActTasksLabel": { + "description": "Activation: small label above the generated tasks" + }, + "onboardingActYourWords": "Here's what you said", + "@onboardingActYourWords": { + "description": "Activation: fallback label when no summary could be made" + }, + "onboardingActCongratsTitle": "You're all set, {name}", + "@onboardingActCongratsTitle": { + "description": "Activation: congratulations title with the user's first name", + "placeholders": { + "name": { + "type": "String", + "example": "Yinka" + } + } + }, + "onboardingActRateTitle": "Enjoying Omi so far?", + "@onboardingActRateTitle": { + "description": "Activation: rating ask on the congratulations screen" + }, + "onboardingActRateYes": "Rate Omi", + "@onboardingActRateYes": { + "description": "Activation: button that opens the system rating prompt" + }, + "onboardingWelcomeCardTitle": "Welcome to Omi", + "@onboardingWelcomeCardTitle": { + "description": "Home: welcome card title for people who skipped the first conversation" + }, + "onboardingWelcomeCardBody": "Wear Omi and talk as usual. Each conversation turns into notes, tasks and memories right here.", + "@onboardingWelcomeCardBody": { + "description": "Home: welcome card text" + }, + "onboardingActDemoTitle": "Lunch with Sam", + "@onboardingActDemoTitle": { + "description": "Activation slide illustration: example note title" + }, + "onboardingActDemoSummary": "Lunch with Sam, who's moving to Lisbon in June. You want to send the deck to your team by Friday and grab milk on the way home.", + "@onboardingActDemoSummary": { + "description": "Activation slide illustration: example summary line" + }, + "onboardingActDemoTask1": "Send the deck to the team by Friday", + "@onboardingActDemoTask1": { + "description": "Activation slide illustration: example task" + }, + "onboardingActDemoTask2": "Grab milk on the way home", + "@onboardingActDemoTask2": { + "description": "Activation slide illustration: example task" + }, + "onboardingActJustNow": "Just now", + "@onboardingActJustNow": { + "description": "Time label on the first note Omi wrote in onboarding" + }, + "onboardingActSavedToTasks": "Saved to your tasks", + "@onboardingActSavedToTasks": { + "description": "Confirmation under the to-dos Omi found in onboarding" + }, + "onboardingConnectTapToConnect": "Tap your Omi to connect.", + "@onboardingConnectTapToConnect": { + "description": "Connect Omi onboarding step, under the title once an Omi is found nearby" + }, + "onboardingActMicFallbackBody": "Your Omi isn't sending audio yet. Use your phone for now.", + "@onboardingActMicFallbackBody": { + "description": "Onboarding microphone ask when the connected Omi is not streaming" + }, + "onboardingActNoAudioOmi": "Can't hear your Omi. Make sure it's on and close by.", + "@onboardingActNoAudioOmi": { + "description": "Onboarding dictation hint after a while with no words from the Omi" + }, + "onboardingActNoAudioPhone": "Can't hear you yet. Try speaking a little louder.", + "@onboardingActNoAudioPhone": { + "description": "Onboarding dictation hint after a while with no words from the phone microphone" + }, + "onboardingActDictateBodyOmi": "Just talk. Your Omi is listening.", + "@onboardingActDictateBodyOmi": { + "description": "Onboarding dictation subtitle when the words come from the connected Omi" + }, + "voiceProfileCardDismissed": "You can set up Voice Profile anytime in Settings.", + "@voiceProfileCardDismissed": { + "description": "Toast after dismissing the Teach Omi your voice card on Home; points to Settings > Voice Profile." + }, + "homeNewRecordingButton": "New Recording", + "@homeNewRecordingButton": { + "description": "Label on Home's main bottom button that starts a new recording with the phone." + }, + "chatWelcomeMessage": "Hi, I'm Omi. Ask me anything you've talked about: what someone said, what you promised, or a summary of your day. I can also add to-dos and remember things for you.", + "@chatWelcomeMessage": { + "description": "First message Omi shows in an empty chat the first time it is opened, explaining what chat can do" + }, + "onboardingStarterTaskAsk": "Ask Omi about your day", + "@onboardingStarterTaskAsk": { + "description": "Starter task added to an empty task list after onboarding: try chatting with Omi" + }, + "onboardingStarterTaskRecord": "Record a conversation with Omi", + "@onboardingStarterTaskRecord": { + "description": "Starter task added to an empty task list after onboarding: try recording" + }, + "onboardingStarterTaskCheck": "Check this off to finish a task", + "@onboardingStarterTaskCheck": { + "description": "Starter task added to an empty task list after onboarding: shows how completing a task works" + }, "paywallRecommended": "Recommended", "@paywallRecommended": { "description": "Backend recommended tier badge; highlights a recommendation without preselecting checkout." @@ -15785,21 +15969,6 @@ "@paywallPlans": { "description": "Title of the new plans sheet. The previous sheet keeps Plans & Billing." }, - "firstCaptureTitle": "How will you capture?", - "firstCaptureDescription": "Choose how you want to capture your first conversation.", - "firstCaptureOmi": "I Have an Omi", - "firstCapturePhone": "This Phone", - "firstCaptureWatch": "Apple Watch", - "firstCaptureSkip": "Skip for Now", - "firstCapturePrompt": "Tell Omi what you're working on today, or what's on your mind. Talk for about 30 seconds.", - "firstCaptureKeepGoing": "Keep going… 30 seconds is ideal.", - "firstCaptureReady": "You can finish now, or keep talking.", - "firstCaptureStart": "Start Recording", - "firstCaptureFinish": "Finish Recording", - "firstCaptureListening": "Listening… your words will appear here.", - "firstCaptureProcessing": "Creating your first conversation…", - "firstCaptureResult": "Your first conversation", - "firstCaptureEmpty": "I didn't catch enough speech, try again with a few sentences", "watchCaptureTitle": "Record from your Apple Watch", "watchCaptureOpen": "Open Omi on your Apple Watch.", "watchCaptureRecord": "Tap Record. Allow microphone access if asked.", @@ -15811,17 +15980,6 @@ "pairingNotFoundOrDropped": "Couldn't reach your Omi. Make sure it's charged, close to your phone, and its light is blinking, then try again.", "pairingBondRecovery": "Forget Omi in your phone’s Bluetooth settings, then try pairing again.", "pairingHowTo": "How to pair", - "firstCaptureWatchInstructions": "Open Omi on your Apple Watch and tap Record. Allow microphone access if asked. Your capture will sync to Omi on this phone.", - "firstCaptureWatchInstall": "In the Watch app on your iPhone, install Omi on your Apple Watch. Then open Omi on your watch and tap Record.", - "firstCaptureWatchPair": "Pair your Apple Watch with this iPhone using the Watch app, then install and open Omi on your watch.", - "firstCaptureUnavailable": "Recording could not start. Check microphone access and your connection, then try again.", - "firstCaptureDelayed": "Your recording is taking longer to process. You can check your conversations on Home or try again.", - "firstCaptureEmptyTitle": "Let's try that again", - "firstCaptureShortTitle": "Finish This Recording?", - "firstCaptureShortWarning": "A few more sentences help Omi understand you. About 30 seconds is ideal.", - "firstCaptureCancelTitle": "Leave This Recording?", - "firstCaptureCancelWarning": "Going back stops this recording. Keep talking to give Omi a few more sentences.", - "firstCaptureSiriTip": "Tip: Enable Omi in Shortcuts → Siri.", "paywallOutlineHeadline": "Never miss a word again.", "@paywallOutlineHeadline": { "description": "Headline of the Outline paywall variant (mobile paywall A/B test)" diff --git a/app/lib/l10n/app_es.arb b/app/lib/l10n/app_es.arb index d845b20f412..b82ba6d6fac 100644 --- a/app/lib/l10n/app_es.arb +++ b/app/lib/l10n/app_es.arb @@ -515,7 +515,7 @@ "dontShowAgain": "No mostrar de nuevo", "iUnderstand": "Entiendo", "enableBluetooth": "Activar Bluetooth", - "bluetoothNeeded": "Omi necesita Bluetooth para conectar tu wearable.", + "bluetoothNeeded": "Omi se conecta a tu teléfono por Bluetooth. Actívalo para continuar.", "contactSupport": "¿Contactar soporte?", "connectLater": "Conectar más tarde", "grantPermissions": "Conceder permisos", @@ -1501,7 +1501,7 @@ "durationSeconds": "Duración: {seconds} segundos", "additionalSpeechSampleRemoved": "Muestra de voz adicional eliminada", "consentDataMessage": "Al continuar, tus conversaciones, grabaciones e información personal se almacenarán de forma segura en nuestros servidores. Tus grabaciones de audio y transcripciones son procesadas por servicios de IA de terceros (incluyendo Deepgram para transcripción y OpenAI para análisis) para proporcionarte información impulsada por IA y habilitar todas las funciones de la aplicación.", - "tasksEmptyStateMessage": "Inicia una conversación para crear una tarea.", + "tasksEmptyStateMessage": "Las tareas de tus conversaciones aparecerán aquí.", "clearChatAction": "Borrar chat", "enableApps": "Habilitar apps", "omiAppName": "Omi", @@ -4004,6 +4004,49 @@ "reviewProposalSuggested": "Sugerido", "reviewProposalConflict": "Esto cambió desde que Dream lo sugirió, así que no se aplicó nada.", "itemLinkListTitle": "{count, plural, =0{Sin elementos} =1{1 elemento} other{{count} elementos}}", + "onboardingPermissionTapToFix": "Toca para solucionarlo", + "onboardingNoOmiYet": "¿Aún no tienes tu Omi?", + "onboardingOmiOrdered": "Ya lo pedí", + "onboardingOmiNotOrdered": "Aún no lo he pedido", + "onboardingConnectOmi": "Conecta tu Omi", + "consentShortMessage": "Omi usa IA para convertir tus conversaciones en notas. Puedes eliminarlas cuando quieras.", + "consentAiProviders": "Procesado con {providers}.", + "onboardingActIntro": "Probemos Omi.", + "onboardingActMicTitle": "Omi necesita tu micrófono", + "onboardingActMicBody": "Para que Omi te escuche cuando le hables.", + "onboardingActMicAllow": "Permitir micrófono", + "onboardingActDictateTitle": "Cuéntale a Omi tu día", + "onboardingActDictateBody": "Dilo a tu manera. Omi se encarga del resto.", + "onboardingActDictateExample": "¡Acabo de comer con Sam, se muda a Lisboa en junio! Recuérdame enviar la presentación al equipo antes del viernes y comprar leche de camino a casa.", + "onboardingActDictateHint": "Toca el check cuando termines", + "onboardingActPhoneMic": "Micrófono del teléfono", + "onboardingActWriting": "Resumiendo…", + "onboardingActRemembers": "Omi lo recuerda para que tú no tengas que hacerlo", + "onboardingActNotesLabel": "Tu día, resumido", + "onboardingActTasksLabel": "Omi detectó tus pendientes", + "onboardingActYourWords": "Esto es lo que dijiste", + "onboardingActCongratsTitle": "Todo listo, {name}", + "onboardingActRateTitle": "¿Te gusta Omi hasta ahora?", + "onboardingActRateYes": "Valorar Omi", + "onboardingWelcomeCardTitle": "Te damos la bienvenida a Omi", + "onboardingWelcomeCardBody": "Lleva Omi y habla como siempre. Cada conversación se convierte aquí en notas, tareas y recuerdos.", + "onboardingActDemoTitle": "Comida con Sam", + "onboardingActDemoSummary": "Almuerzo con Sam, que se muda a Lisboa en junio. Quieres enviar la presentación a tu equipo antes del viernes y comprar leche de camino a casa.", + "onboardingActDemoTask1": "Enviar la presentación al equipo antes del viernes", + "onboardingActDemoTask2": "Comprar leche de camino a casa", + "onboardingActJustNow": "Ahora mismo", + "onboardingActSavedToTasks": "Guardado en tus tareas", + "onboardingConnectTapToConnect": "Toca tu Omi para conectarlo.", + "onboardingActMicFallbackBody": "Tu Omi aún no envía audio. Usa tu teléfono por ahora.", + "onboardingActNoAudioOmi": "No se oye tu Omi. Comprueba que esté encendido y cerca.", + "onboardingActNoAudioPhone": "Aún no te oímos. Prueba a hablar un poco más alto.", + "onboardingActDictateBodyOmi": "Solo habla. Tu Omi te escucha.", + "voiceProfileCardDismissed": "Puedes configurar tu perfil de voz cuando quieras en Ajustes.", + "homeNewRecordingButton": "Nueva grabación", + "chatWelcomeMessage": "Hola, soy Omi. Pregúntame lo que quieras sobre lo que has hablado: lo que dijo alguien, lo que prometiste o un resumen de tu día. También puedo añadir tareas y recordar cosas por ti.", + "onboardingStarterTaskAsk": "Pregúntale a Omi por tu día", + "onboardingStarterTaskRecord": "Graba una conversación con Omi", + "onboardingStarterTaskCheck": "Marca esta tarea para completarla", "paywallRecommended": "Recommended", "paywallNoPlans": "Plans aren't available right now. Try again in a minute.", "paywallOtherPlans": "See all plans", @@ -4014,21 +4057,6 @@ "paywallManageBilling": "Manage billing", "paywallLoadError": "Couldn't load plans. Check your connection and try again.", "paywallPlans": "Plans", - "firstCaptureTitle": "How will you capture?", - "firstCaptureDescription": "Choose how you want to capture your first conversation.", - "firstCaptureOmi": "I Have an Omi", - "firstCapturePhone": "This Phone", - "firstCaptureWatch": "Apple Watch", - "firstCaptureSkip": "Skip for Now", - "firstCapturePrompt": "Tell Omi what you're working on today, or what's on your mind. Talk for about 30 seconds.", - "firstCaptureKeepGoing": "Keep going… 30 seconds is ideal.", - "firstCaptureReady": "You can finish now, or keep talking.", - "firstCaptureStart": "Start Recording", - "firstCaptureFinish": "Finish Recording", - "firstCaptureListening": "Listening… your words will appear here.", - "firstCaptureProcessing": "Creating your first conversation…", - "firstCaptureResult": "Your first conversation", - "firstCaptureEmpty": "I didn't catch enough speech, try again with a few sentences", "watchCaptureTitle": "Record from your Apple Watch", "watchCaptureOpen": "Open Omi on your Apple Watch.", "watchCaptureRecord": "Tap Record. Allow microphone access if asked.", @@ -4040,17 +4068,6 @@ "pairingNotFoundOrDropped": "Couldn't reach your Omi. Make sure it's charged, close to your phone, and its light is blinking, then try again.", "pairingBondRecovery": "Forget Omi in your phone’s Bluetooth settings, then try pairing again.", "pairingHowTo": "How to pair", - "firstCaptureWatchInstructions": "Open Omi on your Apple Watch and tap Record. Allow microphone access if asked. Your capture will sync to Omi on this phone.", - "firstCaptureWatchInstall": "In the Watch app on your iPhone, install Omi on your Apple Watch. Then open Omi on your watch and tap Record.", - "firstCaptureWatchPair": "Pair your Apple Watch with this iPhone using the Watch app, then install and open Omi on your watch.", - "firstCaptureUnavailable": "Recording could not start. Check microphone access and your connection, then try again.", - "firstCaptureDelayed": "Your recording is taking longer to process. You can check your conversations on Home or try again.", - "firstCaptureEmptyTitle": "Let's try that again", - "firstCaptureShortTitle": "Finish This Recording?", - "firstCaptureShortWarning": "A few more sentences help Omi understand you. About 30 seconds is ideal.", - "firstCaptureCancelTitle": "Leave This Recording?", - "firstCaptureCancelWarning": "Going back stops this recording. Keep talking to give Omi a few more sentences.", - "firstCaptureSiriTip": "Tip: Enable Omi in Shortcuts → Siri.", "paywallOutlineHeadline": "No te pierdas ni una palabra nunca más.", "paywallCancelAnytime": "Cancela cuando quieras.", "connectAnAgent": "Conectar un agente", diff --git a/app/lib/l10n/app_et.arb b/app/lib/l10n/app_et.arb index 470b2ca4baa..0310f334a8c 100644 --- a/app/lib/l10n/app_et.arb +++ b/app/lib/l10n/app_et.arb @@ -520,7 +520,7 @@ "dontShowAgain": "Ära näita uuesti", "iUnderstand": "Ma mõistan", "enableBluetooth": "Luba Bluetooth", - "bluetoothNeeded": "Omi vajab Bluetoothi, et ühenduda teie kantava seadmega. Palun lubage Bluetooth ja proovige uuesti.", + "bluetoothNeeded": "Omi ühendub telefoniga Bluetoothi kaudu. Lülita see jätkamiseks sisse.", "contactSupport": "Võta ühendust toega?", "connectLater": "Ühenda hiljem", "grantPermissions": "Anna load", @@ -1501,7 +1501,7 @@ "durationSeconds": "Kestus: {seconds} sekundit", "additionalSpeechSampleRemoved": "Lisakõnenäidis eemaldatud", "consentDataMessage": "Jätkates salvestatakse teie vestlused, salvestised ja isikuandmed turvaliselt meie serverites. Teie helisalvestisi ja transkriptsioone töötlevad kolmandate osapoolte AI-teenused (sealhulgas Deepgram transkriptsiooni ja OpenAI analüüsi jaoks), et pakkuda teile AI-põhiseid ülevaateid ja võimaldada kõiki rakenduse funktsioone.", - "tasksEmptyStateMessage": "Ülesande loomiseks alusta vestlust.", + "tasksEmptyStateMessage": "Sinu vestlustest pärit ülesanded ilmuvad siia.", "clearChatAction": "Tühjenda vestlus", "enableApps": "Luba rakendused", "omiAppName": "Omi", @@ -4004,6 +4004,49 @@ "reviewProposalSuggested": "Soovitatud", "reviewProposalConflict": "See on pärast Dreami soovitust muutunud, seega midagi ei rakendatud.", "itemLinkListTitle": "{count, plural, =0{Üksusi pole} =1{1 üksus} other{{count} üksust}}", + "onboardingPermissionTapToFix": "Puuduta parandamiseks", + "onboardingNoOmiYet": "Sul pole veel Omit?", + "onboardingOmiOrdered": "Olen selle tellinud", + "onboardingOmiNotOrdered": "Pole veel tellinud", + "onboardingConnectOmi": "Ühenda Omi", + "consentShortMessage": "Omi muudab tehisintellekti abil sinu vestlused märkmeteks. Saad need igal ajal kustutada.", + "consentAiProviders": "Töödeldakse teenustega {providers}.", + "onboardingActIntro": "Proovime Omit.", + "onboardingActMicTitle": "Omi vajab su mikrofoni", + "onboardingActMicBody": "Et Omi kuuleks sind, kui sa temaga räägid.", + "onboardingActMicAllow": "Luba mikrofon", + "onboardingActDictateTitle": "Räägi Omile oma päevast", + "onboardingActDictateBody": "Ütle omal moel. Ülejäänu teeb Omi.", + "onboardingActDictateExample": "Sõin just Samiga lõunat, ta kolib juunis Lissaboni! Tuleta mulle meelde saata esitlus reedeks tiimile ja osta koduteel piima.", + "onboardingActDictateHint": "Kui oled valmis, puuduta linnukest", + "onboardingActPhoneMic": "Telefoni mikrofon", + "onboardingActWriting": "Teeme kokkuvõtet…", + "onboardingActRemembers": "Omi mäletab, et sina ei peaks", + "onboardingActNotesLabel": "Sinu päev lühidalt", + "onboardingActTasksLabel": "Omi märkas sinu ülesandeid", + "onboardingActYourWords": "Seda sa ütlesid", + "onboardingActCongratsTitle": "Kõik on valmis, {name}", + "onboardingActRateTitle": "Kas Omi meeldib sulle seni?", + "onboardingActRateYes": "Hinda Omit", + "onboardingWelcomeCardTitle": "Tere tulemast Omisse", + "onboardingWelcomeCardBody": "Kanna Omit ja räägi nagu ikka. Iga vestlus muutub siin märkmeteks, ülesanneteks ja mälestusteks.", + "onboardingActDemoTitle": "Lõuna Samiga", + "onboardingActDemoSummary": "Lõuna Samiga, kes kolib juunis Lissaboni. Tahad reedeks esitluse meeskonnale saata ja koduteel piima osta.", + "onboardingActDemoTask1": "Saata esitlus reedeks tiimile", + "onboardingActDemoTask2": "Osta koduteel piima", + "onboardingActJustNow": "Just praegu", + "onboardingActSavedToTasks": "Salvestatud sinu ülesannetesse", + "onboardingConnectTapToConnect": "Ühendamiseks puuduta oma Omit.", + "onboardingActMicFallbackBody": "Omi ei saada veel heli. Kasuta praegu oma telefoni.", + "onboardingActNoAudioOmi": "Omit pole kuulda. Veendu, et see on sees ja lähedal.", + "onboardingActNoAudioPhone": "Me ei kuule sind veel. Proovi natuke valjemini rääkida.", + "onboardingActDictateBodyOmi": "Lihtsalt räägi. Sinu Omi kuulab.", + "voiceProfileCardDismissed": "Häälprofiili saad seadistada igal ajal seadetes.", + "homeNewRecordingButton": "Uus salvestus", + "chatWelcomeMessage": "Tere, mina olen Omi. Küsi minult kõike, millest oled rääkinud: mida keegi ütles, mida sa lubasid või kokkuvõtet oma päevast. Saan ka ülesandeid lisada ja asju sinu eest meeles pidada.", + "onboardingStarterTaskAsk": "Küsi Omilt oma päeva kohta", + "onboardingStarterTaskRecord": "Salvesta Omiga üks vestlus", + "onboardingStarterTaskCheck": "Märgi see ülesanne tehtuks", "paywallRecommended": "Recommended", "paywallNoPlans": "Plans aren't available right now. Try again in a minute.", "paywallOtherPlans": "See all plans", @@ -4014,21 +4057,6 @@ "paywallManageBilling": "Manage billing", "paywallLoadError": "Couldn't load plans. Check your connection and try again.", "paywallPlans": "Plans", - "firstCaptureTitle": "How will you capture?", - "firstCaptureDescription": "Choose how you want to capture your first conversation.", - "firstCaptureOmi": "I Have an Omi", - "firstCapturePhone": "This Phone", - "firstCaptureWatch": "Apple Watch", - "firstCaptureSkip": "Skip for Now", - "firstCapturePrompt": "Tell Omi what you're working on today, or what's on your mind. Talk for about 30 seconds.", - "firstCaptureKeepGoing": "Keep going… 30 seconds is ideal.", - "firstCaptureReady": "You can finish now, or keep talking.", - "firstCaptureStart": "Start Recording", - "firstCaptureFinish": "Finish Recording", - "firstCaptureListening": "Listening… your words will appear here.", - "firstCaptureProcessing": "Creating your first conversation…", - "firstCaptureResult": "Your first conversation", - "firstCaptureEmpty": "I didn't catch enough speech, try again with a few sentences", "watchCaptureTitle": "Record from your Apple Watch", "watchCaptureOpen": "Open Omi on your Apple Watch.", "watchCaptureRecord": "Tap Record. Allow microphone access if asked.", @@ -4040,17 +4068,6 @@ "pairingNotFoundOrDropped": "Couldn't reach your Omi. Make sure it's charged, close to your phone, and its light is blinking, then try again.", "pairingBondRecovery": "Forget Omi in your phone’s Bluetooth settings, then try pairing again.", "pairingHowTo": "How to pair", - "firstCaptureWatchInstructions": "Open Omi on your Apple Watch and tap Record. Allow microphone access if asked. Your capture will sync to Omi on this phone.", - "firstCaptureWatchInstall": "In the Watch app on your iPhone, install Omi on your Apple Watch. Then open Omi on your watch and tap Record.", - "firstCaptureWatchPair": "Pair your Apple Watch with this iPhone using the Watch app, then install and open Omi on your watch.", - "firstCaptureUnavailable": "Recording could not start. Check microphone access and your connection, then try again.", - "firstCaptureDelayed": "Your recording is taking longer to process. You can check your conversations on Home or try again.", - "firstCaptureEmptyTitle": "Let's try that again", - "firstCaptureShortTitle": "Finish This Recording?", - "firstCaptureShortWarning": "A few more sentences help Omi understand you. About 30 seconds is ideal.", - "firstCaptureCancelTitle": "Leave This Recording?", - "firstCaptureCancelWarning": "Going back stops this recording. Keep talking to give Omi a few more sentences.", - "firstCaptureSiriTip": "Tip: Enable Omi in Shortcuts → Siri.", "paywallOutlineHeadline": "Ära jäta enam ühtegi sõna vahele.", "paywallCancelAnytime": "Tühista igal ajal.", "connectAnAgent": "Ühenda agent", diff --git a/app/lib/l10n/app_fa.arb b/app/lib/l10n/app_fa.arb index 9fd61c622c5..568986ea3be 100644 --- a/app/lib/l10n/app_fa.arb +++ b/app/lib/l10n/app_fa.arb @@ -1586,7 +1586,7 @@ "@enableBluetooth": { "description": "Title for dialog prompting user to enable Bluetooth" }, - "bluetoothNeeded": "Omi نیاز به Bluetooth برای اتصال به دستگاه پوشیدنی دارد. لطفاً Bluetooth را فعال کنید و دوباره تلاش کنید.", + "bluetoothNeeded": "Omi از طریق بلوتوث به گوشی شما وصل می‌شود. برای ادامه آن را روشن کنید.", "@bluetoothNeeded": { "description": "Explanation text for why Bluetooth is required" }, @@ -5782,7 +5782,7 @@ "@consentDataMessage": { "description": "Consent message explaining how user data will be stored and used" }, - "tasksEmptyStateMessage": "برای ایجاد یک کار، گفتگو را شروع کنید.", + "tasksEmptyStateMessage": "کارهای گفتگوهای شما اینجا نمایش داده می‌شوند.", "@tasksEmptyStateMessage": { "description": "Empty state message shown when there are no tasks, with instruction to tap + button" }, @@ -11430,6 +11430,49 @@ "reviewProposalSuggested": "پیشنهادی", "reviewProposalConflict": "این مورد پس از پیشنهاد Dream تغییر کرده است، بنابراین چیزی اعمال نشد.", "itemLinkListTitle": "{count, plural, =0{هیچ موردی نیست} =1{۱ مورد} other{{count} مورد}}", + "onboardingPermissionTapToFix": "برای رفع ضربه بزنید", + "onboardingNoOmiYet": "هنوز Omi ندارید؟", + "onboardingOmiOrdered": "سفارش داده‌ام", + "onboardingOmiNotOrdered": "هنوز سفارش نداده‌ام", + "onboardingConnectOmi": "اتصال Omi", + "consentShortMessage": "Omi با هوش مصنوعی گفتگوهای شما را به یادداشت تبدیل می‌کند. هر زمان بخواهید می‌توانید آن‌ها را حذف کنید.", + "consentAiProviders": "پردازش با {providers}.", + "onboardingActIntro": "بیایید Omi را امتحان کنیم.", + "onboardingActMicTitle": "Omi به میکروفون شما نیاز دارد", + "onboardingActMicBody": "تا Omi وقتی با او حرف می‌زنید صدایتان را بشنود.", + "onboardingActMicAllow": "اجازهٔ میکروفون", + "onboardingActDictateTitle": "از روزتان به Omi بگویید", + "onboardingActDictateBody": "به روش خودتان بگویید. بقیه با Omi.", + "onboardingActDictateExample": "همین الان با سم ناهار خوردم، خرداد به لیسبون می‌رود! یادم بینداز تا جمعه ارائه را برای تیم بفرستم و سر راه خانه شیر بخرم.", + "onboardingActDictateHint": "وقتی تمام شد، روی تیک بزنید", + "onboardingActPhoneMic": "میکروفون گوشی", + "onboardingActWriting": "در حال خلاصه‌سازی…", + "onboardingActRemembers": "Omi به خاطر می‌سپارد تا شما مجبور نباشید", + "onboardingActNotesLabel": "روز شما، خلاصه‌شده", + "onboardingActTasksLabel": "Omi کارهای شما را پیدا کرد", + "onboardingActYourWords": "این چیزی است که گفتید", + "onboardingActCongratsTitle": "همه چیز آماده است، {name}", + "onboardingActRateTitle": "تا اینجا از Omi راضی هستید؟", + "onboardingActRateYes": "به Omi امتیاز دهید", + "onboardingWelcomeCardTitle": "به Omi خوش آمدید", + "onboardingWelcomeCardBody": "Omi را بپوشید و مثل همیشه صحبت کنید. هر گفتگو همین‌جا به یادداشت، کار و خاطره تبدیل می‌شود.", + "onboardingActDemoTitle": "ناهار با سم", + "onboardingActDemoSummary": "ناهار با سم که خرداد به لیسبون می‌رود. می‌خواهید تا جمعه ارائه را برای تیم بفرستید و سر راه خانه شیر بخرید.", + "onboardingActDemoTask1": "فرستادن ارائه برای تیم تا جمعه", + "onboardingActDemoTask2": "خریدن شیر سر راه خانه", + "onboardingActJustNow": "همین الان", + "onboardingActSavedToTasks": "در کارهای شما ذخیره شد", + "onboardingConnectTapToConnect": "برای اتصال روی Omi خود ضربه بزنید.", + "onboardingActMicFallbackBody": "Omi هنوز صدا نمی‌فرستد. فعلاً از گوشی خود استفاده کنید.", + "onboardingActNoAudioOmi": "صدای Omi نمی‌آید. مطمئن شوید روشن و نزدیک است.", + "onboardingActNoAudioPhone": "هنوز صدایتان نمی‌آید. کمی بلندتر صحبت کنید.", + "onboardingActDictateBodyOmi": "فقط صحبت کنید. Omi گوش می‌دهد.", + "voiceProfileCardDismissed": "هر زمان می‌توانید نمایه صدا را در تنظیمات راه‌اندازی کنید.", + "homeNewRecordingButton": "ضبط جدید", + "chatWelcomeMessage": "سلام، من Omi هستم. هر چیزی درباره صحبت‌هایت بپرس: اینکه کسی چه گفت، چه قولی دادی یا خلاصه‌ای از روزت. می‌توانم کار هم اضافه کنم و چیزها را برایت به خاطر بسپارم.", + "onboardingStarterTaskAsk": "از Omi درباره روزت بپرس", + "onboardingStarterTaskRecord": "یک گفتگو را با Omi ضبط کن", + "onboardingStarterTaskCheck": "این کار را تیک بزن تا تمام شود", "paywallRecommended": "Recommended", "paywallNoPlans": "Plans aren't available right now. Try again in a minute.", "paywallOtherPlans": "See all plans", @@ -11440,21 +11483,6 @@ "paywallManageBilling": "Manage billing", "paywallLoadError": "Couldn't load plans. Check your connection and try again.", "paywallPlans": "Plans", - "firstCaptureTitle": "How will you capture?", - "firstCaptureDescription": "Choose how you want to capture your first conversation.", - "firstCaptureOmi": "I Have an Omi", - "firstCapturePhone": "This Phone", - "firstCaptureWatch": "Apple Watch", - "firstCaptureSkip": "Skip for Now", - "firstCapturePrompt": "Tell Omi what you're working on today, or what's on your mind. Talk for about 30 seconds.", - "firstCaptureKeepGoing": "Keep going… 30 seconds is ideal.", - "firstCaptureReady": "You can finish now, or keep talking.", - "firstCaptureStart": "Start Recording", - "firstCaptureFinish": "Finish Recording", - "firstCaptureListening": "Listening… your words will appear here.", - "firstCaptureProcessing": "Creating your first conversation…", - "firstCaptureResult": "Your first conversation", - "firstCaptureEmpty": "I didn't catch enough speech, try again with a few sentences", "watchCaptureTitle": "Record from your Apple Watch", "watchCaptureOpen": "Open Omi on your Apple Watch.", "watchCaptureRecord": "Tap Record. Allow microphone access if asked.", @@ -11466,17 +11494,6 @@ "pairingNotFoundOrDropped": "Couldn't reach your Omi. Make sure it's charged, close to your phone, and its light is blinking, then try again.", "pairingBondRecovery": "Forget Omi in your phone’s Bluetooth settings, then try pairing again.", "pairingHowTo": "How to pair", - "firstCaptureWatchInstructions": "Open Omi on your Apple Watch and tap Record. Allow microphone access if asked. Your capture will sync to Omi on this phone.", - "firstCaptureWatchInstall": "In the Watch app on your iPhone, install Omi on your Apple Watch. Then open Omi on your watch and tap Record.", - "firstCaptureWatchPair": "Pair your Apple Watch with this iPhone using the Watch app, then install and open Omi on your watch.", - "firstCaptureUnavailable": "Recording could not start. Check microphone access and your connection, then try again.", - "firstCaptureDelayed": "Your recording is taking longer to process. You can check your conversations on Home or try again.", - "firstCaptureEmptyTitle": "Let's try that again", - "firstCaptureShortTitle": "Finish This Recording?", - "firstCaptureShortWarning": "A few more sentences help Omi understand you. About 30 seconds is ideal.", - "firstCaptureCancelTitle": "Leave This Recording?", - "firstCaptureCancelWarning": "Going back stops this recording. Keep talking to give Omi a few more sentences.", - "firstCaptureSiriTip": "Tip: Enable Omi in Shortcuts → Siri.", "paywallOutlineHeadline": "دیگر هیچ کلمه‌ای را از دست ندهید.", "paywallCancelAnytime": "هر زمان که بخواهید لغو کنید.", "connectAnAgent": "اتصال یک ایجنت", diff --git a/app/lib/l10n/app_fi.arb b/app/lib/l10n/app_fi.arb index 208273ddafd..874708ebdb8 100644 --- a/app/lib/l10n/app_fi.arb +++ b/app/lib/l10n/app_fi.arb @@ -520,7 +520,7 @@ "dontShowAgain": "Älä näytä uudelleen", "iUnderstand": "Ymmärrän", "enableBluetooth": "Ota Bluetooth käyttöön", - "bluetoothNeeded": "Omi tarvitsee Bluetoothin yhdistääkseen puettavaan laitteeseesi. Ota Bluetooth käyttöön ja yritä uudelleen.", + "bluetoothNeeded": "Omi yhdistyy puhelimeesi Bluetoothilla. Laita se päälle jatkaaksesi.", "contactSupport": "Ota yhteyttä tukeen?", "connectLater": "Yhdistä myöhemmin", "grantPermissions": "Myönnä luvat", @@ -1501,7 +1501,7 @@ "durationSeconds": "Kesto: {seconds} sekuntia", "additionalSpeechSampleRemoved": "Lisäpuhenäyte poistettu", "consentDataMessage": "Jatkamalla keskustelusi, tallenteet ja henkilötietosi tallennetaan turvallisesti palvelimillemme. Äänitallenteitasi ja transkriptioitasi käsittelevät kolmannen osapuolen tekoälypalvelut (mukaan lukien Deepgram transkriptiota ja OpenAI analyysiä varten) tarjotaksemme sinulle tekoälypohjaisia oivalluksia ja mahdollistaaksemme kaikki sovelluksen ominaisuudet.", - "tasksEmptyStateMessage": "Aloita keskustelu luodaksesi tehtävän.", + "tasksEmptyStateMessage": "Keskusteluistasi poimitut tehtävät näkyvät täällä.", "clearChatAction": "Tyhjennä keskustelu", "enableApps": "Ota sovellukset käyttöön", "omiAppName": "Omi", @@ -4004,6 +4004,49 @@ "reviewProposalSuggested": "Ehdotettu", "reviewProposalConflict": "Tämä on muuttunut Dreamin ehdotuksen jälkeen, joten mitään ei otettu käyttöön.", "itemLinkListTitle": "{count, plural, =0{Ei kohteita} =1{1 kohde} other{{count} kohdetta}}", + "onboardingPermissionTapToFix": "Korjaa napauttamalla", + "onboardingNoOmiYet": "Eikö Omi ole vielä saapunut?", + "onboardingOmiOrdered": "Olen tilannut sen", + "onboardingOmiNotOrdered": "En ole vielä tilannut", + "onboardingConnectOmi": "Yhdistä Omi", + "consentShortMessage": "Omi tekee keskusteluistasi muistiinpanoja tekoälyn avulla. Voit poistaa ne milloin tahansa.", + "consentAiProviders": "Käsittely: {providers}.", + "onboardingActIntro": "Kokeillaan Omia.", + "onboardingActMicTitle": "Omi tarvitsee mikrofonisi", + "onboardingActMicBody": "Jotta Omi kuulee sinut, kun puhut sille.", + "onboardingActMicAllow": "Salli mikrofoni", + "onboardingActDictateTitle": "Kerro Omille päivästäsi", + "onboardingActDictateBody": "Sano omin sanoin. Omi hoitaa loput.", + "onboardingActDictateExample": "Söin juuri lounasta Samin kanssa, hän muuttaa kesäkuussa Lissaboniin! Muistuta minua lähettämään esitys tiimille perjantaihin mennessä ja hakemaan maitoa kotimatkalla.", + "onboardingActDictateHint": "Napauta valintamerkkiä, kun olet valmis", + "onboardingActPhoneMic": "Puhelimen mikrofoni", + "onboardingActWriting": "Tiivistetään…", + "onboardingActRemembers": "Omi muistaa, ettei sinun tarvitse", + "onboardingActNotesLabel": "Päiväsi tiivistettynä", + "onboardingActTasksLabel": "Omi poimi tehtäväsi", + "onboardingActYourWords": "Tämän sanoit", + "onboardingActCongratsTitle": "Kaikki valmista, {name}", + "onboardingActRateTitle": "Pidätkö Omista tähän mennessä?", + "onboardingActRateYes": "Arvioi Omi", + "onboardingWelcomeCardTitle": "Tervetuloa Omiin", + "onboardingWelcomeCardBody": "Pidä Omia ja puhu kuten tavallisesti. Jokaisesta keskustelusta tulee tänne muistiinpanoja, tehtäviä ja muistoja.", + "onboardingActDemoTitle": "Lounas Samin kanssa", + "onboardingActDemoSummary": "Lounas Samin kanssa, joka muuttaa kesäkuussa Lissaboniin. Haluat lähettää esityksen tiimille perjantaihin mennessä ja ostaa maitoa kotimatkalla.", + "onboardingActDemoTask1": "Lähetä esitys tiimille perjantaihin mennessä", + "onboardingActDemoTask2": "Hae maitoa kotimatkalla", + "onboardingActJustNow": "Juuri nyt", + "onboardingActSavedToTasks": "Tallennettu tehtäviisi", + "onboardingConnectTapToConnect": "Yhdistä napauttamalla Omiasi.", + "onboardingActMicFallbackBody": "Omi ei vielä lähetä ääntä. Käytä toistaiseksi puhelinta.", + "onboardingActNoAudioOmi": "Omia ei kuulu. Varmista, että se on päällä ja lähellä.", + "onboardingActNoAudioPhone": "Emme kuule sinua vielä. Kokeile puhua vähän kovempaa.", + "onboardingActDictateBodyOmi": "Puhu vain. Omisi kuuntelee.", + "voiceProfileCardDismissed": "Voit määrittää ääniprofiilin milloin tahansa asetuksissa.", + "homeNewRecordingButton": "Uusi tallennus", + "chatWelcomeMessage": "Hei, olen Omi. Kysy minulta mitä tahansa siitä, mistä olet puhunut: mitä joku sanoi, mitä lupasit tai yhteenveto päivästäsi. Voin myös lisätä tehtäviä ja muistaa asioita puolestasi.", + "onboardingStarterTaskAsk": "Kysy Omilta päivästäsi", + "onboardingStarterTaskRecord": "Tallenna keskustelu Omilla", + "onboardingStarterTaskCheck": "Merkitse tämä tehtävä tehdyksi", "paywallRecommended": "Recommended", "paywallNoPlans": "Plans aren't available right now. Try again in a minute.", "paywallOtherPlans": "See all plans", @@ -4014,21 +4057,6 @@ "paywallManageBilling": "Manage billing", "paywallLoadError": "Couldn't load plans. Check your connection and try again.", "paywallPlans": "Plans", - "firstCaptureTitle": "How will you capture?", - "firstCaptureDescription": "Choose how you want to capture your first conversation.", - "firstCaptureOmi": "I Have an Omi", - "firstCapturePhone": "This Phone", - "firstCaptureWatch": "Apple Watch", - "firstCaptureSkip": "Skip for Now", - "firstCapturePrompt": "Tell Omi what you're working on today, or what's on your mind. Talk for about 30 seconds.", - "firstCaptureKeepGoing": "Keep going… 30 seconds is ideal.", - "firstCaptureReady": "You can finish now, or keep talking.", - "firstCaptureStart": "Start Recording", - "firstCaptureFinish": "Finish Recording", - "firstCaptureListening": "Listening… your words will appear here.", - "firstCaptureProcessing": "Creating your first conversation…", - "firstCaptureResult": "Your first conversation", - "firstCaptureEmpty": "I didn't catch enough speech, try again with a few sentences", "watchCaptureTitle": "Record from your Apple Watch", "watchCaptureOpen": "Open Omi on your Apple Watch.", "watchCaptureRecord": "Tap Record. Allow microphone access if asked.", @@ -4040,17 +4068,6 @@ "pairingNotFoundOrDropped": "Couldn't reach your Omi. Make sure it's charged, close to your phone, and its light is blinking, then try again.", "pairingBondRecovery": "Forget Omi in your phone’s Bluetooth settings, then try pairing again.", "pairingHowTo": "How to pair", - "firstCaptureWatchInstructions": "Open Omi on your Apple Watch and tap Record. Allow microphone access if asked. Your capture will sync to Omi on this phone.", - "firstCaptureWatchInstall": "In the Watch app on your iPhone, install Omi on your Apple Watch. Then open Omi on your watch and tap Record.", - "firstCaptureWatchPair": "Pair your Apple Watch with this iPhone using the Watch app, then install and open Omi on your watch.", - "firstCaptureUnavailable": "Recording could not start. Check microphone access and your connection, then try again.", - "firstCaptureDelayed": "Your recording is taking longer to process. You can check your conversations on Home or try again.", - "firstCaptureEmptyTitle": "Let's try that again", - "firstCaptureShortTitle": "Finish This Recording?", - "firstCaptureShortWarning": "A few more sentences help Omi understand you. About 30 seconds is ideal.", - "firstCaptureCancelTitle": "Leave This Recording?", - "firstCaptureCancelWarning": "Going back stops this recording. Keep talking to give Omi a few more sentences.", - "firstCaptureSiriTip": "Tip: Enable Omi in Shortcuts → Siri.", "paywallOutlineHeadline": "Älä enää koskaan missaa sanaakaan.", "paywallCancelAnytime": "Peruuta milloin tahansa.", "connectAnAgent": "Yhdistä agentti", diff --git a/app/lib/l10n/app_fr.arb b/app/lib/l10n/app_fr.arb index 673cacd6e14..282ac47243d 100644 --- a/app/lib/l10n/app_fr.arb +++ b/app/lib/l10n/app_fr.arb @@ -520,7 +520,7 @@ "dontShowAgain": "Ne plus afficher", "iUnderstand": "Je comprends", "enableBluetooth": "Activer le Bluetooth", - "bluetoothNeeded": "Omi a besoin du Bluetooth pour se connecter à votre wearable. Veuillez activer le Bluetooth et réessayer.", + "bluetoothNeeded": "Omi se connecte à votre téléphone en Bluetooth. Activez-le pour continuer.", "contactSupport": "Contacter le support ?", "connectLater": "Se connecter plus tard", "grantPermissions": "Accorder les autorisations", @@ -1501,7 +1501,7 @@ "durationSeconds": "Durée : {seconds} secondes", "additionalSpeechSampleRemoved": "Échantillon vocal supplémentaire supprimé", "consentDataMessage": "En continuant, vos conversations, enregistrements et informations personnelles seront stockés en toute sécurité sur nos serveurs. Vos enregistrements audio et transcriptions sont traités par des services d'IA tiers (notamment Deepgram pour la transcription et OpenAI pour l'analyse) afin de vous fournir des informations alimentées par l'IA et d'activer toutes les fonctionnalités de l'application.", - "tasksEmptyStateMessage": "Démarrez une conversation pour créer une tâche.", + "tasksEmptyStateMessage": "Les tâches issues de vos conversations apparaîtront ici.", "clearChatAction": "Effacer le chat", "enableApps": "Activer les applications", "omiAppName": "Omi", @@ -4039,6 +4039,49 @@ "reviewProposalSuggested": "Suggéré", "reviewProposalConflict": "Ceci a changé depuis la suggestion de Dream, donc rien n'a été appliqué.", "itemLinkListTitle": "{count, plural, =0{Aucun élément} =1{1 élément} other{{count} éléments}}", + "onboardingPermissionTapToFix": "Touchez pour corriger", + "onboardingNoOmiYet": "Vous n'avez pas encore votre Omi ?", + "onboardingOmiOrdered": "Je l'ai commandé", + "onboardingOmiNotOrdered": "Pas encore commandé", + "onboardingConnectOmi": "Connecter Omi", + "consentShortMessage": "Omi utilise l'IA pour transformer vos conversations en notes. Vous pouvez les supprimer à tout moment.", + "consentAiProviders": "Traité avec {providers}.", + "onboardingActIntro": "Essayons Omi.", + "onboardingActMicTitle": "Omi a besoin de votre micro", + "onboardingActMicBody": "Pour qu'Omi vous entende quand vous lui parlez.", + "onboardingActMicAllow": "Autoriser le micro", + "onboardingActDictateTitle": "Racontez votre journée à Omi", + "onboardingActDictateBody": "Dites-le à votre façon. Omi s'occupe du reste.", + "onboardingActDictateExample": "Je viens de déjeuner avec Sam, elle déménage à Lisbonne en juin ! Rappelle-moi d'envoyer la présentation à l'équipe d'ici vendredi et de prendre du lait en rentrant.", + "onboardingActDictateHint": "Touchez la coche quand vous avez fini", + "onboardingActPhoneMic": "Micro du téléphone", + "onboardingActWriting": "Résumé en cours…", + "onboardingActRemembers": "Omi s'en souvient pour vous", + "onboardingActNotesLabel": "Votre journée, résumée", + "onboardingActTasksLabel": "Omi a repéré vos tâches", + "onboardingActYourWords": "Voici ce que vous avez dit", + "onboardingActCongratsTitle": "Tout est prêt, {name}", + "onboardingActRateTitle": "Omi vous plaît pour l'instant ?", + "onboardingActRateYes": "Noter Omi", + "onboardingWelcomeCardTitle": "Bienvenue dans Omi", + "onboardingWelcomeCardBody": "Portez Omi et parlez normalement. Chaque conversation devient ici des notes, des tâches et des souvenirs.", + "onboardingActDemoTitle": "Déjeuner avec Sam", + "onboardingActDemoSummary": "Déjeuner avec Sam, qui part vivre à Lisbonne en juin. Vous voulez envoyer la présentation à l'équipe d'ici vendredi et prendre du lait en rentrant.", + "onboardingActDemoTask1": "Envoyer la présentation à l'équipe d'ici vendredi", + "onboardingActDemoTask2": "Prendre du lait en rentrant", + "onboardingActJustNow": "À l'instant", + "onboardingActSavedToTasks": "Enregistré dans vos tâches", + "onboardingConnectTapToConnect": "Touchez votre Omi pour le connecter.", + "onboardingActMicFallbackBody": "Votre Omi n'envoie pas encore de son. Utilisez votre téléphone pour l'instant.", + "onboardingActNoAudioOmi": "On n'entend pas votre Omi. Vérifiez qu'il est allumé et proche.", + "onboardingActNoAudioPhone": "On ne vous entend pas encore. Parlez un peu plus fort.", + "onboardingActDictateBodyOmi": "Parlez simplement. Votre Omi vous écoute.", + "voiceProfileCardDismissed": "Vous pouvez configurer votre profil vocal à tout moment dans les Réglages.", + "homeNewRecordingButton": "Nouvel enregistrement", + "chatWelcomeMessage": "Salut, je suis Omi. Demandez-moi tout ce dont vous avez parlé : ce que quelqu'un a dit, ce que vous avez promis ou un résumé de votre journée. Je peux aussi ajouter des tâches et retenir des choses pour vous.", + "onboardingStarterTaskAsk": "Demandez à Omi comment s'est passée votre journée", + "onboardingStarterTaskRecord": "Enregistrez une conversation avec Omi", + "onboardingStarterTaskCheck": "Cochez cette tâche pour la terminer", "paywallRecommended": "Recommended", "paywallNoPlans": "Plans aren't available right now. Try again in a minute.", "paywallOtherPlans": "See all plans", @@ -4049,21 +4092,6 @@ "paywallManageBilling": "Manage billing", "paywallLoadError": "Couldn't load plans. Check your connection and try again.", "paywallPlans": "Plans", - "firstCaptureTitle": "How will you capture?", - "firstCaptureDescription": "Choose how you want to capture your first conversation.", - "firstCaptureOmi": "I Have an Omi", - "firstCapturePhone": "This Phone", - "firstCaptureWatch": "Apple Watch", - "firstCaptureSkip": "Skip for Now", - "firstCapturePrompt": "Tell Omi what you're working on today, or what's on your mind. Talk for about 30 seconds.", - "firstCaptureKeepGoing": "Keep going… 30 seconds is ideal.", - "firstCaptureReady": "You can finish now, or keep talking.", - "firstCaptureStart": "Start Recording", - "firstCaptureFinish": "Finish Recording", - "firstCaptureListening": "Listening… your words will appear here.", - "firstCaptureProcessing": "Creating your first conversation…", - "firstCaptureResult": "Your first conversation", - "firstCaptureEmpty": "I didn't catch enough speech, try again with a few sentences", "watchCaptureTitle": "Record from your Apple Watch", "watchCaptureOpen": "Open Omi on your Apple Watch.", "watchCaptureRecord": "Tap Record. Allow microphone access if asked.", @@ -4075,17 +4103,6 @@ "pairingNotFoundOrDropped": "Couldn't reach your Omi. Make sure it's charged, close to your phone, and its light is blinking, then try again.", "pairingBondRecovery": "Forget Omi in your phone’s Bluetooth settings, then try pairing again.", "pairingHowTo": "How to pair", - "firstCaptureWatchInstructions": "Open Omi on your Apple Watch and tap Record. Allow microphone access if asked. Your capture will sync to Omi on this phone.", - "firstCaptureWatchInstall": "In the Watch app on your iPhone, install Omi on your Apple Watch. Then open Omi on your watch and tap Record.", - "firstCaptureWatchPair": "Pair your Apple Watch with this iPhone using the Watch app, then install and open Omi on your watch.", - "firstCaptureUnavailable": "Recording could not start. Check microphone access and your connection, then try again.", - "firstCaptureDelayed": "Your recording is taking longer to process. You can check your conversations on Home or try again.", - "firstCaptureEmptyTitle": "Let's try that again", - "firstCaptureShortTitle": "Finish This Recording?", - "firstCaptureShortWarning": "A few more sentences help Omi understand you. About 30 seconds is ideal.", - "firstCaptureCancelTitle": "Leave This Recording?", - "firstCaptureCancelWarning": "Going back stops this recording. Keep talking to give Omi a few more sentences.", - "firstCaptureSiriTip": "Tip: Enable Omi in Shortcuts → Siri.", "paywallOutlineHeadline": "Ne manquez plus jamais un mot.", "paywallCancelAnytime": "Annulez à tout moment.", "connectAnAgent": "Connecter un agent", diff --git a/app/lib/l10n/app_he.arb b/app/lib/l10n/app_he.arb index 532a4b48c73..93a6ff8a5f6 100644 --- a/app/lib/l10n/app_he.arb +++ b/app/lib/l10n/app_he.arb @@ -1586,7 +1586,7 @@ "@enableBluetooth": { "description": "Title for dialog prompting user to enable Bluetooth" }, - "bluetoothNeeded": "Omi זקוק ל-Bluetooth כדי להתחבר לציוד הלביש שלך. בבקשה הפעל Bluetooth ונסה שנית.", + "bluetoothNeeded": "Omi מתחבר לטלפון דרך Bluetooth. הפעילו אותו כדי להמשיך.", "@bluetoothNeeded": { "description": "Explanation text for why Bluetooth is required" }, @@ -5782,7 +5782,7 @@ "@consentDataMessage": { "description": "Consent message explaining how user data will be stored and used" }, - "tasksEmptyStateMessage": "התחילו שיחה כדי ליצור משימה.", + "tasksEmptyStateMessage": "משימות מהשיחות שלך יופיעו כאן.", "@tasksEmptyStateMessage": { "description": "Empty state message shown when there are no tasks, with instruction to tap + button" }, @@ -11430,6 +11430,49 @@ "reviewProposalSuggested": "מוצע", "reviewProposalConflict": "זה השתנה מאז ש־Dream הציע אותו, ולכן שום דבר לא הוחל.", "itemLinkListTitle": "{count, plural, =0{אין פריטים} =1{פריט אחד} other{{count} פריטים}}", + "onboardingPermissionTapToFix": "הקישו לתיקון", + "onboardingNoOmiYet": "עדיין אין לכם Omi?", + "onboardingOmiOrdered": "הזמנתי", + "onboardingOmiNotOrdered": "עוד לא הזמנתי", + "onboardingConnectOmi": "חיבור Omi", + "consentShortMessage": "Omi משתמש בבינה מלאכותית כדי להפוך את השיחות שלך להערות. אפשר למחוק אותן בכל עת.", + "consentAiProviders": "מעובד באמצעות {providers}.", + "onboardingActIntro": "בואו ננסה את Omi.", + "onboardingActMicTitle": "Omi צריך את המיקרופון", + "onboardingActMicBody": "כדי ש-Omi ישמע אתכם כשאתם מדברים אליו.", + "onboardingActMicAllow": "אפשר מיקרופון", + "onboardingActDictateTitle": "ספרו ל-Omi על היום שלכם", + "onboardingActDictateBody": "תגידו את זה בדרך שלכם. Omi ידאג לשאר.", + "onboardingActDictateExample": "הרגע אכלתי צהריים עם סאם, היא עוברת לליסבון ביוני! תזכיר לי לשלוח את המצגת לצוות עד שישי, ולקנות חלב בדרך הביתה.", + "onboardingActDictateHint": "הקישו על ה-V כשתסיימו", + "onboardingActPhoneMic": "מיקרופון הטלפון", + "onboardingActWriting": "מסכם…", + "onboardingActRemembers": "Omi זוכר, כדי שאתם לא תצטרכו", + "onboardingActNotesLabel": "היום שלך, בתמצית", + "onboardingActTasksLabel": "Omi תפס את המשימות שלך", + "onboardingActYourWords": "זה מה שאמרת", + "onboardingActCongratsTitle": "הכול מוכן, {name}", + "onboardingActRateTitle": "נהנים מ-Omi עד עכשיו?", + "onboardingActRateYes": "דרגו את Omi", + "onboardingWelcomeCardTitle": "ברוכים הבאים ל-Omi", + "onboardingWelcomeCardBody": "ענדו את Omi ודברו כרגיל. כל שיחה הופכת כאן להערות, משימות וזיכרונות.", + "onboardingActDemoTitle": "צהריים עם סאם", + "onboardingActDemoSummary": "ארוחת צהריים עם סם, שעוברת ללישבון ביוני. אתה רוצה לשלוח את המצגת לצוות עד יום שישי ולקנות חלב בדרך הביתה.", + "onboardingActDemoTask1": "לשלוח את המצגת לצוות עד שישי", + "onboardingActDemoTask2": "לקנות חלב בדרך הביתה", + "onboardingActJustNow": "עכשיו", + "onboardingActSavedToTasks": "נשמר במשימות שלך", + "onboardingConnectTapToConnect": "הקש על ה-Omi שלך כדי להתחבר.", + "onboardingActMicFallbackBody": "ה-Omi עדיין לא שולח שמע. בינתיים השתמש בטלפון.", + "onboardingActNoAudioOmi": "לא שומעים את ה-Omi. ודא שהוא דולק וקרוב.", + "onboardingActNoAudioPhone": "עדיין לא שומעים אותך. נסה לדבר קצת יותר חזק.", + "onboardingActDictateBodyOmi": "פשוט דבר. ה-Omi שלך מקשיב.", + "voiceProfileCardDismissed": "אפשר להגדיר פרופיל קולי בכל עת בהגדרות.", + "homeNewRecordingButton": "הקלטה חדשה", + "chatWelcomeMessage": "היי, אני Omi. שאל אותי כל דבר על מה שדיברת: מה מישהו אמר, מה הבטחת או סיכום של היום שלך. אני יכול גם להוסיף משימות ולזכור דברים בשבילך.", + "onboardingStarterTaskAsk": "שאל את Omi על היום שלך", + "onboardingStarterTaskRecord": "הקלט שיחה עם Omi", + "onboardingStarterTaskCheck": "סמן משימה זו כדי לסיים אותה", "paywallRecommended": "Recommended", "paywallNoPlans": "Plans aren't available right now. Try again in a minute.", "paywallOtherPlans": "See all plans", @@ -11440,21 +11483,6 @@ "paywallManageBilling": "Manage billing", "paywallLoadError": "Couldn't load plans. Check your connection and try again.", "paywallPlans": "Plans", - "firstCaptureTitle": "How will you capture?", - "firstCaptureDescription": "Choose how you want to capture your first conversation.", - "firstCaptureOmi": "I Have an Omi", - "firstCapturePhone": "This Phone", - "firstCaptureWatch": "Apple Watch", - "firstCaptureSkip": "Skip for Now", - "firstCapturePrompt": "Tell Omi what you're working on today, or what's on your mind. Talk for about 30 seconds.", - "firstCaptureKeepGoing": "Keep going… 30 seconds is ideal.", - "firstCaptureReady": "You can finish now, or keep talking.", - "firstCaptureStart": "Start Recording", - "firstCaptureFinish": "Finish Recording", - "firstCaptureListening": "Listening… your words will appear here.", - "firstCaptureProcessing": "Creating your first conversation…", - "firstCaptureResult": "Your first conversation", - "firstCaptureEmpty": "I didn't catch enough speech, try again with a few sentences", "watchCaptureTitle": "Record from your Apple Watch", "watchCaptureOpen": "Open Omi on your Apple Watch.", "watchCaptureRecord": "Tap Record. Allow microphone access if asked.", @@ -11466,17 +11494,6 @@ "pairingNotFoundOrDropped": "Couldn't reach your Omi. Make sure it's charged, close to your phone, and its light is blinking, then try again.", "pairingBondRecovery": "Forget Omi in your phone’s Bluetooth settings, then try pairing again.", "pairingHowTo": "How to pair", - "firstCaptureWatchInstructions": "Open Omi on your Apple Watch and tap Record. Allow microphone access if asked. Your capture will sync to Omi on this phone.", - "firstCaptureWatchInstall": "In the Watch app on your iPhone, install Omi on your Apple Watch. Then open Omi on your watch and tap Record.", - "firstCaptureWatchPair": "Pair your Apple Watch with this iPhone using the Watch app, then install and open Omi on your watch.", - "firstCaptureUnavailable": "Recording could not start. Check microphone access and your connection, then try again.", - "firstCaptureDelayed": "Your recording is taking longer to process. You can check your conversations on Home or try again.", - "firstCaptureEmptyTitle": "Let's try that again", - "firstCaptureShortTitle": "Finish This Recording?", - "firstCaptureShortWarning": "A few more sentences help Omi understand you. About 30 seconds is ideal.", - "firstCaptureCancelTitle": "Leave This Recording?", - "firstCaptureCancelWarning": "Going back stops this recording. Keep talking to give Omi a few more sentences.", - "firstCaptureSiriTip": "Tip: Enable Omi in Shortcuts → Siri.", "paywallOutlineHeadline": "אל תפספסו אף מילה יותר.", "paywallCancelAnytime": "ניתן לבטל בכל עת.", "connectAnAgent": "חיבור סוכן", diff --git a/app/lib/l10n/app_hi.arb b/app/lib/l10n/app_hi.arb index bc9b4f75a3b..e13cced1993 100644 --- a/app/lib/l10n/app_hi.arb +++ b/app/lib/l10n/app_hi.arb @@ -515,7 +515,7 @@ "dontShowAgain": "दोबारा न दिखाएं", "iUnderstand": "मैं समझता हूं", "enableBluetooth": "ब्लूटूथ सक्षम करें", - "bluetoothNeeded": "आपके वियरेबल से कनेक्ट करने के लिए Omi को ब्लूटूथ की आवश्यकता है।", + "bluetoothNeeded": "Omi आपके फ़ोन से Bluetooth के ज़रिए जुड़ता है। जारी रखने के लिए इसे चालू करें।", "contactSupport": "संपर्क करें?", "connectLater": "बाद में कनेक्ट करें", "grantPermissions": "अनुमतियां दें", @@ -1501,7 +1501,7 @@ "durationSeconds": "अवधि: {seconds} सेकंड", "additionalSpeechSampleRemoved": "अतिरिक्त भाषण नमूना हटाया गया", "consentDataMessage": "जारी रखने पर, आपकी बातचीत, रिकॉर्डिंग और व्यक्तिगत जानकारी हमारे सर्वर पर सुरक्षित रूप से संग्रहीत की जाएगी। आपकी ऑडियो रिकॉर्डिंग और ट्रांसक्रिप्ट तीसरे पक्ष की AI सेवाओं (ट्रांसक्रिप्शन के लिए Deepgram और विश्लेषण के लिए OpenAI सहित) द्वारा संसाधित किए जाते हैं ताकि आपको AI-संचालित अंतर्दृष्टि प्रदान की जा सके और सभी ऐप सुविधाएँ सक्षम की जा सकें।", - "tasksEmptyStateMessage": "टास्क बनाने के लिए बातचीत शुरू करें।", + "tasksEmptyStateMessage": "आपकी बातचीत से निकले काम यहाँ दिखेंगे।", "clearChatAction": "चैट साफ़ करें", "enableApps": "ऐप्स सक्षम करें", "omiAppName": "Omi", @@ -4004,6 +4004,49 @@ "reviewProposalSuggested": "सुझाया गया", "reviewProposalConflict": "Dream के सुझाव के बाद यह बदल गया, इसलिए कुछ भी लागू नहीं किया गया।", "itemLinkListTitle": "{count, plural, =0{कोई आइटम नहीं} =1{1 आइटम} other{{count} आइटम}}", + "onboardingPermissionTapToFix": "ठीक करने के लिए टैप करें", + "onboardingNoOmiYet": "अभी तक आपका Omi नहीं आया?", + "onboardingOmiOrdered": "मैंने ऑर्डर कर दिया है", + "onboardingOmiNotOrdered": "अभी ऑर्डर नहीं किया", + "onboardingConnectOmi": "Omi कनेक्ट करें", + "consentShortMessage": "Omi AI की मदद से आपकी बातचीत को नोट्स में बदलता है। आप उन्हें कभी भी हटा सकते हैं।", + "consentAiProviders": "{providers} के साथ प्रोसेस किया जाता है।", + "onboardingActIntro": "चलिए Omi आज़माएं।", + "onboardingActMicTitle": "Omi को आपका माइक्रोफ़ोन चाहिए", + "onboardingActMicBody": "ताकि आप बात करें तो Omi सुन सके।", + "onboardingActMicAllow": "माइक्रोफ़ोन की अनुमति दें", + "onboardingActDictateTitle": "Omi को अपने दिन के बारे में बताएँ", + "onboardingActDictateBody": "अपने अंदाज़ में कहिए। बाकी Omi संभाल लेगा।", + "onboardingActDictateExample": "अभी सैम के साथ लंच किया, वो जून में लिस्बन जा रही है! याद दिलाना कि शुक्रवार तक टीम को डेक भेजना है, और घर लौटते हुए दूध लेना है।", + "onboardingActDictateHint": "हो जाने पर टिक पर टैप करें", + "onboardingActPhoneMic": "फ़ोन माइक्रोफ़ोन", + "onboardingActWriting": "सारांश बन रहा है…", + "onboardingActRemembers": "Omi याद रखता है, ताकि आपको न रखना पड़े", + "onboardingActNotesLabel": "आपका दिन, संक्षेप में", + "onboardingActTasksLabel": "Omi ने आपके काम पकड़ लिए", + "onboardingActYourWords": "आपने यह कहा", + "onboardingActCongratsTitle": "सब तैयार है, {name}", + "onboardingActRateTitle": "अब तक Omi कैसा लगा?", + "onboardingActRateYes": "Omi को रेट करें", + "onboardingWelcomeCardTitle": "Omi में आपका स्वागत है", + "onboardingWelcomeCardBody": "Omi पहनें और हमेशा की तरह बात करें। हर बातचीत यहीं नोट्स, कामों और यादों में बदल जाएगी।", + "onboardingActDemoTitle": "सैम के साथ लंच", + "onboardingActDemoSummary": "सैम के साथ लंच, जो जून में लिस्बन जा रही है। आपको शुक्रवार तक टीम को डेक भेजना है और घर लौटते हुए दूध लेना है।", + "onboardingActDemoTask1": "शुक्रवार तक टीम को डेक भेजना", + "onboardingActDemoTask2": "घर लौटते हुए दूध लेना", + "onboardingActJustNow": "अभी-अभी", + "onboardingActSavedToTasks": "आपके कामों में सहेजा गया", + "onboardingConnectTapToConnect": "कनेक्ट करने के लिए अपने Omi पर टैप करें।", + "onboardingActMicFallbackBody": "आपका Omi अभी ऑडियो नहीं भेज रहा। फ़िलहाल अपना फ़ोन इस्तेमाल करें।", + "onboardingActNoAudioOmi": "आपका Omi सुनाई नहीं दे रहा। देखें कि वह चालू है और पास है।", + "onboardingActNoAudioPhone": "अभी आपकी आवाज़ नहीं आ रही। थोड़ा ज़ोर से बोलकर देखें।", + "onboardingActDictateBodyOmi": "बस बोलिए। आपका Omi सुन रहा है।", + "voiceProfileCardDismissed": "आप सेटिंग्स में कभी भी वॉइस प्रोफ़ाइल सेट कर सकते हैं।", + "homeNewRecordingButton": "नई रिकॉर्डिंग", + "chatWelcomeMessage": "नमस्ते, मैं Omi हूँ। आपने जिस बारे में बात की है, उसके बारे में कुछ भी पूछें: किसी ने क्या कहा, आपने क्या वादा किया, या आपके दिन का सारांश। मैं काम जोड़ सकता हूँ और आपके लिए चीज़ें याद भी रख सकता हूँ।", + "onboardingStarterTaskAsk": "Omi से अपने दिन के बारे में पूछें", + "onboardingStarterTaskRecord": "Omi से एक बातचीत रिकॉर्ड करें", + "onboardingStarterTaskCheck": "इस काम को पूरा करने के लिए टिक करें", "paywallRecommended": "Recommended", "paywallNoPlans": "Plans aren't available right now. Try again in a minute.", "paywallOtherPlans": "See all plans", @@ -4014,21 +4057,6 @@ "paywallManageBilling": "Manage billing", "paywallLoadError": "Couldn't load plans. Check your connection and try again.", "paywallPlans": "Plans", - "firstCaptureTitle": "How will you capture?", - "firstCaptureDescription": "Choose how you want to capture your first conversation.", - "firstCaptureOmi": "I Have an Omi", - "firstCapturePhone": "This Phone", - "firstCaptureWatch": "Apple Watch", - "firstCaptureSkip": "Skip for Now", - "firstCapturePrompt": "Tell Omi what you're working on today, or what's on your mind. Talk for about 30 seconds.", - "firstCaptureKeepGoing": "Keep going… 30 seconds is ideal.", - "firstCaptureReady": "You can finish now, or keep talking.", - "firstCaptureStart": "Start Recording", - "firstCaptureFinish": "Finish Recording", - "firstCaptureListening": "Listening… your words will appear here.", - "firstCaptureProcessing": "Creating your first conversation…", - "firstCaptureResult": "Your first conversation", - "firstCaptureEmpty": "I didn't catch enough speech, try again with a few sentences", "watchCaptureTitle": "Record from your Apple Watch", "watchCaptureOpen": "Open Omi on your Apple Watch.", "watchCaptureRecord": "Tap Record. Allow microphone access if asked.", @@ -4040,17 +4068,6 @@ "pairingNotFoundOrDropped": "Couldn't reach your Omi. Make sure it's charged, close to your phone, and its light is blinking, then try again.", "pairingBondRecovery": "Forget Omi in your phone’s Bluetooth settings, then try pairing again.", "pairingHowTo": "How to pair", - "firstCaptureWatchInstructions": "Open Omi on your Apple Watch and tap Record. Allow microphone access if asked. Your capture will sync to Omi on this phone.", - "firstCaptureWatchInstall": "In the Watch app on your iPhone, install Omi on your Apple Watch. Then open Omi on your watch and tap Record.", - "firstCaptureWatchPair": "Pair your Apple Watch with this iPhone using the Watch app, then install and open Omi on your watch.", - "firstCaptureUnavailable": "Recording could not start. Check microphone access and your connection, then try again.", - "firstCaptureDelayed": "Your recording is taking longer to process. You can check your conversations on Home or try again.", - "firstCaptureEmptyTitle": "Let's try that again", - "firstCaptureShortTitle": "Finish This Recording?", - "firstCaptureShortWarning": "A few more sentences help Omi understand you. About 30 seconds is ideal.", - "firstCaptureCancelTitle": "Leave This Recording?", - "firstCaptureCancelWarning": "Going back stops this recording. Keep talking to give Omi a few more sentences.", - "firstCaptureSiriTip": "Tip: Enable Omi in Shortcuts → Siri.", "paywallOutlineHeadline": "अब एक भी शब्द न छूटे।", "paywallCancelAnytime": "कभी भी रद्द करें।", "connectAnAgent": "एजेंट कनेक्ट करें", diff --git a/app/lib/l10n/app_hr.arb b/app/lib/l10n/app_hr.arb index de5f1bb6548..a892bb0c416 100644 --- a/app/lib/l10n/app_hr.arb +++ b/app/lib/l10n/app_hr.arb @@ -1586,7 +1586,7 @@ "@enableBluetooth": { "description": "Title for dialog prompting user to enable Bluetooth" }, - "bluetoothNeeded": "Omi trebaju Bluetooth da bi se spojio na tvoj prijenosni uređaj. Molimo omogući Bluetooth i pokušaj ponovno.", + "bluetoothNeeded": "Omi se s telefonom povezuje putem Bluetootha. Uključite ga za nastavak.", "@bluetoothNeeded": { "description": "Explanation text for why Bluetooth is required" }, @@ -5782,7 +5782,7 @@ "@consentDataMessage": { "description": "Consent message explaining how user data will be stored and used" }, - "tasksEmptyStateMessage": "Započnite razgovor da biste stvorili zadatak.", + "tasksEmptyStateMessage": "Zadaci iz vaših razgovora pojavit će se ovdje.", "@tasksEmptyStateMessage": { "description": "Empty state message shown when there are no tasks, with instruction to tap + button" }, @@ -11430,6 +11430,49 @@ "reviewProposalSuggested": "Predloženo", "reviewProposalConflict": "Ovo se promijenilo otkako je Dream to predložio, pa ništa nije primijenjeno.", "itemLinkListTitle": "{count, plural, =0{Nema stavki} =1{1 stavka} other{{count} stavki}}", + "onboardingPermissionTapToFix": "Dodirnite za popravak", + "onboardingNoOmiYet": "Još nemate Omi?", + "onboardingOmiOrdered": "Naručio/la sam ga", + "onboardingOmiNotOrdered": "Još nisam naručio/la", + "onboardingConnectOmi": "Povežite Omi", + "consentShortMessage": "Omi koristi AI kako bi vaše razgovore pretvorio u bilješke. Možete ih izbrisati bilo kada.", + "consentAiProviders": "Obrađuje se pomoću {providers}.", + "onboardingActIntro": "Isprobajmo Omi.", + "onboardingActMicTitle": "Omi treba vaš mikrofon", + "onboardingActMicBody": "Da vas Omi čuje kad mu se obratite.", + "onboardingActMicAllow": "Dopusti mikrofon", + "onboardingActDictateTitle": "Ispričajte Omiju svoj dan", + "onboardingActDictateBody": "Recite to na svoj način. Omi se brine za ostalo.", + "onboardingActDictateExample": "Upravo sam ručao sa Sam, u lipnju se seli u Lisabon! Podsjeti me da pošaljem prezentaciju timu do petka i da kupim mlijeko na putu kući.", + "onboardingActDictateHint": "Dodirnite kvačicu kad završite", + "onboardingActPhoneMic": "Mikrofon telefona", + "onboardingActWriting": "Sažimamo…", + "onboardingActRemembers": "Omi pamti da vi ne morate", + "onboardingActNotesLabel": "Vaš dan, ukratko", + "onboardingActTasksLabel": "Omi je uhvatio vaše zadatke", + "onboardingActYourWords": "Evo što ste rekli", + "onboardingActCongratsTitle": "Sve je spremno, {name}", + "onboardingActRateTitle": "Sviđa li vam se Omi zasad?", + "onboardingActRateYes": "Ocijenite Omi", + "onboardingWelcomeCardTitle": "Dobro došli u Omi", + "onboardingWelcomeCardBody": "Nosite Omi i pričajte kao i inače. Svaki razgovor ovdje postaje bilješke, zadaci i sjećanja.", + "onboardingActDemoTitle": "Ručak sa Sam", + "onboardingActDemoSummary": "Ručak sa Sam, koja se u lipnju seli u Lisabon. Želite poslati prezentaciju timu do petka i kupiti mlijeko na putu kući.", + "onboardingActDemoTask1": "Poslati prezentaciju timu do petka", + "onboardingActDemoTask2": "Kupiti mlijeko na putu kući", + "onboardingActJustNow": "Upravo sada", + "onboardingActSavedToTasks": "Spremljeno u vaše zadatke", + "onboardingConnectTapToConnect": "Dodirnite svoj Omi da biste se povezali.", + "onboardingActMicFallbackBody": "Omi još ne šalje zvuk. Za sada koristite telefon.", + "onboardingActNoAudioOmi": "Ne čujemo Omi. Provjerite je li uključen i blizu.", + "onboardingActNoAudioPhone": "Još vas ne čujemo. Pokušajte govoriti malo glasnije.", + "onboardingActDictateBodyOmi": "Samo pričajte. Vaš Omi sluša.", + "voiceProfileCardDismissed": "Glasovni profil možete postaviti bilo kada u Postavkama.", + "homeNewRecordingButton": "Novo snimanje", + "chatWelcomeMessage": "Bok, ja sam Omi. Pitaj me bilo što o čemu si razgovarao: što je netko rekao, što si obećao ili sažetak tvog dana. Mogu i dodavati zadatke i pamtiti stvari za tebe.", + "onboardingStarterTaskAsk": "Pitaj Omi o svom danu", + "onboardingStarterTaskRecord": "Snimi razgovor uz Omi", + "onboardingStarterTaskCheck": "Označi ovaj zadatak da ga završiš", "paywallRecommended": "Recommended", "paywallNoPlans": "Plans aren't available right now. Try again in a minute.", "paywallOtherPlans": "See all plans", @@ -11440,21 +11483,6 @@ "paywallManageBilling": "Manage billing", "paywallLoadError": "Couldn't load plans. Check your connection and try again.", "paywallPlans": "Plans", - "firstCaptureTitle": "How will you capture?", - "firstCaptureDescription": "Choose how you want to capture your first conversation.", - "firstCaptureOmi": "I Have an Omi", - "firstCapturePhone": "This Phone", - "firstCaptureWatch": "Apple Watch", - "firstCaptureSkip": "Skip for Now", - "firstCapturePrompt": "Tell Omi what you're working on today, or what's on your mind. Talk for about 30 seconds.", - "firstCaptureKeepGoing": "Keep going… 30 seconds is ideal.", - "firstCaptureReady": "You can finish now, or keep talking.", - "firstCaptureStart": "Start Recording", - "firstCaptureFinish": "Finish Recording", - "firstCaptureListening": "Listening… your words will appear here.", - "firstCaptureProcessing": "Creating your first conversation…", - "firstCaptureResult": "Your first conversation", - "firstCaptureEmpty": "I didn't catch enough speech, try again with a few sentences", "watchCaptureTitle": "Record from your Apple Watch", "watchCaptureOpen": "Open Omi on your Apple Watch.", "watchCaptureRecord": "Tap Record. Allow microphone access if asked.", @@ -11466,17 +11494,6 @@ "pairingNotFoundOrDropped": "Couldn't reach your Omi. Make sure it's charged, close to your phone, and its light is blinking, then try again.", "pairingBondRecovery": "Forget Omi in your phone’s Bluetooth settings, then try pairing again.", "pairingHowTo": "How to pair", - "firstCaptureWatchInstructions": "Open Omi on your Apple Watch and tap Record. Allow microphone access if asked. Your capture will sync to Omi on this phone.", - "firstCaptureWatchInstall": "In the Watch app on your iPhone, install Omi on your Apple Watch. Then open Omi on your watch and tap Record.", - "firstCaptureWatchPair": "Pair your Apple Watch with this iPhone using the Watch app, then install and open Omi on your watch.", - "firstCaptureUnavailable": "Recording could not start. Check microphone access and your connection, then try again.", - "firstCaptureDelayed": "Your recording is taking longer to process. You can check your conversations on Home or try again.", - "firstCaptureEmptyTitle": "Let's try that again", - "firstCaptureShortTitle": "Finish This Recording?", - "firstCaptureShortWarning": "A few more sentences help Omi understand you. About 30 seconds is ideal.", - "firstCaptureCancelTitle": "Leave This Recording?", - "firstCaptureCancelWarning": "Going back stops this recording. Keep talking to give Omi a few more sentences.", - "firstCaptureSiriTip": "Tip: Enable Omi in Shortcuts → Siri.", "paywallOutlineHeadline": "Nikad više ne propustite ni riječ.", "paywallCancelAnytime": "Otkažite bilo kada.", "connectAnAgent": "Povežite agenta", diff --git a/app/lib/l10n/app_hu.arb b/app/lib/l10n/app_hu.arb index 473d07e10d4..438f240da00 100644 --- a/app/lib/l10n/app_hu.arb +++ b/app/lib/l10n/app_hu.arb @@ -520,7 +520,7 @@ "dontShowAgain": "Ne jelenjen meg újra", "iUnderstand": "Megértettem", "enableBluetooth": "Bluetooth engedélyezése", - "bluetoothNeeded": "Az Omi-nak Bluetoothra van szüksége a viselhető eszközhöz való csatlakozáshoz. Kérlek, engedélyezd a Bluetooth-t, és próbáld újra.", + "bluetoothNeeded": "Az Omi Bluetooth-on csatlakozik a telefonodhoz. Kapcsold be a folytatáshoz.", "contactSupport": "Ügyfélszolgálat elérése?", "connectLater": "Csatlakozás később", "grantPermissions": "Engedélyek megadása", @@ -1501,7 +1501,7 @@ "durationSeconds": "Időtartam: {seconds} másodperc", "additionalSpeechSampleRemoved": "További hangminta eltávolítva", "consentDataMessage": "A folytatással beszélgetéseit, felvételeit és személyes adatait biztonságosan tároljuk szervereinken. Hangfelvételeit és átiratait harmadik féltől származó AI szolgáltatások dolgozzák fel (beleértve a Deepgramot az átíráshoz és az OpenAI-t az elemzéshez), hogy AI-alapú betekintéseket nyújtsunk Önnek és az alkalmazás összes funkcióját biztosítsuk.", - "tasksEmptyStateMessage": "Feladat létrehozásához kezdj beszélgetést.", + "tasksEmptyStateMessage": "A beszélgetéseidből származó teendők itt jelennek meg.", "clearChatAction": "Chat törlése", "enableApps": "Alkalmazások engedélyezése", "omiAppName": "Omi", @@ -4100,6 +4100,49 @@ "reviewProposalSuggested": "Javasolt", "reviewProposalConflict": "Ez megváltozott, mióta a Dream javasolta, így semmi sem lett alkalmazva.", "itemLinkListTitle": "{count, plural, =0{Nincsenek elemek} =1{1 elem} other{{count} elem}}", + "onboardingPermissionTapToFix": "Koppints a javításhoz", + "onboardingNoOmiYet": "Még nincs meg az Omid?", + "onboardingOmiOrdered": "Már megrendeltem", + "onboardingOmiNotOrdered": "Még nem rendeltem", + "onboardingConnectOmi": "Omi csatlakoztatása", + "consentShortMessage": "Az Omi mesterséges intelligenciával jegyzetekké alakítja a beszélgetéseidet. Bármikor törölheted őket.", + "consentAiProviders": "Feldolgozás: {providers}.", + "onboardingActIntro": "Próbáljuk ki az Omit.", + "onboardingActMicTitle": "Az Ominak kell a mikrofonod", + "onboardingActMicBody": "Hogy az Omi halljon, amikor beszélsz hozzá.", + "onboardingActMicAllow": "Mikrofon engedélyezése", + "onboardingActDictateTitle": "Meséld el az Ominak a napod", + "onboardingActDictateBody": "Mondd a magad módján. A többit az Omi intézi.", + "onboardingActDictateExample": "Most ebédeltem Sammel, júniusban Lisszabonba költözik! Emlékeztess, hogy péntekig küldjem el a prezentációt a csapatnak, és hazafelé vegyek tejet.", + "onboardingActDictateHint": "Ha végeztél, koppints a pipára", + "onboardingActPhoneMic": "Telefon mikrofonja", + "onboardingActWriting": "Összefoglalás…", + "onboardingActRemembers": "Az Omi emlékszik helyetted", + "onboardingActNotesLabel": "A napod röviden", + "onboardingActTasksLabel": "Az Omi kiszúrta a teendőidet", + "onboardingActYourWords": "Ezt mondtad", + "onboardingActCongratsTitle": "Minden kész, {name}", + "onboardingActRateTitle": "Tetszik eddig az Omi?", + "onboardingActRateYes": "Értékeld az Omit", + "onboardingWelcomeCardTitle": "Üdvözöl az Omi", + "onboardingWelcomeCardBody": "Viseld az Omit, és beszélj, ahogy szoktál. Minden beszélgetésből itt jegyzet, teendő és emlék lesz.", + "onboardingActDemoTitle": "Ebéd Sammel", + "onboardingActDemoSummary": "Ebéd Sammel, aki júniusban Lisszabonba költözik. Péntekig el akarod küldeni a prezentációt a csapatnak, és hazafelé tejet venni.", + "onboardingActDemoTask1": "Péntekig elküldeni a prezentációt a csapatnak", + "onboardingActDemoTask2": "Hazafelé tejet venni", + "onboardingActJustNow": "Épp most", + "onboardingActSavedToTasks": "Mentve a teendőid közé", + "onboardingConnectTapToConnect": "Koppints az Omidra a csatlakozáshoz.", + "onboardingActMicFallbackBody": "Az Omi még nem küld hangot. Egyelőre használd a telefonodat.", + "onboardingActNoAudioOmi": "Nem halljuk az Omit. Ellenőrizd, hogy be van-e kapcsolva és közel van-e.", + "onboardingActNoAudioPhone": "Még nem hallunk. Próbálj kicsit hangosabban beszélni.", + "onboardingActDictateBodyOmi": "Csak beszélj. Az Omid figyel.", + "voiceProfileCardDismissed": "A hangprofilt bármikor beállíthatod a Beállításokban.", + "homeNewRecordingButton": "Új felvétel", + "chatWelcomeMessage": "Szia, Omi vagyok. Kérdezz bármit arról, amiről beszéltél: mit mondott valaki, mit ígértél, vagy kérj összefoglalót a napodról. Feladatokat is fel tudok venni, és dolgokat is megjegyzek helyetted.", + "onboardingStarterTaskAsk": "Kérdezd meg Omit a napodról", + "onboardingStarterTaskRecord": "Rögzíts egy beszélgetést az Omival", + "onboardingStarterTaskCheck": "Pipáld ki ezt a feladatot", "paywallRecommended": "Recommended", "paywallNoPlans": "Plans aren't available right now. Try again in a minute.", "paywallOtherPlans": "See all plans", @@ -4110,21 +4153,6 @@ "paywallManageBilling": "Manage billing", "paywallLoadError": "Couldn't load plans. Check your connection and try again.", "paywallPlans": "Plans", - "firstCaptureTitle": "How will you capture?", - "firstCaptureDescription": "Choose how you want to capture your first conversation.", - "firstCaptureOmi": "I Have an Omi", - "firstCapturePhone": "This Phone", - "firstCaptureWatch": "Apple Watch", - "firstCaptureSkip": "Skip for Now", - "firstCapturePrompt": "Tell Omi what you're working on today, or what's on your mind. Talk for about 30 seconds.", - "firstCaptureKeepGoing": "Keep going… 30 seconds is ideal.", - "firstCaptureReady": "You can finish now, or keep talking.", - "firstCaptureStart": "Start Recording", - "firstCaptureFinish": "Finish Recording", - "firstCaptureListening": "Listening… your words will appear here.", - "firstCaptureProcessing": "Creating your first conversation…", - "firstCaptureResult": "Your first conversation", - "firstCaptureEmpty": "I didn't catch enough speech, try again with a few sentences", "watchCaptureTitle": "Record from your Apple Watch", "watchCaptureOpen": "Open Omi on your Apple Watch.", "watchCaptureRecord": "Tap Record. Allow microphone access if asked.", @@ -4136,17 +4164,6 @@ "pairingNotFoundOrDropped": "Couldn't reach your Omi. Make sure it's charged, close to your phone, and its light is blinking, then try again.", "pairingBondRecovery": "Forget Omi in your phone’s Bluetooth settings, then try pairing again.", "pairingHowTo": "How to pair", - "firstCaptureWatchInstructions": "Open Omi on your Apple Watch and tap Record. Allow microphone access if asked. Your capture will sync to Omi on this phone.", - "firstCaptureWatchInstall": "In the Watch app on your iPhone, install Omi on your Apple Watch. Then open Omi on your watch and tap Record.", - "firstCaptureWatchPair": "Pair your Apple Watch with this iPhone using the Watch app, then install and open Omi on your watch.", - "firstCaptureUnavailable": "Recording could not start. Check microphone access and your connection, then try again.", - "firstCaptureDelayed": "Your recording is taking longer to process. You can check your conversations on Home or try again.", - "firstCaptureEmptyTitle": "Let's try that again", - "firstCaptureShortTitle": "Finish This Recording?", - "firstCaptureShortWarning": "A few more sentences help Omi understand you. About 30 seconds is ideal.", - "firstCaptureCancelTitle": "Leave This Recording?", - "firstCaptureCancelWarning": "Going back stops this recording. Keep talking to give Omi a few more sentences.", - "firstCaptureSiriTip": "Tip: Enable Omi in Shortcuts → Siri.", "paywallOutlineHeadline": "Soha többé ne maradj le egyetlen szóról sem.", "paywallCancelAnytime": "Bármikor lemondható.", "connectAnAgent": "Ügynök csatlakoztatása", diff --git a/app/lib/l10n/app_id.arb b/app/lib/l10n/app_id.arb index d1ad3a00958..a00ae2c2b56 100644 --- a/app/lib/l10n/app_id.arb +++ b/app/lib/l10n/app_id.arb @@ -520,7 +520,7 @@ "dontShowAgain": "Jangan tampilkan lagi", "iUnderstand": "Saya Mengerti", "enableBluetooth": "Aktifkan Bluetooth", - "bluetoothNeeded": "Omi membutuhkan Bluetooth untuk terhubung ke perangkat yang dapat dipakai Anda. Silakan aktifkan Bluetooth dan coba lagi.", + "bluetoothNeeded": "Omi terhubung ke ponsel Anda lewat Bluetooth. Nyalakan untuk melanjutkan.", "contactSupport": "Hubungi Dukungan?", "connectLater": "Hubungkan Nanti", "grantPermissions": "Berikan izin", @@ -1501,7 +1501,7 @@ "durationSeconds": "Durasi: {seconds} detik", "additionalSpeechSampleRemoved": "Sampel suara tambahan dihapus", "consentDataMessage": "Dengan melanjutkan, percakapan, rekaman, dan informasi pribadi Anda akan disimpan dengan aman di server kami. Rekaman audio dan transkrip Anda diproses oleh layanan AI pihak ketiga (termasuk Deepgram untuk transkripsi dan OpenAI untuk analisis) untuk memberikan Anda wawasan berbasis AI dan mengaktifkan semua fitur aplikasi.", - "tasksEmptyStateMessage": "Mulai percakapan untuk membuat tugas.", + "tasksEmptyStateMessage": "Tugas dari percakapan Anda akan muncul di sini.", "clearChatAction": "Hapus obrolan", "enableApps": "Aktifkan aplikasi", "omiAppName": "Omi", @@ -4039,6 +4039,49 @@ "reviewProposalSuggested": "Disarankan", "reviewProposalConflict": "Ini berubah sejak Dream menyarankannya, jadi tidak ada yang diterapkan.", "itemLinkListTitle": "{count, plural, =0{Tidak Ada Item} =1{1 Item} other{{count} Item}}", + "onboardingPermissionTapToFix": "Ketuk untuk memperbaiki", + "onboardingNoOmiYet": "Belum punya Omi?", + "onboardingOmiOrdered": "Saya sudah memesannya", + "onboardingOmiNotOrdered": "Belum memesan", + "onboardingConnectOmi": "Hubungkan Omi", + "consentShortMessage": "Omi menggunakan AI untuk mengubah percakapanmu menjadi catatan. Kamu bisa menghapusnya kapan saja.", + "consentAiProviders": "Diproses dengan {providers}.", + "onboardingActIntro": "Ayo coba Omi.", + "onboardingActMicTitle": "Omi butuh mikrofon Anda", + "onboardingActMicBody": "Agar Omi bisa mendengar saat Anda bicara.", + "onboardingActMicAllow": "Izinkan Mikrofon", + "onboardingActDictateTitle": "Ceritakan hari Anda ke Omi", + "onboardingActDictateBody": "Katakan dengan gaya Anda. Omi urus sisanya.", + "onboardingActDictateExample": "Baru makan siang sama Sam, dia pindah ke Lisbon bulan Juni! Ingatkan aku kirim deck ke tim sebelum Jumat, dan beli susu di jalan pulang.", + "onboardingActDictateHint": "Ketuk centang jika sudah selesai", + "onboardingActPhoneMic": "Mikrofon ponsel", + "onboardingActWriting": "Merangkum…", + "onboardingActRemembers": "Omi mengingat, jadi kamu tidak perlu", + "onboardingActNotesLabel": "Harimu, dirangkum", + "onboardingActTasksLabel": "Omi menangkap tugasmu", + "onboardingActYourWords": "Ini yang Anda katakan", + "onboardingActCongratsTitle": "Semua siap, {name}", + "onboardingActRateTitle": "Suka Omi sejauh ini?", + "onboardingActRateYes": "Beri Nilai Omi", + "onboardingWelcomeCardTitle": "Selamat datang di Omi", + "onboardingWelcomeCardBody": "Pakai Omi dan bicara seperti biasa. Setiap percakapan menjadi catatan, tugas, dan kenangan di sini.", + "onboardingActDemoTitle": "Makan siang dengan Sam", + "onboardingActDemoSummary": "Makan siang dengan Sam, yang pindah ke Lisbon bulan Juni. Kamu mau kirim deck ke tim sebelum Jumat dan beli susu di jalan pulang.", + "onboardingActDemoTask1": "Kirim deck ke tim sebelum Jumat", + "onboardingActDemoTask2": "Beli susu di jalan pulang", + "onboardingActJustNow": "Baru saja", + "onboardingActSavedToTasks": "Disimpan ke tugasmu", + "onboardingConnectTapToConnect": "Ketuk Omi kamu untuk menghubungkan.", + "onboardingActMicFallbackBody": "Omi kamu belum mengirim audio. Pakai ponselmu dulu.", + "onboardingActNoAudioOmi": "Omi kamu tidak terdengar. Pastikan menyala dan dekat.", + "onboardingActNoAudioPhone": "Suaramu belum terdengar. Coba bicara sedikit lebih keras.", + "onboardingActDictateBodyOmi": "Bicara saja. Omi kamu sedang mendengarkan.", + "voiceProfileCardDismissed": "Anda dapat mengatur Profil Suara kapan saja di Pengaturan.", + "homeNewRecordingButton": "Rekaman Baru", + "chatWelcomeMessage": "Hai, aku Omi. Tanyakan apa saja tentang hal yang kamu bicarakan: apa yang dikatakan seseorang, apa yang kamu janjikan, atau ringkasan harimu. Aku juga bisa menambahkan tugas dan mengingat hal-hal untukmu.", + "onboardingStarterTaskAsk": "Tanyakan harimu kepada Omi", + "onboardingStarterTaskRecord": "Rekam percakapan dengan Omi", + "onboardingStarterTaskCheck": "Centang tugas ini untuk menyelesaikannya", "paywallRecommended": "Recommended", "paywallNoPlans": "Plans aren't available right now. Try again in a minute.", "paywallOtherPlans": "See all plans", @@ -4049,21 +4092,6 @@ "paywallManageBilling": "Manage billing", "paywallLoadError": "Couldn't load plans. Check your connection and try again.", "paywallPlans": "Plans", - "firstCaptureTitle": "How will you capture?", - "firstCaptureDescription": "Choose how you want to capture your first conversation.", - "firstCaptureOmi": "I Have an Omi", - "firstCapturePhone": "This Phone", - "firstCaptureWatch": "Apple Watch", - "firstCaptureSkip": "Skip for Now", - "firstCapturePrompt": "Tell Omi what you're working on today, or what's on your mind. Talk for about 30 seconds.", - "firstCaptureKeepGoing": "Keep going… 30 seconds is ideal.", - "firstCaptureReady": "You can finish now, or keep talking.", - "firstCaptureStart": "Start Recording", - "firstCaptureFinish": "Finish Recording", - "firstCaptureListening": "Listening… your words will appear here.", - "firstCaptureProcessing": "Creating your first conversation…", - "firstCaptureResult": "Your first conversation", - "firstCaptureEmpty": "I didn't catch enough speech, try again with a few sentences", "watchCaptureTitle": "Record from your Apple Watch", "watchCaptureOpen": "Open Omi on your Apple Watch.", "watchCaptureRecord": "Tap Record. Allow microphone access if asked.", @@ -4075,17 +4103,6 @@ "pairingNotFoundOrDropped": "Couldn't reach your Omi. Make sure it's charged, close to your phone, and its light is blinking, then try again.", "pairingBondRecovery": "Forget Omi in your phone’s Bluetooth settings, then try pairing again.", "pairingHowTo": "How to pair", - "firstCaptureWatchInstructions": "Open Omi on your Apple Watch and tap Record. Allow microphone access if asked. Your capture will sync to Omi on this phone.", - "firstCaptureWatchInstall": "In the Watch app on your iPhone, install Omi on your Apple Watch. Then open Omi on your watch and tap Record.", - "firstCaptureWatchPair": "Pair your Apple Watch with this iPhone using the Watch app, then install and open Omi on your watch.", - "firstCaptureUnavailable": "Recording could not start. Check microphone access and your connection, then try again.", - "firstCaptureDelayed": "Your recording is taking longer to process. You can check your conversations on Home or try again.", - "firstCaptureEmptyTitle": "Let's try that again", - "firstCaptureShortTitle": "Finish This Recording?", - "firstCaptureShortWarning": "A few more sentences help Omi understand you. About 30 seconds is ideal.", - "firstCaptureCancelTitle": "Leave This Recording?", - "firstCaptureCancelWarning": "Going back stops this recording. Keep talking to give Omi a few more sentences.", - "firstCaptureSiriTip": "Tip: Enable Omi in Shortcuts → Siri.", "paywallOutlineHeadline": "Jangan pernah melewatkan satu kata pun lagi.", "paywallCancelAnytime": "Batalkan kapan saja.", "connectAnAgent": "Hubungkan Agen", diff --git a/app/lib/l10n/app_it.arb b/app/lib/l10n/app_it.arb index a5062e4ad1e..5b00ad346e2 100644 --- a/app/lib/l10n/app_it.arb +++ b/app/lib/l10n/app_it.arb @@ -520,7 +520,7 @@ "dontShowAgain": "Non mostrare più", "iUnderstand": "Ho capito", "enableBluetooth": "Abilita Bluetooth", - "bluetoothNeeded": "Omi ha bisogno del Bluetooth per connettersi al tuo dispositivo indossabile. Abilita il Bluetooth e riprova.", + "bluetoothNeeded": "Omi si collega al telefono tramite Bluetooth. Attivalo per continuare.", "contactSupport": "Contatta Supporto?", "connectLater": "Connetti Più Tardi", "grantPermissions": "Concedi autorizzazioni", @@ -1501,7 +1501,7 @@ "durationSeconds": "Durata: {seconds} secondi", "additionalSpeechSampleRemoved": "Campione vocale aggiuntivo rimosso", "consentDataMessage": "Continuando, le tue conversazioni, registrazioni e informazioni personali saranno archiviate in modo sicuro sui nostri server. Le tue registrazioni audio e trascrizioni vengono elaborate da servizi AI di terze parti (inclusi Deepgram per la trascrizione e OpenAI per l'analisi) per fornirti approfondimenti basati sull'AI e abilitare tutte le funzionalità dell'app.", - "tasksEmptyStateMessage": "Avvia una conversazione per creare un'attività.", + "tasksEmptyStateMessage": "Le attività dalle tue conversazioni appariranno qui.", "clearChatAction": "Cancella chat", "enableApps": "Abilita app", "omiAppName": "Omi", @@ -4004,6 +4004,49 @@ "reviewProposalSuggested": "Suggerito", "reviewProposalConflict": "È cambiato da quando Dream lo ha suggerito, quindi non è stato applicato nulla.", "itemLinkListTitle": "{count, plural, =0{Nessun elemento} =1{1 elemento} other{{count} elementi}}", + "onboardingPermissionTapToFix": "Tocca per risolvere", + "onboardingNoOmiYet": "Non hai ancora il tuo Omi?", + "onboardingOmiOrdered": "L'ho ordinato", + "onboardingOmiNotOrdered": "Non ancora ordinato", + "onboardingConnectOmi": "Connetti Omi", + "consentShortMessage": "Omi usa l'IA per trasformare le tue conversazioni in note. Puoi eliminarle quando vuoi.", + "consentAiProviders": "Elaborato con {providers}.", + "onboardingActIntro": "Proviamo Omi.", + "onboardingActMicTitle": "Omi ha bisogno del microfono", + "onboardingActMicBody": "Così Omi ti sente quando gli parli.", + "onboardingActMicAllow": "Consenti microfono", + "onboardingActDictateTitle": "Racconta a Omi la tua giornata", + "onboardingActDictateBody": "Dillo a modo tuo. Al resto pensa Omi.", + "onboardingActDictateExample": "Ho appena pranzato con Sam, a giugno si trasferisce a Lisbona! Ricordami di mandare la presentazione al team entro venerdì e di prendere il latte tornando a casa.", + "onboardingActDictateHint": "Tocca la spunta quando hai finito", + "onboardingActPhoneMic": "Microfono del telefono", + "onboardingActWriting": "Riassumendo…", + "onboardingActRemembers": "Omi ricorda al posto tuo", + "onboardingActNotesLabel": "La tua giornata, in sintesi", + "onboardingActTasksLabel": "Omi ha colto le tue cose da fare", + "onboardingActYourWords": "Ecco cosa hai detto", + "onboardingActCongratsTitle": "Tutto pronto, {name}", + "onboardingActRateTitle": "Ti piace Omi finora?", + "onboardingActRateYes": "Valuta Omi", + "onboardingWelcomeCardTitle": "Benvenuto in Omi", + "onboardingWelcomeCardBody": "Indossa Omi e parla come sempre. Ogni conversazione diventa qui appunti, attività e ricordi.", + "onboardingActDemoTitle": "Pranzo con Sam", + "onboardingActDemoSummary": "Pranzo con Sam, che a giugno si trasferisce a Lisbona. Vuoi mandare la presentazione al team entro venerdì e prendere il latte tornando a casa.", + "onboardingActDemoTask1": "Mandare la presentazione al team entro venerdì", + "onboardingActDemoTask2": "Prendere il latte tornando a casa", + "onboardingActJustNow": "Proprio ora", + "onboardingActSavedToTasks": "Salvato nelle tue attività", + "onboardingConnectTapToConnect": "Tocca il tuo Omi per collegarlo.", + "onboardingActMicFallbackBody": "Il tuo Omi non invia ancora audio. Per ora usa il telefono.", + "onboardingActNoAudioOmi": "Non sentiamo il tuo Omi. Controlla che sia acceso e vicino.", + "onboardingActNoAudioPhone": "Non ti sentiamo ancora. Prova a parlare un po' più forte.", + "onboardingActDictateBodyOmi": "Parla e basta. Il tuo Omi ti ascolta.", + "voiceProfileCardDismissed": "Puoi configurare il profilo vocale in qualsiasi momento nelle Impostazioni.", + "homeNewRecordingButton": "Nuova registrazione", + "chatWelcomeMessage": "Ciao, sono Omi. Chiedimi qualsiasi cosa di cui hai parlato: cosa ha detto qualcuno, cosa hai promesso o un riepilogo della tua giornata. Posso anche aggiungere attività e ricordare cose per te.", + "onboardingStarterTaskAsk": "Chiedi a Omi della tua giornata", + "onboardingStarterTaskRecord": "Registra una conversazione con Omi", + "onboardingStarterTaskCheck": "Spunta questa attività per completarla", "paywallRecommended": "Recommended", "paywallNoPlans": "Plans aren't available right now. Try again in a minute.", "paywallOtherPlans": "See all plans", @@ -4014,21 +4057,6 @@ "paywallManageBilling": "Manage billing", "paywallLoadError": "Couldn't load plans. Check your connection and try again.", "paywallPlans": "Plans", - "firstCaptureTitle": "How will you capture?", - "firstCaptureDescription": "Choose how you want to capture your first conversation.", - "firstCaptureOmi": "I Have an Omi", - "firstCapturePhone": "This Phone", - "firstCaptureWatch": "Apple Watch", - "firstCaptureSkip": "Skip for Now", - "firstCapturePrompt": "Tell Omi what you're working on today, or what's on your mind. Talk for about 30 seconds.", - "firstCaptureKeepGoing": "Keep going… 30 seconds is ideal.", - "firstCaptureReady": "You can finish now, or keep talking.", - "firstCaptureStart": "Start Recording", - "firstCaptureFinish": "Finish Recording", - "firstCaptureListening": "Listening… your words will appear here.", - "firstCaptureProcessing": "Creating your first conversation…", - "firstCaptureResult": "Your first conversation", - "firstCaptureEmpty": "I didn't catch enough speech, try again with a few sentences", "watchCaptureTitle": "Record from your Apple Watch", "watchCaptureOpen": "Open Omi on your Apple Watch.", "watchCaptureRecord": "Tap Record. Allow microphone access if asked.", @@ -4040,17 +4068,6 @@ "pairingNotFoundOrDropped": "Couldn't reach your Omi. Make sure it's charged, close to your phone, and its light is blinking, then try again.", "pairingBondRecovery": "Forget Omi in your phone’s Bluetooth settings, then try pairing again.", "pairingHowTo": "How to pair", - "firstCaptureWatchInstructions": "Open Omi on your Apple Watch and tap Record. Allow microphone access if asked. Your capture will sync to Omi on this phone.", - "firstCaptureWatchInstall": "In the Watch app on your iPhone, install Omi on your Apple Watch. Then open Omi on your watch and tap Record.", - "firstCaptureWatchPair": "Pair your Apple Watch with this iPhone using the Watch app, then install and open Omi on your watch.", - "firstCaptureUnavailable": "Recording could not start. Check microphone access and your connection, then try again.", - "firstCaptureDelayed": "Your recording is taking longer to process. You can check your conversations on Home or try again.", - "firstCaptureEmptyTitle": "Let's try that again", - "firstCaptureShortTitle": "Finish This Recording?", - "firstCaptureShortWarning": "A few more sentences help Omi understand you. About 30 seconds is ideal.", - "firstCaptureCancelTitle": "Leave This Recording?", - "firstCaptureCancelWarning": "Going back stops this recording. Keep talking to give Omi a few more sentences.", - "firstCaptureSiriTip": "Tip: Enable Omi in Shortcuts → Siri.", "paywallOutlineHeadline": "Non perdere mai più una parola.", "paywallCancelAnytime": "Disdici quando vuoi.", "connectAnAgent": "Collega un agente", diff --git a/app/lib/l10n/app_ja.arb b/app/lib/l10n/app_ja.arb index 02e380dec7e..a3c148d4284 100644 --- a/app/lib/l10n/app_ja.arb +++ b/app/lib/l10n/app_ja.arb @@ -520,7 +520,7 @@ "dontShowAgain": "今後表示しない", "iUnderstand": "理解しました", "enableBluetooth": "Bluetoothを有効にする", - "bluetoothNeeded": "Omiはウェアラブルに接続するためにBluetoothが必要です。Bluetoothを有効にして再試行してください。", + "bluetoothNeeded": "OmiはBluetoothでスマートフォンに接続します。続けるにはオンにしてください。", "contactSupport": "サポートに連絡しますか?", "connectLater": "後で接続", "grantPermissions": "権限を付与", @@ -1502,7 +1502,7 @@ "durationSeconds": "長さ: {seconds} 秒", "additionalSpeechSampleRemoved": "追加の音声サンプルを削除しました", "consentDataMessage": "続行することで、会話、録音、個人情報は安全に当社のサーバーに保存されます。音声録音と文字起こしは、サードパーティのAIサービス(文字起こし用のDeepgramと分析用のOpenAIを含む)によって処理され、AI駆動のインサイトを提供し、すべてのアプリ機能を有効にします。", - "tasksEmptyStateMessage": "会話を始めてタスクを作成しましょう。", + "tasksEmptyStateMessage": "会話から見つかったToDoがここに表示されます。", "clearChatAction": "チャットを消去", "enableApps": "アプリを有効化", "omiAppName": "Omi", @@ -4004,6 +4004,49 @@ "reviewProposalSuggested": "提案", "reviewProposalConflict": "Dreamが提案した後に内容が変わったため、何も適用されませんでした。", "itemLinkListTitle": "{count, plural, =0{アイテムなし} =1{1件のアイテム} other{{count}件のアイテム}}", + "onboardingPermissionTapToFix": "タップして修正", + "onboardingNoOmiYet": "まだOmiをお持ちでないですか?", + "onboardingOmiOrdered": "注文済みです", + "onboardingOmiNotOrdered": "まだ注文していません", + "onboardingConnectOmi": "Omiを接続", + "consentShortMessage": "OmiはAIで会話をメモにまとめます。メモはいつでも削除できます。", + "consentAiProviders": "{providers} で処理されます。", + "onboardingActIntro": "Omiを試してみましょう。", + "onboardingActMicTitle": "Omiにマイクの許可が必要です", + "onboardingActMicBody": "話しかけたときにOmiが聞き取れるようにします。", + "onboardingActMicAllow": "マイクを許可", + "onboardingActDictateTitle": "今日のことをOmiに話そう", + "onboardingActDictateBody": "思うままに話してください。あとはOmiにお任せ。", + "onboardingActDictateExample": "さっきサムとランチしたよ。6月にリスボンへ引っ越すんだって!金曜までにチームへ資料を送るのと、帰りに牛乳を買うのをリマインドして。", + "onboardingActDictateHint": "話し終えたらチェックをタップ", + "onboardingActPhoneMic": "スマホのマイク", + "onboardingActWriting": "要約しています…", + "onboardingActRemembers": "覚えておくのは Omi にお任せ", + "onboardingActNotesLabel": "今日のまとめ", + "onboardingActTasksLabel": "Omiがやることを見つけました", + "onboardingActYourWords": "あなたが話した内容", + "onboardingActCongratsTitle": "準備完了です、{name}さん", + "onboardingActRateTitle": "Omiはいかがですか?", + "onboardingActRateYes": "Omiを評価", + "onboardingWelcomeCardTitle": "Omiへようこそ", + "onboardingWelcomeCardBody": "Omiを身につけて普段どおり話すだけ。会話はここでメモ、タスク、記憶になります。", + "onboardingActDemoTitle": "サムとランチ", + "onboardingActDemoSummary": "6月にリスボンへ引っ越すサムとランチ。金曜までにチームへ資料を送り、帰りに牛乳を買う予定。", + "onboardingActDemoTask1": "金曜までにチームへ資料を送る", + "onboardingActDemoTask2": "帰りに牛乳を買う", + "onboardingActJustNow": "たった今", + "onboardingActSavedToTasks": "タスクに保存しました", + "onboardingConnectTapToConnect": "Omiをタップして接続します。", + "onboardingActMicFallbackBody": "Omiからまだ音声が届いていません。今はスマートフォンを使いましょう。", + "onboardingActNoAudioOmi": "Omiの音声が聞こえません。電源が入っていて近くにあるか確認してください。", + "onboardingActNoAudioPhone": "まだ声が聞こえません。少し大きな声で話してみてください。", + "onboardingActDictateBodyOmi": "話すだけでOK。Omiが聞いています。", + "voiceProfileCardDismissed": "ボイスプロフィールは設定からいつでも設定できます。", + "homeNewRecordingButton": "新規録音", + "chatWelcomeMessage": "こんにちは、Omiです。話した内容なら何でも聞いてください。誰が何を言ったか、何を約束したか、一日のまとめなど。タスクの追加や、覚えておくこともできます。", + "onboardingStarterTaskAsk": "Omiに今日のことを聞いてみる", + "onboardingStarterTaskRecord": "Omiで会話を録音する", + "onboardingStarterTaskCheck": "このタスクにチェックして完了する", "paywallRecommended": "Recommended", "paywallNoPlans": "Plans aren't available right now. Try again in a minute.", "paywallOtherPlans": "See all plans", @@ -4014,21 +4057,6 @@ "paywallManageBilling": "Manage billing", "paywallLoadError": "Couldn't load plans. Check your connection and try again.", "paywallPlans": "Plans", - "firstCaptureTitle": "How will you capture?", - "firstCaptureDescription": "Choose how you want to capture your first conversation.", - "firstCaptureOmi": "I Have an Omi", - "firstCapturePhone": "This Phone", - "firstCaptureWatch": "Apple Watch", - "firstCaptureSkip": "Skip for Now", - "firstCapturePrompt": "Tell Omi what you're working on today, or what's on your mind. Talk for about 30 seconds.", - "firstCaptureKeepGoing": "Keep going… 30 seconds is ideal.", - "firstCaptureReady": "You can finish now, or keep talking.", - "firstCaptureStart": "Start Recording", - "firstCaptureFinish": "Finish Recording", - "firstCaptureListening": "Listening… your words will appear here.", - "firstCaptureProcessing": "Creating your first conversation…", - "firstCaptureResult": "Your first conversation", - "firstCaptureEmpty": "I didn't catch enough speech, try again with a few sentences", "watchCaptureTitle": "Record from your Apple Watch", "watchCaptureOpen": "Open Omi on your Apple Watch.", "watchCaptureRecord": "Tap Record. Allow microphone access if asked.", @@ -4040,17 +4068,6 @@ "pairingNotFoundOrDropped": "Couldn't reach your Omi. Make sure it's charged, close to your phone, and its light is blinking, then try again.", "pairingBondRecovery": "Forget Omi in your phone’s Bluetooth settings, then try pairing again.", "pairingHowTo": "How to pair", - "firstCaptureWatchInstructions": "Open Omi on your Apple Watch and tap Record. Allow microphone access if asked. Your capture will sync to Omi on this phone.", - "firstCaptureWatchInstall": "In the Watch app on your iPhone, install Omi on your Apple Watch. Then open Omi on your watch and tap Record.", - "firstCaptureWatchPair": "Pair your Apple Watch with this iPhone using the Watch app, then install and open Omi on your watch.", - "firstCaptureUnavailable": "Recording could not start. Check microphone access and your connection, then try again.", - "firstCaptureDelayed": "Your recording is taking longer to process. You can check your conversations on Home or try again.", - "firstCaptureEmptyTitle": "Let's try that again", - "firstCaptureShortTitle": "Finish This Recording?", - "firstCaptureShortWarning": "A few more sentences help Omi understand you. About 30 seconds is ideal.", - "firstCaptureCancelTitle": "Leave This Recording?", - "firstCaptureCancelWarning": "Going back stops this recording. Keep talking to give Omi a few more sentences.", - "firstCaptureSiriTip": "Tip: Enable Omi in Shortcuts → Siri.", "paywallOutlineHeadline": "もう一言も聞き逃さない。", "paywallCancelAnytime": "いつでもキャンセルできます。", "connectAnAgent": "エージェントを接続", diff --git a/app/lib/l10n/app_kn.arb b/app/lib/l10n/app_kn.arb index 4b351298e01..a2ae24a6e7c 100644 --- a/app/lib/l10n/app_kn.arb +++ b/app/lib/l10n/app_kn.arb @@ -1586,7 +1586,7 @@ "@enableBluetooth": { "description": "Title for dialog prompting user to enable Bluetooth" }, - "bluetoothNeeded": "Omi ನಿಮ್ಮ ವಿಯರೇಬಲ್‌ನೊಂದಿಗೆ ಸಂಪರ್ಕ ಮಾಡಲು Bluetooth ಅಗತ್ಯವಿದೆ. ದಯವಿಟ್ಟು Bluetooth ಸಕ್ರಿಯಗೊಳಿಸಿ ಮತ್ತೆ ಪ್ರಯತ್ನಿಸಿ.", + "bluetoothNeeded": "Omi ನಿಮ್ಮ ಫೋನ್‌ಗೆ Bluetooth ಮೂಲಕ ಸಂಪರ್ಕಿಸುತ್ತದೆ. ಮುಂದುವರಿಯಲು ಅದನ್ನು ಆನ್ ಮಾಡಿ.", "@bluetoothNeeded": { "description": "Explanation text for why Bluetooth is required" }, @@ -5782,7 +5782,7 @@ "@consentDataMessage": { "description": "Consent message explaining how user data will be stored and used" }, - "tasksEmptyStateMessage": "ಕಾರ್ಯವನ್ನು ರಚಿಸಲು ಸಂಭಾಷಣೆಯನ್ನು ಪ್ರಾರಂಭಿಸಿ.", + "tasksEmptyStateMessage": "ನಿಮ್ಮ ಸಂಭಾಷಣೆಗಳ ಕಾರ್ಯಗಳು ಇಲ್ಲಿ ಕಾಣಿಸುತ್ತವೆ.", "@tasksEmptyStateMessage": { "description": "Empty state message shown when there are no tasks, with instruction to tap + button" }, @@ -11430,6 +11430,49 @@ "reviewProposalSuggested": "ಸೂಚಿಸಲಾಗಿದೆ", "reviewProposalConflict": "Dream ಸೂಚಿಸಿದ ನಂತರ ಇದು ಬದಲಾಗಿದೆ, ಆದ್ದರಿಂದ ಏನನ್ನೂ ಅನ್ವಯಿಸಲಾಗಿಲ್ಲ.", "itemLinkListTitle": "{count, plural, =0{ಯಾವುದೇ ಐಟಂ ಇಲ್ಲ} =1{1 ಐಟಂ} other{{count} ಐಟಂಗಳು}}", + "onboardingPermissionTapToFix": "ಸರಿಪಡಿಸಲು ಟ್ಯಾಪ್ ಮಾಡಿ", + "onboardingNoOmiYet": "ಇನ್ನೂ ನಿಮ್ಮ Omi ಇಲ್ಲವೇ?", + "onboardingOmiOrdered": "ನಾನು ಆರ್ಡರ್ ಮಾಡಿದ್ದೇನೆ", + "onboardingOmiNotOrdered": "ಇನ್ನೂ ಆರ್ಡರ್ ಮಾಡಿಲ್ಲ", + "onboardingConnectOmi": "Omi ಸಂಪರ್ಕಿಸಿ", + "consentShortMessage": "Omi AI ಬಳಸಿ ನಿಮ್ಮ ಸಂಭಾಷಣೆಗಳನ್ನು ಟಿಪ್ಪಣಿಗಳಾಗಿ ಮಾಡುತ್ತದೆ. ಯಾವಾಗ ಬೇಕಾದರೂ ಅಳಿಸಬಹುದು.", + "consentAiProviders": "{providers} ಮೂಲಕ ಸಂಸ್ಕರಿಸಲಾಗುತ್ತದೆ.", + "onboardingActIntro": "Omi ಪ್ರಯತ್ನಿಸೋಣ.", + "onboardingActMicTitle": "Omi ಗೆ ನಿಮ್ಮ ಮೈಕ್ರೊಫೋನ್ ಬೇಕು", + "onboardingActMicBody": "ನೀವು ಮಾತನಾಡಿದಾಗ Omi ಕೇಳಲು.", + "onboardingActMicAllow": "ಮೈಕ್ರೊಫೋನ್ ಅನುಮತಿಸಿ", + "onboardingActDictateTitle": "ನಿಮ್ಮ ದಿನದ ಬಗ್ಗೆ Omi ಗೆ ಹೇಳಿ", + "onboardingActDictateBody": "ನಿಮ್ಮದೇ ರೀತಿಯಲ್ಲಿ ಹೇಳಿ. ಉಳಿದದ್ದು Omi ನೋಡಿಕೊಳ್ಳುತ್ತದೆ.", + "onboardingActDictateExample": "ಈಗಷ್ಟೇ ಸ್ಯಾಮ್ ಜೊತೆ ಊಟ ಮಾಡಿದೆ, ಅವಳು ಜೂನ್‌ನಲ್ಲಿ ಲಿಸ್ಬನ್‌ಗೆ ಹೋಗುತ್ತಿದ್ದಾಳೆ! ಶುಕ್ರವಾರದೊಳಗೆ ತಂಡಕ್ಕೆ ಡೆಕ್ ಕಳುಹಿಸಲು ಮತ್ತು ಮನೆಗೆ ಹೋಗುವಾಗ ಹಾಲು ತರಲು ನೆನಪಿಸು.", + "onboardingActDictateHint": "ಮುಗಿದ ನಂತರ ಟಿಕ್ ಟ್ಯಾಪ್ ಮಾಡಿ", + "onboardingActPhoneMic": "ಫೋನ್ ಮೈಕ್ರೊಫೋನ್", + "onboardingActWriting": "ಸಾರಾಂಶ ಮಾಡುತ್ತಿದೆ…", + "onboardingActRemembers": "ನೀವು ನೆನಪಿಡಬೇಕಿಲ್ಲ, Omi ನೆನಪಿಡುತ್ತದೆ", + "onboardingActNotesLabel": "ನಿಮ್ಮ ದಿನದ ಸಾರಾಂಶ", + "onboardingActTasksLabel": "Omi ನಿಮ್ಮ ಕೆಲಸಗಳನ್ನು ಗುರುತಿಸಿದೆ", + "onboardingActYourWords": "ನೀವು ಹೇಳಿದ್ದು ಇದು", + "onboardingActCongratsTitle": "ಎಲ್ಲವೂ ಸಿದ್ಧ, {name}", + "onboardingActRateTitle": "ಇಲ್ಲಿಯವರೆಗೆ Omi ಇಷ್ಟವಾಯಿತೇ?", + "onboardingActRateYes": "Omi ಗೆ ರೇಟ್ ಮಾಡಿ", + "onboardingWelcomeCardTitle": "Omi ಗೆ ಸ್ವಾಗತ", + "onboardingWelcomeCardBody": "Omi ಧರಿಸಿ ಎಂದಿನಂತೆ ಮಾತನಾಡಿ. ಪ್ರತಿಯೊಂದು ಸಂಭಾಷಣೆ ಇಲ್ಲೇ ಟಿಪ್ಪಣಿ, ಕಾರ್ಯ ಮತ್ತು ನೆನಪುಗಳಾಗುತ್ತದೆ.", + "onboardingActDemoTitle": "ಸ್ಯಾಮ್ ಜೊತೆ ಊಟ", + "onboardingActDemoSummary": "ಜೂನ್‌ನಲ್ಲಿ ಲಿಸ್ಬನ್‌ಗೆ ಹೋಗುತ್ತಿರುವ ಸ್ಯಾಮ್ ಜೊತೆ ಊಟ. ಶುಕ್ರವಾರದೊಳಗೆ ತಂಡಕ್ಕೆ ಡೆಕ್ ಕಳುಹಿಸಬೇಕು, ಮನೆಗೆ ಹೋಗುವಾಗ ಹಾಲು ತರಬೇಕು.", + "onboardingActDemoTask1": "ಶುಕ್ರವಾರದೊಳಗೆ ತಂಡಕ್ಕೆ ಡೆಕ್ ಕಳುಹಿಸುವುದು", + "onboardingActDemoTask2": "ಮನೆಗೆ ಹೋಗುವಾಗ ಹಾಲು ತರುವುದು", + "onboardingActJustNow": "ಈಗಷ್ಟೇ", + "onboardingActSavedToTasks": "ನಿಮ್ಮ ಕೆಲಸಗಳಿಗೆ ಉಳಿಸಲಾಗಿದೆ", + "onboardingConnectTapToConnect": "ಸಂಪರ್ಕಿಸಲು ನಿಮ್ಮ Omi ಟ್ಯಾಪ್ ಮಾಡಿ.", + "onboardingActMicFallbackBody": "ನಿಮ್ಮ Omi ಇನ್ನೂ ಆಡಿಯೋ ಕಳುಹಿಸುತ್ತಿಲ್ಲ. ಸದ್ಯಕ್ಕೆ ನಿಮ್ಮ ಫೋನ್ ಬಳಸಿ.", + "onboardingActNoAudioOmi": "ನಿಮ್ಮ Omi ಕೇಳಿಸುತ್ತಿಲ್ಲ. ಅದು ಆನ್ ಆಗಿದೆ ಮತ್ತು ಹತ್ತಿರವಿದೆಯೇ ನೋಡಿ.", + "onboardingActNoAudioPhone": "ಇನ್ನೂ ನಿಮ್ಮ ಧ್ವನಿ ಕೇಳಿಸುತ್ತಿಲ್ಲ. ಸ್ವಲ್ಪ ಜೋರಾಗಿ ಮಾತಾಡಿ ನೋಡಿ.", + "onboardingActDictateBodyOmi": "ಸುಮ್ಮನೆ ಮಾತಾಡಿ. ನಿಮ್ಮ Omi ಕೇಳುತ್ತಿದೆ.", + "voiceProfileCardDismissed": "ಸೆಟ್ಟಿಂಗ್‌ಗಳಲ್ಲಿ ಯಾವಾಗ ಬೇಕಾದರೂ ಧ್ವನಿ ಪ್ರೊಫೈಲ್ ಹೊಂದಿಸಬಹುದು.", + "homeNewRecordingButton": "ಹೊಸ ರೆಕಾರ್ಡಿಂಗ್", + "chatWelcomeMessage": "ನಮಸ್ಕಾರ, ನಾನು Omi. ನೀವು ಮಾತನಾಡಿದ ಯಾವುದರ ಬಗ್ಗೆಯೂ ಕೇಳಿ: ಯಾರು ಏನು ಹೇಳಿದರು, ನೀವು ಏನು ಭರವಸೆ ನೀಡಿದ್ದೀರಿ ಅಥವಾ ನಿಮ್ಮ ದಿನದ ಸಾರಾಂಶ. ನಾನು ಕಾರ್ಯಗಳನ್ನು ಸೇರಿಸಬಹುದು ಮತ್ತು ನಿಮಗಾಗಿ ವಿಷಯಗಳನ್ನು ನೆನಪಿಟ್ಟುಕೊಳ್ಳಬಹುದು.", + "onboardingStarterTaskAsk": "ನಿಮ್ಮ ದಿನದ ಬಗ್ಗೆ Omi ಅನ್ನು ಕೇಳಿ", + "onboardingStarterTaskRecord": "Omi ಮೂಲಕ ಒಂದು ಸಂಭಾಷಣೆಯನ್ನು ರೆಕಾರ್ಡ್ ಮಾಡಿ", + "onboardingStarterTaskCheck": "ಮುಗಿಸಲು ಈ ಕಾರ್ಯವನ್ನು ಟಿಕ್ ಮಾಡಿ", "paywallRecommended": "Recommended", "paywallNoPlans": "Plans aren't available right now. Try again in a minute.", "paywallOtherPlans": "See all plans", @@ -11440,21 +11483,6 @@ "paywallManageBilling": "Manage billing", "paywallLoadError": "Couldn't load plans. Check your connection and try again.", "paywallPlans": "Plans", - "firstCaptureTitle": "How will you capture?", - "firstCaptureDescription": "Choose how you want to capture your first conversation.", - "firstCaptureOmi": "I Have an Omi", - "firstCapturePhone": "This Phone", - "firstCaptureWatch": "Apple Watch", - "firstCaptureSkip": "Skip for Now", - "firstCapturePrompt": "Tell Omi what you're working on today, or what's on your mind. Talk for about 30 seconds.", - "firstCaptureKeepGoing": "Keep going… 30 seconds is ideal.", - "firstCaptureReady": "You can finish now, or keep talking.", - "firstCaptureStart": "Start Recording", - "firstCaptureFinish": "Finish Recording", - "firstCaptureListening": "Listening… your words will appear here.", - "firstCaptureProcessing": "Creating your first conversation…", - "firstCaptureResult": "Your first conversation", - "firstCaptureEmpty": "I didn't catch enough speech, try again with a few sentences", "watchCaptureTitle": "Record from your Apple Watch", "watchCaptureOpen": "Open Omi on your Apple Watch.", "watchCaptureRecord": "Tap Record. Allow microphone access if asked.", @@ -11466,17 +11494,6 @@ "pairingNotFoundOrDropped": "Couldn't reach your Omi. Make sure it's charged, close to your phone, and its light is blinking, then try again.", "pairingBondRecovery": "Forget Omi in your phone’s Bluetooth settings, then try pairing again.", "pairingHowTo": "How to pair", - "firstCaptureWatchInstructions": "Open Omi on your Apple Watch and tap Record. Allow microphone access if asked. Your capture will sync to Omi on this phone.", - "firstCaptureWatchInstall": "In the Watch app on your iPhone, install Omi on your Apple Watch. Then open Omi on your watch and tap Record.", - "firstCaptureWatchPair": "Pair your Apple Watch with this iPhone using the Watch app, then install and open Omi on your watch.", - "firstCaptureUnavailable": "Recording could not start. Check microphone access and your connection, then try again.", - "firstCaptureDelayed": "Your recording is taking longer to process. You can check your conversations on Home or try again.", - "firstCaptureEmptyTitle": "Let's try that again", - "firstCaptureShortTitle": "Finish This Recording?", - "firstCaptureShortWarning": "A few more sentences help Omi understand you. About 30 seconds is ideal.", - "firstCaptureCancelTitle": "Leave This Recording?", - "firstCaptureCancelWarning": "Going back stops this recording. Keep talking to give Omi a few more sentences.", - "firstCaptureSiriTip": "Tip: Enable Omi in Shortcuts → Siri.", "paywallOutlineHeadline": "ಇನ್ನು ಮುಂದೆ ಒಂದು ಪದವನ್ನೂ ತಪ್ಪಿಸಿಕೊಳ್ಳಬೇಡಿ.", "paywallCancelAnytime": "ಯಾವಾಗ ಬೇಕಾದರೂ ರದ್ದುಗೊಳಿಸಿ.", "connectAnAgent": "ಏಜೆಂಟ್ ಅನ್ನು ಸಂಪರ್ಕಿಸಿ", diff --git a/app/lib/l10n/app_ko.arb b/app/lib/l10n/app_ko.arb index a338dff9dce..7cb1541ec45 100644 --- a/app/lib/l10n/app_ko.arb +++ b/app/lib/l10n/app_ko.arb @@ -520,7 +520,7 @@ "dontShowAgain": "다시 표시하지 않기", "iUnderstand": "이해했습니다", "enableBluetooth": "블루투스 활성화", - "bluetoothNeeded": "Omi가 웨어러블에 연결하려면 블루투스가 필요합니다. 블루투스를 활성화하고 다시 시도해 주세요.", + "bluetoothNeeded": "Omi는 Bluetooth로 휴대폰에 연결돼요. 계속하려면 켜 주세요.", "contactSupport": "지원팀에 문의하시겠습니까?", "connectLater": "나중에 연결", "grantPermissions": "권한 부여", @@ -1501,7 +1501,7 @@ "durationSeconds": "길이: {seconds}초", "additionalSpeechSampleRemoved": "추가 음성 샘플이 삭제되었습니다", "consentDataMessage": "계속하면 대화, 녹음 및 개인 정보가 서버에 안전하게 저장됩니다. 오디오 녹음 및 텍스트 변환은 제3자 AI 서비스(전사를 위한 Deepgram 및 분석을 위한 OpenAI 포함)에 의해 처리되어 AI 기반 인사이트를 제공하고 모든 앱 기능을 활성화합니다.", - "tasksEmptyStateMessage": "대화를 시작하여 할 일을 만드세요.", + "tasksEmptyStateMessage": "대화에서 나온 할 일이 여기에 표시돼요.", "clearChatAction": "채팅 삭제", "enableApps": "앱 활성화", "omiAppName": "Omi", @@ -4004,6 +4004,49 @@ "reviewProposalSuggested": "제안", "reviewProposalConflict": "Dream이 제안한 후 내용이 바뀌어 아무것도 적용되지 않았습니다.", "itemLinkListTitle": "{count, plural, =0{항목 없음} =1{항목 1개} other{항목 {count}개}}", + "onboardingPermissionTapToFix": "탭하여 해결", + "onboardingNoOmiYet": "아직 Omi가 없으신가요?", + "onboardingOmiOrdered": "주문했어요", + "onboardingOmiNotOrdered": "아직 주문하지 않았어요", + "onboardingConnectOmi": "Omi 연결", + "consentShortMessage": "Omi는 AI로 대화를 메모로 정리합니다. 언제든지 삭제할 수 있습니다.", + "consentAiProviders": "{providers}(으)로 처리됩니다.", + "onboardingActIntro": "Omi를 써 볼까요?", + "onboardingActMicTitle": "Omi에 마이크 권한이 필요해요", + "onboardingActMicBody": "말할 때 Omi가 들을 수 있도록요.", + "onboardingActMicAllow": "마이크 허용", + "onboardingActDictateTitle": "Omi에게 오늘 하루를 말해 보세요", + "onboardingActDictateBody": "편하게 말하세요. 나머지는 Omi가 할게요.", + "onboardingActDictateExample": "방금 샘이랑 점심 먹었어. 6월에 리스본으로 이사 간대! 금요일까지 팀에 발표 자료 보내는 거랑, 집에 가는 길에 우유 사는 거 알려 줘.", + "onboardingActDictateHint": "다 말했으면 체크를 누르세요", + "onboardingActPhoneMic": "휴대폰 마이크", + "onboardingActWriting": "요약하는 중…", + "onboardingActRemembers": "기억은 Omi가 대신해요", + "onboardingActNotesLabel": "오늘 하루 요약", + "onboardingActTasksLabel": "Omi가 할 일을 찾았어요", + "onboardingActYourWords": "이렇게 말했어요", + "onboardingActCongratsTitle": "{name}님, 준비가 끝났어요", + "onboardingActRateTitle": "Omi 마음에 드시나요?", + "onboardingActRateYes": "Omi 평가하기", + "onboardingWelcomeCardTitle": "Omi에 오신 것을 환영해요", + "onboardingWelcomeCardBody": "Omi를 착용하고 평소처럼 대화하세요. 모든 대화가 여기서 노트, 할 일, 기억이 돼요.", + "onboardingActDemoTitle": "샘과 점심", + "onboardingActDemoSummary": "6월에 리스본으로 이사하는 샘과 점심. 금요일까지 팀에 자료를 보내고, 집에 가는 길에 우유를 사야 해요.", + "onboardingActDemoTask1": "금요일까지 팀에 발표 자료 보내기", + "onboardingActDemoTask2": "집에 가는 길에 우유 사기", + "onboardingActJustNow": "방금", + "onboardingActSavedToTasks": "할 일에 저장됨", + "onboardingConnectTapToConnect": "Omi를 탭해서 연결하세요.", + "onboardingActMicFallbackBody": "Omi가 아직 오디오를 보내지 않아요. 지금은 휴대폰을 사용하세요.", + "onboardingActNoAudioOmi": "Omi 소리가 들리지 않아요. 켜져 있고 가까이 있는지 확인하세요.", + "onboardingActNoAudioPhone": "아직 목소리가 들리지 않아요. 조금 더 크게 말해 보세요.", + "onboardingActDictateBodyOmi": "그냥 말하세요. Omi가 듣고 있어요.", + "voiceProfileCardDismissed": "음성 프로필은 설정에서 언제든지 설정할 수 있어요.", + "homeNewRecordingButton": "새 녹음", + "chatWelcomeMessage": "안녕하세요, Omi예요. 나눈 대화라면 무엇이든 물어보세요. 누가 무슨 말을 했는지, 무엇을 약속했는지, 하루 요약까지요. 할 일을 추가하고 기억해 둘 수도 있어요.", + "onboardingStarterTaskAsk": "Omi에게 오늘 하루에 대해 물어보기", + "onboardingStarterTaskRecord": "Omi로 대화 녹음하기", + "onboardingStarterTaskCheck": "이 할 일을 체크해서 완료하기", "paywallRecommended": "Recommended", "paywallNoPlans": "Plans aren't available right now. Try again in a minute.", "paywallOtherPlans": "See all plans", @@ -4014,21 +4057,6 @@ "paywallManageBilling": "Manage billing", "paywallLoadError": "Couldn't load plans. Check your connection and try again.", "paywallPlans": "Plans", - "firstCaptureTitle": "How will you capture?", - "firstCaptureDescription": "Choose how you want to capture your first conversation.", - "firstCaptureOmi": "I Have an Omi", - "firstCapturePhone": "This Phone", - "firstCaptureWatch": "Apple Watch", - "firstCaptureSkip": "Skip for Now", - "firstCapturePrompt": "Tell Omi what you're working on today, or what's on your mind. Talk for about 30 seconds.", - "firstCaptureKeepGoing": "Keep going… 30 seconds is ideal.", - "firstCaptureReady": "You can finish now, or keep talking.", - "firstCaptureStart": "Start Recording", - "firstCaptureFinish": "Finish Recording", - "firstCaptureListening": "Listening… your words will appear here.", - "firstCaptureProcessing": "Creating your first conversation…", - "firstCaptureResult": "Your first conversation", - "firstCaptureEmpty": "I didn't catch enough speech, try again with a few sentences", "watchCaptureTitle": "Record from your Apple Watch", "watchCaptureOpen": "Open Omi on your Apple Watch.", "watchCaptureRecord": "Tap Record. Allow microphone access if asked.", @@ -4040,17 +4068,6 @@ "pairingNotFoundOrDropped": "Couldn't reach your Omi. Make sure it's charged, close to your phone, and its light is blinking, then try again.", "pairingBondRecovery": "Forget Omi in your phone’s Bluetooth settings, then try pairing again.", "pairingHowTo": "How to pair", - "firstCaptureWatchInstructions": "Open Omi on your Apple Watch and tap Record. Allow microphone access if asked. Your capture will sync to Omi on this phone.", - "firstCaptureWatchInstall": "In the Watch app on your iPhone, install Omi on your Apple Watch. Then open Omi on your watch and tap Record.", - "firstCaptureWatchPair": "Pair your Apple Watch with this iPhone using the Watch app, then install and open Omi on your watch.", - "firstCaptureUnavailable": "Recording could not start. Check microphone access and your connection, then try again.", - "firstCaptureDelayed": "Your recording is taking longer to process. You can check your conversations on Home or try again.", - "firstCaptureEmptyTitle": "Let's try that again", - "firstCaptureShortTitle": "Finish This Recording?", - "firstCaptureShortWarning": "A few more sentences help Omi understand you. About 30 seconds is ideal.", - "firstCaptureCancelTitle": "Leave This Recording?", - "firstCaptureCancelWarning": "Going back stops this recording. Keep talking to give Omi a few more sentences.", - "firstCaptureSiriTip": "Tip: Enable Omi in Shortcuts → Siri.", "paywallOutlineHeadline": "이제 단 한마디도 놓치지 마세요.", "paywallCancelAnytime": "언제든지 해지할 수 있습니다.", "connectAnAgent": "에이전트 연결", diff --git a/app/lib/l10n/app_localizations.dart b/app/lib/l10n/app_localizations.dart index ba85b0db382..fed55f8cfdd 100644 --- a/app/lib/l10n/app_localizations.dart +++ b/app/lib/l10n/app_localizations.dart @@ -3312,7 +3312,7 @@ abstract class AppLocalizations { /// Explanation text for why Bluetooth is required /// /// In en, this message translates to: - /// **'Omi needs Bluetooth to connect to your wearable. Please enable Bluetooth and try again.'** + /// **'Omi connects to your phone over Bluetooth. Turn it on to continue.'** String get bluetoothNeeded; /// Link text to contact support @@ -9306,7 +9306,7 @@ abstract class AppLocalizations { /// Guidance on the empty Tasks tab: start a conversation to create a task. /// /// In en, this message translates to: - /// **'Start a conversation to create a task.'** + /// **'To-dos from your conversations will show up here.'** String get tasksEmptyStateMessage; /// Menu item text for clearing chat history @@ -23211,287 +23211,389 @@ abstract class AppLocalizations { /// **'{count, plural, =0{No Items} =1{1 Item} other{{count} Items}}'** String itemLinkListTitle(int count); - /// Backend recommended tier badge; highlights a recommendation without preselecting checkout. + /// Onboarding permissions: shown on a denied permission card; tapping opens Settings /// /// In en, this message translates to: - /// **'Recommended'** - String get paywallRecommended; + /// **'Tap to fix'** + String get onboardingPermissionTapToFix; - /// Empty state when the plans list has no tiers. + /// Onboarding connect step: small prompt for people without their Omi device /// /// In en, this message translates to: - /// **'Plans aren\'t available right now. Try again in a minute.'** - String get paywallNoPlans; + /// **'Don\'t have your Omi yet?'** + String get onboardingNoOmiYet; - /// Collapsed section that reveals the rest of the plan ladder. + /// Onboarding connect step: choice for people whose Omi device is on its way /// /// In en, this message translates to: - /// **'See all plans'** - String get paywallOtherPlans; + /// **'I\'ve ordered mine'** + String get onboardingOmiOrdered; - /// Primary button on the usage banner. + /// Onboarding connect step: choice for people who have not ordered an Omi device /// /// In en, this message translates to: - /// **'See plans'** - String get paywallComparePlans; + /// **'Not ordered yet'** + String get onboardingOmiNotOrdered; - /// Badge on the tier card for the plan the person already has. + /// Onboarding connect step title; Omi is the brand name of the wearable /// /// In en, this message translates to: - /// **'Your plan'** - String get paywallYourPlan; + /// **'Connect Omi'** + String get onboardingConnectOmi; - /// Secondary button on the usage banner. + /// Onboarding data & privacy consent: one-line summary of storage and AI processing /// /// In en, this message translates to: - /// **'Not now'** - String get paywallNotNow; + /// **'Omi uses AI to turn your conversations into notes. You can delete them anytime.'** + String get consentShortMessage; - /// Retry button when the plans sheet fails to load. + /// Onboarding consent: names the third-party AI providers (brand names, not translated) /// /// In en, this message translates to: - /// **'Try again'** - String get paywallTryAgain; + /// **'Processed with {providers}.'** + String consentAiProviders(String providers); - /// Button that opens billing management. + /// Activation: animated line before the demo slides /// /// In en, this message translates to: - /// **'Manage billing'** - String get paywallManageBilling; + /// **'Let\'s try Omi.'** + String get onboardingActIntro; - /// Plans sheet error copy. The retry button is separate. + /// Activation: microphone pre-permission title (phone users) /// /// In en, this message translates to: - /// **'Couldn\'t load plans. Check your connection and try again.'** - String get paywallLoadError; + /// **'Omi needs your microphone'** + String get onboardingActMicTitle; - /// Title of the new plans sheet. The previous sheet keeps Plans & Billing. + /// Activation: why the microphone is needed /// /// In en, this message translates to: - /// **'Plans'** - String get paywallPlans; + /// **'So Omi can hear you when you talk to it.'** + String get onboardingActMicBody; - /// No description provided for @firstCaptureTitle. + /// Activation: button that shows the microphone system prompt /// /// In en, this message translates to: - /// **'How will you capture?'** - String get firstCaptureTitle; + /// **'Allow Microphone'** + String get onboardingActMicAllow; - /// No description provided for @firstCaptureDescription. + /// Activation: dictation step title /// /// In en, this message translates to: - /// **'Choose how you want to capture your first conversation.'** - String get firstCaptureDescription; + /// **'Tell Omi about your day'** + String get onboardingActDictateTitle; - /// No description provided for @firstCaptureOmi. + /// Activation: line under the dictation title /// /// In en, this message translates to: - /// **'I Have an Omi'** - String get firstCaptureOmi; + /// **'Say it your way. Omi handles the rest.'** + String get onboardingActDictateBody; - /// No description provided for @firstCapturePhone. + /// Activation: faded example in the dictation card (casual tone; keep 'Remind me' so Omi keeps it; Sam and Lisbon are example names) /// /// In en, this message translates to: - /// **'This Phone'** - String get firstCapturePhone; + /// **'Just had lunch with Sam, she\'s moving to Lisbon in June! Remind me to send the deck to my team by Friday, and to grab milk on the way home.'** + String get onboardingActDictateExample; - /// No description provided for @firstCaptureWatch. + /// Activation: hint in the dictation bar next to the check button /// /// In en, this message translates to: - /// **'Apple Watch'** - String get firstCaptureWatch; + /// **'Tap the check when you\'re done'** + String get onboardingActDictateHint; - /// No description provided for @firstCaptureSkip. + /// Activation: which microphone is listening (phone) /// /// In en, this message translates to: - /// **'Skip for Now'** - String get firstCaptureSkip; + /// **'Phone microphone'** + String get onboardingActPhoneMic; - /// No description provided for @firstCapturePrompt. + /// Activation: shown while Omi processes the dictation /// /// In en, this message translates to: - /// **'Tell Omi what you\'re working on today, or what\'s on your mind. Talk for about 30 seconds.'** - String get firstCapturePrompt; + /// **'Summarizing…'** + String get onboardingActWriting; - /// No description provided for @firstCaptureKeepGoing. + /// Line above the summary title at the end of the onboarding dictation /// /// In en, this message translates to: - /// **'Keep going… 30 seconds is ideal.'** - String get firstCaptureKeepGoing; + /// **'Omi remembers so you don\'t have to'** + String get onboardingActRemembers; - /// No description provided for @firstCaptureReady. + /// Activation: small label above the generated summary /// /// In en, this message translates to: - /// **'You can finish now, or keep talking.'** - String get firstCaptureReady; + /// **'Your day, summarized'** + String get onboardingActNotesLabel; - /// No description provided for @firstCaptureStart. + /// Activation: small label above the generated tasks /// /// In en, this message translates to: - /// **'Start Recording'** - String get firstCaptureStart; + /// **'Omi caught your to-dos'** + String get onboardingActTasksLabel; - /// No description provided for @firstCaptureFinish. + /// Activation: fallback label when no summary could be made /// /// In en, this message translates to: - /// **'Finish Recording'** - String get firstCaptureFinish; + /// **'Here\'s what you said'** + String get onboardingActYourWords; - /// No description provided for @firstCaptureListening. + /// Activation: congratulations title with the user's first name /// /// In en, this message translates to: - /// **'Listening… your words will appear here.'** - String get firstCaptureListening; + /// **'You\'re all set, {name}'** + String onboardingActCongratsTitle(String name); - /// No description provided for @firstCaptureProcessing. + /// Activation: rating ask on the congratulations screen /// /// In en, this message translates to: - /// **'Creating your first conversation…'** - String get firstCaptureProcessing; + /// **'Enjoying Omi so far?'** + String get onboardingActRateTitle; - /// No description provided for @firstCaptureResult. + /// Activation: button that opens the system rating prompt /// /// In en, this message translates to: - /// **'Your first conversation'** - String get firstCaptureResult; + /// **'Rate Omi'** + String get onboardingActRateYes; - /// No description provided for @firstCaptureEmpty. + /// Home: welcome card title for people who skipped the first conversation /// /// In en, this message translates to: - /// **'I didn\'t catch enough speech, try again with a few sentences'** - String get firstCaptureEmpty; + /// **'Welcome to Omi'** + String get onboardingWelcomeCardTitle; - /// No description provided for @watchCaptureTitle. + /// Home: welcome card text /// /// In en, this message translates to: - /// **'Record from your Apple Watch'** - String get watchCaptureTitle; + /// **'Wear Omi and talk as usual. Each conversation turns into notes, tasks and memories right here.'** + String get onboardingWelcomeCardBody; - /// No description provided for @watchCaptureOpen. + /// Activation slide illustration: example note title /// /// In en, this message translates to: - /// **'Open Omi on your Apple Watch.'** - String get watchCaptureOpen; + /// **'Lunch with Sam'** + String get onboardingActDemoTitle; - /// No description provided for @watchCaptureRecord. + /// Activation slide illustration: example summary line /// /// In en, this message translates to: - /// **'Tap Record. Allow microphone access if asked.'** - String get watchCaptureRecord; + /// **'Lunch with Sam, who\'s moving to Lisbon in June. You want to send the deck to your team by Friday and grab milk on the way home.'** + String get onboardingActDemoSummary; - /// No description provided for @watchCaptureSync. + /// Activation slide illustration: example task /// /// In en, this message translates to: - /// **'Your recording syncs to Omi on this phone.'** - String get watchCaptureSync; + /// **'Send the deck to the team by Friday'** + String get onboardingActDemoTask1; - /// No description provided for @pairingBluetoothOff. + /// Activation slide illustration: example task /// /// In en, this message translates to: - /// **'Turn on Bluetooth on your phone, then try again.'** - String get pairingBluetoothOff; + /// **'Grab milk on the way home'** + String get onboardingActDemoTask2; - /// No description provided for @pairingBluetoothPermission. + /// Time label on the first note Omi wrote in onboarding /// /// In en, this message translates to: - /// **'Allow Bluetooth access for Omi in Settings, then try again.'** - String get pairingBluetoothPermission; + /// **'Just now'** + String get onboardingActJustNow; - /// No description provided for @pairingNearbyPermission. + /// Confirmation under the to-dos Omi found in onboarding /// /// In en, this message translates to: - /// **'Allow Nearby devices access for Omi in Settings, then try again.'** - String get pairingNearbyPermission; + /// **'Saved to your tasks'** + String get onboardingActSavedToTasks; - /// No description provided for @pairingLocationPermission. + /// Connect Omi onboarding step, under the title once an Omi is found nearby /// /// In en, this message translates to: - /// **'Allow location access for Omi in Settings so this phone can find your device, then try again.'** - String get pairingLocationPermission; + /// **'Tap your Omi to connect.'** + String get onboardingConnectTapToConnect; - /// No description provided for @pairingNotFoundOrDropped. + /// Onboarding microphone ask when the connected Omi is not streaming /// /// In en, this message translates to: - /// **'Couldn\'t reach your Omi. Make sure it\'s charged, close to your phone, and its light is blinking, then try again.'** - String get pairingNotFoundOrDropped; + /// **'Your Omi isn\'t sending audio yet. Use your phone for now.'** + String get onboardingActMicFallbackBody; - /// No description provided for @pairingBondRecovery. + /// Onboarding dictation hint after a while with no words from the Omi /// /// In en, this message translates to: - /// **'Forget Omi in your phone’s Bluetooth settings, then try pairing again.'** - String get pairingBondRecovery; + /// **'Can\'t hear your Omi. Make sure it\'s on and close by.'** + String get onboardingActNoAudioOmi; - /// No description provided for @pairingHowTo. + /// Onboarding dictation hint after a while with no words from the phone microphone /// /// In en, this message translates to: - /// **'How to pair'** - String get pairingHowTo; + /// **'Can\'t hear you yet. Try speaking a little louder.'** + String get onboardingActNoAudioPhone; + + /// Onboarding dictation subtitle when the words come from the connected Omi + /// + /// In en, this message translates to: + /// **'Just talk. Your Omi is listening.'** + String get onboardingActDictateBodyOmi; + + /// Toast after dismissing the Teach Omi your voice card on Home; points to Settings > Voice Profile. + /// + /// In en, this message translates to: + /// **'You can set up Voice Profile anytime in Settings.'** + String get voiceProfileCardDismissed; + + /// Label on Home's main bottom button that starts a new recording with the phone. + /// + /// In en, this message translates to: + /// **'New Recording'** + String get homeNewRecordingButton; + + /// First message Omi shows in an empty chat the first time it is opened, explaining what chat can do + /// + /// In en, this message translates to: + /// **'Hi, I\'m Omi. Ask me anything you\'ve talked about: what someone said, what you promised, or a summary of your day. I can also add to-dos and remember things for you.'** + String get chatWelcomeMessage; + + /// Starter task added to an empty task list after onboarding: try chatting with Omi + /// + /// In en, this message translates to: + /// **'Ask Omi about your day'** + String get onboardingStarterTaskAsk; + + /// Starter task added to an empty task list after onboarding: try recording + /// + /// In en, this message translates to: + /// **'Record a conversation with Omi'** + String get onboardingStarterTaskRecord; + + /// Starter task added to an empty task list after onboarding: shows how completing a task works + /// + /// In en, this message translates to: + /// **'Check this off to finish a task'** + String get onboardingStarterTaskCheck; + + /// Backend recommended tier badge; highlights a recommendation without preselecting checkout. + /// + /// In en, this message translates to: + /// **'Recommended'** + String get paywallRecommended; + + /// Empty state when the plans list has no tiers. + /// + /// In en, this message translates to: + /// **'Plans aren\'t available right now. Try again in a minute.'** + String get paywallNoPlans; + + /// Collapsed section that reveals the rest of the plan ladder. + /// + /// In en, this message translates to: + /// **'See all plans'** + String get paywallOtherPlans; + + /// Primary button on the usage banner. + /// + /// In en, this message translates to: + /// **'See plans'** + String get paywallComparePlans; + + /// Badge on the tier card for the plan the person already has. + /// + /// In en, this message translates to: + /// **'Your plan'** + String get paywallYourPlan; + + /// Secondary button on the usage banner. + /// + /// In en, this message translates to: + /// **'Not now'** + String get paywallNotNow; + + /// Retry button when the plans sheet fails to load. + /// + /// In en, this message translates to: + /// **'Try again'** + String get paywallTryAgain; + + /// Button that opens billing management. + /// + /// In en, this message translates to: + /// **'Manage billing'** + String get paywallManageBilling; - /// No description provided for @firstCaptureWatchInstructions. + /// Plans sheet error copy. The retry button is separate. /// /// In en, this message translates to: - /// **'Open Omi on your Apple Watch and tap Record. Allow microphone access if asked. Your capture will sync to Omi on this phone.'** - String get firstCaptureWatchInstructions; + /// **'Couldn\'t load plans. Check your connection and try again.'** + String get paywallLoadError; - /// No description provided for @firstCaptureWatchInstall. + /// Title of the new plans sheet. The previous sheet keeps Plans & Billing. /// /// In en, this message translates to: - /// **'In the Watch app on your iPhone, install Omi on your Apple Watch. Then open Omi on your watch and tap Record.'** - String get firstCaptureWatchInstall; + /// **'Plans'** + String get paywallPlans; - /// No description provided for @firstCaptureWatchPair. + /// No description provided for @watchCaptureTitle. /// /// In en, this message translates to: - /// **'Pair your Apple Watch with this iPhone using the Watch app, then install and open Omi on your watch.'** - String get firstCaptureWatchPair; + /// **'Record from your Apple Watch'** + String get watchCaptureTitle; - /// No description provided for @firstCaptureUnavailable. + /// No description provided for @watchCaptureOpen. /// /// In en, this message translates to: - /// **'Recording could not start. Check microphone access and your connection, then try again.'** - String get firstCaptureUnavailable; + /// **'Open Omi on your Apple Watch.'** + String get watchCaptureOpen; - /// No description provided for @firstCaptureDelayed. + /// No description provided for @watchCaptureRecord. /// /// In en, this message translates to: - /// **'Your recording is taking longer to process. You can check your conversations on Home or try again.'** - String get firstCaptureDelayed; + /// **'Tap Record. Allow microphone access if asked.'** + String get watchCaptureRecord; - /// No description provided for @firstCaptureEmptyTitle. + /// No description provided for @watchCaptureSync. /// /// In en, this message translates to: - /// **'Let\'s try that again'** - String get firstCaptureEmptyTitle; + /// **'Your recording syncs to Omi on this phone.'** + String get watchCaptureSync; - /// No description provided for @firstCaptureShortTitle. + /// No description provided for @pairingBluetoothOff. /// /// In en, this message translates to: - /// **'Finish This Recording?'** - String get firstCaptureShortTitle; + /// **'Turn on Bluetooth on your phone, then try again.'** + String get pairingBluetoothOff; - /// No description provided for @firstCaptureShortWarning. + /// No description provided for @pairingBluetoothPermission. /// /// In en, this message translates to: - /// **'A few more sentences help Omi understand you. About 30 seconds is ideal.'** - String get firstCaptureShortWarning; + /// **'Allow Bluetooth access for Omi in Settings, then try again.'** + String get pairingBluetoothPermission; + + /// No description provided for @pairingNearbyPermission. + /// + /// In en, this message translates to: + /// **'Allow Nearby devices access for Omi in Settings, then try again.'** + String get pairingNearbyPermission; + + /// No description provided for @pairingLocationPermission. + /// + /// In en, this message translates to: + /// **'Allow location access for Omi in Settings so this phone can find your device, then try again.'** + String get pairingLocationPermission; - /// No description provided for @firstCaptureCancelTitle. + /// No description provided for @pairingNotFoundOrDropped. /// /// In en, this message translates to: - /// **'Leave This Recording?'** - String get firstCaptureCancelTitle; + /// **'Couldn\'t reach your Omi. Make sure it\'s charged, close to your phone, and its light is blinking, then try again.'** + String get pairingNotFoundOrDropped; - /// No description provided for @firstCaptureCancelWarning. + /// No description provided for @pairingBondRecovery. /// /// In en, this message translates to: - /// **'Going back stops this recording. Keep talking to give Omi a few more sentences.'** - String get firstCaptureCancelWarning; + /// **'Forget Omi in your phone’s Bluetooth settings, then try pairing again.'** + String get pairingBondRecovery; - /// No description provided for @firstCaptureSiriTip. + /// No description provided for @pairingHowTo. /// /// In en, this message translates to: - /// **'Tip: Enable Omi in Shortcuts → Siri.'** - String get firstCaptureSiriTip; + /// **'How to pair'** + String get pairingHowTo; /// Headline of the Outline paywall variant (mobile paywall A/B test) /// diff --git a/app/lib/l10n/app_localizations_ar.dart b/app/lib/l10n/app_localizations_ar.dart index 9a70fdcc2f6..80633c5e370 100644 --- a/app/lib/l10n/app_localizations_ar.dart +++ b/app/lib/l10n/app_localizations_ar.dart @@ -1673,8 +1673,7 @@ class AppLocalizationsAr extends AppLocalizations { String get enableBluetooth => 'تمكين البلوتوث'; @override - String get bluetoothNeeded => - 'يحتاج Omi إلى البلوتوث للاتصال بجهازك القابل للارتداء. يرجى تمكين البلوتوث والمحاولة مرة أخرى.'; + String get bluetoothNeeded => 'يتصل Omi بهاتفك عبر البلوتوث. شغّله للمتابعة.'; @override String get contactSupport => 'الاتصال بالدعم؟'; @@ -4855,7 +4854,7 @@ class AppLocalizationsAr extends AppLocalizations { 'بالمتابعة، سيتم تخزين محادثاتك وتسجيلاتك ومعلوماتك الشخصية بشكل آمن على خوادمنا. تتم معالجة تسجيلاتك الصوتية ونصوصك بواسطة خدمات ذكاء اصطناعي تابعة لجهات خارجية (بما في ذلك Deepgram للنسخ و OpenAI للتحليل) لتزويدك برؤى مدعومة بالذكاء الاصطناعي وتمكين جميع ميزات التطبيق.'; @override - String get tasksEmptyStateMessage => 'ابدأ محادثة لإنشاء مهمة.'; + String get tasksEmptyStateMessage => 'ستظهر هنا المهام من محادثاتك.'; @override String get clearChatAction => 'مسح المحادثة'; @@ -12967,154 +12966,205 @@ class AppLocalizationsAr extends AppLocalizations { } @override - String get paywallRecommended => 'Recommended'; + String get onboardingPermissionTapToFix => 'اضغط للإصلاح'; @override - String get paywallNoPlans => 'Plans aren\'t available right now. Try again in a minute.'; + String get onboardingNoOmiYet => 'ليس لديك Omi بعد؟'; @override - String get paywallOtherPlans => 'See all plans'; + String get onboardingOmiOrdered => 'لقد طلبته'; @override - String get paywallComparePlans => 'See plans'; + String get onboardingOmiNotOrdered => 'لم أطلبه بعد'; @override - String get paywallYourPlan => 'Your plan'; + String get onboardingConnectOmi => 'اتصل بـ Omi'; @override - String get paywallNotNow => 'Not now'; + String get consentShortMessage => 'يستخدم Omi الذكاء الاصطناعي لتحويل محادثاتك إلى ملاحظات. يمكنك حذفها في أي وقت.'; @override - String get paywallTryAgain => 'Try again'; + String consentAiProviders(String providers) { + return 'تتم المعالجة بواسطة $providers.'; + } @override - String get paywallManageBilling => 'Manage billing'; + String get onboardingActIntro => 'لنجرّب Omi.'; @override - String get paywallLoadError => 'Couldn\'t load plans. Check your connection and try again.'; + String get onboardingActMicTitle => 'يحتاج Omi إلى الميكروفون'; @override - String get paywallPlans => 'Plans'; + String get onboardingActMicBody => 'ليسمعك Omi عندما تتحدث إليه.'; @override - String get firstCaptureTitle => 'How will you capture?'; + String get onboardingActMicAllow => 'السماح بالميكروفون'; @override - String get firstCaptureDescription => 'Choose how you want to capture your first conversation.'; + String get onboardingActDictateTitle => 'أخبر Omi عن يومك'; @override - String get firstCaptureOmi => 'I Have an Omi'; + String get onboardingActDictateBody => 'قلها بطريقتك، وOmi يتولى الباقي.'; @override - String get firstCapturePhone => 'This Phone'; + String get onboardingActDictateExample => + 'تغدّيت للتو مع سام، ستنتقل إلى لشبونة في يونيو! ذكّرني بإرسال العرض إلى فريقي قبل الجمعة، وبشراء الحليب في طريق العودة.'; @override - String get firstCaptureWatch => 'Apple Watch'; + String get onboardingActDictateHint => 'اضغط على علامة الصح عند الانتهاء'; @override - String get firstCaptureSkip => 'Skip for Now'; + String get onboardingActPhoneMic => 'ميكروفون الهاتف'; @override - String get firstCapturePrompt => - 'Tell Omi what you\'re working on today, or what\'s on your mind. Talk for about 30 seconds.'; + String get onboardingActWriting => 'جارٍ التلخيص…'; @override - String get firstCaptureKeepGoing => 'Keep going… 30 seconds is ideal.'; + String get onboardingActRemembers => 'يتذكّر Omi عنك حتى لا تضطر أنت'; @override - String get firstCaptureReady => 'You can finish now, or keep talking.'; + String get onboardingActNotesLabel => 'يومك، باختصار'; @override - String get firstCaptureStart => 'Start Recording'; + String get onboardingActTasksLabel => 'التقط Omi مهامك'; @override - String get firstCaptureFinish => 'Finish Recording'; + String get onboardingActYourWords => 'هذا ما قلته'; @override - String get firstCaptureListening => 'Listening… your words will appear here.'; + String onboardingActCongratsTitle(String name) { + return 'كل شيء جاهز يا $name'; + } @override - String get firstCaptureProcessing => 'Creating your first conversation…'; + String get onboardingActRateTitle => 'هل يعجبك Omi حتى الآن؟'; @override - String get firstCaptureResult => 'Your first conversation'; + String get onboardingActRateYes => 'قيّم Omi'; @override - String get firstCaptureEmpty => 'I didn\'t catch enough speech, try again with a few sentences'; + String get onboardingWelcomeCardTitle => 'مرحبًا بك في Omi'; @override - String get watchCaptureTitle => 'Record from your Apple Watch'; + String get onboardingWelcomeCardBody => 'ارتدِ Omi وتحدّث كعادتك. تتحول كل محادثة إلى ملاحظات ومهام وذكريات هنا.'; @override - String get watchCaptureOpen => 'Open Omi on your Apple Watch.'; + String get onboardingActDemoTitle => 'غداء مع سام'; @override - String get watchCaptureRecord => 'Tap Record. Allow microphone access if asked.'; + String get onboardingActDemoSummary => + 'غداء مع سام التي ستنتقل إلى لشبونة في يونيو. تريد إرسال العرض التقديمي إلى فريقك بحلول الجمعة وشراء الحليب في طريق العودة.'; @override - String get watchCaptureSync => 'Your recording syncs to Omi on this phone.'; + String get onboardingActDemoTask1 => 'إرسال العرض إلى الفريق قبل الجمعة'; @override - String get pairingBluetoothOff => 'Turn on Bluetooth on your phone, then try again.'; + String get onboardingActDemoTask2 => 'شراء الحليب في طريق العودة'; @override - String get pairingBluetoothPermission => 'Allow Bluetooth access for Omi in Settings, then try again.'; + String get onboardingActJustNow => 'الآن'; @override - String get pairingNearbyPermission => 'Allow Nearby devices access for Omi in Settings, then try again.'; + String get onboardingActSavedToTasks => 'تم الحفظ في مهامك'; @override - String get pairingLocationPermission => - 'Allow location access for Omi in Settings so this phone can find your device, then try again.'; + String get onboardingConnectTapToConnect => 'اضغط على Omi للاتصال.'; @override - String get pairingNotFoundOrDropped => - 'Couldn\'t reach your Omi. Make sure it\'s charged, close to your phone, and its light is blinking, then try again.'; + String get onboardingActMicFallbackBody => 'لا يرسل Omi الصوت بعد. استخدم هاتفك الآن.'; @override - String get pairingBondRecovery => 'Forget Omi in your phone’s Bluetooth settings, then try pairing again.'; + String get onboardingActNoAudioOmi => 'لا أسمع Omi. تأكد من أنه يعمل وقريب منك.'; @override - String get pairingHowTo => 'How to pair'; + String get onboardingActNoAudioPhone => 'لا أسمعك بعد. جرّب التحدث بصوت أعلى قليلاً.'; + + @override + String get onboardingActDictateBodyOmi => 'تحدّث فقط. Omi يستمع.'; + + @override + String get voiceProfileCardDismissed => 'يمكنك إعداد بصمة صوتك في أي وقت من الإعدادات.'; + + @override + String get homeNewRecordingButton => 'تسجيل جديد'; + + @override + String get chatWelcomeMessage => + 'مرحبًا، أنا Omi. اسألني عن أي شيء تحدثت عنه: ما قاله شخص ما، أو ما وعدت به، أو ملخص يومك. يمكنني أيضًا إضافة مهام وتذكّر الأشياء لك.'; + + @override + String get onboardingStarterTaskAsk => 'اسأل Omi عن يومك'; + + @override + String get onboardingStarterTaskRecord => 'سجّل محادثة مع Omi'; + + @override + String get onboardingStarterTaskCheck => 'ضع علامة على هذه المهمة لإنهائها'; + + @override + String get paywallRecommended => 'Recommended'; @override - String get firstCaptureWatchInstructions => - 'Open Omi on your Apple Watch and tap Record. Allow microphone access if asked. Your capture will sync to Omi on this phone.'; + String get paywallNoPlans => 'Plans aren\'t available right now. Try again in a minute.'; @override - String get firstCaptureWatchInstall => - 'In the Watch app on your iPhone, install Omi on your Apple Watch. Then open Omi on your watch and tap Record.'; + String get paywallOtherPlans => 'See all plans'; @override - String get firstCaptureWatchPair => - 'Pair your Apple Watch with this iPhone using the Watch app, then install and open Omi on your watch.'; + String get paywallComparePlans => 'See plans'; @override - String get firstCaptureUnavailable => - 'Recording could not start. Check microphone access and your connection, then try again.'; + String get paywallYourPlan => 'Your plan'; @override - String get firstCaptureDelayed => - 'Your recording is taking longer to process. You can check your conversations on Home or try again.'; + String get paywallNotNow => 'Not now'; @override - String get firstCaptureEmptyTitle => 'Let\'s try that again'; + String get paywallTryAgain => 'Try again'; @override - String get firstCaptureShortTitle => 'Finish This Recording?'; + String get paywallManageBilling => 'Manage billing'; @override - String get firstCaptureShortWarning => 'A few more sentences help Omi understand you. About 30 seconds is ideal.'; + String get paywallLoadError => 'Couldn\'t load plans. Check your connection and try again.'; @override - String get firstCaptureCancelTitle => 'Leave This Recording?'; + String get paywallPlans => 'Plans'; @override - String get firstCaptureCancelWarning => - 'Going back stops this recording. Keep talking to give Omi a few more sentences.'; + String get watchCaptureTitle => 'Record from your Apple Watch'; @override - String get firstCaptureSiriTip => 'Tip: Enable Omi in Shortcuts → Siri.'; + String get watchCaptureOpen => 'Open Omi on your Apple Watch.'; + + @override + String get watchCaptureRecord => 'Tap Record. Allow microphone access if asked.'; + + @override + String get watchCaptureSync => 'Your recording syncs to Omi on this phone.'; + + @override + String get pairingBluetoothOff => 'Turn on Bluetooth on your phone, then try again.'; + + @override + String get pairingBluetoothPermission => 'Allow Bluetooth access for Omi in Settings, then try again.'; + + @override + String get pairingNearbyPermission => 'Allow Nearby devices access for Omi in Settings, then try again.'; + + @override + String get pairingLocationPermission => + 'Allow location access for Omi in Settings so this phone can find your device, then try again.'; + + @override + String get pairingNotFoundOrDropped => + 'Couldn\'t reach your Omi. Make sure it\'s charged, close to your phone, and its light is blinking, then try again.'; + + @override + String get pairingBondRecovery => 'Forget Omi in your phone’s Bluetooth settings, then try pairing again.'; + + @override + String get pairingHowTo => 'How to pair'; @override String get paywallOutlineHeadline => 'لا تفوّت كلمة واحدة بعد الآن.'; diff --git a/app/lib/l10n/app_localizations_be.dart b/app/lib/l10n/app_localizations_be.dart index 49043da3c9c..b084142368e 100644 --- a/app/lib/l10n/app_localizations_be.dart +++ b/app/lib/l10n/app_localizations_be.dart @@ -1688,8 +1688,7 @@ class AppLocalizationsBe extends AppLocalizations { String get enableBluetooth => 'Уключыць Bluetooth'; @override - String get bluetoothNeeded => - 'Omi трэба Bluetooth для падлучэння да вашага нашпігальнага прыстроя. Калі ласка, уключыце Bluetooth і спробуйце яшчэ раз.'; + String get bluetoothNeeded => 'Omi падключаецца да тэлефона праз Bluetooth. Уключыце яго, каб працягнуць.'; @override String get contactSupport => 'Звярнуцца ў тэхподтрымку?'; @@ -4905,7 +4904,7 @@ class AppLocalizationsBe extends AppLocalizations { 'Працягваючы, вашы размовы, запісы і асабістая інфармацыя будуць надзейна захоўвацца на нашых серверах. Вашы аўдыязапісы і транскрыпцыі апрацоўваюцца староннімі сэрвісамі ШІ (уключаючы Deepgram для транскрыпцыі і OpenAI для аналізу), каб забяспечыць вас аналітыкай на аснове ШІ і ўключыць усе функцыі праграмы.'; @override - String get tasksEmptyStateMessage => 'Пачніце размову, каб стварыць задачу.'; + String get tasksEmptyStateMessage => 'Справы з вашых размоў з\'явяцца тут.'; @override String get clearChatAction => 'Очыстіць Чат'; @@ -13075,154 +13074,207 @@ class AppLocalizationsBe extends AppLocalizations { } @override - String get paywallRecommended => 'Recommended'; + String get onboardingPermissionTapToFix => 'Націсніце, каб выправіць'; @override - String get paywallNoPlans => 'Plans aren\'t available right now. Try again in a minute.'; + String get onboardingNoOmiYet => 'Яшчэ няма Omi?'; @override - String get paywallOtherPlans => 'See all plans'; + String get onboardingOmiOrdered => 'Я ўжо замовіў'; @override - String get paywallComparePlans => 'See plans'; + String get onboardingOmiNotOrdered => 'Яшчэ не замовіў'; @override - String get paywallYourPlan => 'Your plan'; + String get onboardingConnectOmi => 'Падключыце Omi'; @override - String get paywallNotNow => 'Not now'; + String get consentShortMessage => + 'Omi з дапамогай ШІ ператварае вашы размовы ў нататкі. Іх можна выдаліць у любы момант.'; @override - String get paywallTryAgain => 'Try again'; + String consentAiProviders(String providers) { + return 'Апрацоўка: $providers.'; + } @override - String get paywallManageBilling => 'Manage billing'; + String get onboardingActIntro => 'Паспрабуем Omi.'; @override - String get paywallLoadError => 'Couldn\'t load plans. Check your connection and try again.'; + String get onboardingActMicTitle => 'Omi патрэбны мікрафон'; @override - String get paywallPlans => 'Plans'; + String get onboardingActMicBody => 'Каб Omi чуў вас, калі вы з ім размаўляеце.'; @override - String get firstCaptureTitle => 'How will you capture?'; + String get onboardingActMicAllow => 'Дазволіць мікрафон'; @override - String get firstCaptureDescription => 'Choose how you want to capture your first conversation.'; + String get onboardingActDictateTitle => 'Раскажыце Omi пра свой дзень'; @override - String get firstCaptureOmi => 'I Have an Omi'; + String get onboardingActDictateBody => 'Кажыце па-свойму. Астатняе зробіць Omi.'; @override - String get firstCapturePhone => 'This Phone'; + String get onboardingActDictateExample => + 'Толькі што паабедаў з Сэм, яна пераязджае ў Лісабон у чэрвені! Нагадай мне адправіць прэзентацыю камандзе да пятніцы і купіць малако па дарозе дадому.'; @override - String get firstCaptureWatch => 'Apple Watch'; + String get onboardingActDictateHint => 'Націсніце галачку, калі скончыце'; @override - String get firstCaptureSkip => 'Skip for Now'; + String get onboardingActPhoneMic => 'Мікрафон тэлефона'; @override - String get firstCapturePrompt => - 'Tell Omi what you\'re working on today, or what\'s on your mind. Talk for about 30 seconds.'; + String get onboardingActWriting => 'Падсумоўваем…'; @override - String get firstCaptureKeepGoing => 'Keep going… 30 seconds is ideal.'; + String get onboardingActRemembers => 'Omi памятае, каб вам не трэба было'; @override - String get firstCaptureReady => 'You can finish now, or keep talking.'; + String get onboardingActNotesLabel => 'Ваш дзень коратка'; @override - String get firstCaptureStart => 'Start Recording'; + String get onboardingActTasksLabel => 'Omi заўважыў вашы справы'; @override - String get firstCaptureFinish => 'Finish Recording'; + String get onboardingActYourWords => 'Вось што вы сказалі'; @override - String get firstCaptureListening => 'Listening… your words will appear here.'; + String onboardingActCongratsTitle(String name) { + return 'Усё гатова, $name'; + } @override - String get firstCaptureProcessing => 'Creating your first conversation…'; + String get onboardingActRateTitle => 'Як вам Omi?'; @override - String get firstCaptureResult => 'Your first conversation'; + String get onboardingActRateYes => 'Ацаніць Omi'; @override - String get firstCaptureEmpty => 'I didn\'t catch enough speech, try again with a few sentences'; + String get onboardingWelcomeCardTitle => 'Вітаем у Omi'; @override - String get watchCaptureTitle => 'Record from your Apple Watch'; + String get onboardingWelcomeCardBody => + 'Насіце Omi і размаўляйце як звычайна. Кожная размова ператворыцца тут у нататкі, задачы і ўспаміны.'; @override - String get watchCaptureOpen => 'Open Omi on your Apple Watch.'; + String get onboardingActDemoTitle => 'Абед з Сэм'; @override - String get watchCaptureRecord => 'Tap Record. Allow microphone access if asked.'; + String get onboardingActDemoSummary => + 'Абед з Сэм, якая ў чэрвені пераязджае ў Лісабон. Вы хочаце адправіць прэзентацыю камандзе да пятніцы і купіць малако па дарозе дадому.'; @override - String get watchCaptureSync => 'Your recording syncs to Omi on this phone.'; + String get onboardingActDemoTask1 => 'Адправіць прэзентацыю камандзе да пятніцы'; @override - String get pairingBluetoothOff => 'Turn on Bluetooth on your phone, then try again.'; + String get onboardingActDemoTask2 => 'Купіць малако па дарозе дадому'; @override - String get pairingBluetoothPermission => 'Allow Bluetooth access for Omi in Settings, then try again.'; + String get onboardingActJustNow => 'Толькі што'; @override - String get pairingNearbyPermission => 'Allow Nearby devices access for Omi in Settings, then try again.'; + String get onboardingActSavedToTasks => 'Захавана ў вашыя справы'; @override - String get pairingLocationPermission => - 'Allow location access for Omi in Settings so this phone can find your device, then try again.'; + String get onboardingConnectTapToConnect => 'Націсніце на Omi, каб падключыць.'; @override - String get pairingNotFoundOrDropped => - 'Couldn\'t reach your Omi. Make sure it\'s charged, close to your phone, and its light is blinking, then try again.'; + String get onboardingActMicFallbackBody => 'Omi яшчэ не перадае гук. Пакуль скарыстайцеся тэлефонам.'; @override - String get pairingBondRecovery => 'Forget Omi in your phone’s Bluetooth settings, then try pairing again.'; + String get onboardingActNoAudioOmi => 'Не чуваць Omi. Пераканайцеся, што ён уключаны і побач.'; @override - String get pairingHowTo => 'How to pair'; + String get onboardingActNoAudioPhone => 'Пакуль вас не чуваць. Паспрабуйце гаварыць крыху гучней.'; + + @override + String get onboardingActDictateBodyOmi => 'Проста гаварыце. Omi слухае.'; + + @override + String get voiceProfileCardDismissed => 'Профіль голасу заўсёды можна наладзіць у Наладах.'; + + @override + String get homeNewRecordingButton => 'Новы запіс'; + + @override + String get chatWelcomeMessage => + 'Прывітанне, я Omi. Пытайцеся пра ўсё, пра што вы размаўлялі: што хтосьці сказаў, што вы паабяцалі, ці як прайшоў ваш дзень. Я таксама магу дадаваць задачы і запамінаць рэчы за вас.'; + + @override + String get onboardingStarterTaskAsk => 'Спытайце Omi пра свой дзень'; + + @override + String get onboardingStarterTaskRecord => 'Запішыце размову з Omi'; + + @override + String get onboardingStarterTaskCheck => 'Адзначце гэтую задачу, каб завяршыць яе'; + + @override + String get paywallRecommended => 'Recommended'; @override - String get firstCaptureWatchInstructions => - 'Open Omi on your Apple Watch and tap Record. Allow microphone access if asked. Your capture will sync to Omi on this phone.'; + String get paywallNoPlans => 'Plans aren\'t available right now. Try again in a minute.'; @override - String get firstCaptureWatchInstall => - 'In the Watch app on your iPhone, install Omi on your Apple Watch. Then open Omi on your watch and tap Record.'; + String get paywallOtherPlans => 'See all plans'; @override - String get firstCaptureWatchPair => - 'Pair your Apple Watch with this iPhone using the Watch app, then install and open Omi on your watch.'; + String get paywallComparePlans => 'See plans'; @override - String get firstCaptureUnavailable => - 'Recording could not start. Check microphone access and your connection, then try again.'; + String get paywallYourPlan => 'Your plan'; @override - String get firstCaptureDelayed => - 'Your recording is taking longer to process. You can check your conversations on Home or try again.'; + String get paywallNotNow => 'Not now'; @override - String get firstCaptureEmptyTitle => 'Let\'s try that again'; + String get paywallTryAgain => 'Try again'; @override - String get firstCaptureShortTitle => 'Finish This Recording?'; + String get paywallManageBilling => 'Manage billing'; @override - String get firstCaptureShortWarning => 'A few more sentences help Omi understand you. About 30 seconds is ideal.'; + String get paywallLoadError => 'Couldn\'t load plans. Check your connection and try again.'; @override - String get firstCaptureCancelTitle => 'Leave This Recording?'; + String get paywallPlans => 'Plans'; @override - String get firstCaptureCancelWarning => - 'Going back stops this recording. Keep talking to give Omi a few more sentences.'; + String get watchCaptureTitle => 'Record from your Apple Watch'; @override - String get firstCaptureSiriTip => 'Tip: Enable Omi in Shortcuts → Siri.'; + String get watchCaptureOpen => 'Open Omi on your Apple Watch.'; + + @override + String get watchCaptureRecord => 'Tap Record. Allow microphone access if asked.'; + + @override + String get watchCaptureSync => 'Your recording syncs to Omi on this phone.'; + + @override + String get pairingBluetoothOff => 'Turn on Bluetooth on your phone, then try again.'; + + @override + String get pairingBluetoothPermission => 'Allow Bluetooth access for Omi in Settings, then try again.'; + + @override + String get pairingNearbyPermission => 'Allow Nearby devices access for Omi in Settings, then try again.'; + + @override + String get pairingLocationPermission => + 'Allow location access for Omi in Settings so this phone can find your device, then try again.'; + + @override + String get pairingNotFoundOrDropped => + 'Couldn\'t reach your Omi. Make sure it\'s charged, close to your phone, and its light is blinking, then try again.'; + + @override + String get pairingBondRecovery => 'Forget Omi in your phone’s Bluetooth settings, then try pairing again.'; + + @override + String get pairingHowTo => 'How to pair'; @override String get paywallOutlineHeadline => 'Больш не прапусціце ніводнага слова.'; diff --git a/app/lib/l10n/app_localizations_bg.dart b/app/lib/l10n/app_localizations_bg.dart index 483eedc78f0..dbf5cdd4a0b 100644 --- a/app/lib/l10n/app_localizations_bg.dart +++ b/app/lib/l10n/app_localizations_bg.dart @@ -1690,8 +1690,7 @@ class AppLocalizationsBg extends AppLocalizations { String get enableBluetooth => 'Активирай Bluetooth'; @override - String get bluetoothNeeded => - 'Omi се нуждае от Bluetooth, за да се свърже с вашето носимо устройство. Моля, активирайте Bluetooth и опитайте отново.'; + String get bluetoothNeeded => 'Omi се свързва с телефона ви чрез Bluetooth. Включете го, за да продължите.'; @override String get contactSupport => 'Свържете се с поддръжката?'; @@ -4910,7 +4909,7 @@ class AppLocalizationsBg extends AppLocalizations { 'С продължаването вашите разговори, записи и лична информация ще бъдат сигурно съхранени на нашите сървъри. Вашите аудиозаписи и транскрипции се обработват от AI услуги на трети страни (включително Deepgram за транскрипция и OpenAI за анализ), за да ви предоставят прозрения, базирани на AI, и да активират всички функции на приложението.'; @override - String get tasksEmptyStateMessage => 'Започнете разговор, за да създадете задача.'; + String get tasksEmptyStateMessage => 'Задачите от разговорите ви ще се появят тук.'; @override String get clearChatAction => 'Изчисти чата'; @@ -13088,154 +13087,207 @@ class AppLocalizationsBg extends AppLocalizations { } @override - String get paywallRecommended => 'Recommended'; + String get onboardingPermissionTapToFix => 'Докоснете, за да поправите'; @override - String get paywallNoPlans => 'Plans aren\'t available right now. Try again in a minute.'; + String get onboardingNoOmiYet => 'Все още нямате Omi?'; @override - String get paywallOtherPlans => 'See all plans'; + String get onboardingOmiOrdered => 'Поръчал съм го'; @override - String get paywallComparePlans => 'See plans'; + String get onboardingOmiNotOrdered => 'Още не съм поръчал'; @override - String get paywallYourPlan => 'Your plan'; + String get onboardingConnectOmi => 'Свържете Omi'; @override - String get paywallNotNow => 'Not now'; + String get consentShortMessage => + 'Omi използва ИИ, за да превърне разговорите ви в бележки. Можете да ги изтриете по всяко време.'; @override - String get paywallTryAgain => 'Try again'; + String consentAiProviders(String providers) { + return 'Обработва се с $providers.'; + } @override - String get paywallManageBilling => 'Manage billing'; + String get onboardingActIntro => 'Да пробваме Omi.'; @override - String get paywallLoadError => 'Couldn\'t load plans. Check your connection and try again.'; + String get onboardingActMicTitle => 'Omi се нуждае от микрофона ви'; @override - String get paywallPlans => 'Plans'; + String get onboardingActMicBody => 'За да ви чува Omi, когато говорите с него.'; @override - String get firstCaptureTitle => 'How will you capture?'; + String get onboardingActMicAllow => 'Разреши микрофона'; @override - String get firstCaptureDescription => 'Choose how you want to capture your first conversation.'; + String get onboardingActDictateTitle => 'Разкажете на Omi за деня си'; @override - String get firstCaptureOmi => 'I Have an Omi'; + String get onboardingActDictateBody => 'Кажете го по своя начин. Omi ще се погрижи за останалото.'; @override - String get firstCapturePhone => 'This Phone'; + String get onboardingActDictateExample => + 'Току-що обядвах със Сам, мести се в Лисабон през юни! Напомни ми да пратя презентацията на екипа до петък и да купя мляко на връщане.'; @override - String get firstCaptureWatch => 'Apple Watch'; + String get onboardingActDictateHint => 'Докоснете отметката, когато сте готови'; @override - String get firstCaptureSkip => 'Skip for Now'; + String get onboardingActPhoneMic => 'Микрофон на телефона'; @override - String get firstCapturePrompt => - 'Tell Omi what you\'re working on today, or what\'s on your mind. Talk for about 30 seconds.'; + String get onboardingActWriting => 'Обобщаваме…'; @override - String get firstCaptureKeepGoing => 'Keep going… 30 seconds is ideal.'; + String get onboardingActRemembers => 'Omi помни вместо вас'; @override - String get firstCaptureReady => 'You can finish now, or keep talking.'; + String get onboardingActNotesLabel => 'Денят ви накратко'; @override - String get firstCaptureStart => 'Start Recording'; + String get onboardingActTasksLabel => 'Omi улови задачите ви'; @override - String get firstCaptureFinish => 'Finish Recording'; + String get onboardingActYourWords => 'Ето какво казахте'; @override - String get firstCaptureListening => 'Listening… your words will appear here.'; + String onboardingActCongratsTitle(String name) { + return 'Всичко е готово, $name'; + } @override - String get firstCaptureProcessing => 'Creating your first conversation…'; + String get onboardingActRateTitle => 'Харесва ли ви Omi досега?'; @override - String get firstCaptureResult => 'Your first conversation'; + String get onboardingActRateYes => 'Оценете Omi'; @override - String get firstCaptureEmpty => 'I didn\'t catch enough speech, try again with a few sentences'; + String get onboardingWelcomeCardTitle => 'Добре дошли в Omi'; @override - String get watchCaptureTitle => 'Record from your Apple Watch'; + String get onboardingWelcomeCardBody => + 'Носете Omi и говорете както обикновено. Всеки разговор се превръща тук в бележки, задачи и спомени.'; @override - String get watchCaptureOpen => 'Open Omi on your Apple Watch.'; + String get onboardingActDemoTitle => 'Обяд със Сам'; @override - String get watchCaptureRecord => 'Tap Record. Allow microphone access if asked.'; + String get onboardingActDemoSummary => + 'Обяд със Сам, която се мести в Лисабон през юни. Искате да изпратите презентацията на екипа до петък и да вземете мляко на път за вкъщи.'; @override - String get watchCaptureSync => 'Your recording syncs to Omi on this phone.'; + String get onboardingActDemoTask1 => 'Да пратя презентацията на екипа до петък'; @override - String get pairingBluetoothOff => 'Turn on Bluetooth on your phone, then try again.'; + String get onboardingActDemoTask2 => 'Да купя мляко на връщане'; @override - String get pairingBluetoothPermission => 'Allow Bluetooth access for Omi in Settings, then try again.'; + String get onboardingActJustNow => 'Току-що'; @override - String get pairingNearbyPermission => 'Allow Nearby devices access for Omi in Settings, then try again.'; + String get onboardingActSavedToTasks => 'Запазено в задачите ви'; @override - String get pairingLocationPermission => - 'Allow location access for Omi in Settings so this phone can find your device, then try again.'; + String get onboardingConnectTapToConnect => 'Докоснете Omi, за да се свържете.'; @override - String get pairingNotFoundOrDropped => - 'Couldn\'t reach your Omi. Make sure it\'s charged, close to your phone, and its light is blinking, then try again.'; + String get onboardingActMicFallbackBody => 'Omi все още не изпраща звук. Засега използвайте телефона си.'; @override - String get pairingBondRecovery => 'Forget Omi in your phone’s Bluetooth settings, then try pairing again.'; + String get onboardingActNoAudioOmi => 'Не чуваме Omi. Уверете се, че е включен и наблизо.'; @override - String get pairingHowTo => 'How to pair'; + String get onboardingActNoAudioPhone => 'Още не ви чуваме. Опитайте да говорите малко по-силно.'; + + @override + String get onboardingActDictateBodyOmi => 'Просто говорете. Omi слуша.'; + + @override + String get voiceProfileCardDismissed => 'Можете да настроите гласовия профил по всяко време от Настройки.'; + + @override + String get homeNewRecordingButton => 'Нов запис'; + + @override + String get chatWelcomeMessage => + 'Здравей, аз съм Omi. Питай ме за всичко, за което си говорил: какво е казал някой, какво си обещал или обобщение на деня ти. Мога и да добавям задачи и да запомням неща вместо теб.'; + + @override + String get onboardingStarterTaskAsk => 'Попитай Omi за деня си'; + + @override + String get onboardingStarterTaskRecord => 'Запиши разговор с Omi'; + + @override + String get onboardingStarterTaskCheck => 'Отметни тази задача, за да я завършиш'; + + @override + String get paywallRecommended => 'Recommended'; @override - String get firstCaptureWatchInstructions => - 'Open Omi on your Apple Watch and tap Record. Allow microphone access if asked. Your capture will sync to Omi on this phone.'; + String get paywallNoPlans => 'Plans aren\'t available right now. Try again in a minute.'; @override - String get firstCaptureWatchInstall => - 'In the Watch app on your iPhone, install Omi on your Apple Watch. Then open Omi on your watch and tap Record.'; + String get paywallOtherPlans => 'See all plans'; @override - String get firstCaptureWatchPair => - 'Pair your Apple Watch with this iPhone using the Watch app, then install and open Omi on your watch.'; + String get paywallComparePlans => 'See plans'; @override - String get firstCaptureUnavailable => - 'Recording could not start. Check microphone access and your connection, then try again.'; + String get paywallYourPlan => 'Your plan'; @override - String get firstCaptureDelayed => - 'Your recording is taking longer to process. You can check your conversations on Home or try again.'; + String get paywallNotNow => 'Not now'; @override - String get firstCaptureEmptyTitle => 'Let\'s try that again'; + String get paywallTryAgain => 'Try again'; @override - String get firstCaptureShortTitle => 'Finish This Recording?'; + String get paywallManageBilling => 'Manage billing'; @override - String get firstCaptureShortWarning => 'A few more sentences help Omi understand you. About 30 seconds is ideal.'; + String get paywallLoadError => 'Couldn\'t load plans. Check your connection and try again.'; @override - String get firstCaptureCancelTitle => 'Leave This Recording?'; + String get paywallPlans => 'Plans'; @override - String get firstCaptureCancelWarning => - 'Going back stops this recording. Keep talking to give Omi a few more sentences.'; + String get watchCaptureTitle => 'Record from your Apple Watch'; @override - String get firstCaptureSiriTip => 'Tip: Enable Omi in Shortcuts → Siri.'; + String get watchCaptureOpen => 'Open Omi on your Apple Watch.'; + + @override + String get watchCaptureRecord => 'Tap Record. Allow microphone access if asked.'; + + @override + String get watchCaptureSync => 'Your recording syncs to Omi on this phone.'; + + @override + String get pairingBluetoothOff => 'Turn on Bluetooth on your phone, then try again.'; + + @override + String get pairingBluetoothPermission => 'Allow Bluetooth access for Omi in Settings, then try again.'; + + @override + String get pairingNearbyPermission => 'Allow Nearby devices access for Omi in Settings, then try again.'; + + @override + String get pairingLocationPermission => + 'Allow location access for Omi in Settings so this phone can find your device, then try again.'; + + @override + String get pairingNotFoundOrDropped => + 'Couldn\'t reach your Omi. Make sure it\'s charged, close to your phone, and its light is blinking, then try again.'; + + @override + String get pairingBondRecovery => 'Forget Omi in your phone’s Bluetooth settings, then try pairing again.'; + + @override + String get pairingHowTo => 'How to pair'; @override String get paywallOutlineHeadline => 'Никога повече не пропускайте дума.'; diff --git a/app/lib/l10n/app_localizations_bn.dart b/app/lib/l10n/app_localizations_bn.dart index d4de13ed8af..7ee6355e13b 100644 --- a/app/lib/l10n/app_localizations_bn.dart +++ b/app/lib/l10n/app_localizations_bn.dart @@ -1684,8 +1684,7 @@ class AppLocalizationsBn extends AppLocalizations { String get enableBluetooth => 'Bluetooth সক্ষম করুন'; @override - String get bluetoothNeeded => - 'আপনার পরিধানযোগ্যের সাথে সংযোগ করতে Omi এর Bluetooth দরকার। Bluetooth সক্ষম করুন এবং আবার চেষ্টা করুন।'; + String get bluetoothNeeded => 'Omi আপনার ফোনের সাথে Bluetooth-এ সংযুক্ত হয়। চালিয়ে যেতে এটি চালু করুন।'; @override String get contactSupport => 'সাপোর্টের সাথে যোগাযোগ করবেন?'; @@ -4899,7 +4898,7 @@ class AppLocalizationsBn extends AppLocalizations { 'চালিয়ে যাওয়ার মাধ্যমে, আপনার কথোপকথন, রেকর্ডিং এবং ব্যক্তিগত তথ্য আমাদের সার্ভারে নিরাপদে সংরক্ষণ করা হবে। আপনার অডিও রেকর্ডিং এবং ট্রান্সক্রিপ্ট তৃতীয় পক্ষের AI পরিষেবা দ্বারা প্রক্রিয়া করা হয় (ট্রান্সক্রিপশনের জন্য Deepgram এবং বিশ্লেষণের জন্য OpenAI সহ) যাতে আপনাকে AI-চালিত অন্তর্দৃষ্টি প্রদান করা যায় এবং সমস্ত অ্যাপ বৈশিষ্ট্য সক্ষম করা যায়।'; @override - String get tasksEmptyStateMessage => 'একটি কাজ তৈরি করতে কথোপকথন শুরু করুন।'; + String get tasksEmptyStateMessage => 'আপনার কথোপকথনের করণীয়গুলো এখানে দেখা যাবে।'; @override String get clearChatAction => 'চ্যাট সাফ করুন'; @@ -13037,154 +13036,206 @@ class AppLocalizationsBn extends AppLocalizations { } @override - String get paywallRecommended => 'Recommended'; + String get onboardingPermissionTapToFix => 'ঠিক করতে ট্যাপ করুন'; @override - String get paywallNoPlans => 'Plans aren\'t available right now. Try again in a minute.'; + String get onboardingNoOmiYet => 'এখনও Omi নেই?'; @override - String get paywallOtherPlans => 'See all plans'; + String get onboardingOmiOrdered => 'আমি অর্ডার করেছি'; @override - String get paywallComparePlans => 'See plans'; + String get onboardingOmiNotOrdered => 'এখনও অর্ডার করিনি'; @override - String get paywallYourPlan => 'Your plan'; + String get onboardingConnectOmi => 'Omi সংযুক্ত করুন'; @override - String get paywallNotNow => 'Not now'; + String get consentShortMessage => 'Omi AI ব্যবহার করে আপনার কথোপকথনকে নোটে পরিণত করে। যেকোনো সময় মুছে ফেলতে পারেন।'; @override - String get paywallTryAgain => 'Try again'; + String consentAiProviders(String providers) { + return '$providers দিয়ে প্রক্রিয়া করা হয়।'; + } @override - String get paywallManageBilling => 'Manage billing'; + String get onboardingActIntro => 'চলুন Omi চেষ্টা করি।'; @override - String get paywallLoadError => 'Couldn\'t load plans. Check your connection and try again.'; + String get onboardingActMicTitle => 'Omi-এর আপনার মাইক্রোফোন দরকার'; @override - String get paywallPlans => 'Plans'; + String get onboardingActMicBody => 'যাতে আপনি কথা বললে Omi শুনতে পায়।'; @override - String get firstCaptureTitle => 'How will you capture?'; + String get onboardingActMicAllow => 'মাইক্রোফোন অনুমতি দিন'; @override - String get firstCaptureDescription => 'Choose how you want to capture your first conversation.'; + String get onboardingActDictateTitle => 'Omi-কে আপনার দিনের কথা বলুন'; @override - String get firstCaptureOmi => 'I Have an Omi'; + String get onboardingActDictateBody => 'নিজের মতো করে বলুন। বাকিটা Omi সামলাবে।'; @override - String get firstCapturePhone => 'This Phone'; + String get onboardingActDictateExample => + 'এইমাত্র স্যামের সাথে লাঞ্চ করলাম, ও জুনে লিসবনে চলে যাচ্ছে! মনে করিয়ে দিও শুক্রবারের মধ্যে টিমকে ডেকটা পাঠাতে, আর ফেরার পথে দুধ কিনতে।'; @override - String get firstCaptureWatch => 'Apple Watch'; + String get onboardingActDictateHint => 'শেষ হলে টিক চিহ্নে ট্যাপ করুন'; @override - String get firstCaptureSkip => 'Skip for Now'; + String get onboardingActPhoneMic => 'ফোনের মাইক্রোফোন'; @override - String get firstCapturePrompt => - 'Tell Omi what you\'re working on today, or what\'s on your mind. Talk for about 30 seconds.'; + String get onboardingActWriting => 'সারাংশ তৈরি হচ্ছে…'; @override - String get firstCaptureKeepGoing => 'Keep going… 30 seconds is ideal.'; + String get onboardingActRemembers => 'Omi মনে রাখে, যাতে আপনাকে রাখতে না হয়'; @override - String get firstCaptureReady => 'You can finish now, or keep talking.'; + String get onboardingActNotesLabel => 'আপনার দিন, সংক্ষেপে'; @override - String get firstCaptureStart => 'Start Recording'; + String get onboardingActTasksLabel => 'Omi আপনার কাজগুলো ধরে ফেলেছে'; @override - String get firstCaptureFinish => 'Finish Recording'; + String get onboardingActYourWords => 'আপনি যা বললেন'; @override - String get firstCaptureListening => 'Listening… your words will appear here.'; + String onboardingActCongratsTitle(String name) { + return 'সব তৈরি, $name'; + } @override - String get firstCaptureProcessing => 'Creating your first conversation…'; + String get onboardingActRateTitle => 'এখন পর্যন্ত Omi কেমন লাগছে?'; @override - String get firstCaptureResult => 'Your first conversation'; + String get onboardingActRateYes => 'Omi-কে রেটিং দিন'; @override - String get firstCaptureEmpty => 'I didn\'t catch enough speech, try again with a few sentences'; + String get onboardingWelcomeCardTitle => 'Omi-তে স্বাগতম'; @override - String get watchCaptureTitle => 'Record from your Apple Watch'; + String get onboardingWelcomeCardBody => + 'Omi পরে স্বাভাবিকভাবে কথা বলুন। প্রতিটি কথোপকথন এখানেই নোট, কাজ আর স্মৃতিতে পরিণত হবে।'; @override - String get watchCaptureOpen => 'Open Omi on your Apple Watch.'; + String get onboardingActDemoTitle => 'স্যামের সাথে লাঞ্চ'; @override - String get watchCaptureRecord => 'Tap Record. Allow microphone access if asked.'; + String get onboardingActDemoSummary => + 'স্যামের সঙ্গে দুপুরের খাবার, যিনি জুনে লিসবনে চলে যাচ্ছেন। শুক্রবারের মধ্যে টিমকে ডেক পাঠাতে আর বাড়ি ফেরার পথে দুধ কিনতে হবে।'; @override - String get watchCaptureSync => 'Your recording syncs to Omi on this phone.'; + String get onboardingActDemoTask1 => 'শুক্রবারের মধ্যে টিমকে ডেক পাঠানো'; @override - String get pairingBluetoothOff => 'Turn on Bluetooth on your phone, then try again.'; + String get onboardingActDemoTask2 => 'ফেরার পথে দুধ কেনা'; @override - String get pairingBluetoothPermission => 'Allow Bluetooth access for Omi in Settings, then try again.'; + String get onboardingActJustNow => 'এইমাত্র'; @override - String get pairingNearbyPermission => 'Allow Nearby devices access for Omi in Settings, then try again.'; + String get onboardingActSavedToTasks => 'আপনার কাজের তালিকায় সংরক্ষিত'; @override - String get pairingLocationPermission => - 'Allow location access for Omi in Settings so this phone can find your device, then try again.'; + String get onboardingConnectTapToConnect => 'সংযোগ করতে আপনার Omi-তে ট্যাপ করুন।'; @override - String get pairingNotFoundOrDropped => - 'Couldn\'t reach your Omi. Make sure it\'s charged, close to your phone, and its light is blinking, then try again.'; + String get onboardingActMicFallbackBody => 'Omi এখনও অডিও পাঠাচ্ছে না। আপাতত আপনার ফোন ব্যবহার করুন।'; @override - String get pairingBondRecovery => 'Forget Omi in your phone’s Bluetooth settings, then try pairing again.'; + String get onboardingActNoAudioOmi => 'Omi শোনা যাচ্ছে না। এটি চালু আছে ও কাছে আছে কিনা দেখুন।'; @override - String get pairingHowTo => 'How to pair'; + String get onboardingActNoAudioPhone => 'এখনও আপনাকে শোনা যাচ্ছে না। একটু জোরে বলে দেখুন।'; + + @override + String get onboardingActDictateBodyOmi => 'শুধু কথা বলুন। আপনার Omi শুনছে।'; + + @override + String get voiceProfileCardDismissed => 'সেটিংসে যেকোনো সময় ভয়েস প্রোফাইল সেট করতে পারেন।'; + + @override + String get homeNewRecordingButton => 'নতুন রেকর্ডিং'; + + @override + String get chatWelcomeMessage => + 'হাই, আমি Omi। আপনি যা নিয়ে কথা বলেছেন তার যেকোনো কিছু জিজ্ঞেস করুন: কেউ কী বলেছে, আপনি কী প্রতিশ্রুতি দিয়েছেন, বা আপনার দিনের সারাংশ। আমি কাজ যোগ করতে এবং আপনার জন্য জিনিস মনে রাখতেও পারি।'; + + @override + String get onboardingStarterTaskAsk => 'Omi-কে আপনার দিন সম্পর্কে জিজ্ঞেস করুন'; + + @override + String get onboardingStarterTaskRecord => 'Omi দিয়ে একটি কথোপকথন রেকর্ড করুন'; + + @override + String get onboardingStarterTaskCheck => 'শেষ করতে এই কাজটিতে টিক দিন'; + + @override + String get paywallRecommended => 'Recommended'; @override - String get firstCaptureWatchInstructions => - 'Open Omi on your Apple Watch and tap Record. Allow microphone access if asked. Your capture will sync to Omi on this phone.'; + String get paywallNoPlans => 'Plans aren\'t available right now. Try again in a minute.'; @override - String get firstCaptureWatchInstall => - 'In the Watch app on your iPhone, install Omi on your Apple Watch. Then open Omi on your watch and tap Record.'; + String get paywallOtherPlans => 'See all plans'; @override - String get firstCaptureWatchPair => - 'Pair your Apple Watch with this iPhone using the Watch app, then install and open Omi on your watch.'; + String get paywallComparePlans => 'See plans'; @override - String get firstCaptureUnavailable => - 'Recording could not start. Check microphone access and your connection, then try again.'; + String get paywallYourPlan => 'Your plan'; @override - String get firstCaptureDelayed => - 'Your recording is taking longer to process. You can check your conversations on Home or try again.'; + String get paywallNotNow => 'Not now'; @override - String get firstCaptureEmptyTitle => 'Let\'s try that again'; + String get paywallTryAgain => 'Try again'; @override - String get firstCaptureShortTitle => 'Finish This Recording?'; + String get paywallManageBilling => 'Manage billing'; @override - String get firstCaptureShortWarning => 'A few more sentences help Omi understand you. About 30 seconds is ideal.'; + String get paywallLoadError => 'Couldn\'t load plans. Check your connection and try again.'; @override - String get firstCaptureCancelTitle => 'Leave This Recording?'; + String get paywallPlans => 'Plans'; @override - String get firstCaptureCancelWarning => - 'Going back stops this recording. Keep talking to give Omi a few more sentences.'; + String get watchCaptureTitle => 'Record from your Apple Watch'; @override - String get firstCaptureSiriTip => 'Tip: Enable Omi in Shortcuts → Siri.'; + String get watchCaptureOpen => 'Open Omi on your Apple Watch.'; + + @override + String get watchCaptureRecord => 'Tap Record. Allow microphone access if asked.'; + + @override + String get watchCaptureSync => 'Your recording syncs to Omi on this phone.'; + + @override + String get pairingBluetoothOff => 'Turn on Bluetooth on your phone, then try again.'; + + @override + String get pairingBluetoothPermission => 'Allow Bluetooth access for Omi in Settings, then try again.'; + + @override + String get pairingNearbyPermission => 'Allow Nearby devices access for Omi in Settings, then try again.'; + + @override + String get pairingLocationPermission => + 'Allow location access for Omi in Settings so this phone can find your device, then try again.'; + + @override + String get pairingNotFoundOrDropped => + 'Couldn\'t reach your Omi. Make sure it\'s charged, close to your phone, and its light is blinking, then try again.'; + + @override + String get pairingBondRecovery => 'Forget Omi in your phone’s Bluetooth settings, then try pairing again.'; + + @override + String get pairingHowTo => 'How to pair'; @override String get paywallOutlineHeadline => 'আর কখনো একটি শব্দও মিস করবেন না।'; diff --git a/app/lib/l10n/app_localizations_bs.dart b/app/lib/l10n/app_localizations_bs.dart index 6d446abe9bb..f359a676213 100644 --- a/app/lib/l10n/app_localizations_bs.dart +++ b/app/lib/l10n/app_localizations_bs.dart @@ -1685,8 +1685,7 @@ class AppLocalizationsBs extends AppLocalizations { String get enableBluetooth => 'Omogući Bluetooth'; @override - String get bluetoothNeeded => - 'Omi koristi Bluetooth da bi se povezao sa vašim nosivim uređajem. Molimo omogućite Bluetooth i pokušajte ponovo.'; + String get bluetoothNeeded => 'Omi se s telefonom povezuje putem Bluetootha. Uključite ga da nastavite.'; @override String get contactSupport => 'Kontaktiraj podršku?'; @@ -4901,7 +4900,7 @@ class AppLocalizationsBs extends AppLocalizations { 'Nastavljanjem, vaši razgovori, snimke i lični podaci bit će sigurno pohranjeni na našim serverima. Vaši audio zapisi i transkripti se obrađuju od strane AI usluga trećih strana (uključujući Deepgram za transkripciju i OpenAI za analizu) kako bi vam pružili uvide pokretane vještačkom inteligencijom i omogućili sve funkcije aplikacije.'; @override - String get tasksEmptyStateMessage => 'Započnite razgovor da kreirate zadatak.'; + String get tasksEmptyStateMessage => 'Zadaci iz vaših razgovora pojavit će se ovdje.'; @override String get clearChatAction => 'Očisti razgovor'; @@ -13070,154 +13069,207 @@ class AppLocalizationsBs extends AppLocalizations { } @override - String get paywallRecommended => 'Recommended'; + String get onboardingPermissionTapToFix => 'Dodirnite da popravite'; @override - String get paywallNoPlans => 'Plans aren\'t available right now. Try again in a minute.'; + String get onboardingNoOmiYet => 'Još nemate Omi?'; @override - String get paywallOtherPlans => 'See all plans'; + String get onboardingOmiOrdered => 'Naručio/la sam ga'; @override - String get paywallComparePlans => 'See plans'; + String get onboardingOmiNotOrdered => 'Još nisam naručio/la'; @override - String get paywallYourPlan => 'Your plan'; + String get onboardingConnectOmi => 'Povežite Omi'; @override - String get paywallNotNow => 'Not now'; + String get consentShortMessage => + 'Omi koristi AI da vaše razgovore pretvori u bilješke. Možete ih izbrisati bilo kada.'; @override - String get paywallTryAgain => 'Try again'; + String consentAiProviders(String providers) { + return 'Obrađuje se pomoću $providers.'; + } @override - String get paywallManageBilling => 'Manage billing'; + String get onboardingActIntro => 'Isprobajmo Omi.'; @override - String get paywallLoadError => 'Couldn\'t load plans. Check your connection and try again.'; + String get onboardingActMicTitle => 'Omi treba vaš mikrofon'; @override - String get paywallPlans => 'Plans'; + String get onboardingActMicBody => 'Da vas Omi čuje kad mu se obratite.'; @override - String get firstCaptureTitle => 'How will you capture?'; + String get onboardingActMicAllow => 'Dozvoli mikrofon'; @override - String get firstCaptureDescription => 'Choose how you want to capture your first conversation.'; + String get onboardingActDictateTitle => 'Ispričajte Omiju svoj dan'; @override - String get firstCaptureOmi => 'I Have an Omi'; + String get onboardingActDictateBody => 'Recite to na svoj način. Omi će se pobrinuti za ostalo.'; @override - String get firstCapturePhone => 'This Phone'; + String get onboardingActDictateExample => + 'Upravo sam ručao sa Sam, seli se u Lisabon u junu! Podsjeti me da pošaljem prezentaciju timu do petka i da kupim mlijeko na putu kući.'; @override - String get firstCaptureWatch => 'Apple Watch'; + String get onboardingActDictateHint => 'Dodirnite kvačicu kad završite'; @override - String get firstCaptureSkip => 'Skip for Now'; + String get onboardingActPhoneMic => 'Mikrofon telefona'; @override - String get firstCapturePrompt => - 'Tell Omi what you\'re working on today, or what\'s on your mind. Talk for about 30 seconds.'; + String get onboardingActWriting => 'Sažimamo…'; @override - String get firstCaptureKeepGoing => 'Keep going… 30 seconds is ideal.'; + String get onboardingActRemembers => 'Omi pamti da vi ne morate'; @override - String get firstCaptureReady => 'You can finish now, or keep talking.'; + String get onboardingActNotesLabel => 'Vaš dan, ukratko'; @override - String get firstCaptureStart => 'Start Recording'; + String get onboardingActTasksLabel => 'Omi je uhvatio vaše zadatke'; @override - String get firstCaptureFinish => 'Finish Recording'; + String get onboardingActYourWords => 'Evo šta ste rekli'; @override - String get firstCaptureListening => 'Listening… your words will appear here.'; + String onboardingActCongratsTitle(String name) { + return 'Sve je spremno, $name'; + } @override - String get firstCaptureProcessing => 'Creating your first conversation…'; + String get onboardingActRateTitle => 'Sviđa li vam se Omi zasad?'; @override - String get firstCaptureResult => 'Your first conversation'; + String get onboardingActRateYes => 'Ocijenite Omi'; @override - String get firstCaptureEmpty => 'I didn\'t catch enough speech, try again with a few sentences'; + String get onboardingWelcomeCardTitle => 'Dobro došli u Omi'; @override - String get watchCaptureTitle => 'Record from your Apple Watch'; + String get onboardingWelcomeCardBody => + 'Nosite Omi i pričajte kao i inače. Svaki razgovor ovdje postaje bilješke, zadaci i sjećanja.'; @override - String get watchCaptureOpen => 'Open Omi on your Apple Watch.'; + String get onboardingActDemoTitle => 'Ručak sa Sam'; @override - String get watchCaptureRecord => 'Tap Record. Allow microphone access if asked.'; + String get onboardingActDemoSummary => + 'Ručak sa Sam, koja se u junu seli u Lisabon. Želite poslati prezentaciju timu do petka i kupiti mlijeko na putu kući.'; @override - String get watchCaptureSync => 'Your recording syncs to Omi on this phone.'; + String get onboardingActDemoTask1 => 'Poslati prezentaciju timu do petka'; @override - String get pairingBluetoothOff => 'Turn on Bluetooth on your phone, then try again.'; + String get onboardingActDemoTask2 => 'Kupiti mlijeko na putu kući'; @override - String get pairingBluetoothPermission => 'Allow Bluetooth access for Omi in Settings, then try again.'; + String get onboardingActJustNow => 'Upravo sada'; @override - String get pairingNearbyPermission => 'Allow Nearby devices access for Omi in Settings, then try again.'; + String get onboardingActSavedToTasks => 'Sačuvano u vaše zadatke'; @override - String get pairingLocationPermission => - 'Allow location access for Omi in Settings so this phone can find your device, then try again.'; + String get onboardingConnectTapToConnect => 'Dodirnite svoj Omi da se povežete.'; @override - String get pairingNotFoundOrDropped => - 'Couldn\'t reach your Omi. Make sure it\'s charged, close to your phone, and its light is blinking, then try again.'; + String get onboardingActMicFallbackBody => 'Omi još ne šalje zvuk. Za sada koristite telefon.'; @override - String get pairingBondRecovery => 'Forget Omi in your phone’s Bluetooth settings, then try pairing again.'; + String get onboardingActNoAudioOmi => 'Ne čujemo Omi. Provjerite je li uključen i blizu.'; @override - String get pairingHowTo => 'How to pair'; + String get onboardingActNoAudioPhone => 'Još vas ne čujemo. Pokušajte govoriti malo glasnije.'; + + @override + String get onboardingActDictateBodyOmi => 'Samo pričajte. Vaš Omi sluša.'; + + @override + String get voiceProfileCardDismissed => 'Glasovni profil možete podesiti bilo kada u Postavkama.'; + + @override + String get homeNewRecordingButton => 'Novo snimanje'; + + @override + String get chatWelcomeMessage => + 'Zdravo, ja sam Omi. Pitaj me bilo šta o čemu si pričao: šta je neko rekao, šta si obećao ili sažetak tvog dana. Mogu i dodavati zadatke i pamtiti stvari za tebe.'; + + @override + String get onboardingStarterTaskAsk => 'Pitaj Omi o svom danu'; + + @override + String get onboardingStarterTaskRecord => 'Snimi razgovor uz Omi'; + + @override + String get onboardingStarterTaskCheck => 'Označi ovaj zadatak da ga završiš'; + + @override + String get paywallRecommended => 'Recommended'; @override - String get firstCaptureWatchInstructions => - 'Open Omi on your Apple Watch and tap Record. Allow microphone access if asked. Your capture will sync to Omi on this phone.'; + String get paywallNoPlans => 'Plans aren\'t available right now. Try again in a minute.'; @override - String get firstCaptureWatchInstall => - 'In the Watch app on your iPhone, install Omi on your Apple Watch. Then open Omi on your watch and tap Record.'; + String get paywallOtherPlans => 'See all plans'; @override - String get firstCaptureWatchPair => - 'Pair your Apple Watch with this iPhone using the Watch app, then install and open Omi on your watch.'; + String get paywallComparePlans => 'See plans'; @override - String get firstCaptureUnavailable => - 'Recording could not start. Check microphone access and your connection, then try again.'; + String get paywallYourPlan => 'Your plan'; @override - String get firstCaptureDelayed => - 'Your recording is taking longer to process. You can check your conversations on Home or try again.'; + String get paywallNotNow => 'Not now'; @override - String get firstCaptureEmptyTitle => 'Let\'s try that again'; + String get paywallTryAgain => 'Try again'; @override - String get firstCaptureShortTitle => 'Finish This Recording?'; + String get paywallManageBilling => 'Manage billing'; @override - String get firstCaptureShortWarning => 'A few more sentences help Omi understand you. About 30 seconds is ideal.'; + String get paywallLoadError => 'Couldn\'t load plans. Check your connection and try again.'; @override - String get firstCaptureCancelTitle => 'Leave This Recording?'; + String get paywallPlans => 'Plans'; @override - String get firstCaptureCancelWarning => - 'Going back stops this recording. Keep talking to give Omi a few more sentences.'; + String get watchCaptureTitle => 'Record from your Apple Watch'; @override - String get firstCaptureSiriTip => 'Tip: Enable Omi in Shortcuts → Siri.'; + String get watchCaptureOpen => 'Open Omi on your Apple Watch.'; + + @override + String get watchCaptureRecord => 'Tap Record. Allow microphone access if asked.'; + + @override + String get watchCaptureSync => 'Your recording syncs to Omi on this phone.'; + + @override + String get pairingBluetoothOff => 'Turn on Bluetooth on your phone, then try again.'; + + @override + String get pairingBluetoothPermission => 'Allow Bluetooth access for Omi in Settings, then try again.'; + + @override + String get pairingNearbyPermission => 'Allow Nearby devices access for Omi in Settings, then try again.'; + + @override + String get pairingLocationPermission => + 'Allow location access for Omi in Settings so this phone can find your device, then try again.'; + + @override + String get pairingNotFoundOrDropped => + 'Couldn\'t reach your Omi. Make sure it\'s charged, close to your phone, and its light is blinking, then try again.'; + + @override + String get pairingBondRecovery => 'Forget Omi in your phone’s Bluetooth settings, then try pairing again.'; + + @override + String get pairingHowTo => 'How to pair'; @override String get paywallOutlineHeadline => 'Nikad više ne propustite nijednu riječ.'; diff --git a/app/lib/l10n/app_localizations_ca.dart b/app/lib/l10n/app_localizations_ca.dart index af4ed3e83a9..2e6f3717f87 100644 --- a/app/lib/l10n/app_localizations_ca.dart +++ b/app/lib/l10n/app_localizations_ca.dart @@ -1695,8 +1695,7 @@ class AppLocalizationsCa extends AppLocalizations { String get enableBluetooth => 'Activar Bluetooth'; @override - String get bluetoothNeeded => - 'Omi necessita Bluetooth per connectar-se al vostre dispositiu portàtil. Activeu Bluetooth i torneu-ho a provar.'; + String get bluetoothNeeded => 'L\'Omi es connecta al telèfon per Bluetooth. Activa\'l per continuar.'; @override String get contactSupport => 'Contactar amb suport?'; @@ -4922,7 +4921,7 @@ class AppLocalizationsCa extends AppLocalizations { 'Continuant, les vostres converses, enregistraments i informació personal s\'emmagatzemaran de manera segura als nostres servidors. Els vostres enregistraments d\'àudio i transcripcions són processats per serveis d\'IA de tercers (incloent Deepgram per a la transcripció i OpenAI per a l\'anàlisi) per proporcionar-vos informació impulsada per IA i habilitar totes les funcions de l\'aplicació.'; @override - String get tasksEmptyStateMessage => 'Inicia una conversa per crear una tasca.'; + String get tasksEmptyStateMessage => 'Les tasques de les teves converses apareixeran aquí.'; @override String get clearChatAction => 'Esborrar el xat'; @@ -13108,154 +13107,207 @@ class AppLocalizationsCa extends AppLocalizations { } @override - String get paywallRecommended => 'Recommended'; + String get onboardingPermissionTapToFix => 'Toca per solucionar-ho'; @override - String get paywallNoPlans => 'Plans aren\'t available right now. Try again in a minute.'; + String get onboardingNoOmiYet => 'Encara no tens l\'Omi?'; @override - String get paywallOtherPlans => 'See all plans'; + String get onboardingOmiOrdered => 'Ja l\'he demanat'; @override - String get paywallComparePlans => 'See plans'; + String get onboardingOmiNotOrdered => 'Encara no l\'he demanat'; @override - String get paywallYourPlan => 'Your plan'; + String get onboardingConnectOmi => 'Connecta l\'Omi'; @override - String get paywallNotNow => 'Not now'; + String get consentShortMessage => + 'Omi fa servir la IA per convertir les teves converses en notes. Les pots esborrar quan vulguis.'; @override - String get paywallTryAgain => 'Try again'; + String consentAiProviders(String providers) { + return 'Processat amb $providers.'; + } @override - String get paywallManageBilling => 'Manage billing'; + String get onboardingActIntro => 'Provem Omi.'; @override - String get paywallLoadError => 'Couldn\'t load plans. Check your connection and try again.'; + String get onboardingActMicTitle => 'Omi necessita el micròfon'; @override - String get paywallPlans => 'Plans'; + String get onboardingActMicBody => 'Perquè Omi et senti quan li parlis.'; @override - String get firstCaptureTitle => 'How will you capture?'; + String get onboardingActMicAllow => 'Permet el micròfon'; @override - String get firstCaptureDescription => 'Choose how you want to capture your first conversation.'; + String get onboardingActDictateTitle => 'Explica-li a Omi el teu dia'; @override - String get firstCaptureOmi => 'I Have an Omi'; + String get onboardingActDictateBody => 'Digues-ho a la teva manera. Omi s\'encarrega de la resta.'; @override - String get firstCapturePhone => 'This Phone'; + String get onboardingActDictateExample => + 'Acabo de dinar amb la Sam, se\'n va a viure a Lisboa al juny! Recorda\'m d\'enviar la presentació a l\'equip abans de divendres i de comprar llet de tornada a casa.'; @override - String get firstCaptureWatch => 'Apple Watch'; + String get onboardingActDictateHint => 'Toca la marca quan acabis'; @override - String get firstCaptureSkip => 'Skip for Now'; + String get onboardingActPhoneMic => 'Micròfon del telèfon'; @override - String get firstCapturePrompt => - 'Tell Omi what you\'re working on today, or what\'s on your mind. Talk for about 30 seconds.'; + String get onboardingActWriting => 'Resumint…'; @override - String get firstCaptureKeepGoing => 'Keep going… 30 seconds is ideal.'; + String get onboardingActRemembers => 'L\'Omi ho recorda perquè tu no ho hagis de fer'; @override - String get firstCaptureReady => 'You can finish now, or keep talking.'; + String get onboardingActNotesLabel => 'El teu dia, resumit'; @override - String get firstCaptureStart => 'Start Recording'; + String get onboardingActTasksLabel => 'Omi ha trobat les teves tasques'; @override - String get firstCaptureFinish => 'Finish Recording'; + String get onboardingActYourWords => 'Això és el que has dit'; @override - String get firstCaptureListening => 'Listening… your words will appear here.'; + String onboardingActCongratsTitle(String name) { + return 'Tot a punt, $name'; + } @override - String get firstCaptureProcessing => 'Creating your first conversation…'; + String get onboardingActRateTitle => 'T\'agrada Omi de moment?'; @override - String get firstCaptureResult => 'Your first conversation'; + String get onboardingActRateYes => 'Valora Omi'; @override - String get firstCaptureEmpty => 'I didn\'t catch enough speech, try again with a few sentences'; + String get onboardingWelcomeCardTitle => 'Et donem la benvinguda a Omi'; @override - String get watchCaptureTitle => 'Record from your Apple Watch'; + String get onboardingWelcomeCardBody => + 'Porta l\'Omi i parla com sempre. Cada conversa es converteix aquí en notes, tasques i records.'; @override - String get watchCaptureOpen => 'Open Omi on your Apple Watch.'; + String get onboardingActDemoTitle => 'Dinar amb la Sam'; @override - String get watchCaptureRecord => 'Tap Record. Allow microphone access if asked.'; + String get onboardingActDemoSummary => + 'Dinar amb en Sam, que es trasllada a Lisboa al juny. Vols enviar la presentació a l\'equip abans de divendres i comprar llet de camí a casa.'; @override - String get watchCaptureSync => 'Your recording syncs to Omi on this phone.'; + String get onboardingActDemoTask1 => 'Enviar la presentació a l\'equip abans de divendres'; @override - String get pairingBluetoothOff => 'Turn on Bluetooth on your phone, then try again.'; + String get onboardingActDemoTask2 => 'Comprar llet de tornada a casa'; @override - String get pairingBluetoothPermission => 'Allow Bluetooth access for Omi in Settings, then try again.'; + String get onboardingActJustNow => 'Ara mateix'; @override - String get pairingNearbyPermission => 'Allow Nearby devices access for Omi in Settings, then try again.'; + String get onboardingActSavedToTasks => 'Desat a les teves tasques'; @override - String get pairingLocationPermission => - 'Allow location access for Omi in Settings so this phone can find your device, then try again.'; + String get onboardingConnectTapToConnect => 'Toca el teu Omi per connectar-lo.'; @override - String get pairingNotFoundOrDropped => - 'Couldn\'t reach your Omi. Make sure it\'s charged, close to your phone, and its light is blinking, then try again.'; + String get onboardingActMicFallbackBody => 'L\'Omi encara no envia àudio. De moment, fes servir el telèfon.'; @override - String get pairingBondRecovery => 'Forget Omi in your phone’s Bluetooth settings, then try pairing again.'; + String get onboardingActNoAudioOmi => 'No sentim l\'Omi. Comprova que estigui encès i a prop.'; @override - String get pairingHowTo => 'How to pair'; + String get onboardingActNoAudioPhone => 'Encara no et sentim. Prova de parlar una mica més fort.'; + + @override + String get onboardingActDictateBodyOmi => 'Només parla. El teu Omi t\'escolta.'; + + @override + String get voiceProfileCardDismissed => 'Pots configurar el perfil de veu quan vulguis a Configuració.'; + + @override + String get homeNewRecordingButton => 'Nova gravació'; + + @override + String get chatWelcomeMessage => + 'Hola, soc l\'Omi. Pregunta\'m qualsevol cosa del que has parlat: què va dir algú, què vas prometre o un resum del teu dia. També puc afegir tasques i recordar coses per tu.'; + + @override + String get onboardingStarterTaskAsk => 'Pregunta a l\'Omi pel teu dia'; + + @override + String get onboardingStarterTaskRecord => 'Grava una conversa amb l\'Omi'; + + @override + String get onboardingStarterTaskCheck => 'Marca aquesta tasca per completar-la'; + + @override + String get paywallRecommended => 'Recommended'; @override - String get firstCaptureWatchInstructions => - 'Open Omi on your Apple Watch and tap Record. Allow microphone access if asked. Your capture will sync to Omi on this phone.'; + String get paywallNoPlans => 'Plans aren\'t available right now. Try again in a minute.'; @override - String get firstCaptureWatchInstall => - 'In the Watch app on your iPhone, install Omi on your Apple Watch. Then open Omi on your watch and tap Record.'; + String get paywallOtherPlans => 'See all plans'; @override - String get firstCaptureWatchPair => - 'Pair your Apple Watch with this iPhone using the Watch app, then install and open Omi on your watch.'; + String get paywallComparePlans => 'See plans'; @override - String get firstCaptureUnavailable => - 'Recording could not start. Check microphone access and your connection, then try again.'; + String get paywallYourPlan => 'Your plan'; @override - String get firstCaptureDelayed => - 'Your recording is taking longer to process. You can check your conversations on Home or try again.'; + String get paywallNotNow => 'Not now'; @override - String get firstCaptureEmptyTitle => 'Let\'s try that again'; + String get paywallTryAgain => 'Try again'; @override - String get firstCaptureShortTitle => 'Finish This Recording?'; + String get paywallManageBilling => 'Manage billing'; @override - String get firstCaptureShortWarning => 'A few more sentences help Omi understand you. About 30 seconds is ideal.'; + String get paywallLoadError => 'Couldn\'t load plans. Check your connection and try again.'; @override - String get firstCaptureCancelTitle => 'Leave This Recording?'; + String get paywallPlans => 'Plans'; @override - String get firstCaptureCancelWarning => - 'Going back stops this recording. Keep talking to give Omi a few more sentences.'; + String get watchCaptureTitle => 'Record from your Apple Watch'; @override - String get firstCaptureSiriTip => 'Tip: Enable Omi in Shortcuts → Siri.'; + String get watchCaptureOpen => 'Open Omi on your Apple Watch.'; + + @override + String get watchCaptureRecord => 'Tap Record. Allow microphone access if asked.'; + + @override + String get watchCaptureSync => 'Your recording syncs to Omi on this phone.'; + + @override + String get pairingBluetoothOff => 'Turn on Bluetooth on your phone, then try again.'; + + @override + String get pairingBluetoothPermission => 'Allow Bluetooth access for Omi in Settings, then try again.'; + + @override + String get pairingNearbyPermission => 'Allow Nearby devices access for Omi in Settings, then try again.'; + + @override + String get pairingLocationPermission => + 'Allow location access for Omi in Settings so this phone can find your device, then try again.'; + + @override + String get pairingNotFoundOrDropped => + 'Couldn\'t reach your Omi. Make sure it\'s charged, close to your phone, and its light is blinking, then try again.'; + + @override + String get pairingBondRecovery => 'Forget Omi in your phone’s Bluetooth settings, then try pairing again.'; + + @override + String get pairingHowTo => 'How to pair'; @override String get paywallOutlineHeadline => 'No et perdis mai més ni una paraula.'; diff --git a/app/lib/l10n/app_localizations_cs.dart b/app/lib/l10n/app_localizations_cs.dart index 868e0db7614..beeece5f29d 100644 --- a/app/lib/l10n/app_localizations_cs.dart +++ b/app/lib/l10n/app_localizations_cs.dart @@ -1686,8 +1686,7 @@ class AppLocalizationsCs extends AppLocalizations { String get enableBluetooth => 'Povolit Bluetooth'; @override - String get bluetoothNeeded => - 'Omi potřebuje Bluetooth pro připojení k vašemu nositelného zařízení. Povolte prosím Bluetooth a zkuste to znovu.'; + String get bluetoothNeeded => 'Omi se k telefonu připojuje přes Bluetooth. Zapněte ho a pokračujte.'; @override String get contactSupport => 'Kontaktovat podporu?'; @@ -4885,7 +4884,7 @@ class AppLocalizationsCs extends AppLocalizations { 'Pokračováním budou vaše konverzace, nahrávky a osobní údaje bezpečně uloženy na našich serverech. Vaše audio nahrávky a přepisy jsou zpracovávány AI službami třetích stran (včetně Deepgram pro přepis a OpenAI pro analýzu), aby vám poskytly poznatky založené na AI a umožnily všechny funkce aplikace.'; @override - String get tasksEmptyStateMessage => 'Začněte konverzaci a vytvořte úkol.'; + String get tasksEmptyStateMessage => 'Úkoly z vašich konverzací se zobrazí tady.'; @override String get clearChatAction => 'Vymazat chat'; @@ -13042,154 +13041,206 @@ class AppLocalizationsCs extends AppLocalizations { } @override - String get paywallRecommended => 'Recommended'; + String get onboardingPermissionTapToFix => 'Klepnutím opravíte'; @override - String get paywallNoPlans => 'Plans aren\'t available right now. Try again in a minute.'; + String get onboardingNoOmiYet => 'Ještě nemáte Omi?'; @override - String get paywallOtherPlans => 'See all plans'; + String get onboardingOmiOrdered => 'Už jsem ho objednal/a'; @override - String get paywallComparePlans => 'See plans'; + String get onboardingOmiNotOrdered => 'Ještě neobjednáno'; @override - String get paywallYourPlan => 'Your plan'; + String get onboardingConnectOmi => 'Připojte Omi'; @override - String get paywallNotNow => 'Not now'; + String get consentShortMessage => 'Omi pomocí AI mění vaše konverzace v poznámky. Kdykoli je můžete smazat.'; @override - String get paywallTryAgain => 'Try again'; + String consentAiProviders(String providers) { + return 'Zpracovává $providers.'; + } @override - String get paywallManageBilling => 'Manage billing'; + String get onboardingActIntro => 'Vyzkoušejme Omi.'; @override - String get paywallLoadError => 'Couldn\'t load plans. Check your connection and try again.'; + String get onboardingActMicTitle => 'Omi potřebuje mikrofon'; @override - String get paywallPlans => 'Plans'; + String get onboardingActMicBody => 'Aby vás Omi slyšel, když na něj mluvíte.'; @override - String get firstCaptureTitle => 'How will you capture?'; + String get onboardingActMicAllow => 'Povolit mikrofon'; @override - String get firstCaptureDescription => 'Choose how you want to capture your first conversation.'; + String get onboardingActDictateTitle => 'Řekněte Omi o svém dni'; @override - String get firstCaptureOmi => 'I Have an Omi'; + String get onboardingActDictateBody => 'Řekněte to po svém. Zbytek zařídí Omi.'; @override - String get firstCapturePhone => 'This Phone'; + String get onboardingActDictateExample => + 'Právě jsem obědval se Sam, v červnu se stěhuje do Lisabonu! Připomeň mi poslat prezentaci týmu do pátku a koupit cestou domů mléko.'; @override - String get firstCaptureWatch => 'Apple Watch'; + String get onboardingActDictateHint => 'Až budete hotovi, klepněte na fajfku'; @override - String get firstCaptureSkip => 'Skip for Now'; + String get onboardingActPhoneMic => 'Mikrofon telefonu'; @override - String get firstCapturePrompt => - 'Tell Omi what you\'re working on today, or what\'s on your mind. Talk for about 30 seconds.'; + String get onboardingActWriting => 'Shrnujeme…'; @override - String get firstCaptureKeepGoing => 'Keep going… 30 seconds is ideal.'; + String get onboardingActRemembers => 'Omi si to pamatuje, abyste nemuseli vy'; @override - String get firstCaptureReady => 'You can finish now, or keep talking.'; + String get onboardingActNotesLabel => 'Váš den ve zkratce'; @override - String get firstCaptureStart => 'Start Recording'; + String get onboardingActTasksLabel => 'Omi zachytil vaše úkoly'; @override - String get firstCaptureFinish => 'Finish Recording'; + String get onboardingActYourWords => 'Tohle jste řekli'; @override - String get firstCaptureListening => 'Listening… your words will appear here.'; + String onboardingActCongratsTitle(String name) { + return 'Vše je připraveno, $name'; + } @override - String get firstCaptureProcessing => 'Creating your first conversation…'; + String get onboardingActRateTitle => 'Líbí se vám zatím Omi?'; @override - String get firstCaptureResult => 'Your first conversation'; + String get onboardingActRateYes => 'Ohodnotit Omi'; @override - String get firstCaptureEmpty => 'I didn\'t catch enough speech, try again with a few sentences'; + String get onboardingWelcomeCardTitle => 'Vítejte v Omi'; @override - String get watchCaptureTitle => 'Record from your Apple Watch'; + String get onboardingWelcomeCardBody => + 'Noste Omi a mluvte jako obvykle. Každá konverzace se tu promění v poznámky, úkoly a vzpomínky.'; @override - String get watchCaptureOpen => 'Open Omi on your Apple Watch.'; + String get onboardingActDemoTitle => 'Oběd se Sam'; @override - String get watchCaptureRecord => 'Tap Record. Allow microphone access if asked.'; + String get onboardingActDemoSummary => + 'Oběd se Sam, která se v červnu stěhuje do Lisabonu. Chcete do pátku poslat týmu prezentaci a cestou domů koupit mléko.'; @override - String get watchCaptureSync => 'Your recording syncs to Omi on this phone.'; + String get onboardingActDemoTask1 => 'Poslat prezentaci týmu do pátku'; @override - String get pairingBluetoothOff => 'Turn on Bluetooth on your phone, then try again.'; + String get onboardingActDemoTask2 => 'Koupit cestou domů mléko'; @override - String get pairingBluetoothPermission => 'Allow Bluetooth access for Omi in Settings, then try again.'; + String get onboardingActJustNow => 'Právě teď'; @override - String get pairingNearbyPermission => 'Allow Nearby devices access for Omi in Settings, then try again.'; + String get onboardingActSavedToTasks => 'Uloženo do vašich úkolů'; @override - String get pairingLocationPermission => - 'Allow location access for Omi in Settings so this phone can find your device, then try again.'; + String get onboardingConnectTapToConnect => 'Klepnutím na Omi se připojíte.'; @override - String get pairingNotFoundOrDropped => - 'Couldn\'t reach your Omi. Make sure it\'s charged, close to your phone, and its light is blinking, then try again.'; + String get onboardingActMicFallbackBody => 'Omi zatím neposílá zvuk. Zatím použijte telefon.'; @override - String get pairingBondRecovery => 'Forget Omi in your phone’s Bluetooth settings, then try pairing again.'; + String get onboardingActNoAudioOmi => 'Omi neslyšíme. Zkontrolujte, že je zapnuté a blízko.'; @override - String get pairingHowTo => 'How to pair'; + String get onboardingActNoAudioPhone => 'Zatím vás neslyšíme. Zkuste mluvit trochu hlasitěji.'; + + @override + String get onboardingActDictateBodyOmi => 'Prostě mluvte. Vaše Omi poslouchá.'; + + @override + String get voiceProfileCardDismissed => 'Hlasový profil můžete kdykoli nastavit v Nastavení.'; + + @override + String get homeNewRecordingButton => 'Nový záznam'; + + @override + String get chatWelcomeMessage => + 'Ahoj, jsem Omi. Zeptej se mě na cokoli, o čem jsi mluvil: co kdo řekl, co jsi slíbil nebo shrnutí tvého dne. Umím také přidávat úkoly a pamatovat si věci za tebe.'; + + @override + String get onboardingStarterTaskAsk => 'Zeptej se Omi na svůj den'; + + @override + String get onboardingStarterTaskRecord => 'Nahraj rozhovor s Omi'; + + @override + String get onboardingStarterTaskCheck => 'Odškrtni tento úkol a dokonči ho'; + + @override + String get paywallRecommended => 'Recommended'; @override - String get firstCaptureWatchInstructions => - 'Open Omi on your Apple Watch and tap Record. Allow microphone access if asked. Your capture will sync to Omi on this phone.'; + String get paywallNoPlans => 'Plans aren\'t available right now. Try again in a minute.'; @override - String get firstCaptureWatchInstall => - 'In the Watch app on your iPhone, install Omi on your Apple Watch. Then open Omi on your watch and tap Record.'; + String get paywallOtherPlans => 'See all plans'; @override - String get firstCaptureWatchPair => - 'Pair your Apple Watch with this iPhone using the Watch app, then install and open Omi on your watch.'; + String get paywallComparePlans => 'See plans'; @override - String get firstCaptureUnavailable => - 'Recording could not start. Check microphone access and your connection, then try again.'; + String get paywallYourPlan => 'Your plan'; @override - String get firstCaptureDelayed => - 'Your recording is taking longer to process. You can check your conversations on Home or try again.'; + String get paywallNotNow => 'Not now'; @override - String get firstCaptureEmptyTitle => 'Let\'s try that again'; + String get paywallTryAgain => 'Try again'; @override - String get firstCaptureShortTitle => 'Finish This Recording?'; + String get paywallManageBilling => 'Manage billing'; @override - String get firstCaptureShortWarning => 'A few more sentences help Omi understand you. About 30 seconds is ideal.'; + String get paywallLoadError => 'Couldn\'t load plans. Check your connection and try again.'; @override - String get firstCaptureCancelTitle => 'Leave This Recording?'; + String get paywallPlans => 'Plans'; @override - String get firstCaptureCancelWarning => - 'Going back stops this recording. Keep talking to give Omi a few more sentences.'; + String get watchCaptureTitle => 'Record from your Apple Watch'; @override - String get firstCaptureSiriTip => 'Tip: Enable Omi in Shortcuts → Siri.'; + String get watchCaptureOpen => 'Open Omi on your Apple Watch.'; + + @override + String get watchCaptureRecord => 'Tap Record. Allow microphone access if asked.'; + + @override + String get watchCaptureSync => 'Your recording syncs to Omi on this phone.'; + + @override + String get pairingBluetoothOff => 'Turn on Bluetooth on your phone, then try again.'; + + @override + String get pairingBluetoothPermission => 'Allow Bluetooth access for Omi in Settings, then try again.'; + + @override + String get pairingNearbyPermission => 'Allow Nearby devices access for Omi in Settings, then try again.'; + + @override + String get pairingLocationPermission => + 'Allow location access for Omi in Settings so this phone can find your device, then try again.'; + + @override + String get pairingNotFoundOrDropped => + 'Couldn\'t reach your Omi. Make sure it\'s charged, close to your phone, and its light is blinking, then try again.'; + + @override + String get pairingBondRecovery => 'Forget Omi in your phone’s Bluetooth settings, then try pairing again.'; + + @override + String get pairingHowTo => 'How to pair'; @override String get paywallOutlineHeadline => 'Už vám neunikne ani slovo.'; diff --git a/app/lib/l10n/app_localizations_da.dart b/app/lib/l10n/app_localizations_da.dart index 69ad44b44bf..aff4907a942 100644 --- a/app/lib/l10n/app_localizations_da.dart +++ b/app/lib/l10n/app_localizations_da.dart @@ -1681,7 +1681,7 @@ class AppLocalizationsDa extends AppLocalizations { String get enableBluetooth => 'Aktivér Bluetooth'; @override - String get bluetoothNeeded => 'Bluetooth påkrævet'; + String get bluetoothNeeded => 'Omi forbinder til din telefon via Bluetooth. Slå det til for at fortsætte.'; @override String get contactSupport => 'Kontakt support'; @@ -4880,7 +4880,7 @@ class AppLocalizationsDa extends AppLocalizations { 'Ved at fortsætte vil dine samtaler, optagelser og personlige oplysninger blive sikkert gemt på vores servere. Dine lydoptagelser og udskrifter behandles af tredjeparts AI-tjenester (herunder Deepgram til transskription og OpenAI til analyse) for at give dig AI-drevne indsigter og aktivere alle appfunktioner.'; @override - String get tasksEmptyStateMessage => 'Start en samtale for at oprette en opgave.'; + String get tasksEmptyStateMessage => 'Opgaver fra dine samtaler vises her.'; @override String get clearChatAction => 'Ryd chat'; @@ -13027,154 +13027,207 @@ class AppLocalizationsDa extends AppLocalizations { } @override - String get paywallRecommended => 'Recommended'; + String get onboardingPermissionTapToFix => 'Tryk for at rette'; @override - String get paywallNoPlans => 'Plans aren\'t available right now. Try again in a minute.'; + String get onboardingNoOmiYet => 'Har du ikke din Omi endnu?'; @override - String get paywallOtherPlans => 'See all plans'; + String get onboardingOmiOrdered => 'Jeg har bestilt den'; @override - String get paywallComparePlans => 'See plans'; + String get onboardingOmiNotOrdered => 'Ikke bestilt endnu'; @override - String get paywallYourPlan => 'Your plan'; + String get onboardingConnectOmi => 'Forbind Omi'; @override - String get paywallNotNow => 'Not now'; + String get consentShortMessage => + 'Omi bruger AI til at gøre dine samtaler til noter. Du kan slette dem når som helst.'; @override - String get paywallTryAgain => 'Try again'; + String consentAiProviders(String providers) { + return 'Behandles med $providers.'; + } @override - String get paywallManageBilling => 'Manage billing'; + String get onboardingActIntro => 'Lad os prøve Omi.'; @override - String get paywallLoadError => 'Couldn\'t load plans. Check your connection and try again.'; + String get onboardingActMicTitle => 'Omi skal bruge din mikrofon'; @override - String get paywallPlans => 'Plans'; + String get onboardingActMicBody => 'Så Omi kan høre dig, når du taler til den.'; @override - String get firstCaptureTitle => 'How will you capture?'; + String get onboardingActMicAllow => 'Tillad mikrofon'; @override - String get firstCaptureDescription => 'Choose how you want to capture your first conversation.'; + String get onboardingActDictateTitle => 'Fortæl Omi om din dag'; @override - String get firstCaptureOmi => 'I Have an Omi'; + String get onboardingActDictateBody => 'Sig det på din måde. Omi klarer resten.'; @override - String get firstCapturePhone => 'This Phone'; + String get onboardingActDictateExample => + 'Har lige spist frokost med Sam, hun flytter til Lissabon i juni! Mind mig om at sende præsentationen til holdet inden fredag og købe mælk på vej hjem.'; @override - String get firstCaptureWatch => 'Apple Watch'; + String get onboardingActDictateHint => 'Tryk på fluebenet, når du er færdig'; @override - String get firstCaptureSkip => 'Skip for Now'; + String get onboardingActPhoneMic => 'Telefonens mikrofon'; @override - String get firstCapturePrompt => - 'Tell Omi what you\'re working on today, or what\'s on your mind. Talk for about 30 seconds.'; + String get onboardingActWriting => 'Opsummerer…'; @override - String get firstCaptureKeepGoing => 'Keep going… 30 seconds is ideal.'; + String get onboardingActRemembers => 'Omi husker det, så du ikke behøver'; @override - String get firstCaptureReady => 'You can finish now, or keep talking.'; + String get onboardingActNotesLabel => 'Din dag, opsummeret'; @override - String get firstCaptureStart => 'Start Recording'; + String get onboardingActTasksLabel => 'Omi fangede dine to-dos'; @override - String get firstCaptureFinish => 'Finish Recording'; + String get onboardingActYourWords => 'Det her sagde du'; @override - String get firstCaptureListening => 'Listening… your words will appear here.'; + String onboardingActCongratsTitle(String name) { + return 'Du er klar, $name'; + } @override - String get firstCaptureProcessing => 'Creating your first conversation…'; + String get onboardingActRateTitle => 'Kan du lide Omi indtil videre?'; @override - String get firstCaptureResult => 'Your first conversation'; + String get onboardingActRateYes => 'Bedøm Omi'; @override - String get firstCaptureEmpty => 'I didn\'t catch enough speech, try again with a few sentences'; + String get onboardingWelcomeCardTitle => 'Velkommen til Omi'; @override - String get watchCaptureTitle => 'Record from your Apple Watch'; + String get onboardingWelcomeCardBody => + 'Bær Omi og tal som normalt. Hver samtale bliver her til noter, opgaver og minder.'; @override - String get watchCaptureOpen => 'Open Omi on your Apple Watch.'; + String get onboardingActDemoTitle => 'Frokost med Sam'; @override - String get watchCaptureRecord => 'Tap Record. Allow microphone access if asked.'; + String get onboardingActDemoSummary => + 'Frokost med Sam, der flytter til Lissabon i juni. Du vil sende præsentationen til teamet inden fredag og købe mælk på vejen hjem.'; @override - String get watchCaptureSync => 'Your recording syncs to Omi on this phone.'; + String get onboardingActDemoTask1 => 'Send præsentationen til holdet inden fredag'; @override - String get pairingBluetoothOff => 'Turn on Bluetooth on your phone, then try again.'; + String get onboardingActDemoTask2 => 'Køb mælk på vej hjem'; @override - String get pairingBluetoothPermission => 'Allow Bluetooth access for Omi in Settings, then try again.'; + String get onboardingActJustNow => 'Lige nu'; @override - String get pairingNearbyPermission => 'Allow Nearby devices access for Omi in Settings, then try again.'; + String get onboardingActSavedToTasks => 'Gemt i dine opgaver'; @override - String get pairingLocationPermission => - 'Allow location access for Omi in Settings so this phone can find your device, then try again.'; + String get onboardingConnectTapToConnect => 'Tryk på din Omi for at forbinde.'; @override - String get pairingNotFoundOrDropped => - 'Couldn\'t reach your Omi. Make sure it\'s charged, close to your phone, and its light is blinking, then try again.'; + String get onboardingActMicFallbackBody => 'Din Omi sender ikke lyd endnu. Brug din telefon indtil videre.'; @override - String get pairingBondRecovery => 'Forget Omi in your phone’s Bluetooth settings, then try pairing again.'; + String get onboardingActNoAudioOmi => 'Kan ikke høre din Omi. Tjek, at den er tændt og tæt på.'; @override - String get pairingHowTo => 'How to pair'; + String get onboardingActNoAudioPhone => 'Kan ikke høre dig endnu. Prøv at tale lidt højere.'; + + @override + String get onboardingActDictateBodyOmi => 'Bare tal. Din Omi lytter.'; + + @override + String get voiceProfileCardDismissed => 'Du kan altid oprette din stemmeprofil under Indstillinger.'; + + @override + String get homeNewRecordingButton => 'Ny optagelse'; + + @override + String get chatWelcomeMessage => + 'Hej, jeg er Omi. Spørg mig om alt, du har talt om: hvad nogen sagde, hvad du lovede, eller et overblik over din dag. Jeg kan også tilføje opgaver og huske ting for dig.'; + + @override + String get onboardingStarterTaskAsk => 'Spørg Omi om din dag'; + + @override + String get onboardingStarterTaskRecord => 'Optag en samtale med Omi'; + + @override + String get onboardingStarterTaskCheck => 'Sæt flueben ved denne opgave for at fuldføre den'; + + @override + String get paywallRecommended => 'Recommended'; @override - String get firstCaptureWatchInstructions => - 'Open Omi on your Apple Watch and tap Record. Allow microphone access if asked. Your capture will sync to Omi on this phone.'; + String get paywallNoPlans => 'Plans aren\'t available right now. Try again in a minute.'; @override - String get firstCaptureWatchInstall => - 'In the Watch app on your iPhone, install Omi on your Apple Watch. Then open Omi on your watch and tap Record.'; + String get paywallOtherPlans => 'See all plans'; @override - String get firstCaptureWatchPair => - 'Pair your Apple Watch with this iPhone using the Watch app, then install and open Omi on your watch.'; + String get paywallComparePlans => 'See plans'; @override - String get firstCaptureUnavailable => - 'Recording could not start. Check microphone access and your connection, then try again.'; + String get paywallYourPlan => 'Your plan'; @override - String get firstCaptureDelayed => - 'Your recording is taking longer to process. You can check your conversations on Home or try again.'; + String get paywallNotNow => 'Not now'; @override - String get firstCaptureEmptyTitle => 'Let\'s try that again'; + String get paywallTryAgain => 'Try again'; @override - String get firstCaptureShortTitle => 'Finish This Recording?'; + String get paywallManageBilling => 'Manage billing'; @override - String get firstCaptureShortWarning => 'A few more sentences help Omi understand you. About 30 seconds is ideal.'; + String get paywallLoadError => 'Couldn\'t load plans. Check your connection and try again.'; @override - String get firstCaptureCancelTitle => 'Leave This Recording?'; + String get paywallPlans => 'Plans'; @override - String get firstCaptureCancelWarning => - 'Going back stops this recording. Keep talking to give Omi a few more sentences.'; + String get watchCaptureTitle => 'Record from your Apple Watch'; @override - String get firstCaptureSiriTip => 'Tip: Enable Omi in Shortcuts → Siri.'; + String get watchCaptureOpen => 'Open Omi on your Apple Watch.'; + + @override + String get watchCaptureRecord => 'Tap Record. Allow microphone access if asked.'; + + @override + String get watchCaptureSync => 'Your recording syncs to Omi on this phone.'; + + @override + String get pairingBluetoothOff => 'Turn on Bluetooth on your phone, then try again.'; + + @override + String get pairingBluetoothPermission => 'Allow Bluetooth access for Omi in Settings, then try again.'; + + @override + String get pairingNearbyPermission => 'Allow Nearby devices access for Omi in Settings, then try again.'; + + @override + String get pairingLocationPermission => + 'Allow location access for Omi in Settings so this phone can find your device, then try again.'; + + @override + String get pairingNotFoundOrDropped => + 'Couldn\'t reach your Omi. Make sure it\'s charged, close to your phone, and its light is blinking, then try again.'; + + @override + String get pairingBondRecovery => 'Forget Omi in your phone’s Bluetooth settings, then try pairing again.'; + + @override + String get pairingHowTo => 'How to pair'; @override String get paywallOutlineHeadline => 'Gå aldrig glip af et ord igen.'; diff --git a/app/lib/l10n/app_localizations_de.dart b/app/lib/l10n/app_localizations_de.dart index d1beb561652..5a9da055da8 100644 --- a/app/lib/l10n/app_localizations_de.dart +++ b/app/lib/l10n/app_localizations_de.dart @@ -1700,8 +1700,7 @@ class AppLocalizationsDe extends AppLocalizations { String get enableBluetooth => 'Bluetooth aktivieren'; @override - String get bluetoothNeeded => - 'Omi benötigt Bluetooth, um sich mit Ihrem Wearable zu verbinden. Bitte aktivieren Sie Bluetooth und versuchen Sie es erneut.'; + String get bluetoothNeeded => 'Omi verbindet sich über Bluetooth mit deinem Handy. Schalte es ein, um fortzufahren.'; @override String get contactSupport => 'Support kontaktieren?'; @@ -4931,7 +4930,7 @@ class AppLocalizationsDe extends AppLocalizations { 'Durch Fortfahren werden Ihre Gespräche, Aufnahmen und persönlichen Daten sicher auf unseren Servern gespeichert. Ihre Audioaufnahmen und Transkripte werden von KI-Diensten Dritter verarbeitet (einschließlich Deepgram für die Transkription und OpenAI für die Analyse), um Ihnen KI-gestützte Erkenntnisse zu liefern und alle App-Funktionen zu ermöglichen.'; @override - String get tasksEmptyStateMessage => 'Starte ein Gespräch, um eine Aufgabe zu erstellen.'; + String get tasksEmptyStateMessage => 'To-dos aus deinen Gesprächen erscheinen hier.'; @override String get clearChatAction => 'Chat löschen'; @@ -13139,154 +13138,206 @@ class AppLocalizationsDe extends AppLocalizations { } @override - String get paywallRecommended => 'Recommended'; + String get onboardingPermissionTapToFix => 'Zum Beheben tippen'; @override - String get paywallNoPlans => 'Plans aren\'t available right now. Try again in a minute.'; + String get onboardingNoOmiYet => 'Hast du dein Omi noch nicht?'; @override - String get paywallOtherPlans => 'See all plans'; + String get onboardingOmiOrdered => 'Ich habe es bestellt'; @override - String get paywallComparePlans => 'See plans'; + String get onboardingOmiNotOrdered => 'Noch nicht bestellt'; @override - String get paywallYourPlan => 'Your plan'; + String get onboardingConnectOmi => 'Omi verbinden'; @override - String get paywallNotNow => 'Not now'; + String get consentShortMessage => 'Omi macht mit KI aus deinen Gesprächen Notizen. Du kannst sie jederzeit löschen.'; @override - String get paywallTryAgain => 'Try again'; + String consentAiProviders(String providers) { + return 'Verarbeitet mit $providers.'; + } @override - String get paywallManageBilling => 'Manage billing'; + String get onboardingActIntro => 'Probier Omi aus.'; @override - String get paywallLoadError => 'Couldn\'t load plans. Check your connection and try again.'; + String get onboardingActMicTitle => 'Omi braucht dein Mikrofon'; @override - String get paywallPlans => 'Plans'; + String get onboardingActMicBody => 'Damit Omi dich hört, wenn du mit ihm sprichst.'; @override - String get firstCaptureTitle => 'How will you capture?'; + String get onboardingActMicAllow => 'Mikrofon erlauben'; @override - String get firstCaptureDescription => 'Choose how you want to capture your first conversation.'; + String get onboardingActDictateTitle => 'Erzähl Omi von deinem Tag'; @override - String get firstCaptureOmi => 'I Have an Omi'; + String get onboardingActDictateBody => 'Sag es auf deine Art. Omi erledigt den Rest.'; @override - String get firstCapturePhone => 'This Phone'; + String get onboardingActDictateExample => + 'Hatte gerade Mittagessen mit Sam, sie zieht im Juni nach Lissabon! Erinnere mich, die Präsentation bis Freitag ans Team zu schicken und auf dem Heimweg Milch zu holen.'; @override - String get firstCaptureWatch => 'Apple Watch'; + String get onboardingActDictateHint => 'Tippe auf den Haken, wenn du fertig bist'; @override - String get firstCaptureSkip => 'Skip for Now'; + String get onboardingActPhoneMic => 'Handymikrofon'; @override - String get firstCapturePrompt => - 'Tell Omi what you\'re working on today, or what\'s on your mind. Talk for about 30 seconds.'; + String get onboardingActWriting => 'Wird zusammengefasst…'; @override - String get firstCaptureKeepGoing => 'Keep going… 30 seconds is ideal.'; + String get onboardingActRemembers => 'Omi merkt es sich, damit du es nicht musst'; @override - String get firstCaptureReady => 'You can finish now, or keep talking.'; + String get onboardingActNotesLabel => 'Dein Tag, zusammengefasst'; @override - String get firstCaptureStart => 'Start Recording'; + String get onboardingActTasksLabel => 'Omi hat deine To-dos erkannt'; @override - String get firstCaptureFinish => 'Finish Recording'; + String get onboardingActYourWords => 'Das hast du gesagt'; @override - String get firstCaptureListening => 'Listening… your words will appear here.'; + String onboardingActCongratsTitle(String name) { + return 'Alles bereit, $name'; + } @override - String get firstCaptureProcessing => 'Creating your first conversation…'; + String get onboardingActRateTitle => 'Gefällt dir Omi bisher?'; @override - String get firstCaptureResult => 'Your first conversation'; + String get onboardingActRateYes => 'Omi bewerten'; @override - String get firstCaptureEmpty => 'I didn\'t catch enough speech, try again with a few sentences'; + String get onboardingWelcomeCardTitle => 'Willkommen bei Omi'; @override - String get watchCaptureTitle => 'Record from your Apple Watch'; + String get onboardingWelcomeCardBody => + 'Trag Omi und sprich wie gewohnt. Jedes Gespräch wird hier zu Notizen, Aufgaben und Erinnerungen.'; @override - String get watchCaptureOpen => 'Open Omi on your Apple Watch.'; + String get onboardingActDemoTitle => 'Mittagessen mit Sam'; @override - String get watchCaptureRecord => 'Tap Record. Allow microphone access if asked.'; + String get onboardingActDemoSummary => + 'Mittagessen mit Sam, der im Juni nach Lissabon zieht. Du willst die Präsentation bis Freitag ans Team schicken und auf dem Heimweg Milch holen.'; @override - String get watchCaptureSync => 'Your recording syncs to Omi on this phone.'; + String get onboardingActDemoTask1 => 'Präsentation bis Freitag ans Team schicken'; @override - String get pairingBluetoothOff => 'Turn on Bluetooth on your phone, then try again.'; + String get onboardingActDemoTask2 => 'Auf dem Heimweg Milch holen'; @override - String get pairingBluetoothPermission => 'Allow Bluetooth access for Omi in Settings, then try again.'; + String get onboardingActJustNow => 'Gerade eben'; @override - String get pairingNearbyPermission => 'Allow Nearby devices access for Omi in Settings, then try again.'; + String get onboardingActSavedToTasks => 'In deinen Aufgaben gespeichert'; @override - String get pairingLocationPermission => - 'Allow location access for Omi in Settings so this phone can find your device, then try again.'; + String get onboardingConnectTapToConnect => 'Tippe auf dein Omi, um es zu verbinden.'; @override - String get pairingNotFoundOrDropped => - 'Couldn\'t reach your Omi. Make sure it\'s charged, close to your phone, and its light is blinking, then try again.'; + String get onboardingActMicFallbackBody => 'Dein Omi sendet noch keinen Ton. Nutze vorerst dein Handy.'; @override - String get pairingBondRecovery => 'Forget Omi in your phone’s Bluetooth settings, then try pairing again.'; + String get onboardingActNoAudioOmi => 'Dein Omi ist nicht zu hören. Ist es an und in der Nähe?'; @override - String get pairingHowTo => 'How to pair'; + String get onboardingActNoAudioPhone => 'Wir hören dich noch nicht. Sprich etwas lauter.'; + + @override + String get onboardingActDictateBodyOmi => 'Sprich einfach. Dein Omi hört zu.'; + + @override + String get voiceProfileCardDismissed => 'Dein Stimmprofil kannst du jederzeit in den Einstellungen einrichten.'; + + @override + String get homeNewRecordingButton => 'Neue Aufnahme'; + + @override + String get chatWelcomeMessage => + 'Hi, ich bin Omi. Frag mich alles, worüber du gesprochen hast: was jemand gesagt hat, was du versprochen hast oder eine Zusammenfassung deines Tages. Ich kann auch Aufgaben hinzufügen und mir Dinge für dich merken.'; + + @override + String get onboardingStarterTaskAsk => 'Frag Omi nach deinem Tag'; + + @override + String get onboardingStarterTaskRecord => 'Nimm ein Gespräch mit Omi auf'; + + @override + String get onboardingStarterTaskCheck => 'Hake diese Aufgabe ab, um sie zu erledigen'; + + @override + String get paywallRecommended => 'Recommended'; @override - String get firstCaptureWatchInstructions => - 'Open Omi on your Apple Watch and tap Record. Allow microphone access if asked. Your capture will sync to Omi on this phone.'; + String get paywallNoPlans => 'Plans aren\'t available right now. Try again in a minute.'; @override - String get firstCaptureWatchInstall => - 'In the Watch app on your iPhone, install Omi on your Apple Watch. Then open Omi on your watch and tap Record.'; + String get paywallOtherPlans => 'See all plans'; @override - String get firstCaptureWatchPair => - 'Pair your Apple Watch with this iPhone using the Watch app, then install and open Omi on your watch.'; + String get paywallComparePlans => 'See plans'; @override - String get firstCaptureUnavailable => - 'Recording could not start. Check microphone access and your connection, then try again.'; + String get paywallYourPlan => 'Your plan'; @override - String get firstCaptureDelayed => - 'Your recording is taking longer to process. You can check your conversations on Home or try again.'; + String get paywallNotNow => 'Not now'; @override - String get firstCaptureEmptyTitle => 'Let\'s try that again'; + String get paywallTryAgain => 'Try again'; @override - String get firstCaptureShortTitle => 'Finish This Recording?'; + String get paywallManageBilling => 'Manage billing'; @override - String get firstCaptureShortWarning => 'A few more sentences help Omi understand you. About 30 seconds is ideal.'; + String get paywallLoadError => 'Couldn\'t load plans. Check your connection and try again.'; @override - String get firstCaptureCancelTitle => 'Leave This Recording?'; + String get paywallPlans => 'Plans'; @override - String get firstCaptureCancelWarning => - 'Going back stops this recording. Keep talking to give Omi a few more sentences.'; + String get watchCaptureTitle => 'Record from your Apple Watch'; @override - String get firstCaptureSiriTip => 'Tip: Enable Omi in Shortcuts → Siri.'; + String get watchCaptureOpen => 'Open Omi on your Apple Watch.'; + + @override + String get watchCaptureRecord => 'Tap Record. Allow microphone access if asked.'; + + @override + String get watchCaptureSync => 'Your recording syncs to Omi on this phone.'; + + @override + String get pairingBluetoothOff => 'Turn on Bluetooth on your phone, then try again.'; + + @override + String get pairingBluetoothPermission => 'Allow Bluetooth access for Omi in Settings, then try again.'; + + @override + String get pairingNearbyPermission => 'Allow Nearby devices access for Omi in Settings, then try again.'; + + @override + String get pairingLocationPermission => + 'Allow location access for Omi in Settings so this phone can find your device, then try again.'; + + @override + String get pairingNotFoundOrDropped => + 'Couldn\'t reach your Omi. Make sure it\'s charged, close to your phone, and its light is blinking, then try again.'; + + @override + String get pairingBondRecovery => 'Forget Omi in your phone’s Bluetooth settings, then try pairing again.'; + + @override + String get pairingHowTo => 'How to pair'; @override String get paywallOutlineHeadline => 'Verpasse nie wieder ein Wort.'; diff --git a/app/lib/l10n/app_localizations_el.dart b/app/lib/l10n/app_localizations_el.dart index 778a451e048..091659f3ba1 100644 --- a/app/lib/l10n/app_localizations_el.dart +++ b/app/lib/l10n/app_localizations_el.dart @@ -1698,7 +1698,7 @@ class AppLocalizationsEl extends AppLocalizations { @override String get bluetoothNeeded => - 'Το Omi χρειάζεται Bluetooth για σύνδεση με τη φορητή σας συσκευή. Παρακαλώ ενεργοποιήστε το Bluetooth και δοκιμάστε ξανά.'; + 'Το Omi συνδέεται με το τηλέφωνό σας μέσω Bluetooth. Ενεργοποιήστε το για να συνεχίσετε.'; @override String get contactSupport => 'Επικοινωνία με Υποστήριξη;'; @@ -4932,7 +4932,7 @@ class AppLocalizationsEl extends AppLocalizations { 'Συνεχίζοντας, οι συνομιλίες, οι εγγραφές και τα προσωπικά σας στοιχεία θα αποθηκευτούν με ασφάλεια στους διακομιστές μας. Οι ηχογραφήσεις και τα μεταγραφές σας επεξεργάζονται από υπηρεσίες τεχνητής νοημοσύνης τρίτων (συμπεριλαμβανομένων των Deepgram για μεταγραφή και OpenAI για ανάλυση) για να σας παρέχουν γνώσεις βασισμένες σε AI και να ενεργοποιήσουν όλες τις λειτουργίες της εφαρμογής.'; @override - String get tasksEmptyStateMessage => 'Ξεκινήστε μια συνομιλία για να δημιουργήσετε μια εργασία.'; + String get tasksEmptyStateMessage => 'Οι εργασίες από τις συζητήσεις σας θα εμφανίζονται εδώ.'; @override String get clearChatAction => 'Διαγραφή συνομιλίας'; @@ -13123,154 +13123,208 @@ class AppLocalizationsEl extends AppLocalizations { } @override - String get paywallRecommended => 'Recommended'; + String get onboardingPermissionTapToFix => 'Πατήστε για διόρθωση'; @override - String get paywallNoPlans => 'Plans aren\'t available right now. Try again in a minute.'; + String get onboardingNoOmiYet => 'Δεν έχετε ακόμα το Omi σας;'; @override - String get paywallOtherPlans => 'See all plans'; + String get onboardingOmiOrdered => 'Το έχω παραγγείλει'; @override - String get paywallComparePlans => 'See plans'; + String get onboardingOmiNotOrdered => 'Δεν το έχω παραγγείλει ακόμα'; @override - String get paywallYourPlan => 'Your plan'; + String get onboardingConnectOmi => 'Συνδέστε το Omi'; @override - String get paywallNotNow => 'Not now'; + String get consentShortMessage => + 'Το Omi χρησιμοποιεί AI για να μετατρέπει τις συνομιλίες σου σε σημειώσεις. Μπορείς να τις διαγράψεις οποιαδήποτε στιγμή.'; @override - String get paywallTryAgain => 'Try again'; + String consentAiProviders(String providers) { + return 'Επεξεργασία μέσω $providers.'; + } @override - String get paywallManageBilling => 'Manage billing'; + String get onboardingActIntro => 'Ας δοκιμάσουμε το Omi.'; @override - String get paywallLoadError => 'Couldn\'t load plans. Check your connection and try again.'; + String get onboardingActMicTitle => 'Το Omi χρειάζεται το μικρόφωνό σας'; @override - String get paywallPlans => 'Plans'; + String get onboardingActMicBody => 'Για να σας ακούει το Omi όταν του μιλάτε.'; @override - String get firstCaptureTitle => 'How will you capture?'; + String get onboardingActMicAllow => 'Να επιτρέπεται το μικρόφωνο'; @override - String get firstCaptureDescription => 'Choose how you want to capture your first conversation.'; + String get onboardingActDictateTitle => 'Πείτε στο Omi για τη μέρα σας'; @override - String get firstCaptureOmi => 'I Have an Omi'; + String get onboardingActDictateBody => 'Πείτε το με τον τρόπο σας. Το Omi φροντίζει τα υπόλοιπα.'; @override - String get firstCapturePhone => 'This Phone'; + String get onboardingActDictateExample => + 'Μόλις έφαγα με τη Σαμ, μετακομίζει στη Λισαβόνα τον Ιούνιο! Θύμισέ μου να στείλω την παρουσίαση στην ομάδα ως την Παρασκευή και να πάρω γάλα στον δρόμο.'; @override - String get firstCaptureWatch => 'Apple Watch'; + String get onboardingActDictateHint => 'Πατήστε το τικ όταν τελειώσετε'; @override - String get firstCaptureSkip => 'Skip for Now'; + String get onboardingActPhoneMic => 'Μικρόφωνο τηλεφώνου'; @override - String get firstCapturePrompt => - 'Tell Omi what you\'re working on today, or what\'s on your mind. Talk for about 30 seconds.'; + String get onboardingActWriting => 'Γίνεται σύνοψη…'; @override - String get firstCaptureKeepGoing => 'Keep going… 30 seconds is ideal.'; + String get onboardingActRemembers => 'Το Omi θυμάται, για να μη χρειάζεται εσύ'; @override - String get firstCaptureReady => 'You can finish now, or keep talking.'; + String get onboardingActNotesLabel => 'Η μέρα σου, σε σύνοψη'; @override - String get firstCaptureStart => 'Start Recording'; + String get onboardingActTasksLabel => 'Το Omi εντόπισε τις εκκρεμότητές σου'; @override - String get firstCaptureFinish => 'Finish Recording'; + String get onboardingActYourWords => 'Αυτά είπατε'; @override - String get firstCaptureListening => 'Listening… your words will appear here.'; + String onboardingActCongratsTitle(String name) { + return 'Όλα έτοιμα, $name'; + } @override - String get firstCaptureProcessing => 'Creating your first conversation…'; + String get onboardingActRateTitle => 'Σας αρέσει το Omi μέχρι τώρα;'; @override - String get firstCaptureResult => 'Your first conversation'; + String get onboardingActRateYes => 'Αξιολογήστε το Omi'; @override - String get firstCaptureEmpty => 'I didn\'t catch enough speech, try again with a few sentences'; + String get onboardingWelcomeCardTitle => 'Καλώς ήρθατε στο Omi'; @override - String get watchCaptureTitle => 'Record from your Apple Watch'; + String get onboardingWelcomeCardBody => + 'Φορέστε το Omi και μιλήστε όπως πάντα. Κάθε συνομιλία γίνεται εδώ σημειώσεις, εργασίες και αναμνήσεις.'; @override - String get watchCaptureOpen => 'Open Omi on your Apple Watch.'; + String get onboardingActDemoTitle => 'Γεύμα με τη Σαμ'; @override - String get watchCaptureRecord => 'Tap Record. Allow microphone access if asked.'; + String get onboardingActDemoSummary => + 'Μεσημεριανό με τον Σαμ, που μετακομίζει στη Λισαβόνα τον Ιούνιο. Θέλεις να στείλεις την παρουσίαση στην ομάδα ως την Παρασκευή και να πάρεις γάλα στον δρόμο για το σπίτι.'; @override - String get watchCaptureSync => 'Your recording syncs to Omi on this phone.'; + String get onboardingActDemoTask1 => 'Αποστολή της παρουσίασης στην ομάδα ως την Παρασκευή'; @override - String get pairingBluetoothOff => 'Turn on Bluetooth on your phone, then try again.'; + String get onboardingActDemoTask2 => 'Γάλα στον δρόμο για το σπίτι'; @override - String get pairingBluetoothPermission => 'Allow Bluetooth access for Omi in Settings, then try again.'; + String get onboardingActJustNow => 'Μόλις τώρα'; @override - String get pairingNearbyPermission => 'Allow Nearby devices access for Omi in Settings, then try again.'; + String get onboardingActSavedToTasks => 'Αποθηκεύτηκε στις εργασίες σου'; @override - String get pairingLocationPermission => - 'Allow location access for Omi in Settings so this phone can find your device, then try again.'; + String get onboardingConnectTapToConnect => 'Πάτησε το Omi σου για σύνδεση.'; @override - String get pairingNotFoundOrDropped => - 'Couldn\'t reach your Omi. Make sure it\'s charged, close to your phone, and its light is blinking, then try again.'; + String get onboardingActMicFallbackBody => + 'Το Omi δεν στέλνει ακόμα ήχο. Χρησιμοποίησε προς το παρόν το τηλέφωνό σου.'; @override - String get pairingBondRecovery => 'Forget Omi in your phone’s Bluetooth settings, then try pairing again.'; + String get onboardingActNoAudioOmi => 'Δεν ακούμε το Omi. Βεβαιώσου ότι είναι ανοιχτό και κοντά σου.'; @override - String get pairingHowTo => 'How to pair'; + String get onboardingActNoAudioPhone => 'Δεν σε ακούμε ακόμα. Δοκίμασε να μιλήσεις λίγο πιο δυνατά.'; + + @override + String get onboardingActDictateBodyOmi => 'Απλώς μίλα. Το Omi σου ακούει.'; + + @override + String get voiceProfileCardDismissed => 'Μπορείτε να ρυθμίσετε το προφίλ φωνής ανά πάσα στιγμή από τις Ρυθμίσεις.'; + + @override + String get homeNewRecordingButton => 'Νέα ηχογράφηση'; + + @override + String get chatWelcomeMessage => + 'Γεια, είμαι το Omi. Ρώτα με οτιδήποτε έχεις συζητήσει: τι είπε κάποιος, τι υποσχέθηκες ή μια σύνοψη της μέρας σου. Μπορώ επίσης να προσθέτω εργασίες και να θυμάμαι πράγματα για σένα.'; + + @override + String get onboardingStarterTaskAsk => 'Ρώτα το Omi για τη μέρα σου'; + + @override + String get onboardingStarterTaskRecord => 'Ηχογράφησε μια συζήτηση με το Omi'; + + @override + String get onboardingStarterTaskCheck => 'Σημείωσε αυτή την εργασία για να την ολοκληρώσεις'; + + @override + String get paywallRecommended => 'Recommended'; @override - String get firstCaptureWatchInstructions => - 'Open Omi on your Apple Watch and tap Record. Allow microphone access if asked. Your capture will sync to Omi on this phone.'; + String get paywallNoPlans => 'Plans aren\'t available right now. Try again in a minute.'; @override - String get firstCaptureWatchInstall => - 'In the Watch app on your iPhone, install Omi on your Apple Watch. Then open Omi on your watch and tap Record.'; + String get paywallOtherPlans => 'See all plans'; @override - String get firstCaptureWatchPair => - 'Pair your Apple Watch with this iPhone using the Watch app, then install and open Omi on your watch.'; + String get paywallComparePlans => 'See plans'; @override - String get firstCaptureUnavailable => - 'Recording could not start. Check microphone access and your connection, then try again.'; + String get paywallYourPlan => 'Your plan'; @override - String get firstCaptureDelayed => - 'Your recording is taking longer to process. You can check your conversations on Home or try again.'; + String get paywallNotNow => 'Not now'; @override - String get firstCaptureEmptyTitle => 'Let\'s try that again'; + String get paywallTryAgain => 'Try again'; @override - String get firstCaptureShortTitle => 'Finish This Recording?'; + String get paywallManageBilling => 'Manage billing'; @override - String get firstCaptureShortWarning => 'A few more sentences help Omi understand you. About 30 seconds is ideal.'; + String get paywallLoadError => 'Couldn\'t load plans. Check your connection and try again.'; @override - String get firstCaptureCancelTitle => 'Leave This Recording?'; + String get paywallPlans => 'Plans'; @override - String get firstCaptureCancelWarning => - 'Going back stops this recording. Keep talking to give Omi a few more sentences.'; + String get watchCaptureTitle => 'Record from your Apple Watch'; @override - String get firstCaptureSiriTip => 'Tip: Enable Omi in Shortcuts → Siri.'; + String get watchCaptureOpen => 'Open Omi on your Apple Watch.'; + + @override + String get watchCaptureRecord => 'Tap Record. Allow microphone access if asked.'; + + @override + String get watchCaptureSync => 'Your recording syncs to Omi on this phone.'; + + @override + String get pairingBluetoothOff => 'Turn on Bluetooth on your phone, then try again.'; + + @override + String get pairingBluetoothPermission => 'Allow Bluetooth access for Omi in Settings, then try again.'; + + @override + String get pairingNearbyPermission => 'Allow Nearby devices access for Omi in Settings, then try again.'; + + @override + String get pairingLocationPermission => + 'Allow location access for Omi in Settings so this phone can find your device, then try again.'; + + @override + String get pairingNotFoundOrDropped => + 'Couldn\'t reach your Omi. Make sure it\'s charged, close to your phone, and its light is blinking, then try again.'; + + @override + String get pairingBondRecovery => 'Forget Omi in your phone’s Bluetooth settings, then try pairing again.'; + + @override + String get pairingHowTo => 'How to pair'; @override String get paywallOutlineHeadline => 'Μη χάσετε ποτέ ξανά ούτε λέξη.'; diff --git a/app/lib/l10n/app_localizations_en.dart b/app/lib/l10n/app_localizations_en.dart index 02b3e910710..8db705e09ee 100644 --- a/app/lib/l10n/app_localizations_en.dart +++ b/app/lib/l10n/app_localizations_en.dart @@ -1682,8 +1682,7 @@ class AppLocalizationsEn extends AppLocalizations { String get enableBluetooth => 'Enable Bluetooth'; @override - String get bluetoothNeeded => - 'Omi needs Bluetooth to connect to your wearable. Please enable Bluetooth and try again.'; + String get bluetoothNeeded => 'Omi connects to your phone over Bluetooth. Turn it on to continue.'; @override String get contactSupport => 'Contact Support?'; @@ -4891,7 +4890,7 @@ class AppLocalizationsEn extends AppLocalizations { 'By continuing, your conversations, recordings, and personal information will be securely stored on our servers. Your audio recordings and transcripts are processed by third-party AI services — Deepgram for transcription and OpenAI for analysis — to provide you with AI-powered insights and enable all app features.'; @override - String get tasksEmptyStateMessage => 'Start a conversation to create a task.'; + String get tasksEmptyStateMessage => 'To-dos from your conversations will show up here.'; @override String get clearChatAction => 'Clear Chat'; @@ -13025,154 +13024,206 @@ class AppLocalizationsEn extends AppLocalizations { } @override - String get paywallRecommended => 'Recommended'; + String get onboardingPermissionTapToFix => 'Tap to fix'; @override - String get paywallNoPlans => 'Plans aren\'t available right now. Try again in a minute.'; + String get onboardingNoOmiYet => 'Don\'t have your Omi yet?'; @override - String get paywallOtherPlans => 'See all plans'; + String get onboardingOmiOrdered => 'I\'ve ordered mine'; @override - String get paywallComparePlans => 'See plans'; + String get onboardingOmiNotOrdered => 'Not ordered yet'; @override - String get paywallYourPlan => 'Your plan'; + String get onboardingConnectOmi => 'Connect Omi'; @override - String get paywallNotNow => 'Not now'; + String get consentShortMessage => 'Omi uses AI to turn your conversations into notes. You can delete them anytime.'; @override - String get paywallTryAgain => 'Try again'; + String consentAiProviders(String providers) { + return 'Processed with $providers.'; + } @override - String get paywallManageBilling => 'Manage billing'; + String get onboardingActIntro => 'Let\'s try Omi.'; @override - String get paywallLoadError => 'Couldn\'t load plans. Check your connection and try again.'; + String get onboardingActMicTitle => 'Omi needs your microphone'; @override - String get paywallPlans => 'Plans'; + String get onboardingActMicBody => 'So Omi can hear you when you talk to it.'; @override - String get firstCaptureTitle => 'How will you capture?'; + String get onboardingActMicAllow => 'Allow Microphone'; @override - String get firstCaptureDescription => 'Choose how you want to capture your first conversation.'; + String get onboardingActDictateTitle => 'Tell Omi about your day'; @override - String get firstCaptureOmi => 'I Have an Omi'; + String get onboardingActDictateBody => 'Say it your way. Omi handles the rest.'; @override - String get firstCapturePhone => 'This Phone'; + String get onboardingActDictateExample => + 'Just had lunch with Sam, she\'s moving to Lisbon in June! Remind me to send the deck to my team by Friday, and to grab milk on the way home.'; @override - String get firstCaptureWatch => 'Apple Watch'; + String get onboardingActDictateHint => 'Tap the check when you\'re done'; @override - String get firstCaptureSkip => 'Skip for Now'; + String get onboardingActPhoneMic => 'Phone microphone'; @override - String get firstCapturePrompt => - 'Tell Omi what you\'re working on today, or what\'s on your mind. Talk for about 30 seconds.'; + String get onboardingActWriting => 'Summarizing…'; @override - String get firstCaptureKeepGoing => 'Keep going… 30 seconds is ideal.'; + String get onboardingActRemembers => 'Omi remembers so you don\'t have to'; @override - String get firstCaptureReady => 'You can finish now, or keep talking.'; + String get onboardingActNotesLabel => 'Your day, summarized'; @override - String get firstCaptureStart => 'Start Recording'; + String get onboardingActTasksLabel => 'Omi caught your to-dos'; @override - String get firstCaptureFinish => 'Finish Recording'; + String get onboardingActYourWords => 'Here\'s what you said'; @override - String get firstCaptureListening => 'Listening… your words will appear here.'; + String onboardingActCongratsTitle(String name) { + return 'You\'re all set, $name'; + } @override - String get firstCaptureProcessing => 'Creating your first conversation…'; + String get onboardingActRateTitle => 'Enjoying Omi so far?'; @override - String get firstCaptureResult => 'Your first conversation'; + String get onboardingActRateYes => 'Rate Omi'; @override - String get firstCaptureEmpty => 'I didn\'t catch enough speech, try again with a few sentences'; + String get onboardingWelcomeCardTitle => 'Welcome to Omi'; @override - String get watchCaptureTitle => 'Record from your Apple Watch'; + String get onboardingWelcomeCardBody => + 'Wear Omi and talk as usual. Each conversation turns into notes, tasks and memories right here.'; @override - String get watchCaptureOpen => 'Open Omi on your Apple Watch.'; + String get onboardingActDemoTitle => 'Lunch with Sam'; @override - String get watchCaptureRecord => 'Tap Record. Allow microphone access if asked.'; + String get onboardingActDemoSummary => + 'Lunch with Sam, who\'s moving to Lisbon in June. You want to send the deck to your team by Friday and grab milk on the way home.'; @override - String get watchCaptureSync => 'Your recording syncs to Omi on this phone.'; + String get onboardingActDemoTask1 => 'Send the deck to the team by Friday'; @override - String get pairingBluetoothOff => 'Turn on Bluetooth on your phone, then try again.'; + String get onboardingActDemoTask2 => 'Grab milk on the way home'; @override - String get pairingBluetoothPermission => 'Allow Bluetooth access for Omi in Settings, then try again.'; + String get onboardingActJustNow => 'Just now'; @override - String get pairingNearbyPermission => 'Allow Nearby devices access for Omi in Settings, then try again.'; + String get onboardingActSavedToTasks => 'Saved to your tasks'; @override - String get pairingLocationPermission => - 'Allow location access for Omi in Settings so this phone can find your device, then try again.'; + String get onboardingConnectTapToConnect => 'Tap your Omi to connect.'; @override - String get pairingNotFoundOrDropped => - 'Couldn\'t reach your Omi. Make sure it\'s charged, close to your phone, and its light is blinking, then try again.'; + String get onboardingActMicFallbackBody => 'Your Omi isn\'t sending audio yet. Use your phone for now.'; @override - String get pairingBondRecovery => 'Forget Omi in your phone’s Bluetooth settings, then try pairing again.'; + String get onboardingActNoAudioOmi => 'Can\'t hear your Omi. Make sure it\'s on and close by.'; @override - String get pairingHowTo => 'How to pair'; + String get onboardingActNoAudioPhone => 'Can\'t hear you yet. Try speaking a little louder.'; + + @override + String get onboardingActDictateBodyOmi => 'Just talk. Your Omi is listening.'; + + @override + String get voiceProfileCardDismissed => 'You can set up Voice Profile anytime in Settings.'; + + @override + String get homeNewRecordingButton => 'New Recording'; + + @override + String get chatWelcomeMessage => + 'Hi, I\'m Omi. Ask me anything you\'ve talked about: what someone said, what you promised, or a summary of your day. I can also add to-dos and remember things for you.'; + + @override + String get onboardingStarterTaskAsk => 'Ask Omi about your day'; + + @override + String get onboardingStarterTaskRecord => 'Record a conversation with Omi'; + + @override + String get onboardingStarterTaskCheck => 'Check this off to finish a task'; + + @override + String get paywallRecommended => 'Recommended'; @override - String get firstCaptureWatchInstructions => - 'Open Omi on your Apple Watch and tap Record. Allow microphone access if asked. Your capture will sync to Omi on this phone.'; + String get paywallNoPlans => 'Plans aren\'t available right now. Try again in a minute.'; @override - String get firstCaptureWatchInstall => - 'In the Watch app on your iPhone, install Omi on your Apple Watch. Then open Omi on your watch and tap Record.'; + String get paywallOtherPlans => 'See all plans'; @override - String get firstCaptureWatchPair => - 'Pair your Apple Watch with this iPhone using the Watch app, then install and open Omi on your watch.'; + String get paywallComparePlans => 'See plans'; @override - String get firstCaptureUnavailable => - 'Recording could not start. Check microphone access and your connection, then try again.'; + String get paywallYourPlan => 'Your plan'; @override - String get firstCaptureDelayed => - 'Your recording is taking longer to process. You can check your conversations on Home or try again.'; + String get paywallNotNow => 'Not now'; @override - String get firstCaptureEmptyTitle => 'Let\'s try that again'; + String get paywallTryAgain => 'Try again'; @override - String get firstCaptureShortTitle => 'Finish This Recording?'; + String get paywallManageBilling => 'Manage billing'; @override - String get firstCaptureShortWarning => 'A few more sentences help Omi understand you. About 30 seconds is ideal.'; + String get paywallLoadError => 'Couldn\'t load plans. Check your connection and try again.'; @override - String get firstCaptureCancelTitle => 'Leave This Recording?'; + String get paywallPlans => 'Plans'; @override - String get firstCaptureCancelWarning => - 'Going back stops this recording. Keep talking to give Omi a few more sentences.'; + String get watchCaptureTitle => 'Record from your Apple Watch'; @override - String get firstCaptureSiriTip => 'Tip: Enable Omi in Shortcuts → Siri.'; + String get watchCaptureOpen => 'Open Omi on your Apple Watch.'; + + @override + String get watchCaptureRecord => 'Tap Record. Allow microphone access if asked.'; + + @override + String get watchCaptureSync => 'Your recording syncs to Omi on this phone.'; + + @override + String get pairingBluetoothOff => 'Turn on Bluetooth on your phone, then try again.'; + + @override + String get pairingBluetoothPermission => 'Allow Bluetooth access for Omi in Settings, then try again.'; + + @override + String get pairingNearbyPermission => 'Allow Nearby devices access for Omi in Settings, then try again.'; + + @override + String get pairingLocationPermission => + 'Allow location access for Omi in Settings so this phone can find your device, then try again.'; + + @override + String get pairingNotFoundOrDropped => + 'Couldn\'t reach your Omi. Make sure it\'s charged, close to your phone, and its light is blinking, then try again.'; + + @override + String get pairingBondRecovery => 'Forget Omi in your phone’s Bluetooth settings, then try pairing again.'; + + @override + String get pairingHowTo => 'How to pair'; @override String get paywallOutlineHeadline => 'Never miss a word again.'; diff --git a/app/lib/l10n/app_localizations_es.dart b/app/lib/l10n/app_localizations_es.dart index 7500982c51b..7f2de11532e 100644 --- a/app/lib/l10n/app_localizations_es.dart +++ b/app/lib/l10n/app_localizations_es.dart @@ -1683,7 +1683,7 @@ class AppLocalizationsEs extends AppLocalizations { String get enableBluetooth => 'Activar Bluetooth'; @override - String get bluetoothNeeded => 'Omi necesita Bluetooth para conectar tu wearable.'; + String get bluetoothNeeded => 'Omi se conecta a tu teléfono por Bluetooth. Actívalo para continuar.'; @override String get contactSupport => '¿Contactar soporte?'; @@ -4897,7 +4897,7 @@ class AppLocalizationsEs extends AppLocalizations { 'Al continuar, tus conversaciones, grabaciones e información personal se almacenarán de forma segura en nuestros servidores. Tus grabaciones de audio y transcripciones son procesadas por servicios de IA de terceros (incluyendo Deepgram para transcripción y OpenAI para análisis) para proporcionarte información impulsada por IA y habilitar todas las funciones de la aplicación.'; @override - String get tasksEmptyStateMessage => 'Inicia una conversación para crear una tarea.'; + String get tasksEmptyStateMessage => 'Las tareas de tus conversaciones aparecerán aquí.'; @override String get clearChatAction => 'Borrar chat'; @@ -13079,154 +13079,207 @@ class AppLocalizationsEs extends AppLocalizations { } @override - String get paywallRecommended => 'Recommended'; + String get onboardingPermissionTapToFix => 'Toca para solucionarlo'; @override - String get paywallNoPlans => 'Plans aren\'t available right now. Try again in a minute.'; + String get onboardingNoOmiYet => '¿Aún no tienes tu Omi?'; @override - String get paywallOtherPlans => 'See all plans'; + String get onboardingOmiOrdered => 'Ya lo pedí'; @override - String get paywallComparePlans => 'See plans'; + String get onboardingOmiNotOrdered => 'Aún no lo he pedido'; @override - String get paywallYourPlan => 'Your plan'; + String get onboardingConnectOmi => 'Conecta tu Omi'; @override - String get paywallNotNow => 'Not now'; + String get consentShortMessage => + 'Omi usa IA para convertir tus conversaciones en notas. Puedes eliminarlas cuando quieras.'; @override - String get paywallTryAgain => 'Try again'; + String consentAiProviders(String providers) { + return 'Procesado con $providers.'; + } @override - String get paywallManageBilling => 'Manage billing'; + String get onboardingActIntro => 'Probemos Omi.'; @override - String get paywallLoadError => 'Couldn\'t load plans. Check your connection and try again.'; + String get onboardingActMicTitle => 'Omi necesita tu micrófono'; @override - String get paywallPlans => 'Plans'; + String get onboardingActMicBody => 'Para que Omi te escuche cuando le hables.'; @override - String get firstCaptureTitle => 'How will you capture?'; + String get onboardingActMicAllow => 'Permitir micrófono'; @override - String get firstCaptureDescription => 'Choose how you want to capture your first conversation.'; + String get onboardingActDictateTitle => 'Cuéntale a Omi tu día'; @override - String get firstCaptureOmi => 'I Have an Omi'; + String get onboardingActDictateBody => 'Dilo a tu manera. Omi se encarga del resto.'; @override - String get firstCapturePhone => 'This Phone'; + String get onboardingActDictateExample => + '¡Acabo de comer con Sam, se muda a Lisboa en junio! Recuérdame enviar la presentación al equipo antes del viernes y comprar leche de camino a casa.'; @override - String get firstCaptureWatch => 'Apple Watch'; + String get onboardingActDictateHint => 'Toca el check cuando termines'; @override - String get firstCaptureSkip => 'Skip for Now'; + String get onboardingActPhoneMic => 'Micrófono del teléfono'; @override - String get firstCapturePrompt => - 'Tell Omi what you\'re working on today, or what\'s on your mind. Talk for about 30 seconds.'; + String get onboardingActWriting => 'Resumiendo…'; @override - String get firstCaptureKeepGoing => 'Keep going… 30 seconds is ideal.'; + String get onboardingActRemembers => 'Omi lo recuerda para que tú no tengas que hacerlo'; @override - String get firstCaptureReady => 'You can finish now, or keep talking.'; + String get onboardingActNotesLabel => 'Tu día, resumido'; @override - String get firstCaptureStart => 'Start Recording'; + String get onboardingActTasksLabel => 'Omi detectó tus pendientes'; @override - String get firstCaptureFinish => 'Finish Recording'; + String get onboardingActYourWords => 'Esto es lo que dijiste'; @override - String get firstCaptureListening => 'Listening… your words will appear here.'; + String onboardingActCongratsTitle(String name) { + return 'Todo listo, $name'; + } @override - String get firstCaptureProcessing => 'Creating your first conversation…'; + String get onboardingActRateTitle => '¿Te gusta Omi hasta ahora?'; @override - String get firstCaptureResult => 'Your first conversation'; + String get onboardingActRateYes => 'Valorar Omi'; @override - String get firstCaptureEmpty => 'I didn\'t catch enough speech, try again with a few sentences'; + String get onboardingWelcomeCardTitle => 'Te damos la bienvenida a Omi'; @override - String get watchCaptureTitle => 'Record from your Apple Watch'; + String get onboardingWelcomeCardBody => + 'Lleva Omi y habla como siempre. Cada conversación se convierte aquí en notas, tareas y recuerdos.'; @override - String get watchCaptureOpen => 'Open Omi on your Apple Watch.'; + String get onboardingActDemoTitle => 'Comida con Sam'; @override - String get watchCaptureRecord => 'Tap Record. Allow microphone access if asked.'; + String get onboardingActDemoSummary => + 'Almuerzo con Sam, que se muda a Lisboa en junio. Quieres enviar la presentación a tu equipo antes del viernes y comprar leche de camino a casa.'; @override - String get watchCaptureSync => 'Your recording syncs to Omi on this phone.'; + String get onboardingActDemoTask1 => 'Enviar la presentación al equipo antes del viernes'; @override - String get pairingBluetoothOff => 'Turn on Bluetooth on your phone, then try again.'; + String get onboardingActDemoTask2 => 'Comprar leche de camino a casa'; @override - String get pairingBluetoothPermission => 'Allow Bluetooth access for Omi in Settings, then try again.'; + String get onboardingActJustNow => 'Ahora mismo'; @override - String get pairingNearbyPermission => 'Allow Nearby devices access for Omi in Settings, then try again.'; + String get onboardingActSavedToTasks => 'Guardado en tus tareas'; @override - String get pairingLocationPermission => - 'Allow location access for Omi in Settings so this phone can find your device, then try again.'; + String get onboardingConnectTapToConnect => 'Toca tu Omi para conectarlo.'; @override - String get pairingNotFoundOrDropped => - 'Couldn\'t reach your Omi. Make sure it\'s charged, close to your phone, and its light is blinking, then try again.'; + String get onboardingActMicFallbackBody => 'Tu Omi aún no envía audio. Usa tu teléfono por ahora.'; @override - String get pairingBondRecovery => 'Forget Omi in your phone’s Bluetooth settings, then try pairing again.'; + String get onboardingActNoAudioOmi => 'No se oye tu Omi. Comprueba que esté encendido y cerca.'; @override - String get pairingHowTo => 'How to pair'; + String get onboardingActNoAudioPhone => 'Aún no te oímos. Prueba a hablar un poco más alto.'; + + @override + String get onboardingActDictateBodyOmi => 'Solo habla. Tu Omi te escucha.'; + + @override + String get voiceProfileCardDismissed => 'Puedes configurar tu perfil de voz cuando quieras en Ajustes.'; + + @override + String get homeNewRecordingButton => 'Nueva grabación'; + + @override + String get chatWelcomeMessage => + 'Hola, soy Omi. Pregúntame lo que quieras sobre lo que has hablado: lo que dijo alguien, lo que prometiste o un resumen de tu día. También puedo añadir tareas y recordar cosas por ti.'; + + @override + String get onboardingStarterTaskAsk => 'Pregúntale a Omi por tu día'; + + @override + String get onboardingStarterTaskRecord => 'Graba una conversación con Omi'; + + @override + String get onboardingStarterTaskCheck => 'Marca esta tarea para completarla'; + + @override + String get paywallRecommended => 'Recommended'; @override - String get firstCaptureWatchInstructions => - 'Open Omi on your Apple Watch and tap Record. Allow microphone access if asked. Your capture will sync to Omi on this phone.'; + String get paywallNoPlans => 'Plans aren\'t available right now. Try again in a minute.'; @override - String get firstCaptureWatchInstall => - 'In the Watch app on your iPhone, install Omi on your Apple Watch. Then open Omi on your watch and tap Record.'; + String get paywallOtherPlans => 'See all plans'; @override - String get firstCaptureWatchPair => - 'Pair your Apple Watch with this iPhone using the Watch app, then install and open Omi on your watch.'; + String get paywallComparePlans => 'See plans'; @override - String get firstCaptureUnavailable => - 'Recording could not start. Check microphone access and your connection, then try again.'; + String get paywallYourPlan => 'Your plan'; @override - String get firstCaptureDelayed => - 'Your recording is taking longer to process. You can check your conversations on Home or try again.'; + String get paywallNotNow => 'Not now'; @override - String get firstCaptureEmptyTitle => 'Let\'s try that again'; + String get paywallTryAgain => 'Try again'; @override - String get firstCaptureShortTitle => 'Finish This Recording?'; + String get paywallManageBilling => 'Manage billing'; @override - String get firstCaptureShortWarning => 'A few more sentences help Omi understand you. About 30 seconds is ideal.'; + String get paywallLoadError => 'Couldn\'t load plans. Check your connection and try again.'; @override - String get firstCaptureCancelTitle => 'Leave This Recording?'; + String get paywallPlans => 'Plans'; @override - String get firstCaptureCancelWarning => - 'Going back stops this recording. Keep talking to give Omi a few more sentences.'; + String get watchCaptureTitle => 'Record from your Apple Watch'; @override - String get firstCaptureSiriTip => 'Tip: Enable Omi in Shortcuts → Siri.'; + String get watchCaptureOpen => 'Open Omi on your Apple Watch.'; + + @override + String get watchCaptureRecord => 'Tap Record. Allow microphone access if asked.'; + + @override + String get watchCaptureSync => 'Your recording syncs to Omi on this phone.'; + + @override + String get pairingBluetoothOff => 'Turn on Bluetooth on your phone, then try again.'; + + @override + String get pairingBluetoothPermission => 'Allow Bluetooth access for Omi in Settings, then try again.'; + + @override + String get pairingNearbyPermission => 'Allow Nearby devices access for Omi in Settings, then try again.'; + + @override + String get pairingLocationPermission => + 'Allow location access for Omi in Settings so this phone can find your device, then try again.'; + + @override + String get pairingNotFoundOrDropped => + 'Couldn\'t reach your Omi. Make sure it\'s charged, close to your phone, and its light is blinking, then try again.'; + + @override + String get pairingBondRecovery => 'Forget Omi in your phone’s Bluetooth settings, then try pairing again.'; + + @override + String get pairingHowTo => 'How to pair'; @override String get paywallOutlineHeadline => 'No te pierdas ni una palabra nunca más.'; diff --git a/app/lib/l10n/app_localizations_et.dart b/app/lib/l10n/app_localizations_et.dart index 465591451e4..7ae30928a5e 100644 --- a/app/lib/l10n/app_localizations_et.dart +++ b/app/lib/l10n/app_localizations_et.dart @@ -1683,8 +1683,7 @@ class AppLocalizationsEt extends AppLocalizations { String get enableBluetooth => 'Luba Bluetooth'; @override - String get bluetoothNeeded => - 'Omi vajab Bluetoothi, et ühenduda teie kantava seadmega. Palun lubage Bluetooth ja proovige uuesti.'; + String get bluetoothNeeded => 'Omi ühendub telefoniga Bluetoothi kaudu. Lülita see jätkamiseks sisse.'; @override String get contactSupport => 'Võta ühendust toega?'; @@ -4889,7 +4888,7 @@ class AppLocalizationsEt extends AppLocalizations { 'Jätkates salvestatakse teie vestlused, salvestised ja isikuandmed turvaliselt meie serverites. Teie helisalvestisi ja transkriptsioone töötlevad kolmandate osapoolte AI-teenused (sealhulgas Deepgram transkriptsiooni ja OpenAI analüüsi jaoks), et pakkuda teile AI-põhiseid ülevaateid ja võimaldada kõiki rakenduse funktsioone.'; @override - String get tasksEmptyStateMessage => 'Ülesande loomiseks alusta vestlust.'; + String get tasksEmptyStateMessage => 'Sinu vestlustest pärit ülesanded ilmuvad siia.'; @override String get clearChatAction => 'Tühjenda vestlus'; @@ -13034,154 +13033,207 @@ class AppLocalizationsEt extends AppLocalizations { } @override - String get paywallRecommended => 'Recommended'; + String get onboardingPermissionTapToFix => 'Puuduta parandamiseks'; @override - String get paywallNoPlans => 'Plans aren\'t available right now. Try again in a minute.'; + String get onboardingNoOmiYet => 'Sul pole veel Omit?'; @override - String get paywallOtherPlans => 'See all plans'; + String get onboardingOmiOrdered => 'Olen selle tellinud'; @override - String get paywallComparePlans => 'See plans'; + String get onboardingOmiNotOrdered => 'Pole veel tellinud'; @override - String get paywallYourPlan => 'Your plan'; + String get onboardingConnectOmi => 'Ühenda Omi'; @override - String get paywallNotNow => 'Not now'; + String get consentShortMessage => + 'Omi muudab tehisintellekti abil sinu vestlused märkmeteks. Saad need igal ajal kustutada.'; @override - String get paywallTryAgain => 'Try again'; + String consentAiProviders(String providers) { + return 'Töödeldakse teenustega $providers.'; + } @override - String get paywallManageBilling => 'Manage billing'; + String get onboardingActIntro => 'Proovime Omit.'; @override - String get paywallLoadError => 'Couldn\'t load plans. Check your connection and try again.'; + String get onboardingActMicTitle => 'Omi vajab su mikrofoni'; @override - String get paywallPlans => 'Plans'; + String get onboardingActMicBody => 'Et Omi kuuleks sind, kui sa temaga räägid.'; @override - String get firstCaptureTitle => 'How will you capture?'; + String get onboardingActMicAllow => 'Luba mikrofon'; @override - String get firstCaptureDescription => 'Choose how you want to capture your first conversation.'; + String get onboardingActDictateTitle => 'Räägi Omile oma päevast'; @override - String get firstCaptureOmi => 'I Have an Omi'; + String get onboardingActDictateBody => 'Ütle omal moel. Ülejäänu teeb Omi.'; @override - String get firstCapturePhone => 'This Phone'; + String get onboardingActDictateExample => + 'Sõin just Samiga lõunat, ta kolib juunis Lissaboni! Tuleta mulle meelde saata esitlus reedeks tiimile ja osta koduteel piima.'; @override - String get firstCaptureWatch => 'Apple Watch'; + String get onboardingActDictateHint => 'Kui oled valmis, puuduta linnukest'; @override - String get firstCaptureSkip => 'Skip for Now'; + String get onboardingActPhoneMic => 'Telefoni mikrofon'; @override - String get firstCapturePrompt => - 'Tell Omi what you\'re working on today, or what\'s on your mind. Talk for about 30 seconds.'; + String get onboardingActWriting => 'Teeme kokkuvõtet…'; @override - String get firstCaptureKeepGoing => 'Keep going… 30 seconds is ideal.'; + String get onboardingActRemembers => 'Omi mäletab, et sina ei peaks'; @override - String get firstCaptureReady => 'You can finish now, or keep talking.'; + String get onboardingActNotesLabel => 'Sinu päev lühidalt'; @override - String get firstCaptureStart => 'Start Recording'; + String get onboardingActTasksLabel => 'Omi märkas sinu ülesandeid'; @override - String get firstCaptureFinish => 'Finish Recording'; + String get onboardingActYourWords => 'Seda sa ütlesid'; @override - String get firstCaptureListening => 'Listening… your words will appear here.'; + String onboardingActCongratsTitle(String name) { + return 'Kõik on valmis, $name'; + } @override - String get firstCaptureProcessing => 'Creating your first conversation…'; + String get onboardingActRateTitle => 'Kas Omi meeldib sulle seni?'; @override - String get firstCaptureResult => 'Your first conversation'; + String get onboardingActRateYes => 'Hinda Omit'; @override - String get firstCaptureEmpty => 'I didn\'t catch enough speech, try again with a few sentences'; + String get onboardingWelcomeCardTitle => 'Tere tulemast Omisse'; @override - String get watchCaptureTitle => 'Record from your Apple Watch'; + String get onboardingWelcomeCardBody => + 'Kanna Omit ja räägi nagu ikka. Iga vestlus muutub siin märkmeteks, ülesanneteks ja mälestusteks.'; @override - String get watchCaptureOpen => 'Open Omi on your Apple Watch.'; + String get onboardingActDemoTitle => 'Lõuna Samiga'; @override - String get watchCaptureRecord => 'Tap Record. Allow microphone access if asked.'; + String get onboardingActDemoSummary => + 'Lõuna Samiga, kes kolib juunis Lissaboni. Tahad reedeks esitluse meeskonnale saata ja koduteel piima osta.'; @override - String get watchCaptureSync => 'Your recording syncs to Omi on this phone.'; + String get onboardingActDemoTask1 => 'Saata esitlus reedeks tiimile'; @override - String get pairingBluetoothOff => 'Turn on Bluetooth on your phone, then try again.'; + String get onboardingActDemoTask2 => 'Osta koduteel piima'; @override - String get pairingBluetoothPermission => 'Allow Bluetooth access for Omi in Settings, then try again.'; + String get onboardingActJustNow => 'Just praegu'; @override - String get pairingNearbyPermission => 'Allow Nearby devices access for Omi in Settings, then try again.'; + String get onboardingActSavedToTasks => 'Salvestatud sinu ülesannetesse'; @override - String get pairingLocationPermission => - 'Allow location access for Omi in Settings so this phone can find your device, then try again.'; + String get onboardingConnectTapToConnect => 'Ühendamiseks puuduta oma Omit.'; @override - String get pairingNotFoundOrDropped => - 'Couldn\'t reach your Omi. Make sure it\'s charged, close to your phone, and its light is blinking, then try again.'; + String get onboardingActMicFallbackBody => 'Omi ei saada veel heli. Kasuta praegu oma telefoni.'; @override - String get pairingBondRecovery => 'Forget Omi in your phone’s Bluetooth settings, then try pairing again.'; + String get onboardingActNoAudioOmi => 'Omit pole kuulda. Veendu, et see on sees ja lähedal.'; @override - String get pairingHowTo => 'How to pair'; + String get onboardingActNoAudioPhone => 'Me ei kuule sind veel. Proovi natuke valjemini rääkida.'; + + @override + String get onboardingActDictateBodyOmi => 'Lihtsalt räägi. Sinu Omi kuulab.'; + + @override + String get voiceProfileCardDismissed => 'Häälprofiili saad seadistada igal ajal seadetes.'; + + @override + String get homeNewRecordingButton => 'Uus salvestus'; + + @override + String get chatWelcomeMessage => + 'Tere, mina olen Omi. Küsi minult kõike, millest oled rääkinud: mida keegi ütles, mida sa lubasid või kokkuvõtet oma päevast. Saan ka ülesandeid lisada ja asju sinu eest meeles pidada.'; + + @override + String get onboardingStarterTaskAsk => 'Küsi Omilt oma päeva kohta'; + + @override + String get onboardingStarterTaskRecord => 'Salvesta Omiga üks vestlus'; + + @override + String get onboardingStarterTaskCheck => 'Märgi see ülesanne tehtuks'; + + @override + String get paywallRecommended => 'Recommended'; @override - String get firstCaptureWatchInstructions => - 'Open Omi on your Apple Watch and tap Record. Allow microphone access if asked. Your capture will sync to Omi on this phone.'; + String get paywallNoPlans => 'Plans aren\'t available right now. Try again in a minute.'; @override - String get firstCaptureWatchInstall => - 'In the Watch app on your iPhone, install Omi on your Apple Watch. Then open Omi on your watch and tap Record.'; + String get paywallOtherPlans => 'See all plans'; @override - String get firstCaptureWatchPair => - 'Pair your Apple Watch with this iPhone using the Watch app, then install and open Omi on your watch.'; + String get paywallComparePlans => 'See plans'; @override - String get firstCaptureUnavailable => - 'Recording could not start. Check microphone access and your connection, then try again.'; + String get paywallYourPlan => 'Your plan'; @override - String get firstCaptureDelayed => - 'Your recording is taking longer to process. You can check your conversations on Home or try again.'; + String get paywallNotNow => 'Not now'; @override - String get firstCaptureEmptyTitle => 'Let\'s try that again'; + String get paywallTryAgain => 'Try again'; @override - String get firstCaptureShortTitle => 'Finish This Recording?'; + String get paywallManageBilling => 'Manage billing'; @override - String get firstCaptureShortWarning => 'A few more sentences help Omi understand you. About 30 seconds is ideal.'; + String get paywallLoadError => 'Couldn\'t load plans. Check your connection and try again.'; @override - String get firstCaptureCancelTitle => 'Leave This Recording?'; + String get paywallPlans => 'Plans'; @override - String get firstCaptureCancelWarning => - 'Going back stops this recording. Keep talking to give Omi a few more sentences.'; + String get watchCaptureTitle => 'Record from your Apple Watch'; @override - String get firstCaptureSiriTip => 'Tip: Enable Omi in Shortcuts → Siri.'; + String get watchCaptureOpen => 'Open Omi on your Apple Watch.'; + + @override + String get watchCaptureRecord => 'Tap Record. Allow microphone access if asked.'; + + @override + String get watchCaptureSync => 'Your recording syncs to Omi on this phone.'; + + @override + String get pairingBluetoothOff => 'Turn on Bluetooth on your phone, then try again.'; + + @override + String get pairingBluetoothPermission => 'Allow Bluetooth access for Omi in Settings, then try again.'; + + @override + String get pairingNearbyPermission => 'Allow Nearby devices access for Omi in Settings, then try again.'; + + @override + String get pairingLocationPermission => + 'Allow location access for Omi in Settings so this phone can find your device, then try again.'; + + @override + String get pairingNotFoundOrDropped => + 'Couldn\'t reach your Omi. Make sure it\'s charged, close to your phone, and its light is blinking, then try again.'; + + @override + String get pairingBondRecovery => 'Forget Omi in your phone’s Bluetooth settings, then try pairing again.'; + + @override + String get pairingHowTo => 'How to pair'; @override String get paywallOutlineHeadline => 'Ära jäta enam ühtegi sõna vahele.'; diff --git a/app/lib/l10n/app_localizations_fa.dart b/app/lib/l10n/app_localizations_fa.dart index c22835d57a4..ac9bdbc9c1f 100644 --- a/app/lib/l10n/app_localizations_fa.dart +++ b/app/lib/l10n/app_localizations_fa.dart @@ -1685,8 +1685,7 @@ class AppLocalizationsFa extends AppLocalizations { String get enableBluetooth => 'فعال‌سازی Bluetooth'; @override - String get bluetoothNeeded => - 'Omi نیاز به Bluetooth برای اتصال به دستگاه پوشیدنی دارد. لطفاً Bluetooth را فعال کنید و دوباره تلاش کنید.'; + String get bluetoothNeeded => 'Omi از طریق بلوتوث به گوشی شما وصل می‌شود. برای ادامه آن را روشن کنید.'; @override String get contactSupport => 'تماس با پشتیبانی؟'; @@ -4892,7 +4891,7 @@ class AppLocalizationsFa extends AppLocalizations { 'با ادامه دادن، مکالمات، ضبط‌ها و اطلاعات شخصی شما به طور ایمن در سرورهای ما ذخیره می‌شود. ضبط‌های صوتی و رونوشت‌های شما توسط سرویس‌های هوش مصنوعی شخص ثالث (از جمله Deepgram برای رونویسی و OpenAI برای تحلیل) پردازش می‌شوند تا بینش‌های مبتنی بر هوش مصنوعی را به شما ارائه دهند و تمام ویژگی‌های برنامه را فعال کنند.'; @override - String get tasksEmptyStateMessage => 'برای ایجاد یک کار، گفتگو را شروع کنید.'; + String get tasksEmptyStateMessage => 'کارهای گفتگوهای شما اینجا نمایش داده می‌شوند.'; @override String get clearChatAction => 'پاک‌کردن چت'; @@ -13035,154 +13034,207 @@ class AppLocalizationsFa extends AppLocalizations { } @override - String get paywallRecommended => 'Recommended'; + String get onboardingPermissionTapToFix => 'برای رفع ضربه بزنید'; @override - String get paywallNoPlans => 'Plans aren\'t available right now. Try again in a minute.'; + String get onboardingNoOmiYet => 'هنوز Omi ندارید؟'; @override - String get paywallOtherPlans => 'See all plans'; + String get onboardingOmiOrdered => 'سفارش داده‌ام'; @override - String get paywallComparePlans => 'See plans'; + String get onboardingOmiNotOrdered => 'هنوز سفارش نداده‌ام'; @override - String get paywallYourPlan => 'Your plan'; + String get onboardingConnectOmi => 'اتصال Omi'; @override - String get paywallNotNow => 'Not now'; + String get consentShortMessage => + 'Omi با هوش مصنوعی گفتگوهای شما را به یادداشت تبدیل می‌کند. هر زمان بخواهید می‌توانید آن‌ها را حذف کنید.'; @override - String get paywallTryAgain => 'Try again'; + String consentAiProviders(String providers) { + return 'پردازش با $providers.'; + } @override - String get paywallManageBilling => 'Manage billing'; + String get onboardingActIntro => 'بیایید Omi را امتحان کنیم.'; @override - String get paywallLoadError => 'Couldn\'t load plans. Check your connection and try again.'; + String get onboardingActMicTitle => 'Omi به میکروفون شما نیاز دارد'; @override - String get paywallPlans => 'Plans'; + String get onboardingActMicBody => 'تا Omi وقتی با او حرف می‌زنید صدایتان را بشنود.'; @override - String get firstCaptureTitle => 'How will you capture?'; + String get onboardingActMicAllow => 'اجازهٔ میکروفون'; @override - String get firstCaptureDescription => 'Choose how you want to capture your first conversation.'; + String get onboardingActDictateTitle => 'از روزتان به Omi بگویید'; @override - String get firstCaptureOmi => 'I Have an Omi'; + String get onboardingActDictateBody => 'به روش خودتان بگویید. بقیه با Omi.'; @override - String get firstCapturePhone => 'This Phone'; + String get onboardingActDictateExample => + 'همین الان با سم ناهار خوردم، خرداد به لیسبون می‌رود! یادم بینداز تا جمعه ارائه را برای تیم بفرستم و سر راه خانه شیر بخرم.'; @override - String get firstCaptureWatch => 'Apple Watch'; + String get onboardingActDictateHint => 'وقتی تمام شد، روی تیک بزنید'; @override - String get firstCaptureSkip => 'Skip for Now'; + String get onboardingActPhoneMic => 'میکروفون گوشی'; @override - String get firstCapturePrompt => - 'Tell Omi what you\'re working on today, or what\'s on your mind. Talk for about 30 seconds.'; + String get onboardingActWriting => 'در حال خلاصه‌سازی…'; @override - String get firstCaptureKeepGoing => 'Keep going… 30 seconds is ideal.'; + String get onboardingActRemembers => 'Omi به خاطر می‌سپارد تا شما مجبور نباشید'; @override - String get firstCaptureReady => 'You can finish now, or keep talking.'; + String get onboardingActNotesLabel => 'روز شما، خلاصه‌شده'; @override - String get firstCaptureStart => 'Start Recording'; + String get onboardingActTasksLabel => 'Omi کارهای شما را پیدا کرد'; @override - String get firstCaptureFinish => 'Finish Recording'; + String get onboardingActYourWords => 'این چیزی است که گفتید'; @override - String get firstCaptureListening => 'Listening… your words will appear here.'; + String onboardingActCongratsTitle(String name) { + return 'همه چیز آماده است، $name'; + } @override - String get firstCaptureProcessing => 'Creating your first conversation…'; + String get onboardingActRateTitle => 'تا اینجا از Omi راضی هستید؟'; @override - String get firstCaptureResult => 'Your first conversation'; + String get onboardingActRateYes => 'به Omi امتیاز دهید'; @override - String get firstCaptureEmpty => 'I didn\'t catch enough speech, try again with a few sentences'; + String get onboardingWelcomeCardTitle => 'به Omi خوش آمدید'; @override - String get watchCaptureTitle => 'Record from your Apple Watch'; + String get onboardingWelcomeCardBody => + 'Omi را بپوشید و مثل همیشه صحبت کنید. هر گفتگو همین‌جا به یادداشت، کار و خاطره تبدیل می‌شود.'; @override - String get watchCaptureOpen => 'Open Omi on your Apple Watch.'; + String get onboardingActDemoTitle => 'ناهار با سم'; @override - String get watchCaptureRecord => 'Tap Record. Allow microphone access if asked.'; + String get onboardingActDemoSummary => + 'ناهار با سم که خرداد به لیسبون می‌رود. می‌خواهید تا جمعه ارائه را برای تیم بفرستید و سر راه خانه شیر بخرید.'; @override - String get watchCaptureSync => 'Your recording syncs to Omi on this phone.'; + String get onboardingActDemoTask1 => 'فرستادن ارائه برای تیم تا جمعه'; @override - String get pairingBluetoothOff => 'Turn on Bluetooth on your phone, then try again.'; + String get onboardingActDemoTask2 => 'خریدن شیر سر راه خانه'; @override - String get pairingBluetoothPermission => 'Allow Bluetooth access for Omi in Settings, then try again.'; + String get onboardingActJustNow => 'همین الان'; @override - String get pairingNearbyPermission => 'Allow Nearby devices access for Omi in Settings, then try again.'; + String get onboardingActSavedToTasks => 'در کارهای شما ذخیره شد'; @override - String get pairingLocationPermission => - 'Allow location access for Omi in Settings so this phone can find your device, then try again.'; + String get onboardingConnectTapToConnect => 'برای اتصال روی Omi خود ضربه بزنید.'; @override - String get pairingNotFoundOrDropped => - 'Couldn\'t reach your Omi. Make sure it\'s charged, close to your phone, and its light is blinking, then try again.'; + String get onboardingActMicFallbackBody => 'Omi هنوز صدا نمی‌فرستد. فعلاً از گوشی خود استفاده کنید.'; @override - String get pairingBondRecovery => 'Forget Omi in your phone’s Bluetooth settings, then try pairing again.'; + String get onboardingActNoAudioOmi => 'صدای Omi نمی‌آید. مطمئن شوید روشن و نزدیک است.'; @override - String get pairingHowTo => 'How to pair'; + String get onboardingActNoAudioPhone => 'هنوز صدایتان نمی‌آید. کمی بلندتر صحبت کنید.'; + + @override + String get onboardingActDictateBodyOmi => 'فقط صحبت کنید. Omi گوش می‌دهد.'; + + @override + String get voiceProfileCardDismissed => 'هر زمان می‌توانید نمایه صدا را در تنظیمات راه‌اندازی کنید.'; + + @override + String get homeNewRecordingButton => 'ضبط جدید'; + + @override + String get chatWelcomeMessage => + 'سلام، من Omi هستم. هر چیزی درباره صحبت‌هایت بپرس: اینکه کسی چه گفت، چه قولی دادی یا خلاصه‌ای از روزت. می‌توانم کار هم اضافه کنم و چیزها را برایت به خاطر بسپارم.'; + + @override + String get onboardingStarterTaskAsk => 'از Omi درباره روزت بپرس'; + + @override + String get onboardingStarterTaskRecord => 'یک گفتگو را با Omi ضبط کن'; + + @override + String get onboardingStarterTaskCheck => 'این کار را تیک بزن تا تمام شود'; + + @override + String get paywallRecommended => 'Recommended'; @override - String get firstCaptureWatchInstructions => - 'Open Omi on your Apple Watch and tap Record. Allow microphone access if asked. Your capture will sync to Omi on this phone.'; + String get paywallNoPlans => 'Plans aren\'t available right now. Try again in a minute.'; @override - String get firstCaptureWatchInstall => - 'In the Watch app on your iPhone, install Omi on your Apple Watch. Then open Omi on your watch and tap Record.'; + String get paywallOtherPlans => 'See all plans'; @override - String get firstCaptureWatchPair => - 'Pair your Apple Watch with this iPhone using the Watch app, then install and open Omi on your watch.'; + String get paywallComparePlans => 'See plans'; @override - String get firstCaptureUnavailable => - 'Recording could not start. Check microphone access and your connection, then try again.'; + String get paywallYourPlan => 'Your plan'; @override - String get firstCaptureDelayed => - 'Your recording is taking longer to process. You can check your conversations on Home or try again.'; + String get paywallNotNow => 'Not now'; @override - String get firstCaptureEmptyTitle => 'Let\'s try that again'; + String get paywallTryAgain => 'Try again'; @override - String get firstCaptureShortTitle => 'Finish This Recording?'; + String get paywallManageBilling => 'Manage billing'; @override - String get firstCaptureShortWarning => 'A few more sentences help Omi understand you. About 30 seconds is ideal.'; + String get paywallLoadError => 'Couldn\'t load plans. Check your connection and try again.'; @override - String get firstCaptureCancelTitle => 'Leave This Recording?'; + String get paywallPlans => 'Plans'; @override - String get firstCaptureCancelWarning => - 'Going back stops this recording. Keep talking to give Omi a few more sentences.'; + String get watchCaptureTitle => 'Record from your Apple Watch'; @override - String get firstCaptureSiriTip => 'Tip: Enable Omi in Shortcuts → Siri.'; + String get watchCaptureOpen => 'Open Omi on your Apple Watch.'; + + @override + String get watchCaptureRecord => 'Tap Record. Allow microphone access if asked.'; + + @override + String get watchCaptureSync => 'Your recording syncs to Omi on this phone.'; + + @override + String get pairingBluetoothOff => 'Turn on Bluetooth on your phone, then try again.'; + + @override + String get pairingBluetoothPermission => 'Allow Bluetooth access for Omi in Settings, then try again.'; + + @override + String get pairingNearbyPermission => 'Allow Nearby devices access for Omi in Settings, then try again.'; + + @override + String get pairingLocationPermission => + 'Allow location access for Omi in Settings so this phone can find your device, then try again.'; + + @override + String get pairingNotFoundOrDropped => + 'Couldn\'t reach your Omi. Make sure it\'s charged, close to your phone, and its light is blinking, then try again.'; + + @override + String get pairingBondRecovery => 'Forget Omi in your phone’s Bluetooth settings, then try pairing again.'; + + @override + String get pairingHowTo => 'How to pair'; @override String get paywallOutlineHeadline => 'دیگر هیچ کلمه‌ای را از دست ندهید.'; diff --git a/app/lib/l10n/app_localizations_fi.dart b/app/lib/l10n/app_localizations_fi.dart index f7dd019fbf9..483ea583aa0 100644 --- a/app/lib/l10n/app_localizations_fi.dart +++ b/app/lib/l10n/app_localizations_fi.dart @@ -1686,8 +1686,7 @@ class AppLocalizationsFi extends AppLocalizations { String get enableBluetooth => 'Ota Bluetooth käyttöön'; @override - String get bluetoothNeeded => - 'Omi tarvitsee Bluetoothin yhdistääkseen puettavaan laitteeseesi. Ota Bluetooth käyttöön ja yritä uudelleen.'; + String get bluetoothNeeded => 'Omi yhdistyy puhelimeesi Bluetoothilla. Laita se päälle jatkaaksesi.'; @override String get contactSupport => 'Ota yhteyttä tukeen?'; @@ -4894,7 +4893,7 @@ class AppLocalizationsFi extends AppLocalizations { 'Jatkamalla keskustelusi, tallenteet ja henkilötietosi tallennetaan turvallisesti palvelimillemme. Äänitallenteitasi ja transkriptioitasi käsittelevät kolmannen osapuolen tekoälypalvelut (mukaan lukien Deepgram transkriptiota ja OpenAI analyysiä varten) tarjotaksemme sinulle tekoälypohjaisia oivalluksia ja mahdollistaaksemme kaikki sovelluksen ominaisuudet.'; @override - String get tasksEmptyStateMessage => 'Aloita keskustelu luodaksesi tehtävän.'; + String get tasksEmptyStateMessage => 'Keskusteluistasi poimitut tehtävät näkyvät täällä.'; @override String get clearChatAction => 'Tyhjennä keskustelu'; @@ -13045,154 +13044,207 @@ class AppLocalizationsFi extends AppLocalizations { } @override - String get paywallRecommended => 'Recommended'; + String get onboardingPermissionTapToFix => 'Korjaa napauttamalla'; @override - String get paywallNoPlans => 'Plans aren\'t available right now. Try again in a minute.'; + String get onboardingNoOmiYet => 'Eikö Omi ole vielä saapunut?'; @override - String get paywallOtherPlans => 'See all plans'; + String get onboardingOmiOrdered => 'Olen tilannut sen'; @override - String get paywallComparePlans => 'See plans'; + String get onboardingOmiNotOrdered => 'En ole vielä tilannut'; @override - String get paywallYourPlan => 'Your plan'; + String get onboardingConnectOmi => 'Yhdistä Omi'; @override - String get paywallNotNow => 'Not now'; + String get consentShortMessage => + 'Omi tekee keskusteluistasi muistiinpanoja tekoälyn avulla. Voit poistaa ne milloin tahansa.'; @override - String get paywallTryAgain => 'Try again'; + String consentAiProviders(String providers) { + return 'Käsittely: $providers.'; + } @override - String get paywallManageBilling => 'Manage billing'; + String get onboardingActIntro => 'Kokeillaan Omia.'; @override - String get paywallLoadError => 'Couldn\'t load plans. Check your connection and try again.'; + String get onboardingActMicTitle => 'Omi tarvitsee mikrofonisi'; @override - String get paywallPlans => 'Plans'; + String get onboardingActMicBody => 'Jotta Omi kuulee sinut, kun puhut sille.'; @override - String get firstCaptureTitle => 'How will you capture?'; + String get onboardingActMicAllow => 'Salli mikrofoni'; @override - String get firstCaptureDescription => 'Choose how you want to capture your first conversation.'; + String get onboardingActDictateTitle => 'Kerro Omille päivästäsi'; @override - String get firstCaptureOmi => 'I Have an Omi'; + String get onboardingActDictateBody => 'Sano omin sanoin. Omi hoitaa loput.'; @override - String get firstCapturePhone => 'This Phone'; + String get onboardingActDictateExample => + 'Söin juuri lounasta Samin kanssa, hän muuttaa kesäkuussa Lissaboniin! Muistuta minua lähettämään esitys tiimille perjantaihin mennessä ja hakemaan maitoa kotimatkalla.'; @override - String get firstCaptureWatch => 'Apple Watch'; + String get onboardingActDictateHint => 'Napauta valintamerkkiä, kun olet valmis'; @override - String get firstCaptureSkip => 'Skip for Now'; + String get onboardingActPhoneMic => 'Puhelimen mikrofoni'; @override - String get firstCapturePrompt => - 'Tell Omi what you\'re working on today, or what\'s on your mind. Talk for about 30 seconds.'; + String get onboardingActWriting => 'Tiivistetään…'; @override - String get firstCaptureKeepGoing => 'Keep going… 30 seconds is ideal.'; + String get onboardingActRemembers => 'Omi muistaa, ettei sinun tarvitse'; @override - String get firstCaptureReady => 'You can finish now, or keep talking.'; + String get onboardingActNotesLabel => 'Päiväsi tiivistettynä'; @override - String get firstCaptureStart => 'Start Recording'; + String get onboardingActTasksLabel => 'Omi poimi tehtäväsi'; @override - String get firstCaptureFinish => 'Finish Recording'; + String get onboardingActYourWords => 'Tämän sanoit'; @override - String get firstCaptureListening => 'Listening… your words will appear here.'; + String onboardingActCongratsTitle(String name) { + return 'Kaikki valmista, $name'; + } @override - String get firstCaptureProcessing => 'Creating your first conversation…'; + String get onboardingActRateTitle => 'Pidätkö Omista tähän mennessä?'; @override - String get firstCaptureResult => 'Your first conversation'; + String get onboardingActRateYes => 'Arvioi Omi'; @override - String get firstCaptureEmpty => 'I didn\'t catch enough speech, try again with a few sentences'; + String get onboardingWelcomeCardTitle => 'Tervetuloa Omiin'; @override - String get watchCaptureTitle => 'Record from your Apple Watch'; + String get onboardingWelcomeCardBody => + 'Pidä Omia ja puhu kuten tavallisesti. Jokaisesta keskustelusta tulee tänne muistiinpanoja, tehtäviä ja muistoja.'; @override - String get watchCaptureOpen => 'Open Omi on your Apple Watch.'; + String get onboardingActDemoTitle => 'Lounas Samin kanssa'; @override - String get watchCaptureRecord => 'Tap Record. Allow microphone access if asked.'; + String get onboardingActDemoSummary => + 'Lounas Samin kanssa, joka muuttaa kesäkuussa Lissaboniin. Haluat lähettää esityksen tiimille perjantaihin mennessä ja ostaa maitoa kotimatkalla.'; @override - String get watchCaptureSync => 'Your recording syncs to Omi on this phone.'; + String get onboardingActDemoTask1 => 'Lähetä esitys tiimille perjantaihin mennessä'; @override - String get pairingBluetoothOff => 'Turn on Bluetooth on your phone, then try again.'; + String get onboardingActDemoTask2 => 'Hae maitoa kotimatkalla'; @override - String get pairingBluetoothPermission => 'Allow Bluetooth access for Omi in Settings, then try again.'; + String get onboardingActJustNow => 'Juuri nyt'; @override - String get pairingNearbyPermission => 'Allow Nearby devices access for Omi in Settings, then try again.'; + String get onboardingActSavedToTasks => 'Tallennettu tehtäviisi'; @override - String get pairingLocationPermission => - 'Allow location access for Omi in Settings so this phone can find your device, then try again.'; + String get onboardingConnectTapToConnect => 'Yhdistä napauttamalla Omiasi.'; @override - String get pairingNotFoundOrDropped => - 'Couldn\'t reach your Omi. Make sure it\'s charged, close to your phone, and its light is blinking, then try again.'; + String get onboardingActMicFallbackBody => 'Omi ei vielä lähetä ääntä. Käytä toistaiseksi puhelinta.'; @override - String get pairingBondRecovery => 'Forget Omi in your phone’s Bluetooth settings, then try pairing again.'; + String get onboardingActNoAudioOmi => 'Omia ei kuulu. Varmista, että se on päällä ja lähellä.'; @override - String get pairingHowTo => 'How to pair'; + String get onboardingActNoAudioPhone => 'Emme kuule sinua vielä. Kokeile puhua vähän kovempaa.'; + + @override + String get onboardingActDictateBodyOmi => 'Puhu vain. Omisi kuuntelee.'; + + @override + String get voiceProfileCardDismissed => 'Voit määrittää ääniprofiilin milloin tahansa asetuksissa.'; + + @override + String get homeNewRecordingButton => 'Uusi tallennus'; + + @override + String get chatWelcomeMessage => + 'Hei, olen Omi. Kysy minulta mitä tahansa siitä, mistä olet puhunut: mitä joku sanoi, mitä lupasit tai yhteenveto päivästäsi. Voin myös lisätä tehtäviä ja muistaa asioita puolestasi.'; + + @override + String get onboardingStarterTaskAsk => 'Kysy Omilta päivästäsi'; + + @override + String get onboardingStarterTaskRecord => 'Tallenna keskustelu Omilla'; + + @override + String get onboardingStarterTaskCheck => 'Merkitse tämä tehtävä tehdyksi'; + + @override + String get paywallRecommended => 'Recommended'; @override - String get firstCaptureWatchInstructions => - 'Open Omi on your Apple Watch and tap Record. Allow microphone access if asked. Your capture will sync to Omi on this phone.'; + String get paywallNoPlans => 'Plans aren\'t available right now. Try again in a minute.'; @override - String get firstCaptureWatchInstall => - 'In the Watch app on your iPhone, install Omi on your Apple Watch. Then open Omi on your watch and tap Record.'; + String get paywallOtherPlans => 'See all plans'; @override - String get firstCaptureWatchPair => - 'Pair your Apple Watch with this iPhone using the Watch app, then install and open Omi on your watch.'; + String get paywallComparePlans => 'See plans'; @override - String get firstCaptureUnavailable => - 'Recording could not start. Check microphone access and your connection, then try again.'; + String get paywallYourPlan => 'Your plan'; @override - String get firstCaptureDelayed => - 'Your recording is taking longer to process. You can check your conversations on Home or try again.'; + String get paywallNotNow => 'Not now'; @override - String get firstCaptureEmptyTitle => 'Let\'s try that again'; + String get paywallTryAgain => 'Try again'; @override - String get firstCaptureShortTitle => 'Finish This Recording?'; + String get paywallManageBilling => 'Manage billing'; @override - String get firstCaptureShortWarning => 'A few more sentences help Omi understand you. About 30 seconds is ideal.'; + String get paywallLoadError => 'Couldn\'t load plans. Check your connection and try again.'; @override - String get firstCaptureCancelTitle => 'Leave This Recording?'; + String get paywallPlans => 'Plans'; @override - String get firstCaptureCancelWarning => - 'Going back stops this recording. Keep talking to give Omi a few more sentences.'; + String get watchCaptureTitle => 'Record from your Apple Watch'; @override - String get firstCaptureSiriTip => 'Tip: Enable Omi in Shortcuts → Siri.'; + String get watchCaptureOpen => 'Open Omi on your Apple Watch.'; + + @override + String get watchCaptureRecord => 'Tap Record. Allow microphone access if asked.'; + + @override + String get watchCaptureSync => 'Your recording syncs to Omi on this phone.'; + + @override + String get pairingBluetoothOff => 'Turn on Bluetooth on your phone, then try again.'; + + @override + String get pairingBluetoothPermission => 'Allow Bluetooth access for Omi in Settings, then try again.'; + + @override + String get pairingNearbyPermission => 'Allow Nearby devices access for Omi in Settings, then try again.'; + + @override + String get pairingLocationPermission => + 'Allow location access for Omi in Settings so this phone can find your device, then try again.'; + + @override + String get pairingNotFoundOrDropped => + 'Couldn\'t reach your Omi. Make sure it\'s charged, close to your phone, and its light is blinking, then try again.'; + + @override + String get pairingBondRecovery => 'Forget Omi in your phone’s Bluetooth settings, then try pairing again.'; + + @override + String get pairingHowTo => 'How to pair'; @override String get paywallOutlineHeadline => 'Älä enää koskaan missaa sanaakaan.'; diff --git a/app/lib/l10n/app_localizations_fr.dart b/app/lib/l10n/app_localizations_fr.dart index a109adecc30..0d630a1765b 100644 --- a/app/lib/l10n/app_localizations_fr.dart +++ b/app/lib/l10n/app_localizations_fr.dart @@ -1701,8 +1701,7 @@ class AppLocalizationsFr extends AppLocalizations { String get enableBluetooth => 'Activer le Bluetooth'; @override - String get bluetoothNeeded => - 'Omi a besoin du Bluetooth pour se connecter à votre wearable. Veuillez activer le Bluetooth et réessayer.'; + String get bluetoothNeeded => 'Omi se connecte à votre téléphone en Bluetooth. Activez-le pour continuer.'; @override String get contactSupport => 'Contacter le support ?'; @@ -4942,7 +4941,7 @@ class AppLocalizationsFr extends AppLocalizations { 'En continuant, vos conversations, enregistrements et informations personnelles seront stockés en toute sécurité sur nos serveurs. Vos enregistrements audio et transcriptions sont traités par des services d\'IA tiers (notamment Deepgram pour la transcription et OpenAI pour l\'analyse) afin de vous fournir des informations alimentées par l\'IA et d\'activer toutes les fonctionnalités de l\'application.'; @override - String get tasksEmptyStateMessage => 'Démarrez une conversation pour créer une tâche.'; + String get tasksEmptyStateMessage => 'Les tâches issues de vos conversations apparaîtront ici.'; @override String get clearChatAction => 'Effacer le chat'; @@ -13143,154 +13142,208 @@ class AppLocalizationsFr extends AppLocalizations { } @override - String get paywallRecommended => 'Recommended'; + String get onboardingPermissionTapToFix => 'Touchez pour corriger'; @override - String get paywallNoPlans => 'Plans aren\'t available right now. Try again in a minute.'; + String get onboardingNoOmiYet => 'Vous n\'avez pas encore votre Omi ?'; @override - String get paywallOtherPlans => 'See all plans'; + String get onboardingOmiOrdered => 'Je l\'ai commandé'; @override - String get paywallComparePlans => 'See plans'; + String get onboardingOmiNotOrdered => 'Pas encore commandé'; @override - String get paywallYourPlan => 'Your plan'; + String get onboardingConnectOmi => 'Connecter Omi'; @override - String get paywallNotNow => 'Not now'; + String get consentShortMessage => + 'Omi utilise l\'IA pour transformer vos conversations en notes. Vous pouvez les supprimer à tout moment.'; @override - String get paywallTryAgain => 'Try again'; + String consentAiProviders(String providers) { + return 'Traité avec $providers.'; + } @override - String get paywallManageBilling => 'Manage billing'; + String get onboardingActIntro => 'Essayons Omi.'; @override - String get paywallLoadError => 'Couldn\'t load plans. Check your connection and try again.'; + String get onboardingActMicTitle => 'Omi a besoin de votre micro'; @override - String get paywallPlans => 'Plans'; + String get onboardingActMicBody => 'Pour qu\'Omi vous entende quand vous lui parlez.'; @override - String get firstCaptureTitle => 'How will you capture?'; + String get onboardingActMicAllow => 'Autoriser le micro'; @override - String get firstCaptureDescription => 'Choose how you want to capture your first conversation.'; + String get onboardingActDictateTitle => 'Racontez votre journée à Omi'; @override - String get firstCaptureOmi => 'I Have an Omi'; + String get onboardingActDictateBody => 'Dites-le à votre façon. Omi s\'occupe du reste.'; @override - String get firstCapturePhone => 'This Phone'; + String get onboardingActDictateExample => + 'Je viens de déjeuner avec Sam, elle déménage à Lisbonne en juin ! Rappelle-moi d\'envoyer la présentation à l\'équipe d\'ici vendredi et de prendre du lait en rentrant.'; @override - String get firstCaptureWatch => 'Apple Watch'; + String get onboardingActDictateHint => 'Touchez la coche quand vous avez fini'; @override - String get firstCaptureSkip => 'Skip for Now'; + String get onboardingActPhoneMic => 'Micro du téléphone'; @override - String get firstCapturePrompt => - 'Tell Omi what you\'re working on today, or what\'s on your mind. Talk for about 30 seconds.'; + String get onboardingActWriting => 'Résumé en cours…'; @override - String get firstCaptureKeepGoing => 'Keep going… 30 seconds is ideal.'; + String get onboardingActRemembers => 'Omi s\'en souvient pour vous'; @override - String get firstCaptureReady => 'You can finish now, or keep talking.'; + String get onboardingActNotesLabel => 'Votre journée, résumée'; @override - String get firstCaptureStart => 'Start Recording'; + String get onboardingActTasksLabel => 'Omi a repéré vos tâches'; @override - String get firstCaptureFinish => 'Finish Recording'; + String get onboardingActYourWords => 'Voici ce que vous avez dit'; @override - String get firstCaptureListening => 'Listening… your words will appear here.'; + String onboardingActCongratsTitle(String name) { + return 'Tout est prêt, $name'; + } @override - String get firstCaptureProcessing => 'Creating your first conversation…'; + String get onboardingActRateTitle => 'Omi vous plaît pour l\'instant ?'; @override - String get firstCaptureResult => 'Your first conversation'; + String get onboardingActRateYes => 'Noter Omi'; @override - String get firstCaptureEmpty => 'I didn\'t catch enough speech, try again with a few sentences'; + String get onboardingWelcomeCardTitle => 'Bienvenue dans Omi'; @override - String get watchCaptureTitle => 'Record from your Apple Watch'; + String get onboardingWelcomeCardBody => + 'Portez Omi et parlez normalement. Chaque conversation devient ici des notes, des tâches et des souvenirs.'; @override - String get watchCaptureOpen => 'Open Omi on your Apple Watch.'; + String get onboardingActDemoTitle => 'Déjeuner avec Sam'; @override - String get watchCaptureRecord => 'Tap Record. Allow microphone access if asked.'; + String get onboardingActDemoSummary => + 'Déjeuner avec Sam, qui part vivre à Lisbonne en juin. Vous voulez envoyer la présentation à l\'équipe d\'ici vendredi et prendre du lait en rentrant.'; @override - String get watchCaptureSync => 'Your recording syncs to Omi on this phone.'; + String get onboardingActDemoTask1 => 'Envoyer la présentation à l\'équipe d\'ici vendredi'; @override - String get pairingBluetoothOff => 'Turn on Bluetooth on your phone, then try again.'; + String get onboardingActDemoTask2 => 'Prendre du lait en rentrant'; @override - String get pairingBluetoothPermission => 'Allow Bluetooth access for Omi in Settings, then try again.'; + String get onboardingActJustNow => 'À l\'instant'; @override - String get pairingNearbyPermission => 'Allow Nearby devices access for Omi in Settings, then try again.'; + String get onboardingActSavedToTasks => 'Enregistré dans vos tâches'; @override - String get pairingLocationPermission => - 'Allow location access for Omi in Settings so this phone can find your device, then try again.'; + String get onboardingConnectTapToConnect => 'Touchez votre Omi pour le connecter.'; @override - String get pairingNotFoundOrDropped => - 'Couldn\'t reach your Omi. Make sure it\'s charged, close to your phone, and its light is blinking, then try again.'; + String get onboardingActMicFallbackBody => + 'Votre Omi n\'envoie pas encore de son. Utilisez votre téléphone pour l\'instant.'; @override - String get pairingBondRecovery => 'Forget Omi in your phone’s Bluetooth settings, then try pairing again.'; + String get onboardingActNoAudioOmi => 'On n\'entend pas votre Omi. Vérifiez qu\'il est allumé et proche.'; @override - String get pairingHowTo => 'How to pair'; + String get onboardingActNoAudioPhone => 'On ne vous entend pas encore. Parlez un peu plus fort.'; + + @override + String get onboardingActDictateBodyOmi => 'Parlez simplement. Votre Omi vous écoute.'; + + @override + String get voiceProfileCardDismissed => 'Vous pouvez configurer votre profil vocal à tout moment dans les Réglages.'; + + @override + String get homeNewRecordingButton => 'Nouvel enregistrement'; + + @override + String get chatWelcomeMessage => + 'Salut, je suis Omi. Demandez-moi tout ce dont vous avez parlé : ce que quelqu\'un a dit, ce que vous avez promis ou un résumé de votre journée. Je peux aussi ajouter des tâches et retenir des choses pour vous.'; + + @override + String get onboardingStarterTaskAsk => 'Demandez à Omi comment s\'est passée votre journée'; + + @override + String get onboardingStarterTaskRecord => 'Enregistrez une conversation avec Omi'; + + @override + String get onboardingStarterTaskCheck => 'Cochez cette tâche pour la terminer'; + + @override + String get paywallRecommended => 'Recommended'; @override - String get firstCaptureWatchInstructions => - 'Open Omi on your Apple Watch and tap Record. Allow microphone access if asked. Your capture will sync to Omi on this phone.'; + String get paywallNoPlans => 'Plans aren\'t available right now. Try again in a minute.'; @override - String get firstCaptureWatchInstall => - 'In the Watch app on your iPhone, install Omi on your Apple Watch. Then open Omi on your watch and tap Record.'; + String get paywallOtherPlans => 'See all plans'; @override - String get firstCaptureWatchPair => - 'Pair your Apple Watch with this iPhone using the Watch app, then install and open Omi on your watch.'; + String get paywallComparePlans => 'See plans'; @override - String get firstCaptureUnavailable => - 'Recording could not start. Check microphone access and your connection, then try again.'; + String get paywallYourPlan => 'Your plan'; @override - String get firstCaptureDelayed => - 'Your recording is taking longer to process. You can check your conversations on Home or try again.'; + String get paywallNotNow => 'Not now'; @override - String get firstCaptureEmptyTitle => 'Let\'s try that again'; + String get paywallTryAgain => 'Try again'; @override - String get firstCaptureShortTitle => 'Finish This Recording?'; + String get paywallManageBilling => 'Manage billing'; @override - String get firstCaptureShortWarning => 'A few more sentences help Omi understand you. About 30 seconds is ideal.'; + String get paywallLoadError => 'Couldn\'t load plans. Check your connection and try again.'; @override - String get firstCaptureCancelTitle => 'Leave This Recording?'; + String get paywallPlans => 'Plans'; @override - String get firstCaptureCancelWarning => - 'Going back stops this recording. Keep talking to give Omi a few more sentences.'; + String get watchCaptureTitle => 'Record from your Apple Watch'; @override - String get firstCaptureSiriTip => 'Tip: Enable Omi in Shortcuts → Siri.'; + String get watchCaptureOpen => 'Open Omi on your Apple Watch.'; + + @override + String get watchCaptureRecord => 'Tap Record. Allow microphone access if asked.'; + + @override + String get watchCaptureSync => 'Your recording syncs to Omi on this phone.'; + + @override + String get pairingBluetoothOff => 'Turn on Bluetooth on your phone, then try again.'; + + @override + String get pairingBluetoothPermission => 'Allow Bluetooth access for Omi in Settings, then try again.'; + + @override + String get pairingNearbyPermission => 'Allow Nearby devices access for Omi in Settings, then try again.'; + + @override + String get pairingLocationPermission => + 'Allow location access for Omi in Settings so this phone can find your device, then try again.'; + + @override + String get pairingNotFoundOrDropped => + 'Couldn\'t reach your Omi. Make sure it\'s charged, close to your phone, and its light is blinking, then try again.'; + + @override + String get pairingBondRecovery => 'Forget Omi in your phone’s Bluetooth settings, then try pairing again.'; + + @override + String get pairingHowTo => 'How to pair'; @override String get paywallOutlineHeadline => 'Ne manquez plus jamais un mot.'; diff --git a/app/lib/l10n/app_localizations_he.dart b/app/lib/l10n/app_localizations_he.dart index 0faa95a2ed9..8dd0bc4a976 100644 --- a/app/lib/l10n/app_localizations_he.dart +++ b/app/lib/l10n/app_localizations_he.dart @@ -1675,7 +1675,7 @@ class AppLocalizationsHe extends AppLocalizations { String get enableBluetooth => 'הפעל Bluetooth'; @override - String get bluetoothNeeded => 'Omi זקוק ל-Bluetooth כדי להתחבר לציוד הלביש שלך. בבקשה הפעל Bluetooth ונסה שנית.'; + String get bluetoothNeeded => 'Omi מתחבר לטלפון דרך Bluetooth. הפעילו אותו כדי להמשיך.'; @override String get contactSupport => 'יצור קשר עם תמיכה?'; @@ -4854,7 +4854,7 @@ class AppLocalizationsHe extends AppLocalizations { 'בהמשך, השיחות, ההקלטות והמידע האישי שלך יאוחסנו בצורה מאובטחת בשרתים שלנו. הקלטות האודיו והתמלולים שלך מעובדים על ידי שירותי AI של צד שלישי (כולל Deepgram לתמלול ו-OpenAI לניתוח) כדי לספק לך תובנות מבוססות AI ולאפשר את כל תכונות האפליקציה.'; @override - String get tasksEmptyStateMessage => 'התחילו שיחה כדי ליצור משימה.'; + String get tasksEmptyStateMessage => 'משימות מהשיחות שלך יופיעו כאן.'; @override String get clearChatAction => 'נקה שיחה'; @@ -12947,154 +12947,205 @@ class AppLocalizationsHe extends AppLocalizations { } @override - String get paywallRecommended => 'Recommended'; + String get onboardingPermissionTapToFix => 'הקישו לתיקון'; @override - String get paywallNoPlans => 'Plans aren\'t available right now. Try again in a minute.'; + String get onboardingNoOmiYet => 'עדיין אין לכם Omi?'; @override - String get paywallOtherPlans => 'See all plans'; + String get onboardingOmiOrdered => 'הזמנתי'; @override - String get paywallComparePlans => 'See plans'; + String get onboardingOmiNotOrdered => 'עוד לא הזמנתי'; @override - String get paywallYourPlan => 'Your plan'; + String get onboardingConnectOmi => 'חיבור Omi'; @override - String get paywallNotNow => 'Not now'; + String get consentShortMessage => 'Omi משתמש בבינה מלאכותית כדי להפוך את השיחות שלך להערות. אפשר למחוק אותן בכל עת.'; @override - String get paywallTryAgain => 'Try again'; + String consentAiProviders(String providers) { + return 'מעובד באמצעות $providers.'; + } @override - String get paywallManageBilling => 'Manage billing'; + String get onboardingActIntro => 'בואו ננסה את Omi.'; @override - String get paywallLoadError => 'Couldn\'t load plans. Check your connection and try again.'; + String get onboardingActMicTitle => 'Omi צריך את המיקרופון'; @override - String get paywallPlans => 'Plans'; + String get onboardingActMicBody => 'כדי ש-Omi ישמע אתכם כשאתם מדברים אליו.'; @override - String get firstCaptureTitle => 'How will you capture?'; + String get onboardingActMicAllow => 'אפשר מיקרופון'; @override - String get firstCaptureDescription => 'Choose how you want to capture your first conversation.'; + String get onboardingActDictateTitle => 'ספרו ל-Omi על היום שלכם'; @override - String get firstCaptureOmi => 'I Have an Omi'; + String get onboardingActDictateBody => 'תגידו את זה בדרך שלכם. Omi ידאג לשאר.'; @override - String get firstCapturePhone => 'This Phone'; + String get onboardingActDictateExample => + 'הרגע אכלתי צהריים עם סאם, היא עוברת לליסבון ביוני! תזכיר לי לשלוח את המצגת לצוות עד שישי, ולקנות חלב בדרך הביתה.'; @override - String get firstCaptureWatch => 'Apple Watch'; + String get onboardingActDictateHint => 'הקישו על ה-V כשתסיימו'; @override - String get firstCaptureSkip => 'Skip for Now'; + String get onboardingActPhoneMic => 'מיקרופון הטלפון'; @override - String get firstCapturePrompt => - 'Tell Omi what you\'re working on today, or what\'s on your mind. Talk for about 30 seconds.'; + String get onboardingActWriting => 'מסכם…'; @override - String get firstCaptureKeepGoing => 'Keep going… 30 seconds is ideal.'; + String get onboardingActRemembers => 'Omi זוכר, כדי שאתם לא תצטרכו'; @override - String get firstCaptureReady => 'You can finish now, or keep talking.'; + String get onboardingActNotesLabel => 'היום שלך, בתמצית'; @override - String get firstCaptureStart => 'Start Recording'; + String get onboardingActTasksLabel => 'Omi תפס את המשימות שלך'; @override - String get firstCaptureFinish => 'Finish Recording'; + String get onboardingActYourWords => 'זה מה שאמרת'; @override - String get firstCaptureListening => 'Listening… your words will appear here.'; + String onboardingActCongratsTitle(String name) { + return 'הכול מוכן, $name'; + } @override - String get firstCaptureProcessing => 'Creating your first conversation…'; + String get onboardingActRateTitle => 'נהנים מ-Omi עד עכשיו?'; @override - String get firstCaptureResult => 'Your first conversation'; + String get onboardingActRateYes => 'דרגו את Omi'; @override - String get firstCaptureEmpty => 'I didn\'t catch enough speech, try again with a few sentences'; + String get onboardingWelcomeCardTitle => 'ברוכים הבאים ל-Omi'; @override - String get watchCaptureTitle => 'Record from your Apple Watch'; + String get onboardingWelcomeCardBody => 'ענדו את Omi ודברו כרגיל. כל שיחה הופכת כאן להערות, משימות וזיכרונות.'; @override - String get watchCaptureOpen => 'Open Omi on your Apple Watch.'; + String get onboardingActDemoTitle => 'צהריים עם סאם'; @override - String get watchCaptureRecord => 'Tap Record. Allow microphone access if asked.'; + String get onboardingActDemoSummary => + 'ארוחת צהריים עם סם, שעוברת ללישבון ביוני. אתה רוצה לשלוח את המצגת לצוות עד יום שישי ולקנות חלב בדרך הביתה.'; @override - String get watchCaptureSync => 'Your recording syncs to Omi on this phone.'; + String get onboardingActDemoTask1 => 'לשלוח את המצגת לצוות עד שישי'; @override - String get pairingBluetoothOff => 'Turn on Bluetooth on your phone, then try again.'; + String get onboardingActDemoTask2 => 'לקנות חלב בדרך הביתה'; @override - String get pairingBluetoothPermission => 'Allow Bluetooth access for Omi in Settings, then try again.'; + String get onboardingActJustNow => 'עכשיו'; @override - String get pairingNearbyPermission => 'Allow Nearby devices access for Omi in Settings, then try again.'; + String get onboardingActSavedToTasks => 'נשמר במשימות שלך'; @override - String get pairingLocationPermission => - 'Allow location access for Omi in Settings so this phone can find your device, then try again.'; + String get onboardingConnectTapToConnect => 'הקש על ה-Omi שלך כדי להתחבר.'; @override - String get pairingNotFoundOrDropped => - 'Couldn\'t reach your Omi. Make sure it\'s charged, close to your phone, and its light is blinking, then try again.'; + String get onboardingActMicFallbackBody => 'ה-Omi עדיין לא שולח שמע. בינתיים השתמש בטלפון.'; @override - String get pairingBondRecovery => 'Forget Omi in your phone’s Bluetooth settings, then try pairing again.'; + String get onboardingActNoAudioOmi => 'לא שומעים את ה-Omi. ודא שהוא דולק וקרוב.'; @override - String get pairingHowTo => 'How to pair'; + String get onboardingActNoAudioPhone => 'עדיין לא שומעים אותך. נסה לדבר קצת יותר חזק.'; + + @override + String get onboardingActDictateBodyOmi => 'פשוט דבר. ה-Omi שלך מקשיב.'; + + @override + String get voiceProfileCardDismissed => 'אפשר להגדיר פרופיל קולי בכל עת בהגדרות.'; + + @override + String get homeNewRecordingButton => 'הקלטה חדשה'; + + @override + String get chatWelcomeMessage => + 'היי, אני Omi. שאל אותי כל דבר על מה שדיברת: מה מישהו אמר, מה הבטחת או סיכום של היום שלך. אני יכול גם להוסיף משימות ולזכור דברים בשבילך.'; + + @override + String get onboardingStarterTaskAsk => 'שאל את Omi על היום שלך'; + + @override + String get onboardingStarterTaskRecord => 'הקלט שיחה עם Omi'; + + @override + String get onboardingStarterTaskCheck => 'סמן משימה זו כדי לסיים אותה'; + + @override + String get paywallRecommended => 'Recommended'; @override - String get firstCaptureWatchInstructions => - 'Open Omi on your Apple Watch and tap Record. Allow microphone access if asked. Your capture will sync to Omi on this phone.'; + String get paywallNoPlans => 'Plans aren\'t available right now. Try again in a minute.'; @override - String get firstCaptureWatchInstall => - 'In the Watch app on your iPhone, install Omi on your Apple Watch. Then open Omi on your watch and tap Record.'; + String get paywallOtherPlans => 'See all plans'; @override - String get firstCaptureWatchPair => - 'Pair your Apple Watch with this iPhone using the Watch app, then install and open Omi on your watch.'; + String get paywallComparePlans => 'See plans'; @override - String get firstCaptureUnavailable => - 'Recording could not start. Check microphone access and your connection, then try again.'; + String get paywallYourPlan => 'Your plan'; @override - String get firstCaptureDelayed => - 'Your recording is taking longer to process. You can check your conversations on Home or try again.'; + String get paywallNotNow => 'Not now'; @override - String get firstCaptureEmptyTitle => 'Let\'s try that again'; + String get paywallTryAgain => 'Try again'; @override - String get firstCaptureShortTitle => 'Finish This Recording?'; + String get paywallManageBilling => 'Manage billing'; @override - String get firstCaptureShortWarning => 'A few more sentences help Omi understand you. About 30 seconds is ideal.'; + String get paywallLoadError => 'Couldn\'t load plans. Check your connection and try again.'; @override - String get firstCaptureCancelTitle => 'Leave This Recording?'; + String get paywallPlans => 'Plans'; @override - String get firstCaptureCancelWarning => - 'Going back stops this recording. Keep talking to give Omi a few more sentences.'; + String get watchCaptureTitle => 'Record from your Apple Watch'; @override - String get firstCaptureSiriTip => 'Tip: Enable Omi in Shortcuts → Siri.'; + String get watchCaptureOpen => 'Open Omi on your Apple Watch.'; + + @override + String get watchCaptureRecord => 'Tap Record. Allow microphone access if asked.'; + + @override + String get watchCaptureSync => 'Your recording syncs to Omi on this phone.'; + + @override + String get pairingBluetoothOff => 'Turn on Bluetooth on your phone, then try again.'; + + @override + String get pairingBluetoothPermission => 'Allow Bluetooth access for Omi in Settings, then try again.'; + + @override + String get pairingNearbyPermission => 'Allow Nearby devices access for Omi in Settings, then try again.'; + + @override + String get pairingLocationPermission => + 'Allow location access for Omi in Settings so this phone can find your device, then try again.'; + + @override + String get pairingNotFoundOrDropped => + 'Couldn\'t reach your Omi. Make sure it\'s charged, close to your phone, and its light is blinking, then try again.'; + + @override + String get pairingBondRecovery => 'Forget Omi in your phone’s Bluetooth settings, then try pairing again.'; + + @override + String get pairingHowTo => 'How to pair'; @override String get paywallOutlineHeadline => 'אל תפספסו אף מילה יותר.'; diff --git a/app/lib/l10n/app_localizations_hi.dart b/app/lib/l10n/app_localizations_hi.dart index d54d2cd8343..2f3e16c8b28 100644 --- a/app/lib/l10n/app_localizations_hi.dart +++ b/app/lib/l10n/app_localizations_hi.dart @@ -1673,7 +1673,7 @@ class AppLocalizationsHi extends AppLocalizations { String get enableBluetooth => 'ब्लूटूथ सक्षम करें'; @override - String get bluetoothNeeded => 'आपके वियरेबल से कनेक्ट करने के लिए Omi को ब्लूटूथ की आवश्यकता है।'; + String get bluetoothNeeded => 'Omi आपके फ़ोन से Bluetooth के ज़रिए जुड़ता है। जारी रखने के लिए इसे चालू करें।'; @override String get contactSupport => 'संपर्क करें?'; @@ -4869,7 +4869,7 @@ class AppLocalizationsHi extends AppLocalizations { 'जारी रखने पर, आपकी बातचीत, रिकॉर्डिंग और व्यक्तिगत जानकारी हमारे सर्वर पर सुरक्षित रूप से संग्रहीत की जाएगी। आपकी ऑडियो रिकॉर्डिंग और ट्रांसक्रिप्ट तीसरे पक्ष की AI सेवाओं (ट्रांसक्रिप्शन के लिए Deepgram और विश्लेषण के लिए OpenAI सहित) द्वारा संसाधित किए जाते हैं ताकि आपको AI-संचालित अंतर्दृष्टि प्रदान की जा सके और सभी ऐप सुविधाएँ सक्षम की जा सकें।'; @override - String get tasksEmptyStateMessage => 'टास्क बनाने के लिए बातचीत शुरू करें।'; + String get tasksEmptyStateMessage => 'आपकी बातचीत से निकले काम यहाँ दिखेंगे।'; @override String get clearChatAction => 'चैट साफ़ करें'; @@ -13021,154 +13021,207 @@ class AppLocalizationsHi extends AppLocalizations { } @override - String get paywallRecommended => 'Recommended'; + String get onboardingPermissionTapToFix => 'ठीक करने के लिए टैप करें'; @override - String get paywallNoPlans => 'Plans aren\'t available right now. Try again in a minute.'; + String get onboardingNoOmiYet => 'अभी तक आपका Omi नहीं आया?'; @override - String get paywallOtherPlans => 'See all plans'; + String get onboardingOmiOrdered => 'मैंने ऑर्डर कर दिया है'; @override - String get paywallComparePlans => 'See plans'; + String get onboardingOmiNotOrdered => 'अभी ऑर्डर नहीं किया'; @override - String get paywallYourPlan => 'Your plan'; + String get onboardingConnectOmi => 'Omi कनेक्ट करें'; @override - String get paywallNotNow => 'Not now'; + String get consentShortMessage => + 'Omi AI की मदद से आपकी बातचीत को नोट्स में बदलता है। आप उन्हें कभी भी हटा सकते हैं।'; @override - String get paywallTryAgain => 'Try again'; + String consentAiProviders(String providers) { + return '$providers के साथ प्रोसेस किया जाता है।'; + } @override - String get paywallManageBilling => 'Manage billing'; + String get onboardingActIntro => 'चलिए Omi आज़माएं।'; @override - String get paywallLoadError => 'Couldn\'t load plans. Check your connection and try again.'; + String get onboardingActMicTitle => 'Omi को आपका माइक्रोफ़ोन चाहिए'; @override - String get paywallPlans => 'Plans'; + String get onboardingActMicBody => 'ताकि आप बात करें तो Omi सुन सके।'; @override - String get firstCaptureTitle => 'How will you capture?'; + String get onboardingActMicAllow => 'माइक्रोफ़ोन की अनुमति दें'; @override - String get firstCaptureDescription => 'Choose how you want to capture your first conversation.'; + String get onboardingActDictateTitle => 'Omi को अपने दिन के बारे में बताएँ'; @override - String get firstCaptureOmi => 'I Have an Omi'; + String get onboardingActDictateBody => 'अपने अंदाज़ में कहिए। बाकी Omi संभाल लेगा।'; @override - String get firstCapturePhone => 'This Phone'; + String get onboardingActDictateExample => + 'अभी सैम के साथ लंच किया, वो जून में लिस्बन जा रही है! याद दिलाना कि शुक्रवार तक टीम को डेक भेजना है, और घर लौटते हुए दूध लेना है।'; @override - String get firstCaptureWatch => 'Apple Watch'; + String get onboardingActDictateHint => 'हो जाने पर टिक पर टैप करें'; @override - String get firstCaptureSkip => 'Skip for Now'; + String get onboardingActPhoneMic => 'फ़ोन माइक्रोफ़ोन'; @override - String get firstCapturePrompt => - 'Tell Omi what you\'re working on today, or what\'s on your mind. Talk for about 30 seconds.'; + String get onboardingActWriting => 'सारांश बन रहा है…'; @override - String get firstCaptureKeepGoing => 'Keep going… 30 seconds is ideal.'; + String get onboardingActRemembers => 'Omi याद रखता है, ताकि आपको न रखना पड़े'; @override - String get firstCaptureReady => 'You can finish now, or keep talking.'; + String get onboardingActNotesLabel => 'आपका दिन, संक्षेप में'; @override - String get firstCaptureStart => 'Start Recording'; + String get onboardingActTasksLabel => 'Omi ने आपके काम पकड़ लिए'; @override - String get firstCaptureFinish => 'Finish Recording'; + String get onboardingActYourWords => 'आपने यह कहा'; @override - String get firstCaptureListening => 'Listening… your words will appear here.'; + String onboardingActCongratsTitle(String name) { + return 'सब तैयार है, $name'; + } @override - String get firstCaptureProcessing => 'Creating your first conversation…'; + String get onboardingActRateTitle => 'अब तक Omi कैसा लगा?'; @override - String get firstCaptureResult => 'Your first conversation'; + String get onboardingActRateYes => 'Omi को रेट करें'; @override - String get firstCaptureEmpty => 'I didn\'t catch enough speech, try again with a few sentences'; + String get onboardingWelcomeCardTitle => 'Omi में आपका स्वागत है'; @override - String get watchCaptureTitle => 'Record from your Apple Watch'; + String get onboardingWelcomeCardBody => + 'Omi पहनें और हमेशा की तरह बात करें। हर बातचीत यहीं नोट्स, कामों और यादों में बदल जाएगी।'; @override - String get watchCaptureOpen => 'Open Omi on your Apple Watch.'; + String get onboardingActDemoTitle => 'सैम के साथ लंच'; @override - String get watchCaptureRecord => 'Tap Record. Allow microphone access if asked.'; + String get onboardingActDemoSummary => + 'सैम के साथ लंच, जो जून में लिस्बन जा रही है। आपको शुक्रवार तक टीम को डेक भेजना है और घर लौटते हुए दूध लेना है।'; @override - String get watchCaptureSync => 'Your recording syncs to Omi on this phone.'; + String get onboardingActDemoTask1 => 'शुक्रवार तक टीम को डेक भेजना'; @override - String get pairingBluetoothOff => 'Turn on Bluetooth on your phone, then try again.'; + String get onboardingActDemoTask2 => 'घर लौटते हुए दूध लेना'; @override - String get pairingBluetoothPermission => 'Allow Bluetooth access for Omi in Settings, then try again.'; + String get onboardingActJustNow => 'अभी-अभी'; @override - String get pairingNearbyPermission => 'Allow Nearby devices access for Omi in Settings, then try again.'; + String get onboardingActSavedToTasks => 'आपके कामों में सहेजा गया'; @override - String get pairingLocationPermission => - 'Allow location access for Omi in Settings so this phone can find your device, then try again.'; + String get onboardingConnectTapToConnect => 'कनेक्ट करने के लिए अपने Omi पर टैप करें।'; @override - String get pairingNotFoundOrDropped => - 'Couldn\'t reach your Omi. Make sure it\'s charged, close to your phone, and its light is blinking, then try again.'; + String get onboardingActMicFallbackBody => 'आपका Omi अभी ऑडियो नहीं भेज रहा। फ़िलहाल अपना फ़ोन इस्तेमाल करें।'; @override - String get pairingBondRecovery => 'Forget Omi in your phone’s Bluetooth settings, then try pairing again.'; + String get onboardingActNoAudioOmi => 'आपका Omi सुनाई नहीं दे रहा। देखें कि वह चालू है और पास है।'; @override - String get pairingHowTo => 'How to pair'; + String get onboardingActNoAudioPhone => 'अभी आपकी आवाज़ नहीं आ रही। थोड़ा ज़ोर से बोलकर देखें।'; + + @override + String get onboardingActDictateBodyOmi => 'बस बोलिए। आपका Omi सुन रहा है।'; + + @override + String get voiceProfileCardDismissed => 'आप सेटिंग्स में कभी भी वॉइस प्रोफ़ाइल सेट कर सकते हैं।'; + + @override + String get homeNewRecordingButton => 'नई रिकॉर्डिंग'; + + @override + String get chatWelcomeMessage => + 'नमस्ते, मैं Omi हूँ। आपने जिस बारे में बात की है, उसके बारे में कुछ भी पूछें: किसी ने क्या कहा, आपने क्या वादा किया, या आपके दिन का सारांश। मैं काम जोड़ सकता हूँ और आपके लिए चीज़ें याद भी रख सकता हूँ।'; + + @override + String get onboardingStarterTaskAsk => 'Omi से अपने दिन के बारे में पूछें'; + + @override + String get onboardingStarterTaskRecord => 'Omi से एक बातचीत रिकॉर्ड करें'; + + @override + String get onboardingStarterTaskCheck => 'इस काम को पूरा करने के लिए टिक करें'; + + @override + String get paywallRecommended => 'Recommended'; @override - String get firstCaptureWatchInstructions => - 'Open Omi on your Apple Watch and tap Record. Allow microphone access if asked. Your capture will sync to Omi on this phone.'; + String get paywallNoPlans => 'Plans aren\'t available right now. Try again in a minute.'; @override - String get firstCaptureWatchInstall => - 'In the Watch app on your iPhone, install Omi on your Apple Watch. Then open Omi on your watch and tap Record.'; + String get paywallOtherPlans => 'See all plans'; @override - String get firstCaptureWatchPair => - 'Pair your Apple Watch with this iPhone using the Watch app, then install and open Omi on your watch.'; + String get paywallComparePlans => 'See plans'; @override - String get firstCaptureUnavailable => - 'Recording could not start. Check microphone access and your connection, then try again.'; + String get paywallYourPlan => 'Your plan'; @override - String get firstCaptureDelayed => - 'Your recording is taking longer to process. You can check your conversations on Home or try again.'; + String get paywallNotNow => 'Not now'; @override - String get firstCaptureEmptyTitle => 'Let\'s try that again'; + String get paywallTryAgain => 'Try again'; @override - String get firstCaptureShortTitle => 'Finish This Recording?'; + String get paywallManageBilling => 'Manage billing'; @override - String get firstCaptureShortWarning => 'A few more sentences help Omi understand you. About 30 seconds is ideal.'; + String get paywallLoadError => 'Couldn\'t load plans. Check your connection and try again.'; @override - String get firstCaptureCancelTitle => 'Leave This Recording?'; + String get paywallPlans => 'Plans'; @override - String get firstCaptureCancelWarning => - 'Going back stops this recording. Keep talking to give Omi a few more sentences.'; + String get watchCaptureTitle => 'Record from your Apple Watch'; @override - String get firstCaptureSiriTip => 'Tip: Enable Omi in Shortcuts → Siri.'; + String get watchCaptureOpen => 'Open Omi on your Apple Watch.'; + + @override + String get watchCaptureRecord => 'Tap Record. Allow microphone access if asked.'; + + @override + String get watchCaptureSync => 'Your recording syncs to Omi on this phone.'; + + @override + String get pairingBluetoothOff => 'Turn on Bluetooth on your phone, then try again.'; + + @override + String get pairingBluetoothPermission => 'Allow Bluetooth access for Omi in Settings, then try again.'; + + @override + String get pairingNearbyPermission => 'Allow Nearby devices access for Omi in Settings, then try again.'; + + @override + String get pairingLocationPermission => + 'Allow location access for Omi in Settings so this phone can find your device, then try again.'; + + @override + String get pairingNotFoundOrDropped => + 'Couldn\'t reach your Omi. Make sure it\'s charged, close to your phone, and its light is blinking, then try again.'; + + @override + String get pairingBondRecovery => 'Forget Omi in your phone’s Bluetooth settings, then try pairing again.'; + + @override + String get pairingHowTo => 'How to pair'; @override String get paywallOutlineHeadline => 'अब एक भी शब्द न छूटे।'; diff --git a/app/lib/l10n/app_localizations_hr.dart b/app/lib/l10n/app_localizations_hr.dart index b6a28340df4..c4864105891 100644 --- a/app/lib/l10n/app_localizations_hr.dart +++ b/app/lib/l10n/app_localizations_hr.dart @@ -1687,8 +1687,7 @@ class AppLocalizationsHr extends AppLocalizations { String get enableBluetooth => 'Omogući Bluetooth'; @override - String get bluetoothNeeded => - 'Omi trebaju Bluetooth da bi se spojio na tvoj prijenosni uređaj. Molimo omogući Bluetooth i pokušaj ponovno.'; + String get bluetoothNeeded => 'Omi se s telefonom povezuje putem Bluetootha. Uključite ga za nastavak.'; @override String get contactSupport => 'Kontaktiraj Podršku?'; @@ -4910,7 +4909,7 @@ class AppLocalizationsHr extends AppLocalizations { 'Nastavljanjem, vaši razgovori, snimke i osobni podaci bit će sigurno pohranjeni na našim poslužiteljima. Vaši audio zapisi i transkripti obrađuju se od strane AI usluga trećih strana (uključujući Deepgram za transkripciju i OpenAI za analizu) kako bi vam pružili uvide pokretane umjetnom inteligencijom i omogućili sve značajke aplikacije.'; @override - String get tasksEmptyStateMessage => 'Započnite razgovor da biste stvorili zadatak.'; + String get tasksEmptyStateMessage => 'Zadaci iz vaših razgovora pojavit će se ovdje.'; @override String get clearChatAction => 'Očisti razgovor'; @@ -13074,154 +13073,207 @@ class AppLocalizationsHr extends AppLocalizations { } @override - String get paywallRecommended => 'Recommended'; + String get onboardingPermissionTapToFix => 'Dodirnite za popravak'; @override - String get paywallNoPlans => 'Plans aren\'t available right now. Try again in a minute.'; + String get onboardingNoOmiYet => 'Još nemate Omi?'; @override - String get paywallOtherPlans => 'See all plans'; + String get onboardingOmiOrdered => 'Naručio/la sam ga'; @override - String get paywallComparePlans => 'See plans'; + String get onboardingOmiNotOrdered => 'Još nisam naručio/la'; @override - String get paywallYourPlan => 'Your plan'; + String get onboardingConnectOmi => 'Povežite Omi'; @override - String get paywallNotNow => 'Not now'; + String get consentShortMessage => + 'Omi koristi AI kako bi vaše razgovore pretvorio u bilješke. Možete ih izbrisati bilo kada.'; @override - String get paywallTryAgain => 'Try again'; + String consentAiProviders(String providers) { + return 'Obrađuje se pomoću $providers.'; + } @override - String get paywallManageBilling => 'Manage billing'; + String get onboardingActIntro => 'Isprobajmo Omi.'; @override - String get paywallLoadError => 'Couldn\'t load plans. Check your connection and try again.'; + String get onboardingActMicTitle => 'Omi treba vaš mikrofon'; @override - String get paywallPlans => 'Plans'; + String get onboardingActMicBody => 'Da vas Omi čuje kad mu se obratite.'; @override - String get firstCaptureTitle => 'How will you capture?'; + String get onboardingActMicAllow => 'Dopusti mikrofon'; @override - String get firstCaptureDescription => 'Choose how you want to capture your first conversation.'; + String get onboardingActDictateTitle => 'Ispričajte Omiju svoj dan'; @override - String get firstCaptureOmi => 'I Have an Omi'; + String get onboardingActDictateBody => 'Recite to na svoj način. Omi se brine za ostalo.'; @override - String get firstCapturePhone => 'This Phone'; + String get onboardingActDictateExample => + 'Upravo sam ručao sa Sam, u lipnju se seli u Lisabon! Podsjeti me da pošaljem prezentaciju timu do petka i da kupim mlijeko na putu kući.'; @override - String get firstCaptureWatch => 'Apple Watch'; + String get onboardingActDictateHint => 'Dodirnite kvačicu kad završite'; @override - String get firstCaptureSkip => 'Skip for Now'; + String get onboardingActPhoneMic => 'Mikrofon telefona'; @override - String get firstCapturePrompt => - 'Tell Omi what you\'re working on today, or what\'s on your mind. Talk for about 30 seconds.'; + String get onboardingActWriting => 'Sažimamo…'; @override - String get firstCaptureKeepGoing => 'Keep going… 30 seconds is ideal.'; + String get onboardingActRemembers => 'Omi pamti da vi ne morate'; @override - String get firstCaptureReady => 'You can finish now, or keep talking.'; + String get onboardingActNotesLabel => 'Vaš dan, ukratko'; @override - String get firstCaptureStart => 'Start Recording'; + String get onboardingActTasksLabel => 'Omi je uhvatio vaše zadatke'; @override - String get firstCaptureFinish => 'Finish Recording'; + String get onboardingActYourWords => 'Evo što ste rekli'; @override - String get firstCaptureListening => 'Listening… your words will appear here.'; + String onboardingActCongratsTitle(String name) { + return 'Sve je spremno, $name'; + } @override - String get firstCaptureProcessing => 'Creating your first conversation…'; + String get onboardingActRateTitle => 'Sviđa li vam se Omi zasad?'; @override - String get firstCaptureResult => 'Your first conversation'; + String get onboardingActRateYes => 'Ocijenite Omi'; @override - String get firstCaptureEmpty => 'I didn\'t catch enough speech, try again with a few sentences'; + String get onboardingWelcomeCardTitle => 'Dobro došli u Omi'; @override - String get watchCaptureTitle => 'Record from your Apple Watch'; + String get onboardingWelcomeCardBody => + 'Nosite Omi i pričajte kao i inače. Svaki razgovor ovdje postaje bilješke, zadaci i sjećanja.'; @override - String get watchCaptureOpen => 'Open Omi on your Apple Watch.'; + String get onboardingActDemoTitle => 'Ručak sa Sam'; @override - String get watchCaptureRecord => 'Tap Record. Allow microphone access if asked.'; + String get onboardingActDemoSummary => + 'Ručak sa Sam, koja se u lipnju seli u Lisabon. Želite poslati prezentaciju timu do petka i kupiti mlijeko na putu kući.'; @override - String get watchCaptureSync => 'Your recording syncs to Omi on this phone.'; + String get onboardingActDemoTask1 => 'Poslati prezentaciju timu do petka'; @override - String get pairingBluetoothOff => 'Turn on Bluetooth on your phone, then try again.'; + String get onboardingActDemoTask2 => 'Kupiti mlijeko na putu kući'; @override - String get pairingBluetoothPermission => 'Allow Bluetooth access for Omi in Settings, then try again.'; + String get onboardingActJustNow => 'Upravo sada'; @override - String get pairingNearbyPermission => 'Allow Nearby devices access for Omi in Settings, then try again.'; + String get onboardingActSavedToTasks => 'Spremljeno u vaše zadatke'; @override - String get pairingLocationPermission => - 'Allow location access for Omi in Settings so this phone can find your device, then try again.'; + String get onboardingConnectTapToConnect => 'Dodirnite svoj Omi da biste se povezali.'; @override - String get pairingNotFoundOrDropped => - 'Couldn\'t reach your Omi. Make sure it\'s charged, close to your phone, and its light is blinking, then try again.'; + String get onboardingActMicFallbackBody => 'Omi još ne šalje zvuk. Za sada koristite telefon.'; @override - String get pairingBondRecovery => 'Forget Omi in your phone’s Bluetooth settings, then try pairing again.'; + String get onboardingActNoAudioOmi => 'Ne čujemo Omi. Provjerite je li uključen i blizu.'; @override - String get pairingHowTo => 'How to pair'; + String get onboardingActNoAudioPhone => 'Još vas ne čujemo. Pokušajte govoriti malo glasnije.'; + + @override + String get onboardingActDictateBodyOmi => 'Samo pričajte. Vaš Omi sluša.'; + + @override + String get voiceProfileCardDismissed => 'Glasovni profil možete postaviti bilo kada u Postavkama.'; + + @override + String get homeNewRecordingButton => 'Novo snimanje'; + + @override + String get chatWelcomeMessage => + 'Bok, ja sam Omi. Pitaj me bilo što o čemu si razgovarao: što je netko rekao, što si obećao ili sažetak tvog dana. Mogu i dodavati zadatke i pamtiti stvari za tebe.'; + + @override + String get onboardingStarterTaskAsk => 'Pitaj Omi o svom danu'; + + @override + String get onboardingStarterTaskRecord => 'Snimi razgovor uz Omi'; + + @override + String get onboardingStarterTaskCheck => 'Označi ovaj zadatak da ga završiš'; + + @override + String get paywallRecommended => 'Recommended'; @override - String get firstCaptureWatchInstructions => - 'Open Omi on your Apple Watch and tap Record. Allow microphone access if asked. Your capture will sync to Omi on this phone.'; + String get paywallNoPlans => 'Plans aren\'t available right now. Try again in a minute.'; @override - String get firstCaptureWatchInstall => - 'In the Watch app on your iPhone, install Omi on your Apple Watch. Then open Omi on your watch and tap Record.'; + String get paywallOtherPlans => 'See all plans'; @override - String get firstCaptureWatchPair => - 'Pair your Apple Watch with this iPhone using the Watch app, then install and open Omi on your watch.'; + String get paywallComparePlans => 'See plans'; @override - String get firstCaptureUnavailable => - 'Recording could not start. Check microphone access and your connection, then try again.'; + String get paywallYourPlan => 'Your plan'; @override - String get firstCaptureDelayed => - 'Your recording is taking longer to process. You can check your conversations on Home or try again.'; + String get paywallNotNow => 'Not now'; @override - String get firstCaptureEmptyTitle => 'Let\'s try that again'; + String get paywallTryAgain => 'Try again'; @override - String get firstCaptureShortTitle => 'Finish This Recording?'; + String get paywallManageBilling => 'Manage billing'; @override - String get firstCaptureShortWarning => 'A few more sentences help Omi understand you. About 30 seconds is ideal.'; + String get paywallLoadError => 'Couldn\'t load plans. Check your connection and try again.'; @override - String get firstCaptureCancelTitle => 'Leave This Recording?'; + String get paywallPlans => 'Plans'; @override - String get firstCaptureCancelWarning => - 'Going back stops this recording. Keep talking to give Omi a few more sentences.'; + String get watchCaptureTitle => 'Record from your Apple Watch'; @override - String get firstCaptureSiriTip => 'Tip: Enable Omi in Shortcuts → Siri.'; + String get watchCaptureOpen => 'Open Omi on your Apple Watch.'; + + @override + String get watchCaptureRecord => 'Tap Record. Allow microphone access if asked.'; + + @override + String get watchCaptureSync => 'Your recording syncs to Omi on this phone.'; + + @override + String get pairingBluetoothOff => 'Turn on Bluetooth on your phone, then try again.'; + + @override + String get pairingBluetoothPermission => 'Allow Bluetooth access for Omi in Settings, then try again.'; + + @override + String get pairingNearbyPermission => 'Allow Nearby devices access for Omi in Settings, then try again.'; + + @override + String get pairingLocationPermission => + 'Allow location access for Omi in Settings so this phone can find your device, then try again.'; + + @override + String get pairingNotFoundOrDropped => + 'Couldn\'t reach your Omi. Make sure it\'s charged, close to your phone, and its light is blinking, then try again.'; + + @override + String get pairingBondRecovery => 'Forget Omi in your phone’s Bluetooth settings, then try pairing again.'; + + @override + String get pairingHowTo => 'How to pair'; @override String get paywallOutlineHeadline => 'Nikad više ne propustite ni riječ.'; diff --git a/app/lib/l10n/app_localizations_hu.dart b/app/lib/l10n/app_localizations_hu.dart index 90ec42699ab..2e6340a784b 100644 --- a/app/lib/l10n/app_localizations_hu.dart +++ b/app/lib/l10n/app_localizations_hu.dart @@ -1697,8 +1697,7 @@ class AppLocalizationsHu extends AppLocalizations { String get enableBluetooth => 'Bluetooth engedélyezése'; @override - String get bluetoothNeeded => - 'Az Omi-nak Bluetoothra van szüksége a viselhető eszközhöz való csatlakozáshoz. Kérlek, engedélyezd a Bluetooth-t, és próbáld újra.'; + String get bluetoothNeeded => 'Az Omi Bluetooth-on csatlakozik a telefonodhoz. Kapcsold be a folytatáshoz.'; @override String get contactSupport => 'Ügyfélszolgálat elérése?'; @@ -4919,7 +4918,7 @@ class AppLocalizationsHu extends AppLocalizations { 'A folytatással beszélgetéseit, felvételeit és személyes adatait biztonságosan tároljuk szervereinken. Hangfelvételeit és átiratait harmadik féltől származó AI szolgáltatások dolgozzák fel (beleértve a Deepgramot az átíráshoz és az OpenAI-t az elemzéshez), hogy AI-alapú betekintéseket nyújtsunk Önnek és az alkalmazás összes funkcióját biztosítsuk.'; @override - String get tasksEmptyStateMessage => 'Feladat létrehozásához kezdj beszélgetést.'; + String get tasksEmptyStateMessage => 'A beszélgetéseidből származó teendők itt jelennek meg.'; @override String get clearChatAction => 'Chat törlése'; @@ -13092,154 +13091,207 @@ class AppLocalizationsHu extends AppLocalizations { } @override - String get paywallRecommended => 'Recommended'; + String get onboardingPermissionTapToFix => 'Koppints a javításhoz'; @override - String get paywallNoPlans => 'Plans aren\'t available right now. Try again in a minute.'; + String get onboardingNoOmiYet => 'Még nincs meg az Omid?'; @override - String get paywallOtherPlans => 'See all plans'; + String get onboardingOmiOrdered => 'Már megrendeltem'; @override - String get paywallComparePlans => 'See plans'; + String get onboardingOmiNotOrdered => 'Még nem rendeltem'; @override - String get paywallYourPlan => 'Your plan'; + String get onboardingConnectOmi => 'Omi csatlakoztatása'; @override - String get paywallNotNow => 'Not now'; + String get consentShortMessage => + 'Az Omi mesterséges intelligenciával jegyzetekké alakítja a beszélgetéseidet. Bármikor törölheted őket.'; @override - String get paywallTryAgain => 'Try again'; + String consentAiProviders(String providers) { + return 'Feldolgozás: $providers.'; + } @override - String get paywallManageBilling => 'Manage billing'; + String get onboardingActIntro => 'Próbáljuk ki az Omit.'; @override - String get paywallLoadError => 'Couldn\'t load plans. Check your connection and try again.'; + String get onboardingActMicTitle => 'Az Ominak kell a mikrofonod'; @override - String get paywallPlans => 'Plans'; + String get onboardingActMicBody => 'Hogy az Omi halljon, amikor beszélsz hozzá.'; @override - String get firstCaptureTitle => 'How will you capture?'; + String get onboardingActMicAllow => 'Mikrofon engedélyezése'; @override - String get firstCaptureDescription => 'Choose how you want to capture your first conversation.'; + String get onboardingActDictateTitle => 'Meséld el az Ominak a napod'; @override - String get firstCaptureOmi => 'I Have an Omi'; + String get onboardingActDictateBody => 'Mondd a magad módján. A többit az Omi intézi.'; @override - String get firstCapturePhone => 'This Phone'; + String get onboardingActDictateExample => + 'Most ebédeltem Sammel, júniusban Lisszabonba költözik! Emlékeztess, hogy péntekig küldjem el a prezentációt a csapatnak, és hazafelé vegyek tejet.'; @override - String get firstCaptureWatch => 'Apple Watch'; + String get onboardingActDictateHint => 'Ha végeztél, koppints a pipára'; @override - String get firstCaptureSkip => 'Skip for Now'; + String get onboardingActPhoneMic => 'Telefon mikrofonja'; @override - String get firstCapturePrompt => - 'Tell Omi what you\'re working on today, or what\'s on your mind. Talk for about 30 seconds.'; + String get onboardingActWriting => 'Összefoglalás…'; @override - String get firstCaptureKeepGoing => 'Keep going… 30 seconds is ideal.'; + String get onboardingActRemembers => 'Az Omi emlékszik helyetted'; @override - String get firstCaptureReady => 'You can finish now, or keep talking.'; + String get onboardingActNotesLabel => 'A napod röviden'; @override - String get firstCaptureStart => 'Start Recording'; + String get onboardingActTasksLabel => 'Az Omi kiszúrta a teendőidet'; @override - String get firstCaptureFinish => 'Finish Recording'; + String get onboardingActYourWords => 'Ezt mondtad'; @override - String get firstCaptureListening => 'Listening… your words will appear here.'; + String onboardingActCongratsTitle(String name) { + return 'Minden kész, $name'; + } @override - String get firstCaptureProcessing => 'Creating your first conversation…'; + String get onboardingActRateTitle => 'Tetszik eddig az Omi?'; @override - String get firstCaptureResult => 'Your first conversation'; + String get onboardingActRateYes => 'Értékeld az Omit'; @override - String get firstCaptureEmpty => 'I didn\'t catch enough speech, try again with a few sentences'; + String get onboardingWelcomeCardTitle => 'Üdvözöl az Omi'; @override - String get watchCaptureTitle => 'Record from your Apple Watch'; + String get onboardingWelcomeCardBody => + 'Viseld az Omit, és beszélj, ahogy szoktál. Minden beszélgetésből itt jegyzet, teendő és emlék lesz.'; @override - String get watchCaptureOpen => 'Open Omi on your Apple Watch.'; + String get onboardingActDemoTitle => 'Ebéd Sammel'; @override - String get watchCaptureRecord => 'Tap Record. Allow microphone access if asked.'; + String get onboardingActDemoSummary => + 'Ebéd Sammel, aki júniusban Lisszabonba költözik. Péntekig el akarod küldeni a prezentációt a csapatnak, és hazafelé tejet venni.'; @override - String get watchCaptureSync => 'Your recording syncs to Omi on this phone.'; + String get onboardingActDemoTask1 => 'Péntekig elküldeni a prezentációt a csapatnak'; @override - String get pairingBluetoothOff => 'Turn on Bluetooth on your phone, then try again.'; + String get onboardingActDemoTask2 => 'Hazafelé tejet venni'; @override - String get pairingBluetoothPermission => 'Allow Bluetooth access for Omi in Settings, then try again.'; + String get onboardingActJustNow => 'Épp most'; @override - String get pairingNearbyPermission => 'Allow Nearby devices access for Omi in Settings, then try again.'; + String get onboardingActSavedToTasks => 'Mentve a teendőid közé'; @override - String get pairingLocationPermission => - 'Allow location access for Omi in Settings so this phone can find your device, then try again.'; + String get onboardingConnectTapToConnect => 'Koppints az Omidra a csatlakozáshoz.'; @override - String get pairingNotFoundOrDropped => - 'Couldn\'t reach your Omi. Make sure it\'s charged, close to your phone, and its light is blinking, then try again.'; + String get onboardingActMicFallbackBody => 'Az Omi még nem küld hangot. Egyelőre használd a telefonodat.'; @override - String get pairingBondRecovery => 'Forget Omi in your phone’s Bluetooth settings, then try pairing again.'; + String get onboardingActNoAudioOmi => 'Nem halljuk az Omit. Ellenőrizd, hogy be van-e kapcsolva és közel van-e.'; @override - String get pairingHowTo => 'How to pair'; + String get onboardingActNoAudioPhone => 'Még nem hallunk. Próbálj kicsit hangosabban beszélni.'; + + @override + String get onboardingActDictateBodyOmi => 'Csak beszélj. Az Omid figyel.'; + + @override + String get voiceProfileCardDismissed => 'A hangprofilt bármikor beállíthatod a Beállításokban.'; + + @override + String get homeNewRecordingButton => 'Új felvétel'; + + @override + String get chatWelcomeMessage => + 'Szia, Omi vagyok. Kérdezz bármit arról, amiről beszéltél: mit mondott valaki, mit ígértél, vagy kérj összefoglalót a napodról. Feladatokat is fel tudok venni, és dolgokat is megjegyzek helyetted.'; + + @override + String get onboardingStarterTaskAsk => 'Kérdezd meg Omit a napodról'; + + @override + String get onboardingStarterTaskRecord => 'Rögzíts egy beszélgetést az Omival'; + + @override + String get onboardingStarterTaskCheck => 'Pipáld ki ezt a feladatot'; + + @override + String get paywallRecommended => 'Recommended'; @override - String get firstCaptureWatchInstructions => - 'Open Omi on your Apple Watch and tap Record. Allow microphone access if asked. Your capture will sync to Omi on this phone.'; + String get paywallNoPlans => 'Plans aren\'t available right now. Try again in a minute.'; @override - String get firstCaptureWatchInstall => - 'In the Watch app on your iPhone, install Omi on your Apple Watch. Then open Omi on your watch and tap Record.'; + String get paywallOtherPlans => 'See all plans'; @override - String get firstCaptureWatchPair => - 'Pair your Apple Watch with this iPhone using the Watch app, then install and open Omi on your watch.'; + String get paywallComparePlans => 'See plans'; @override - String get firstCaptureUnavailable => - 'Recording could not start. Check microphone access and your connection, then try again.'; + String get paywallYourPlan => 'Your plan'; @override - String get firstCaptureDelayed => - 'Your recording is taking longer to process. You can check your conversations on Home or try again.'; + String get paywallNotNow => 'Not now'; @override - String get firstCaptureEmptyTitle => 'Let\'s try that again'; + String get paywallTryAgain => 'Try again'; @override - String get firstCaptureShortTitle => 'Finish This Recording?'; + String get paywallManageBilling => 'Manage billing'; @override - String get firstCaptureShortWarning => 'A few more sentences help Omi understand you. About 30 seconds is ideal.'; + String get paywallLoadError => 'Couldn\'t load plans. Check your connection and try again.'; @override - String get firstCaptureCancelTitle => 'Leave This Recording?'; + String get paywallPlans => 'Plans'; @override - String get firstCaptureCancelWarning => - 'Going back stops this recording. Keep talking to give Omi a few more sentences.'; + String get watchCaptureTitle => 'Record from your Apple Watch'; @override - String get firstCaptureSiriTip => 'Tip: Enable Omi in Shortcuts → Siri.'; + String get watchCaptureOpen => 'Open Omi on your Apple Watch.'; + + @override + String get watchCaptureRecord => 'Tap Record. Allow microphone access if asked.'; + + @override + String get watchCaptureSync => 'Your recording syncs to Omi on this phone.'; + + @override + String get pairingBluetoothOff => 'Turn on Bluetooth on your phone, then try again.'; + + @override + String get pairingBluetoothPermission => 'Allow Bluetooth access for Omi in Settings, then try again.'; + + @override + String get pairingNearbyPermission => 'Allow Nearby devices access for Omi in Settings, then try again.'; + + @override + String get pairingLocationPermission => + 'Allow location access for Omi in Settings so this phone can find your device, then try again.'; + + @override + String get pairingNotFoundOrDropped => + 'Couldn\'t reach your Omi. Make sure it\'s charged, close to your phone, and its light is blinking, then try again.'; + + @override + String get pairingBondRecovery => 'Forget Omi in your phone’s Bluetooth settings, then try pairing again.'; + + @override + String get pairingHowTo => 'How to pair'; @override String get paywallOutlineHeadline => 'Soha többé ne maradj le egyetlen szóról sem.'; diff --git a/app/lib/l10n/app_localizations_id.dart b/app/lib/l10n/app_localizations_id.dart index 36b0ad72b86..332fd291747 100644 --- a/app/lib/l10n/app_localizations_id.dart +++ b/app/lib/l10n/app_localizations_id.dart @@ -1689,8 +1689,7 @@ class AppLocalizationsId extends AppLocalizations { String get enableBluetooth => 'Aktifkan Bluetooth'; @override - String get bluetoothNeeded => - 'Omi membutuhkan Bluetooth untuk terhubung ke perangkat yang dapat dipakai Anda. Silakan aktifkan Bluetooth dan coba lagi.'; + String get bluetoothNeeded => 'Omi terhubung ke ponsel Anda lewat Bluetooth. Nyalakan untuk melanjutkan.'; @override String get contactSupport => 'Hubungi Dukungan?'; @@ -4899,7 +4898,7 @@ class AppLocalizationsId extends AppLocalizations { 'Dengan melanjutkan, percakapan, rekaman, dan informasi pribadi Anda akan disimpan dengan aman di server kami. Rekaman audio dan transkrip Anda diproses oleh layanan AI pihak ketiga (termasuk Deepgram untuk transkripsi dan OpenAI untuk analisis) untuk memberikan Anda wawasan berbasis AI dan mengaktifkan semua fitur aplikasi.'; @override - String get tasksEmptyStateMessage => 'Mulai percakapan untuk membuat tugas.'; + String get tasksEmptyStateMessage => 'Tugas dari percakapan Anda akan muncul di sini.'; @override String get clearChatAction => 'Hapus obrolan'; @@ -13050,154 +13049,207 @@ class AppLocalizationsId extends AppLocalizations { } @override - String get paywallRecommended => 'Recommended'; + String get onboardingPermissionTapToFix => 'Ketuk untuk memperbaiki'; @override - String get paywallNoPlans => 'Plans aren\'t available right now. Try again in a minute.'; + String get onboardingNoOmiYet => 'Belum punya Omi?'; @override - String get paywallOtherPlans => 'See all plans'; + String get onboardingOmiOrdered => 'Saya sudah memesannya'; @override - String get paywallComparePlans => 'See plans'; + String get onboardingOmiNotOrdered => 'Belum memesan'; @override - String get paywallYourPlan => 'Your plan'; + String get onboardingConnectOmi => 'Hubungkan Omi'; @override - String get paywallNotNow => 'Not now'; + String get consentShortMessage => + 'Omi menggunakan AI untuk mengubah percakapanmu menjadi catatan. Kamu bisa menghapusnya kapan saja.'; @override - String get paywallTryAgain => 'Try again'; + String consentAiProviders(String providers) { + return 'Diproses dengan $providers.'; + } @override - String get paywallManageBilling => 'Manage billing'; + String get onboardingActIntro => 'Ayo coba Omi.'; @override - String get paywallLoadError => 'Couldn\'t load plans. Check your connection and try again.'; + String get onboardingActMicTitle => 'Omi butuh mikrofon Anda'; @override - String get paywallPlans => 'Plans'; + String get onboardingActMicBody => 'Agar Omi bisa mendengar saat Anda bicara.'; @override - String get firstCaptureTitle => 'How will you capture?'; + String get onboardingActMicAllow => 'Izinkan Mikrofon'; @override - String get firstCaptureDescription => 'Choose how you want to capture your first conversation.'; + String get onboardingActDictateTitle => 'Ceritakan hari Anda ke Omi'; @override - String get firstCaptureOmi => 'I Have an Omi'; + String get onboardingActDictateBody => 'Katakan dengan gaya Anda. Omi urus sisanya.'; @override - String get firstCapturePhone => 'This Phone'; + String get onboardingActDictateExample => + 'Baru makan siang sama Sam, dia pindah ke Lisbon bulan Juni! Ingatkan aku kirim deck ke tim sebelum Jumat, dan beli susu di jalan pulang.'; @override - String get firstCaptureWatch => 'Apple Watch'; + String get onboardingActDictateHint => 'Ketuk centang jika sudah selesai'; @override - String get firstCaptureSkip => 'Skip for Now'; + String get onboardingActPhoneMic => 'Mikrofon ponsel'; @override - String get firstCapturePrompt => - 'Tell Omi what you\'re working on today, or what\'s on your mind. Talk for about 30 seconds.'; + String get onboardingActWriting => 'Merangkum…'; @override - String get firstCaptureKeepGoing => 'Keep going… 30 seconds is ideal.'; + String get onboardingActRemembers => 'Omi mengingat, jadi kamu tidak perlu'; @override - String get firstCaptureReady => 'You can finish now, or keep talking.'; + String get onboardingActNotesLabel => 'Harimu, dirangkum'; @override - String get firstCaptureStart => 'Start Recording'; + String get onboardingActTasksLabel => 'Omi menangkap tugasmu'; @override - String get firstCaptureFinish => 'Finish Recording'; + String get onboardingActYourWords => 'Ini yang Anda katakan'; @override - String get firstCaptureListening => 'Listening… your words will appear here.'; + String onboardingActCongratsTitle(String name) { + return 'Semua siap, $name'; + } @override - String get firstCaptureProcessing => 'Creating your first conversation…'; + String get onboardingActRateTitle => 'Suka Omi sejauh ini?'; @override - String get firstCaptureResult => 'Your first conversation'; + String get onboardingActRateYes => 'Beri Nilai Omi'; @override - String get firstCaptureEmpty => 'I didn\'t catch enough speech, try again with a few sentences'; + String get onboardingWelcomeCardTitle => 'Selamat datang di Omi'; @override - String get watchCaptureTitle => 'Record from your Apple Watch'; + String get onboardingWelcomeCardBody => + 'Pakai Omi dan bicara seperti biasa. Setiap percakapan menjadi catatan, tugas, dan kenangan di sini.'; @override - String get watchCaptureOpen => 'Open Omi on your Apple Watch.'; + String get onboardingActDemoTitle => 'Makan siang dengan Sam'; @override - String get watchCaptureRecord => 'Tap Record. Allow microphone access if asked.'; + String get onboardingActDemoSummary => + 'Makan siang dengan Sam, yang pindah ke Lisbon bulan Juni. Kamu mau kirim deck ke tim sebelum Jumat dan beli susu di jalan pulang.'; @override - String get watchCaptureSync => 'Your recording syncs to Omi on this phone.'; + String get onboardingActDemoTask1 => 'Kirim deck ke tim sebelum Jumat'; @override - String get pairingBluetoothOff => 'Turn on Bluetooth on your phone, then try again.'; + String get onboardingActDemoTask2 => 'Beli susu di jalan pulang'; @override - String get pairingBluetoothPermission => 'Allow Bluetooth access for Omi in Settings, then try again.'; + String get onboardingActJustNow => 'Baru saja'; @override - String get pairingNearbyPermission => 'Allow Nearby devices access for Omi in Settings, then try again.'; + String get onboardingActSavedToTasks => 'Disimpan ke tugasmu'; @override - String get pairingLocationPermission => - 'Allow location access for Omi in Settings so this phone can find your device, then try again.'; + String get onboardingConnectTapToConnect => 'Ketuk Omi kamu untuk menghubungkan.'; @override - String get pairingNotFoundOrDropped => - 'Couldn\'t reach your Omi. Make sure it\'s charged, close to your phone, and its light is blinking, then try again.'; + String get onboardingActMicFallbackBody => 'Omi kamu belum mengirim audio. Pakai ponselmu dulu.'; @override - String get pairingBondRecovery => 'Forget Omi in your phone’s Bluetooth settings, then try pairing again.'; + String get onboardingActNoAudioOmi => 'Omi kamu tidak terdengar. Pastikan menyala dan dekat.'; @override - String get pairingHowTo => 'How to pair'; + String get onboardingActNoAudioPhone => 'Suaramu belum terdengar. Coba bicara sedikit lebih keras.'; + + @override + String get onboardingActDictateBodyOmi => 'Bicara saja. Omi kamu sedang mendengarkan.'; + + @override + String get voiceProfileCardDismissed => 'Anda dapat mengatur Profil Suara kapan saja di Pengaturan.'; + + @override + String get homeNewRecordingButton => 'Rekaman Baru'; + + @override + String get chatWelcomeMessage => + 'Hai, aku Omi. Tanyakan apa saja tentang hal yang kamu bicarakan: apa yang dikatakan seseorang, apa yang kamu janjikan, atau ringkasan harimu. Aku juga bisa menambahkan tugas dan mengingat hal-hal untukmu.'; + + @override + String get onboardingStarterTaskAsk => 'Tanyakan harimu kepada Omi'; + + @override + String get onboardingStarterTaskRecord => 'Rekam percakapan dengan Omi'; + + @override + String get onboardingStarterTaskCheck => 'Centang tugas ini untuk menyelesaikannya'; + + @override + String get paywallRecommended => 'Recommended'; @override - String get firstCaptureWatchInstructions => - 'Open Omi on your Apple Watch and tap Record. Allow microphone access if asked. Your capture will sync to Omi on this phone.'; + String get paywallNoPlans => 'Plans aren\'t available right now. Try again in a minute.'; @override - String get firstCaptureWatchInstall => - 'In the Watch app on your iPhone, install Omi on your Apple Watch. Then open Omi on your watch and tap Record.'; + String get paywallOtherPlans => 'See all plans'; @override - String get firstCaptureWatchPair => - 'Pair your Apple Watch with this iPhone using the Watch app, then install and open Omi on your watch.'; + String get paywallComparePlans => 'See plans'; @override - String get firstCaptureUnavailable => - 'Recording could not start. Check microphone access and your connection, then try again.'; + String get paywallYourPlan => 'Your plan'; @override - String get firstCaptureDelayed => - 'Your recording is taking longer to process. You can check your conversations on Home or try again.'; + String get paywallNotNow => 'Not now'; @override - String get firstCaptureEmptyTitle => 'Let\'s try that again'; + String get paywallTryAgain => 'Try again'; @override - String get firstCaptureShortTitle => 'Finish This Recording?'; + String get paywallManageBilling => 'Manage billing'; @override - String get firstCaptureShortWarning => 'A few more sentences help Omi understand you. About 30 seconds is ideal.'; + String get paywallLoadError => 'Couldn\'t load plans. Check your connection and try again.'; @override - String get firstCaptureCancelTitle => 'Leave This Recording?'; + String get paywallPlans => 'Plans'; @override - String get firstCaptureCancelWarning => - 'Going back stops this recording. Keep talking to give Omi a few more sentences.'; + String get watchCaptureTitle => 'Record from your Apple Watch'; @override - String get firstCaptureSiriTip => 'Tip: Enable Omi in Shortcuts → Siri.'; + String get watchCaptureOpen => 'Open Omi on your Apple Watch.'; + + @override + String get watchCaptureRecord => 'Tap Record. Allow microphone access if asked.'; + + @override + String get watchCaptureSync => 'Your recording syncs to Omi on this phone.'; + + @override + String get pairingBluetoothOff => 'Turn on Bluetooth on your phone, then try again.'; + + @override + String get pairingBluetoothPermission => 'Allow Bluetooth access for Omi in Settings, then try again.'; + + @override + String get pairingNearbyPermission => 'Allow Nearby devices access for Omi in Settings, then try again.'; + + @override + String get pairingLocationPermission => + 'Allow location access for Omi in Settings so this phone can find your device, then try again.'; + + @override + String get pairingNotFoundOrDropped => + 'Couldn\'t reach your Omi. Make sure it\'s charged, close to your phone, and its light is blinking, then try again.'; + + @override + String get pairingBondRecovery => 'Forget Omi in your phone’s Bluetooth settings, then try pairing again.'; + + @override + String get pairingHowTo => 'How to pair'; @override String get paywallOutlineHeadline => 'Jangan pernah melewatkan satu kata pun lagi.'; diff --git a/app/lib/l10n/app_localizations_it.dart b/app/lib/l10n/app_localizations_it.dart index b95e6447013..c7d4522b0ce 100644 --- a/app/lib/l10n/app_localizations_it.dart +++ b/app/lib/l10n/app_localizations_it.dart @@ -1693,8 +1693,7 @@ class AppLocalizationsIt extends AppLocalizations { String get enableBluetooth => 'Abilita Bluetooth'; @override - String get bluetoothNeeded => - 'Omi ha bisogno del Bluetooth per connettersi al tuo dispositivo indossabile. Abilita il Bluetooth e riprova.'; + String get bluetoothNeeded => 'Omi si collega al telefono tramite Bluetooth. Attivalo per continuare.'; @override String get contactSupport => 'Contatta Supporto?'; @@ -4922,7 +4921,7 @@ class AppLocalizationsIt extends AppLocalizations { 'Continuando, le tue conversazioni, registrazioni e informazioni personali saranno archiviate in modo sicuro sui nostri server. Le tue registrazioni audio e trascrizioni vengono elaborate da servizi AI di terze parti (inclusi Deepgram per la trascrizione e OpenAI per l\'analisi) per fornirti approfondimenti basati sull\'AI e abilitare tutte le funzionalità dell\'app.'; @override - String get tasksEmptyStateMessage => 'Avvia una conversazione per creare un\'attività.'; + String get tasksEmptyStateMessage => 'Le attività dalle tue conversazioni appariranno qui.'; @override String get clearChatAction => 'Cancella chat'; @@ -13110,154 +13109,207 @@ class AppLocalizationsIt extends AppLocalizations { } @override - String get paywallRecommended => 'Recommended'; + String get onboardingPermissionTapToFix => 'Tocca per risolvere'; @override - String get paywallNoPlans => 'Plans aren\'t available right now. Try again in a minute.'; + String get onboardingNoOmiYet => 'Non hai ancora il tuo Omi?'; @override - String get paywallOtherPlans => 'See all plans'; + String get onboardingOmiOrdered => 'L\'ho ordinato'; @override - String get paywallComparePlans => 'See plans'; + String get onboardingOmiNotOrdered => 'Non ancora ordinato'; @override - String get paywallYourPlan => 'Your plan'; + String get onboardingConnectOmi => 'Connetti Omi'; @override - String get paywallNotNow => 'Not now'; + String get consentShortMessage => + 'Omi usa l\'IA per trasformare le tue conversazioni in note. Puoi eliminarle quando vuoi.'; @override - String get paywallTryAgain => 'Try again'; + String consentAiProviders(String providers) { + return 'Elaborato con $providers.'; + } @override - String get paywallManageBilling => 'Manage billing'; + String get onboardingActIntro => 'Proviamo Omi.'; @override - String get paywallLoadError => 'Couldn\'t load plans. Check your connection and try again.'; + String get onboardingActMicTitle => 'Omi ha bisogno del microfono'; @override - String get paywallPlans => 'Plans'; + String get onboardingActMicBody => 'Così Omi ti sente quando gli parli.'; @override - String get firstCaptureTitle => 'How will you capture?'; + String get onboardingActMicAllow => 'Consenti microfono'; @override - String get firstCaptureDescription => 'Choose how you want to capture your first conversation.'; + String get onboardingActDictateTitle => 'Racconta a Omi la tua giornata'; @override - String get firstCaptureOmi => 'I Have an Omi'; + String get onboardingActDictateBody => 'Dillo a modo tuo. Al resto pensa Omi.'; @override - String get firstCapturePhone => 'This Phone'; + String get onboardingActDictateExample => + 'Ho appena pranzato con Sam, a giugno si trasferisce a Lisbona! Ricordami di mandare la presentazione al team entro venerdì e di prendere il latte tornando a casa.'; @override - String get firstCaptureWatch => 'Apple Watch'; + String get onboardingActDictateHint => 'Tocca la spunta quando hai finito'; @override - String get firstCaptureSkip => 'Skip for Now'; + String get onboardingActPhoneMic => 'Microfono del telefono'; @override - String get firstCapturePrompt => - 'Tell Omi what you\'re working on today, or what\'s on your mind. Talk for about 30 seconds.'; + String get onboardingActWriting => 'Riassumendo…'; @override - String get firstCaptureKeepGoing => 'Keep going… 30 seconds is ideal.'; + String get onboardingActRemembers => 'Omi ricorda al posto tuo'; @override - String get firstCaptureReady => 'You can finish now, or keep talking.'; + String get onboardingActNotesLabel => 'La tua giornata, in sintesi'; @override - String get firstCaptureStart => 'Start Recording'; + String get onboardingActTasksLabel => 'Omi ha colto le tue cose da fare'; @override - String get firstCaptureFinish => 'Finish Recording'; + String get onboardingActYourWords => 'Ecco cosa hai detto'; @override - String get firstCaptureListening => 'Listening… your words will appear here.'; + String onboardingActCongratsTitle(String name) { + return 'Tutto pronto, $name'; + } @override - String get firstCaptureProcessing => 'Creating your first conversation…'; + String get onboardingActRateTitle => 'Ti piace Omi finora?'; @override - String get firstCaptureResult => 'Your first conversation'; + String get onboardingActRateYes => 'Valuta Omi'; @override - String get firstCaptureEmpty => 'I didn\'t catch enough speech, try again with a few sentences'; + String get onboardingWelcomeCardTitle => 'Benvenuto in Omi'; @override - String get watchCaptureTitle => 'Record from your Apple Watch'; + String get onboardingWelcomeCardBody => + 'Indossa Omi e parla come sempre. Ogni conversazione diventa qui appunti, attività e ricordi.'; @override - String get watchCaptureOpen => 'Open Omi on your Apple Watch.'; + String get onboardingActDemoTitle => 'Pranzo con Sam'; @override - String get watchCaptureRecord => 'Tap Record. Allow microphone access if asked.'; + String get onboardingActDemoSummary => + 'Pranzo con Sam, che a giugno si trasferisce a Lisbona. Vuoi mandare la presentazione al team entro venerdì e prendere il latte tornando a casa.'; @override - String get watchCaptureSync => 'Your recording syncs to Omi on this phone.'; + String get onboardingActDemoTask1 => 'Mandare la presentazione al team entro venerdì'; @override - String get pairingBluetoothOff => 'Turn on Bluetooth on your phone, then try again.'; + String get onboardingActDemoTask2 => 'Prendere il latte tornando a casa'; @override - String get pairingBluetoothPermission => 'Allow Bluetooth access for Omi in Settings, then try again.'; + String get onboardingActJustNow => 'Proprio ora'; @override - String get pairingNearbyPermission => 'Allow Nearby devices access for Omi in Settings, then try again.'; + String get onboardingActSavedToTasks => 'Salvato nelle tue attività'; @override - String get pairingLocationPermission => - 'Allow location access for Omi in Settings so this phone can find your device, then try again.'; + String get onboardingConnectTapToConnect => 'Tocca il tuo Omi per collegarlo.'; @override - String get pairingNotFoundOrDropped => - 'Couldn\'t reach your Omi. Make sure it\'s charged, close to your phone, and its light is blinking, then try again.'; + String get onboardingActMicFallbackBody => 'Il tuo Omi non invia ancora audio. Per ora usa il telefono.'; @override - String get pairingBondRecovery => 'Forget Omi in your phone’s Bluetooth settings, then try pairing again.'; + String get onboardingActNoAudioOmi => 'Non sentiamo il tuo Omi. Controlla che sia acceso e vicino.'; @override - String get pairingHowTo => 'How to pair'; + String get onboardingActNoAudioPhone => 'Non ti sentiamo ancora. Prova a parlare un po\' più forte.'; + + @override + String get onboardingActDictateBodyOmi => 'Parla e basta. Il tuo Omi ti ascolta.'; + + @override + String get voiceProfileCardDismissed => 'Puoi configurare il profilo vocale in qualsiasi momento nelle Impostazioni.'; + + @override + String get homeNewRecordingButton => 'Nuova registrazione'; + + @override + String get chatWelcomeMessage => + 'Ciao, sono Omi. Chiedimi qualsiasi cosa di cui hai parlato: cosa ha detto qualcuno, cosa hai promesso o un riepilogo della tua giornata. Posso anche aggiungere attività e ricordare cose per te.'; + + @override + String get onboardingStarterTaskAsk => 'Chiedi a Omi della tua giornata'; + + @override + String get onboardingStarterTaskRecord => 'Registra una conversazione con Omi'; + + @override + String get onboardingStarterTaskCheck => 'Spunta questa attività per completarla'; + + @override + String get paywallRecommended => 'Recommended'; @override - String get firstCaptureWatchInstructions => - 'Open Omi on your Apple Watch and tap Record. Allow microphone access if asked. Your capture will sync to Omi on this phone.'; + String get paywallNoPlans => 'Plans aren\'t available right now. Try again in a minute.'; @override - String get firstCaptureWatchInstall => - 'In the Watch app on your iPhone, install Omi on your Apple Watch. Then open Omi on your watch and tap Record.'; + String get paywallOtherPlans => 'See all plans'; @override - String get firstCaptureWatchPair => - 'Pair your Apple Watch with this iPhone using the Watch app, then install and open Omi on your watch.'; + String get paywallComparePlans => 'See plans'; @override - String get firstCaptureUnavailable => - 'Recording could not start. Check microphone access and your connection, then try again.'; + String get paywallYourPlan => 'Your plan'; @override - String get firstCaptureDelayed => - 'Your recording is taking longer to process. You can check your conversations on Home or try again.'; + String get paywallNotNow => 'Not now'; @override - String get firstCaptureEmptyTitle => 'Let\'s try that again'; + String get paywallTryAgain => 'Try again'; @override - String get firstCaptureShortTitle => 'Finish This Recording?'; + String get paywallManageBilling => 'Manage billing'; @override - String get firstCaptureShortWarning => 'A few more sentences help Omi understand you. About 30 seconds is ideal.'; + String get paywallLoadError => 'Couldn\'t load plans. Check your connection and try again.'; @override - String get firstCaptureCancelTitle => 'Leave This Recording?'; + String get paywallPlans => 'Plans'; @override - String get firstCaptureCancelWarning => - 'Going back stops this recording. Keep talking to give Omi a few more sentences.'; + String get watchCaptureTitle => 'Record from your Apple Watch'; @override - String get firstCaptureSiriTip => 'Tip: Enable Omi in Shortcuts → Siri.'; + String get watchCaptureOpen => 'Open Omi on your Apple Watch.'; + + @override + String get watchCaptureRecord => 'Tap Record. Allow microphone access if asked.'; + + @override + String get watchCaptureSync => 'Your recording syncs to Omi on this phone.'; + + @override + String get pairingBluetoothOff => 'Turn on Bluetooth on your phone, then try again.'; + + @override + String get pairingBluetoothPermission => 'Allow Bluetooth access for Omi in Settings, then try again.'; + + @override + String get pairingNearbyPermission => 'Allow Nearby devices access for Omi in Settings, then try again.'; + + @override + String get pairingLocationPermission => + 'Allow location access for Omi in Settings so this phone can find your device, then try again.'; + + @override + String get pairingNotFoundOrDropped => + 'Couldn\'t reach your Omi. Make sure it\'s charged, close to your phone, and its light is blinking, then try again.'; + + @override + String get pairingBondRecovery => 'Forget Omi in your phone’s Bluetooth settings, then try pairing again.'; + + @override + String get pairingHowTo => 'How to pair'; @override String get paywallOutlineHeadline => 'Non perdere mai più una parola.'; diff --git a/app/lib/l10n/app_localizations_ja.dart b/app/lib/l10n/app_localizations_ja.dart index 721a8614ae6..d85a08b7a7a 100644 --- a/app/lib/l10n/app_localizations_ja.dart +++ b/app/lib/l10n/app_localizations_ja.dart @@ -1662,7 +1662,7 @@ class AppLocalizationsJa extends AppLocalizations { String get enableBluetooth => 'Bluetoothを有効にする'; @override - String get bluetoothNeeded => 'Omiはウェアラブルに接続するためにBluetoothが必要です。Bluetoothを有効にして再試行してください。'; + String get bluetoothNeeded => 'OmiはBluetoothでスマートフォンに接続します。続けるにはオンにしてください。'; @override String get contactSupport => 'サポートに連絡しますか?'; @@ -4813,7 +4813,7 @@ class AppLocalizationsJa extends AppLocalizations { '続行することで、会話、録音、個人情報は安全に当社のサーバーに保存されます。音声録音と文字起こしは、サードパーティのAIサービス(文字起こし用のDeepgramと分析用のOpenAIを含む)によって処理され、AI駆動のインサイトを提供し、すべてのアプリ機能を有効にします。'; @override - String get tasksEmptyStateMessage => '会話を始めてタスクを作成しましょう。'; + String get tasksEmptyStateMessage => '会話から見つかったToDoがここに表示されます。'; @override String get clearChatAction => 'チャットを消去'; @@ -12854,154 +12854,202 @@ class AppLocalizationsJa extends AppLocalizations { } @override - String get paywallRecommended => 'Recommended'; + String get onboardingPermissionTapToFix => 'タップして修正'; @override - String get paywallNoPlans => 'Plans aren\'t available right now. Try again in a minute.'; + String get onboardingNoOmiYet => 'まだOmiをお持ちでないですか?'; @override - String get paywallOtherPlans => 'See all plans'; + String get onboardingOmiOrdered => '注文済みです'; @override - String get paywallComparePlans => 'See plans'; + String get onboardingOmiNotOrdered => 'まだ注文していません'; @override - String get paywallYourPlan => 'Your plan'; + String get onboardingConnectOmi => 'Omiを接続'; @override - String get paywallNotNow => 'Not now'; + String get consentShortMessage => 'OmiはAIで会話をメモにまとめます。メモはいつでも削除できます。'; @override - String get paywallTryAgain => 'Try again'; + String consentAiProviders(String providers) { + return '$providers で処理されます。'; + } @override - String get paywallManageBilling => 'Manage billing'; + String get onboardingActIntro => 'Omiを試してみましょう。'; @override - String get paywallLoadError => 'Couldn\'t load plans. Check your connection and try again.'; + String get onboardingActMicTitle => 'Omiにマイクの許可が必要です'; @override - String get paywallPlans => 'Plans'; + String get onboardingActMicBody => '話しかけたときにOmiが聞き取れるようにします。'; @override - String get firstCaptureTitle => 'How will you capture?'; + String get onboardingActMicAllow => 'マイクを許可'; @override - String get firstCaptureDescription => 'Choose how you want to capture your first conversation.'; + String get onboardingActDictateTitle => '今日のことをOmiに話そう'; @override - String get firstCaptureOmi => 'I Have an Omi'; + String get onboardingActDictateBody => '思うままに話してください。あとはOmiにお任せ。'; @override - String get firstCapturePhone => 'This Phone'; + String get onboardingActDictateExample => 'さっきサムとランチしたよ。6月にリスボンへ引っ越すんだって!金曜までにチームへ資料を送るのと、帰りに牛乳を買うのをリマインドして。'; @override - String get firstCaptureWatch => 'Apple Watch'; + String get onboardingActDictateHint => '話し終えたらチェックをタップ'; @override - String get firstCaptureSkip => 'Skip for Now'; + String get onboardingActPhoneMic => 'スマホのマイク'; @override - String get firstCapturePrompt => - 'Tell Omi what you\'re working on today, or what\'s on your mind. Talk for about 30 seconds.'; + String get onboardingActWriting => '要約しています…'; @override - String get firstCaptureKeepGoing => 'Keep going… 30 seconds is ideal.'; + String get onboardingActRemembers => '覚えておくのは Omi にお任せ'; @override - String get firstCaptureReady => 'You can finish now, or keep talking.'; + String get onboardingActNotesLabel => '今日のまとめ'; @override - String get firstCaptureStart => 'Start Recording'; + String get onboardingActTasksLabel => 'Omiがやることを見つけました'; @override - String get firstCaptureFinish => 'Finish Recording'; + String get onboardingActYourWords => 'あなたが話した内容'; @override - String get firstCaptureListening => 'Listening… your words will appear here.'; + String onboardingActCongratsTitle(String name) { + return '準備完了です、$nameさん'; + } @override - String get firstCaptureProcessing => 'Creating your first conversation…'; + String get onboardingActRateTitle => 'Omiはいかがですか?'; @override - String get firstCaptureResult => 'Your first conversation'; + String get onboardingActRateYes => 'Omiを評価'; @override - String get firstCaptureEmpty => 'I didn\'t catch enough speech, try again with a few sentences'; + String get onboardingWelcomeCardTitle => 'Omiへようこそ'; @override - String get watchCaptureTitle => 'Record from your Apple Watch'; + String get onboardingWelcomeCardBody => 'Omiを身につけて普段どおり話すだけ。会話はここでメモ、タスク、記憶になります。'; @override - String get watchCaptureOpen => 'Open Omi on your Apple Watch.'; + String get onboardingActDemoTitle => 'サムとランチ'; @override - String get watchCaptureRecord => 'Tap Record. Allow microphone access if asked.'; + String get onboardingActDemoSummary => '6月にリスボンへ引っ越すサムとランチ。金曜までにチームへ資料を送り、帰りに牛乳を買う予定。'; @override - String get watchCaptureSync => 'Your recording syncs to Omi on this phone.'; + String get onboardingActDemoTask1 => '金曜までにチームへ資料を送る'; @override - String get pairingBluetoothOff => 'Turn on Bluetooth on your phone, then try again.'; + String get onboardingActDemoTask2 => '帰りに牛乳を買う'; @override - String get pairingBluetoothPermission => 'Allow Bluetooth access for Omi in Settings, then try again.'; + String get onboardingActJustNow => 'たった今'; @override - String get pairingNearbyPermission => 'Allow Nearby devices access for Omi in Settings, then try again.'; + String get onboardingActSavedToTasks => 'タスクに保存しました'; @override - String get pairingLocationPermission => - 'Allow location access for Omi in Settings so this phone can find your device, then try again.'; + String get onboardingConnectTapToConnect => 'Omiをタップして接続します。'; @override - String get pairingNotFoundOrDropped => - 'Couldn\'t reach your Omi. Make sure it\'s charged, close to your phone, and its light is blinking, then try again.'; + String get onboardingActMicFallbackBody => 'Omiからまだ音声が届いていません。今はスマートフォンを使いましょう。'; @override - String get pairingBondRecovery => 'Forget Omi in your phone’s Bluetooth settings, then try pairing again.'; + String get onboardingActNoAudioOmi => 'Omiの音声が聞こえません。電源が入っていて近くにあるか確認してください。'; @override - String get pairingHowTo => 'How to pair'; + String get onboardingActNoAudioPhone => 'まだ声が聞こえません。少し大きな声で話してみてください。'; + + @override + String get onboardingActDictateBodyOmi => '話すだけでOK。Omiが聞いています。'; + + @override + String get voiceProfileCardDismissed => 'ボイスプロフィールは設定からいつでも設定できます。'; + + @override + String get homeNewRecordingButton => '新規録音'; + + @override + String get chatWelcomeMessage => 'こんにちは、Omiです。話した内容なら何でも聞いてください。誰が何を言ったか、何を約束したか、一日のまとめなど。タスクの追加や、覚えておくこともできます。'; + + @override + String get onboardingStarterTaskAsk => 'Omiに今日のことを聞いてみる'; + + @override + String get onboardingStarterTaskRecord => 'Omiで会話を録音する'; + + @override + String get onboardingStarterTaskCheck => 'このタスクにチェックして完了する'; + + @override + String get paywallRecommended => 'Recommended'; @override - String get firstCaptureWatchInstructions => - 'Open Omi on your Apple Watch and tap Record. Allow microphone access if asked. Your capture will sync to Omi on this phone.'; + String get paywallNoPlans => 'Plans aren\'t available right now. Try again in a minute.'; @override - String get firstCaptureWatchInstall => - 'In the Watch app on your iPhone, install Omi on your Apple Watch. Then open Omi on your watch and tap Record.'; + String get paywallOtherPlans => 'See all plans'; @override - String get firstCaptureWatchPair => - 'Pair your Apple Watch with this iPhone using the Watch app, then install and open Omi on your watch.'; + String get paywallComparePlans => 'See plans'; @override - String get firstCaptureUnavailable => - 'Recording could not start. Check microphone access and your connection, then try again.'; + String get paywallYourPlan => 'Your plan'; @override - String get firstCaptureDelayed => - 'Your recording is taking longer to process. You can check your conversations on Home or try again.'; + String get paywallNotNow => 'Not now'; @override - String get firstCaptureEmptyTitle => 'Let\'s try that again'; + String get paywallTryAgain => 'Try again'; @override - String get firstCaptureShortTitle => 'Finish This Recording?'; + String get paywallManageBilling => 'Manage billing'; @override - String get firstCaptureShortWarning => 'A few more sentences help Omi understand you. About 30 seconds is ideal.'; + String get paywallLoadError => 'Couldn\'t load plans. Check your connection and try again.'; @override - String get firstCaptureCancelTitle => 'Leave This Recording?'; + String get paywallPlans => 'Plans'; @override - String get firstCaptureCancelWarning => - 'Going back stops this recording. Keep talking to give Omi a few more sentences.'; + String get watchCaptureTitle => 'Record from your Apple Watch'; @override - String get firstCaptureSiriTip => 'Tip: Enable Omi in Shortcuts → Siri.'; + String get watchCaptureOpen => 'Open Omi on your Apple Watch.'; + + @override + String get watchCaptureRecord => 'Tap Record. Allow microphone access if asked.'; + + @override + String get watchCaptureSync => 'Your recording syncs to Omi on this phone.'; + + @override + String get pairingBluetoothOff => 'Turn on Bluetooth on your phone, then try again.'; + + @override + String get pairingBluetoothPermission => 'Allow Bluetooth access for Omi in Settings, then try again.'; + + @override + String get pairingNearbyPermission => 'Allow Nearby devices access for Omi in Settings, then try again.'; + + @override + String get pairingLocationPermission => + 'Allow location access for Omi in Settings so this phone can find your device, then try again.'; + + @override + String get pairingNotFoundOrDropped => + 'Couldn\'t reach your Omi. Make sure it\'s charged, close to your phone, and its light is blinking, then try again.'; + + @override + String get pairingBondRecovery => 'Forget Omi in your phone’s Bluetooth settings, then try pairing again.'; + + @override + String get pairingHowTo => 'How to pair'; @override String get paywallOutlineHeadline => 'もう一言も聞き逃さない。'; diff --git a/app/lib/l10n/app_localizations_kn.dart b/app/lib/l10n/app_localizations_kn.dart index f49f6f755c4..3c1c1dca314 100644 --- a/app/lib/l10n/app_localizations_kn.dart +++ b/app/lib/l10n/app_localizations_kn.dart @@ -1689,8 +1689,7 @@ class AppLocalizationsKn extends AppLocalizations { String get enableBluetooth => 'Bluetooth ಸಕ್ರಿಯಗೊಳಿಸಿ'; @override - String get bluetoothNeeded => - 'Omi ನಿಮ್ಮ ವಿಯರೇಬಲ್‌ನೊಂದಿಗೆ ಸಂಪರ್ಕ ಮಾಡಲು Bluetooth ಅಗತ್ಯವಿದೆ. ದಯವಿಟ್ಟು Bluetooth ಸಕ್ರಿಯಗೊಳಿಸಿ ಮತ್ತೆ ಪ್ರಯತ್ನಿಸಿ.'; + String get bluetoothNeeded => 'Omi ನಿಮ್ಮ ಫೋನ್‌ಗೆ Bluetooth ಮೂಲಕ ಸಂಪರ್ಕಿಸುತ್ತದೆ. ಮುಂದುವರಿಯಲು ಅದನ್ನು ಆನ್ ಮಾಡಿ.'; @override String get contactSupport => 'ಸಹಾಯಕ ಸಂಪರ್ಕ ಮಾಡಿ?'; @@ -4913,7 +4912,7 @@ class AppLocalizationsKn extends AppLocalizations { 'ಮುಂದುವರಿಯುವ ಮೂಲಕ, ನಿಮ್ಮ ಸಂಭಾಷಣೆಗಳು, ರೆಕಾರ್ಡಿಂಗ್‌ಗಳು ಮತ್ತು ವೈಯಕ್ತಿಕ ಮಾಹಿತಿಯನ್ನು ನಮ್ಮ ಸರ್ವರ್‌ಗಳಲ್ಲಿ ಸುರಕ್ಷಿತವಾಗಿ ಸಂಗ್ರಹಿಸಲಾಗುತ್ತದೆ. ನಿಮ್ಮ ಆಡಿಯೋ ರೆಕಾರ್ಡಿಂಗ್‌ಗಳು ಮತ್ತು ಟ್ರಾನ್ಸ್‌ಕ್ರಿಪ್ಟ್‌ಗಳನ್ನು ಮೂರನೇ ವ್ಯಕ್ತಿಯ AI ಸೇವೆಗಳಿಂದ ಸಂಸ್ಕರಿಸಲಾಗುತ್ತದೆ (ಟ್ರಾನ್ಸ್‌ಕ್ರಿಪ್ಷನ್‌ಗಾಗಿ Deepgram ಮತ್ತು ವಿಶ್ಲೇಷಣೆಗಾಗಿ OpenAI ಸೇರಿದಂತೆ) AI-ಚಾಲಿತ ಒಳನೋಟಗಳನ್ನು ಒದಗಿಸಲು ಮತ್ತು ಎಲ್ಲಾ ಅಪ್ಲಿಕೇಶನ್ ವೈಶಿಷ್ಟ್ಯಗಳನ್ನು ಸಕ್ರಿಯಗೊಳಿಸಲು.'; @override - String get tasksEmptyStateMessage => 'ಕಾರ್ಯವನ್ನು ರಚಿಸಲು ಸಂಭಾಷಣೆಯನ್ನು ಪ್ರಾರಂಭಿಸಿ.'; + String get tasksEmptyStateMessage => 'ನಿಮ್ಮ ಸಂಭಾಷಣೆಗಳ ಕಾರ್ಯಗಳು ಇಲ್ಲಿ ಕಾಣಿಸುತ್ತವೆ.'; @override String get clearChatAction => 'ಚ್ಯಾಟ್ ತೀಕ್ಷ್ಣ ಮಾಡಿ'; @@ -13079,154 +13078,206 @@ class AppLocalizationsKn extends AppLocalizations { } @override - String get paywallRecommended => 'Recommended'; + String get onboardingPermissionTapToFix => 'ಸರಿಪಡಿಸಲು ಟ್ಯಾಪ್ ಮಾಡಿ'; @override - String get paywallNoPlans => 'Plans aren\'t available right now. Try again in a minute.'; + String get onboardingNoOmiYet => 'ಇನ್ನೂ ನಿಮ್ಮ Omi ಇಲ್ಲವೇ?'; @override - String get paywallOtherPlans => 'See all plans'; + String get onboardingOmiOrdered => 'ನಾನು ಆರ್ಡರ್ ಮಾಡಿದ್ದೇನೆ'; @override - String get paywallComparePlans => 'See plans'; + String get onboardingOmiNotOrdered => 'ಇನ್ನೂ ಆರ್ಡರ್ ಮಾಡಿಲ್ಲ'; @override - String get paywallYourPlan => 'Your plan'; + String get onboardingConnectOmi => 'Omi ಸಂಪರ್ಕಿಸಿ'; @override - String get paywallNotNow => 'Not now'; + String get consentShortMessage => 'Omi AI ಬಳಸಿ ನಿಮ್ಮ ಸಂಭಾಷಣೆಗಳನ್ನು ಟಿಪ್ಪಣಿಗಳಾಗಿ ಮಾಡುತ್ತದೆ. ಯಾವಾಗ ಬೇಕಾದರೂ ಅಳಿಸಬಹುದು.'; @override - String get paywallTryAgain => 'Try again'; + String consentAiProviders(String providers) { + return '$providers ಮೂಲಕ ಸಂಸ್ಕರಿಸಲಾಗುತ್ತದೆ.'; + } @override - String get paywallManageBilling => 'Manage billing'; + String get onboardingActIntro => 'Omi ಪ್ರಯತ್ನಿಸೋಣ.'; @override - String get paywallLoadError => 'Couldn\'t load plans. Check your connection and try again.'; + String get onboardingActMicTitle => 'Omi ಗೆ ನಿಮ್ಮ ಮೈಕ್ರೊಫೋನ್ ಬೇಕು'; @override - String get paywallPlans => 'Plans'; + String get onboardingActMicBody => 'ನೀವು ಮಾತನಾಡಿದಾಗ Omi ಕೇಳಲು.'; @override - String get firstCaptureTitle => 'How will you capture?'; + String get onboardingActMicAllow => 'ಮೈಕ್ರೊಫೋನ್ ಅನುಮತಿಸಿ'; @override - String get firstCaptureDescription => 'Choose how you want to capture your first conversation.'; + String get onboardingActDictateTitle => 'ನಿಮ್ಮ ದಿನದ ಬಗ್ಗೆ Omi ಗೆ ಹೇಳಿ'; @override - String get firstCaptureOmi => 'I Have an Omi'; + String get onboardingActDictateBody => 'ನಿಮ್ಮದೇ ರೀತಿಯಲ್ಲಿ ಹೇಳಿ. ಉಳಿದದ್ದು Omi ನೋಡಿಕೊಳ್ಳುತ್ತದೆ.'; @override - String get firstCapturePhone => 'This Phone'; + String get onboardingActDictateExample => + 'ಈಗಷ್ಟೇ ಸ್ಯಾಮ್ ಜೊತೆ ಊಟ ಮಾಡಿದೆ, ಅವಳು ಜೂನ್‌ನಲ್ಲಿ ಲಿಸ್ಬನ್‌ಗೆ ಹೋಗುತ್ತಿದ್ದಾಳೆ! ಶುಕ್ರವಾರದೊಳಗೆ ತಂಡಕ್ಕೆ ಡೆಕ್ ಕಳುಹಿಸಲು ಮತ್ತು ಮನೆಗೆ ಹೋಗುವಾಗ ಹಾಲು ತರಲು ನೆನಪಿಸು.'; @override - String get firstCaptureWatch => 'Apple Watch'; + String get onboardingActDictateHint => 'ಮುಗಿದ ನಂತರ ಟಿಕ್ ಟ್ಯಾಪ್ ಮಾಡಿ'; @override - String get firstCaptureSkip => 'Skip for Now'; + String get onboardingActPhoneMic => 'ಫೋನ್ ಮೈಕ್ರೊಫೋನ್'; @override - String get firstCapturePrompt => - 'Tell Omi what you\'re working on today, or what\'s on your mind. Talk for about 30 seconds.'; + String get onboardingActWriting => 'ಸಾರಾಂಶ ಮಾಡುತ್ತಿದೆ…'; @override - String get firstCaptureKeepGoing => 'Keep going… 30 seconds is ideal.'; + String get onboardingActRemembers => 'ನೀವು ನೆನಪಿಡಬೇಕಿಲ್ಲ, Omi ನೆನಪಿಡುತ್ತದೆ'; @override - String get firstCaptureReady => 'You can finish now, or keep talking.'; + String get onboardingActNotesLabel => 'ನಿಮ್ಮ ದಿನದ ಸಾರಾಂಶ'; @override - String get firstCaptureStart => 'Start Recording'; + String get onboardingActTasksLabel => 'Omi ನಿಮ್ಮ ಕೆಲಸಗಳನ್ನು ಗುರುತಿಸಿದೆ'; @override - String get firstCaptureFinish => 'Finish Recording'; + String get onboardingActYourWords => 'ನೀವು ಹೇಳಿದ್ದು ಇದು'; @override - String get firstCaptureListening => 'Listening… your words will appear here.'; + String onboardingActCongratsTitle(String name) { + return 'ಎಲ್ಲವೂ ಸಿದ್ಧ, $name'; + } @override - String get firstCaptureProcessing => 'Creating your first conversation…'; + String get onboardingActRateTitle => 'ಇಲ್ಲಿಯವರೆಗೆ Omi ಇಷ್ಟವಾಯಿತೇ?'; @override - String get firstCaptureResult => 'Your first conversation'; + String get onboardingActRateYes => 'Omi ಗೆ ರೇಟ್ ಮಾಡಿ'; @override - String get firstCaptureEmpty => 'I didn\'t catch enough speech, try again with a few sentences'; + String get onboardingWelcomeCardTitle => 'Omi ಗೆ ಸ್ವಾಗತ'; @override - String get watchCaptureTitle => 'Record from your Apple Watch'; + String get onboardingWelcomeCardBody => + 'Omi ಧರಿಸಿ ಎಂದಿನಂತೆ ಮಾತನಾಡಿ. ಪ್ರತಿಯೊಂದು ಸಂಭಾಷಣೆ ಇಲ್ಲೇ ಟಿಪ್ಪಣಿ, ಕಾರ್ಯ ಮತ್ತು ನೆನಪುಗಳಾಗುತ್ತದೆ.'; @override - String get watchCaptureOpen => 'Open Omi on your Apple Watch.'; + String get onboardingActDemoTitle => 'ಸ್ಯಾಮ್ ಜೊತೆ ಊಟ'; @override - String get watchCaptureRecord => 'Tap Record. Allow microphone access if asked.'; + String get onboardingActDemoSummary => + 'ಜೂನ್‌ನಲ್ಲಿ ಲಿಸ್ಬನ್‌ಗೆ ಹೋಗುತ್ತಿರುವ ಸ್ಯಾಮ್ ಜೊತೆ ಊಟ. ಶುಕ್ರವಾರದೊಳಗೆ ತಂಡಕ್ಕೆ ಡೆಕ್ ಕಳುಹಿಸಬೇಕು, ಮನೆಗೆ ಹೋಗುವಾಗ ಹಾಲು ತರಬೇಕು.'; @override - String get watchCaptureSync => 'Your recording syncs to Omi on this phone.'; + String get onboardingActDemoTask1 => 'ಶುಕ್ರವಾರದೊಳಗೆ ತಂಡಕ್ಕೆ ಡೆಕ್ ಕಳುಹಿಸುವುದು'; @override - String get pairingBluetoothOff => 'Turn on Bluetooth on your phone, then try again.'; + String get onboardingActDemoTask2 => 'ಮನೆಗೆ ಹೋಗುವಾಗ ಹಾಲು ತರುವುದು'; @override - String get pairingBluetoothPermission => 'Allow Bluetooth access for Omi in Settings, then try again.'; + String get onboardingActJustNow => 'ಈಗಷ್ಟೇ'; @override - String get pairingNearbyPermission => 'Allow Nearby devices access for Omi in Settings, then try again.'; + String get onboardingActSavedToTasks => 'ನಿಮ್ಮ ಕೆಲಸಗಳಿಗೆ ಉಳಿಸಲಾಗಿದೆ'; @override - String get pairingLocationPermission => - 'Allow location access for Omi in Settings so this phone can find your device, then try again.'; + String get onboardingConnectTapToConnect => 'ಸಂಪರ್ಕಿಸಲು ನಿಮ್ಮ Omi ಟ್ಯಾಪ್ ಮಾಡಿ.'; @override - String get pairingNotFoundOrDropped => - 'Couldn\'t reach your Omi. Make sure it\'s charged, close to your phone, and its light is blinking, then try again.'; + String get onboardingActMicFallbackBody => 'ನಿಮ್ಮ Omi ಇನ್ನೂ ಆಡಿಯೋ ಕಳುಹಿಸುತ್ತಿಲ್ಲ. ಸದ್ಯಕ್ಕೆ ನಿಮ್ಮ ಫೋನ್ ಬಳಸಿ.'; @override - String get pairingBondRecovery => 'Forget Omi in your phone’s Bluetooth settings, then try pairing again.'; + String get onboardingActNoAudioOmi => 'ನಿಮ್ಮ Omi ಕೇಳಿಸುತ್ತಿಲ್ಲ. ಅದು ಆನ್ ಆಗಿದೆ ಮತ್ತು ಹತ್ತಿರವಿದೆಯೇ ನೋಡಿ.'; @override - String get pairingHowTo => 'How to pair'; + String get onboardingActNoAudioPhone => 'ಇನ್ನೂ ನಿಮ್ಮ ಧ್ವನಿ ಕೇಳಿಸುತ್ತಿಲ್ಲ. ಸ್ವಲ್ಪ ಜೋರಾಗಿ ಮಾತಾಡಿ ನೋಡಿ.'; + + @override + String get onboardingActDictateBodyOmi => 'ಸುಮ್ಮನೆ ಮಾತಾಡಿ. ನಿಮ್ಮ Omi ಕೇಳುತ್ತಿದೆ.'; + + @override + String get voiceProfileCardDismissed => 'ಸೆಟ್ಟಿಂಗ್‌ಗಳಲ್ಲಿ ಯಾವಾಗ ಬೇಕಾದರೂ ಧ್ವನಿ ಪ್ರೊಫೈಲ್ ಹೊಂದಿಸಬಹುದು.'; + + @override + String get homeNewRecordingButton => 'ಹೊಸ ರೆಕಾರ್ಡಿಂಗ್'; + + @override + String get chatWelcomeMessage => + 'ನಮಸ್ಕಾರ, ನಾನು Omi. ನೀವು ಮಾತನಾಡಿದ ಯಾವುದರ ಬಗ್ಗೆಯೂ ಕೇಳಿ: ಯಾರು ಏನು ಹೇಳಿದರು, ನೀವು ಏನು ಭರವಸೆ ನೀಡಿದ್ದೀರಿ ಅಥವಾ ನಿಮ್ಮ ದಿನದ ಸಾರಾಂಶ. ನಾನು ಕಾರ್ಯಗಳನ್ನು ಸೇರಿಸಬಹುದು ಮತ್ತು ನಿಮಗಾಗಿ ವಿಷಯಗಳನ್ನು ನೆನಪಿಟ್ಟುಕೊಳ್ಳಬಹುದು.'; + + @override + String get onboardingStarterTaskAsk => 'ನಿಮ್ಮ ದಿನದ ಬಗ್ಗೆ Omi ಅನ್ನು ಕೇಳಿ'; + + @override + String get onboardingStarterTaskRecord => 'Omi ಮೂಲಕ ಒಂದು ಸಂಭಾಷಣೆಯನ್ನು ರೆಕಾರ್ಡ್ ಮಾಡಿ'; + + @override + String get onboardingStarterTaskCheck => 'ಮುಗಿಸಲು ಈ ಕಾರ್ಯವನ್ನು ಟಿಕ್ ಮಾಡಿ'; + + @override + String get paywallRecommended => 'Recommended'; @override - String get firstCaptureWatchInstructions => - 'Open Omi on your Apple Watch and tap Record. Allow microphone access if asked. Your capture will sync to Omi on this phone.'; + String get paywallNoPlans => 'Plans aren\'t available right now. Try again in a minute.'; @override - String get firstCaptureWatchInstall => - 'In the Watch app on your iPhone, install Omi on your Apple Watch. Then open Omi on your watch and tap Record.'; + String get paywallOtherPlans => 'See all plans'; @override - String get firstCaptureWatchPair => - 'Pair your Apple Watch with this iPhone using the Watch app, then install and open Omi on your watch.'; + String get paywallComparePlans => 'See plans'; @override - String get firstCaptureUnavailable => - 'Recording could not start. Check microphone access and your connection, then try again.'; + String get paywallYourPlan => 'Your plan'; @override - String get firstCaptureDelayed => - 'Your recording is taking longer to process. You can check your conversations on Home or try again.'; + String get paywallNotNow => 'Not now'; @override - String get firstCaptureEmptyTitle => 'Let\'s try that again'; + String get paywallTryAgain => 'Try again'; @override - String get firstCaptureShortTitle => 'Finish This Recording?'; + String get paywallManageBilling => 'Manage billing'; @override - String get firstCaptureShortWarning => 'A few more sentences help Omi understand you. About 30 seconds is ideal.'; + String get paywallLoadError => 'Couldn\'t load plans. Check your connection and try again.'; @override - String get firstCaptureCancelTitle => 'Leave This Recording?'; + String get paywallPlans => 'Plans'; @override - String get firstCaptureCancelWarning => - 'Going back stops this recording. Keep talking to give Omi a few more sentences.'; + String get watchCaptureTitle => 'Record from your Apple Watch'; @override - String get firstCaptureSiriTip => 'Tip: Enable Omi in Shortcuts → Siri.'; + String get watchCaptureOpen => 'Open Omi on your Apple Watch.'; + + @override + String get watchCaptureRecord => 'Tap Record. Allow microphone access if asked.'; + + @override + String get watchCaptureSync => 'Your recording syncs to Omi on this phone.'; + + @override + String get pairingBluetoothOff => 'Turn on Bluetooth on your phone, then try again.'; + + @override + String get pairingBluetoothPermission => 'Allow Bluetooth access for Omi in Settings, then try again.'; + + @override + String get pairingNearbyPermission => 'Allow Nearby devices access for Omi in Settings, then try again.'; + + @override + String get pairingLocationPermission => + 'Allow location access for Omi in Settings so this phone can find your device, then try again.'; + + @override + String get pairingNotFoundOrDropped => + 'Couldn\'t reach your Omi. Make sure it\'s charged, close to your phone, and its light is blinking, then try again.'; + + @override + String get pairingBondRecovery => 'Forget Omi in your phone’s Bluetooth settings, then try pairing again.'; + + @override + String get pairingHowTo => 'How to pair'; @override String get paywallOutlineHeadline => 'ಇನ್ನು ಮುಂದೆ ಒಂದು ಪದವನ್ನೂ ತಪ್ಪಿಸಿಕೊಳ್ಳಬೇಡಿ.'; diff --git a/app/lib/l10n/app_localizations_ko.dart b/app/lib/l10n/app_localizations_ko.dart index 80b677992f4..214ca6cea2c 100644 --- a/app/lib/l10n/app_localizations_ko.dart +++ b/app/lib/l10n/app_localizations_ko.dart @@ -1662,7 +1662,7 @@ class AppLocalizationsKo extends AppLocalizations { String get enableBluetooth => '블루투스 활성화'; @override - String get bluetoothNeeded => 'Omi가 웨어러블에 연결하려면 블루투스가 필요합니다. 블루투스를 활성화하고 다시 시도해 주세요.'; + String get bluetoothNeeded => 'Omi는 Bluetooth로 휴대폰에 연결돼요. 계속하려면 켜 주세요.'; @override String get contactSupport => '지원팀에 문의하시겠습니까?'; @@ -4814,7 +4814,7 @@ class AppLocalizationsKo extends AppLocalizations { '계속하면 대화, 녹음 및 개인 정보가 서버에 안전하게 저장됩니다. 오디오 녹음 및 텍스트 변환은 제3자 AI 서비스(전사를 위한 Deepgram 및 분석을 위한 OpenAI 포함)에 의해 처리되어 AI 기반 인사이트를 제공하고 모든 앱 기능을 활성화합니다.'; @override - String get tasksEmptyStateMessage => '대화를 시작하여 할 일을 만드세요.'; + String get tasksEmptyStateMessage => '대화에서 나온 할 일이 여기에 표시돼요.'; @override String get clearChatAction => '채팅 삭제'; @@ -12859,154 +12859,204 @@ class AppLocalizationsKo extends AppLocalizations { } @override - String get paywallRecommended => 'Recommended'; + String get onboardingPermissionTapToFix => '탭하여 해결'; @override - String get paywallNoPlans => 'Plans aren\'t available right now. Try again in a minute.'; + String get onboardingNoOmiYet => '아직 Omi가 없으신가요?'; @override - String get paywallOtherPlans => 'See all plans'; + String get onboardingOmiOrdered => '주문했어요'; @override - String get paywallComparePlans => 'See plans'; + String get onboardingOmiNotOrdered => '아직 주문하지 않았어요'; @override - String get paywallYourPlan => 'Your plan'; + String get onboardingConnectOmi => 'Omi 연결'; @override - String get paywallNotNow => 'Not now'; + String get consentShortMessage => 'Omi는 AI로 대화를 메모로 정리합니다. 언제든지 삭제할 수 있습니다.'; @override - String get paywallTryAgain => 'Try again'; + String consentAiProviders(String providers) { + return '$providers(으)로 처리됩니다.'; + } @override - String get paywallManageBilling => 'Manage billing'; + String get onboardingActIntro => 'Omi를 써 볼까요?'; @override - String get paywallLoadError => 'Couldn\'t load plans. Check your connection and try again.'; + String get onboardingActMicTitle => 'Omi에 마이크 권한이 필요해요'; @override - String get paywallPlans => 'Plans'; + String get onboardingActMicBody => '말할 때 Omi가 들을 수 있도록요.'; @override - String get firstCaptureTitle => 'How will you capture?'; + String get onboardingActMicAllow => '마이크 허용'; @override - String get firstCaptureDescription => 'Choose how you want to capture your first conversation.'; + String get onboardingActDictateTitle => 'Omi에게 오늘 하루를 말해 보세요'; @override - String get firstCaptureOmi => 'I Have an Omi'; + String get onboardingActDictateBody => '편하게 말하세요. 나머지는 Omi가 할게요.'; @override - String get firstCapturePhone => 'This Phone'; + String get onboardingActDictateExample => + '방금 샘이랑 점심 먹었어. 6월에 리스본으로 이사 간대! 금요일까지 팀에 발표 자료 보내는 거랑, 집에 가는 길에 우유 사는 거 알려 줘.'; @override - String get firstCaptureWatch => 'Apple Watch'; + String get onboardingActDictateHint => '다 말했으면 체크를 누르세요'; @override - String get firstCaptureSkip => 'Skip for Now'; + String get onboardingActPhoneMic => '휴대폰 마이크'; @override - String get firstCapturePrompt => - 'Tell Omi what you\'re working on today, or what\'s on your mind. Talk for about 30 seconds.'; + String get onboardingActWriting => '요약하는 중…'; @override - String get firstCaptureKeepGoing => 'Keep going… 30 seconds is ideal.'; + String get onboardingActRemembers => '기억은 Omi가 대신해요'; @override - String get firstCaptureReady => 'You can finish now, or keep talking.'; + String get onboardingActNotesLabel => '오늘 하루 요약'; @override - String get firstCaptureStart => 'Start Recording'; + String get onboardingActTasksLabel => 'Omi가 할 일을 찾았어요'; @override - String get firstCaptureFinish => 'Finish Recording'; + String get onboardingActYourWords => '이렇게 말했어요'; @override - String get firstCaptureListening => 'Listening… your words will appear here.'; + String onboardingActCongratsTitle(String name) { + return '$name님, 준비가 끝났어요'; + } @override - String get firstCaptureProcessing => 'Creating your first conversation…'; + String get onboardingActRateTitle => 'Omi 마음에 드시나요?'; @override - String get firstCaptureResult => 'Your first conversation'; + String get onboardingActRateYes => 'Omi 평가하기'; @override - String get firstCaptureEmpty => 'I didn\'t catch enough speech, try again with a few sentences'; + String get onboardingWelcomeCardTitle => 'Omi에 오신 것을 환영해요'; @override - String get watchCaptureTitle => 'Record from your Apple Watch'; + String get onboardingWelcomeCardBody => 'Omi를 착용하고 평소처럼 대화하세요. 모든 대화가 여기서 노트, 할 일, 기억이 돼요.'; @override - String get watchCaptureOpen => 'Open Omi on your Apple Watch.'; + String get onboardingActDemoTitle => '샘과 점심'; @override - String get watchCaptureRecord => 'Tap Record. Allow microphone access if asked.'; + String get onboardingActDemoSummary => '6월에 리스본으로 이사하는 샘과 점심. 금요일까지 팀에 자료를 보내고, 집에 가는 길에 우유를 사야 해요.'; @override - String get watchCaptureSync => 'Your recording syncs to Omi on this phone.'; + String get onboardingActDemoTask1 => '금요일까지 팀에 발표 자료 보내기'; @override - String get pairingBluetoothOff => 'Turn on Bluetooth on your phone, then try again.'; + String get onboardingActDemoTask2 => '집에 가는 길에 우유 사기'; @override - String get pairingBluetoothPermission => 'Allow Bluetooth access for Omi in Settings, then try again.'; + String get onboardingActJustNow => '방금'; @override - String get pairingNearbyPermission => 'Allow Nearby devices access for Omi in Settings, then try again.'; + String get onboardingActSavedToTasks => '할 일에 저장됨'; @override - String get pairingLocationPermission => - 'Allow location access for Omi in Settings so this phone can find your device, then try again.'; + String get onboardingConnectTapToConnect => 'Omi를 탭해서 연결하세요.'; @override - String get pairingNotFoundOrDropped => - 'Couldn\'t reach your Omi. Make sure it\'s charged, close to your phone, and its light is blinking, then try again.'; + String get onboardingActMicFallbackBody => 'Omi가 아직 오디오를 보내지 않아요. 지금은 휴대폰을 사용하세요.'; @override - String get pairingBondRecovery => 'Forget Omi in your phone’s Bluetooth settings, then try pairing again.'; + String get onboardingActNoAudioOmi => 'Omi 소리가 들리지 않아요. 켜져 있고 가까이 있는지 확인하세요.'; @override - String get pairingHowTo => 'How to pair'; + String get onboardingActNoAudioPhone => '아직 목소리가 들리지 않아요. 조금 더 크게 말해 보세요.'; + + @override + String get onboardingActDictateBodyOmi => '그냥 말하세요. Omi가 듣고 있어요.'; + + @override + String get voiceProfileCardDismissed => '음성 프로필은 설정에서 언제든지 설정할 수 있어요.'; + + @override + String get homeNewRecordingButton => '새 녹음'; + + @override + String get chatWelcomeMessage => + '안녕하세요, Omi예요. 나눈 대화라면 무엇이든 물어보세요. 누가 무슨 말을 했는지, 무엇을 약속했는지, 하루 요약까지요. 할 일을 추가하고 기억해 둘 수도 있어요.'; + + @override + String get onboardingStarterTaskAsk => 'Omi에게 오늘 하루에 대해 물어보기'; + + @override + String get onboardingStarterTaskRecord => 'Omi로 대화 녹음하기'; + + @override + String get onboardingStarterTaskCheck => '이 할 일을 체크해서 완료하기'; + + @override + String get paywallRecommended => 'Recommended'; @override - String get firstCaptureWatchInstructions => - 'Open Omi on your Apple Watch and tap Record. Allow microphone access if asked. Your capture will sync to Omi on this phone.'; + String get paywallNoPlans => 'Plans aren\'t available right now. Try again in a minute.'; @override - String get firstCaptureWatchInstall => - 'In the Watch app on your iPhone, install Omi on your Apple Watch. Then open Omi on your watch and tap Record.'; + String get paywallOtherPlans => 'See all plans'; @override - String get firstCaptureWatchPair => - 'Pair your Apple Watch with this iPhone using the Watch app, then install and open Omi on your watch.'; + String get paywallComparePlans => 'See plans'; @override - String get firstCaptureUnavailable => - 'Recording could not start. Check microphone access and your connection, then try again.'; + String get paywallYourPlan => 'Your plan'; @override - String get firstCaptureDelayed => - 'Your recording is taking longer to process. You can check your conversations on Home or try again.'; + String get paywallNotNow => 'Not now'; @override - String get firstCaptureEmptyTitle => 'Let\'s try that again'; + String get paywallTryAgain => 'Try again'; @override - String get firstCaptureShortTitle => 'Finish This Recording?'; + String get paywallManageBilling => 'Manage billing'; @override - String get firstCaptureShortWarning => 'A few more sentences help Omi understand you. About 30 seconds is ideal.'; + String get paywallLoadError => 'Couldn\'t load plans. Check your connection and try again.'; @override - String get firstCaptureCancelTitle => 'Leave This Recording?'; + String get paywallPlans => 'Plans'; @override - String get firstCaptureCancelWarning => - 'Going back stops this recording. Keep talking to give Omi a few more sentences.'; + String get watchCaptureTitle => 'Record from your Apple Watch'; @override - String get firstCaptureSiriTip => 'Tip: Enable Omi in Shortcuts → Siri.'; + String get watchCaptureOpen => 'Open Omi on your Apple Watch.'; + + @override + String get watchCaptureRecord => 'Tap Record. Allow microphone access if asked.'; + + @override + String get watchCaptureSync => 'Your recording syncs to Omi on this phone.'; + + @override + String get pairingBluetoothOff => 'Turn on Bluetooth on your phone, then try again.'; + + @override + String get pairingBluetoothPermission => 'Allow Bluetooth access for Omi in Settings, then try again.'; + + @override + String get pairingNearbyPermission => 'Allow Nearby devices access for Omi in Settings, then try again.'; + + @override + String get pairingLocationPermission => + 'Allow location access for Omi in Settings so this phone can find your device, then try again.'; + + @override + String get pairingNotFoundOrDropped => + 'Couldn\'t reach your Omi. Make sure it\'s charged, close to your phone, and its light is blinking, then try again.'; + + @override + String get pairingBondRecovery => 'Forget Omi in your phone’s Bluetooth settings, then try pairing again.'; + + @override + String get pairingHowTo => 'How to pair'; @override String get paywallOutlineHeadline => '이제 단 한마디도 놓치지 마세요.'; diff --git a/app/lib/l10n/app_localizations_lt.dart b/app/lib/l10n/app_localizations_lt.dart index dc4fc8ed1b1..494a8995865 100644 --- a/app/lib/l10n/app_localizations_lt.dart +++ b/app/lib/l10n/app_localizations_lt.dart @@ -1684,8 +1684,7 @@ class AppLocalizationsLt extends AppLocalizations { String get enableBluetooth => 'Įjungti Bluetooth'; @override - String get bluetoothNeeded => - 'Omi reikia Bluetooth, kad prisijungtų prie jūsų nešiojamo įrenginio. Įjunkite Bluetooth ir bandykite dar kartą.'; + String get bluetoothNeeded => 'Omi prie telefono jungiasi per Bluetooth. Įjunkite jį, kad tęstumėte.'; @override String get contactSupport => 'Susisiekti su palaikymu?'; @@ -4893,7 +4892,7 @@ class AppLocalizationsLt extends AppLocalizations { 'Tęsdami, jūsų pokalbiai, įrašai ir asmeninė informacija bus saugiai saugomi mūsų serveriuose. Jūsų garso įrašai ir transkripcijos apdorojami trečiųjų šalių AI paslaugų (įskaitant Deepgram transkripcijai ir OpenAI analizei), kad suteiktų jums AI paremtas įžvalgas ir įgalintų visas programėlės funkcijas.'; @override - String get tasksEmptyStateMessage => 'Pradėkite pokalbį, kad sukurtumėte užduotį.'; + String get tasksEmptyStateMessage => 'Užduotys iš jūsų pokalbių bus rodomos čia.'; @override String get clearChatAction => 'Išvalyti pokalbį'; @@ -13064,154 +13063,207 @@ class AppLocalizationsLt extends AppLocalizations { } @override - String get paywallRecommended => 'Recommended'; + String get onboardingPermissionTapToFix => 'Bakstelėkite, kad pataisytumėte'; @override - String get paywallNoPlans => 'Plans aren\'t available right now. Try again in a minute.'; + String get onboardingNoOmiYet => 'Dar neturite Omi?'; @override - String get paywallOtherPlans => 'See all plans'; + String get onboardingOmiOrdered => 'Jau užsisakiau'; @override - String get paywallComparePlans => 'See plans'; + String get onboardingOmiNotOrdered => 'Dar neužsisakiau'; @override - String get paywallYourPlan => 'Your plan'; + String get onboardingConnectOmi => 'Prijunkite Omi'; @override - String get paywallNotNow => 'Not now'; + String get consentShortMessage => + 'Omi naudoja DI, kad jūsų pokalbius paverstų užrašais. Juos galite ištrinti bet kada.'; @override - String get paywallTryAgain => 'Try again'; + String consentAiProviders(String providers) { + return 'Apdorojama su $providers.'; + } @override - String get paywallManageBilling => 'Manage billing'; + String get onboardingActIntro => 'Išbandykime Omi.'; @override - String get paywallLoadError => 'Couldn\'t load plans. Check your connection and try again.'; + String get onboardingActMicTitle => 'Omi reikia mikrofono'; @override - String get paywallPlans => 'Plans'; + String get onboardingActMicBody => 'Kad Omi girdėtų, kai su juo kalbate.'; @override - String get firstCaptureTitle => 'How will you capture?'; + String get onboardingActMicAllow => 'Leisti mikrofoną'; @override - String get firstCaptureDescription => 'Choose how you want to capture your first conversation.'; + String get onboardingActDictateTitle => 'Papasakokite Omi apie savo dieną'; @override - String get firstCaptureOmi => 'I Have an Omi'; + String get onboardingActDictateBody => 'Sakykite savaip. Visa kita padarys Omi.'; @override - String get firstCapturePhone => 'This Phone'; + String get onboardingActDictateExample => + 'Ką tik pietavau su Sem, birželį ji kraustosi į Lisaboną! Primink man iki penktadienio nusiųsti pristatymą komandai ir grįžtant namo nupirkti pieno.'; @override - String get firstCaptureWatch => 'Apple Watch'; + String get onboardingActDictateHint => 'Baigę bakstelėkite varnelę'; @override - String get firstCaptureSkip => 'Skip for Now'; + String get onboardingActPhoneMic => 'Telefono mikrofonas'; @override - String get firstCapturePrompt => - 'Tell Omi what you\'re working on today, or what\'s on your mind. Talk for about 30 seconds.'; + String get onboardingActWriting => 'Apibendriname…'; @override - String get firstCaptureKeepGoing => 'Keep going… 30 seconds is ideal.'; + String get onboardingActRemembers => 'Omi prisimena, kad jums nereikėtų'; @override - String get firstCaptureReady => 'You can finish now, or keep talking.'; + String get onboardingActNotesLabel => 'Jūsų diena trumpai'; @override - String get firstCaptureStart => 'Start Recording'; + String get onboardingActTasksLabel => 'Omi pastebėjo jūsų darbus'; @override - String get firstCaptureFinish => 'Finish Recording'; + String get onboardingActYourWords => 'Štai ką pasakėte'; @override - String get firstCaptureListening => 'Listening… your words will appear here.'; + String onboardingActCongratsTitle(String name) { + return 'Viskas paruošta, $name'; + } @override - String get firstCaptureProcessing => 'Creating your first conversation…'; + String get onboardingActRateTitle => 'Ar Omi jums patinka?'; @override - String get firstCaptureResult => 'Your first conversation'; + String get onboardingActRateYes => 'Įvertinti Omi'; @override - String get firstCaptureEmpty => 'I didn\'t catch enough speech, try again with a few sentences'; + String get onboardingWelcomeCardTitle => 'Sveiki atvykę į Omi'; @override - String get watchCaptureTitle => 'Record from your Apple Watch'; + String get onboardingWelcomeCardBody => + 'Nešiokite Omi ir kalbėkite kaip įprasta. Kiekvienas pokalbis čia virsta užrašais, užduotimis ir prisiminimais.'; @override - String get watchCaptureOpen => 'Open Omi on your Apple Watch.'; + String get onboardingActDemoTitle => 'Pietūs su Sem'; @override - String get watchCaptureRecord => 'Tap Record. Allow microphone access if asked.'; + String get onboardingActDemoSummary => + 'Pietūs su Sem, kuri birželį persikelia į Lisaboną. Iki penktadienio norite išsiųsti komandai pristatymą ir grįžtant nupirkti pieno.'; @override - String get watchCaptureSync => 'Your recording syncs to Omi on this phone.'; + String get onboardingActDemoTask1 => 'Iki penktadienio nusiųsti pristatymą komandai'; @override - String get pairingBluetoothOff => 'Turn on Bluetooth on your phone, then try again.'; + String get onboardingActDemoTask2 => 'Grįžtant namo nupirkti pieno'; @override - String get pairingBluetoothPermission => 'Allow Bluetooth access for Omi in Settings, then try again.'; + String get onboardingActJustNow => 'Ką tik'; @override - String get pairingNearbyPermission => 'Allow Nearby devices access for Omi in Settings, then try again.'; + String get onboardingActSavedToTasks => 'Išsaugota jūsų užduotyse'; @override - String get pairingLocationPermission => - 'Allow location access for Omi in Settings so this phone can find your device, then try again.'; + String get onboardingConnectTapToConnect => 'Palieskite savo Omi, kad prisijungtumėte.'; @override - String get pairingNotFoundOrDropped => - 'Couldn\'t reach your Omi. Make sure it\'s charged, close to your phone, and its light is blinking, then try again.'; + String get onboardingActMicFallbackBody => 'Omi dar nesiunčia garso. Kol kas naudokite telefoną.'; @override - String get pairingBondRecovery => 'Forget Omi in your phone’s Bluetooth settings, then try pairing again.'; + String get onboardingActNoAudioOmi => 'Omi negirdėti. Patikrinkite, ar jis įjungtas ir šalia.'; @override - String get pairingHowTo => 'How to pair'; + String get onboardingActNoAudioPhone => 'Dar jūsų negirdime. Pabandykite kalbėti šiek tiek garsiau.'; + + @override + String get onboardingActDictateBodyOmi => 'Tiesiog kalbėkite. Jūsų Omi klauso.'; + + @override + String get voiceProfileCardDismissed => 'Balso profilį galite nustatyti bet kada nustatymuose.'; + + @override + String get homeNewRecordingButton => 'Naujas įrašas'; + + @override + String get chatWelcomeMessage => + 'Labas, aš Omi. Klausk manęs apie bet ką, apie ką kalbėjai: ką kas nors pasakė, ką pažadėjai ar tavo dienos santrauką. Taip pat galiu pridėti užduočių ir prisiminti dalykus už tave.'; + + @override + String get onboardingStarterTaskAsk => 'Paklausk Omi apie savo dieną'; + + @override + String get onboardingStarterTaskRecord => 'Įrašyk pokalbį su Omi'; + + @override + String get onboardingStarterTaskCheck => 'Pažymėk šią užduotį kaip atliktą'; + + @override + String get paywallRecommended => 'Recommended'; @override - String get firstCaptureWatchInstructions => - 'Open Omi on your Apple Watch and tap Record. Allow microphone access if asked. Your capture will sync to Omi on this phone.'; + String get paywallNoPlans => 'Plans aren\'t available right now. Try again in a minute.'; @override - String get firstCaptureWatchInstall => - 'In the Watch app on your iPhone, install Omi on your Apple Watch. Then open Omi on your watch and tap Record.'; + String get paywallOtherPlans => 'See all plans'; @override - String get firstCaptureWatchPair => - 'Pair your Apple Watch with this iPhone using the Watch app, then install and open Omi on your watch.'; + String get paywallComparePlans => 'See plans'; @override - String get firstCaptureUnavailable => - 'Recording could not start. Check microphone access and your connection, then try again.'; + String get paywallYourPlan => 'Your plan'; @override - String get firstCaptureDelayed => - 'Your recording is taking longer to process. You can check your conversations on Home or try again.'; + String get paywallNotNow => 'Not now'; @override - String get firstCaptureEmptyTitle => 'Let\'s try that again'; + String get paywallTryAgain => 'Try again'; @override - String get firstCaptureShortTitle => 'Finish This Recording?'; + String get paywallManageBilling => 'Manage billing'; @override - String get firstCaptureShortWarning => 'A few more sentences help Omi understand you. About 30 seconds is ideal.'; + String get paywallLoadError => 'Couldn\'t load plans. Check your connection and try again.'; @override - String get firstCaptureCancelTitle => 'Leave This Recording?'; + String get paywallPlans => 'Plans'; @override - String get firstCaptureCancelWarning => - 'Going back stops this recording. Keep talking to give Omi a few more sentences.'; + String get watchCaptureTitle => 'Record from your Apple Watch'; @override - String get firstCaptureSiriTip => 'Tip: Enable Omi in Shortcuts → Siri.'; + String get watchCaptureOpen => 'Open Omi on your Apple Watch.'; + + @override + String get watchCaptureRecord => 'Tap Record. Allow microphone access if asked.'; + + @override + String get watchCaptureSync => 'Your recording syncs to Omi on this phone.'; + + @override + String get pairingBluetoothOff => 'Turn on Bluetooth on your phone, then try again.'; + + @override + String get pairingBluetoothPermission => 'Allow Bluetooth access for Omi in Settings, then try again.'; + + @override + String get pairingNearbyPermission => 'Allow Nearby devices access for Omi in Settings, then try again.'; + + @override + String get pairingLocationPermission => + 'Allow location access for Omi in Settings so this phone can find your device, then try again.'; + + @override + String get pairingNotFoundOrDropped => + 'Couldn\'t reach your Omi. Make sure it\'s charged, close to your phone, and its light is blinking, then try again.'; + + @override + String get pairingBondRecovery => 'Forget Omi in your phone’s Bluetooth settings, then try pairing again.'; + + @override + String get pairingHowTo => 'How to pair'; @override String get paywallOutlineHeadline => 'Daugiau nepraleiskite nė žodžio.'; diff --git a/app/lib/l10n/app_localizations_lv.dart b/app/lib/l10n/app_localizations_lv.dart index 2bfb9063af8..5a84fd91e16 100644 --- a/app/lib/l10n/app_localizations_lv.dart +++ b/app/lib/l10n/app_localizations_lv.dart @@ -1690,8 +1690,7 @@ class AppLocalizationsLv extends AppLocalizations { String get enableBluetooth => 'Iespējot Bluetooth'; @override - String get bluetoothNeeded => - 'Omi nepieciešams Bluetooth, lai savienotos ar jūsu valkājamo ierīci. Lūdzu, iespējojiet Bluetooth un mēģiniet vēlreiz.'; + String get bluetoothNeeded => 'Omi ar tālruni savienojas, izmantojot Bluetooth. Ieslēdziet to, lai turpinātu.'; @override String get contactSupport => 'Sazināties ar atbalstu?'; @@ -4900,7 +4899,7 @@ class AppLocalizationsLv extends AppLocalizations { 'Turpinot, jūsu sarunas, ieraksti un personiskā informācija tiks droši glabāta mūsu serveros. Jūsu audio ieraksti un transkripcijas tiek apstrādātas ar trešo pušu AI pakalpojumiem (ieskaitot Deepgram transkripcijai un OpenAI analīzei), lai sniegtu jums AI vadītus ieskatus un iespējotu visas lietotnes funkcijas.'; @override - String get tasksEmptyStateMessage => 'Sāciet sarunu, lai izveidotu uzdevumu.'; + String get tasksEmptyStateMessage => 'Uzdevumi no jūsu sarunām parādīsies šeit.'; @override String get clearChatAction => 'Notīrīt tērzēšanu'; @@ -13069,154 +13068,206 @@ class AppLocalizationsLv extends AppLocalizations { } @override - String get paywallRecommended => 'Recommended'; + String get onboardingPermissionTapToFix => 'Pieskarieties, lai labotu'; @override - String get paywallNoPlans => 'Plans aren\'t available right now. Try again in a minute.'; + String get onboardingNoOmiYet => 'Vēl nav Omi?'; @override - String get paywallOtherPlans => 'See all plans'; + String get onboardingOmiOrdered => 'Esmu to pasūtījis'; @override - String get paywallComparePlans => 'See plans'; + String get onboardingOmiNotOrdered => 'Vēl neesmu pasūtījis'; @override - String get paywallYourPlan => 'Your plan'; + String get onboardingConnectOmi => 'Savienot Omi'; @override - String get paywallNotNow => 'Not now'; + String get consentShortMessage => 'Omi ar MI palīdzību pārvērš jūsu sarunas piezīmēs. Tās var izdzēst jebkurā laikā.'; @override - String get paywallTryAgain => 'Try again'; + String consentAiProviders(String providers) { + return 'Apstrādā $providers.'; + } @override - String get paywallManageBilling => 'Manage billing'; + String get onboardingActIntro => 'Izmēģināsim Omi.'; @override - String get paywallLoadError => 'Couldn\'t load plans. Check your connection and try again.'; + String get onboardingActMicTitle => 'Omi vajag jūsu mikrofonu'; @override - String get paywallPlans => 'Plans'; + String get onboardingActMicBody => 'Lai Omi jūs dzirdētu, kad ar to runājat.'; @override - String get firstCaptureTitle => 'How will you capture?'; + String get onboardingActMicAllow => 'Atļaut mikrofonu'; @override - String get firstCaptureDescription => 'Choose how you want to capture your first conversation.'; + String get onboardingActDictateTitle => 'Pastāstiet Omi par savu dienu'; @override - String get firstCaptureOmi => 'I Have an Omi'; + String get onboardingActDictateBody => 'Sakiet savā veidā. Pārējo paveiks Omi.'; @override - String get firstCapturePhone => 'This Phone'; + String get onboardingActDictateExample => + 'Tikko paēdu pusdienas ar Semu, jūnijā viņa pārceļas uz Lisabonu! Atgādini man līdz piektdienai nosūtīt prezentāciju komandai un pa ceļam mājās nopirkt pienu.'; @override - String get firstCaptureWatch => 'Apple Watch'; + String get onboardingActDictateHint => 'Kad esat pabeidzis, pieskarieties ķeksītim'; @override - String get firstCaptureSkip => 'Skip for Now'; + String get onboardingActPhoneMic => 'Tālruņa mikrofons'; @override - String get firstCapturePrompt => - 'Tell Omi what you\'re working on today, or what\'s on your mind. Talk for about 30 seconds.'; + String get onboardingActWriting => 'Apkopojam…'; @override - String get firstCaptureKeepGoing => 'Keep going… 30 seconds is ideal.'; + String get onboardingActRemembers => 'Omi atceras, lai jums nebūtu jāatceras'; @override - String get firstCaptureReady => 'You can finish now, or keep talking.'; + String get onboardingActNotesLabel => 'Jūsu diena īsumā'; @override - String get firstCaptureStart => 'Start Recording'; + String get onboardingActTasksLabel => 'Omi pamanīja jūsu darāmo'; @override - String get firstCaptureFinish => 'Finish Recording'; + String get onboardingActYourWords => 'Lūk, ko jūs teicāt'; @override - String get firstCaptureListening => 'Listening… your words will appear here.'; + String onboardingActCongratsTitle(String name) { + return 'Viss gatavs, $name'; + } @override - String get firstCaptureProcessing => 'Creating your first conversation…'; + String get onboardingActRateTitle => 'Vai Omi jums patīk?'; @override - String get firstCaptureResult => 'Your first conversation'; + String get onboardingActRateYes => 'Novērtēt Omi'; @override - String get firstCaptureEmpty => 'I didn\'t catch enough speech, try again with a few sentences'; + String get onboardingWelcomeCardTitle => 'Laipni lūgti Omi'; @override - String get watchCaptureTitle => 'Record from your Apple Watch'; + String get onboardingWelcomeCardBody => + 'Valkājiet Omi un runājiet kā parasti. Katra saruna šeit kļūst par piezīmēm, uzdevumiem un atmiņām.'; @override - String get watchCaptureOpen => 'Open Omi on your Apple Watch.'; + String get onboardingActDemoTitle => 'Pusdienas ar Semu'; @override - String get watchCaptureRecord => 'Tap Record. Allow microphone access if asked.'; + String get onboardingActDemoSummary => + 'Pusdienas ar Semu, kura jūnijā pārceļas uz Lisabonu. Līdz piektdienai jānosūta prezentācija komandai un mājupceļā jānopērk piens.'; @override - String get watchCaptureSync => 'Your recording syncs to Omi on this phone.'; + String get onboardingActDemoTask1 => 'Līdz piektdienai nosūtīt prezentāciju komandai'; @override - String get pairingBluetoothOff => 'Turn on Bluetooth on your phone, then try again.'; + String get onboardingActDemoTask2 => 'Pa ceļam mājās nopirkt pienu'; @override - String get pairingBluetoothPermission => 'Allow Bluetooth access for Omi in Settings, then try again.'; + String get onboardingActJustNow => 'Tikko'; @override - String get pairingNearbyPermission => 'Allow Nearby devices access for Omi in Settings, then try again.'; + String get onboardingActSavedToTasks => 'Saglabāts jūsu uzdevumos'; @override - String get pairingLocationPermission => - 'Allow location access for Omi in Settings so this phone can find your device, then try again.'; + String get onboardingConnectTapToConnect => 'Pieskarieties savam Omi, lai savienotu.'; @override - String get pairingNotFoundOrDropped => - 'Couldn\'t reach your Omi. Make sure it\'s charged, close to your phone, and its light is blinking, then try again.'; + String get onboardingActMicFallbackBody => 'Omi vēl nesūta skaņu. Pagaidām izmantojiet tālruni.'; @override - String get pairingBondRecovery => 'Forget Omi in your phone’s Bluetooth settings, then try pairing again.'; + String get onboardingActNoAudioOmi => 'Omi nav dzirdams. Pārliecinieties, ka tas ir ieslēgts un tuvumā.'; @override - String get pairingHowTo => 'How to pair'; + String get onboardingActNoAudioPhone => 'Vēl jūs nedzirdam. Mēģiniet runāt mazliet skaļāk.'; + + @override + String get onboardingActDictateBodyOmi => 'Vienkārši runājiet. Jūsu Omi klausās.'; + + @override + String get voiceProfileCardDismissed => 'Balss profilu varat iestatīt jebkurā laikā iestatījumos.'; + + @override + String get homeNewRecordingButton => 'Jauns ieraksts'; + + @override + String get chatWelcomeMessage => + 'Sveiki, es esmu Omi. Jautā man jebko par to, par ko esi runājis: ko kāds teica, ko tu apsolīji vai kopsavilkumu par tavu dienu. Es varu arī pievienot uzdevumus un atcerēties lietas tavā vietā.'; + + @override + String get onboardingStarterTaskAsk => 'Pajautā Omi par savu dienu'; + + @override + String get onboardingStarterTaskRecord => 'Ieraksti sarunu ar Omi'; + + @override + String get onboardingStarterTaskCheck => 'Atzīmē šo uzdevumu, lai to pabeigtu'; + + @override + String get paywallRecommended => 'Recommended'; @override - String get firstCaptureWatchInstructions => - 'Open Omi on your Apple Watch and tap Record. Allow microphone access if asked. Your capture will sync to Omi on this phone.'; + String get paywallNoPlans => 'Plans aren\'t available right now. Try again in a minute.'; @override - String get firstCaptureWatchInstall => - 'In the Watch app on your iPhone, install Omi on your Apple Watch. Then open Omi on your watch and tap Record.'; + String get paywallOtherPlans => 'See all plans'; @override - String get firstCaptureWatchPair => - 'Pair your Apple Watch with this iPhone using the Watch app, then install and open Omi on your watch.'; + String get paywallComparePlans => 'See plans'; @override - String get firstCaptureUnavailable => - 'Recording could not start. Check microphone access and your connection, then try again.'; + String get paywallYourPlan => 'Your plan'; @override - String get firstCaptureDelayed => - 'Your recording is taking longer to process. You can check your conversations on Home or try again.'; + String get paywallNotNow => 'Not now'; @override - String get firstCaptureEmptyTitle => 'Let\'s try that again'; + String get paywallTryAgain => 'Try again'; @override - String get firstCaptureShortTitle => 'Finish This Recording?'; + String get paywallManageBilling => 'Manage billing'; @override - String get firstCaptureShortWarning => 'A few more sentences help Omi understand you. About 30 seconds is ideal.'; + String get paywallLoadError => 'Couldn\'t load plans. Check your connection and try again.'; @override - String get firstCaptureCancelTitle => 'Leave This Recording?'; + String get paywallPlans => 'Plans'; @override - String get firstCaptureCancelWarning => - 'Going back stops this recording. Keep talking to give Omi a few more sentences.'; + String get watchCaptureTitle => 'Record from your Apple Watch'; @override - String get firstCaptureSiriTip => 'Tip: Enable Omi in Shortcuts → Siri.'; + String get watchCaptureOpen => 'Open Omi on your Apple Watch.'; + + @override + String get watchCaptureRecord => 'Tap Record. Allow microphone access if asked.'; + + @override + String get watchCaptureSync => 'Your recording syncs to Omi on this phone.'; + + @override + String get pairingBluetoothOff => 'Turn on Bluetooth on your phone, then try again.'; + + @override + String get pairingBluetoothPermission => 'Allow Bluetooth access for Omi in Settings, then try again.'; + + @override + String get pairingNearbyPermission => 'Allow Nearby devices access for Omi in Settings, then try again.'; + + @override + String get pairingLocationPermission => + 'Allow location access for Omi in Settings so this phone can find your device, then try again.'; + + @override + String get pairingNotFoundOrDropped => + 'Couldn\'t reach your Omi. Make sure it\'s charged, close to your phone, and its light is blinking, then try again.'; + + @override + String get pairingBondRecovery => 'Forget Omi in your phone’s Bluetooth settings, then try pairing again.'; + + @override + String get pairingHowTo => 'How to pair'; @override String get paywallOutlineHeadline => 'Nekad vairs nepalaidiet garām nevienu vārdu.'; diff --git a/app/lib/l10n/app_localizations_mk.dart b/app/lib/l10n/app_localizations_mk.dart index 04832902bb0..2e2a0d76e64 100644 --- a/app/lib/l10n/app_localizations_mk.dart +++ b/app/lib/l10n/app_localizations_mk.dart @@ -1690,8 +1690,7 @@ class AppLocalizationsMk extends AppLocalizations { String get enableBluetooth => 'Активирај Bluetooth'; @override - String get bluetoothNeeded => - 'Omi има потреба од Bluetooth за поврзување на вашето нослива опрема. Ве молиме активирајте Bluetooth и пробајте повторно.'; + String get bluetoothNeeded => 'Omi се поврзува со телефонот преку Bluetooth. Вклучете го за да продолжите.'; @override String get contactSupport => 'Контактирај поддршка?'; @@ -4916,7 +4915,7 @@ class AppLocalizationsMk extends AppLocalizations { 'Со продолжување, вашите разговори, снимки и лични информации ќе бидат безбедно зачувани на нашите сервери. Вашите аудио снимки и транскрипти се обработуваат од AI услуги на трети страни (вклучувајќи Deepgram за транскрипција и OpenAI за анализа) за да ви обезбедат увиди базирани на AI и да ги овозможат сите функции на апликацијата.'; @override - String get tasksEmptyStateMessage => 'Започнете разговор за да создадете задача.'; + String get tasksEmptyStateMessage => 'Задачите од вашите разговори ќе се појават тука.'; @override String get clearChatAction => 'Очисти разговор'; @@ -13101,154 +13100,207 @@ class AppLocalizationsMk extends AppLocalizations { } @override - String get paywallRecommended => 'Recommended'; + String get onboardingPermissionTapToFix => 'Допрете за да поправите'; @override - String get paywallNoPlans => 'Plans aren\'t available right now. Try again in a minute.'; + String get onboardingNoOmiYet => 'Сè уште немате Omi?'; @override - String get paywallOtherPlans => 'See all plans'; + String get onboardingOmiOrdered => 'Го нарачав'; @override - String get paywallComparePlans => 'See plans'; + String get onboardingOmiNotOrdered => 'Сè уште не сум нарачал'; @override - String get paywallYourPlan => 'Your plan'; + String get onboardingConnectOmi => 'Поврзете Omi'; @override - String get paywallNotNow => 'Not now'; + String get consentShortMessage => + 'Omi користи вештачка интелигенција за да ги претвори вашите разговори во белешки. Можете да ги избришете во секое време.'; @override - String get paywallTryAgain => 'Try again'; + String consentAiProviders(String providers) { + return 'Се обработува со $providers.'; + } @override - String get paywallManageBilling => 'Manage billing'; + String get onboardingActIntro => 'Да го пробаме Omi.'; @override - String get paywallLoadError => 'Couldn\'t load plans. Check your connection and try again.'; + String get onboardingActMicTitle => 'На Omi му треба микрофонот'; @override - String get paywallPlans => 'Plans'; + String get onboardingActMicBody => 'За Omi да ве слуша кога зборувате со него.'; @override - String get firstCaptureTitle => 'How will you capture?'; + String get onboardingActMicAllow => 'Дозволи микрофон'; @override - String get firstCaptureDescription => 'Choose how you want to capture your first conversation.'; + String get onboardingActDictateTitle => 'Кажете му на Omi за вашиот ден'; @override - String get firstCaptureOmi => 'I Have an Omi'; + String get onboardingActDictateBody => 'Кажете го на свој начин. Omi ќе се погрижи за останатото.'; @override - String get firstCapturePhone => 'This Phone'; + String get onboardingActDictateExample => + 'Штотуку ручав со Сем, се сели во Лисабон во јуни! Потсети ме да ја пратам презентацијата на тимот до петок и да купам млеко на пат кон дома.'; @override - String get firstCaptureWatch => 'Apple Watch'; + String get onboardingActDictateHint => 'Допрете ја штиклата кога ќе завршите'; @override - String get firstCaptureSkip => 'Skip for Now'; + String get onboardingActPhoneMic => 'Микрофон на телефонот'; @override - String get firstCapturePrompt => - 'Tell Omi what you\'re working on today, or what\'s on your mind. Talk for about 30 seconds.'; + String get onboardingActWriting => 'Сумираме…'; @override - String get firstCaptureKeepGoing => 'Keep going… 30 seconds is ideal.'; + String get onboardingActRemembers => 'Omi памети наместо вас'; @override - String get firstCaptureReady => 'You can finish now, or keep talking.'; + String get onboardingActNotesLabel => 'Вашиот ден, накратко'; @override - String get firstCaptureStart => 'Start Recording'; + String get onboardingActTasksLabel => 'Omi ги фати вашите задачи'; @override - String get firstCaptureFinish => 'Finish Recording'; + String get onboardingActYourWords => 'Еве што рековте'; @override - String get firstCaptureListening => 'Listening… your words will appear here.'; + String onboardingActCongratsTitle(String name) { + return 'Сè е подготвено, $name'; + } @override - String get firstCaptureProcessing => 'Creating your first conversation…'; + String get onboardingActRateTitle => 'Ви се допаѓа Omi досега?'; @override - String get firstCaptureResult => 'Your first conversation'; + String get onboardingActRateYes => 'Оценете го Omi'; @override - String get firstCaptureEmpty => 'I didn\'t catch enough speech, try again with a few sentences'; + String get onboardingWelcomeCardTitle => 'Добредојдовте во Omi'; @override - String get watchCaptureTitle => 'Record from your Apple Watch'; + String get onboardingWelcomeCardBody => + 'Носете го Omi и зборувајте како и обично. Секој разговор тука станува белешки, задачи и спомени.'; @override - String get watchCaptureOpen => 'Open Omi on your Apple Watch.'; + String get onboardingActDemoTitle => 'Ручек со Сем'; @override - String get watchCaptureRecord => 'Tap Record. Allow microphone access if asked.'; + String get onboardingActDemoSummary => + 'Ручек со Сем, која во јуни се сели во Лисабон. Сакате да ја испратите презентацијата до тимот до петок и да купите млеко на пат кон дома.'; @override - String get watchCaptureSync => 'Your recording syncs to Omi on this phone.'; + String get onboardingActDemoTask1 => 'Да ја пратам презентацијата на тимот до петок'; @override - String get pairingBluetoothOff => 'Turn on Bluetooth on your phone, then try again.'; + String get onboardingActDemoTask2 => 'Да купам млеко на пат кон дома'; @override - String get pairingBluetoothPermission => 'Allow Bluetooth access for Omi in Settings, then try again.'; + String get onboardingActJustNow => 'Токму сега'; @override - String get pairingNearbyPermission => 'Allow Nearby devices access for Omi in Settings, then try again.'; + String get onboardingActSavedToTasks => 'Зачувано во вашите задачи'; @override - String get pairingLocationPermission => - 'Allow location access for Omi in Settings so this phone can find your device, then try again.'; + String get onboardingConnectTapToConnect => 'Допрете го вашиот Omi за да се поврзете.'; @override - String get pairingNotFoundOrDropped => - 'Couldn\'t reach your Omi. Make sure it\'s charged, close to your phone, and its light is blinking, then try again.'; + String get onboardingActMicFallbackBody => 'Omi сè уште не испраќа звук. Засега користете го телефонот.'; @override - String get pairingBondRecovery => 'Forget Omi in your phone’s Bluetooth settings, then try pairing again.'; + String get onboardingActNoAudioOmi => 'Не се слуша Omi. Проверете дали е вклучен и блиску.'; @override - String get pairingHowTo => 'How to pair'; + String get onboardingActNoAudioPhone => 'Сè уште не ве слушаме. Обидете се да зборувате малку погласно.'; + + @override + String get onboardingActDictateBodyOmi => 'Само зборувајте. Вашиот Omi слуша.'; + + @override + String get voiceProfileCardDismissed => 'Гласовниот профил можете да го поставите во секое време во Поставки.'; + + @override + String get homeNewRecordingButton => 'Ново снимање'; + + @override + String get chatWelcomeMessage => + 'Здраво, јас сум Omi. Прашај ме било што за она за што си зборувал: што рекол некој, што си ветил или резиме на твојот ден. Можам и да додавам задачи и да паметам работи за тебе.'; + + @override + String get onboardingStarterTaskAsk => 'Прашај го Omi за твојот ден'; + + @override + String get onboardingStarterTaskRecord => 'Сними разговор со Omi'; + + @override + String get onboardingStarterTaskCheck => 'Штиклирај ја оваа задача за да ја завршиш'; + + @override + String get paywallRecommended => 'Recommended'; @override - String get firstCaptureWatchInstructions => - 'Open Omi on your Apple Watch and tap Record. Allow microphone access if asked. Your capture will sync to Omi on this phone.'; + String get paywallNoPlans => 'Plans aren\'t available right now. Try again in a minute.'; @override - String get firstCaptureWatchInstall => - 'In the Watch app on your iPhone, install Omi on your Apple Watch. Then open Omi on your watch and tap Record.'; + String get paywallOtherPlans => 'See all plans'; @override - String get firstCaptureWatchPair => - 'Pair your Apple Watch with this iPhone using the Watch app, then install and open Omi on your watch.'; + String get paywallComparePlans => 'See plans'; @override - String get firstCaptureUnavailable => - 'Recording could not start. Check microphone access and your connection, then try again.'; + String get paywallYourPlan => 'Your plan'; @override - String get firstCaptureDelayed => - 'Your recording is taking longer to process. You can check your conversations on Home or try again.'; + String get paywallNotNow => 'Not now'; @override - String get firstCaptureEmptyTitle => 'Let\'s try that again'; + String get paywallTryAgain => 'Try again'; @override - String get firstCaptureShortTitle => 'Finish This Recording?'; + String get paywallManageBilling => 'Manage billing'; @override - String get firstCaptureShortWarning => 'A few more sentences help Omi understand you. About 30 seconds is ideal.'; + String get paywallLoadError => 'Couldn\'t load plans. Check your connection and try again.'; @override - String get firstCaptureCancelTitle => 'Leave This Recording?'; + String get paywallPlans => 'Plans'; @override - String get firstCaptureCancelWarning => - 'Going back stops this recording. Keep talking to give Omi a few more sentences.'; + String get watchCaptureTitle => 'Record from your Apple Watch'; @override - String get firstCaptureSiriTip => 'Tip: Enable Omi in Shortcuts → Siri.'; + String get watchCaptureOpen => 'Open Omi on your Apple Watch.'; + + @override + String get watchCaptureRecord => 'Tap Record. Allow microphone access if asked.'; + + @override + String get watchCaptureSync => 'Your recording syncs to Omi on this phone.'; + + @override + String get pairingBluetoothOff => 'Turn on Bluetooth on your phone, then try again.'; + + @override + String get pairingBluetoothPermission => 'Allow Bluetooth access for Omi in Settings, then try again.'; + + @override + String get pairingNearbyPermission => 'Allow Nearby devices access for Omi in Settings, then try again.'; + + @override + String get pairingLocationPermission => + 'Allow location access for Omi in Settings so this phone can find your device, then try again.'; + + @override + String get pairingNotFoundOrDropped => + 'Couldn\'t reach your Omi. Make sure it\'s charged, close to your phone, and its light is blinking, then try again.'; + + @override + String get pairingBondRecovery => 'Forget Omi in your phone’s Bluetooth settings, then try pairing again.'; + + @override + String get pairingHowTo => 'How to pair'; @override String get paywallOutlineHeadline => 'Никогаш повеќе не пропуштајте ниту збор.'; diff --git a/app/lib/l10n/app_localizations_mr.dart b/app/lib/l10n/app_localizations_mr.dart index 5b3c930c1f8..839efa468c8 100644 --- a/app/lib/l10n/app_localizations_mr.dart +++ b/app/lib/l10n/app_localizations_mr.dart @@ -1687,8 +1687,7 @@ class AppLocalizationsMr extends AppLocalizations { String get enableBluetooth => 'Bluetooth सक्षम करा'; @override - String get bluetoothNeeded => - 'Omi आपल्या पहनने केल्या जाणार्‍या उपकरणाला जोडण्यासाठी Bluetooth आवश्यक आहे. कृपया Bluetooth सक्षम करा आणि पुन्हा प्रयत्न करा.'; + String get bluetoothNeeded => 'Omi तुमच्या फोनशी Bluetooth द्वारे जोडतो. पुढे जाण्यासाठी ते सुरू करा.'; @override String get contactSupport => 'समर्थन संपर्क करायचे?'; @@ -4903,7 +4902,7 @@ class AppLocalizationsMr extends AppLocalizations { 'पुढे चालू ठेवल्यास, तुमच्या संभाषणा, रेकॉर्डिंग आणि वैयक्तिक माहिती आमच्या सर्व्हरवर सुरक्षितपणे साठवली जाईल. तुमच्या ऑडिओ रेकॉर्डिंग आणि ट्रान्सक्रिप्ट तृतीय-पक्ष AI सेवांद्वारे प्रक्रिया केली जातात (ट्रान्सक्रिप्शनसाठी Deepgram आणि विश्लेषणासाठी OpenAI सह) तुम्हाला AI-चालित अंतर्दृष्टी प्रदान करण्यासाठी आणि सर्व अॅप वैशिष्ट्ये सक्षम करण्यासाठी.'; @override - String get tasksEmptyStateMessage => 'कार्य तयार करण्यासाठी संभाषण सुरू करा.'; + String get tasksEmptyStateMessage => 'तुमच्या संभाषणांमधील कामे इथे दिसतील.'; @override String get clearChatAction => 'चॅट साफ करा'; @@ -13046,154 +13045,206 @@ class AppLocalizationsMr extends AppLocalizations { } @override - String get paywallRecommended => 'Recommended'; + String get onboardingPermissionTapToFix => 'दुरुस्त करण्यासाठी टॅप करा'; @override - String get paywallNoPlans => 'Plans aren\'t available right now. Try again in a minute.'; + String get onboardingNoOmiYet => 'अजून तुमचा Omi आला नाही?'; @override - String get paywallOtherPlans => 'See all plans'; + String get onboardingOmiOrdered => 'मी ऑर्डर केला आहे'; @override - String get paywallComparePlans => 'See plans'; + String get onboardingOmiNotOrdered => 'अजून ऑर्डर केलेला नाही'; @override - String get paywallYourPlan => 'Your plan'; + String get onboardingConnectOmi => 'Omi कनेक्ट करा'; @override - String get paywallNotNow => 'Not now'; + String get consentShortMessage => 'Omi AI वापरून तुमच्या संभाषणांच्या नोट्स बनवते. तुम्ही त्या कधीही हटवू शकता.'; @override - String get paywallTryAgain => 'Try again'; + String consentAiProviders(String providers) { + return '$providers द्वारे प्रक्रिया केली जाते.'; + } @override - String get paywallManageBilling => 'Manage billing'; + String get onboardingActIntro => 'चला, Omi वापरून पाहू.'; @override - String get paywallLoadError => 'Couldn\'t load plans. Check your connection and try again.'; + String get onboardingActMicTitle => 'Omi ला तुमचा मायक्रोफोन हवा आहे'; @override - String get paywallPlans => 'Plans'; + String get onboardingActMicBody => 'तुम्ही बोलता तेव्हा Omi ला ऐकू यावे म्हणून.'; @override - String get firstCaptureTitle => 'How will you capture?'; + String get onboardingActMicAllow => 'मायक्रोफोनला परवानगी द्या'; @override - String get firstCaptureDescription => 'Choose how you want to capture your first conversation.'; + String get onboardingActDictateTitle => 'Omi ला तुमच्या दिवसाबद्दल सांगा'; @override - String get firstCaptureOmi => 'I Have an Omi'; + String get onboardingActDictateBody => 'तुमच्या पद्धतीने सांगा. बाकी Omi पाहील.'; @override - String get firstCapturePhone => 'This Phone'; + String get onboardingActDictateExample => + 'आत्ताच सॅमसोबत जेवलो, ती जूनमध्ये लिस्बनला जातेय! शुक्रवारपर्यंत टीमला डेक पाठवायची आणि घरी जाताना दूध घ्यायची आठवण करून दे.'; @override - String get firstCaptureWatch => 'Apple Watch'; + String get onboardingActDictateHint => 'झाले की टिकवर टॅप करा'; @override - String get firstCaptureSkip => 'Skip for Now'; + String get onboardingActPhoneMic => 'फोनचा मायक्रोफोन'; @override - String get firstCapturePrompt => - 'Tell Omi what you\'re working on today, or what\'s on your mind. Talk for about 30 seconds.'; + String get onboardingActWriting => 'सारांश तयार होत आहे…'; @override - String get firstCaptureKeepGoing => 'Keep going… 30 seconds is ideal.'; + String get onboardingActRemembers => 'तुम्हाला लक्षात ठेवावं लागू नये म्हणून Omi लक्षात ठेवतो'; @override - String get firstCaptureReady => 'You can finish now, or keep talking.'; + String get onboardingActNotesLabel => 'तुमचा दिवस, थोडक्यात'; @override - String get firstCaptureStart => 'Start Recording'; + String get onboardingActTasksLabel => 'Omi ने तुमची कामे टिपली'; @override - String get firstCaptureFinish => 'Finish Recording'; + String get onboardingActYourWords => 'तुम्ही हे सांगितले'; @override - String get firstCaptureListening => 'Listening… your words will appear here.'; + String onboardingActCongratsTitle(String name) { + return 'सगळे तयार आहे, $name'; + } @override - String get firstCaptureProcessing => 'Creating your first conversation…'; + String get onboardingActRateTitle => 'आत्तापर्यंत Omi आवडतोय का?'; @override - String get firstCaptureResult => 'Your first conversation'; + String get onboardingActRateYes => 'Omi ला रेटिंग द्या'; @override - String get firstCaptureEmpty => 'I didn\'t catch enough speech, try again with a few sentences'; + String get onboardingWelcomeCardTitle => 'Omi मध्ये स्वागत आहे'; @override - String get watchCaptureTitle => 'Record from your Apple Watch'; + String get onboardingWelcomeCardBody => + 'Omi घाला आणि नेहमीसारखे बोला. प्रत्येक संभाषण इथेच नोट्स, कामे आणि आठवणी बनते.'; @override - String get watchCaptureOpen => 'Open Omi on your Apple Watch.'; + String get onboardingActDemoTitle => 'सॅमसोबत जेवण'; @override - String get watchCaptureRecord => 'Tap Record. Allow microphone access if asked.'; + String get onboardingActDemoSummary => + 'जूनमध्ये लिस्बनला जाणाऱ्या सॅमसोबत जेवण. शुक्रवारपर्यंत टीमला डेक पाठवायचा आहे आणि घरी जाताना दूध घ्यायचे आहे.'; @override - String get watchCaptureSync => 'Your recording syncs to Omi on this phone.'; + String get onboardingActDemoTask1 => 'शुक्रवारपर्यंत टीमला डेक पाठवणे'; @override - String get pairingBluetoothOff => 'Turn on Bluetooth on your phone, then try again.'; + String get onboardingActDemoTask2 => 'घरी जाताना दूध घेणे'; @override - String get pairingBluetoothPermission => 'Allow Bluetooth access for Omi in Settings, then try again.'; + String get onboardingActJustNow => 'आत्ताच'; @override - String get pairingNearbyPermission => 'Allow Nearby devices access for Omi in Settings, then try again.'; + String get onboardingActSavedToTasks => 'तुमच्या कामांमध्ये जतन केले'; @override - String get pairingLocationPermission => - 'Allow location access for Omi in Settings so this phone can find your device, then try again.'; + String get onboardingConnectTapToConnect => 'कनेक्ट करण्यासाठी तुमच्या Omi वर टॅप करा.'; @override - String get pairingNotFoundOrDropped => - 'Couldn\'t reach your Omi. Make sure it\'s charged, close to your phone, and its light is blinking, then try again.'; + String get onboardingActMicFallbackBody => 'तुमचा Omi अजून ऑडिओ पाठवत नाही. सध्या तुमचा फोन वापरा.'; @override - String get pairingBondRecovery => 'Forget Omi in your phone’s Bluetooth settings, then try pairing again.'; + String get onboardingActNoAudioOmi => 'तुमचा Omi ऐकू येत नाही. तो सुरू आहे आणि जवळ आहे का ते पाहा.'; @override - String get pairingHowTo => 'How to pair'; + String get onboardingActNoAudioPhone => 'अजून तुमचा आवाज ऐकू येत नाही. थोडं मोठ्याने बोलून पाहा.'; + + @override + String get onboardingActDictateBodyOmi => 'फक्त बोला. तुमचा Omi ऐकत आहे.'; + + @override + String get voiceProfileCardDismissed => 'सेटिंग्जमध्ये कधीही व्हॉइस प्रोफाइल सेट करू शकता.'; + + @override + String get homeNewRecordingButton => 'नवीन रेकॉर्डिंग'; + + @override + String get chatWelcomeMessage => + 'नमस्कार, मी Omi आहे. तुम्ही ज्याबद्दल बोललात त्याबद्दल काहीही विचारा: कोणी काय सांगितले, तुम्ही काय वचन दिले किंवा तुमच्या दिवसाचा सारांश. मी कामे जोडू शकतो आणि तुमच्यासाठी गोष्टी लक्षातही ठेवू शकतो.'; + + @override + String get onboardingStarterTaskAsk => 'Omi ला तुमच्या दिवसाबद्दल विचारा'; + + @override + String get onboardingStarterTaskRecord => 'Omi ने एक संभाषण रेकॉर्ड करा'; + + @override + String get onboardingStarterTaskCheck => 'पूर्ण करण्यासाठी हे काम टिक करा'; + + @override + String get paywallRecommended => 'Recommended'; @override - String get firstCaptureWatchInstructions => - 'Open Omi on your Apple Watch and tap Record. Allow microphone access if asked. Your capture will sync to Omi on this phone.'; + String get paywallNoPlans => 'Plans aren\'t available right now. Try again in a minute.'; @override - String get firstCaptureWatchInstall => - 'In the Watch app on your iPhone, install Omi on your Apple Watch. Then open Omi on your watch and tap Record.'; + String get paywallOtherPlans => 'See all plans'; @override - String get firstCaptureWatchPair => - 'Pair your Apple Watch with this iPhone using the Watch app, then install and open Omi on your watch.'; + String get paywallComparePlans => 'See plans'; @override - String get firstCaptureUnavailable => - 'Recording could not start. Check microphone access and your connection, then try again.'; + String get paywallYourPlan => 'Your plan'; @override - String get firstCaptureDelayed => - 'Your recording is taking longer to process. You can check your conversations on Home or try again.'; + String get paywallNotNow => 'Not now'; @override - String get firstCaptureEmptyTitle => 'Let\'s try that again'; + String get paywallTryAgain => 'Try again'; @override - String get firstCaptureShortTitle => 'Finish This Recording?'; + String get paywallManageBilling => 'Manage billing'; @override - String get firstCaptureShortWarning => 'A few more sentences help Omi understand you. About 30 seconds is ideal.'; + String get paywallLoadError => 'Couldn\'t load plans. Check your connection and try again.'; @override - String get firstCaptureCancelTitle => 'Leave This Recording?'; + String get paywallPlans => 'Plans'; @override - String get firstCaptureCancelWarning => - 'Going back stops this recording. Keep talking to give Omi a few more sentences.'; + String get watchCaptureTitle => 'Record from your Apple Watch'; @override - String get firstCaptureSiriTip => 'Tip: Enable Omi in Shortcuts → Siri.'; + String get watchCaptureOpen => 'Open Omi on your Apple Watch.'; + + @override + String get watchCaptureRecord => 'Tap Record. Allow microphone access if asked.'; + + @override + String get watchCaptureSync => 'Your recording syncs to Omi on this phone.'; + + @override + String get pairingBluetoothOff => 'Turn on Bluetooth on your phone, then try again.'; + + @override + String get pairingBluetoothPermission => 'Allow Bluetooth access for Omi in Settings, then try again.'; + + @override + String get pairingNearbyPermission => 'Allow Nearby devices access for Omi in Settings, then try again.'; + + @override + String get pairingLocationPermission => + 'Allow location access for Omi in Settings so this phone can find your device, then try again.'; + + @override + String get pairingNotFoundOrDropped => + 'Couldn\'t reach your Omi. Make sure it\'s charged, close to your phone, and its light is blinking, then try again.'; + + @override + String get pairingBondRecovery => 'Forget Omi in your phone’s Bluetooth settings, then try pairing again.'; + + @override + String get pairingHowTo => 'How to pair'; @override String get paywallOutlineHeadline => 'आता एकही शब्द चुकवू नका.'; diff --git a/app/lib/l10n/app_localizations_ms.dart b/app/lib/l10n/app_localizations_ms.dart index dfcbf7ebf73..435fa5bac2f 100644 --- a/app/lib/l10n/app_localizations_ms.dart +++ b/app/lib/l10n/app_localizations_ms.dart @@ -1690,8 +1690,7 @@ class AppLocalizationsMs extends AppLocalizations { String get enableBluetooth => 'Dayakan Bluetooth'; @override - String get bluetoothNeeded => - 'Omi memerlukan Bluetooth untuk menyambung ke peranti boleh pakai anda. Sila dayakan Bluetooth dan cuba lagi.'; + String get bluetoothNeeded => 'Omi bersambung ke telefon anda melalui Bluetooth. Hidupkannya untuk meneruskan.'; @override String get contactSupport => 'Hubungi Sokongan?'; @@ -4908,7 +4907,7 @@ class AppLocalizationsMs extends AppLocalizations { 'Dengan meneruskan, perbualan, rakaman dan maklumat peribadi anda akan disimpan dengan selamat di pelayan kami. Rakaman audio dan transkrip anda diproses oleh perkhidmatan AI pihak ketiga (termasuk Deepgram untuk transkripsi dan OpenAI untuk analisis) untuk memberikan anda pandangan dikuasakan AI dan membolehkan semua ciri aplikasi.'; @override - String get tasksEmptyStateMessage => 'Mulakan perbualan untuk mencipta tugasan.'; + String get tasksEmptyStateMessage => 'Tugasan daripada perbualan anda akan dipaparkan di sini.'; @override String get clearChatAction => 'Kosongkan sembang'; @@ -13071,154 +13070,207 @@ class AppLocalizationsMs extends AppLocalizations { } @override - String get paywallRecommended => 'Recommended'; + String get onboardingPermissionTapToFix => 'Ketik untuk membetulkan'; @override - String get paywallNoPlans => 'Plans aren\'t available right now. Try again in a minute.'; + String get onboardingNoOmiYet => 'Belum ada Omi lagi?'; @override - String get paywallOtherPlans => 'See all plans'; + String get onboardingOmiOrdered => 'Saya sudah memesannya'; @override - String get paywallComparePlans => 'See plans'; + String get onboardingOmiNotOrdered => 'Belum memesan'; @override - String get paywallYourPlan => 'Your plan'; + String get onboardingConnectOmi => 'Sambungkan Omi'; @override - String get paywallNotNow => 'Not now'; + String get consentShortMessage => + 'Omi menggunakan AI untuk menukar perbualan anda kepada nota. Anda boleh memadamkannya bila-bila masa.'; @override - String get paywallTryAgain => 'Try again'; + String consentAiProviders(String providers) { + return 'Diproses dengan $providers.'; + } @override - String get paywallManageBilling => 'Manage billing'; + String get onboardingActIntro => 'Jom cuba Omi.'; @override - String get paywallLoadError => 'Couldn\'t load plans. Check your connection and try again.'; + String get onboardingActMicTitle => 'Omi perlukan mikrofon anda'; @override - String get paywallPlans => 'Plans'; + String get onboardingActMicBody => 'Supaya Omi dapat mendengar anda bercakap.'; @override - String get firstCaptureTitle => 'How will you capture?'; + String get onboardingActMicAllow => 'Benarkan Mikrofon'; @override - String get firstCaptureDescription => 'Choose how you want to capture your first conversation.'; + String get onboardingActDictateTitle => 'Ceritakan hari anda kepada Omi'; @override - String get firstCaptureOmi => 'I Have an Omi'; + String get onboardingActDictateBody => 'Cakap dengan cara anda. Omi uruskan selebihnya.'; @override - String get firstCapturePhone => 'This Phone'; + String get onboardingActDictateExample => + 'Baru makan tengah hari dengan Sam, dia akan pindah ke Lisbon bulan Jun! Ingatkan saya hantar deck kepada pasukan sebelum Jumaat, dan beli susu dalam perjalanan pulang.'; @override - String get firstCaptureWatch => 'Apple Watch'; + String get onboardingActDictateHint => 'Ketik tanda semak apabila selesai'; @override - String get firstCaptureSkip => 'Skip for Now'; + String get onboardingActPhoneMic => 'Mikrofon telefon'; @override - String get firstCapturePrompt => - 'Tell Omi what you\'re working on today, or what\'s on your mind. Talk for about 30 seconds.'; + String get onboardingActWriting => 'Meringkaskan…'; @override - String get firstCaptureKeepGoing => 'Keep going… 30 seconds is ideal.'; + String get onboardingActRemembers => 'Omi ingat supaya anda tidak perlu'; @override - String get firstCaptureReady => 'You can finish now, or keep talking.'; + String get onboardingActNotesLabel => 'Hari anda, diringkaskan'; @override - String get firstCaptureStart => 'Start Recording'; + String get onboardingActTasksLabel => 'Omi menangkap tugasan anda'; @override - String get firstCaptureFinish => 'Finish Recording'; + String get onboardingActYourWords => 'Ini yang anda katakan'; @override - String get firstCaptureListening => 'Listening… your words will appear here.'; + String onboardingActCongratsTitle(String name) { + return 'Semua sudah sedia, $name'; + } @override - String get firstCaptureProcessing => 'Creating your first conversation…'; + String get onboardingActRateTitle => 'Suka Omi setakat ini?'; @override - String get firstCaptureResult => 'Your first conversation'; + String get onboardingActRateYes => 'Nilai Omi'; @override - String get firstCaptureEmpty => 'I didn\'t catch enough speech, try again with a few sentences'; + String get onboardingWelcomeCardTitle => 'Selamat datang ke Omi'; @override - String get watchCaptureTitle => 'Record from your Apple Watch'; + String get onboardingWelcomeCardBody => + 'Pakai Omi dan bercakap seperti biasa. Setiap perbualan menjadi nota, tugasan dan kenangan di sini.'; @override - String get watchCaptureOpen => 'Open Omi on your Apple Watch.'; + String get onboardingActDemoTitle => 'Makan tengah hari dengan Sam'; @override - String get watchCaptureRecord => 'Tap Record. Allow microphone access if asked.'; + String get onboardingActDemoSummary => + 'Makan tengah hari dengan Sam, yang berpindah ke Lisbon pada Jun. Anda mahu hantar dek kepada pasukan sebelum Jumaat dan beli susu dalam perjalanan pulang.'; @override - String get watchCaptureSync => 'Your recording syncs to Omi on this phone.'; + String get onboardingActDemoTask1 => 'Hantar deck kepada pasukan sebelum Jumaat'; @override - String get pairingBluetoothOff => 'Turn on Bluetooth on your phone, then try again.'; + String get onboardingActDemoTask2 => 'Beli susu dalam perjalanan pulang'; @override - String get pairingBluetoothPermission => 'Allow Bluetooth access for Omi in Settings, then try again.'; + String get onboardingActJustNow => 'Baru sahaja'; @override - String get pairingNearbyPermission => 'Allow Nearby devices access for Omi in Settings, then try again.'; + String get onboardingActSavedToTasks => 'Disimpan ke tugasan anda'; @override - String get pairingLocationPermission => - 'Allow location access for Omi in Settings so this phone can find your device, then try again.'; + String get onboardingConnectTapToConnect => 'Ketik Omi anda untuk menyambung.'; @override - String get pairingNotFoundOrDropped => - 'Couldn\'t reach your Omi. Make sure it\'s charged, close to your phone, and its light is blinking, then try again.'; + String get onboardingActMicFallbackBody => 'Omi anda belum menghantar audio. Gunakan telefon anda buat masa ini.'; @override - String get pairingBondRecovery => 'Forget Omi in your phone’s Bluetooth settings, then try pairing again.'; + String get onboardingActNoAudioOmi => 'Omi anda tidak kedengaran. Pastikan ia dihidupkan dan dekat.'; @override - String get pairingHowTo => 'How to pair'; + String get onboardingActNoAudioPhone => 'Suara anda belum kedengaran. Cuba bercakap lebih kuat sedikit.'; + + @override + String get onboardingActDictateBodyOmi => 'Cakap sahaja. Omi anda sedang mendengar.'; + + @override + String get voiceProfileCardDismissed => 'Anda boleh menyediakan Profil Suara pada bila-bila masa dalam Tetapan.'; + + @override + String get homeNewRecordingButton => 'Rakaman Baharu'; + + @override + String get chatWelcomeMessage => + 'Hai, saya Omi. Tanya saya apa sahaja tentang perkara yang anda bualkan: apa yang dikatakan seseorang, apa yang anda janjikan atau ringkasan hari anda. Saya juga boleh menambah tugasan dan mengingati sesuatu untuk anda.'; + + @override + String get onboardingStarterTaskAsk => 'Tanya Omi tentang hari anda'; + + @override + String get onboardingStarterTaskRecord => 'Rakam perbualan dengan Omi'; + + @override + String get onboardingStarterTaskCheck => 'Tandakan tugasan ini untuk menyelesaikannya'; + + @override + String get paywallRecommended => 'Recommended'; @override - String get firstCaptureWatchInstructions => - 'Open Omi on your Apple Watch and tap Record. Allow microphone access if asked. Your capture will sync to Omi on this phone.'; + String get paywallNoPlans => 'Plans aren\'t available right now. Try again in a minute.'; @override - String get firstCaptureWatchInstall => - 'In the Watch app on your iPhone, install Omi on your Apple Watch. Then open Omi on your watch and tap Record.'; + String get paywallOtherPlans => 'See all plans'; @override - String get firstCaptureWatchPair => - 'Pair your Apple Watch with this iPhone using the Watch app, then install and open Omi on your watch.'; + String get paywallComparePlans => 'See plans'; @override - String get firstCaptureUnavailable => - 'Recording could not start. Check microphone access and your connection, then try again.'; + String get paywallYourPlan => 'Your plan'; @override - String get firstCaptureDelayed => - 'Your recording is taking longer to process. You can check your conversations on Home or try again.'; + String get paywallNotNow => 'Not now'; @override - String get firstCaptureEmptyTitle => 'Let\'s try that again'; + String get paywallTryAgain => 'Try again'; @override - String get firstCaptureShortTitle => 'Finish This Recording?'; + String get paywallManageBilling => 'Manage billing'; @override - String get firstCaptureShortWarning => 'A few more sentences help Omi understand you. About 30 seconds is ideal.'; + String get paywallLoadError => 'Couldn\'t load plans. Check your connection and try again.'; @override - String get firstCaptureCancelTitle => 'Leave This Recording?'; + String get paywallPlans => 'Plans'; @override - String get firstCaptureCancelWarning => - 'Going back stops this recording. Keep talking to give Omi a few more sentences.'; + String get watchCaptureTitle => 'Record from your Apple Watch'; @override - String get firstCaptureSiriTip => 'Tip: Enable Omi in Shortcuts → Siri.'; + String get watchCaptureOpen => 'Open Omi on your Apple Watch.'; + + @override + String get watchCaptureRecord => 'Tap Record. Allow microphone access if asked.'; + + @override + String get watchCaptureSync => 'Your recording syncs to Omi on this phone.'; + + @override + String get pairingBluetoothOff => 'Turn on Bluetooth on your phone, then try again.'; + + @override + String get pairingBluetoothPermission => 'Allow Bluetooth access for Omi in Settings, then try again.'; + + @override + String get pairingNearbyPermission => 'Allow Nearby devices access for Omi in Settings, then try again.'; + + @override + String get pairingLocationPermission => + 'Allow location access for Omi in Settings so this phone can find your device, then try again.'; + + @override + String get pairingNotFoundOrDropped => + 'Couldn\'t reach your Omi. Make sure it\'s charged, close to your phone, and its light is blinking, then try again.'; + + @override + String get pairingBondRecovery => 'Forget Omi in your phone’s Bluetooth settings, then try pairing again.'; + + @override + String get pairingHowTo => 'How to pair'; @override String get paywallOutlineHeadline => 'Jangan terlepas satu perkataan pun lagi.'; diff --git a/app/lib/l10n/app_localizations_nl.dart b/app/lib/l10n/app_localizations_nl.dart index ff490f8580b..809bfb62e2e 100644 --- a/app/lib/l10n/app_localizations_nl.dart +++ b/app/lib/l10n/app_localizations_nl.dart @@ -1688,8 +1688,7 @@ class AppLocalizationsNl extends AppLocalizations { String get enableBluetooth => 'Bluetooth inschakelen'; @override - String get bluetoothNeeded => - 'Omi heeft Bluetooth nodig om verbinding te maken met je wearable. Schakel Bluetooth in en probeer het opnieuw.'; + String get bluetoothNeeded => 'Omi maakt via Bluetooth verbinding met je telefoon. Zet het aan om door te gaan.'; @override String get contactSupport => 'Contact opnemen met support?'; @@ -4904,7 +4903,7 @@ class AppLocalizationsNl extends AppLocalizations { 'Door verder te gaan worden uw gesprekken, opnames en persoonlijke informatie veilig opgeslagen op onze servers. Uw audio-opnames en transcripties worden verwerkt door AI-diensten van derden (waaronder Deepgram voor transcriptie en OpenAI voor analyse) om u AI-gestuurde inzichten te bieden en alle app-functies mogelijk te maken.'; @override - String get tasksEmptyStateMessage => 'Begin een gesprek om een taak aan te maken.'; + String get tasksEmptyStateMessage => 'Taken uit je gesprekken verschijnen hier.'; @override String get clearChatAction => 'Chat wissen'; @@ -13076,154 +13075,206 @@ class AppLocalizationsNl extends AppLocalizations { } @override - String get paywallRecommended => 'Recommended'; + String get onboardingPermissionTapToFix => 'Tik om te herstellen'; @override - String get paywallNoPlans => 'Plans aren\'t available right now. Try again in a minute.'; + String get onboardingNoOmiYet => 'Heb je je Omi nog niet?'; @override - String get paywallOtherPlans => 'See all plans'; + String get onboardingOmiOrdered => 'Ik heb hem besteld'; @override - String get paywallComparePlans => 'See plans'; + String get onboardingOmiNotOrdered => 'Nog niet besteld'; @override - String get paywallYourPlan => 'Your plan'; + String get onboardingConnectOmi => 'Omi verbinden'; @override - String get paywallNotNow => 'Not now'; + String get consentShortMessage => 'Omi maakt met AI notities van je gesprekken. Je kunt ze altijd verwijderen.'; @override - String get paywallTryAgain => 'Try again'; + String consentAiProviders(String providers) { + return 'Verwerkt met $providers.'; + } @override - String get paywallManageBilling => 'Manage billing'; + String get onboardingActIntro => 'Probeer Omi eens.'; @override - String get paywallLoadError => 'Couldn\'t load plans. Check your connection and try again.'; + String get onboardingActMicTitle => 'Omi heeft je microfoon nodig'; @override - String get paywallPlans => 'Plans'; + String get onboardingActMicBody => 'Zodat Omi je hoort als je tegen hem praat.'; @override - String get firstCaptureTitle => 'How will you capture?'; + String get onboardingActMicAllow => 'Microfoon toestaan'; @override - String get firstCaptureDescription => 'Choose how you want to capture your first conversation.'; + String get onboardingActDictateTitle => 'Vertel Omi over je dag'; @override - String get firstCaptureOmi => 'I Have an Omi'; + String get onboardingActDictateBody => 'Zeg het op jouw manier. Omi doet de rest.'; @override - String get firstCapturePhone => 'This Phone'; + String get onboardingActDictateExample => + 'Net geluncht met Sam, ze verhuist in juni naar Lissabon! Herinner me eraan de presentatie vóór vrijdag naar het team te sturen en op weg naar huis melk te halen.'; @override - String get firstCaptureWatch => 'Apple Watch'; + String get onboardingActDictateHint => 'Tik op het vinkje als je klaar bent'; @override - String get firstCaptureSkip => 'Skip for Now'; + String get onboardingActPhoneMic => 'Telefoonmicrofoon'; @override - String get firstCapturePrompt => - 'Tell Omi what you\'re working on today, or what\'s on your mind. Talk for about 30 seconds.'; + String get onboardingActWriting => 'Samenvatten…'; @override - String get firstCaptureKeepGoing => 'Keep going… 30 seconds is ideal.'; + String get onboardingActRemembers => 'Omi onthoudt het, zodat jij dat niet hoeft'; @override - String get firstCaptureReady => 'You can finish now, or keep talking.'; + String get onboardingActNotesLabel => 'Je dag, samengevat'; @override - String get firstCaptureStart => 'Start Recording'; + String get onboardingActTasksLabel => 'Omi heeft je to-do\'s opgepikt'; @override - String get firstCaptureFinish => 'Finish Recording'; + String get onboardingActYourWords => 'Dit zei je'; @override - String get firstCaptureListening => 'Listening… your words will appear here.'; + String onboardingActCongratsTitle(String name) { + return 'Je bent klaar, $name'; + } @override - String get firstCaptureProcessing => 'Creating your first conversation…'; + String get onboardingActRateTitle => 'Bevalt Omi tot nu toe?'; @override - String get firstCaptureResult => 'Your first conversation'; + String get onboardingActRateYes => 'Omi beoordelen'; @override - String get firstCaptureEmpty => 'I didn\'t catch enough speech, try again with a few sentences'; + String get onboardingWelcomeCardTitle => 'Welkom bij Omi'; @override - String get watchCaptureTitle => 'Record from your Apple Watch'; + String get onboardingWelcomeCardBody => + 'Draag Omi en praat zoals altijd. Elk gesprek wordt hier notities, taken en herinneringen.'; @override - String get watchCaptureOpen => 'Open Omi on your Apple Watch.'; + String get onboardingActDemoTitle => 'Lunch met Sam'; @override - String get watchCaptureRecord => 'Tap Record. Allow microphone access if asked.'; + String get onboardingActDemoSummary => + 'Lunch met Sam, die in juni naar Lissabon verhuist. Je wilt de presentatie vóór vrijdag naar je team sturen en op weg naar huis melk halen.'; @override - String get watchCaptureSync => 'Your recording syncs to Omi on this phone.'; + String get onboardingActDemoTask1 => 'Presentatie vóór vrijdag naar het team sturen'; @override - String get pairingBluetoothOff => 'Turn on Bluetooth on your phone, then try again.'; + String get onboardingActDemoTask2 => 'Op weg naar huis melk halen'; @override - String get pairingBluetoothPermission => 'Allow Bluetooth access for Omi in Settings, then try again.'; + String get onboardingActJustNow => 'Zojuist'; @override - String get pairingNearbyPermission => 'Allow Nearby devices access for Omi in Settings, then try again.'; + String get onboardingActSavedToTasks => 'Opgeslagen in je taken'; @override - String get pairingLocationPermission => - 'Allow location access for Omi in Settings so this phone can find your device, then try again.'; + String get onboardingConnectTapToConnect => 'Tik op je Omi om te verbinden.'; @override - String get pairingNotFoundOrDropped => - 'Couldn\'t reach your Omi. Make sure it\'s charged, close to your phone, and its light is blinking, then try again.'; + String get onboardingActMicFallbackBody => 'Je Omi stuurt nog geen geluid. Gebruik voorlopig je telefoon.'; @override - String get pairingBondRecovery => 'Forget Omi in your phone’s Bluetooth settings, then try pairing again.'; + String get onboardingActNoAudioOmi => 'We horen je Omi niet. Controleer of hij aan staat en dichtbij is.'; @override - String get pairingHowTo => 'How to pair'; + String get onboardingActNoAudioPhone => 'We horen je nog niet. Probeer iets harder te praten.'; + + @override + String get onboardingActDictateBodyOmi => 'Praat gewoon. Je Omi luistert.'; + + @override + String get voiceProfileCardDismissed => 'Je kunt je stemprofiel altijd instellen via Instellingen.'; + + @override + String get homeNewRecordingButton => 'Nieuwe opname'; + + @override + String get chatWelcomeMessage => + 'Hoi, ik ben Omi. Vraag me alles over waar je het over hebt gehad: wat iemand zei, wat je beloofde of een samenvatting van je dag. Ik kan ook taken toevoegen en dingen voor je onthouden.'; + + @override + String get onboardingStarterTaskAsk => 'Vraag Omi naar je dag'; + + @override + String get onboardingStarterTaskRecord => 'Neem een gesprek op met Omi'; + + @override + String get onboardingStarterTaskCheck => 'Vink deze taak af om hem af te ronden'; + + @override + String get paywallRecommended => 'Recommended'; @override - String get firstCaptureWatchInstructions => - 'Open Omi on your Apple Watch and tap Record. Allow microphone access if asked. Your capture will sync to Omi on this phone.'; + String get paywallNoPlans => 'Plans aren\'t available right now. Try again in a minute.'; @override - String get firstCaptureWatchInstall => - 'In the Watch app on your iPhone, install Omi on your Apple Watch. Then open Omi on your watch and tap Record.'; + String get paywallOtherPlans => 'See all plans'; @override - String get firstCaptureWatchPair => - 'Pair your Apple Watch with this iPhone using the Watch app, then install and open Omi on your watch.'; + String get paywallComparePlans => 'See plans'; @override - String get firstCaptureUnavailable => - 'Recording could not start. Check microphone access and your connection, then try again.'; + String get paywallYourPlan => 'Your plan'; @override - String get firstCaptureDelayed => - 'Your recording is taking longer to process. You can check your conversations on Home or try again.'; + String get paywallNotNow => 'Not now'; @override - String get firstCaptureEmptyTitle => 'Let\'s try that again'; + String get paywallTryAgain => 'Try again'; @override - String get firstCaptureShortTitle => 'Finish This Recording?'; + String get paywallManageBilling => 'Manage billing'; @override - String get firstCaptureShortWarning => 'A few more sentences help Omi understand you. About 30 seconds is ideal.'; + String get paywallLoadError => 'Couldn\'t load plans. Check your connection and try again.'; @override - String get firstCaptureCancelTitle => 'Leave This Recording?'; + String get paywallPlans => 'Plans'; @override - String get firstCaptureCancelWarning => - 'Going back stops this recording. Keep talking to give Omi a few more sentences.'; + String get watchCaptureTitle => 'Record from your Apple Watch'; @override - String get firstCaptureSiriTip => 'Tip: Enable Omi in Shortcuts → Siri.'; + String get watchCaptureOpen => 'Open Omi on your Apple Watch.'; + + @override + String get watchCaptureRecord => 'Tap Record. Allow microphone access if asked.'; + + @override + String get watchCaptureSync => 'Your recording syncs to Omi on this phone.'; + + @override + String get pairingBluetoothOff => 'Turn on Bluetooth on your phone, then try again.'; + + @override + String get pairingBluetoothPermission => 'Allow Bluetooth access for Omi in Settings, then try again.'; + + @override + String get pairingNearbyPermission => 'Allow Nearby devices access for Omi in Settings, then try again.'; + + @override + String get pairingLocationPermission => + 'Allow location access for Omi in Settings so this phone can find your device, then try again.'; + + @override + String get pairingNotFoundOrDropped => + 'Couldn\'t reach your Omi. Make sure it\'s charged, close to your phone, and its light is blinking, then try again.'; + + @override + String get pairingBondRecovery => 'Forget Omi in your phone’s Bluetooth settings, then try pairing again.'; + + @override + String get pairingHowTo => 'How to pair'; @override String get paywallOutlineHeadline => 'Mis nooit meer een woord.'; diff --git a/app/lib/l10n/app_localizations_no.dart b/app/lib/l10n/app_localizations_no.dart index 9705066cfe3..59ac4bca6f3 100644 --- a/app/lib/l10n/app_localizations_no.dart +++ b/app/lib/l10n/app_localizations_no.dart @@ -1685,8 +1685,7 @@ class AppLocalizationsNo extends AppLocalizations { String get enableBluetooth => 'Aktiver Bluetooth'; @override - String get bluetoothNeeded => - 'Omi trenger Bluetooth for å koble til den bærbare enheten din. Aktiver Bluetooth og prøv igjen.'; + String get bluetoothNeeded => 'Omi kobles til telefonen via Bluetooth. Slå det på for å fortsette.'; @override String get contactSupport => 'Kontakte support?'; @@ -4894,7 +4893,7 @@ class AppLocalizationsNo extends AppLocalizations { 'Ved å fortsette vil samtalene, opptakene og den personlige informasjonen din bli lagret sikkert på våre servere. Lydopptakene og transkripsjonene dine behandles av tredjeparts AI-tjenester (inkludert Deepgram for transkripsjon og OpenAI for analyse) for å gi deg AI-drevne innsikter og aktivere alle appfunksjoner.'; @override - String get tasksEmptyStateMessage => 'Start en samtale for å opprette en oppgave.'; + String get tasksEmptyStateMessage => 'Gjøremål fra samtalene dine vises her.'; @override String get clearChatAction => 'Tøm chat'; @@ -13041,154 +13040,207 @@ class AppLocalizationsNo extends AppLocalizations { } @override - String get paywallRecommended => 'Recommended'; + String get onboardingPermissionTapToFix => 'Trykk for å fikse'; @override - String get paywallNoPlans => 'Plans aren\'t available right now. Try again in a minute.'; + String get onboardingNoOmiYet => 'Har du ikke fått Omi ennå?'; @override - String get paywallOtherPlans => 'See all plans'; + String get onboardingOmiOrdered => 'Jeg har bestilt den'; @override - String get paywallComparePlans => 'See plans'; + String get onboardingOmiNotOrdered => 'Ikke bestilt ennå'; @override - String get paywallYourPlan => 'Your plan'; + String get onboardingConnectOmi => 'Koble til Omi'; @override - String get paywallNotNow => 'Not now'; + String get consentShortMessage => + 'Omi bruker KI til å gjøre samtalene dine om til notater. Du kan slette dem når som helst.'; @override - String get paywallTryAgain => 'Try again'; + String consentAiProviders(String providers) { + return 'Behandles med $providers.'; + } @override - String get paywallManageBilling => 'Manage billing'; + String get onboardingActIntro => 'La oss prøve Omi.'; @override - String get paywallLoadError => 'Couldn\'t load plans. Check your connection and try again.'; + String get onboardingActMicTitle => 'Omi trenger mikrofonen din'; @override - String get paywallPlans => 'Plans'; + String get onboardingActMicBody => 'Så Omi hører deg når du snakker til den.'; @override - String get firstCaptureTitle => 'How will you capture?'; + String get onboardingActMicAllow => 'Tillat mikrofon'; @override - String get firstCaptureDescription => 'Choose how you want to capture your first conversation.'; + String get onboardingActDictateTitle => 'Fortell Omi om dagen din'; @override - String get firstCaptureOmi => 'I Have an Omi'; + String get onboardingActDictateBody => 'Si det på din måte. Omi tar seg av resten.'; @override - String get firstCapturePhone => 'This Phone'; + String get onboardingActDictateExample => + 'Spiste nettopp lunsj med Sam, hun flytter til Lisboa i juni! Minn meg på å sende presentasjonen til teamet innen fredag og kjøpe melk på vei hjem.'; @override - String get firstCaptureWatch => 'Apple Watch'; + String get onboardingActDictateHint => 'Trykk på haken når du er ferdig'; @override - String get firstCaptureSkip => 'Skip for Now'; + String get onboardingActPhoneMic => 'Telefonmikrofon'; @override - String get firstCapturePrompt => - 'Tell Omi what you\'re working on today, or what\'s on your mind. Talk for about 30 seconds.'; + String get onboardingActWriting => 'Oppsummerer…'; @override - String get firstCaptureKeepGoing => 'Keep going… 30 seconds is ideal.'; + String get onboardingActRemembers => 'Omi husker det, så du slipper'; @override - String get firstCaptureReady => 'You can finish now, or keep talking.'; + String get onboardingActNotesLabel => 'Dagen din, oppsummert'; @override - String get firstCaptureStart => 'Start Recording'; + String get onboardingActTasksLabel => 'Omi fanget opp gjøremålene dine'; @override - String get firstCaptureFinish => 'Finish Recording'; + String get onboardingActYourWords => 'Dette sa du'; @override - String get firstCaptureListening => 'Listening… your words will appear here.'; + String onboardingActCongratsTitle(String name) { + return 'Alt er klart, $name'; + } @override - String get firstCaptureProcessing => 'Creating your first conversation…'; + String get onboardingActRateTitle => 'Liker du Omi så langt?'; @override - String get firstCaptureResult => 'Your first conversation'; + String get onboardingActRateYes => 'Gi Omi en vurdering'; @override - String get firstCaptureEmpty => 'I didn\'t catch enough speech, try again with a few sentences'; + String get onboardingWelcomeCardTitle => 'Velkommen til Omi'; @override - String get watchCaptureTitle => 'Record from your Apple Watch'; + String get onboardingWelcomeCardBody => + 'Bruk Omi og snakk som vanlig. Hver samtale blir til notater, oppgaver og minner her.'; @override - String get watchCaptureOpen => 'Open Omi on your Apple Watch.'; + String get onboardingActDemoTitle => 'Lunsj med Sam'; @override - String get watchCaptureRecord => 'Tap Record. Allow microphone access if asked.'; + String get onboardingActDemoSummary => + 'Lunsj med Sam, som flytter til Lisboa i juni. Du vil sende presentasjonen til teamet innen fredag og kjøpe melk på vei hjem.'; @override - String get watchCaptureSync => 'Your recording syncs to Omi on this phone.'; + String get onboardingActDemoTask1 => 'Send presentasjonen til teamet innen fredag'; @override - String get pairingBluetoothOff => 'Turn on Bluetooth on your phone, then try again.'; + String get onboardingActDemoTask2 => 'Kjøp melk på vei hjem'; @override - String get pairingBluetoothPermission => 'Allow Bluetooth access for Omi in Settings, then try again.'; + String get onboardingActJustNow => 'Akkurat nå'; @override - String get pairingNearbyPermission => 'Allow Nearby devices access for Omi in Settings, then try again.'; + String get onboardingActSavedToTasks => 'Lagret i oppgavene dine'; @override - String get pairingLocationPermission => - 'Allow location access for Omi in Settings so this phone can find your device, then try again.'; + String get onboardingConnectTapToConnect => 'Trykk på Omi-en din for å koble til.'; @override - String get pairingNotFoundOrDropped => - 'Couldn\'t reach your Omi. Make sure it\'s charged, close to your phone, and its light is blinking, then try again.'; + String get onboardingActMicFallbackBody => 'Omi-en din sender ikke lyd ennå. Bruk telefonen inntil videre.'; @override - String get pairingBondRecovery => 'Forget Omi in your phone’s Bluetooth settings, then try pairing again.'; + String get onboardingActNoAudioOmi => 'Hører ikke Omi-en din. Sjekk at den er på og i nærheten.'; @override - String get pairingHowTo => 'How to pair'; + String get onboardingActNoAudioPhone => 'Hører deg ikke ennå. Prøv å snakke litt høyere.'; + + @override + String get onboardingActDictateBodyOmi => 'Bare snakk. Omi-en din lytter.'; + + @override + String get voiceProfileCardDismissed => 'Du kan sette opp stemmeprofilen når som helst i Innstillinger.'; + + @override + String get homeNewRecordingButton => 'Ny opptak'; + + @override + String get chatWelcomeMessage => + 'Hei, jeg er Omi. Spør meg om alt du har snakket om: hva noen sa, hva du lovet, eller et sammendrag av dagen din. Jeg kan også legge til oppgaver og huske ting for deg.'; + + @override + String get onboardingStarterTaskAsk => 'Spør Omi om dagen din'; + + @override + String get onboardingStarterTaskRecord => 'Ta opp en samtale med Omi'; + + @override + String get onboardingStarterTaskCheck => 'Kryss av denne oppgaven for å fullføre den'; + + @override + String get paywallRecommended => 'Recommended'; @override - String get firstCaptureWatchInstructions => - 'Open Omi on your Apple Watch and tap Record. Allow microphone access if asked. Your capture will sync to Omi on this phone.'; + String get paywallNoPlans => 'Plans aren\'t available right now. Try again in a minute.'; @override - String get firstCaptureWatchInstall => - 'In the Watch app on your iPhone, install Omi on your Apple Watch. Then open Omi on your watch and tap Record.'; + String get paywallOtherPlans => 'See all plans'; @override - String get firstCaptureWatchPair => - 'Pair your Apple Watch with this iPhone using the Watch app, then install and open Omi on your watch.'; + String get paywallComparePlans => 'See plans'; @override - String get firstCaptureUnavailable => - 'Recording could not start. Check microphone access and your connection, then try again.'; + String get paywallYourPlan => 'Your plan'; @override - String get firstCaptureDelayed => - 'Your recording is taking longer to process. You can check your conversations on Home or try again.'; + String get paywallNotNow => 'Not now'; @override - String get firstCaptureEmptyTitle => 'Let\'s try that again'; + String get paywallTryAgain => 'Try again'; @override - String get firstCaptureShortTitle => 'Finish This Recording?'; + String get paywallManageBilling => 'Manage billing'; @override - String get firstCaptureShortWarning => 'A few more sentences help Omi understand you. About 30 seconds is ideal.'; + String get paywallLoadError => 'Couldn\'t load plans. Check your connection and try again.'; @override - String get firstCaptureCancelTitle => 'Leave This Recording?'; + String get paywallPlans => 'Plans'; @override - String get firstCaptureCancelWarning => - 'Going back stops this recording. Keep talking to give Omi a few more sentences.'; + String get watchCaptureTitle => 'Record from your Apple Watch'; @override - String get firstCaptureSiriTip => 'Tip: Enable Omi in Shortcuts → Siri.'; + String get watchCaptureOpen => 'Open Omi on your Apple Watch.'; + + @override + String get watchCaptureRecord => 'Tap Record. Allow microphone access if asked.'; + + @override + String get watchCaptureSync => 'Your recording syncs to Omi on this phone.'; + + @override + String get pairingBluetoothOff => 'Turn on Bluetooth on your phone, then try again.'; + + @override + String get pairingBluetoothPermission => 'Allow Bluetooth access for Omi in Settings, then try again.'; + + @override + String get pairingNearbyPermission => 'Allow Nearby devices access for Omi in Settings, then try again.'; + + @override + String get pairingLocationPermission => + 'Allow location access for Omi in Settings so this phone can find your device, then try again.'; + + @override + String get pairingNotFoundOrDropped => + 'Couldn\'t reach your Omi. Make sure it\'s charged, close to your phone, and its light is blinking, then try again.'; + + @override + String get pairingBondRecovery => 'Forget Omi in your phone’s Bluetooth settings, then try pairing again.'; + + @override + String get pairingHowTo => 'How to pair'; @override String get paywallOutlineHeadline => 'Gå aldri glipp av et ord igjen.'; diff --git a/app/lib/l10n/app_localizations_pl.dart b/app/lib/l10n/app_localizations_pl.dart index 85e83ed6956..7205f5afffb 100644 --- a/app/lib/l10n/app_localizations_pl.dart +++ b/app/lib/l10n/app_localizations_pl.dart @@ -1687,8 +1687,7 @@ class AppLocalizationsPl extends AppLocalizations { String get enableBluetooth => 'Włącz Bluetooth'; @override - String get bluetoothNeeded => - 'Omi potrzebuje Bluetooth, aby połączyć się z Twoim urządzeniem noszonym. Włącz Bluetooth i spróbuj ponownie.'; + String get bluetoothNeeded => 'Omi łączy się z telefonem przez Bluetooth. Włącz go, aby kontynuować.'; @override String get contactSupport => 'Skontaktuj się z pomocą techniczną?'; @@ -4902,7 +4901,7 @@ class AppLocalizationsPl extends AppLocalizations { 'Kontynuując, Twoje rozmowy, nagrania i dane osobowe będą bezpiecznie przechowywane na naszych serwerach. Twoje nagrania audio i transkrypcje są przetwarzane przez zewnętrzne usługi AI (w tym Deepgram do transkrypcji i OpenAI do analizy), aby dostarczyć Ci wglądy oparte na AI i umożliwić wszystkie funkcje aplikacji.'; @override - String get tasksEmptyStateMessage => 'Rozpocznij rozmowę, aby utworzyć zadanie.'; + String get tasksEmptyStateMessage => 'Zadania z Twoich rozmów pojawią się tutaj.'; @override String get clearChatAction => 'Wyczyść czat'; @@ -13079,154 +13078,207 @@ class AppLocalizationsPl extends AppLocalizations { } @override - String get paywallRecommended => 'Recommended'; + String get onboardingPermissionTapToFix => 'Dotknij, aby naprawić'; @override - String get paywallNoPlans => 'Plans aren\'t available right now. Try again in a minute.'; + String get onboardingNoOmiYet => 'Nie masz jeszcze Omi?'; @override - String get paywallOtherPlans => 'See all plans'; + String get onboardingOmiOrdered => 'Już zamówiłem/am'; @override - String get paywallComparePlans => 'See plans'; + String get onboardingOmiNotOrdered => 'Jeszcze nie zamówiłem/am'; @override - String get paywallYourPlan => 'Your plan'; + String get onboardingConnectOmi => 'Połącz Omi'; @override - String get paywallNotNow => 'Not now'; + String get consentShortMessage => + 'Omi za pomocą AI zamienia Twoje rozmowy w notatki. Możesz je usunąć w każdej chwili.'; @override - String get paywallTryAgain => 'Try again'; + String consentAiProviders(String providers) { + return 'Przetwarzane przez $providers.'; + } @override - String get paywallManageBilling => 'Manage billing'; + String get onboardingActIntro => 'Wypróbujmy Omi.'; @override - String get paywallLoadError => 'Couldn\'t load plans. Check your connection and try again.'; + String get onboardingActMicTitle => 'Omi potrzebuje mikrofonu'; @override - String get paywallPlans => 'Plans'; + String get onboardingActMicBody => 'Aby Omi słyszał Cię, gdy do niego mówisz.'; @override - String get firstCaptureTitle => 'How will you capture?'; + String get onboardingActMicAllow => 'Zezwól na mikrofon'; @override - String get firstCaptureDescription => 'Choose how you want to capture your first conversation.'; + String get onboardingActDictateTitle => 'Opowiedz Omi o swoim dniu'; @override - String get firstCaptureOmi => 'I Have an Omi'; + String get onboardingActDictateBody => 'Powiedz to po swojemu. Resztą zajmie się Omi.'; @override - String get firstCapturePhone => 'This Phone'; + String get onboardingActDictateExample => + 'Właśnie zjadłem lunch z Sam, w czerwcu przeprowadza się do Lizbony! Przypomnij mi, żeby wysłać prezentację zespołowi do piątku i kupić mleko w drodze do domu.'; @override - String get firstCaptureWatch => 'Apple Watch'; + String get onboardingActDictateHint => 'Gdy skończysz, dotknij znacznika'; @override - String get firstCaptureSkip => 'Skip for Now'; + String get onboardingActPhoneMic => 'Mikrofon telefonu'; @override - String get firstCapturePrompt => - 'Tell Omi what you\'re working on today, or what\'s on your mind. Talk for about 30 seconds.'; + String get onboardingActWriting => 'Podsumowujemy…'; @override - String get firstCaptureKeepGoing => 'Keep going… 30 seconds is ideal.'; + String get onboardingActRemembers => 'Omi pamięta, żebyś Ty nie musiał'; @override - String get firstCaptureReady => 'You can finish now, or keep talking.'; + String get onboardingActNotesLabel => 'Twój dzień w skrócie'; @override - String get firstCaptureStart => 'Start Recording'; + String get onboardingActTasksLabel => 'Omi wyłapał Twoje zadania'; @override - String get firstCaptureFinish => 'Finish Recording'; + String get onboardingActYourWords => 'Oto, co powiedziałeś'; @override - String get firstCaptureListening => 'Listening… your words will appear here.'; + String onboardingActCongratsTitle(String name) { + return 'Wszystko gotowe, $name'; + } @override - String get firstCaptureProcessing => 'Creating your first conversation…'; + String get onboardingActRateTitle => 'Podoba Ci się Omi?'; @override - String get firstCaptureResult => 'Your first conversation'; + String get onboardingActRateYes => 'Oceń Omi'; @override - String get firstCaptureEmpty => 'I didn\'t catch enough speech, try again with a few sentences'; + String get onboardingWelcomeCardTitle => 'Witaj w Omi'; @override - String get watchCaptureTitle => 'Record from your Apple Watch'; + String get onboardingWelcomeCardBody => + 'Noś Omi i rozmawiaj jak zwykle. Każda rozmowa zamienia się tutaj w notatki, zadania i wspomnienia.'; @override - String get watchCaptureOpen => 'Open Omi on your Apple Watch.'; + String get onboardingActDemoTitle => 'Lunch z Sam'; @override - String get watchCaptureRecord => 'Tap Record. Allow microphone access if asked.'; + String get onboardingActDemoSummary => + 'Lunch z Sam, która w czerwcu przeprowadza się do Lizbony. Chcesz do piątku wysłać zespołowi prezentację i kupić mleko w drodze do domu.'; @override - String get watchCaptureSync => 'Your recording syncs to Omi on this phone.'; + String get onboardingActDemoTask1 => 'Wysłać prezentację zespołowi do piątku'; @override - String get pairingBluetoothOff => 'Turn on Bluetooth on your phone, then try again.'; + String get onboardingActDemoTask2 => 'Kupić mleko w drodze do domu'; @override - String get pairingBluetoothPermission => 'Allow Bluetooth access for Omi in Settings, then try again.'; + String get onboardingActJustNow => 'Przed chwilą'; @override - String get pairingNearbyPermission => 'Allow Nearby devices access for Omi in Settings, then try again.'; + String get onboardingActSavedToTasks => 'Zapisano w Twoich zadaniach'; @override - String get pairingLocationPermission => - 'Allow location access for Omi in Settings so this phone can find your device, then try again.'; + String get onboardingConnectTapToConnect => 'Stuknij swoje Omi, aby połączyć.'; @override - String get pairingNotFoundOrDropped => - 'Couldn\'t reach your Omi. Make sure it\'s charged, close to your phone, and its light is blinking, then try again.'; + String get onboardingActMicFallbackBody => 'Twoje Omi jeszcze nie przesyła dźwięku. Na razie użyj telefonu.'; @override - String get pairingBondRecovery => 'Forget Omi in your phone’s Bluetooth settings, then try pairing again.'; + String get onboardingActNoAudioOmi => 'Nie słychać Omi. Sprawdź, czy jest włączone i blisko.'; @override - String get pairingHowTo => 'How to pair'; + String get onboardingActNoAudioPhone => 'Jeszcze Cię nie słychać. Spróbuj mówić trochę głośniej.'; + + @override + String get onboardingActDictateBodyOmi => 'Po prostu mów. Twoje Omi słucha.'; + + @override + String get voiceProfileCardDismissed => 'Profil głosowy możesz skonfigurować w każdej chwili w Ustawieniach.'; + + @override + String get homeNewRecordingButton => 'Nowe nagranie'; + + @override + String get chatWelcomeMessage => + 'Cześć, jestem Omi. Pytaj mnie o wszystko, o czym rozmawiałeś: co ktoś powiedział, co obiecałeś albo jak minął ci dzień. Mogę też dodawać zadania i zapamiętywać rzeczy za ciebie.'; + + @override + String get onboardingStarterTaskAsk => 'Zapytaj Omi o swój dzień'; + + @override + String get onboardingStarterTaskRecord => 'Nagraj rozmowę z Omi'; + + @override + String get onboardingStarterTaskCheck => 'Odhacz to zadanie, aby je ukończyć'; + + @override + String get paywallRecommended => 'Recommended'; @override - String get firstCaptureWatchInstructions => - 'Open Omi on your Apple Watch and tap Record. Allow microphone access if asked. Your capture will sync to Omi on this phone.'; + String get paywallNoPlans => 'Plans aren\'t available right now. Try again in a minute.'; @override - String get firstCaptureWatchInstall => - 'In the Watch app on your iPhone, install Omi on your Apple Watch. Then open Omi on your watch and tap Record.'; + String get paywallOtherPlans => 'See all plans'; @override - String get firstCaptureWatchPair => - 'Pair your Apple Watch with this iPhone using the Watch app, then install and open Omi on your watch.'; + String get paywallComparePlans => 'See plans'; @override - String get firstCaptureUnavailable => - 'Recording could not start. Check microphone access and your connection, then try again.'; + String get paywallYourPlan => 'Your plan'; @override - String get firstCaptureDelayed => - 'Your recording is taking longer to process. You can check your conversations on Home or try again.'; + String get paywallNotNow => 'Not now'; @override - String get firstCaptureEmptyTitle => 'Let\'s try that again'; + String get paywallTryAgain => 'Try again'; @override - String get firstCaptureShortTitle => 'Finish This Recording?'; + String get paywallManageBilling => 'Manage billing'; @override - String get firstCaptureShortWarning => 'A few more sentences help Omi understand you. About 30 seconds is ideal.'; + String get paywallLoadError => 'Couldn\'t load plans. Check your connection and try again.'; @override - String get firstCaptureCancelTitle => 'Leave This Recording?'; + String get paywallPlans => 'Plans'; @override - String get firstCaptureCancelWarning => - 'Going back stops this recording. Keep talking to give Omi a few more sentences.'; + String get watchCaptureTitle => 'Record from your Apple Watch'; @override - String get firstCaptureSiriTip => 'Tip: Enable Omi in Shortcuts → Siri.'; + String get watchCaptureOpen => 'Open Omi on your Apple Watch.'; + + @override + String get watchCaptureRecord => 'Tap Record. Allow microphone access if asked.'; + + @override + String get watchCaptureSync => 'Your recording syncs to Omi on this phone.'; + + @override + String get pairingBluetoothOff => 'Turn on Bluetooth on your phone, then try again.'; + + @override + String get pairingBluetoothPermission => 'Allow Bluetooth access for Omi in Settings, then try again.'; + + @override + String get pairingNearbyPermission => 'Allow Nearby devices access for Omi in Settings, then try again.'; + + @override + String get pairingLocationPermission => + 'Allow location access for Omi in Settings so this phone can find your device, then try again.'; + + @override + String get pairingNotFoundOrDropped => + 'Couldn\'t reach your Omi. Make sure it\'s charged, close to your phone, and its light is blinking, then try again.'; + + @override + String get pairingBondRecovery => 'Forget Omi in your phone’s Bluetooth settings, then try pairing again.'; + + @override + String get pairingHowTo => 'How to pair'; @override String get paywallOutlineHeadline => 'Nigdy więcej nie przegap ani słowa.'; diff --git a/app/lib/l10n/app_localizations_pt.dart b/app/lib/l10n/app_localizations_pt.dart index 74e0acc0738..fbf19a2c71f 100644 --- a/app/lib/l10n/app_localizations_pt.dart +++ b/app/lib/l10n/app_localizations_pt.dart @@ -1676,7 +1676,7 @@ class AppLocalizationsPt extends AppLocalizations { String get enableBluetooth => 'Ativar Bluetooth'; @override - String get bluetoothNeeded => 'Omi precisa de Bluetooth para conectar seu wearable.'; + String get bluetoothNeeded => 'O Omi se conecta ao seu telefone por Bluetooth. Ative-o para continuar.'; @override String get contactSupport => 'Contatar suporte?'; @@ -4889,7 +4889,7 @@ class AppLocalizationsPt extends AppLocalizations { 'Ao continuar, suas conversas, gravações e informações pessoais serão armazenadas com segurança em nossos servidores. Suas gravações de áudio e transcrições são processadas por serviços de IA de terceiros (incluindo Deepgram para transcrição e OpenAI para análise) para fornecer insights baseados em IA e habilitar todos os recursos do aplicativo.'; @override - String get tasksEmptyStateMessage => 'Inicie uma conversa para criar uma tarefa.'; + String get tasksEmptyStateMessage => 'As tarefas das suas conversas aparecerão aqui.'; @override String get clearChatAction => 'Limpar conversa'; @@ -13063,154 +13063,207 @@ class AppLocalizationsPt extends AppLocalizations { } @override - String get paywallRecommended => 'Recommended'; + String get onboardingPermissionTapToFix => 'Toque para corrigir'; @override - String get paywallNoPlans => 'Plans aren\'t available right now. Try again in a minute.'; + String get onboardingNoOmiYet => 'Ainda não tem o seu Omi?'; @override - String get paywallOtherPlans => 'See all plans'; + String get onboardingOmiOrdered => 'Já encomendei'; @override - String get paywallComparePlans => 'See plans'; + String get onboardingOmiNotOrdered => 'Ainda não encomendei'; @override - String get paywallYourPlan => 'Your plan'; + String get onboardingConnectOmi => 'Conectar Omi'; @override - String get paywallNotNow => 'Not now'; + String get consentShortMessage => + 'O Omi usa IA para transformar suas conversas em notas. Você pode excluí-las quando quiser.'; @override - String get paywallTryAgain => 'Try again'; + String consentAiProviders(String providers) { + return 'Processado com $providers.'; + } @override - String get paywallManageBilling => 'Manage billing'; + String get onboardingActIntro => 'Vamos testar o Omi.'; @override - String get paywallLoadError => 'Couldn\'t load plans. Check your connection and try again.'; + String get onboardingActMicTitle => 'O Omi precisa do seu microfone'; @override - String get paywallPlans => 'Plans'; + String get onboardingActMicBody => 'Para o Omi ouvir quando você falar com ele.'; @override - String get firstCaptureTitle => 'How will you capture?'; + String get onboardingActMicAllow => 'Permitir microfone'; @override - String get firstCaptureDescription => 'Choose how you want to capture your first conversation.'; + String get onboardingActDictateTitle => 'Conte ao Omi sobre o seu dia'; @override - String get firstCaptureOmi => 'I Have an Omi'; + String get onboardingActDictateBody => 'Fale do seu jeito. O Omi cuida do resto.'; @override - String get firstCapturePhone => 'This Phone'; + String get onboardingActDictateExample => + 'Acabei de almoçar com a Sam, ela vai se mudar para Lisboa em junho! Me lembra de mandar a apresentação para o time até sexta e de comprar leite na volta para casa.'; @override - String get firstCaptureWatch => 'Apple Watch'; + String get onboardingActDictateHint => 'Toque no check quando terminar'; @override - String get firstCaptureSkip => 'Skip for Now'; + String get onboardingActPhoneMic => 'Microfone do celular'; @override - String get firstCapturePrompt => - 'Tell Omi what you\'re working on today, or what\'s on your mind. Talk for about 30 seconds.'; + String get onboardingActWriting => 'Resumindo…'; @override - String get firstCaptureKeepGoing => 'Keep going… 30 seconds is ideal.'; + String get onboardingActRemembers => 'O Omi lembra por você'; @override - String get firstCaptureReady => 'You can finish now, or keep talking.'; + String get onboardingActNotesLabel => 'Seu dia, resumido'; @override - String get firstCaptureStart => 'Start Recording'; + String get onboardingActTasksLabel => 'O Omi pegou suas tarefas'; @override - String get firstCaptureFinish => 'Finish Recording'; + String get onboardingActYourWords => 'Veja o que você disse'; @override - String get firstCaptureListening => 'Listening… your words will appear here.'; + String onboardingActCongratsTitle(String name) { + return 'Tudo pronto, $name'; + } @override - String get firstCaptureProcessing => 'Creating your first conversation…'; + String get onboardingActRateTitle => 'Está gostando do Omi?'; @override - String get firstCaptureResult => 'Your first conversation'; + String get onboardingActRateYes => 'Avaliar o Omi'; @override - String get firstCaptureEmpty => 'I didn\'t catch enough speech, try again with a few sentences'; + String get onboardingWelcomeCardTitle => 'Boas-vindas ao Omi'; @override - String get watchCaptureTitle => 'Record from your Apple Watch'; + String get onboardingWelcomeCardBody => + 'Use o Omi e converse normalmente. Cada conversa vira notas, tarefas e memórias bem aqui.'; @override - String get watchCaptureOpen => 'Open Omi on your Apple Watch.'; + String get onboardingActDemoTitle => 'Almoço com a Sam'; @override - String get watchCaptureRecord => 'Tap Record. Allow microphone access if asked.'; + String get onboardingActDemoSummary => + 'Almoço com o Sam, que vai se mudar para Lisboa em junho. Você quer enviar a apresentação para a equipe até sexta e comprar leite no caminho de casa.'; @override - String get watchCaptureSync => 'Your recording syncs to Omi on this phone.'; + String get onboardingActDemoTask1 => 'Mandar a apresentação para o time até sexta'; @override - String get pairingBluetoothOff => 'Turn on Bluetooth on your phone, then try again.'; + String get onboardingActDemoTask2 => 'Comprar leite na volta para casa'; @override - String get pairingBluetoothPermission => 'Allow Bluetooth access for Omi in Settings, then try again.'; + String get onboardingActJustNow => 'Agora mesmo'; @override - String get pairingNearbyPermission => 'Allow Nearby devices access for Omi in Settings, then try again.'; + String get onboardingActSavedToTasks => 'Salvo nas suas tarefas'; @override - String get pairingLocationPermission => - 'Allow location access for Omi in Settings so this phone can find your device, then try again.'; + String get onboardingConnectTapToConnect => 'Toque no seu Omi para conectar.'; @override - String get pairingNotFoundOrDropped => - 'Couldn\'t reach your Omi. Make sure it\'s charged, close to your phone, and its light is blinking, then try again.'; + String get onboardingActMicFallbackBody => 'Seu Omi ainda não está enviando áudio. Use o celular por enquanto.'; @override - String get pairingBondRecovery => 'Forget Omi in your phone’s Bluetooth settings, then try pairing again.'; + String get onboardingActNoAudioOmi => 'Não estamos ouvindo seu Omi. Veja se ele está ligado e por perto.'; @override - String get pairingHowTo => 'How to pair'; + String get onboardingActNoAudioPhone => 'Ainda não estamos te ouvindo. Tente falar um pouco mais alto.'; + + @override + String get onboardingActDictateBodyOmi => 'É só falar. Seu Omi está ouvindo.'; + + @override + String get voiceProfileCardDismissed => 'Você pode configurar o perfil de voz a qualquer momento em Ajustes.'; + + @override + String get homeNewRecordingButton => 'Nova gravação'; + + @override + String get chatWelcomeMessage => + 'Oi, eu sou o Omi. Pergunte qualquer coisa sobre o que você conversou: o que alguém disse, o que você prometeu ou um resumo do seu dia. Também posso adicionar tarefas e lembrar coisas para você.'; + + @override + String get onboardingStarterTaskAsk => 'Pergunte ao Omi sobre o seu dia'; + + @override + String get onboardingStarterTaskRecord => 'Grave uma conversa com o Omi'; + + @override + String get onboardingStarterTaskCheck => 'Marque esta tarefa para concluí-la'; + + @override + String get paywallRecommended => 'Recommended'; @override - String get firstCaptureWatchInstructions => - 'Open Omi on your Apple Watch and tap Record. Allow microphone access if asked. Your capture will sync to Omi on this phone.'; + String get paywallNoPlans => 'Plans aren\'t available right now. Try again in a minute.'; @override - String get firstCaptureWatchInstall => - 'In the Watch app on your iPhone, install Omi on your Apple Watch. Then open Omi on your watch and tap Record.'; + String get paywallOtherPlans => 'See all plans'; @override - String get firstCaptureWatchPair => - 'Pair your Apple Watch with this iPhone using the Watch app, then install and open Omi on your watch.'; + String get paywallComparePlans => 'See plans'; @override - String get firstCaptureUnavailable => - 'Recording could not start. Check microphone access and your connection, then try again.'; + String get paywallYourPlan => 'Your plan'; @override - String get firstCaptureDelayed => - 'Your recording is taking longer to process. You can check your conversations on Home or try again.'; + String get paywallNotNow => 'Not now'; @override - String get firstCaptureEmptyTitle => 'Let\'s try that again'; + String get paywallTryAgain => 'Try again'; @override - String get firstCaptureShortTitle => 'Finish This Recording?'; + String get paywallManageBilling => 'Manage billing'; @override - String get firstCaptureShortWarning => 'A few more sentences help Omi understand you. About 30 seconds is ideal.'; + String get paywallLoadError => 'Couldn\'t load plans. Check your connection and try again.'; @override - String get firstCaptureCancelTitle => 'Leave This Recording?'; + String get paywallPlans => 'Plans'; @override - String get firstCaptureCancelWarning => - 'Going back stops this recording. Keep talking to give Omi a few more sentences.'; + String get watchCaptureTitle => 'Record from your Apple Watch'; @override - String get firstCaptureSiriTip => 'Tip: Enable Omi in Shortcuts → Siri.'; + String get watchCaptureOpen => 'Open Omi on your Apple Watch.'; + + @override + String get watchCaptureRecord => 'Tap Record. Allow microphone access if asked.'; + + @override + String get watchCaptureSync => 'Your recording syncs to Omi on this phone.'; + + @override + String get pairingBluetoothOff => 'Turn on Bluetooth on your phone, then try again.'; + + @override + String get pairingBluetoothPermission => 'Allow Bluetooth access for Omi in Settings, then try again.'; + + @override + String get pairingNearbyPermission => 'Allow Nearby devices access for Omi in Settings, then try again.'; + + @override + String get pairingLocationPermission => + 'Allow location access for Omi in Settings so this phone can find your device, then try again.'; + + @override + String get pairingNotFoundOrDropped => + 'Couldn\'t reach your Omi. Make sure it\'s charged, close to your phone, and its light is blinking, then try again.'; + + @override + String get pairingBondRecovery => 'Forget Omi in your phone’s Bluetooth settings, then try pairing again.'; + + @override + String get pairingHowTo => 'How to pair'; @override String get paywallOutlineHeadline => 'Nunca mais perca uma palavra.'; diff --git a/app/lib/l10n/app_localizations_ro.dart b/app/lib/l10n/app_localizations_ro.dart index 73e65c646bf..f8ba232f6f9 100644 --- a/app/lib/l10n/app_localizations_ro.dart +++ b/app/lib/l10n/app_localizations_ro.dart @@ -1694,8 +1694,7 @@ class AppLocalizationsRo extends AppLocalizations { String get enableBluetooth => 'Activare Bluetooth'; @override - String get bluetoothNeeded => - 'Omi are nevoie de Bluetooth pentru a se conecta la dispozitivul dvs. Activați Bluetooth și încercați din nou.'; + String get bluetoothNeeded => 'Omi se conectează la telefon prin Bluetooth. Activează-l pentru a continua.'; @override String get contactSupport => 'Contactează suportul?'; @@ -4917,7 +4916,7 @@ class AppLocalizationsRo extends AppLocalizations { 'Continuând, conversațiile, înregistrările și informațiile dvs. personale vor fi stocate în siguranță pe serverele noastre. Înregistrările audio și transcrierile dvs. sunt procesate de servicii AI terțe (inclusiv Deepgram pentru transcriere și OpenAI pentru analiză) pentru a vă oferi informații bazate pe AI și a activa toate funcțiile aplicației.'; @override - String get tasksEmptyStateMessage => 'Începe o conversație pentru a crea o sarcină.'; + String get tasksEmptyStateMessage => 'Sarcinile din conversațiile tale vor apărea aici.'; @override String get clearChatAction => 'Șterge conversația'; @@ -13095,154 +13094,207 @@ class AppLocalizationsRo extends AppLocalizations { } @override - String get paywallRecommended => 'Recommended'; + String get onboardingPermissionTapToFix => 'Atinge pentru a remedia'; @override - String get paywallNoPlans => 'Plans aren\'t available right now. Try again in a minute.'; + String get onboardingNoOmiYet => 'Nu ai încă Omi?'; @override - String get paywallOtherPlans => 'See all plans'; + String get onboardingOmiOrdered => 'L-am comandat'; @override - String get paywallComparePlans => 'See plans'; + String get onboardingOmiNotOrdered => 'Încă nu am comandat'; @override - String get paywallYourPlan => 'Your plan'; + String get onboardingConnectOmi => 'Conectează Omi'; @override - String get paywallNotNow => 'Not now'; + String get consentShortMessage => + 'Omi folosește AI pentru a transforma conversațiile tale în notițe. Le poți șterge oricând.'; @override - String get paywallTryAgain => 'Try again'; + String consentAiProviders(String providers) { + return 'Procesat cu $providers.'; + } @override - String get paywallManageBilling => 'Manage billing'; + String get onboardingActIntro => 'Hai să încercăm Omi.'; @override - String get paywallLoadError => 'Couldn\'t load plans. Check your connection and try again.'; + String get onboardingActMicTitle => 'Omi are nevoie de microfon'; @override - String get paywallPlans => 'Plans'; + String get onboardingActMicBody => 'Ca Omi să te audă când vorbești cu el.'; @override - String get firstCaptureTitle => 'How will you capture?'; + String get onboardingActMicAllow => 'Permite microfonul'; @override - String get firstCaptureDescription => 'Choose how you want to capture your first conversation.'; + String get onboardingActDictateTitle => 'Povestește-i lui Omi despre ziua ta'; @override - String get firstCaptureOmi => 'I Have an Omi'; + String get onboardingActDictateBody => 'Spune-o în felul tău. Omi se ocupă de rest.'; @override - String get firstCapturePhone => 'This Phone'; + String get onboardingActDictateExample => + 'Tocmai am luat prânzul cu Sam, se mută la Lisabona în iunie! Amintește-mi să trimit prezentarea echipei până vineri și să iau lapte în drum spre casă.'; @override - String get firstCaptureWatch => 'Apple Watch'; + String get onboardingActDictateHint => 'Atinge bifa când ai terminat'; @override - String get firstCaptureSkip => 'Skip for Now'; + String get onboardingActPhoneMic => 'Microfonul telefonului'; @override - String get firstCapturePrompt => - 'Tell Omi what you\'re working on today, or what\'s on your mind. Talk for about 30 seconds.'; + String get onboardingActWriting => 'Rezumăm…'; @override - String get firstCaptureKeepGoing => 'Keep going… 30 seconds is ideal.'; + String get onboardingActRemembers => 'Omi își amintește, ca să nu fie nevoie să o faci tu'; @override - String get firstCaptureReady => 'You can finish now, or keep talking.'; + String get onboardingActNotesLabel => 'Ziua ta, pe scurt'; @override - String get firstCaptureStart => 'Start Recording'; + String get onboardingActTasksLabel => 'Omi ți-a prins sarcinile'; @override - String get firstCaptureFinish => 'Finish Recording'; + String get onboardingActYourWords => 'Iată ce ai spus'; @override - String get firstCaptureListening => 'Listening… your words will appear here.'; + String onboardingActCongratsTitle(String name) { + return 'Totul e gata, $name'; + } @override - String get firstCaptureProcessing => 'Creating your first conversation…'; + String get onboardingActRateTitle => 'Îți place Omi până acum?'; @override - String get firstCaptureResult => 'Your first conversation'; + String get onboardingActRateYes => 'Evaluează Omi'; @override - String get firstCaptureEmpty => 'I didn\'t catch enough speech, try again with a few sentences'; + String get onboardingWelcomeCardTitle => 'Bun venit în Omi'; @override - String get watchCaptureTitle => 'Record from your Apple Watch'; + String get onboardingWelcomeCardBody => + 'Poartă Omi și vorbește ca de obicei. Fiecare conversație devine aici notițe, sarcini și amintiri.'; @override - String get watchCaptureOpen => 'Open Omi on your Apple Watch.'; + String get onboardingActDemoTitle => 'Prânz cu Sam'; @override - String get watchCaptureRecord => 'Tap Record. Allow microphone access if asked.'; + String get onboardingActDemoSummary => + 'Prânz cu Sam, care se mută la Lisabona în iunie. Vrei să trimiți prezentarea echipei până vineri și să iei lapte în drum spre casă.'; @override - String get watchCaptureSync => 'Your recording syncs to Omi on this phone.'; + String get onboardingActDemoTask1 => 'Trimite prezentarea echipei până vineri'; @override - String get pairingBluetoothOff => 'Turn on Bluetooth on your phone, then try again.'; + String get onboardingActDemoTask2 => 'Ia lapte în drum spre casă'; @override - String get pairingBluetoothPermission => 'Allow Bluetooth access for Omi in Settings, then try again.'; + String get onboardingActJustNow => 'Chiar acum'; @override - String get pairingNearbyPermission => 'Allow Nearby devices access for Omi in Settings, then try again.'; + String get onboardingActSavedToTasks => 'Salvat în sarcinile tale'; @override - String get pairingLocationPermission => - 'Allow location access for Omi in Settings so this phone can find your device, then try again.'; + String get onboardingConnectTapToConnect => 'Atinge Omi-ul tău pentru a te conecta.'; @override - String get pairingNotFoundOrDropped => - 'Couldn\'t reach your Omi. Make sure it\'s charged, close to your phone, and its light is blinking, then try again.'; + String get onboardingActMicFallbackBody => 'Omi nu trimite încă sunet. Deocamdată folosește telefonul.'; @override - String get pairingBondRecovery => 'Forget Omi in your phone’s Bluetooth settings, then try pairing again.'; + String get onboardingActNoAudioOmi => 'Nu se aude Omi. Verifică dacă e pornit și aproape.'; @override - String get pairingHowTo => 'How to pair'; + String get onboardingActNoAudioPhone => 'Încă nu te auzim. Încearcă să vorbești puțin mai tare.'; + + @override + String get onboardingActDictateBodyOmi => 'Doar vorbește. Omi te ascultă.'; + + @override + String get voiceProfileCardDismissed => 'Poți configura profilul vocal oricând din Setări.'; + + @override + String get homeNewRecordingButton => 'Înregistrare nouă'; + + @override + String get chatWelcomeMessage => + 'Salut, sunt Omi. Întreabă-mă orice despre ce ai vorbit: ce a spus cineva, ce ai promis sau un rezumat al zilei tale. Pot și să adaug sarcini și să țin minte lucruri pentru tine.'; + + @override + String get onboardingStarterTaskAsk => 'Întreabă-l pe Omi despre ziua ta'; + + @override + String get onboardingStarterTaskRecord => 'Înregistrează o conversație cu Omi'; + + @override + String get onboardingStarterTaskCheck => 'Bifează această sarcină ca s-o finalizezi'; + + @override + String get paywallRecommended => 'Recommended'; @override - String get firstCaptureWatchInstructions => - 'Open Omi on your Apple Watch and tap Record. Allow microphone access if asked. Your capture will sync to Omi on this phone.'; + String get paywallNoPlans => 'Plans aren\'t available right now. Try again in a minute.'; @override - String get firstCaptureWatchInstall => - 'In the Watch app on your iPhone, install Omi on your Apple Watch. Then open Omi on your watch and tap Record.'; + String get paywallOtherPlans => 'See all plans'; @override - String get firstCaptureWatchPair => - 'Pair your Apple Watch with this iPhone using the Watch app, then install and open Omi on your watch.'; + String get paywallComparePlans => 'See plans'; @override - String get firstCaptureUnavailable => - 'Recording could not start. Check microphone access and your connection, then try again.'; + String get paywallYourPlan => 'Your plan'; @override - String get firstCaptureDelayed => - 'Your recording is taking longer to process. You can check your conversations on Home or try again.'; + String get paywallNotNow => 'Not now'; @override - String get firstCaptureEmptyTitle => 'Let\'s try that again'; + String get paywallTryAgain => 'Try again'; @override - String get firstCaptureShortTitle => 'Finish This Recording?'; + String get paywallManageBilling => 'Manage billing'; @override - String get firstCaptureShortWarning => 'A few more sentences help Omi understand you. About 30 seconds is ideal.'; + String get paywallLoadError => 'Couldn\'t load plans. Check your connection and try again.'; @override - String get firstCaptureCancelTitle => 'Leave This Recording?'; + String get paywallPlans => 'Plans'; @override - String get firstCaptureCancelWarning => - 'Going back stops this recording. Keep talking to give Omi a few more sentences.'; + String get watchCaptureTitle => 'Record from your Apple Watch'; @override - String get firstCaptureSiriTip => 'Tip: Enable Omi in Shortcuts → Siri.'; + String get watchCaptureOpen => 'Open Omi on your Apple Watch.'; + + @override + String get watchCaptureRecord => 'Tap Record. Allow microphone access if asked.'; + + @override + String get watchCaptureSync => 'Your recording syncs to Omi on this phone.'; + + @override + String get pairingBluetoothOff => 'Turn on Bluetooth on your phone, then try again.'; + + @override + String get pairingBluetoothPermission => 'Allow Bluetooth access for Omi in Settings, then try again.'; + + @override + String get pairingNearbyPermission => 'Allow Nearby devices access for Omi in Settings, then try again.'; + + @override + String get pairingLocationPermission => + 'Allow location access for Omi in Settings so this phone can find your device, then try again.'; + + @override + String get pairingNotFoundOrDropped => + 'Couldn\'t reach your Omi. Make sure it\'s charged, close to your phone, and its light is blinking, then try again.'; + + @override + String get pairingBondRecovery => 'Forget Omi in your phone’s Bluetooth settings, then try pairing again.'; + + @override + String get pairingHowTo => 'How to pair'; @override String get paywallOutlineHeadline => 'Nu mai rata niciun cuvânt.'; diff --git a/app/lib/l10n/app_localizations_ru.dart b/app/lib/l10n/app_localizations_ru.dart index e93fa590e03..e2cb62998b9 100644 --- a/app/lib/l10n/app_localizations_ru.dart +++ b/app/lib/l10n/app_localizations_ru.dart @@ -1691,8 +1691,7 @@ class AppLocalizationsRu extends AppLocalizations { String get enableBluetooth => 'Включите Bluetooth'; @override - String get bluetoothNeeded => - 'Omi требуется Bluetooth для подключения к вашему устройству. Пожалуйста, включите Bluetooth и попробуйте снова.'; + String get bluetoothNeeded => 'Omi подключается к телефону по Bluetooth. Включите его, чтобы продолжить.'; @override String get contactSupport => 'Связаться с поддержкой?'; @@ -4905,7 +4904,7 @@ class AppLocalizationsRu extends AppLocalizations { 'Продолжая, ваши разговоры, записи и личная информация будут надежно храниться на наших серверах. Ваши аудиозаписи и транскрипции обрабатываются сторонними AI-сервисами (включая Deepgram для транскрипции и OpenAI для анализа), чтобы предоставить вам аналитику на основе ИИ и обеспечить работу всех функций приложения.'; @override - String get tasksEmptyStateMessage => 'Начните разговор, чтобы создать задачу.'; + String get tasksEmptyStateMessage => 'Задачи из ваших разговоров появятся здесь.'; @override String get clearChatAction => 'Очистить чат'; @@ -13086,154 +13085,207 @@ class AppLocalizationsRu extends AppLocalizations { } @override - String get paywallRecommended => 'Recommended'; + String get onboardingPermissionTapToFix => 'Нажмите, чтобы исправить'; @override - String get paywallNoPlans => 'Plans aren\'t available right now. Try again in a minute.'; + String get onboardingNoOmiYet => 'Ещё нет Omi?'; @override - String get paywallOtherPlans => 'See all plans'; + String get onboardingOmiOrdered => 'Я уже заказал'; @override - String get paywallComparePlans => 'See plans'; + String get onboardingOmiNotOrdered => 'Ещё не заказал'; @override - String get paywallYourPlan => 'Your plan'; + String get onboardingConnectOmi => 'Подключите Omi'; @override - String get paywallNotNow => 'Not now'; + String get consentShortMessage => + 'Omi с помощью ИИ превращает ваши разговоры в заметки. Их можно удалить в любой момент.'; @override - String get paywallTryAgain => 'Try again'; + String consentAiProviders(String providers) { + return 'Обработка: $providers.'; + } @override - String get paywallManageBilling => 'Manage billing'; + String get onboardingActIntro => 'Попробуем Omi.'; @override - String get paywallLoadError => 'Couldn\'t load plans. Check your connection and try again.'; + String get onboardingActMicTitle => 'Omi нужен микрофон'; @override - String get paywallPlans => 'Plans'; + String get onboardingActMicBody => 'Чтобы Omi слышал вас, когда вы с ним говорите.'; @override - String get firstCaptureTitle => 'How will you capture?'; + String get onboardingActMicAllow => 'Разрешить микрофон'; @override - String get firstCaptureDescription => 'Choose how you want to capture your first conversation.'; + String get onboardingActDictateTitle => 'Расскажите Omi о своём дне'; @override - String get firstCaptureOmi => 'I Have an Omi'; + String get onboardingActDictateBody => 'Говорите как вам удобно. Остальное сделает Omi.'; @override - String get firstCapturePhone => 'This Phone'; + String get onboardingActDictateExample => + 'Только что пообедал с Сэм, в июне она переезжает в Лиссабон! Напомни отправить презентацию команде до пятницы и купить молоко по дороге домой.'; @override - String get firstCaptureWatch => 'Apple Watch'; + String get onboardingActDictateHint => 'Нажмите галочку, когда закончите'; @override - String get firstCaptureSkip => 'Skip for Now'; + String get onboardingActPhoneMic => 'Микрофон телефона'; @override - String get firstCapturePrompt => - 'Tell Omi what you\'re working on today, or what\'s on your mind. Talk for about 30 seconds.'; + String get onboardingActWriting => 'Подводим итоги…'; @override - String get firstCaptureKeepGoing => 'Keep going… 30 seconds is ideal.'; + String get onboardingActRemembers => 'Omi запоминает, чтобы вам не пришлось'; @override - String get firstCaptureReady => 'You can finish now, or keep talking.'; + String get onboardingActNotesLabel => 'Ваш день вкратце'; @override - String get firstCaptureStart => 'Start Recording'; + String get onboardingActTasksLabel => 'Omi нашёл ваши дела'; @override - String get firstCaptureFinish => 'Finish Recording'; + String get onboardingActYourWords => 'Вот что вы сказали'; @override - String get firstCaptureListening => 'Listening… your words will appear here.'; + String onboardingActCongratsTitle(String name) { + return 'Всё готово, $name'; + } @override - String get firstCaptureProcessing => 'Creating your first conversation…'; + String get onboardingActRateTitle => 'Нравится Omi?'; @override - String get firstCaptureResult => 'Your first conversation'; + String get onboardingActRateYes => 'Оценить Omi'; @override - String get firstCaptureEmpty => 'I didn\'t catch enough speech, try again with a few sentences'; + String get onboardingWelcomeCardTitle => 'Добро пожаловать в Omi'; @override - String get watchCaptureTitle => 'Record from your Apple Watch'; + String get onboardingWelcomeCardBody => + 'Носите Omi и говорите как обычно. Каждый разговор превратится здесь в заметки, задачи и воспоминания.'; @override - String get watchCaptureOpen => 'Open Omi on your Apple Watch.'; + String get onboardingActDemoTitle => 'Обед с Сэм'; @override - String get watchCaptureRecord => 'Tap Record. Allow microphone access if asked.'; + String get onboardingActDemoSummary => + 'Обед с Сэм, которая в июне переезжает в Лиссабон. Нужно отправить презентацию команде до пятницы и купить молоко по дороге домой.'; @override - String get watchCaptureSync => 'Your recording syncs to Omi on this phone.'; + String get onboardingActDemoTask1 => 'Отправить презентацию команде до пятницы'; @override - String get pairingBluetoothOff => 'Turn on Bluetooth on your phone, then try again.'; + String get onboardingActDemoTask2 => 'Купить молоко по дороге домой'; @override - String get pairingBluetoothPermission => 'Allow Bluetooth access for Omi in Settings, then try again.'; + String get onboardingActJustNow => 'Только что'; @override - String get pairingNearbyPermission => 'Allow Nearby devices access for Omi in Settings, then try again.'; + String get onboardingActSavedToTasks => 'Сохранено в ваши задачи'; @override - String get pairingLocationPermission => - 'Allow location access for Omi in Settings so this phone can find your device, then try again.'; + String get onboardingConnectTapToConnect => 'Нажмите на Omi, чтобы подключить.'; @override - String get pairingNotFoundOrDropped => - 'Couldn\'t reach your Omi. Make sure it\'s charged, close to your phone, and its light is blinking, then try again.'; + String get onboardingActMicFallbackBody => 'Omi пока не передаёт звук. Пока используйте телефон.'; @override - String get pairingBondRecovery => 'Forget Omi in your phone’s Bluetooth settings, then try pairing again.'; + String get onboardingActNoAudioOmi => 'Omi не слышно. Проверьте, что он включён и рядом.'; @override - String get pairingHowTo => 'How to pair'; + String get onboardingActNoAudioPhone => 'Пока вас не слышно. Попробуйте говорить чуть громче.'; + + @override + String get onboardingActDictateBodyOmi => 'Просто говорите. Omi слушает.'; + + @override + String get voiceProfileCardDismissed => 'Голосовой профиль можно настроить в любое время в Настройках.'; + + @override + String get homeNewRecordingButton => 'Новая запись'; + + @override + String get chatWelcomeMessage => + 'Привет, я Omi. Спрашивайте обо всём, о чём вы говорили: что сказал кто-то, что вы пообещали или как прошёл ваш день. Я также могу добавлять задачи и запоминать важное за вас.'; + + @override + String get onboardingStarterTaskAsk => 'Спросите Omi о своём дне'; + + @override + String get onboardingStarterTaskRecord => 'Запишите разговор с Omi'; + + @override + String get onboardingStarterTaskCheck => 'Отметьте эту задачу, чтобы завершить её'; + + @override + String get paywallRecommended => 'Recommended'; @override - String get firstCaptureWatchInstructions => - 'Open Omi on your Apple Watch and tap Record. Allow microphone access if asked. Your capture will sync to Omi on this phone.'; + String get paywallNoPlans => 'Plans aren\'t available right now. Try again in a minute.'; @override - String get firstCaptureWatchInstall => - 'In the Watch app on your iPhone, install Omi on your Apple Watch. Then open Omi on your watch and tap Record.'; + String get paywallOtherPlans => 'See all plans'; @override - String get firstCaptureWatchPair => - 'Pair your Apple Watch with this iPhone using the Watch app, then install and open Omi on your watch.'; + String get paywallComparePlans => 'See plans'; @override - String get firstCaptureUnavailable => - 'Recording could not start. Check microphone access and your connection, then try again.'; + String get paywallYourPlan => 'Your plan'; @override - String get firstCaptureDelayed => - 'Your recording is taking longer to process. You can check your conversations on Home or try again.'; + String get paywallNotNow => 'Not now'; @override - String get firstCaptureEmptyTitle => 'Let\'s try that again'; + String get paywallTryAgain => 'Try again'; @override - String get firstCaptureShortTitle => 'Finish This Recording?'; + String get paywallManageBilling => 'Manage billing'; @override - String get firstCaptureShortWarning => 'A few more sentences help Omi understand you. About 30 seconds is ideal.'; + String get paywallLoadError => 'Couldn\'t load plans. Check your connection and try again.'; @override - String get firstCaptureCancelTitle => 'Leave This Recording?'; + String get paywallPlans => 'Plans'; @override - String get firstCaptureCancelWarning => - 'Going back stops this recording. Keep talking to give Omi a few more sentences.'; + String get watchCaptureTitle => 'Record from your Apple Watch'; @override - String get firstCaptureSiriTip => 'Tip: Enable Omi in Shortcuts → Siri.'; + String get watchCaptureOpen => 'Open Omi on your Apple Watch.'; + + @override + String get watchCaptureRecord => 'Tap Record. Allow microphone access if asked.'; + + @override + String get watchCaptureSync => 'Your recording syncs to Omi on this phone.'; + + @override + String get pairingBluetoothOff => 'Turn on Bluetooth on your phone, then try again.'; + + @override + String get pairingBluetoothPermission => 'Allow Bluetooth access for Omi in Settings, then try again.'; + + @override + String get pairingNearbyPermission => 'Allow Nearby devices access for Omi in Settings, then try again.'; + + @override + String get pairingLocationPermission => + 'Allow location access for Omi in Settings so this phone can find your device, then try again.'; + + @override + String get pairingNotFoundOrDropped => + 'Couldn\'t reach your Omi. Make sure it\'s charged, close to your phone, and its light is blinking, then try again.'; + + @override + String get pairingBondRecovery => 'Forget Omi in your phone’s Bluetooth settings, then try pairing again.'; + + @override + String get pairingHowTo => 'How to pair'; @override String get paywallOutlineHeadline => 'Больше не упускайте ни слова.'; diff --git a/app/lib/l10n/app_localizations_sk.dart b/app/lib/l10n/app_localizations_sk.dart index d0c6eacf41b..86408e61178 100644 --- a/app/lib/l10n/app_localizations_sk.dart +++ b/app/lib/l10n/app_localizations_sk.dart @@ -1690,8 +1690,7 @@ class AppLocalizationsSk extends AppLocalizations { String get enableBluetooth => 'Povoliť Bluetooth'; @override - String get bluetoothNeeded => - 'Omi potrebuje Bluetooth na pripojenie k vášmu nositeľnému zariadeniu. Povoľte prosím Bluetooth a skúste to znova.'; + String get bluetoothNeeded => 'Omi sa k telefónu pripája cez Bluetooth. Zapnite ho a pokračujte.'; @override String get contactSupport => 'Kontaktovať podporu?'; @@ -4889,7 +4888,7 @@ class AppLocalizationsSk extends AppLocalizations { 'Pokračovaním budú vaše konverzácie, nahrávky a osobné údaje bezpečne uložené na našich serveroch. Vaše audio nahrávky a prepisy sú spracovávané AI službami tretích strán (vrátane Deepgram na prepis a OpenAI na analýzu), aby vám poskytli poznatky založené na AI a umožnili všetky funkcie aplikácie.'; @override - String get tasksEmptyStateMessage => 'Začnite konverzáciu a vytvorte úlohu.'; + String get tasksEmptyStateMessage => 'Úlohy z vašich rozhovorov sa zobrazia tu.'; @override String get clearChatAction => 'Vymazať chat'; @@ -13036,154 +13035,206 @@ class AppLocalizationsSk extends AppLocalizations { } @override - String get paywallRecommended => 'Recommended'; + String get onboardingPermissionTapToFix => 'Ťuknutím opravíte'; @override - String get paywallNoPlans => 'Plans aren\'t available right now. Try again in a minute.'; + String get onboardingNoOmiYet => 'Ešte nemáte Omi?'; @override - String get paywallOtherPlans => 'See all plans'; + String get onboardingOmiOrdered => 'Už som ho objednal/a'; @override - String get paywallComparePlans => 'See plans'; + String get onboardingOmiNotOrdered => 'Ešte neobjednané'; @override - String get paywallYourPlan => 'Your plan'; + String get onboardingConnectOmi => 'Pripojte Omi'; @override - String get paywallNotNow => 'Not now'; + String get consentShortMessage => 'Omi pomocou AI mení vaše konverzácie na poznámky. Kedykoľvek ich môžete vymazať.'; @override - String get paywallTryAgain => 'Try again'; + String consentAiProviders(String providers) { + return 'Spracúva $providers.'; + } @override - String get paywallManageBilling => 'Manage billing'; + String get onboardingActIntro => 'Vyskúšajme Omi.'; @override - String get paywallLoadError => 'Couldn\'t load plans. Check your connection and try again.'; + String get onboardingActMicTitle => 'Omi potrebuje mikrofón'; @override - String get paywallPlans => 'Plans'; + String get onboardingActMicBody => 'Aby vás Omi počul, keď naň hovoríte.'; @override - String get firstCaptureTitle => 'How will you capture?'; + String get onboardingActMicAllow => 'Povoliť mikrofón'; @override - String get firstCaptureDescription => 'Choose how you want to capture your first conversation.'; + String get onboardingActDictateTitle => 'Povedzte Omi o svojom dni'; @override - String get firstCaptureOmi => 'I Have an Omi'; + String get onboardingActDictateBody => 'Povedzte to po svojom. Zvyšok zariadi Omi.'; @override - String get firstCapturePhone => 'This Phone'; + String get onboardingActDictateExample => + 'Práve som obedoval so Sam, v júni sa sťahuje do Lisabonu! Pripomeň mi poslať prezentáciu tímu do piatka a cestou domov kúpiť mlieko.'; @override - String get firstCaptureWatch => 'Apple Watch'; + String get onboardingActDictateHint => 'Keď skončíte, ťuknite na fajku'; @override - String get firstCaptureSkip => 'Skip for Now'; + String get onboardingActPhoneMic => 'Mikrofón telefónu'; @override - String get firstCapturePrompt => - 'Tell Omi what you\'re working on today, or what\'s on your mind. Talk for about 30 seconds.'; + String get onboardingActWriting => 'Zhrnujeme…'; @override - String get firstCaptureKeepGoing => 'Keep going… 30 seconds is ideal.'; + String get onboardingActRemembers => 'Omi si to pamätá, aby ste nemuseli vy'; @override - String get firstCaptureReady => 'You can finish now, or keep talking.'; + String get onboardingActNotesLabel => 'Váš deň v skratke'; @override - String get firstCaptureStart => 'Start Recording'; + String get onboardingActTasksLabel => 'Omi zachytil vaše úlohy'; @override - String get firstCaptureFinish => 'Finish Recording'; + String get onboardingActYourWords => 'Toto ste povedali'; @override - String get firstCaptureListening => 'Listening… your words will appear here.'; + String onboardingActCongratsTitle(String name) { + return 'Všetko je pripravené, $name'; + } @override - String get firstCaptureProcessing => 'Creating your first conversation…'; + String get onboardingActRateTitle => 'Páči sa vám zatiaľ Omi?'; @override - String get firstCaptureResult => 'Your first conversation'; + String get onboardingActRateYes => 'Ohodnotiť Omi'; @override - String get firstCaptureEmpty => 'I didn\'t catch enough speech, try again with a few sentences'; + String get onboardingWelcomeCardTitle => 'Vitajte v Omi'; @override - String get watchCaptureTitle => 'Record from your Apple Watch'; + String get onboardingWelcomeCardBody => + 'Noste Omi a hovorte ako zvyčajne. Každý rozhovor sa tu zmení na poznámky, úlohy a spomienky.'; @override - String get watchCaptureOpen => 'Open Omi on your Apple Watch.'; + String get onboardingActDemoTitle => 'Obed so Sam'; @override - String get watchCaptureRecord => 'Tap Record. Allow microphone access if asked.'; + String get onboardingActDemoSummary => + 'Obed so Sam, ktorá sa v júni sťahuje do Lisabonu. Do piatku chcete poslať tímu prezentáciu a cestou domov kúpiť mlieko.'; @override - String get watchCaptureSync => 'Your recording syncs to Omi on this phone.'; + String get onboardingActDemoTask1 => 'Poslať prezentáciu tímu do piatka'; @override - String get pairingBluetoothOff => 'Turn on Bluetooth on your phone, then try again.'; + String get onboardingActDemoTask2 => 'Cestou domov kúpiť mlieko'; @override - String get pairingBluetoothPermission => 'Allow Bluetooth access for Omi in Settings, then try again.'; + String get onboardingActJustNow => 'Práve teraz'; @override - String get pairingNearbyPermission => 'Allow Nearby devices access for Omi in Settings, then try again.'; + String get onboardingActSavedToTasks => 'Uložené do vašich úloh'; @override - String get pairingLocationPermission => - 'Allow location access for Omi in Settings so this phone can find your device, then try again.'; + String get onboardingConnectTapToConnect => 'Ťuknutím na Omi sa pripojíte.'; @override - String get pairingNotFoundOrDropped => - 'Couldn\'t reach your Omi. Make sure it\'s charged, close to your phone, and its light is blinking, then try again.'; + String get onboardingActMicFallbackBody => 'Omi zatiaľ neposiela zvuk. Zatiaľ použite telefón.'; @override - String get pairingBondRecovery => 'Forget Omi in your phone’s Bluetooth settings, then try pairing again.'; + String get onboardingActNoAudioOmi => 'Omi nepočuť. Skontrolujte, či je zapnuté a blízko.'; @override - String get pairingHowTo => 'How to pair'; + String get onboardingActNoAudioPhone => 'Zatiaľ vás nepočuť. Skúste hovoriť trochu hlasnejšie.'; + + @override + String get onboardingActDictateBodyOmi => 'Jednoducho hovorte. Vaše Omi počúva.'; + + @override + String get voiceProfileCardDismissed => 'Hlasový profil si môžete kedykoľvek nastaviť v Nastaveniach.'; + + @override + String get homeNewRecordingButton => 'Nový záznam'; + + @override + String get chatWelcomeMessage => + 'Ahoj, som Omi. Opýtaj sa ma na čokoľvek, o čom si hovoril: čo kto povedal, čo si sľúbil alebo zhrnutie tvojho dňa. Viem aj pridávať úlohy a pamätať si veci za teba.'; + + @override + String get onboardingStarterTaskAsk => 'Opýtaj sa Omi na svoj deň'; + + @override + String get onboardingStarterTaskRecord => 'Nahraj rozhovor s Omi'; + + @override + String get onboardingStarterTaskCheck => 'Odškrtni túto úlohu a dokonči ju'; + + @override + String get paywallRecommended => 'Recommended'; @override - String get firstCaptureWatchInstructions => - 'Open Omi on your Apple Watch and tap Record. Allow microphone access if asked. Your capture will sync to Omi on this phone.'; + String get paywallNoPlans => 'Plans aren\'t available right now. Try again in a minute.'; @override - String get firstCaptureWatchInstall => - 'In the Watch app on your iPhone, install Omi on your Apple Watch. Then open Omi on your watch and tap Record.'; + String get paywallOtherPlans => 'See all plans'; @override - String get firstCaptureWatchPair => - 'Pair your Apple Watch with this iPhone using the Watch app, then install and open Omi on your watch.'; + String get paywallComparePlans => 'See plans'; @override - String get firstCaptureUnavailable => - 'Recording could not start. Check microphone access and your connection, then try again.'; + String get paywallYourPlan => 'Your plan'; @override - String get firstCaptureDelayed => - 'Your recording is taking longer to process. You can check your conversations on Home or try again.'; + String get paywallNotNow => 'Not now'; @override - String get firstCaptureEmptyTitle => 'Let\'s try that again'; + String get paywallTryAgain => 'Try again'; @override - String get firstCaptureShortTitle => 'Finish This Recording?'; + String get paywallManageBilling => 'Manage billing'; @override - String get firstCaptureShortWarning => 'A few more sentences help Omi understand you. About 30 seconds is ideal.'; + String get paywallLoadError => 'Couldn\'t load plans. Check your connection and try again.'; @override - String get firstCaptureCancelTitle => 'Leave This Recording?'; + String get paywallPlans => 'Plans'; @override - String get firstCaptureCancelWarning => - 'Going back stops this recording. Keep talking to give Omi a few more sentences.'; + String get watchCaptureTitle => 'Record from your Apple Watch'; @override - String get firstCaptureSiriTip => 'Tip: Enable Omi in Shortcuts → Siri.'; + String get watchCaptureOpen => 'Open Omi on your Apple Watch.'; + + @override + String get watchCaptureRecord => 'Tap Record. Allow microphone access if asked.'; + + @override + String get watchCaptureSync => 'Your recording syncs to Omi on this phone.'; + + @override + String get pairingBluetoothOff => 'Turn on Bluetooth on your phone, then try again.'; + + @override + String get pairingBluetoothPermission => 'Allow Bluetooth access for Omi in Settings, then try again.'; + + @override + String get pairingNearbyPermission => 'Allow Nearby devices access for Omi in Settings, then try again.'; + + @override + String get pairingLocationPermission => + 'Allow location access for Omi in Settings so this phone can find your device, then try again.'; + + @override + String get pairingNotFoundOrDropped => + 'Couldn\'t reach your Omi. Make sure it\'s charged, close to your phone, and its light is blinking, then try again.'; + + @override + String get pairingBondRecovery => 'Forget Omi in your phone’s Bluetooth settings, then try pairing again.'; + + @override + String get pairingHowTo => 'How to pair'; @override String get paywallOutlineHeadline => 'Už vám neunikne ani slovo.'; diff --git a/app/lib/l10n/app_localizations_sl.dart b/app/lib/l10n/app_localizations_sl.dart index 1ba3039f403..319a5de95b2 100644 --- a/app/lib/l10n/app_localizations_sl.dart +++ b/app/lib/l10n/app_localizations_sl.dart @@ -1685,8 +1685,7 @@ class AppLocalizationsSl extends AppLocalizations { String get enableBluetooth => 'Omogočite Bluetooth'; @override - String get bluetoothNeeded => - 'Omi potrebuje Bluetooth za povezavo z vašo nosljivo napravo. Prosimo omogočite Bluetooth in poskusite znova.'; + String get bluetoothNeeded => 'Omi se s telefonom poveže prek Bluetootha. Vklopite ga za nadaljevanje.'; @override String get contactSupport => 'Stopite v kontakt s podporo?'; @@ -4902,7 +4901,7 @@ class AppLocalizationsSl extends AppLocalizations { 'Z nadaljevanjem bodo vaši pogovori, posnetki in osebni podatki varno shranjeni na naših strežnikih. Vaši zvočni posnetki in prepisi se obdelujejo s storitvami umetne inteligence tretjih oseb (vključno z Deepgram za prepis in OpenAI za analizo), da vam zagotovimo vpoglede, ki jih poganja umetna inteligenca, in omogočimo vse funkcije aplikacije.'; @override - String get tasksEmptyStateMessage => 'Začnite pogovor, da ustvarite nalogo.'; + String get tasksEmptyStateMessage => 'Opravila iz vaših pogovorov se bodo prikazala tukaj.'; @override String get clearChatAction => 'Počistite klepet'; @@ -13070,154 +13069,207 @@ class AppLocalizationsSl extends AppLocalizations { } @override - String get paywallRecommended => 'Recommended'; + String get onboardingPermissionTapToFix => 'Tapnite za popravilo'; @override - String get paywallNoPlans => 'Plans aren\'t available right now. Try again in a minute.'; + String get onboardingNoOmiYet => 'Še nimate Omi?'; @override - String get paywallOtherPlans => 'See all plans'; + String get onboardingOmiOrdered => 'Naročil/a sem ga'; @override - String get paywallComparePlans => 'See plans'; + String get onboardingOmiNotOrdered => 'Še nisem naročil/a'; @override - String get paywallYourPlan => 'Your plan'; + String get onboardingConnectOmi => 'Povežite Omi'; @override - String get paywallNotNow => 'Not now'; + String get consentShortMessage => + 'Omi z umetno inteligenco spremeni vaše pogovore v zapiske. Kadar koli jih lahko izbrišete.'; @override - String get paywallTryAgain => 'Try again'; + String consentAiProviders(String providers) { + return 'Obdelano z $providers.'; + } @override - String get paywallManageBilling => 'Manage billing'; + String get onboardingActIntro => 'Preizkusimo Omi.'; @override - String get paywallLoadError => 'Couldn\'t load plans. Check your connection and try again.'; + String get onboardingActMicTitle => 'Omi potrebuje mikrofon'; @override - String get paywallPlans => 'Plans'; + String get onboardingActMicBody => 'Da vas Omi sliši, ko mu govorite.'; @override - String get firstCaptureTitle => 'How will you capture?'; + String get onboardingActMicAllow => 'Dovoli mikrofon'; @override - String get firstCaptureDescription => 'Choose how you want to capture your first conversation.'; + String get onboardingActDictateTitle => 'Povejte Omiju o svojem dnevu'; @override - String get firstCaptureOmi => 'I Have an Omi'; + String get onboardingActDictateBody => 'Povejte po svoje. Za ostalo poskrbi Omi.'; @override - String get firstCapturePhone => 'This Phone'; + String get onboardingActDictateExample => + 'Pravkar sem kosil s Sam, junija se seli v Lizbono! Opomni me, naj do petka pošljem predstavitev ekipi in na poti domov kupim mleko.'; @override - String get firstCaptureWatch => 'Apple Watch'; + String get onboardingActDictateHint => 'Ko končate, tapnite kljukico'; @override - String get firstCaptureSkip => 'Skip for Now'; + String get onboardingActPhoneMic => 'Mikrofon telefona'; @override - String get firstCapturePrompt => - 'Tell Omi what you\'re working on today, or what\'s on your mind. Talk for about 30 seconds.'; + String get onboardingActWriting => 'Povzemamo…'; @override - String get firstCaptureKeepGoing => 'Keep going… 30 seconds is ideal.'; + String get onboardingActRemembers => 'Omi si zapomni namesto vas'; @override - String get firstCaptureReady => 'You can finish now, or keep talking.'; + String get onboardingActNotesLabel => 'Vaš dan na kratko'; @override - String get firstCaptureStart => 'Start Recording'; + String get onboardingActTasksLabel => 'Omi je ujel vaša opravila'; @override - String get firstCaptureFinish => 'Finish Recording'; + String get onboardingActYourWords => 'To ste povedali'; @override - String get firstCaptureListening => 'Listening… your words will appear here.'; + String onboardingActCongratsTitle(String name) { + return 'Vse je pripravljeno, $name'; + } @override - String get firstCaptureProcessing => 'Creating your first conversation…'; + String get onboardingActRateTitle => 'Vam je Omi všeč?'; @override - String get firstCaptureResult => 'Your first conversation'; + String get onboardingActRateYes => 'Ocenite Omi'; @override - String get firstCaptureEmpty => 'I didn\'t catch enough speech, try again with a few sentences'; + String get onboardingWelcomeCardTitle => 'Dobrodošli v Omi'; @override - String get watchCaptureTitle => 'Record from your Apple Watch'; + String get onboardingWelcomeCardBody => + 'Nosite Omi in govorite kot običajno. Vsak pogovor tukaj postane zapiski, naloge in spomini.'; @override - String get watchCaptureOpen => 'Open Omi on your Apple Watch.'; + String get onboardingActDemoTitle => 'Kosilo s Sam'; @override - String get watchCaptureRecord => 'Tap Record. Allow microphone access if asked.'; + String get onboardingActDemoSummary => + 'Kosilo s Samom, ki se junija seli v Lizbono. Do petka želite ekipi poslati predstavitev in na poti domov kupiti mleko.'; @override - String get watchCaptureSync => 'Your recording syncs to Omi on this phone.'; + String get onboardingActDemoTask1 => 'Do petka poslati predstavitev ekipi'; @override - String get pairingBluetoothOff => 'Turn on Bluetooth on your phone, then try again.'; + String get onboardingActDemoTask2 => 'Na poti domov kupiti mleko'; @override - String get pairingBluetoothPermission => 'Allow Bluetooth access for Omi in Settings, then try again.'; + String get onboardingActJustNow => 'Pravkar'; @override - String get pairingNearbyPermission => 'Allow Nearby devices access for Omi in Settings, then try again.'; + String get onboardingActSavedToTasks => 'Shranjeno med vaša opravila'; @override - String get pairingLocationPermission => - 'Allow location access for Omi in Settings so this phone can find your device, then try again.'; + String get onboardingConnectTapToConnect => 'Tapnite svoj Omi za povezavo.'; @override - String get pairingNotFoundOrDropped => - 'Couldn\'t reach your Omi. Make sure it\'s charged, close to your phone, and its light is blinking, then try again.'; + String get onboardingActMicFallbackBody => 'Omi še ne pošilja zvoka. Za zdaj uporabite telefon.'; @override - String get pairingBondRecovery => 'Forget Omi in your phone’s Bluetooth settings, then try pairing again.'; + String get onboardingActNoAudioOmi => 'Omija ne slišimo. Preverite, ali je vklopljen in blizu.'; @override - String get pairingHowTo => 'How to pair'; + String get onboardingActNoAudioPhone => 'Še vas ne slišimo. Poskusite govoriti malo glasneje.'; + + @override + String get onboardingActDictateBodyOmi => 'Samo govorite. Vaš Omi posluša.'; + + @override + String get voiceProfileCardDismissed => 'Glasovni profil lahko kadar koli nastavite v Nastavitvah.'; + + @override + String get homeNewRecordingButton => 'Novo snemanje'; + + @override + String get chatWelcomeMessage => + 'Živjo, sem Omi. Vprašaj me karkoli o tem, o čemer si govoril: kaj je kdo rekel, kaj si obljubil ali povzetek tvojega dne. Lahko tudi dodajam opravila in si zapomnim stvari namesto tebe.'; + + @override + String get onboardingStarterTaskAsk => 'Vprašaj Omi o svojem dnevu'; + + @override + String get onboardingStarterTaskRecord => 'Posnemi pogovor z Omi'; + + @override + String get onboardingStarterTaskCheck => 'Odkljukaj to opravilo, da ga končaš'; + + @override + String get paywallRecommended => 'Recommended'; @override - String get firstCaptureWatchInstructions => - 'Open Omi on your Apple Watch and tap Record. Allow microphone access if asked. Your capture will sync to Omi on this phone.'; + String get paywallNoPlans => 'Plans aren\'t available right now. Try again in a minute.'; @override - String get firstCaptureWatchInstall => - 'In the Watch app on your iPhone, install Omi on your Apple Watch. Then open Omi on your watch and tap Record.'; + String get paywallOtherPlans => 'See all plans'; @override - String get firstCaptureWatchPair => - 'Pair your Apple Watch with this iPhone using the Watch app, then install and open Omi on your watch.'; + String get paywallComparePlans => 'See plans'; @override - String get firstCaptureUnavailable => - 'Recording could not start. Check microphone access and your connection, then try again.'; + String get paywallYourPlan => 'Your plan'; @override - String get firstCaptureDelayed => - 'Your recording is taking longer to process. You can check your conversations on Home or try again.'; + String get paywallNotNow => 'Not now'; @override - String get firstCaptureEmptyTitle => 'Let\'s try that again'; + String get paywallTryAgain => 'Try again'; @override - String get firstCaptureShortTitle => 'Finish This Recording?'; + String get paywallManageBilling => 'Manage billing'; @override - String get firstCaptureShortWarning => 'A few more sentences help Omi understand you. About 30 seconds is ideal.'; + String get paywallLoadError => 'Couldn\'t load plans. Check your connection and try again.'; @override - String get firstCaptureCancelTitle => 'Leave This Recording?'; + String get paywallPlans => 'Plans'; @override - String get firstCaptureCancelWarning => - 'Going back stops this recording. Keep talking to give Omi a few more sentences.'; + String get watchCaptureTitle => 'Record from your Apple Watch'; @override - String get firstCaptureSiriTip => 'Tip: Enable Omi in Shortcuts → Siri.'; + String get watchCaptureOpen => 'Open Omi on your Apple Watch.'; + + @override + String get watchCaptureRecord => 'Tap Record. Allow microphone access if asked.'; + + @override + String get watchCaptureSync => 'Your recording syncs to Omi on this phone.'; + + @override + String get pairingBluetoothOff => 'Turn on Bluetooth on your phone, then try again.'; + + @override + String get pairingBluetoothPermission => 'Allow Bluetooth access for Omi in Settings, then try again.'; + + @override + String get pairingNearbyPermission => 'Allow Nearby devices access for Omi in Settings, then try again.'; + + @override + String get pairingLocationPermission => + 'Allow location access for Omi in Settings so this phone can find your device, then try again.'; + + @override + String get pairingNotFoundOrDropped => + 'Couldn\'t reach your Omi. Make sure it\'s charged, close to your phone, and its light is blinking, then try again.'; + + @override + String get pairingBondRecovery => 'Forget Omi in your phone’s Bluetooth settings, then try pairing again.'; + + @override + String get pairingHowTo => 'How to pair'; @override String get paywallOutlineHeadline => 'Nikoli več ne zamudite niti besede.'; diff --git a/app/lib/l10n/app_localizations_sr.dart b/app/lib/l10n/app_localizations_sr.dart index 908a7e526ed..fd667df0522 100644 --- a/app/lib/l10n/app_localizations_sr.dart +++ b/app/lib/l10n/app_localizations_sr.dart @@ -1685,8 +1685,7 @@ class AppLocalizationsSr extends AppLocalizations { String get enableBluetooth => 'Омогући Bluetooth'; @override - String get bluetoothNeeded => - 'Omi треба Bluetooth да би се повезао на вашу носиву. Омогућите Bluetooth и покушајте поново.'; + String get bluetoothNeeded => 'Omi се са телефоном повезује преко Bluetooth-а. Укључите га да наставите.'; @override String get contactSupport => 'Контактирајте подршку?'; @@ -4894,7 +4893,7 @@ class AppLocalizationsSr extends AppLocalizations { 'Настављањем, ваши разговори, снимци и лични подаци биће безбедно ускладиштени на нашим серверима. Ваши аудио снимци и транскрипти се обрађују од стране AI сервиса трећих страна (укључујући Deepgram за транскрипцију и OpenAI за анализу) како би вам пружили увиде засноване на вештачкој интелигенцији и омогућили све функције апликације.'; @override - String get tasksEmptyStateMessage => 'Започните разговор да бисте направили задатак.'; + String get tasksEmptyStateMessage => 'Задаци из ваших разговора појавиће се овде.'; @override String get clearChatAction => 'Обриши разговор'; @@ -13049,154 +13048,207 @@ class AppLocalizationsSr extends AppLocalizations { } @override - String get paywallRecommended => 'Recommended'; + String get onboardingPermissionTapToFix => 'Додирните да поправите'; @override - String get paywallNoPlans => 'Plans aren\'t available right now. Try again in a minute.'; + String get onboardingNoOmiYet => 'Још немате Omi?'; @override - String get paywallOtherPlans => 'See all plans'; + String get onboardingOmiOrdered => 'Наручио/ла сам га'; @override - String get paywallComparePlans => 'See plans'; + String get onboardingOmiNotOrdered => 'Још нисам наручио/ла'; @override - String get paywallYourPlan => 'Your plan'; + String get onboardingConnectOmi => 'Повежите Omi'; @override - String get paywallNotNow => 'Not now'; + String get consentShortMessage => + 'Omi користи AI да ваше разговоре претвори у белешке. Можете их обрисати у било ком тренутку.'; @override - String get paywallTryAgain => 'Try again'; + String consentAiProviders(String providers) { + return 'Обрађује се помоћу $providers.'; + } @override - String get paywallManageBilling => 'Manage billing'; + String get onboardingActIntro => 'Испробајмо Omi.'; @override - String get paywallLoadError => 'Couldn\'t load plans. Check your connection and try again.'; + String get onboardingActMicTitle => 'Omi-ју треба микрофон'; @override - String get paywallPlans => 'Plans'; + String get onboardingActMicBody => 'Да вас Omi чује када му се обратите.'; @override - String get firstCaptureTitle => 'How will you capture?'; + String get onboardingActMicAllow => 'Дозволи микрофон'; @override - String get firstCaptureDescription => 'Choose how you want to capture your first conversation.'; + String get onboardingActDictateTitle => 'Испричајте Omi-ју свој дан'; @override - String get firstCaptureOmi => 'I Have an Omi'; + String get onboardingActDictateBody => 'Реците то на свој начин. Omi ће се побринути за остало.'; @override - String get firstCapturePhone => 'This Phone'; + String get onboardingActDictateExample => + 'Управо сам ручао са Сем, у јуну се сели у Лисабон! Подсети ме да пошаљем презентацију тиму до петка и да купим млеко на путу кући.'; @override - String get firstCaptureWatch => 'Apple Watch'; + String get onboardingActDictateHint => 'Додирните квачицу када завршите'; @override - String get firstCaptureSkip => 'Skip for Now'; + String get onboardingActPhoneMic => 'Микрофон телефона'; @override - String get firstCapturePrompt => - 'Tell Omi what you\'re working on today, or what\'s on your mind. Talk for about 30 seconds.'; + String get onboardingActWriting => 'Сажимамо…'; @override - String get firstCaptureKeepGoing => 'Keep going… 30 seconds is ideal.'; + String get onboardingActRemembers => 'Omi памти уместо вас'; @override - String get firstCaptureReady => 'You can finish now, or keep talking.'; + String get onboardingActNotesLabel => 'Ваш дан, укратко'; @override - String get firstCaptureStart => 'Start Recording'; + String get onboardingActTasksLabel => 'Omi је ухватио ваше задатке'; @override - String get firstCaptureFinish => 'Finish Recording'; + String get onboardingActYourWords => 'Ево шта сте рекли'; @override - String get firstCaptureListening => 'Listening… your words will appear here.'; + String onboardingActCongratsTitle(String name) { + return 'Све је спремно, $name'; + } @override - String get firstCaptureProcessing => 'Creating your first conversation…'; + String get onboardingActRateTitle => 'Да ли вам се Omi допада?'; @override - String get firstCaptureResult => 'Your first conversation'; + String get onboardingActRateYes => 'Оцените Omi'; @override - String get firstCaptureEmpty => 'I didn\'t catch enough speech, try again with a few sentences'; + String get onboardingWelcomeCardTitle => 'Добро дошли у Omi'; @override - String get watchCaptureTitle => 'Record from your Apple Watch'; + String get onboardingWelcomeCardBody => + 'Носите Omi и причајте као и обично. Сваки разговор овде постаје белешке, задаци и сећања.'; @override - String get watchCaptureOpen => 'Open Omi on your Apple Watch.'; + String get onboardingActDemoTitle => 'Ручак са Сем'; @override - String get watchCaptureRecord => 'Tap Record. Allow microphone access if asked.'; + String get onboardingActDemoSummary => + 'Ручак са Сем, која се у јуну сели у Лисабон. Желите да пошаљете презентацију тиму до петка и купите млеко на путу кући.'; @override - String get watchCaptureSync => 'Your recording syncs to Omi on this phone.'; + String get onboardingActDemoTask1 => 'Послати презентацију тиму до петка'; @override - String get pairingBluetoothOff => 'Turn on Bluetooth on your phone, then try again.'; + String get onboardingActDemoTask2 => 'Купити млеко на путу кући'; @override - String get pairingBluetoothPermission => 'Allow Bluetooth access for Omi in Settings, then try again.'; + String get onboardingActJustNow => 'Управо сада'; @override - String get pairingNearbyPermission => 'Allow Nearby devices access for Omi in Settings, then try again.'; + String get onboardingActSavedToTasks => 'Сачувано у ваше задатке'; @override - String get pairingLocationPermission => - 'Allow location access for Omi in Settings so this phone can find your device, then try again.'; + String get onboardingConnectTapToConnect => 'Додирните свој Omi да бисте се повезали.'; @override - String get pairingNotFoundOrDropped => - 'Couldn\'t reach your Omi. Make sure it\'s charged, close to your phone, and its light is blinking, then try again.'; + String get onboardingActMicFallbackBody => 'Omi још не шаље звук. За сада користите телефон.'; @override - String get pairingBondRecovery => 'Forget Omi in your phone’s Bluetooth settings, then try pairing again.'; + String get onboardingActNoAudioOmi => 'Не чујемо Omi. Проверите да ли је укључен и близу.'; @override - String get pairingHowTo => 'How to pair'; + String get onboardingActNoAudioPhone => 'Још вас не чујемо. Покушајте да говорите мало гласније.'; + + @override + String get onboardingActDictateBodyOmi => 'Само причајте. Ваш Omi слуша.'; + + @override + String get voiceProfileCardDismissed => 'Гласовни профил можете подесити било када у Подешавањима.'; + + @override + String get homeNewRecordingButton => 'Ново снимање'; + + @override + String get chatWelcomeMessage => + 'Zdravo, ja sam Omi. Pitaj me bilo šta o čemu si pričao: šta je neko rekao, šta si obećao ili rezime tvog dana. Mogu i da dodajem zadatke i pamtim stvari za tebe.'; + + @override + String get onboardingStarterTaskAsk => 'Pitaj Omi o svom danu'; + + @override + String get onboardingStarterTaskRecord => 'Snimi razgovor uz Omi'; + + @override + String get onboardingStarterTaskCheck => 'Označi ovaj zadatak da ga završiš'; + + @override + String get paywallRecommended => 'Recommended'; @override - String get firstCaptureWatchInstructions => - 'Open Omi on your Apple Watch and tap Record. Allow microphone access if asked. Your capture will sync to Omi on this phone.'; + String get paywallNoPlans => 'Plans aren\'t available right now. Try again in a minute.'; @override - String get firstCaptureWatchInstall => - 'In the Watch app on your iPhone, install Omi on your Apple Watch. Then open Omi on your watch and tap Record.'; + String get paywallOtherPlans => 'See all plans'; @override - String get firstCaptureWatchPair => - 'Pair your Apple Watch with this iPhone using the Watch app, then install and open Omi on your watch.'; + String get paywallComparePlans => 'See plans'; @override - String get firstCaptureUnavailable => - 'Recording could not start. Check microphone access and your connection, then try again.'; + String get paywallYourPlan => 'Your plan'; @override - String get firstCaptureDelayed => - 'Your recording is taking longer to process. You can check your conversations on Home or try again.'; + String get paywallNotNow => 'Not now'; @override - String get firstCaptureEmptyTitle => 'Let\'s try that again'; + String get paywallTryAgain => 'Try again'; @override - String get firstCaptureShortTitle => 'Finish This Recording?'; + String get paywallManageBilling => 'Manage billing'; @override - String get firstCaptureShortWarning => 'A few more sentences help Omi understand you. About 30 seconds is ideal.'; + String get paywallLoadError => 'Couldn\'t load plans. Check your connection and try again.'; @override - String get firstCaptureCancelTitle => 'Leave This Recording?'; + String get paywallPlans => 'Plans'; @override - String get firstCaptureCancelWarning => - 'Going back stops this recording. Keep talking to give Omi a few more sentences.'; + String get watchCaptureTitle => 'Record from your Apple Watch'; @override - String get firstCaptureSiriTip => 'Tip: Enable Omi in Shortcuts → Siri.'; + String get watchCaptureOpen => 'Open Omi on your Apple Watch.'; + + @override + String get watchCaptureRecord => 'Tap Record. Allow microphone access if asked.'; + + @override + String get watchCaptureSync => 'Your recording syncs to Omi on this phone.'; + + @override + String get pairingBluetoothOff => 'Turn on Bluetooth on your phone, then try again.'; + + @override + String get pairingBluetoothPermission => 'Allow Bluetooth access for Omi in Settings, then try again.'; + + @override + String get pairingNearbyPermission => 'Allow Nearby devices access for Omi in Settings, then try again.'; + + @override + String get pairingLocationPermission => + 'Allow location access for Omi in Settings so this phone can find your device, then try again.'; + + @override + String get pairingNotFoundOrDropped => + 'Couldn\'t reach your Omi. Make sure it\'s charged, close to your phone, and its light is blinking, then try again.'; + + @override + String get pairingBondRecovery => 'Forget Omi in your phone’s Bluetooth settings, then try pairing again.'; + + @override + String get pairingHowTo => 'How to pair'; @override String get paywallOutlineHeadline => 'Никада више не пропустите ни реч.'; diff --git a/app/lib/l10n/app_localizations_sv.dart b/app/lib/l10n/app_localizations_sv.dart index 9af3b45fb7a..ce4cd1bce44 100644 --- a/app/lib/l10n/app_localizations_sv.dart +++ b/app/lib/l10n/app_localizations_sv.dart @@ -1688,8 +1688,7 @@ class AppLocalizationsSv extends AppLocalizations { String get enableBluetooth => 'Aktivera Bluetooth'; @override - String get bluetoothNeeded => - 'Omi behöver Bluetooth för att ansluta till din bärbara enhet. Aktivera Bluetooth och försök igen.'; + String get bluetoothNeeded => 'Omi ansluter till din telefon via Bluetooth. Slå på det för att fortsätta.'; @override String get contactSupport => 'Kontakta support?'; @@ -4895,7 +4894,7 @@ class AppLocalizationsSv extends AppLocalizations { 'Genom att fortsätta kommer dina konversationer, inspelningar och personlig information att lagras säkert på våra servrar. Dina ljudinspelningar och transkriptioner behandlas av AI-tjänster från tredje part (inklusive Deepgram för transkription och OpenAI för analys) för att ge dig AI-drivna insikter och aktivera alla appfunktioner.'; @override - String get tasksEmptyStateMessage => 'Starta ett samtal för att skapa en uppgift.'; + String get tasksEmptyStateMessage => 'Att göra från dina samtal visas här.'; @override String get clearChatAction => 'Rensa chatt'; @@ -13045,154 +13044,207 @@ class AppLocalizationsSv extends AppLocalizations { } @override - String get paywallRecommended => 'Recommended'; + String get onboardingPermissionTapToFix => 'Tryck för att åtgärda'; @override - String get paywallNoPlans => 'Plans aren\'t available right now. Try again in a minute.'; + String get onboardingNoOmiYet => 'Har du inte fått din Omi än?'; @override - String get paywallOtherPlans => 'See all plans'; + String get onboardingOmiOrdered => 'Jag har beställt den'; @override - String get paywallComparePlans => 'See plans'; + String get onboardingOmiNotOrdered => 'Inte beställt än'; @override - String get paywallYourPlan => 'Your plan'; + String get onboardingConnectOmi => 'Anslut Omi'; @override - String get paywallNotNow => 'Not now'; + String get consentShortMessage => + 'Omi använder AI för att göra dina samtal till anteckningar. Du kan radera dem när som helst.'; @override - String get paywallTryAgain => 'Try again'; + String consentAiProviders(String providers) { + return 'Bearbetas med $providers.'; + } @override - String get paywallManageBilling => 'Manage billing'; + String get onboardingActIntro => 'Vi testar Omi.'; @override - String get paywallLoadError => 'Couldn\'t load plans. Check your connection and try again.'; + String get onboardingActMicTitle => 'Omi behöver din mikrofon'; @override - String get paywallPlans => 'Plans'; + String get onboardingActMicBody => 'Så att Omi hör dig när du pratar med den.'; @override - String get firstCaptureTitle => 'How will you capture?'; + String get onboardingActMicAllow => 'Tillåt mikrofon'; @override - String get firstCaptureDescription => 'Choose how you want to capture your first conversation.'; + String get onboardingActDictateTitle => 'Berätta för Omi om din dag'; @override - String get firstCaptureOmi => 'I Have an Omi'; + String get onboardingActDictateBody => 'Säg det på ditt sätt. Omi sköter resten.'; @override - String get firstCapturePhone => 'This Phone'; + String get onboardingActDictateExample => + 'Åt precis lunch med Sam, hon flyttar till Lissabon i juni! Påminn mig om att skicka presentationen till teamet senast fredag och att köpa mjölk på vägen hem.'; @override - String get firstCaptureWatch => 'Apple Watch'; + String get onboardingActDictateHint => 'Tryck på bocken när du är klar'; @override - String get firstCaptureSkip => 'Skip for Now'; + String get onboardingActPhoneMic => 'Telefonens mikrofon'; @override - String get firstCapturePrompt => - 'Tell Omi what you\'re working on today, or what\'s on your mind. Talk for about 30 seconds.'; + String get onboardingActWriting => 'Sammanfattar…'; @override - String get firstCaptureKeepGoing => 'Keep going… 30 seconds is ideal.'; + String get onboardingActRemembers => 'Omi minns så att du slipper'; @override - String get firstCaptureReady => 'You can finish now, or keep talking.'; + String get onboardingActNotesLabel => 'Din dag, sammanfattad'; @override - String get firstCaptureStart => 'Start Recording'; + String get onboardingActTasksLabel => 'Omi fångade dina att-göra'; @override - String get firstCaptureFinish => 'Finish Recording'; + String get onboardingActYourWords => 'Det här sa du'; @override - String get firstCaptureListening => 'Listening… your words will appear here.'; + String onboardingActCongratsTitle(String name) { + return 'Allt är klart, $name'; + } @override - String get firstCaptureProcessing => 'Creating your first conversation…'; + String get onboardingActRateTitle => 'Gillar du Omi hittills?'; @override - String get firstCaptureResult => 'Your first conversation'; + String get onboardingActRateYes => 'Betygsätt Omi'; @override - String get firstCaptureEmpty => 'I didn\'t catch enough speech, try again with a few sentences'; + String get onboardingWelcomeCardTitle => 'Välkommen till Omi'; @override - String get watchCaptureTitle => 'Record from your Apple Watch'; + String get onboardingWelcomeCardBody => + 'Bär Omi och prata som vanligt. Varje samtal blir här till anteckningar, uppgifter och minnen.'; @override - String get watchCaptureOpen => 'Open Omi on your Apple Watch.'; + String get onboardingActDemoTitle => 'Lunch med Sam'; @override - String get watchCaptureRecord => 'Tap Record. Allow microphone access if asked.'; + String get onboardingActDemoSummary => + 'Lunch med Sam, som flyttar till Lissabon i juni. Du vill skicka presentationen till teamet senast fredag och köpa mjölk på vägen hem.'; @override - String get watchCaptureSync => 'Your recording syncs to Omi on this phone.'; + String get onboardingActDemoTask1 => 'Skicka presentationen till teamet senast fredag'; @override - String get pairingBluetoothOff => 'Turn on Bluetooth on your phone, then try again.'; + String get onboardingActDemoTask2 => 'Köp mjölk på vägen hem'; @override - String get pairingBluetoothPermission => 'Allow Bluetooth access for Omi in Settings, then try again.'; + String get onboardingActJustNow => 'Precis nu'; @override - String get pairingNearbyPermission => 'Allow Nearby devices access for Omi in Settings, then try again.'; + String get onboardingActSavedToTasks => 'Sparat i dina uppgifter'; @override - String get pairingLocationPermission => - 'Allow location access for Omi in Settings so this phone can find your device, then try again.'; + String get onboardingConnectTapToConnect => 'Tryck på din Omi för att ansluta.'; @override - String get pairingNotFoundOrDropped => - 'Couldn\'t reach your Omi. Make sure it\'s charged, close to your phone, and its light is blinking, then try again.'; + String get onboardingActMicFallbackBody => 'Din Omi skickar inget ljud än. Använd telefonen så länge.'; @override - String get pairingBondRecovery => 'Forget Omi in your phone’s Bluetooth settings, then try pairing again.'; + String get onboardingActNoAudioOmi => 'Hör inte din Omi. Kontrollera att den är på och nära.'; @override - String get pairingHowTo => 'How to pair'; + String get onboardingActNoAudioPhone => 'Hör dig inte än. Försök prata lite högre.'; + + @override + String get onboardingActDictateBodyOmi => 'Prata bara. Din Omi lyssnar.'; + + @override + String get voiceProfileCardDismissed => 'Du kan ställa in din röstprofil när som helst i Inställningar.'; + + @override + String get homeNewRecordingButton => 'Ny inspelning'; + + @override + String get chatWelcomeMessage => + 'Hej, jag är Omi. Fråga mig om allt du har pratat om: vad någon sa, vad du lovade eller en sammanfattning av din dag. Jag kan också lägga till uppgifter och komma ihåg saker åt dig.'; + + @override + String get onboardingStarterTaskAsk => 'Fråga Omi om din dag'; + + @override + String get onboardingStarterTaskRecord => 'Spela in ett samtal med Omi'; + + @override + String get onboardingStarterTaskCheck => 'Bocka av den här uppgiften för att slutföra den'; + + @override + String get paywallRecommended => 'Recommended'; @override - String get firstCaptureWatchInstructions => - 'Open Omi on your Apple Watch and tap Record. Allow microphone access if asked. Your capture will sync to Omi on this phone.'; + String get paywallNoPlans => 'Plans aren\'t available right now. Try again in a minute.'; @override - String get firstCaptureWatchInstall => - 'In the Watch app on your iPhone, install Omi on your Apple Watch. Then open Omi on your watch and tap Record.'; + String get paywallOtherPlans => 'See all plans'; @override - String get firstCaptureWatchPair => - 'Pair your Apple Watch with this iPhone using the Watch app, then install and open Omi on your watch.'; + String get paywallComparePlans => 'See plans'; @override - String get firstCaptureUnavailable => - 'Recording could not start. Check microphone access and your connection, then try again.'; + String get paywallYourPlan => 'Your plan'; @override - String get firstCaptureDelayed => - 'Your recording is taking longer to process. You can check your conversations on Home or try again.'; + String get paywallNotNow => 'Not now'; @override - String get firstCaptureEmptyTitle => 'Let\'s try that again'; + String get paywallTryAgain => 'Try again'; @override - String get firstCaptureShortTitle => 'Finish This Recording?'; + String get paywallManageBilling => 'Manage billing'; @override - String get firstCaptureShortWarning => 'A few more sentences help Omi understand you. About 30 seconds is ideal.'; + String get paywallLoadError => 'Couldn\'t load plans. Check your connection and try again.'; @override - String get firstCaptureCancelTitle => 'Leave This Recording?'; + String get paywallPlans => 'Plans'; @override - String get firstCaptureCancelWarning => - 'Going back stops this recording. Keep talking to give Omi a few more sentences.'; + String get watchCaptureTitle => 'Record from your Apple Watch'; @override - String get firstCaptureSiriTip => 'Tip: Enable Omi in Shortcuts → Siri.'; + String get watchCaptureOpen => 'Open Omi on your Apple Watch.'; + + @override + String get watchCaptureRecord => 'Tap Record. Allow microphone access if asked.'; + + @override + String get watchCaptureSync => 'Your recording syncs to Omi on this phone.'; + + @override + String get pairingBluetoothOff => 'Turn on Bluetooth on your phone, then try again.'; + + @override + String get pairingBluetoothPermission => 'Allow Bluetooth access for Omi in Settings, then try again.'; + + @override + String get pairingNearbyPermission => 'Allow Nearby devices access for Omi in Settings, then try again.'; + + @override + String get pairingLocationPermission => + 'Allow location access for Omi in Settings so this phone can find your device, then try again.'; + + @override + String get pairingNotFoundOrDropped => + 'Couldn\'t reach your Omi. Make sure it\'s charged, close to your phone, and its light is blinking, then try again.'; + + @override + String get pairingBondRecovery => 'Forget Omi in your phone’s Bluetooth settings, then try pairing again.'; + + @override + String get pairingHowTo => 'How to pair'; @override String get paywallOutlineHeadline => 'Missa aldrig ett ord igen.'; diff --git a/app/lib/l10n/app_localizations_ta.dart b/app/lib/l10n/app_localizations_ta.dart index 841c3fdf3b2..dc7e51dec20 100644 --- a/app/lib/l10n/app_localizations_ta.dart +++ b/app/lib/l10n/app_localizations_ta.dart @@ -1696,8 +1696,7 @@ class AppLocalizationsTa extends AppLocalizations { String get enableBluetooth => 'Bluetooth ஐ இயக்கவும்'; @override - String get bluetoothNeeded => - 'Omi உங்கள் உணர்வுமான சாதனத்துடன் இணைக்க Bluetooth தேவை. Bluetooth ஐ இயக்கவும் மற்றும் மீண்டும் முயற்சி செய்யவும்.'; + String get bluetoothNeeded => 'Omi உங்கள் போனுடன் Bluetooth மூலம் இணைகிறது. தொடர அதை இயக்கவும்.'; @override String get contactSupport => 'ஆதரவுடன் যোগாசையை கொள்ளவா?'; @@ -4933,7 +4932,7 @@ class AppLocalizationsTa extends AppLocalizations { 'தொடர்வதன் மூலம், உங்கள் உரையாடல்கள், பதிவுகள் மற்றும் தனிப்பட்ட தகவல்கள் எங்கள் சேவையகங்களில் பாதுகாப்பாக சேமிக்கப்படும். உங்கள் ஆடியோ பதிவுகள் மற்றும் படியெடுப்புகள் மூன்றாம் தரப்பு AI சேவைகளால் செயலாக்கப்படுகின்றன (படியெடுப்பிற்கு Deepgram மற்றும் பகுப்பாய்விற்கு OpenAI உட்பட) AI இயக்கும் நுண்ணறிவுகளை உங்களுக்கு வழங்கவும் அனைத்து பயன்பாட்டு அம்சங்களையும் இயக்கவும்.'; @override - String get tasksEmptyStateMessage => 'ஒரு பணியை உருவாக்க உரையாடலைத் தொடங்குங்கள்.'; + String get tasksEmptyStateMessage => 'உங்கள் உரையாடல்களில் உள்ள பணிகள் இங்கே தோன்றும்.'; @override String get clearChatAction => 'உரையாடலைத் தெளிக்கவும்'; @@ -13130,154 +13129,209 @@ class AppLocalizationsTa extends AppLocalizations { } @override - String get paywallRecommended => 'Recommended'; + String get onboardingPermissionTapToFix => 'சரிசெய்ய தட்டவும்'; @override - String get paywallNoPlans => 'Plans aren\'t available right now. Try again in a minute.'; + String get onboardingNoOmiYet => 'இன்னும் உங்கள் Omi இல்லையா?'; @override - String get paywallOtherPlans => 'See all plans'; + String get onboardingOmiOrdered => 'நான் ஆர்டர் செய்துவிட்டேன்'; @override - String get paywallComparePlans => 'See plans'; + String get onboardingOmiNotOrdered => 'இன்னும் ஆர்டர் செய்யவில்லை'; @override - String get paywallYourPlan => 'Your plan'; + String get onboardingConnectOmi => 'Omi-ஐ இணைக்கவும்'; @override - String get paywallNotNow => 'Not now'; + String get consentShortMessage => + 'Omi AI மூலம் உங்கள் உரையாடல்களைக் குறிப்புகளாக மாற்றுகிறது. எப்போது வேண்டுமானாலும் நீக்கலாம்.'; @override - String get paywallTryAgain => 'Try again'; + String consentAiProviders(String providers) { + return '$providers மூலம் செயலாக்கப்படுகிறது.'; + } @override - String get paywallManageBilling => 'Manage billing'; + String get onboardingActIntro => 'Omi-ஐ முயற்சிப்போம்.'; @override - String get paywallLoadError => 'Couldn\'t load plans. Check your connection and try again.'; + String get onboardingActMicTitle => 'Omi-க்கு உங்கள் மைக்ரோஃபோன் தேவை'; @override - String get paywallPlans => 'Plans'; + String get onboardingActMicBody => 'நீங்கள் பேசும்போது Omi கேட்க.'; @override - String get firstCaptureTitle => 'How will you capture?'; + String get onboardingActMicAllow => 'மைக்ரோஃபோனை அனுமதி'; @override - String get firstCaptureDescription => 'Choose how you want to capture your first conversation.'; + String get onboardingActDictateTitle => 'உங்கள் நாளைப் பற்றி Omi-யிடம் சொல்லுங்கள்'; @override - String get firstCaptureOmi => 'I Have an Omi'; + String get onboardingActDictateBody => 'உங்கள் பாணியில் சொல்லுங்கள். மீதியை Omi பார்த்துக்கொள்ளும்.'; @override - String get firstCapturePhone => 'This Phone'; + String get onboardingActDictateExample => + 'இப்போதுதான் சாமுடன் மதிய உணவு சாப்பிட்டேன், அவள் ஜூனில் லிஸ்பனுக்குப் போகிறாள்! வெள்ளிக்குள் குழுவுக்கு டெக் அனுப்பவும், வீட்டுக்குப் போகும் வழியில் பால் வாங்கவும் நினைவூட்டு.'; @override - String get firstCaptureWatch => 'Apple Watch'; + String get onboardingActDictateHint => 'முடிந்ததும் டிக்கைத் தட்டவும்'; @override - String get firstCaptureSkip => 'Skip for Now'; + String get onboardingActPhoneMic => 'தொலைபேசி மைக்ரோஃபோன்'; @override - String get firstCapturePrompt => - 'Tell Omi what you\'re working on today, or what\'s on your mind. Talk for about 30 seconds.'; + String get onboardingActWriting => 'சுருக்கப்படுகிறது…'; @override - String get firstCaptureKeepGoing => 'Keep going… 30 seconds is ideal.'; + String get onboardingActRemembers => 'நீங்கள் நினைவில் வைக்க வேண்டியதில்லை, Omi வைத்துக்கொள்ளும்'; @override - String get firstCaptureReady => 'You can finish now, or keep talking.'; + String get onboardingActNotesLabel => 'உங்கள் நாள், சுருக்கமாக'; @override - String get firstCaptureStart => 'Start Recording'; + String get onboardingActTasksLabel => 'Omi உங்கள் வேலைகளைக் கண்டறிந்தது'; @override - String get firstCaptureFinish => 'Finish Recording'; + String get onboardingActYourWords => 'நீங்கள் சொன்னது இது'; @override - String get firstCaptureListening => 'Listening… your words will appear here.'; + String onboardingActCongratsTitle(String name) { + return 'எல்லாம் தயார், $name'; + } @override - String get firstCaptureProcessing => 'Creating your first conversation…'; + String get onboardingActRateTitle => 'இதுவரை Omi பிடித்திருக்கிறதா?'; @override - String get firstCaptureResult => 'Your first conversation'; + String get onboardingActRateYes => 'Omi-ஐ மதிப்பிடுங்கள்'; @override - String get firstCaptureEmpty => 'I didn\'t catch enough speech, try again with a few sentences'; + String get onboardingWelcomeCardTitle => 'Omi-க்கு வரவேற்கிறோம்'; @override - String get watchCaptureTitle => 'Record from your Apple Watch'; + String get onboardingWelcomeCardBody => + 'Omi-ஐ அணிந்து வழக்கம்போல் பேசுங்கள். ஒவ்வொரு உரையாடலும் இங்கேயே குறிப்புகள், பணிகள், நினைவுகளாகும்.'; @override - String get watchCaptureOpen => 'Open Omi on your Apple Watch.'; + String get onboardingActDemoTitle => 'சாமுடன் மதிய உணவு'; @override - String get watchCaptureRecord => 'Tap Record. Allow microphone access if asked.'; + String get onboardingActDemoSummary => + 'ஜூனில் லிஸ்பனுக்குச் செல்லும் சாமுடன் மதிய உணவு. வெள்ளிக்குள் குழுவுக்கு டெக் அனுப்பவும், வீட்டுக்குப் போகும் வழியில் பால் வாங்கவும் வேண்டும்.'; @override - String get watchCaptureSync => 'Your recording syncs to Omi on this phone.'; + String get onboardingActDemoTask1 => 'வெள்ளிக்குள் குழுவுக்கு டெக் அனுப்புவது'; @override - String get pairingBluetoothOff => 'Turn on Bluetooth on your phone, then try again.'; + String get onboardingActDemoTask2 => 'வீட்டுக்குப் போகும் வழியில் பால் வாங்குவது'; @override - String get pairingBluetoothPermission => 'Allow Bluetooth access for Omi in Settings, then try again.'; + String get onboardingActJustNow => 'இப்போதுதான்'; @override - String get pairingNearbyPermission => 'Allow Nearby devices access for Omi in Settings, then try again.'; + String get onboardingActSavedToTasks => 'உங்கள் பணிகளில் சேமிக்கப்பட்டது'; @override - String get pairingLocationPermission => - 'Allow location access for Omi in Settings so this phone can find your device, then try again.'; + String get onboardingConnectTapToConnect => 'இணைக்க உங்கள் Omi-ஐத் தட்டவும்.'; @override - String get pairingNotFoundOrDropped => - 'Couldn\'t reach your Omi. Make sure it\'s charged, close to your phone, and its light is blinking, then try again.'; + String get onboardingActMicFallbackBody => + 'உங்கள் Omi இன்னும் ஒலியை அனுப்பவில்லை. இப்போதைக்கு உங்கள் போனைப் பயன்படுத்துங்கள்.'; @override - String get pairingBondRecovery => 'Forget Omi in your phone’s Bluetooth settings, then try pairing again.'; + String get onboardingActNoAudioOmi => + 'உங்கள் Omi கேட்கவில்லை. அது இயக்கத்தில் உள்ளதா, அருகில் உள்ளதா எனப் பாருங்கள்.'; @override - String get pairingHowTo => 'How to pair'; + String get onboardingActNoAudioPhone => 'இன்னும் உங்கள் குரல் கேட்கவில்லை. சற்று சத்தமாகப் பேசிப் பாருங்கள்.'; + + @override + String get onboardingActDictateBodyOmi => 'பேசுங்கள் போதும். உங்கள் Omi கேட்டுக்கொண்டிருக்கிறது.'; + + @override + String get voiceProfileCardDismissed => 'அமைப்புகளில் எப்போது வேண்டுமானாலும் குரல் சுயவிவரத்தை அமைக்கலாம்.'; + + @override + String get homeNewRecordingButton => 'புதிய பதிவு'; + + @override + String get chatWelcomeMessage => + 'வணக்கம், நான் Omi. நீங்கள் பேசிய எதைப் பற்றியும் கேளுங்கள்: யார் என்ன சொன்னார்கள், நீங்கள் என்ன வாக்குறுதி அளித்தீர்கள் அல்லது உங்கள் நாளின் சுருக்கம். பணிகளைச் சேர்க்கவும் உங்களுக்காக விஷயங்களை நினைவில் வைக்கவும் என்னால் முடியும்.'; + + @override + String get onboardingStarterTaskAsk => 'உங்கள் நாளைப் பற்றி Omi-யிடம் கேளுங்கள்'; + + @override + String get onboardingStarterTaskRecord => 'Omi மூலம் ஒரு உரையாடலைப் பதிவுசெய்யுங்கள்'; + + @override + String get onboardingStarterTaskCheck => 'முடிக்க இந்தப் பணியைக் குறியிடுங்கள்'; + + @override + String get paywallRecommended => 'Recommended'; @override - String get firstCaptureWatchInstructions => - 'Open Omi on your Apple Watch and tap Record. Allow microphone access if asked. Your capture will sync to Omi on this phone.'; + String get paywallNoPlans => 'Plans aren\'t available right now. Try again in a minute.'; @override - String get firstCaptureWatchInstall => - 'In the Watch app on your iPhone, install Omi on your Apple Watch. Then open Omi on your watch and tap Record.'; + String get paywallOtherPlans => 'See all plans'; @override - String get firstCaptureWatchPair => - 'Pair your Apple Watch with this iPhone using the Watch app, then install and open Omi on your watch.'; + String get paywallComparePlans => 'See plans'; @override - String get firstCaptureUnavailable => - 'Recording could not start. Check microphone access and your connection, then try again.'; + String get paywallYourPlan => 'Your plan'; @override - String get firstCaptureDelayed => - 'Your recording is taking longer to process. You can check your conversations on Home or try again.'; + String get paywallNotNow => 'Not now'; @override - String get firstCaptureEmptyTitle => 'Let\'s try that again'; + String get paywallTryAgain => 'Try again'; @override - String get firstCaptureShortTitle => 'Finish This Recording?'; + String get paywallManageBilling => 'Manage billing'; @override - String get firstCaptureShortWarning => 'A few more sentences help Omi understand you. About 30 seconds is ideal.'; + String get paywallLoadError => 'Couldn\'t load plans. Check your connection and try again.'; @override - String get firstCaptureCancelTitle => 'Leave This Recording?'; + String get paywallPlans => 'Plans'; @override - String get firstCaptureCancelWarning => - 'Going back stops this recording. Keep talking to give Omi a few more sentences.'; + String get watchCaptureTitle => 'Record from your Apple Watch'; @override - String get firstCaptureSiriTip => 'Tip: Enable Omi in Shortcuts → Siri.'; + String get watchCaptureOpen => 'Open Omi on your Apple Watch.'; + + @override + String get watchCaptureRecord => 'Tap Record. Allow microphone access if asked.'; + + @override + String get watchCaptureSync => 'Your recording syncs to Omi on this phone.'; + + @override + String get pairingBluetoothOff => 'Turn on Bluetooth on your phone, then try again.'; + + @override + String get pairingBluetoothPermission => 'Allow Bluetooth access for Omi in Settings, then try again.'; + + @override + String get pairingNearbyPermission => 'Allow Nearby devices access for Omi in Settings, then try again.'; + + @override + String get pairingLocationPermission => + 'Allow location access for Omi in Settings so this phone can find your device, then try again.'; + + @override + String get pairingNotFoundOrDropped => + 'Couldn\'t reach your Omi. Make sure it\'s charged, close to your phone, and its light is blinking, then try again.'; + + @override + String get pairingBondRecovery => 'Forget Omi in your phone’s Bluetooth settings, then try pairing again.'; + + @override + String get pairingHowTo => 'How to pair'; @override String get paywallOutlineHeadline => 'இனி ஒரு வார்த்தையையும் தவறவிடாதீர்கள்.'; diff --git a/app/lib/l10n/app_localizations_te.dart b/app/lib/l10n/app_localizations_te.dart index f4d28266bd0..24dc309db5d 100644 --- a/app/lib/l10n/app_localizations_te.dart +++ b/app/lib/l10n/app_localizations_te.dart @@ -1692,8 +1692,7 @@ class AppLocalizationsTe extends AppLocalizations { String get enableBluetooth => 'బ్లూటూత్ ప్రారంభించండి'; @override - String get bluetoothNeeded => - 'మీ ధరించదగిన పరికరానికి కనెక్ట్ చేయడానికి Omi కు బ్లూటూత్ అవసరం. దయచేసి బ్లూటూత్‌ను ప్రారంభించి మళ్లీ ప్రయత్నించండి.'; + String get bluetoothNeeded => 'Omi మీ ఫోన్‌కు Bluetooth ద్వారా కనెక్ట్ అవుతుంది. కొనసాగడానికి దాన్ని ఆన్ చేయండి.'; @override String get contactSupport => 'సపోర్టుకు సంబంధం?'; @@ -4921,7 +4920,7 @@ class AppLocalizationsTe extends AppLocalizations { 'కొనసాగించడం ద్వారా, మీ సంభాషణలు, రికార్డింగ్‌లు మరియు వ్యక్తిగత సమాచారం మా సర్వర్‌లలో సురక్షితంగా నిల్వ చేయబడతాయి. మీ ఆడియో రికార్డింగ్‌లు మరియు ట్రాన్‌స్క్రిప్ట్‌లు థర్డ్-పార్టీ AI సేవల ద్వారా ప్రాసెస్ చేయబడతాయి (ట్రాన్‌స్క్రిప్షన్ కోసం Deepgram మరియు విశ్లేషణ కోసం OpenAI సహా) AI-ఆధారిత అంతర్దృష్టులను అందించడానికి మరియు అన్ని యాప్ ఫీచర్‌లను ప్రారంభించడానికి.'; @override - String get tasksEmptyStateMessage => 'పనిని సృష్టించడానికి సంభాషణను ప్రారంభించండి.'; + String get tasksEmptyStateMessage => 'మీ సంభాషణల నుండి పనులు ఇక్కడ కనిపిస్తాయి.'; @override String get clearChatAction => 'చాట్‌ను క్లియర్ చేయండి'; @@ -13092,154 +13091,206 @@ class AppLocalizationsTe extends AppLocalizations { } @override - String get paywallRecommended => 'Recommended'; + String get onboardingPermissionTapToFix => 'సరిచేయడానికి నొక్కండి'; @override - String get paywallNoPlans => 'Plans aren\'t available right now. Try again in a minute.'; + String get onboardingNoOmiYet => 'ఇంకా మీ Omi లేదా?'; @override - String get paywallOtherPlans => 'See all plans'; + String get onboardingOmiOrdered => 'నేను ఆర్డర్ చేశాను'; @override - String get paywallComparePlans => 'See plans'; + String get onboardingOmiNotOrdered => 'ఇంకా ఆర్డర్ చేయలేదు'; @override - String get paywallYourPlan => 'Your plan'; + String get onboardingConnectOmi => 'Omiని కనెక్ట్ చేయండి'; @override - String get paywallNotNow => 'Not now'; + String get consentShortMessage => 'Omi AI సహాయంతో మీ సంభాషణలను నోట్స్‌గా మారుస్తుంది. వాటిని ఎప్పుడైనా తొలగించవచ్చు.'; @override - String get paywallTryAgain => 'Try again'; + String consentAiProviders(String providers) { + return '$providers ద్వారా ప్రాసెస్ చేయబడుతుంది.'; + } @override - String get paywallManageBilling => 'Manage billing'; + String get onboardingActIntro => 'Omi ని ప్రయత్నిద్దాం.'; @override - String get paywallLoadError => 'Couldn\'t load plans. Check your connection and try again.'; + String get onboardingActMicTitle => 'Omiకి మీ మైక్రోఫోన్ కావాలి'; @override - String get paywallPlans => 'Plans'; + String get onboardingActMicBody => 'మీరు మాట్లాడినప్పుడు Omi వినడానికి.'; @override - String get firstCaptureTitle => 'How will you capture?'; + String get onboardingActMicAllow => 'మైక్రోఫోన్‌ను అనుమతించండి'; @override - String get firstCaptureDescription => 'Choose how you want to capture your first conversation.'; + String get onboardingActDictateTitle => 'మీ రోజు గురించి Omiకి చెప్పండి'; @override - String get firstCaptureOmi => 'I Have an Omi'; + String get onboardingActDictateBody => 'మీ శైలిలో చెప్పండి. మిగతాది Omi చూసుకుంటుంది.'; @override - String get firstCapturePhone => 'This Phone'; + String get onboardingActDictateExample => + 'ఇప్పుడే సామ్‌తో లంచ్ చేశా, తను జూన్‌లో లిస్బన్‌కి వెళ్తోంది! శుక్రవారంలోగా టీమ్‌కి డెక్ పంపాలని, ఇంటికి వెళ్లేటప్పుడు పాలు తీసుకోవాలని గుర్తు చేయి.'; @override - String get firstCaptureWatch => 'Apple Watch'; + String get onboardingActDictateHint => 'అయ్యాక టిక్ నొక్కండి'; @override - String get firstCaptureSkip => 'Skip for Now'; + String get onboardingActPhoneMic => 'ఫోన్ మైక్రోఫోన్'; @override - String get firstCapturePrompt => - 'Tell Omi what you\'re working on today, or what\'s on your mind. Talk for about 30 seconds.'; + String get onboardingActWriting => 'సారాంశం తయారవుతోంది…'; @override - String get firstCaptureKeepGoing => 'Keep going… 30 seconds is ideal.'; + String get onboardingActRemembers => 'మీరు గుర్తుంచుకోనవసరం లేదు, Omi గుర్తుంచుకుంటుంది'; @override - String get firstCaptureReady => 'You can finish now, or keep talking.'; + String get onboardingActNotesLabel => 'మీ రోజు, సంక్షిప్తంగా'; @override - String get firstCaptureStart => 'Start Recording'; + String get onboardingActTasksLabel => 'Omi మీ పనులను గుర్తించింది'; @override - String get firstCaptureFinish => 'Finish Recording'; + String get onboardingActYourWords => 'మీరు చెప్పింది ఇది'; @override - String get firstCaptureListening => 'Listening… your words will appear here.'; + String onboardingActCongratsTitle(String name) { + return 'అంతా సిద్ధం, $name'; + } @override - String get firstCaptureProcessing => 'Creating your first conversation…'; + String get onboardingActRateTitle => 'ఇప్పటివరకు Omi నచ్చిందా?'; @override - String get firstCaptureResult => 'Your first conversation'; + String get onboardingActRateYes => 'Omiని రేట్ చేయండి'; @override - String get firstCaptureEmpty => 'I didn\'t catch enough speech, try again with a few sentences'; + String get onboardingWelcomeCardTitle => 'Omiకి స్వాగతం'; @override - String get watchCaptureTitle => 'Record from your Apple Watch'; + String get onboardingWelcomeCardBody => + 'Omi ధరించి మామూలుగా మాట్లాడండి. ప్రతి సంభాషణ ఇక్కడే నోట్స్, పనులు, జ్ఞాపకాలుగా మారుతుంది.'; @override - String get watchCaptureOpen => 'Open Omi on your Apple Watch.'; + String get onboardingActDemoTitle => 'సామ్‌తో లంచ్'; @override - String get watchCaptureRecord => 'Tap Record. Allow microphone access if asked.'; + String get onboardingActDemoSummary => + 'జూన్‌లో లిస్బన్‌కు వెళ్తున్న సామ్‌తో లంచ్. శుక్రవారం లోపు టీమ్‌కు డెక్ పంపాలి, ఇంటికి వెళ్తూ పాలు కొనాలి.'; @override - String get watchCaptureSync => 'Your recording syncs to Omi on this phone.'; + String get onboardingActDemoTask1 => 'శుక్రవారంలోగా టీమ్‌కి డెక్ పంపడం'; @override - String get pairingBluetoothOff => 'Turn on Bluetooth on your phone, then try again.'; + String get onboardingActDemoTask2 => 'ఇంటికి వెళ్లేటప్పుడు పాలు తీసుకోవడం'; @override - String get pairingBluetoothPermission => 'Allow Bluetooth access for Omi in Settings, then try again.'; + String get onboardingActJustNow => 'ఇప్పుడే'; @override - String get pairingNearbyPermission => 'Allow Nearby devices access for Omi in Settings, then try again.'; + String get onboardingActSavedToTasks => 'మీ పనుల్లో సేవ్ అయింది'; @override - String get pairingLocationPermission => - 'Allow location access for Omi in Settings so this phone can find your device, then try again.'; + String get onboardingConnectTapToConnect => 'కనెక్ట్ చేయడానికి మీ Omi ని ట్యాప్ చేయండి.'; @override - String get pairingNotFoundOrDropped => - 'Couldn\'t reach your Omi. Make sure it\'s charged, close to your phone, and its light is blinking, then try again.'; + String get onboardingActMicFallbackBody => 'మీ Omi ఇంకా ఆడియో పంపడం లేదు. ప్రస్తుతానికి మీ ఫోన్ వాడండి.'; @override - String get pairingBondRecovery => 'Forget Omi in your phone’s Bluetooth settings, then try pairing again.'; + String get onboardingActNoAudioOmi => 'మీ Omi వినిపించడం లేదు. అది ఆన్‌లో ఉందో, దగ్గర్లో ఉందో చూడండి.'; @override - String get pairingHowTo => 'How to pair'; + String get onboardingActNoAudioPhone => 'ఇంకా మీ గొంతు వినిపించడం లేదు. కొంచెం గట్టిగా మాట్లాడి చూడండి.'; + + @override + String get onboardingActDictateBodyOmi => 'మాట్లాడండి చాలు. మీ Omi వింటోంది.'; + + @override + String get voiceProfileCardDismissed => 'సెట్టింగ్‌లలో ఎప్పుడైనా వాయిస్ ప్రొఫైల్ సెటప్ చేయవచ్చు.'; + + @override + String get homeNewRecordingButton => 'కొత్త రికార్డింగ్'; + + @override + String get chatWelcomeMessage => + 'హాయ్, నేను Omi. మీరు మాట్లాడిన దేని గురించైనా అడగండి: ఎవరు ఏమన్నారు, మీరు ఏమి మాట ఇచ్చారు లేదా మీ రోజు సారాంశం. నేను పనులను జోడించగలను, మీ కోసం విషయాలు గుర్తుంచుకోగలను.'; + + @override + String get onboardingStarterTaskAsk => 'మీ రోజు గురించి Omi ని అడగండి'; + + @override + String get onboardingStarterTaskRecord => 'Omi తో ఒక సంభాషణను రికార్డ్ చేయండి'; + + @override + String get onboardingStarterTaskCheck => 'పూర్తి చేయడానికి ఈ పనిని టిక్ చేయండి'; + + @override + String get paywallRecommended => 'Recommended'; @override - String get firstCaptureWatchInstructions => - 'Open Omi on your Apple Watch and tap Record. Allow microphone access if asked. Your capture will sync to Omi on this phone.'; + String get paywallNoPlans => 'Plans aren\'t available right now. Try again in a minute.'; @override - String get firstCaptureWatchInstall => - 'In the Watch app on your iPhone, install Omi on your Apple Watch. Then open Omi on your watch and tap Record.'; + String get paywallOtherPlans => 'See all plans'; @override - String get firstCaptureWatchPair => - 'Pair your Apple Watch with this iPhone using the Watch app, then install and open Omi on your watch.'; + String get paywallComparePlans => 'See plans'; @override - String get firstCaptureUnavailable => - 'Recording could not start. Check microphone access and your connection, then try again.'; + String get paywallYourPlan => 'Your plan'; @override - String get firstCaptureDelayed => - 'Your recording is taking longer to process. You can check your conversations on Home or try again.'; + String get paywallNotNow => 'Not now'; @override - String get firstCaptureEmptyTitle => 'Let\'s try that again'; + String get paywallTryAgain => 'Try again'; @override - String get firstCaptureShortTitle => 'Finish This Recording?'; + String get paywallManageBilling => 'Manage billing'; @override - String get firstCaptureShortWarning => 'A few more sentences help Omi understand you. About 30 seconds is ideal.'; + String get paywallLoadError => 'Couldn\'t load plans. Check your connection and try again.'; @override - String get firstCaptureCancelTitle => 'Leave This Recording?'; + String get paywallPlans => 'Plans'; @override - String get firstCaptureCancelWarning => - 'Going back stops this recording. Keep talking to give Omi a few more sentences.'; + String get watchCaptureTitle => 'Record from your Apple Watch'; @override - String get firstCaptureSiriTip => 'Tip: Enable Omi in Shortcuts → Siri.'; + String get watchCaptureOpen => 'Open Omi on your Apple Watch.'; + + @override + String get watchCaptureRecord => 'Tap Record. Allow microphone access if asked.'; + + @override + String get watchCaptureSync => 'Your recording syncs to Omi on this phone.'; + + @override + String get pairingBluetoothOff => 'Turn on Bluetooth on your phone, then try again.'; + + @override + String get pairingBluetoothPermission => 'Allow Bluetooth access for Omi in Settings, then try again.'; + + @override + String get pairingNearbyPermission => 'Allow Nearby devices access for Omi in Settings, then try again.'; + + @override + String get pairingLocationPermission => + 'Allow location access for Omi in Settings so this phone can find your device, then try again.'; + + @override + String get pairingNotFoundOrDropped => + 'Couldn\'t reach your Omi. Make sure it\'s charged, close to your phone, and its light is blinking, then try again.'; + + @override + String get pairingBondRecovery => 'Forget Omi in your phone’s Bluetooth settings, then try pairing again.'; + + @override + String get pairingHowTo => 'How to pair'; @override String get paywallOutlineHeadline => 'ఇకపై ఒక్క మాట కూడా మిస్ అవ్వకండి.'; diff --git a/app/lib/l10n/app_localizations_th.dart b/app/lib/l10n/app_localizations_th.dart index c0494688b4b..f8caa94c085 100644 --- a/app/lib/l10n/app_localizations_th.dart +++ b/app/lib/l10n/app_localizations_th.dart @@ -1678,8 +1678,7 @@ class AppLocalizationsTh extends AppLocalizations { String get enableBluetooth => 'เปิดใช้งาน Bluetooth'; @override - String get bluetoothNeeded => - 'Omi ต้องการ Bluetooth เพื่อเชื่อมต่อกับอุปกรณ์สวมใส่ของคุณ กรุณาเปิดใช้งาน Bluetooth แล้วลองอีกครั้ง'; + String get bluetoothNeeded => 'Omi เชื่อมต่อกับโทรศัพท์ผ่าน Bluetooth เปิดเพื่อดำเนินการต่อ'; @override String get contactSupport => 'ติดต่อฝ่ายสนับสนุน?'; @@ -4865,7 +4864,7 @@ class AppLocalizationsTh extends AppLocalizations { 'เมื่อดำเนินการต่อ การสนทนา การบันทึก และข้อมูลส่วนบุคคลของคุณจะถูกจัดเก็บอย่างปลอดภัยบนเซิร์ฟเวอร์ของเรา การบันทึกเสียงและการถอดความของคุณจะถูกประมวลผลโดยบริการ AI ของบุคคลที่สาม (รวมถึง Deepgram สำหรับการถอดความ และ OpenAI สำหรับการวิเคราะห์) เพื่อมอบข้อมูลเชิงลึกที่ขับเคลื่อนด้วย AI และเปิดใช้งานคุณสมบัติทั้งหมดของแอป'; @override - String get tasksEmptyStateMessage => 'เริ่มการสนทนาเพื่อสร้างงาน'; + String get tasksEmptyStateMessage => 'สิ่งที่ต้องทำจากบทสนทนาของคุณจะแสดงที่นี่'; @override String get clearChatAction => 'ล้างแชท'; @@ -12974,154 +12973,205 @@ class AppLocalizationsTh extends AppLocalizations { } @override - String get paywallRecommended => 'Recommended'; + String get onboardingPermissionTapToFix => 'แตะเพื่อแก้ไข'; @override - String get paywallNoPlans => 'Plans aren\'t available right now. Try again in a minute.'; + String get onboardingNoOmiYet => 'ยังไม่มี Omi ใช่ไหม'; @override - String get paywallOtherPlans => 'See all plans'; + String get onboardingOmiOrdered => 'สั่งซื้อแล้ว'; @override - String get paywallComparePlans => 'See plans'; + String get onboardingOmiNotOrdered => 'ยังไม่ได้สั่งซื้อ'; @override - String get paywallYourPlan => 'Your plan'; + String get onboardingConnectOmi => 'เชื่อมต่อ Omi'; @override - String get paywallNotNow => 'Not now'; + String get consentShortMessage => 'Omi ใช้ AI เปลี่ยนบทสนทนาของคุณเป็นโน้ต คุณลบได้ทุกเมื่อ'; @override - String get paywallTryAgain => 'Try again'; + String consentAiProviders(String providers) { + return 'ประมวลผลด้วย $providers'; + } @override - String get paywallManageBilling => 'Manage billing'; + String get onboardingActIntro => 'มาลองใช้ Omi กัน'; @override - String get paywallLoadError => 'Couldn\'t load plans. Check your connection and try again.'; + String get onboardingActMicTitle => 'Omi ต้องใช้ไมโครโฟน'; @override - String get paywallPlans => 'Plans'; + String get onboardingActMicBody => 'เพื่อให้ Omi ได้ยินเมื่อคุณพูดด้วย'; @override - String get firstCaptureTitle => 'How will you capture?'; + String get onboardingActMicAllow => 'อนุญาตไมโครโฟน'; @override - String get firstCaptureDescription => 'Choose how you want to capture your first conversation.'; + String get onboardingActDictateTitle => 'เล่าวันของคุณให้ Omi ฟัง'; @override - String get firstCaptureOmi => 'I Have an Omi'; + String get onboardingActDictateBody => 'พูดแบบที่คุณถนัด ที่เหลือ Omi จัดการเอง'; @override - String get firstCapturePhone => 'This Phone'; + String get onboardingActDictateExample => + 'เพิ่งกินข้าวกลางวันกับแซม เธอจะย้ายไปลิสบอนเดือนมิถุนายน! เตือนฉันส่งสไลด์ให้ทีมภายในวันศุกร์ แล้วก็ซื้อนมตอนกลับบ้านด้วย'; @override - String get firstCaptureWatch => 'Apple Watch'; + String get onboardingActDictateHint => 'แตะเครื่องหมายถูกเมื่อพูดเสร็จ'; @override - String get firstCaptureSkip => 'Skip for Now'; + String get onboardingActPhoneMic => 'ไมโครโฟนโทรศัพท์'; @override - String get firstCapturePrompt => - 'Tell Omi what you\'re working on today, or what\'s on your mind. Talk for about 30 seconds.'; + String get onboardingActWriting => 'กำลังสรุป…'; @override - String get firstCaptureKeepGoing => 'Keep going… 30 seconds is ideal.'; + String get onboardingActRemembers => 'Omi จำให้ คุณไม่ต้องจำเอง'; @override - String get firstCaptureReady => 'You can finish now, or keep talking.'; + String get onboardingActNotesLabel => 'สรุปวันของคุณ'; @override - String get firstCaptureStart => 'Start Recording'; + String get onboardingActTasksLabel => 'Omi จับสิ่งที่ต้องทำให้แล้ว'; @override - String get firstCaptureFinish => 'Finish Recording'; + String get onboardingActYourWords => 'สิ่งที่คุณพูด'; @override - String get firstCaptureListening => 'Listening… your words will appear here.'; + String onboardingActCongratsTitle(String name) { + return 'พร้อมแล้ว $name'; + } @override - String get firstCaptureProcessing => 'Creating your first conversation…'; + String get onboardingActRateTitle => 'ชอบ Omi ไหม'; @override - String get firstCaptureResult => 'Your first conversation'; + String get onboardingActRateYes => 'ให้คะแนน Omi'; @override - String get firstCaptureEmpty => 'I didn\'t catch enough speech, try again with a few sentences'; + String get onboardingWelcomeCardTitle => 'ยินดีต้อนรับสู่ Omi'; @override - String get watchCaptureTitle => 'Record from your Apple Watch'; + String get onboardingWelcomeCardBody => 'สวม Omi แล้วคุยตามปกติ ทุกบทสนทนาจะกลายเป็นโน้ต งาน และความทรงจำตรงนี้'; @override - String get watchCaptureOpen => 'Open Omi on your Apple Watch.'; + String get onboardingActDemoTitle => 'มื้อกลางวันกับแซม'; @override - String get watchCaptureRecord => 'Tap Record. Allow microphone access if asked.'; + String get onboardingActDemoSummary => + 'ทานข้าวกลางวันกับแซมที่จะย้ายไปลิสบอนเดือนมิถุนายน คุณต้องส่งสไลด์ให้ทีมภายในวันศุกร์ และซื้อนมระหว่างทางกลับบ้าน'; @override - String get watchCaptureSync => 'Your recording syncs to Omi on this phone.'; + String get onboardingActDemoTask1 => 'ส่งสไลด์ให้ทีมภายในวันศุกร์'; @override - String get pairingBluetoothOff => 'Turn on Bluetooth on your phone, then try again.'; + String get onboardingActDemoTask2 => 'ซื้อนมตอนกลับบ้าน'; @override - String get pairingBluetoothPermission => 'Allow Bluetooth access for Omi in Settings, then try again.'; + String get onboardingActJustNow => 'เมื่อสักครู่'; @override - String get pairingNearbyPermission => 'Allow Nearby devices access for Omi in Settings, then try again.'; + String get onboardingActSavedToTasks => 'บันทึกไว้ในงานของคุณแล้ว'; @override - String get pairingLocationPermission => - 'Allow location access for Omi in Settings so this phone can find your device, then try again.'; + String get onboardingConnectTapToConnect => 'แตะ Omi ของคุณเพื่อเชื่อมต่อ'; @override - String get pairingNotFoundOrDropped => - 'Couldn\'t reach your Omi. Make sure it\'s charged, close to your phone, and its light is blinking, then try again.'; + String get onboardingActMicFallbackBody => 'Omi ยังไม่ส่งเสียงมา ใช้โทรศัพท์ไปก่อนนะ'; @override - String get pairingBondRecovery => 'Forget Omi in your phone’s Bluetooth settings, then try pairing again.'; + String get onboardingActNoAudioOmi => 'ไม่ได้ยินเสียงจาก Omi ตรวจดูว่าเปิดอยู่และอยู่ใกล้ๆ'; @override - String get pairingHowTo => 'How to pair'; + String get onboardingActNoAudioPhone => 'ยังไม่ได้ยินเสียงคุณ ลองพูดดังขึ้นอีกนิด'; + + @override + String get onboardingActDictateBodyOmi => 'พูดได้เลย Omi กำลังฟังอยู่'; + + @override + String get voiceProfileCardDismissed => 'คุณตั้งค่าโปรไฟล์เสียงได้ทุกเมื่อในการตั้งค่า'; + + @override + String get homeNewRecordingButton => 'บันทึกใหม่'; + + @override + String get chatWelcomeMessage => + 'สวัสดี ฉันคือ Omi ถามอะไรก็ได้เกี่ยวกับสิ่งที่คุณคุยไว้ เช่น ใครพูดอะไร คุณสัญญาอะไรไว้ หรือสรุปวันของคุณ ฉันยังเพิ่มสิ่งที่ต้องทำและจำเรื่องต่าง ๆ ให้คุณได้ด้วย'; + + @override + String get onboardingStarterTaskAsk => 'ถาม Omi เกี่ยวกับวันของคุณ'; + + @override + String get onboardingStarterTaskRecord => 'บันทึกบทสนทนาด้วย Omi'; + + @override + String get onboardingStarterTaskCheck => 'ติ๊กงานนี้เพื่อทำให้เสร็จ'; + + @override + String get paywallRecommended => 'Recommended'; @override - String get firstCaptureWatchInstructions => - 'Open Omi on your Apple Watch and tap Record. Allow microphone access if asked. Your capture will sync to Omi on this phone.'; + String get paywallNoPlans => 'Plans aren\'t available right now. Try again in a minute.'; @override - String get firstCaptureWatchInstall => - 'In the Watch app on your iPhone, install Omi on your Apple Watch. Then open Omi on your watch and tap Record.'; + String get paywallOtherPlans => 'See all plans'; @override - String get firstCaptureWatchPair => - 'Pair your Apple Watch with this iPhone using the Watch app, then install and open Omi on your watch.'; + String get paywallComparePlans => 'See plans'; @override - String get firstCaptureUnavailable => - 'Recording could not start. Check microphone access and your connection, then try again.'; + String get paywallYourPlan => 'Your plan'; @override - String get firstCaptureDelayed => - 'Your recording is taking longer to process. You can check your conversations on Home or try again.'; + String get paywallNotNow => 'Not now'; @override - String get firstCaptureEmptyTitle => 'Let\'s try that again'; + String get paywallTryAgain => 'Try again'; @override - String get firstCaptureShortTitle => 'Finish This Recording?'; + String get paywallManageBilling => 'Manage billing'; @override - String get firstCaptureShortWarning => 'A few more sentences help Omi understand you. About 30 seconds is ideal.'; + String get paywallLoadError => 'Couldn\'t load plans. Check your connection and try again.'; @override - String get firstCaptureCancelTitle => 'Leave This Recording?'; + String get paywallPlans => 'Plans'; @override - String get firstCaptureCancelWarning => - 'Going back stops this recording. Keep talking to give Omi a few more sentences.'; + String get watchCaptureTitle => 'Record from your Apple Watch'; @override - String get firstCaptureSiriTip => 'Tip: Enable Omi in Shortcuts → Siri.'; + String get watchCaptureOpen => 'Open Omi on your Apple Watch.'; + + @override + String get watchCaptureRecord => 'Tap Record. Allow microphone access if asked.'; + + @override + String get watchCaptureSync => 'Your recording syncs to Omi on this phone.'; + + @override + String get pairingBluetoothOff => 'Turn on Bluetooth on your phone, then try again.'; + + @override + String get pairingBluetoothPermission => 'Allow Bluetooth access for Omi in Settings, then try again.'; + + @override + String get pairingNearbyPermission => 'Allow Nearby devices access for Omi in Settings, then try again.'; + + @override + String get pairingLocationPermission => + 'Allow location access for Omi in Settings so this phone can find your device, then try again.'; + + @override + String get pairingNotFoundOrDropped => + 'Couldn\'t reach your Omi. Make sure it\'s charged, close to your phone, and its light is blinking, then try again.'; + + @override + String get pairingBondRecovery => 'Forget Omi in your phone’s Bluetooth settings, then try pairing again.'; + + @override + String get pairingHowTo => 'How to pair'; @override String get paywallOutlineHeadline => 'ไม่พลาดแม้แต่คำเดียวอีกต่อไป'; diff --git a/app/lib/l10n/app_localizations_tl.dart b/app/lib/l10n/app_localizations_tl.dart index 969f03080c2..e14bc8df263 100644 --- a/app/lib/l10n/app_localizations_tl.dart +++ b/app/lib/l10n/app_localizations_tl.dart @@ -1695,8 +1695,7 @@ class AppLocalizationsTl extends AppLocalizations { String get enableBluetooth => 'Paganahin ang Bluetooth'; @override - String get bluetoothNeeded => - 'Ang Omi ay kailangan ng Bluetooth upang kumonekta sa iyong wearable. Mangyaring paganahin ang Bluetooth at subukan muli.'; + String get bluetoothNeeded => 'Kumokonekta ang Omi sa iyong phone gamit ang Bluetooth. I-on ito para magpatuloy.'; @override String get contactSupport => 'Makipag-ugnayan sa Support?'; @@ -4934,7 +4933,7 @@ class AppLocalizationsTl extends AppLocalizations { 'Sa pagpapatuloy, ang iyong mga pag-uusap, recording, at personal na impormasyon ay ligtas na maiimbak sa aming mga server. Ang iyong mga audio recording at transcript ay pinoproseso ng third-party na mga serbisyo ng AI (kabilang ang Deepgram para sa transcription at OpenAI para sa analysis) upang mabigyan ka ng AI-powered na mga insight at ma-enable ang lahat ng feature ng app.'; @override - String get tasksEmptyStateMessage => 'Magsimula ng pag-uusap para gumawa ng gawain.'; + String get tasksEmptyStateMessage => 'Lalabas dito ang mga gagawin mula sa iyong mga usapan.'; @override String get clearChatAction => 'Burahin ang Chat'; @@ -13145,154 +13144,207 @@ class AppLocalizationsTl extends AppLocalizations { } @override - String get paywallRecommended => 'Recommended'; + String get onboardingPermissionTapToFix => 'I-tap para ayusin'; @override - String get paywallNoPlans => 'Plans aren\'t available right now. Try again in a minute.'; + String get onboardingNoOmiYet => 'Wala ka pang Omi?'; @override - String get paywallOtherPlans => 'See all plans'; + String get onboardingOmiOrdered => 'Na-order ko na'; @override - String get paywallComparePlans => 'See plans'; + String get onboardingOmiNotOrdered => 'Hindi pa naka-order'; @override - String get paywallYourPlan => 'Your plan'; + String get onboardingConnectOmi => 'Ikonekta ang Omi'; @override - String get paywallNotNow => 'Not now'; + String get consentShortMessage => + 'Ginagamit ng Omi ang AI para gawing notes ang iyong mga usapan. Puwede mo itong burahin anumang oras.'; @override - String get paywallTryAgain => 'Try again'; + String consentAiProviders(String providers) { + return 'Pinoproseso gamit ang $providers.'; + } @override - String get paywallManageBilling => 'Manage billing'; + String get onboardingActIntro => 'Subukan natin ang Omi.'; @override - String get paywallLoadError => 'Couldn\'t load plans. Check your connection and try again.'; + String get onboardingActMicTitle => 'Kailangan ng Omi ang mikropono mo'; @override - String get paywallPlans => 'Plans'; + String get onboardingActMicBody => 'Para marinig ka ng Omi kapag kinakausap mo ito.'; @override - String get firstCaptureTitle => 'How will you capture?'; + String get onboardingActMicAllow => 'Payagan ang Mikropono'; @override - String get firstCaptureDescription => 'Choose how you want to capture your first conversation.'; + String get onboardingActDictateTitle => 'Ikuwento sa Omi ang araw mo'; @override - String get firstCaptureOmi => 'I Have an Omi'; + String get onboardingActDictateBody => 'Sabihin mo sa sarili mong paraan. Ang Omi na ang bahala.'; @override - String get firstCapturePhone => 'This Phone'; + String get onboardingActDictateExample => + 'Kakatapos ko lang mag-lunch kasama si Sam, lilipat na siya sa Lisbon sa Hunyo! Ipaalala mo sa akin na ipadala ang deck sa team bago mag-Biyernes, at bumili ng gatas pauwi.'; @override - String get firstCaptureWatch => 'Apple Watch'; + String get onboardingActDictateHint => 'I-tap ang check kapag tapos ka na'; @override - String get firstCaptureSkip => 'Skip for Now'; + String get onboardingActPhoneMic => 'Mikropono ng phone'; @override - String get firstCapturePrompt => - 'Tell Omi what you\'re working on today, or what\'s on your mind. Talk for about 30 seconds.'; + String get onboardingActWriting => 'Binubuod…'; @override - String get firstCaptureKeepGoing => 'Keep going… 30 seconds is ideal.'; + String get onboardingActRemembers => 'Tinatandaan ng Omi para hindi mo na kailangan'; @override - String get firstCaptureReady => 'You can finish now, or keep talking.'; + String get onboardingActNotesLabel => 'Ang araw mo, buod'; @override - String get firstCaptureStart => 'Start Recording'; + String get onboardingActTasksLabel => 'Nahuli ng Omi ang mga gagawin mo'; @override - String get firstCaptureFinish => 'Finish Recording'; + String get onboardingActYourWords => 'Ito ang sinabi mo'; @override - String get firstCaptureListening => 'Listening… your words will appear here.'; + String onboardingActCongratsTitle(String name) { + return 'Handa ka na, $name'; + } @override - String get firstCaptureProcessing => 'Creating your first conversation…'; + String get onboardingActRateTitle => 'Gusto mo ba ang Omi sa ngayon?'; @override - String get firstCaptureResult => 'Your first conversation'; + String get onboardingActRateYes => 'I-rate ang Omi'; @override - String get firstCaptureEmpty => 'I didn\'t catch enough speech, try again with a few sentences'; + String get onboardingWelcomeCardTitle => 'Maligayang pagdating sa Omi'; @override - String get watchCaptureTitle => 'Record from your Apple Watch'; + String get onboardingWelcomeCardBody => + 'Isuot ang Omi at makipag-usap gaya ng dati. Nagiging tala, gawain, at alaala dito ang bawat usapan.'; @override - String get watchCaptureOpen => 'Open Omi on your Apple Watch.'; + String get onboardingActDemoTitle => 'Lunch kasama si Sam'; @override - String get watchCaptureRecord => 'Tap Record. Allow microphone access if asked.'; + String get onboardingActDemoSummary => + 'Tanghalian kasama si Sam, na lilipat sa Lisbon sa Hunyo. Gusto mong ipadala ang deck sa team bago mag-Biyernes at bumili ng gatas pauwi.'; @override - String get watchCaptureSync => 'Your recording syncs to Omi on this phone.'; + String get onboardingActDemoTask1 => 'Ipadala ang deck sa team bago mag-Biyernes'; @override - String get pairingBluetoothOff => 'Turn on Bluetooth on your phone, then try again.'; + String get onboardingActDemoTask2 => 'Bumili ng gatas pauwi'; @override - String get pairingBluetoothPermission => 'Allow Bluetooth access for Omi in Settings, then try again.'; + String get onboardingActJustNow => 'Ngayon lang'; @override - String get pairingNearbyPermission => 'Allow Nearby devices access for Omi in Settings, then try again.'; + String get onboardingActSavedToTasks => 'Na-save sa mga gawain mo'; @override - String get pairingLocationPermission => - 'Allow location access for Omi in Settings so this phone can find your device, then try again.'; + String get onboardingConnectTapToConnect => 'I-tap ang iyong Omi para kumonekta.'; @override - String get pairingNotFoundOrDropped => - 'Couldn\'t reach your Omi. Make sure it\'s charged, close to your phone, and its light is blinking, then try again.'; + String get onboardingActMicFallbackBody => 'Hindi pa nagpapadala ng audio ang Omi mo. Gamitin muna ang iyong phone.'; @override - String get pairingBondRecovery => 'Forget Omi in your phone’s Bluetooth settings, then try pairing again.'; + String get onboardingActNoAudioOmi => 'Hindi marinig ang Omi mo. Siguraduhing naka-on ito at malapit.'; @override - String get pairingHowTo => 'How to pair'; + String get onboardingActNoAudioPhone => 'Hindi ka pa marinig. Subukang magsalita nang medyo malakas.'; + + @override + String get onboardingActDictateBodyOmi => 'Magsalita ka lang. Nakikinig ang Omi mo.'; + + @override + String get voiceProfileCardDismissed => 'Maaari mong i-set up ang Voice Profile anumang oras sa Settings.'; + + @override + String get homeNewRecordingButton => 'Bagong Recording'; + + @override + String get chatWelcomeMessage => + 'Hi, ako si Omi. Itanong mo sa akin ang kahit ano tungkol sa mga napag-usapan mo: ang sinabi ng isang tao, ang ipinangako mo, o buod ng araw mo. Kaya ko ring magdagdag ng mga gawain at tandaan ang mga bagay para sa iyo.'; + + @override + String get onboardingStarterTaskAsk => 'Tanungin si Omi tungkol sa araw mo'; + + @override + String get onboardingStarterTaskRecord => 'Mag-record ng usapan gamit ang Omi'; + + @override + String get onboardingStarterTaskCheck => 'Lagyan ng tsek ang gawaing ito para matapos'; + + @override + String get paywallRecommended => 'Recommended'; @override - String get firstCaptureWatchInstructions => - 'Open Omi on your Apple Watch and tap Record. Allow microphone access if asked. Your capture will sync to Omi on this phone.'; + String get paywallNoPlans => 'Plans aren\'t available right now. Try again in a minute.'; @override - String get firstCaptureWatchInstall => - 'In the Watch app on your iPhone, install Omi on your Apple Watch. Then open Omi on your watch and tap Record.'; + String get paywallOtherPlans => 'See all plans'; @override - String get firstCaptureWatchPair => - 'Pair your Apple Watch with this iPhone using the Watch app, then install and open Omi on your watch.'; + String get paywallComparePlans => 'See plans'; @override - String get firstCaptureUnavailable => - 'Recording could not start. Check microphone access and your connection, then try again.'; + String get paywallYourPlan => 'Your plan'; @override - String get firstCaptureDelayed => - 'Your recording is taking longer to process. You can check your conversations on Home or try again.'; + String get paywallNotNow => 'Not now'; @override - String get firstCaptureEmptyTitle => 'Let\'s try that again'; + String get paywallTryAgain => 'Try again'; @override - String get firstCaptureShortTitle => 'Finish This Recording?'; + String get paywallManageBilling => 'Manage billing'; @override - String get firstCaptureShortWarning => 'A few more sentences help Omi understand you. About 30 seconds is ideal.'; + String get paywallLoadError => 'Couldn\'t load plans. Check your connection and try again.'; @override - String get firstCaptureCancelTitle => 'Leave This Recording?'; + String get paywallPlans => 'Plans'; @override - String get firstCaptureCancelWarning => - 'Going back stops this recording. Keep talking to give Omi a few more sentences.'; + String get watchCaptureTitle => 'Record from your Apple Watch'; @override - String get firstCaptureSiriTip => 'Tip: Enable Omi in Shortcuts → Siri.'; + String get watchCaptureOpen => 'Open Omi on your Apple Watch.'; + + @override + String get watchCaptureRecord => 'Tap Record. Allow microphone access if asked.'; + + @override + String get watchCaptureSync => 'Your recording syncs to Omi on this phone.'; + + @override + String get pairingBluetoothOff => 'Turn on Bluetooth on your phone, then try again.'; + + @override + String get pairingBluetoothPermission => 'Allow Bluetooth access for Omi in Settings, then try again.'; + + @override + String get pairingNearbyPermission => 'Allow Nearby devices access for Omi in Settings, then try again.'; + + @override + String get pairingLocationPermission => + 'Allow location access for Omi in Settings so this phone can find your device, then try again.'; + + @override + String get pairingNotFoundOrDropped => + 'Couldn\'t reach your Omi. Make sure it\'s charged, close to your phone, and its light is blinking, then try again.'; + + @override + String get pairingBondRecovery => 'Forget Omi in your phone’s Bluetooth settings, then try pairing again.'; + + @override + String get pairingHowTo => 'How to pair'; @override String get paywallOutlineHeadline => 'Huwag nang palampasin ang kahit isang salita.'; diff --git a/app/lib/l10n/app_localizations_tr.dart b/app/lib/l10n/app_localizations_tr.dart index 2bda1a6ec31..57471b0ee6d 100644 --- a/app/lib/l10n/app_localizations_tr.dart +++ b/app/lib/l10n/app_localizations_tr.dart @@ -1690,8 +1690,7 @@ class AppLocalizationsTr extends AppLocalizations { String get enableBluetooth => 'Bluetooth\'u Etkinleştir'; @override - String get bluetoothNeeded => - 'Omi\'nin giyilebilir cihazınıza bağlanması için Bluetooth gereklidir. Lütfen Bluetooth\'u etkinleştirin ve tekrar deneyin.'; + String get bluetoothNeeded => 'Omi telefonuna Bluetooth ile bağlanır. Devam etmek için açın.'; @override String get contactSupport => 'Desteğe Başvur?'; @@ -4901,7 +4900,7 @@ class AppLocalizationsTr extends AppLocalizations { 'Devam ederek, konuşmalarınız, kayıtlarınız ve kişisel bilgileriniz sunucularımızda güvenli bir şekilde saklanacaktır. Ses kayıtlarınız ve transkriptleriniz, size yapay zeka destekli içgörüler sağlamak ve tüm uygulama özelliklerini etkinleştirmek için üçüncü taraf yapay zeka hizmetleri (transkripsiyon için Deepgram ve analiz için OpenAI dahil) tarafından işlenir.'; @override - String get tasksEmptyStateMessage => 'Görev oluşturmak için bir konuşma başlatın.'; + String get tasksEmptyStateMessage => 'Konuşmalarındaki yapılacaklar burada görünecek.'; @override String get clearChatAction => 'Sohbeti temizle'; @@ -13053,154 +13052,207 @@ class AppLocalizationsTr extends AppLocalizations { } @override - String get paywallRecommended => 'Recommended'; + String get onboardingPermissionTapToFix => 'Düzeltmek için dokunun'; @override - String get paywallNoPlans => 'Plans aren\'t available right now. Try again in a minute.'; + String get onboardingNoOmiYet => 'Omi\'n henüz yok mu?'; @override - String get paywallOtherPlans => 'See all plans'; + String get onboardingOmiOrdered => 'Sipariş verdim'; @override - String get paywallComparePlans => 'See plans'; + String get onboardingOmiNotOrdered => 'Henüz sipariş vermedim'; @override - String get paywallYourPlan => 'Your plan'; + String get onboardingConnectOmi => 'Omi\'yi bağla'; @override - String get paywallNotNow => 'Not now'; + String get consentShortMessage => + 'Omi, konuşmalarını yapay zekâ ile notlara dönüştürür. İstediğin zaman silebilirsin.'; @override - String get paywallTryAgain => 'Try again'; + String consentAiProviders(String providers) { + return '$providers ile işlenir.'; + } @override - String get paywallManageBilling => 'Manage billing'; + String get onboardingActIntro => 'Hadi Omi\'yi deneyelim.'; @override - String get paywallLoadError => 'Couldn\'t load plans. Check your connection and try again.'; + String get onboardingActMicTitle => 'Omi\'nin mikrofonuna ihtiyacı var'; @override - String get paywallPlans => 'Plans'; + String get onboardingActMicBody => 'Omi\'yle konuştuğunda seni duyabilmesi için.'; @override - String get firstCaptureTitle => 'How will you capture?'; + String get onboardingActMicAllow => 'Mikrofona İzin Ver'; @override - String get firstCaptureDescription => 'Choose how you want to capture your first conversation.'; + String get onboardingActDictateTitle => 'Omi\'ye gününü anlat'; @override - String get firstCaptureOmi => 'I Have an Omi'; + String get onboardingActDictateBody => 'Kendi tarzınla söyle. Gerisini Omi halleder.'; @override - String get firstCapturePhone => 'This Phone'; + String get onboardingActDictateExample => + 'Az önce Sam\'le öğle yemeği yedim, haziranda Lizbon\'a taşınıyor! Cumaya kadar sunumu ekibe göndermemi ve eve dönerken süt almamı hatırlat.'; @override - String get firstCaptureWatch => 'Apple Watch'; + String get onboardingActDictateHint => 'Bitince onay işaretine dokun'; @override - String get firstCaptureSkip => 'Skip for Now'; + String get onboardingActPhoneMic => 'Telefon mikrofonu'; @override - String get firstCapturePrompt => - 'Tell Omi what you\'re working on today, or what\'s on your mind. Talk for about 30 seconds.'; + String get onboardingActWriting => 'Özetleniyor…'; @override - String get firstCaptureKeepGoing => 'Keep going… 30 seconds is ideal.'; + String get onboardingActRemembers => 'Omi hatırlar, senin hatırlamana gerek kalmaz'; @override - String get firstCaptureReady => 'You can finish now, or keep talking.'; + String get onboardingActNotesLabel => 'Günün, özetle'; @override - String get firstCaptureStart => 'Start Recording'; + String get onboardingActTasksLabel => 'Omi yapılacaklarını yakaladı'; @override - String get firstCaptureFinish => 'Finish Recording'; + String get onboardingActYourWords => 'Söylediklerin'; @override - String get firstCaptureListening => 'Listening… your words will appear here.'; + String onboardingActCongratsTitle(String name) { + return 'Her şey hazır, $name'; + } @override - String get firstCaptureProcessing => 'Creating your first conversation…'; + String get onboardingActRateTitle => 'Omi\'yi beğendin mi?'; @override - String get firstCaptureResult => 'Your first conversation'; + String get onboardingActRateYes => 'Omi\'yi Puanla'; @override - String get firstCaptureEmpty => 'I didn\'t catch enough speech, try again with a few sentences'; + String get onboardingWelcomeCardTitle => 'Omi\'ye hoş geldin'; @override - String get watchCaptureTitle => 'Record from your Apple Watch'; + String get onboardingWelcomeCardBody => + 'Omi\'yi tak ve her zamanki gibi konuş. Her konuşma burada nota, göreve ve anıya dönüşür.'; @override - String get watchCaptureOpen => 'Open Omi on your Apple Watch.'; + String get onboardingActDemoTitle => 'Sam\'le öğle yemeği'; @override - String get watchCaptureRecord => 'Tap Record. Allow microphone access if asked.'; + String get onboardingActDemoSummary => + 'Haziranda Lizbon\'a taşınan Sam\'le öğle yemeği. Cumaya kadar sunumu ekibine göndermek ve eve dönerken süt almak istiyorsun.'; @override - String get watchCaptureSync => 'Your recording syncs to Omi on this phone.'; + String get onboardingActDemoTask1 => 'Cumaya kadar sunumu ekibe gönder'; @override - String get pairingBluetoothOff => 'Turn on Bluetooth on your phone, then try again.'; + String get onboardingActDemoTask2 => 'Eve dönerken süt al'; @override - String get pairingBluetoothPermission => 'Allow Bluetooth access for Omi in Settings, then try again.'; + String get onboardingActJustNow => 'Az önce'; @override - String get pairingNearbyPermission => 'Allow Nearby devices access for Omi in Settings, then try again.'; + String get onboardingActSavedToTasks => 'Görevlerine kaydedildi'; @override - String get pairingLocationPermission => - 'Allow location access for Omi in Settings so this phone can find your device, then try again.'; + String get onboardingConnectTapToConnect => 'Bağlamak için Omi\'ne dokun.'; @override - String get pairingNotFoundOrDropped => - 'Couldn\'t reach your Omi. Make sure it\'s charged, close to your phone, and its light is blinking, then try again.'; + String get onboardingActMicFallbackBody => 'Omi\'n henüz ses göndermiyor. Şimdilik telefonunu kullan.'; @override - String get pairingBondRecovery => 'Forget Omi in your phone’s Bluetooth settings, then try pairing again.'; + String get onboardingActNoAudioOmi => 'Omi\'n duyulmuyor. Açık ve yakında olduğundan emin ol.'; @override - String get pairingHowTo => 'How to pair'; + String get onboardingActNoAudioPhone => 'Henüz seni duyamıyoruz. Biraz daha yüksek sesle konuşmayı dene.'; + + @override + String get onboardingActDictateBodyOmi => 'Sadece konuş. Omi\'n dinliyor.'; + + @override + String get voiceProfileCardDismissed => 'Ses profilini istediğin zaman Ayarlar\'dan kurabilirsin.'; + + @override + String get homeNewRecordingButton => 'Yeni Kayıt'; + + @override + String get chatWelcomeMessage => + 'Merhaba, ben Omi. Konuştuğun her şeyi bana sorabilirsin: birinin ne dediğini, ne söz verdiğini ya da gününün özetini. Görev de ekleyebilir ve senin için bir şeyleri hatırlayabilirim.'; + + @override + String get onboardingStarterTaskAsk => 'Omi\'ye gününü sor'; + + @override + String get onboardingStarterTaskRecord => 'Omi ile bir konuşma kaydet'; + + @override + String get onboardingStarterTaskCheck => 'Bu görevi tamamlamak için işaretle'; + + @override + String get paywallRecommended => 'Recommended'; @override - String get firstCaptureWatchInstructions => - 'Open Omi on your Apple Watch and tap Record. Allow microphone access if asked. Your capture will sync to Omi on this phone.'; + String get paywallNoPlans => 'Plans aren\'t available right now. Try again in a minute.'; @override - String get firstCaptureWatchInstall => - 'In the Watch app on your iPhone, install Omi on your Apple Watch. Then open Omi on your watch and tap Record.'; + String get paywallOtherPlans => 'See all plans'; @override - String get firstCaptureWatchPair => - 'Pair your Apple Watch with this iPhone using the Watch app, then install and open Omi on your watch.'; + String get paywallComparePlans => 'See plans'; @override - String get firstCaptureUnavailable => - 'Recording could not start. Check microphone access and your connection, then try again.'; + String get paywallYourPlan => 'Your plan'; @override - String get firstCaptureDelayed => - 'Your recording is taking longer to process. You can check your conversations on Home or try again.'; + String get paywallNotNow => 'Not now'; @override - String get firstCaptureEmptyTitle => 'Let\'s try that again'; + String get paywallTryAgain => 'Try again'; @override - String get firstCaptureShortTitle => 'Finish This Recording?'; + String get paywallManageBilling => 'Manage billing'; @override - String get firstCaptureShortWarning => 'A few more sentences help Omi understand you. About 30 seconds is ideal.'; + String get paywallLoadError => 'Couldn\'t load plans. Check your connection and try again.'; @override - String get firstCaptureCancelTitle => 'Leave This Recording?'; + String get paywallPlans => 'Plans'; @override - String get firstCaptureCancelWarning => - 'Going back stops this recording. Keep talking to give Omi a few more sentences.'; + String get watchCaptureTitle => 'Record from your Apple Watch'; @override - String get firstCaptureSiriTip => 'Tip: Enable Omi in Shortcuts → Siri.'; + String get watchCaptureOpen => 'Open Omi on your Apple Watch.'; + + @override + String get watchCaptureRecord => 'Tap Record. Allow microphone access if asked.'; + + @override + String get watchCaptureSync => 'Your recording syncs to Omi on this phone.'; + + @override + String get pairingBluetoothOff => 'Turn on Bluetooth on your phone, then try again.'; + + @override + String get pairingBluetoothPermission => 'Allow Bluetooth access for Omi in Settings, then try again.'; + + @override + String get pairingNearbyPermission => 'Allow Nearby devices access for Omi in Settings, then try again.'; + + @override + String get pairingLocationPermission => + 'Allow location access for Omi in Settings so this phone can find your device, then try again.'; + + @override + String get pairingNotFoundOrDropped => + 'Couldn\'t reach your Omi. Make sure it\'s charged, close to your phone, and its light is blinking, then try again.'; + + @override + String get pairingBondRecovery => 'Forget Omi in your phone’s Bluetooth settings, then try pairing again.'; + + @override + String get pairingHowTo => 'How to pair'; @override String get paywallOutlineHeadline => 'Bir daha tek kelimeyi bile kaçırma.'; diff --git a/app/lib/l10n/app_localizations_uk.dart b/app/lib/l10n/app_localizations_uk.dart index 37349db1f20..1cf9c4dcf6f 100644 --- a/app/lib/l10n/app_localizations_uk.dart +++ b/app/lib/l10n/app_localizations_uk.dart @@ -1689,8 +1689,7 @@ class AppLocalizationsUk extends AppLocalizations { String get enableBluetooth => 'Увімкнути Bluetooth'; @override - String get bluetoothNeeded => - 'Omi потребує Bluetooth для підключення до вашого носимого пристрою. Будь ласка, увімкніть Bluetooth та спробуйте ще раз.'; + String get bluetoothNeeded => 'Omi підключається до телефона через Bluetooth. Увімкніть його, щоб продовжити.'; @override String get contactSupport => 'Зв\'язатися з підтримкою?'; @@ -4899,7 +4898,7 @@ class AppLocalizationsUk extends AppLocalizations { 'Продовжуючи, ваші розмови, записи та особиста інформація будуть надійно зберігатися на наших серверах. Ваші аудіозаписи та транскрипції обробляються сторонніми AI-сервісами (включаючи Deepgram для транскрипції та OpenAI для аналізу), щоб надати вам аналітику на основі ШІ та увімкнути всі функції додатку.'; @override - String get tasksEmptyStateMessage => 'Почніть розмову, щоб створити завдання.'; + String get tasksEmptyStateMessage => 'Завдання з ваших розмов з\'являться тут.'; @override String get clearChatAction => 'Очистити чат'; @@ -13070,154 +13069,207 @@ class AppLocalizationsUk extends AppLocalizations { } @override - String get paywallRecommended => 'Recommended'; + String get onboardingPermissionTapToFix => 'Торкніться, щоб виправити'; @override - String get paywallNoPlans => 'Plans aren\'t available right now. Try again in a minute.'; + String get onboardingNoOmiYet => 'Ще немає Omi?'; @override - String get paywallOtherPlans => 'See all plans'; + String get onboardingOmiOrdered => 'Я вже замовив'; @override - String get paywallComparePlans => 'See plans'; + String get onboardingOmiNotOrdered => 'Ще не замовив'; @override - String get paywallYourPlan => 'Your plan'; + String get onboardingConnectOmi => 'Підключіть Omi'; @override - String get paywallNotNow => 'Not now'; + String get consentShortMessage => + 'Omi за допомогою ШІ перетворює ваші розмови на нотатки. Їх можна видалити будь-коли.'; @override - String get paywallTryAgain => 'Try again'; + String consentAiProviders(String providers) { + return 'Обробка: $providers.'; + } @override - String get paywallManageBilling => 'Manage billing'; + String get onboardingActIntro => 'Спробуймо Omi.'; @override - String get paywallLoadError => 'Couldn\'t load plans. Check your connection and try again.'; + String get onboardingActMicTitle => 'Omi потрібен мікрофон'; @override - String get paywallPlans => 'Plans'; + String get onboardingActMicBody => 'Щоб Omi чув вас, коли ви з ним говорите.'; @override - String get firstCaptureTitle => 'How will you capture?'; + String get onboardingActMicAllow => 'Дозволити мікрофон'; @override - String get firstCaptureDescription => 'Choose how you want to capture your first conversation.'; + String get onboardingActDictateTitle => 'Розкажіть Omi про свій день'; @override - String get firstCaptureOmi => 'I Have an Omi'; + String get onboardingActDictateBody => 'Кажіть по-своєму. Решту зробить Omi.'; @override - String get firstCapturePhone => 'This Phone'; + String get onboardingActDictateExample => + 'Щойно пообідав із Сем, у червні вона переїжджає до Лісабона! Нагадай надіслати презентацію команді до п\'ятниці й купити молоко дорогою додому.'; @override - String get firstCaptureWatch => 'Apple Watch'; + String get onboardingActDictateHint => 'Торкніться галочки, коли закінчите'; @override - String get firstCaptureSkip => 'Skip for Now'; + String get onboardingActPhoneMic => 'Мікрофон телефона'; @override - String get firstCapturePrompt => - 'Tell Omi what you\'re working on today, or what\'s on your mind. Talk for about 30 seconds.'; + String get onboardingActWriting => 'Підсумовуємо…'; @override - String get firstCaptureKeepGoing => 'Keep going… 30 seconds is ideal.'; + String get onboardingActRemembers => 'Omi пам\'ятає, щоб вам не доводилося'; @override - String get firstCaptureReady => 'You can finish now, or keep talking.'; + String get onboardingActNotesLabel => 'Ваш день коротко'; @override - String get firstCaptureStart => 'Start Recording'; + String get onboardingActTasksLabel => 'Omi помітив ваші справи'; @override - String get firstCaptureFinish => 'Finish Recording'; + String get onboardingActYourWords => 'Ось що ви сказали'; @override - String get firstCaptureListening => 'Listening… your words will appear here.'; + String onboardingActCongratsTitle(String name) { + return 'Усе готово, $name'; + } @override - String get firstCaptureProcessing => 'Creating your first conversation…'; + String get onboardingActRateTitle => 'Подобається Omi?'; @override - String get firstCaptureResult => 'Your first conversation'; + String get onboardingActRateYes => 'Оцінити Omi'; @override - String get firstCaptureEmpty => 'I didn\'t catch enough speech, try again with a few sentences'; + String get onboardingWelcomeCardTitle => 'Вітаємо в Omi'; @override - String get watchCaptureTitle => 'Record from your Apple Watch'; + String get onboardingWelcomeCardBody => + 'Носіть Omi й говоріть як завжди. Кожна розмова стане тут нотатками, завданнями й спогадами.'; @override - String get watchCaptureOpen => 'Open Omi on your Apple Watch.'; + String get onboardingActDemoTitle => 'Обід із Сем'; @override - String get watchCaptureRecord => 'Tap Record. Allow microphone access if asked.'; + String get onboardingActDemoSummary => + 'Обід із Сем, яка в червні переїжджає до Лісабона. Треба надіслати презентацію команді до пʼятниці й купити молоко дорогою додому.'; @override - String get watchCaptureSync => 'Your recording syncs to Omi on this phone.'; + String get onboardingActDemoTask1 => 'Надіслати презентацію команді до п\'ятниці'; @override - String get pairingBluetoothOff => 'Turn on Bluetooth on your phone, then try again.'; + String get onboardingActDemoTask2 => 'Купити молоко дорогою додому'; @override - String get pairingBluetoothPermission => 'Allow Bluetooth access for Omi in Settings, then try again.'; + String get onboardingActJustNow => 'Щойно'; @override - String get pairingNearbyPermission => 'Allow Nearby devices access for Omi in Settings, then try again.'; + String get onboardingActSavedToTasks => 'Збережено у ваші завдання'; @override - String get pairingLocationPermission => - 'Allow location access for Omi in Settings so this phone can find your device, then try again.'; + String get onboardingConnectTapToConnect => 'Торкніться Omi, щоб підключити.'; @override - String get pairingNotFoundOrDropped => - 'Couldn\'t reach your Omi. Make sure it\'s charged, close to your phone, and its light is blinking, then try again.'; + String get onboardingActMicFallbackBody => 'Omi ще не передає звук. Поки що скористайтеся телефоном.'; @override - String get pairingBondRecovery => 'Forget Omi in your phone’s Bluetooth settings, then try pairing again.'; + String get onboardingActNoAudioOmi => 'Omi не чути. Перевірте, що він увімкнений і поруч.'; @override - String get pairingHowTo => 'How to pair'; + String get onboardingActNoAudioPhone => 'Поки вас не чути. Спробуйте говорити трохи голосніше.'; + + @override + String get onboardingActDictateBodyOmi => 'Просто говоріть. Omi слухає.'; + + @override + String get voiceProfileCardDismissed => 'Голосовий профіль можна налаштувати будь-коли в Налаштуваннях.'; + + @override + String get homeNewRecordingButton => 'Новий запис'; + + @override + String get chatWelcomeMessage => + 'Привіт, я Omi. Питайте про все, про що ви говорили: що хтось сказав, що ви пообіцяли або як минув ваш день. Я також можу додавати завдання й запам\'ятовувати важливе за вас.'; + + @override + String get onboardingStarterTaskAsk => 'Запитайте Omi про свій день'; + + @override + String get onboardingStarterTaskRecord => 'Запишіть розмову з Omi'; + + @override + String get onboardingStarterTaskCheck => 'Позначте це завдання, щоб завершити його'; + + @override + String get paywallRecommended => 'Recommended'; @override - String get firstCaptureWatchInstructions => - 'Open Omi on your Apple Watch and tap Record. Allow microphone access if asked. Your capture will sync to Omi on this phone.'; + String get paywallNoPlans => 'Plans aren\'t available right now. Try again in a minute.'; @override - String get firstCaptureWatchInstall => - 'In the Watch app on your iPhone, install Omi on your Apple Watch. Then open Omi on your watch and tap Record.'; + String get paywallOtherPlans => 'See all plans'; @override - String get firstCaptureWatchPair => - 'Pair your Apple Watch with this iPhone using the Watch app, then install and open Omi on your watch.'; + String get paywallComparePlans => 'See plans'; @override - String get firstCaptureUnavailable => - 'Recording could not start. Check microphone access and your connection, then try again.'; + String get paywallYourPlan => 'Your plan'; @override - String get firstCaptureDelayed => - 'Your recording is taking longer to process. You can check your conversations on Home or try again.'; + String get paywallNotNow => 'Not now'; @override - String get firstCaptureEmptyTitle => 'Let\'s try that again'; + String get paywallTryAgain => 'Try again'; @override - String get firstCaptureShortTitle => 'Finish This Recording?'; + String get paywallManageBilling => 'Manage billing'; @override - String get firstCaptureShortWarning => 'A few more sentences help Omi understand you. About 30 seconds is ideal.'; + String get paywallLoadError => 'Couldn\'t load plans. Check your connection and try again.'; @override - String get firstCaptureCancelTitle => 'Leave This Recording?'; + String get paywallPlans => 'Plans'; @override - String get firstCaptureCancelWarning => - 'Going back stops this recording. Keep talking to give Omi a few more sentences.'; + String get watchCaptureTitle => 'Record from your Apple Watch'; @override - String get firstCaptureSiriTip => 'Tip: Enable Omi in Shortcuts → Siri.'; + String get watchCaptureOpen => 'Open Omi on your Apple Watch.'; + + @override + String get watchCaptureRecord => 'Tap Record. Allow microphone access if asked.'; + + @override + String get watchCaptureSync => 'Your recording syncs to Omi on this phone.'; + + @override + String get pairingBluetoothOff => 'Turn on Bluetooth on your phone, then try again.'; + + @override + String get pairingBluetoothPermission => 'Allow Bluetooth access for Omi in Settings, then try again.'; + + @override + String get pairingNearbyPermission => 'Allow Nearby devices access for Omi in Settings, then try again.'; + + @override + String get pairingLocationPermission => + 'Allow location access for Omi in Settings so this phone can find your device, then try again.'; + + @override + String get pairingNotFoundOrDropped => + 'Couldn\'t reach your Omi. Make sure it\'s charged, close to your phone, and its light is blinking, then try again.'; + + @override + String get pairingBondRecovery => 'Forget Omi in your phone’s Bluetooth settings, then try pairing again.'; + + @override + String get pairingHowTo => 'How to pair'; @override String get paywallOutlineHeadline => 'Більше не пропускайте жодного слова.'; diff --git a/app/lib/l10n/app_localizations_ur.dart b/app/lib/l10n/app_localizations_ur.dart index fbaffd3c2c8..160bcba719f 100644 --- a/app/lib/l10n/app_localizations_ur.dart +++ b/app/lib/l10n/app_localizations_ur.dart @@ -1685,8 +1685,7 @@ class AppLocalizationsUr extends AppLocalizations { String get enableBluetooth => 'Bluetooth فعال کریں'; @override - String get bluetoothNeeded => - 'Omi کو اپنے قابل پہننے کے آلے سے جڑنے کے لیے Bluetooth کی ضرورت ہے۔ براہ کرم Bluetooth فعال کریں اور دوبارہ کوشش کریں۔'; + String get bluetoothNeeded => 'Omi آپ کے فون سے Bluetooth کے ذریعے جڑتا ہے۔ جاری رکھنے کے لیے اسے آن کریں۔'; @override String get contactSupport => 'معاونت سے رابطہ کریں؟'; @@ -4901,7 +4900,7 @@ class AppLocalizationsUr extends AppLocalizations { 'جاری رکھ کر، آپ کی بات چیت، ریکارڈنگز اور ذاتی معلومات ہمارے سرورز پر محفوظ طریقے سے ذخیرہ کی جائیں گی۔ آپ کی آڈیو ریکارڈنگز اور ٹرانسکرپٹس تھرڈ پارٹی AI سروسز کے ذریعے پراسیس کی جاتی ہیں (بشمول ٹرانسکرپشن کے لیے Deepgram اور تجزیے کے لیے OpenAI) تاکہ آپ کو AI سے چلنے والی بصیرتیں فراہم کی جا سکیں اور ایپ کی تمام خصوصیات کو فعال کیا جا سکے۔'; @override - String get tasksEmptyStateMessage => 'ٹاسک بنانے کے لیے گفتگو شروع کریں۔'; + String get tasksEmptyStateMessage => 'آپ کی گفتگو سے کام یہاں ظاہر ہوں گے۔'; @override String get clearChatAction => 'بات کو صاف کریں'; @@ -13054,154 +13053,207 @@ class AppLocalizationsUr extends AppLocalizations { } @override - String get paywallRecommended => 'Recommended'; + String get onboardingPermissionTapToFix => 'ٹھیک کرنے کے لیے ٹیپ کریں'; @override - String get paywallNoPlans => 'Plans aren\'t available right now. Try again in a minute.'; + String get onboardingNoOmiYet => 'ابھی تک Omi نہیں ہے؟'; @override - String get paywallOtherPlans => 'See all plans'; + String get onboardingOmiOrdered => 'میں نے آرڈر کر دیا ہے'; @override - String get paywallComparePlans => 'See plans'; + String get onboardingOmiNotOrdered => 'ابھی آرڈر نہیں کیا'; @override - String get paywallYourPlan => 'Your plan'; + String get onboardingConnectOmi => 'Omi کو جوڑیں'; @override - String get paywallNotNow => 'Not now'; + String get consentShortMessage => + 'Omi AI کی مدد سے آپ کی گفتگو کو نوٹس میں بدلتا ہے۔ آپ انہیں کسی بھی وقت حذف کر سکتے ہیں۔'; @override - String get paywallTryAgain => 'Try again'; + String consentAiProviders(String providers) { + return '$providers کے ذریعے پروسیس کیا جاتا ہے۔'; + } @override - String get paywallManageBilling => 'Manage billing'; + String get onboardingActIntro => 'آئیے Omi آزمائیں۔'; @override - String get paywallLoadError => 'Couldn\'t load plans. Check your connection and try again.'; + String get onboardingActMicTitle => 'Omi کو آپ کا مائیکروفون چاہیے'; @override - String get paywallPlans => 'Plans'; + String get onboardingActMicBody => 'تاکہ جب آپ بات کریں تو Omi سن سکے۔'; @override - String get firstCaptureTitle => 'How will you capture?'; + String get onboardingActMicAllow => 'مائیکروفون کی اجازت دیں'; @override - String get firstCaptureDescription => 'Choose how you want to capture your first conversation.'; + String get onboardingActDictateTitle => 'Omi کو اپنے دن کے بارے میں بتائیں'; @override - String get firstCaptureOmi => 'I Have an Omi'; + String get onboardingActDictateBody => 'اپنے انداز میں کہیں۔ باقی Omi سنبھال لے گا۔'; @override - String get firstCapturePhone => 'This Phone'; + String get onboardingActDictateExample => + 'ابھی سیم کے ساتھ لنچ کیا، وہ جون میں لزبن جا رہی ہے! مجھے یاد دلانا کہ جمعہ تک ٹیم کو ڈیک بھیجنا ہے، اور گھر جاتے ہوئے دودھ لینا ہے۔'; @override - String get firstCaptureWatch => 'Apple Watch'; + String get onboardingActDictateHint => 'ختم ہونے پر ٹک پر ٹیپ کریں'; @override - String get firstCaptureSkip => 'Skip for Now'; + String get onboardingActPhoneMic => 'فون کا مائیکروفون'; @override - String get firstCapturePrompt => - 'Tell Omi what you\'re working on today, or what\'s on your mind. Talk for about 30 seconds.'; + String get onboardingActWriting => 'خلاصہ بن رہا ہے…'; @override - String get firstCaptureKeepGoing => 'Keep going… 30 seconds is ideal.'; + String get onboardingActRemembers => 'Omi یاد رکھتا ہے تاکہ آپ کو نہ رکھنا پڑے'; @override - String get firstCaptureReady => 'You can finish now, or keep talking.'; + String get onboardingActNotesLabel => 'آپ کا دن، مختصراً'; @override - String get firstCaptureStart => 'Start Recording'; + String get onboardingActTasksLabel => 'Omi نے آپ کے کام پکڑ لیے'; @override - String get firstCaptureFinish => 'Finish Recording'; + String get onboardingActYourWords => 'آپ نے یہ کہا'; @override - String get firstCaptureListening => 'Listening… your words will appear here.'; + String onboardingActCongratsTitle(String name) { + return 'سب تیار ہے، $name'; + } @override - String get firstCaptureProcessing => 'Creating your first conversation…'; + String get onboardingActRateTitle => 'اب تک Omi پسند آیا؟'; @override - String get firstCaptureResult => 'Your first conversation'; + String get onboardingActRateYes => 'Omi کو ریٹ کریں'; @override - String get firstCaptureEmpty => 'I didn\'t catch enough speech, try again with a few sentences'; + String get onboardingWelcomeCardTitle => 'Omi میں خوش آمدید'; @override - String get watchCaptureTitle => 'Record from your Apple Watch'; + String get onboardingWelcomeCardBody => + 'Omi پہنیں اور معمول کے مطابق بات کریں۔ ہر گفتگو یہیں نوٹس، کاموں اور یادوں میں بدل جاتی ہے۔'; @override - String get watchCaptureOpen => 'Open Omi on your Apple Watch.'; + String get onboardingActDemoTitle => 'سیم کے ساتھ لنچ'; @override - String get watchCaptureRecord => 'Tap Record. Allow microphone access if asked.'; + String get onboardingActDemoSummary => + 'سام کے ساتھ لنچ، جو جون میں لزبن جا رہی ہے۔ آپ کو جمعہ تک ٹیم کو ڈیک بھیجنا ہے اور گھر جاتے ہوئے دودھ لینا ہے۔'; @override - String get watchCaptureSync => 'Your recording syncs to Omi on this phone.'; + String get onboardingActDemoTask1 => 'جمعہ تک ٹیم کو ڈیک بھیجنا'; @override - String get pairingBluetoothOff => 'Turn on Bluetooth on your phone, then try again.'; + String get onboardingActDemoTask2 => 'گھر جاتے ہوئے دودھ لینا'; @override - String get pairingBluetoothPermission => 'Allow Bluetooth access for Omi in Settings, then try again.'; + String get onboardingActJustNow => 'ابھی ابھی'; @override - String get pairingNearbyPermission => 'Allow Nearby devices access for Omi in Settings, then try again.'; + String get onboardingActSavedToTasks => 'آپ کے کاموں میں محفوظ'; @override - String get pairingLocationPermission => - 'Allow location access for Omi in Settings so this phone can find your device, then try again.'; + String get onboardingConnectTapToConnect => 'کنیکٹ کرنے کے لیے اپنے Omi پر ٹیپ کریں۔'; @override - String get pairingNotFoundOrDropped => - 'Couldn\'t reach your Omi. Make sure it\'s charged, close to your phone, and its light is blinking, then try again.'; + String get onboardingActMicFallbackBody => 'آپ کا Omi ابھی آڈیو نہیں بھیج رہا۔ فی الحال اپنا فون استعمال کریں۔'; @override - String get pairingBondRecovery => 'Forget Omi in your phone’s Bluetooth settings, then try pairing again.'; + String get onboardingActNoAudioOmi => 'آپ کا Omi سنائی نہیں دے رہا۔ دیکھیں کہ وہ آن اور قریب ہو۔'; @override - String get pairingHowTo => 'How to pair'; + String get onboardingActNoAudioPhone => 'ابھی آپ کی آواز نہیں آ رہی۔ تھوڑا اونچا بول کر دیکھیں۔'; + + @override + String get onboardingActDictateBodyOmi => 'بس بولیں۔ آپ کا Omi سن رہا ہے۔'; + + @override + String get voiceProfileCardDismissed => 'آپ کسی بھی وقت سیٹنگز میں وائس پروفائل سیٹ کر سکتے ہیں۔'; + + @override + String get homeNewRecordingButton => 'نئی ریکارڈنگ'; + + @override + String get chatWelcomeMessage => + 'ہیلو، میں Omi ہوں۔ آپ نے جس بارے میں بات کی، اس کے بارے میں کچھ بھی پوچھیں: کسی نے کیا کہا، آپ نے کیا وعدہ کیا یا آپ کے دن کا خلاصہ۔ میں کام بھی شامل کر سکتا ہوں اور آپ کے لیے باتیں یاد بھی رکھ سکتا ہوں۔'; + + @override + String get onboardingStarterTaskAsk => 'Omi سے اپنے دن کے بارے میں پوچھیں'; + + @override + String get onboardingStarterTaskRecord => 'Omi کے ساتھ ایک گفتگو ریکارڈ کریں'; + + @override + String get onboardingStarterTaskCheck => 'مکمل کرنے کے لیے اس کام پر نشان لگائیں'; + + @override + String get paywallRecommended => 'Recommended'; @override - String get firstCaptureWatchInstructions => - 'Open Omi on your Apple Watch and tap Record. Allow microphone access if asked. Your capture will sync to Omi on this phone.'; + String get paywallNoPlans => 'Plans aren\'t available right now. Try again in a minute.'; @override - String get firstCaptureWatchInstall => - 'In the Watch app on your iPhone, install Omi on your Apple Watch. Then open Omi on your watch and tap Record.'; + String get paywallOtherPlans => 'See all plans'; @override - String get firstCaptureWatchPair => - 'Pair your Apple Watch with this iPhone using the Watch app, then install and open Omi on your watch.'; + String get paywallComparePlans => 'See plans'; @override - String get firstCaptureUnavailable => - 'Recording could not start. Check microphone access and your connection, then try again.'; + String get paywallYourPlan => 'Your plan'; @override - String get firstCaptureDelayed => - 'Your recording is taking longer to process. You can check your conversations on Home or try again.'; + String get paywallNotNow => 'Not now'; @override - String get firstCaptureEmptyTitle => 'Let\'s try that again'; + String get paywallTryAgain => 'Try again'; @override - String get firstCaptureShortTitle => 'Finish This Recording?'; + String get paywallManageBilling => 'Manage billing'; @override - String get firstCaptureShortWarning => 'A few more sentences help Omi understand you. About 30 seconds is ideal.'; + String get paywallLoadError => 'Couldn\'t load plans. Check your connection and try again.'; @override - String get firstCaptureCancelTitle => 'Leave This Recording?'; + String get paywallPlans => 'Plans'; @override - String get firstCaptureCancelWarning => - 'Going back stops this recording. Keep talking to give Omi a few more sentences.'; + String get watchCaptureTitle => 'Record from your Apple Watch'; @override - String get firstCaptureSiriTip => 'Tip: Enable Omi in Shortcuts → Siri.'; + String get watchCaptureOpen => 'Open Omi on your Apple Watch.'; + + @override + String get watchCaptureRecord => 'Tap Record. Allow microphone access if asked.'; + + @override + String get watchCaptureSync => 'Your recording syncs to Omi on this phone.'; + + @override + String get pairingBluetoothOff => 'Turn on Bluetooth on your phone, then try again.'; + + @override + String get pairingBluetoothPermission => 'Allow Bluetooth access for Omi in Settings, then try again.'; + + @override + String get pairingNearbyPermission => 'Allow Nearby devices access for Omi in Settings, then try again.'; + + @override + String get pairingLocationPermission => + 'Allow location access for Omi in Settings so this phone can find your device, then try again.'; + + @override + String get pairingNotFoundOrDropped => + 'Couldn\'t reach your Omi. Make sure it\'s charged, close to your phone, and its light is blinking, then try again.'; + + @override + String get pairingBondRecovery => 'Forget Omi in your phone’s Bluetooth settings, then try pairing again.'; + + @override + String get pairingHowTo => 'How to pair'; @override String get paywallOutlineHeadline => 'اب ایک لفظ بھی نہ چھوڑیں۔'; diff --git a/app/lib/l10n/app_localizations_vi.dart b/app/lib/l10n/app_localizations_vi.dart index 2a37c656111..d4f4f43106f 100644 --- a/app/lib/l10n/app_localizations_vi.dart +++ b/app/lib/l10n/app_localizations_vi.dart @@ -1694,8 +1694,7 @@ class AppLocalizationsVi extends AppLocalizations { String get enableBluetooth => 'Bật Bluetooth'; @override - String get bluetoothNeeded => - 'Omi cần Bluetooth để kết nối với thiết bị đeo của bạn. Vui lòng bật Bluetooth và thử lại.'; + String get bluetoothNeeded => 'Omi kết nối với điện thoại qua Bluetooth. Hãy bật để tiếp tục.'; @override String get contactSupport => 'Liên hệ hỗ trợ?'; @@ -4903,7 +4902,7 @@ class AppLocalizationsVi extends AppLocalizations { 'Bằng cách tiếp tục, các cuộc trò chuyện, bản ghi âm và thông tin cá nhân của bạn sẽ được lưu trữ an toàn trên máy chủ của chúng tôi. Bản ghi âm và bản phiên âm của bạn được xử lý bởi các dịch vụ AI bên thứ ba (bao gồm Deepgram cho phiên âm và OpenAI cho phân tích) để cung cấp cho bạn thông tin chi tiết được hỗ trợ bởi AI và kích hoạt tất cả các tính năng ứng dụng.'; @override - String get tasksEmptyStateMessage => 'Bắt đầu cuộc trò chuyện để tạo nhiệm vụ.'; + String get tasksEmptyStateMessage => 'Việc cần làm từ các cuộc trò chuyện của bạn sẽ hiện ở đây.'; @override String get clearChatAction => 'Xóa cuộc trò chuyện'; @@ -13038,154 +13037,207 @@ class AppLocalizationsVi extends AppLocalizations { } @override - String get paywallRecommended => 'Recommended'; + String get onboardingPermissionTapToFix => 'Nhấn để khắc phục'; @override - String get paywallNoPlans => 'Plans aren\'t available right now. Try again in a minute.'; + String get onboardingNoOmiYet => 'Bạn chưa có Omi?'; @override - String get paywallOtherPlans => 'See all plans'; + String get onboardingOmiOrdered => 'Tôi đã đặt hàng'; @override - String get paywallComparePlans => 'See plans'; + String get onboardingOmiNotOrdered => 'Chưa đặt hàng'; @override - String get paywallYourPlan => 'Your plan'; + String get onboardingConnectOmi => 'Kết nối Omi'; @override - String get paywallNotNow => 'Not now'; + String get consentShortMessage => + 'Omi dùng AI để biến các cuộc trò chuyện của bạn thành ghi chú. Bạn có thể xóa chúng bất cứ lúc nào.'; @override - String get paywallTryAgain => 'Try again'; + String consentAiProviders(String providers) { + return 'Được xử lý bằng $providers.'; + } @override - String get paywallManageBilling => 'Manage billing'; + String get onboardingActIntro => 'Cùng thử Omi nhé.'; @override - String get paywallLoadError => 'Couldn\'t load plans. Check your connection and try again.'; + String get onboardingActMicTitle => 'Omi cần micrô của bạn'; @override - String get paywallPlans => 'Plans'; + String get onboardingActMicBody => 'Để Omi nghe được khi bạn nói.'; @override - String get firstCaptureTitle => 'How will you capture?'; + String get onboardingActMicAllow => 'Cho phép micrô'; @override - String get firstCaptureDescription => 'Choose how you want to capture your first conversation.'; + String get onboardingActDictateTitle => 'Kể cho Omi nghe về ngày của bạn'; @override - String get firstCaptureOmi => 'I Have an Omi'; + String get onboardingActDictateBody => 'Cứ nói theo cách của bạn. Omi lo phần còn lại.'; @override - String get firstCapturePhone => 'This Phone'; + String get onboardingActDictateExample => + 'Vừa ăn trưa với Sam, tháng Sáu cô ấy chuyển đến Lisbon! Nhắc mình gửi slide cho nhóm trước thứ Sáu, và mua sữa trên đường về nhà.'; @override - String get firstCaptureWatch => 'Apple Watch'; + String get onboardingActDictateHint => 'Nhấn dấu tích khi bạn nói xong'; @override - String get firstCaptureSkip => 'Skip for Now'; + String get onboardingActPhoneMic => 'Micrô điện thoại'; @override - String get firstCapturePrompt => - 'Tell Omi what you\'re working on today, or what\'s on your mind. Talk for about 30 seconds.'; + String get onboardingActWriting => 'Đang tóm tắt…'; @override - String get firstCaptureKeepGoing => 'Keep going… 30 seconds is ideal.'; + String get onboardingActRemembers => 'Omi ghi nhớ để bạn không phải nhớ'; @override - String get firstCaptureReady => 'You can finish now, or keep talking.'; + String get onboardingActNotesLabel => 'Ngày của bạn, tóm tắt'; @override - String get firstCaptureStart => 'Start Recording'; + String get onboardingActTasksLabel => 'Omi đã nắm được việc cần làm'; @override - String get firstCaptureFinish => 'Finish Recording'; + String get onboardingActYourWords => 'Đây là những gì bạn nói'; @override - String get firstCaptureListening => 'Listening… your words will appear here.'; + String onboardingActCongratsTitle(String name) { + return 'Mọi thứ đã sẵn sàng, $name'; + } @override - String get firstCaptureProcessing => 'Creating your first conversation…'; + String get onboardingActRateTitle => 'Bạn thích Omi chứ?'; @override - String get firstCaptureResult => 'Your first conversation'; + String get onboardingActRateYes => 'Đánh giá Omi'; @override - String get firstCaptureEmpty => 'I didn\'t catch enough speech, try again with a few sentences'; + String get onboardingWelcomeCardTitle => 'Chào mừng đến với Omi'; @override - String get watchCaptureTitle => 'Record from your Apple Watch'; + String get onboardingWelcomeCardBody => + 'Đeo Omi và nói chuyện như bình thường. Mỗi cuộc trò chuyện sẽ thành ghi chú, việc cần làm và ký ức ngay tại đây.'; @override - String get watchCaptureOpen => 'Open Omi on your Apple Watch.'; + String get onboardingActDemoTitle => 'Ăn trưa với Sam'; @override - String get watchCaptureRecord => 'Tap Record. Allow microphone access if asked.'; + String get onboardingActDemoSummary => + 'Ăn trưa với Sam, người sẽ chuyển đến Lisbon vào tháng Sáu. Bạn muốn gửi bản trình bày cho nhóm trước thứ Sáu và mua sữa trên đường về.'; @override - String get watchCaptureSync => 'Your recording syncs to Omi on this phone.'; + String get onboardingActDemoTask1 => 'Gửi slide cho nhóm trước thứ Sáu'; @override - String get pairingBluetoothOff => 'Turn on Bluetooth on your phone, then try again.'; + String get onboardingActDemoTask2 => 'Mua sữa trên đường về nhà'; @override - String get pairingBluetoothPermission => 'Allow Bluetooth access for Omi in Settings, then try again.'; + String get onboardingActJustNow => 'Vừa xong'; @override - String get pairingNearbyPermission => 'Allow Nearby devices access for Omi in Settings, then try again.'; + String get onboardingActSavedToTasks => 'Đã lưu vào việc cần làm'; @override - String get pairingLocationPermission => - 'Allow location access for Omi in Settings so this phone can find your device, then try again.'; + String get onboardingConnectTapToConnect => 'Chạm vào Omi của bạn để kết nối.'; @override - String get pairingNotFoundOrDropped => - 'Couldn\'t reach your Omi. Make sure it\'s charged, close to your phone, and its light is blinking, then try again.'; + String get onboardingActMicFallbackBody => 'Omi chưa gửi âm thanh. Tạm thời hãy dùng điện thoại.'; @override - String get pairingBondRecovery => 'Forget Omi in your phone’s Bluetooth settings, then try pairing again.'; + String get onboardingActNoAudioOmi => 'Không nghe thấy Omi. Hãy kiểm tra xem nó đã bật và ở gần chưa.'; @override - String get pairingHowTo => 'How to pair'; + String get onboardingActNoAudioPhone => 'Chưa nghe thấy bạn. Hãy thử nói to hơn một chút.'; + + @override + String get onboardingActDictateBodyOmi => 'Cứ nói thôi. Omi đang lắng nghe.'; + + @override + String get voiceProfileCardDismissed => 'Bạn có thể thiết lập Hồ sơ giọng nói bất cứ lúc nào trong Cài đặt.'; + + @override + String get homeNewRecordingButton => 'Ghi âm mới'; + + @override + String get chatWelcomeMessage => + 'Chào bạn, mình là Omi. Hỏi mình bất cứ điều gì bạn đã nói: ai đã nói gì, bạn đã hứa gì hay tóm tắt ngày của bạn. Mình cũng có thể thêm việc cần làm và ghi nhớ mọi thứ giúp bạn.'; + + @override + String get onboardingStarterTaskAsk => 'Hỏi Omi về ngày của bạn'; + + @override + String get onboardingStarterTaskRecord => 'Ghi âm một cuộc trò chuyện với Omi'; + + @override + String get onboardingStarterTaskCheck => 'Đánh dấu việc này để hoàn thành'; + + @override + String get paywallRecommended => 'Recommended'; @override - String get firstCaptureWatchInstructions => - 'Open Omi on your Apple Watch and tap Record. Allow microphone access if asked. Your capture will sync to Omi on this phone.'; + String get paywallNoPlans => 'Plans aren\'t available right now. Try again in a minute.'; @override - String get firstCaptureWatchInstall => - 'In the Watch app on your iPhone, install Omi on your Apple Watch. Then open Omi on your watch and tap Record.'; + String get paywallOtherPlans => 'See all plans'; @override - String get firstCaptureWatchPair => - 'Pair your Apple Watch with this iPhone using the Watch app, then install and open Omi on your watch.'; + String get paywallComparePlans => 'See plans'; @override - String get firstCaptureUnavailable => - 'Recording could not start. Check microphone access and your connection, then try again.'; + String get paywallYourPlan => 'Your plan'; @override - String get firstCaptureDelayed => - 'Your recording is taking longer to process. You can check your conversations on Home or try again.'; + String get paywallNotNow => 'Not now'; @override - String get firstCaptureEmptyTitle => 'Let\'s try that again'; + String get paywallTryAgain => 'Try again'; @override - String get firstCaptureShortTitle => 'Finish This Recording?'; + String get paywallManageBilling => 'Manage billing'; @override - String get firstCaptureShortWarning => 'A few more sentences help Omi understand you. About 30 seconds is ideal.'; + String get paywallLoadError => 'Couldn\'t load plans. Check your connection and try again.'; @override - String get firstCaptureCancelTitle => 'Leave This Recording?'; + String get paywallPlans => 'Plans'; @override - String get firstCaptureCancelWarning => - 'Going back stops this recording. Keep talking to give Omi a few more sentences.'; + String get watchCaptureTitle => 'Record from your Apple Watch'; @override - String get firstCaptureSiriTip => 'Tip: Enable Omi in Shortcuts → Siri.'; + String get watchCaptureOpen => 'Open Omi on your Apple Watch.'; + + @override + String get watchCaptureRecord => 'Tap Record. Allow microphone access if asked.'; + + @override + String get watchCaptureSync => 'Your recording syncs to Omi on this phone.'; + + @override + String get pairingBluetoothOff => 'Turn on Bluetooth on your phone, then try again.'; + + @override + String get pairingBluetoothPermission => 'Allow Bluetooth access for Omi in Settings, then try again.'; + + @override + String get pairingNearbyPermission => 'Allow Nearby devices access for Omi in Settings, then try again.'; + + @override + String get pairingLocationPermission => + 'Allow location access for Omi in Settings so this phone can find your device, then try again.'; + + @override + String get pairingNotFoundOrDropped => + 'Couldn\'t reach your Omi. Make sure it\'s charged, close to your phone, and its light is blinking, then try again.'; + + @override + String get pairingBondRecovery => 'Forget Omi in your phone’s Bluetooth settings, then try pairing again.'; + + @override + String get pairingHowTo => 'How to pair'; @override String get paywallOutlineHeadline => 'Không bao giờ bỏ lỡ một lời nào nữa.'; diff --git a/app/lib/l10n/app_localizations_zh.dart b/app/lib/l10n/app_localizations_zh.dart index 92cea4ee064..457a137ee7d 100644 --- a/app/lib/l10n/app_localizations_zh.dart +++ b/app/lib/l10n/app_localizations_zh.dart @@ -1661,7 +1661,7 @@ class AppLocalizationsZh extends AppLocalizations { String get enableBluetooth => '启用蓝牙'; @override - String get bluetoothNeeded => 'Omi 需要蓝牙来连接您的穿戴设备。'; + String get bluetoothNeeded => 'Omi 通过蓝牙连接你的手机。打开蓝牙以继续。'; @override String get contactSupport => '联系支持?'; @@ -4809,7 +4809,7 @@ class AppLocalizationsZh extends AppLocalizations { '继续即表示您的对话、录音和个人信息将安全存储在我们的服务器上。您的音频录音和转录由第三方AI服务处理(包括用于转录的Deepgram和用于分析的OpenAI),以为您提供AI驱动的洞察并启用所有应用功能。'; @override - String get tasksEmptyStateMessage => '开始对话以创建任务。'; + String get tasksEmptyStateMessage => '对话中的待办事项会显示在这里。'; @override String get clearChatAction => '清除聊天'; @@ -12835,154 +12835,202 @@ class AppLocalizationsZh extends AppLocalizations { } @override - String get paywallRecommended => 'Recommended'; + String get onboardingPermissionTapToFix => '点按以修复'; @override - String get paywallNoPlans => 'Plans aren\'t available right now. Try again in a minute.'; + String get onboardingNoOmiYet => '还没有 Omi?'; @override - String get paywallOtherPlans => 'See all plans'; + String get onboardingOmiOrdered => '我已下单'; @override - String get paywallComparePlans => 'See plans'; + String get onboardingOmiNotOrdered => '还没下单'; @override - String get paywallYourPlan => 'Your plan'; + String get onboardingConnectOmi => '连接 Omi'; @override - String get paywallNotNow => 'Not now'; + String get consentShortMessage => 'Omi 用 AI 把你的对话整理成笔记。你可以随时删除。'; @override - String get paywallTryAgain => 'Try again'; + String consentAiProviders(String providers) { + return '由 $providers 处理。'; + } @override - String get paywallManageBilling => 'Manage billing'; + String get onboardingActIntro => '来试试 Omi 吧。'; @override - String get paywallLoadError => 'Couldn\'t load plans. Check your connection and try again.'; + String get onboardingActMicTitle => 'Omi 需要使用麦克风'; @override - String get paywallPlans => 'Plans'; + String get onboardingActMicBody => '这样你说话时 Omi 才能听到。'; @override - String get firstCaptureTitle => 'How will you capture?'; + String get onboardingActMicAllow => '允许使用麦克风'; @override - String get firstCaptureDescription => 'Choose how you want to capture your first conversation.'; + String get onboardingActDictateTitle => '跟 Omi 说说你的一天'; @override - String get firstCaptureOmi => 'I Have an Omi'; + String get onboardingActDictateBody => '随便说,剩下的交给 Omi。'; @override - String get firstCapturePhone => 'This Phone'; + String get onboardingActDictateExample => '刚和 Sam 吃完午饭,她六月要搬去里斯本!提醒我周五前把演示文稿发给团队,回家路上顺便买牛奶。'; @override - String get firstCaptureWatch => 'Apple Watch'; + String get onboardingActDictateHint => '说完后点对勾'; @override - String get firstCaptureSkip => 'Skip for Now'; + String get onboardingActPhoneMic => '手机麦克风'; @override - String get firstCapturePrompt => - 'Tell Omi what you\'re working on today, or what\'s on your mind. Talk for about 30 seconds.'; + String get onboardingActWriting => '正在总结…'; @override - String get firstCaptureKeepGoing => 'Keep going… 30 seconds is ideal.'; + String get onboardingActRemembers => 'Omi 帮你记住,你无需费心'; @override - String get firstCaptureReady => 'You can finish now, or keep talking.'; + String get onboardingActNotesLabel => '你的一天,总结好了'; @override - String get firstCaptureStart => 'Start Recording'; + String get onboardingActTasksLabel => 'Omi 记下了你的待办'; @override - String get firstCaptureFinish => 'Finish Recording'; + String get onboardingActYourWords => '你说的内容'; @override - String get firstCaptureListening => 'Listening… your words will appear here.'; + String onboardingActCongratsTitle(String name) { + return '一切就绪,$name'; + } @override - String get firstCaptureProcessing => 'Creating your first conversation…'; + String get onboardingActRateTitle => '目前喜欢 Omi 吗?'; @override - String get firstCaptureResult => 'Your first conversation'; + String get onboardingActRateYes => '给 Omi 评分'; @override - String get firstCaptureEmpty => 'I didn\'t catch enough speech, try again with a few sentences'; + String get onboardingWelcomeCardTitle => '欢迎使用 Omi'; @override - String get watchCaptureTitle => 'Record from your Apple Watch'; + String get onboardingWelcomeCardBody => '戴上 Omi,照常说话。每段对话都会在这里变成笔记、待办和记忆。'; @override - String get watchCaptureOpen => 'Open Omi on your Apple Watch.'; + String get onboardingActDemoTitle => '和 Sam 吃午饭'; @override - String get watchCaptureRecord => 'Tap Record. Allow microphone access if asked.'; + String get onboardingActDemoSummary => '和 Sam 吃了午饭,她六月要搬去里斯本。你打算周五前把演示稿发给团队,回家路上买牛奶。'; @override - String get watchCaptureSync => 'Your recording syncs to Omi on this phone.'; + String get onboardingActDemoTask1 => '周五前把演示文稿发给团队'; @override - String get pairingBluetoothOff => 'Turn on Bluetooth on your phone, then try again.'; + String get onboardingActDemoTask2 => '回家路上买牛奶'; @override - String get pairingBluetoothPermission => 'Allow Bluetooth access for Omi in Settings, then try again.'; + String get onboardingActJustNow => '刚刚'; @override - String get pairingNearbyPermission => 'Allow Nearby devices access for Omi in Settings, then try again.'; + String get onboardingActSavedToTasks => '已保存到你的任务'; @override - String get pairingLocationPermission => - 'Allow location access for Omi in Settings so this phone can find your device, then try again.'; + String get onboardingConnectTapToConnect => '轻点你的 Omi 进行连接。'; @override - String get pairingNotFoundOrDropped => - 'Couldn\'t reach your Omi. Make sure it\'s charged, close to your phone, and its light is blinking, then try again.'; + String get onboardingActMicFallbackBody => '你的 Omi 还没有传来声音,先用手机吧。'; @override - String get pairingBondRecovery => 'Forget Omi in your phone’s Bluetooth settings, then try pairing again.'; + String get onboardingActNoAudioOmi => '听不到你的 Omi。请确认它已开启并在附近。'; @override - String get pairingHowTo => 'How to pair'; + String get onboardingActNoAudioPhone => '还听不到你的声音,试着大声一点。'; + + @override + String get onboardingActDictateBodyOmi => '直接说就好,你的 Omi 正在听。'; + + @override + String get voiceProfileCardDismissed => '你可以随时在设置中设置声音档案。'; + + @override + String get homeNewRecordingButton => '新录音'; + + @override + String get chatWelcomeMessage => '你好,我是 Omi。关于你聊过的任何事都可以问我:某人说了什么、你答应过什么,或者今天的总结。我还能帮你添加待办、记住重要的事。'; + + @override + String get onboardingStarterTaskAsk => '问问 Omi 你今天过得怎么样'; + + @override + String get onboardingStarterTaskRecord => '用 Omi 录一段对话'; + + @override + String get onboardingStarterTaskCheck => '勾选这项任务来完成它'; + + @override + String get paywallRecommended => 'Recommended'; @override - String get firstCaptureWatchInstructions => - 'Open Omi on your Apple Watch and tap Record. Allow microphone access if asked. Your capture will sync to Omi on this phone.'; + String get paywallNoPlans => 'Plans aren\'t available right now. Try again in a minute.'; @override - String get firstCaptureWatchInstall => - 'In the Watch app on your iPhone, install Omi on your Apple Watch. Then open Omi on your watch and tap Record.'; + String get paywallOtherPlans => 'See all plans'; @override - String get firstCaptureWatchPair => - 'Pair your Apple Watch with this iPhone using the Watch app, then install and open Omi on your watch.'; + String get paywallComparePlans => 'See plans'; @override - String get firstCaptureUnavailable => - 'Recording could not start. Check microphone access and your connection, then try again.'; + String get paywallYourPlan => 'Your plan'; @override - String get firstCaptureDelayed => - 'Your recording is taking longer to process. You can check your conversations on Home or try again.'; + String get paywallNotNow => 'Not now'; @override - String get firstCaptureEmptyTitle => 'Let\'s try that again'; + String get paywallTryAgain => 'Try again'; @override - String get firstCaptureShortTitle => 'Finish This Recording?'; + String get paywallManageBilling => 'Manage billing'; @override - String get firstCaptureShortWarning => 'A few more sentences help Omi understand you. About 30 seconds is ideal.'; + String get paywallLoadError => 'Couldn\'t load plans. Check your connection and try again.'; @override - String get firstCaptureCancelTitle => 'Leave This Recording?'; + String get paywallPlans => 'Plans'; @override - String get firstCaptureCancelWarning => - 'Going back stops this recording. Keep talking to give Omi a few more sentences.'; + String get watchCaptureTitle => 'Record from your Apple Watch'; @override - String get firstCaptureSiriTip => 'Tip: Enable Omi in Shortcuts → Siri.'; + String get watchCaptureOpen => 'Open Omi on your Apple Watch.'; + + @override + String get watchCaptureRecord => 'Tap Record. Allow microphone access if asked.'; + + @override + String get watchCaptureSync => 'Your recording syncs to Omi on this phone.'; + + @override + String get pairingBluetoothOff => 'Turn on Bluetooth on your phone, then try again.'; + + @override + String get pairingBluetoothPermission => 'Allow Bluetooth access for Omi in Settings, then try again.'; + + @override + String get pairingNearbyPermission => 'Allow Nearby devices access for Omi in Settings, then try again.'; + + @override + String get pairingLocationPermission => + 'Allow location access for Omi in Settings so this phone can find your device, then try again.'; + + @override + String get pairingNotFoundOrDropped => + 'Couldn\'t reach your Omi. Make sure it\'s charged, close to your phone, and its light is blinking, then try again.'; + + @override + String get pairingBondRecovery => 'Forget Omi in your phone’s Bluetooth settings, then try pairing again.'; + + @override + String get pairingHowTo => 'How to pair'; @override String get paywallOutlineHeadline => '再也不错过任何一句话。'; diff --git a/app/lib/l10n/app_lt.arb b/app/lib/l10n/app_lt.arb index 3e33c1d2e7f..9a77b2f4a34 100644 --- a/app/lib/l10n/app_lt.arb +++ b/app/lib/l10n/app_lt.arb @@ -300,7 +300,7 @@ "bleSpeed": "~30 KB/s per BLE", "bluetooth": "Bluetooth", "bluetoothMethodDescription": "Naudoja standartinį Bluetooth Low Energy ryšį. Lėčiau, bet neturi įtakos WiFi ryšiui.", - "bluetoothNeeded": "Omi reikia Bluetooth, kad prisijungtų prie jūsų nešiojamo įrenginio. Įjunkite Bluetooth ir bandykite dar kartą.", + "bluetoothNeeded": "Omi prie telefono jungiasi per Bluetooth. Įjunkite jį, kad tęstumėte.", "bluetoothSyncEnabled": "Bluetooth sinchronizavimas įjungtas", "bringYourOwn": "Naudokite savo", "browseInstallCreateApps": "Naršykite, įdiekite ir kurkite programas", @@ -1934,7 +1934,7 @@ "tasksAddedToList": "Užduotys bus pridėtos į šį sąrašą", "tasksCompleted": "Užduotys atliktos", "tasksCreatedInWorkspace": "Užduotys bus sukurtos šioje darbo srityje", - "tasksEmptyStateMessage": "Pradėkite pokalbį, kad sukurtumėte užduotį.", + "tasksEmptyStateMessage": "Užduotys iš jūsų pokalbių bus rodomos čia.", "tasksExportedOneApp": "Užduotys gali būti eksportuojamos į vieną programėlę vienu metu.", "tasksFromConversationsWillAppear": "Užduotys iš jūsų pokalbių bus rodomos čia.\nSpustelėkite Kurti, kad pridėtumėte vieną rankiniu būdu.", "tasksLater": "Vėliau", @@ -4004,6 +4004,49 @@ "reviewProposalSuggested": "Siūloma", "reviewProposalConflict": "Tai pasikeitė po Dream pasiūlymo, todėl nieko nepritaikyta.", "itemLinkListTitle": "{count, plural, =0{Nėra elementų} =1{1 elementas} other{{count} elementai}}", + "onboardingPermissionTapToFix": "Bakstelėkite, kad pataisytumėte", + "onboardingNoOmiYet": "Dar neturite Omi?", + "onboardingOmiOrdered": "Jau užsisakiau", + "onboardingOmiNotOrdered": "Dar neužsisakiau", + "onboardingConnectOmi": "Prijunkite Omi", + "consentShortMessage": "Omi naudoja DI, kad jūsų pokalbius paverstų užrašais. Juos galite ištrinti bet kada.", + "consentAiProviders": "Apdorojama su {providers}.", + "onboardingActIntro": "Išbandykime Omi.", + "onboardingActMicTitle": "Omi reikia mikrofono", + "onboardingActMicBody": "Kad Omi girdėtų, kai su juo kalbate.", + "onboardingActMicAllow": "Leisti mikrofoną", + "onboardingActDictateTitle": "Papasakokite Omi apie savo dieną", + "onboardingActDictateBody": "Sakykite savaip. Visa kita padarys Omi.", + "onboardingActDictateExample": "Ką tik pietavau su Sem, birželį ji kraustosi į Lisaboną! Primink man iki penktadienio nusiųsti pristatymą komandai ir grįžtant namo nupirkti pieno.", + "onboardingActDictateHint": "Baigę bakstelėkite varnelę", + "onboardingActPhoneMic": "Telefono mikrofonas", + "onboardingActWriting": "Apibendriname…", + "onboardingActRemembers": "Omi prisimena, kad jums nereikėtų", + "onboardingActNotesLabel": "Jūsų diena trumpai", + "onboardingActTasksLabel": "Omi pastebėjo jūsų darbus", + "onboardingActYourWords": "Štai ką pasakėte", + "onboardingActCongratsTitle": "Viskas paruošta, {name}", + "onboardingActRateTitle": "Ar Omi jums patinka?", + "onboardingActRateYes": "Įvertinti Omi", + "onboardingWelcomeCardTitle": "Sveiki atvykę į Omi", + "onboardingWelcomeCardBody": "Nešiokite Omi ir kalbėkite kaip įprasta. Kiekvienas pokalbis čia virsta užrašais, užduotimis ir prisiminimais.", + "onboardingActDemoTitle": "Pietūs su Sem", + "onboardingActDemoSummary": "Pietūs su Sem, kuri birželį persikelia į Lisaboną. Iki penktadienio norite išsiųsti komandai pristatymą ir grįžtant nupirkti pieno.", + "onboardingActDemoTask1": "Iki penktadienio nusiųsti pristatymą komandai", + "onboardingActDemoTask2": "Grįžtant namo nupirkti pieno", + "onboardingActJustNow": "Ką tik", + "onboardingActSavedToTasks": "Išsaugota jūsų užduotyse", + "onboardingConnectTapToConnect": "Palieskite savo Omi, kad prisijungtumėte.", + "onboardingActMicFallbackBody": "Omi dar nesiunčia garso. Kol kas naudokite telefoną.", + "onboardingActNoAudioOmi": "Omi negirdėti. Patikrinkite, ar jis įjungtas ir šalia.", + "onboardingActNoAudioPhone": "Dar jūsų negirdime. Pabandykite kalbėti šiek tiek garsiau.", + "onboardingActDictateBodyOmi": "Tiesiog kalbėkite. Jūsų Omi klauso.", + "voiceProfileCardDismissed": "Balso profilį galite nustatyti bet kada nustatymuose.", + "homeNewRecordingButton": "Naujas įrašas", + "chatWelcomeMessage": "Labas, aš Omi. Klausk manęs apie bet ką, apie ką kalbėjai: ką kas nors pasakė, ką pažadėjai ar tavo dienos santrauką. Taip pat galiu pridėti užduočių ir prisiminti dalykus už tave.", + "onboardingStarterTaskAsk": "Paklausk Omi apie savo dieną", + "onboardingStarterTaskRecord": "Įrašyk pokalbį su Omi", + "onboardingStarterTaskCheck": "Pažymėk šią užduotį kaip atliktą", "paywallRecommended": "Recommended", "paywallNoPlans": "Plans aren't available right now. Try again in a minute.", "paywallOtherPlans": "See all plans", @@ -4014,21 +4057,6 @@ "paywallManageBilling": "Manage billing", "paywallLoadError": "Couldn't load plans. Check your connection and try again.", "paywallPlans": "Plans", - "firstCaptureTitle": "How will you capture?", - "firstCaptureDescription": "Choose how you want to capture your first conversation.", - "firstCaptureOmi": "I Have an Omi", - "firstCapturePhone": "This Phone", - "firstCaptureWatch": "Apple Watch", - "firstCaptureSkip": "Skip for Now", - "firstCapturePrompt": "Tell Omi what you're working on today, or what's on your mind. Talk for about 30 seconds.", - "firstCaptureKeepGoing": "Keep going… 30 seconds is ideal.", - "firstCaptureReady": "You can finish now, or keep talking.", - "firstCaptureStart": "Start Recording", - "firstCaptureFinish": "Finish Recording", - "firstCaptureListening": "Listening… your words will appear here.", - "firstCaptureProcessing": "Creating your first conversation…", - "firstCaptureResult": "Your first conversation", - "firstCaptureEmpty": "I didn't catch enough speech, try again with a few sentences", "watchCaptureTitle": "Record from your Apple Watch", "watchCaptureOpen": "Open Omi on your Apple Watch.", "watchCaptureRecord": "Tap Record. Allow microphone access if asked.", @@ -4040,17 +4068,6 @@ "pairingNotFoundOrDropped": "Couldn't reach your Omi. Make sure it's charged, close to your phone, and its light is blinking, then try again.", "pairingBondRecovery": "Forget Omi in your phone’s Bluetooth settings, then try pairing again.", "pairingHowTo": "How to pair", - "firstCaptureWatchInstructions": "Open Omi on your Apple Watch and tap Record. Allow microphone access if asked. Your capture will sync to Omi on this phone.", - "firstCaptureWatchInstall": "In the Watch app on your iPhone, install Omi on your Apple Watch. Then open Omi on your watch and tap Record.", - "firstCaptureWatchPair": "Pair your Apple Watch with this iPhone using the Watch app, then install and open Omi on your watch.", - "firstCaptureUnavailable": "Recording could not start. Check microphone access and your connection, then try again.", - "firstCaptureDelayed": "Your recording is taking longer to process. You can check your conversations on Home or try again.", - "firstCaptureEmptyTitle": "Let's try that again", - "firstCaptureShortTitle": "Finish This Recording?", - "firstCaptureShortWarning": "A few more sentences help Omi understand you. About 30 seconds is ideal.", - "firstCaptureCancelTitle": "Leave This Recording?", - "firstCaptureCancelWarning": "Going back stops this recording. Keep talking to give Omi a few more sentences.", - "firstCaptureSiriTip": "Tip: Enable Omi in Shortcuts → Siri.", "paywallOutlineHeadline": "Daugiau nepraleiskite nė žodžio.", "paywallCancelAnytime": "Atšaukite bet kada.", "connectAnAgent": "Prijungti agentą", diff --git a/app/lib/l10n/app_lv.arb b/app/lib/l10n/app_lv.arb index 613ac569c01..fbfcf41a8b7 100644 --- a/app/lib/l10n/app_lv.arb +++ b/app/lib/l10n/app_lv.arb @@ -520,7 +520,7 @@ "dontShowAgain": "Nerādīt vēlreiz", "iUnderstand": "Es saprotu", "enableBluetooth": "Iespējot Bluetooth", - "bluetoothNeeded": "Omi nepieciešams Bluetooth, lai savienotos ar jūsu valkājamo ierīci. Lūdzu, iespējojiet Bluetooth un mēģiniet vēlreiz.", + "bluetoothNeeded": "Omi ar tālruni savienojas, izmantojot Bluetooth. Ieslēdziet to, lai turpinātu.", "contactSupport": "Sazināties ar atbalstu?", "connectLater": "Savienot vēlāk", "grantPermissions": "Piešķirt atļaujas", @@ -1501,7 +1501,7 @@ "durationSeconds": "Ilgums: {seconds} sekundes", "additionalSpeechSampleRemoved": "Papildu balss paraugs noņemts", "consentDataMessage": "Turpinot, jūsu sarunas, ieraksti un personiskā informācija tiks droši glabāta mūsu serveros. Jūsu audio ieraksti un transkripcijas tiek apstrādātas ar trešo pušu AI pakalpojumiem (ieskaitot Deepgram transkripcijai un OpenAI analīzei), lai sniegtu jums AI vadītus ieskatus un iespējotu visas lietotnes funkcijas.", - "tasksEmptyStateMessage": "Sāciet sarunu, lai izveidotu uzdevumu.", + "tasksEmptyStateMessage": "Uzdevumi no jūsu sarunām parādīsies šeit.", "clearChatAction": "Notīrīt tērzēšanu", "enableApps": "Iespējot lietotnes", "omiAppName": "Omi", @@ -4004,6 +4004,49 @@ "reviewProposalSuggested": "Ieteikts", "reviewProposalConflict": "Kopš Dream to ieteica, tas ir mainījies, tāpēc nekas netika lietots.", "itemLinkListTitle": "{count, plural, =0{Nav vienību} =1{1 vienība} other{{count} vienības}}", + "onboardingPermissionTapToFix": "Pieskarieties, lai labotu", + "onboardingNoOmiYet": "Vēl nav Omi?", + "onboardingOmiOrdered": "Esmu to pasūtījis", + "onboardingOmiNotOrdered": "Vēl neesmu pasūtījis", + "onboardingConnectOmi": "Savienot Omi", + "consentShortMessage": "Omi ar MI palīdzību pārvērš jūsu sarunas piezīmēs. Tās var izdzēst jebkurā laikā.", + "consentAiProviders": "Apstrādā {providers}.", + "onboardingActIntro": "Izmēģināsim Omi.", + "onboardingActMicTitle": "Omi vajag jūsu mikrofonu", + "onboardingActMicBody": "Lai Omi jūs dzirdētu, kad ar to runājat.", + "onboardingActMicAllow": "Atļaut mikrofonu", + "onboardingActDictateTitle": "Pastāstiet Omi par savu dienu", + "onboardingActDictateBody": "Sakiet savā veidā. Pārējo paveiks Omi.", + "onboardingActDictateExample": "Tikko paēdu pusdienas ar Semu, jūnijā viņa pārceļas uz Lisabonu! Atgādini man līdz piektdienai nosūtīt prezentāciju komandai un pa ceļam mājās nopirkt pienu.", + "onboardingActDictateHint": "Kad esat pabeidzis, pieskarieties ķeksītim", + "onboardingActPhoneMic": "Tālruņa mikrofons", + "onboardingActWriting": "Apkopojam…", + "onboardingActRemembers": "Omi atceras, lai jums nebūtu jāatceras", + "onboardingActNotesLabel": "Jūsu diena īsumā", + "onboardingActTasksLabel": "Omi pamanīja jūsu darāmo", + "onboardingActYourWords": "Lūk, ko jūs teicāt", + "onboardingActCongratsTitle": "Viss gatavs, {name}", + "onboardingActRateTitle": "Vai Omi jums patīk?", + "onboardingActRateYes": "Novērtēt Omi", + "onboardingWelcomeCardTitle": "Laipni lūgti Omi", + "onboardingWelcomeCardBody": "Valkājiet Omi un runājiet kā parasti. Katra saruna šeit kļūst par piezīmēm, uzdevumiem un atmiņām.", + "onboardingActDemoTitle": "Pusdienas ar Semu", + "onboardingActDemoSummary": "Pusdienas ar Semu, kura jūnijā pārceļas uz Lisabonu. Līdz piektdienai jānosūta prezentācija komandai un mājupceļā jānopērk piens.", + "onboardingActDemoTask1": "Līdz piektdienai nosūtīt prezentāciju komandai", + "onboardingActDemoTask2": "Pa ceļam mājās nopirkt pienu", + "onboardingActJustNow": "Tikko", + "onboardingActSavedToTasks": "Saglabāts jūsu uzdevumos", + "onboardingConnectTapToConnect": "Pieskarieties savam Omi, lai savienotu.", + "onboardingActMicFallbackBody": "Omi vēl nesūta skaņu. Pagaidām izmantojiet tālruni.", + "onboardingActNoAudioOmi": "Omi nav dzirdams. Pārliecinieties, ka tas ir ieslēgts un tuvumā.", + "onboardingActNoAudioPhone": "Vēl jūs nedzirdam. Mēģiniet runāt mazliet skaļāk.", + "onboardingActDictateBodyOmi": "Vienkārši runājiet. Jūsu Omi klausās.", + "voiceProfileCardDismissed": "Balss profilu varat iestatīt jebkurā laikā iestatījumos.", + "homeNewRecordingButton": "Jauns ieraksts", + "chatWelcomeMessage": "Sveiki, es esmu Omi. Jautā man jebko par to, par ko esi runājis: ko kāds teica, ko tu apsolīji vai kopsavilkumu par tavu dienu. Es varu arī pievienot uzdevumus un atcerēties lietas tavā vietā.", + "onboardingStarterTaskAsk": "Pajautā Omi par savu dienu", + "onboardingStarterTaskRecord": "Ieraksti sarunu ar Omi", + "onboardingStarterTaskCheck": "Atzīmē šo uzdevumu, lai to pabeigtu", "paywallRecommended": "Recommended", "paywallNoPlans": "Plans aren't available right now. Try again in a minute.", "paywallOtherPlans": "See all plans", @@ -4014,21 +4057,6 @@ "paywallManageBilling": "Manage billing", "paywallLoadError": "Couldn't load plans. Check your connection and try again.", "paywallPlans": "Plans", - "firstCaptureTitle": "How will you capture?", - "firstCaptureDescription": "Choose how you want to capture your first conversation.", - "firstCaptureOmi": "I Have an Omi", - "firstCapturePhone": "This Phone", - "firstCaptureWatch": "Apple Watch", - "firstCaptureSkip": "Skip for Now", - "firstCapturePrompt": "Tell Omi what you're working on today, or what's on your mind. Talk for about 30 seconds.", - "firstCaptureKeepGoing": "Keep going… 30 seconds is ideal.", - "firstCaptureReady": "You can finish now, or keep talking.", - "firstCaptureStart": "Start Recording", - "firstCaptureFinish": "Finish Recording", - "firstCaptureListening": "Listening… your words will appear here.", - "firstCaptureProcessing": "Creating your first conversation…", - "firstCaptureResult": "Your first conversation", - "firstCaptureEmpty": "I didn't catch enough speech, try again with a few sentences", "watchCaptureTitle": "Record from your Apple Watch", "watchCaptureOpen": "Open Omi on your Apple Watch.", "watchCaptureRecord": "Tap Record. Allow microphone access if asked.", @@ -4040,17 +4068,6 @@ "pairingNotFoundOrDropped": "Couldn't reach your Omi. Make sure it's charged, close to your phone, and its light is blinking, then try again.", "pairingBondRecovery": "Forget Omi in your phone’s Bluetooth settings, then try pairing again.", "pairingHowTo": "How to pair", - "firstCaptureWatchInstructions": "Open Omi on your Apple Watch and tap Record. Allow microphone access if asked. Your capture will sync to Omi on this phone.", - "firstCaptureWatchInstall": "In the Watch app on your iPhone, install Omi on your Apple Watch. Then open Omi on your watch and tap Record.", - "firstCaptureWatchPair": "Pair your Apple Watch with this iPhone using the Watch app, then install and open Omi on your watch.", - "firstCaptureUnavailable": "Recording could not start. Check microphone access and your connection, then try again.", - "firstCaptureDelayed": "Your recording is taking longer to process. You can check your conversations on Home or try again.", - "firstCaptureEmptyTitle": "Let's try that again", - "firstCaptureShortTitle": "Finish This Recording?", - "firstCaptureShortWarning": "A few more sentences help Omi understand you. About 30 seconds is ideal.", - "firstCaptureCancelTitle": "Leave This Recording?", - "firstCaptureCancelWarning": "Going back stops this recording. Keep talking to give Omi a few more sentences.", - "firstCaptureSiriTip": "Tip: Enable Omi in Shortcuts → Siri.", "paywallOutlineHeadline": "Nekad vairs nepalaidiet garām nevienu vārdu.", "paywallCancelAnytime": "Atcelt jebkurā laikā.", "connectAnAgent": "Pievienot aģentu", diff --git a/app/lib/l10n/app_mk.arb b/app/lib/l10n/app_mk.arb index b4ff49d74a2..51ca0f8bcc7 100644 --- a/app/lib/l10n/app_mk.arb +++ b/app/lib/l10n/app_mk.arb @@ -1586,7 +1586,7 @@ "@enableBluetooth": { "description": "Title for dialog prompting user to enable Bluetooth" }, - "bluetoothNeeded": "Omi има потреба од Bluetooth за поврзување на вашето нослива опрема. Ве молиме активирајте Bluetooth и пробајте повторно.", + "bluetoothNeeded": "Omi се поврзува со телефонот преку Bluetooth. Вклучете го за да продолжите.", "@bluetoothNeeded": { "description": "Explanation text for why Bluetooth is required" }, @@ -5782,7 +5782,7 @@ "@consentDataMessage": { "description": "Consent message explaining how user data will be stored and used" }, - "tasksEmptyStateMessage": "Започнете разговор за да создадете задача.", + "tasksEmptyStateMessage": "Задачите од вашите разговори ќе се појават тука.", "@tasksEmptyStateMessage": { "description": "Empty state message shown when there are no tasks, with instruction to tap + button" }, @@ -11430,6 +11430,49 @@ "reviewProposalSuggested": "Предложено", "reviewProposalConflict": "Ова се промени откако Dream го предложи, па ништо не е применето.", "itemLinkListTitle": "{count, plural, =0{Нема ставки} =1{1 ставка} other{{count} ставки}}", + "onboardingPermissionTapToFix": "Допрете за да поправите", + "onboardingNoOmiYet": "Сè уште немате Omi?", + "onboardingOmiOrdered": "Го нарачав", + "onboardingOmiNotOrdered": "Сè уште не сум нарачал", + "onboardingConnectOmi": "Поврзете Omi", + "consentShortMessage": "Omi користи вештачка интелигенција за да ги претвори вашите разговори во белешки. Можете да ги избришете во секое време.", + "consentAiProviders": "Се обработува со {providers}.", + "onboardingActIntro": "Да го пробаме Omi.", + "onboardingActMicTitle": "На Omi му треба микрофонот", + "onboardingActMicBody": "За Omi да ве слуша кога зборувате со него.", + "onboardingActMicAllow": "Дозволи микрофон", + "onboardingActDictateTitle": "Кажете му на Omi за вашиот ден", + "onboardingActDictateBody": "Кажете го на свој начин. Omi ќе се погрижи за останатото.", + "onboardingActDictateExample": "Штотуку ручав со Сем, се сели во Лисабон во јуни! Потсети ме да ја пратам презентацијата на тимот до петок и да купам млеко на пат кон дома.", + "onboardingActDictateHint": "Допрете ја штиклата кога ќе завршите", + "onboardingActPhoneMic": "Микрофон на телефонот", + "onboardingActWriting": "Сумираме…", + "onboardingActRemembers": "Omi памети наместо вас", + "onboardingActNotesLabel": "Вашиот ден, накратко", + "onboardingActTasksLabel": "Omi ги фати вашите задачи", + "onboardingActYourWords": "Еве што рековте", + "onboardingActCongratsTitle": "Сè е подготвено, {name}", + "onboardingActRateTitle": "Ви се допаѓа Omi досега?", + "onboardingActRateYes": "Оценете го Omi", + "onboardingWelcomeCardTitle": "Добредојдовте во Omi", + "onboardingWelcomeCardBody": "Носете го Omi и зборувајте како и обично. Секој разговор тука станува белешки, задачи и спомени.", + "onboardingActDemoTitle": "Ручек со Сем", + "onboardingActDemoSummary": "Ручек со Сем, која во јуни се сели во Лисабон. Сакате да ја испратите презентацијата до тимот до петок и да купите млеко на пат кон дома.", + "onboardingActDemoTask1": "Да ја пратам презентацијата на тимот до петок", + "onboardingActDemoTask2": "Да купам млеко на пат кон дома", + "onboardingActJustNow": "Токму сега", + "onboardingActSavedToTasks": "Зачувано во вашите задачи", + "onboardingConnectTapToConnect": "Допрете го вашиот Omi за да се поврзете.", + "onboardingActMicFallbackBody": "Omi сè уште не испраќа звук. Засега користете го телефонот.", + "onboardingActNoAudioOmi": "Не се слуша Omi. Проверете дали е вклучен и блиску.", + "onboardingActNoAudioPhone": "Сè уште не ве слушаме. Обидете се да зборувате малку погласно.", + "onboardingActDictateBodyOmi": "Само зборувајте. Вашиот Omi слуша.", + "voiceProfileCardDismissed": "Гласовниот профил можете да го поставите во секое време во Поставки.", + "homeNewRecordingButton": "Ново снимање", + "chatWelcomeMessage": "Здраво, јас сум Omi. Прашај ме било што за она за што си зборувал: што рекол некој, што си ветил или резиме на твојот ден. Можам и да додавам задачи и да паметам работи за тебе.", + "onboardingStarterTaskAsk": "Прашај го Omi за твојот ден", + "onboardingStarterTaskRecord": "Сними разговор со Omi", + "onboardingStarterTaskCheck": "Штиклирај ја оваа задача за да ја завршиш", "paywallRecommended": "Recommended", "paywallNoPlans": "Plans aren't available right now. Try again in a minute.", "paywallOtherPlans": "See all plans", @@ -11440,21 +11483,6 @@ "paywallManageBilling": "Manage billing", "paywallLoadError": "Couldn't load plans. Check your connection and try again.", "paywallPlans": "Plans", - "firstCaptureTitle": "How will you capture?", - "firstCaptureDescription": "Choose how you want to capture your first conversation.", - "firstCaptureOmi": "I Have an Omi", - "firstCapturePhone": "This Phone", - "firstCaptureWatch": "Apple Watch", - "firstCaptureSkip": "Skip for Now", - "firstCapturePrompt": "Tell Omi what you're working on today, or what's on your mind. Talk for about 30 seconds.", - "firstCaptureKeepGoing": "Keep going… 30 seconds is ideal.", - "firstCaptureReady": "You can finish now, or keep talking.", - "firstCaptureStart": "Start Recording", - "firstCaptureFinish": "Finish Recording", - "firstCaptureListening": "Listening… your words will appear here.", - "firstCaptureProcessing": "Creating your first conversation…", - "firstCaptureResult": "Your first conversation", - "firstCaptureEmpty": "I didn't catch enough speech, try again with a few sentences", "watchCaptureTitle": "Record from your Apple Watch", "watchCaptureOpen": "Open Omi on your Apple Watch.", "watchCaptureRecord": "Tap Record. Allow microphone access if asked.", @@ -11466,17 +11494,6 @@ "pairingNotFoundOrDropped": "Couldn't reach your Omi. Make sure it's charged, close to your phone, and its light is blinking, then try again.", "pairingBondRecovery": "Forget Omi in your phone’s Bluetooth settings, then try pairing again.", "pairingHowTo": "How to pair", - "firstCaptureWatchInstructions": "Open Omi on your Apple Watch and tap Record. Allow microphone access if asked. Your capture will sync to Omi on this phone.", - "firstCaptureWatchInstall": "In the Watch app on your iPhone, install Omi on your Apple Watch. Then open Omi on your watch and tap Record.", - "firstCaptureWatchPair": "Pair your Apple Watch with this iPhone using the Watch app, then install and open Omi on your watch.", - "firstCaptureUnavailable": "Recording could not start. Check microphone access and your connection, then try again.", - "firstCaptureDelayed": "Your recording is taking longer to process. You can check your conversations on Home or try again.", - "firstCaptureEmptyTitle": "Let's try that again", - "firstCaptureShortTitle": "Finish This Recording?", - "firstCaptureShortWarning": "A few more sentences help Omi understand you. About 30 seconds is ideal.", - "firstCaptureCancelTitle": "Leave This Recording?", - "firstCaptureCancelWarning": "Going back stops this recording. Keep talking to give Omi a few more sentences.", - "firstCaptureSiriTip": "Tip: Enable Omi in Shortcuts → Siri.", "paywallOutlineHeadline": "Никогаш повеќе не пропуштајте ниту збор.", "paywallCancelAnytime": "Откажете во секое време.", "connectAnAgent": "Поврзете агент", diff --git a/app/lib/l10n/app_mr.arb b/app/lib/l10n/app_mr.arb index a8e8ad1c989..588eea0dee4 100644 --- a/app/lib/l10n/app_mr.arb +++ b/app/lib/l10n/app_mr.arb @@ -1586,7 +1586,7 @@ "@enableBluetooth": { "description": "Title for dialog prompting user to enable Bluetooth" }, - "bluetoothNeeded": "Omi आपल्या पहनने केल्या जाणार्‍या उपकरणाला जोडण्यासाठी Bluetooth आवश्यक आहे. कृपया Bluetooth सक्षम करा आणि पुन्हा प्रयत्न करा.", + "bluetoothNeeded": "Omi तुमच्या फोनशी Bluetooth द्वारे जोडतो. पुढे जाण्यासाठी ते सुरू करा.", "@bluetoothNeeded": { "description": "Explanation text for why Bluetooth is required" }, @@ -5782,7 +5782,7 @@ "@consentDataMessage": { "description": "Consent message explaining how user data will be stored and used" }, - "tasksEmptyStateMessage": "कार्य तयार करण्यासाठी संभाषण सुरू करा.", + "tasksEmptyStateMessage": "तुमच्या संभाषणांमधील कामे इथे दिसतील.", "@tasksEmptyStateMessage": { "description": "Empty state message shown when there are no tasks, with instruction to tap + button" }, @@ -11430,6 +11430,49 @@ "reviewProposalSuggested": "सुचवलेले", "reviewProposalConflict": "Dream ने सुचवल्यानंतर हे बदलले, म्हणून काहीही लागू केले नाही.", "itemLinkListTitle": "{count, plural, =0{कोणतेही आयटम नाहीत} =1{1 आयटम} other{{count} आयटम}}", + "onboardingPermissionTapToFix": "दुरुस्त करण्यासाठी टॅप करा", + "onboardingNoOmiYet": "अजून तुमचा Omi आला नाही?", + "onboardingOmiOrdered": "मी ऑर्डर केला आहे", + "onboardingOmiNotOrdered": "अजून ऑर्डर केलेला नाही", + "onboardingConnectOmi": "Omi कनेक्ट करा", + "consentShortMessage": "Omi AI वापरून तुमच्या संभाषणांच्या नोट्स बनवते. तुम्ही त्या कधीही हटवू शकता.", + "consentAiProviders": "{providers} द्वारे प्रक्रिया केली जाते.", + "onboardingActIntro": "चला, Omi वापरून पाहू.", + "onboardingActMicTitle": "Omi ला तुमचा मायक्रोफोन हवा आहे", + "onboardingActMicBody": "तुम्ही बोलता तेव्हा Omi ला ऐकू यावे म्हणून.", + "onboardingActMicAllow": "मायक्रोफोनला परवानगी द्या", + "onboardingActDictateTitle": "Omi ला तुमच्या दिवसाबद्दल सांगा", + "onboardingActDictateBody": "तुमच्या पद्धतीने सांगा. बाकी Omi पाहील.", + "onboardingActDictateExample": "आत्ताच सॅमसोबत जेवलो, ती जूनमध्ये लिस्बनला जातेय! शुक्रवारपर्यंत टीमला डेक पाठवायची आणि घरी जाताना दूध घ्यायची आठवण करून दे.", + "onboardingActDictateHint": "झाले की टिकवर टॅप करा", + "onboardingActPhoneMic": "फोनचा मायक्रोफोन", + "onboardingActWriting": "सारांश तयार होत आहे…", + "onboardingActRemembers": "तुम्हाला लक्षात ठेवावं लागू नये म्हणून Omi लक्षात ठेवतो", + "onboardingActNotesLabel": "तुमचा दिवस, थोडक्यात", + "onboardingActTasksLabel": "Omi ने तुमची कामे टिपली", + "onboardingActYourWords": "तुम्ही हे सांगितले", + "onboardingActCongratsTitle": "सगळे तयार आहे, {name}", + "onboardingActRateTitle": "आत्तापर्यंत Omi आवडतोय का?", + "onboardingActRateYes": "Omi ला रेटिंग द्या", + "onboardingWelcomeCardTitle": "Omi मध्ये स्वागत आहे", + "onboardingWelcomeCardBody": "Omi घाला आणि नेहमीसारखे बोला. प्रत्येक संभाषण इथेच नोट्स, कामे आणि आठवणी बनते.", + "onboardingActDemoTitle": "सॅमसोबत जेवण", + "onboardingActDemoSummary": "जूनमध्ये लिस्बनला जाणाऱ्या सॅमसोबत जेवण. शुक्रवारपर्यंत टीमला डेक पाठवायचा आहे आणि घरी जाताना दूध घ्यायचे आहे.", + "onboardingActDemoTask1": "शुक्रवारपर्यंत टीमला डेक पाठवणे", + "onboardingActDemoTask2": "घरी जाताना दूध घेणे", + "onboardingActJustNow": "आत्ताच", + "onboardingActSavedToTasks": "तुमच्या कामांमध्ये जतन केले", + "onboardingConnectTapToConnect": "कनेक्ट करण्यासाठी तुमच्या Omi वर टॅप करा.", + "onboardingActMicFallbackBody": "तुमचा Omi अजून ऑडिओ पाठवत नाही. सध्या तुमचा फोन वापरा.", + "onboardingActNoAudioOmi": "तुमचा Omi ऐकू येत नाही. तो सुरू आहे आणि जवळ आहे का ते पाहा.", + "onboardingActNoAudioPhone": "अजून तुमचा आवाज ऐकू येत नाही. थोडं मोठ्याने बोलून पाहा.", + "onboardingActDictateBodyOmi": "फक्त बोला. तुमचा Omi ऐकत आहे.", + "voiceProfileCardDismissed": "सेटिंग्जमध्ये कधीही व्हॉइस प्रोफाइल सेट करू शकता.", + "homeNewRecordingButton": "नवीन रेकॉर्डिंग", + "chatWelcomeMessage": "नमस्कार, मी Omi आहे. तुम्ही ज्याबद्दल बोललात त्याबद्दल काहीही विचारा: कोणी काय सांगितले, तुम्ही काय वचन दिले किंवा तुमच्या दिवसाचा सारांश. मी कामे जोडू शकतो आणि तुमच्यासाठी गोष्टी लक्षातही ठेवू शकतो.", + "onboardingStarterTaskAsk": "Omi ला तुमच्या दिवसाबद्दल विचारा", + "onboardingStarterTaskRecord": "Omi ने एक संभाषण रेकॉर्ड करा", + "onboardingStarterTaskCheck": "पूर्ण करण्यासाठी हे काम टिक करा", "paywallRecommended": "Recommended", "paywallNoPlans": "Plans aren't available right now. Try again in a minute.", "paywallOtherPlans": "See all plans", @@ -11440,21 +11483,6 @@ "paywallManageBilling": "Manage billing", "paywallLoadError": "Couldn't load plans. Check your connection and try again.", "paywallPlans": "Plans", - "firstCaptureTitle": "How will you capture?", - "firstCaptureDescription": "Choose how you want to capture your first conversation.", - "firstCaptureOmi": "I Have an Omi", - "firstCapturePhone": "This Phone", - "firstCaptureWatch": "Apple Watch", - "firstCaptureSkip": "Skip for Now", - "firstCapturePrompt": "Tell Omi what you're working on today, or what's on your mind. Talk for about 30 seconds.", - "firstCaptureKeepGoing": "Keep going… 30 seconds is ideal.", - "firstCaptureReady": "You can finish now, or keep talking.", - "firstCaptureStart": "Start Recording", - "firstCaptureFinish": "Finish Recording", - "firstCaptureListening": "Listening… your words will appear here.", - "firstCaptureProcessing": "Creating your first conversation…", - "firstCaptureResult": "Your first conversation", - "firstCaptureEmpty": "I didn't catch enough speech, try again with a few sentences", "watchCaptureTitle": "Record from your Apple Watch", "watchCaptureOpen": "Open Omi on your Apple Watch.", "watchCaptureRecord": "Tap Record. Allow microphone access if asked.", @@ -11466,17 +11494,6 @@ "pairingNotFoundOrDropped": "Couldn't reach your Omi. Make sure it's charged, close to your phone, and its light is blinking, then try again.", "pairingBondRecovery": "Forget Omi in your phone’s Bluetooth settings, then try pairing again.", "pairingHowTo": "How to pair", - "firstCaptureWatchInstructions": "Open Omi on your Apple Watch and tap Record. Allow microphone access if asked. Your capture will sync to Omi on this phone.", - "firstCaptureWatchInstall": "In the Watch app on your iPhone, install Omi on your Apple Watch. Then open Omi on your watch and tap Record.", - "firstCaptureWatchPair": "Pair your Apple Watch with this iPhone using the Watch app, then install and open Omi on your watch.", - "firstCaptureUnavailable": "Recording could not start. Check microphone access and your connection, then try again.", - "firstCaptureDelayed": "Your recording is taking longer to process. You can check your conversations on Home or try again.", - "firstCaptureEmptyTitle": "Let's try that again", - "firstCaptureShortTitle": "Finish This Recording?", - "firstCaptureShortWarning": "A few more sentences help Omi understand you. About 30 seconds is ideal.", - "firstCaptureCancelTitle": "Leave This Recording?", - "firstCaptureCancelWarning": "Going back stops this recording. Keep talking to give Omi a few more sentences.", - "firstCaptureSiriTip": "Tip: Enable Omi in Shortcuts → Siri.", "paywallOutlineHeadline": "आता एकही शब्द चुकवू नका.", "paywallCancelAnytime": "कधीही रद्द करा.", "connectAnAgent": "एजंट कनेक्ट करा", diff --git a/app/lib/l10n/app_ms.arb b/app/lib/l10n/app_ms.arb index 9aad221dff9..568e8b81be7 100644 --- a/app/lib/l10n/app_ms.arb +++ b/app/lib/l10n/app_ms.arb @@ -520,7 +520,7 @@ "dontShowAgain": "Jangan tunjukkan lagi", "iUnderstand": "Saya Faham", "enableBluetooth": "Dayakan Bluetooth", - "bluetoothNeeded": "Omi memerlukan Bluetooth untuk menyambung ke peranti boleh pakai anda. Sila dayakan Bluetooth dan cuba lagi.", + "bluetoothNeeded": "Omi bersambung ke telefon anda melalui Bluetooth. Hidupkannya untuk meneruskan.", "contactSupport": "Hubungi Sokongan?", "connectLater": "Sambung Kemudian", "grantPermissions": "Berikan kebenaran", @@ -1501,7 +1501,7 @@ "durationSeconds": "Tempoh: {seconds} saat", "additionalSpeechSampleRemoved": "Sampel suara tambahan dibuang", "consentDataMessage": "Dengan meneruskan, perbualan, rakaman dan maklumat peribadi anda akan disimpan dengan selamat di pelayan kami. Rakaman audio dan transkrip anda diproses oleh perkhidmatan AI pihak ketiga (termasuk Deepgram untuk transkripsi dan OpenAI untuk analisis) untuk memberikan anda pandangan dikuasakan AI dan membolehkan semua ciri aplikasi.", - "tasksEmptyStateMessage": "Mulakan perbualan untuk mencipta tugasan.", + "tasksEmptyStateMessage": "Tugasan daripada perbualan anda akan dipaparkan di sini.", "clearChatAction": "Kosongkan sembang", "enableApps": "Dayakan aplikasi", "omiAppName": "Omi", @@ -4004,6 +4004,49 @@ "reviewProposalSuggested": "Dicadangkan", "reviewProposalConflict": "Ini telah berubah sejak Dream mencadangkannya, jadi tiada apa yang digunakan.", "itemLinkListTitle": "{count, plural, =0{Tiada Item} =1{1 Item} other{{count} Item}}", + "onboardingPermissionTapToFix": "Ketik untuk membetulkan", + "onboardingNoOmiYet": "Belum ada Omi lagi?", + "onboardingOmiOrdered": "Saya sudah memesannya", + "onboardingOmiNotOrdered": "Belum memesan", + "onboardingConnectOmi": "Sambungkan Omi", + "consentShortMessage": "Omi menggunakan AI untuk menukar perbualan anda kepada nota. Anda boleh memadamkannya bila-bila masa.", + "consentAiProviders": "Diproses dengan {providers}.", + "onboardingActIntro": "Jom cuba Omi.", + "onboardingActMicTitle": "Omi perlukan mikrofon anda", + "onboardingActMicBody": "Supaya Omi dapat mendengar anda bercakap.", + "onboardingActMicAllow": "Benarkan Mikrofon", + "onboardingActDictateTitle": "Ceritakan hari anda kepada Omi", + "onboardingActDictateBody": "Cakap dengan cara anda. Omi uruskan selebihnya.", + "onboardingActDictateExample": "Baru makan tengah hari dengan Sam, dia akan pindah ke Lisbon bulan Jun! Ingatkan saya hantar deck kepada pasukan sebelum Jumaat, dan beli susu dalam perjalanan pulang.", + "onboardingActDictateHint": "Ketik tanda semak apabila selesai", + "onboardingActPhoneMic": "Mikrofon telefon", + "onboardingActWriting": "Meringkaskan…", + "onboardingActRemembers": "Omi ingat supaya anda tidak perlu", + "onboardingActNotesLabel": "Hari anda, diringkaskan", + "onboardingActTasksLabel": "Omi menangkap tugasan anda", + "onboardingActYourWords": "Ini yang anda katakan", + "onboardingActCongratsTitle": "Semua sudah sedia, {name}", + "onboardingActRateTitle": "Suka Omi setakat ini?", + "onboardingActRateYes": "Nilai Omi", + "onboardingWelcomeCardTitle": "Selamat datang ke Omi", + "onboardingWelcomeCardBody": "Pakai Omi dan bercakap seperti biasa. Setiap perbualan menjadi nota, tugasan dan kenangan di sini.", + "onboardingActDemoTitle": "Makan tengah hari dengan Sam", + "onboardingActDemoSummary": "Makan tengah hari dengan Sam, yang berpindah ke Lisbon pada Jun. Anda mahu hantar dek kepada pasukan sebelum Jumaat dan beli susu dalam perjalanan pulang.", + "onboardingActDemoTask1": "Hantar deck kepada pasukan sebelum Jumaat", + "onboardingActDemoTask2": "Beli susu dalam perjalanan pulang", + "onboardingActJustNow": "Baru sahaja", + "onboardingActSavedToTasks": "Disimpan ke tugasan anda", + "onboardingConnectTapToConnect": "Ketik Omi anda untuk menyambung.", + "onboardingActMicFallbackBody": "Omi anda belum menghantar audio. Gunakan telefon anda buat masa ini.", + "onboardingActNoAudioOmi": "Omi anda tidak kedengaran. Pastikan ia dihidupkan dan dekat.", + "onboardingActNoAudioPhone": "Suara anda belum kedengaran. Cuba bercakap lebih kuat sedikit.", + "onboardingActDictateBodyOmi": "Cakap sahaja. Omi anda sedang mendengar.", + "voiceProfileCardDismissed": "Anda boleh menyediakan Profil Suara pada bila-bila masa dalam Tetapan.", + "homeNewRecordingButton": "Rakaman Baharu", + "chatWelcomeMessage": "Hai, saya Omi. Tanya saya apa sahaja tentang perkara yang anda bualkan: apa yang dikatakan seseorang, apa yang anda janjikan atau ringkasan hari anda. Saya juga boleh menambah tugasan dan mengingati sesuatu untuk anda.", + "onboardingStarterTaskAsk": "Tanya Omi tentang hari anda", + "onboardingStarterTaskRecord": "Rakam perbualan dengan Omi", + "onboardingStarterTaskCheck": "Tandakan tugasan ini untuk menyelesaikannya", "paywallRecommended": "Recommended", "paywallNoPlans": "Plans aren't available right now. Try again in a minute.", "paywallOtherPlans": "See all plans", @@ -4014,21 +4057,6 @@ "paywallManageBilling": "Manage billing", "paywallLoadError": "Couldn't load plans. Check your connection and try again.", "paywallPlans": "Plans", - "firstCaptureTitle": "How will you capture?", - "firstCaptureDescription": "Choose how you want to capture your first conversation.", - "firstCaptureOmi": "I Have an Omi", - "firstCapturePhone": "This Phone", - "firstCaptureWatch": "Apple Watch", - "firstCaptureSkip": "Skip for Now", - "firstCapturePrompt": "Tell Omi what you're working on today, or what's on your mind. Talk for about 30 seconds.", - "firstCaptureKeepGoing": "Keep going… 30 seconds is ideal.", - "firstCaptureReady": "You can finish now, or keep talking.", - "firstCaptureStart": "Start Recording", - "firstCaptureFinish": "Finish Recording", - "firstCaptureListening": "Listening… your words will appear here.", - "firstCaptureProcessing": "Creating your first conversation…", - "firstCaptureResult": "Your first conversation", - "firstCaptureEmpty": "I didn't catch enough speech, try again with a few sentences", "watchCaptureTitle": "Record from your Apple Watch", "watchCaptureOpen": "Open Omi on your Apple Watch.", "watchCaptureRecord": "Tap Record. Allow microphone access if asked.", @@ -4040,17 +4068,6 @@ "pairingNotFoundOrDropped": "Couldn't reach your Omi. Make sure it's charged, close to your phone, and its light is blinking, then try again.", "pairingBondRecovery": "Forget Omi in your phone’s Bluetooth settings, then try pairing again.", "pairingHowTo": "How to pair", - "firstCaptureWatchInstructions": "Open Omi on your Apple Watch and tap Record. Allow microphone access if asked. Your capture will sync to Omi on this phone.", - "firstCaptureWatchInstall": "In the Watch app on your iPhone, install Omi on your Apple Watch. Then open Omi on your watch and tap Record.", - "firstCaptureWatchPair": "Pair your Apple Watch with this iPhone using the Watch app, then install and open Omi on your watch.", - "firstCaptureUnavailable": "Recording could not start. Check microphone access and your connection, then try again.", - "firstCaptureDelayed": "Your recording is taking longer to process. You can check your conversations on Home or try again.", - "firstCaptureEmptyTitle": "Let's try that again", - "firstCaptureShortTitle": "Finish This Recording?", - "firstCaptureShortWarning": "A few more sentences help Omi understand you. About 30 seconds is ideal.", - "firstCaptureCancelTitle": "Leave This Recording?", - "firstCaptureCancelWarning": "Going back stops this recording. Keep talking to give Omi a few more sentences.", - "firstCaptureSiriTip": "Tip: Enable Omi in Shortcuts → Siri.", "paywallOutlineHeadline": "Jangan terlepas satu perkataan pun lagi.", "paywallCancelAnytime": "Batalkan pada bila-bila masa.", "connectAnAgent": "Sambungkan Ejen", diff --git a/app/lib/l10n/app_nl.arb b/app/lib/l10n/app_nl.arb index 7c889e35342..3d43e40b345 100644 --- a/app/lib/l10n/app_nl.arb +++ b/app/lib/l10n/app_nl.arb @@ -520,7 +520,7 @@ "dontShowAgain": "Niet meer weergeven", "iUnderstand": "Ik begrijp het", "enableBluetooth": "Bluetooth inschakelen", - "bluetoothNeeded": "Omi heeft Bluetooth nodig om verbinding te maken met je wearable. Schakel Bluetooth in en probeer het opnieuw.", + "bluetoothNeeded": "Omi maakt via Bluetooth verbinding met je telefoon. Zet het aan om door te gaan.", "contactSupport": "Contact opnemen met support?", "connectLater": "Later verbinden", "grantPermissions": "Machtigingen verlenen", @@ -1501,7 +1501,7 @@ "durationSeconds": "Duur: {seconds} seconden", "additionalSpeechSampleRemoved": "Extra spraakvoorbeeld verwijderd", "consentDataMessage": "Door verder te gaan worden uw gesprekken, opnames en persoonlijke informatie veilig opgeslagen op onze servers. Uw audio-opnames en transcripties worden verwerkt door AI-diensten van derden (waaronder Deepgram voor transcriptie en OpenAI voor analyse) om u AI-gestuurde inzichten te bieden en alle app-functies mogelijk te maken.", - "tasksEmptyStateMessage": "Begin een gesprek om een taak aan te maken.", + "tasksEmptyStateMessage": "Taken uit je gesprekken verschijnen hier.", "clearChatAction": "Chat wissen", "enableApps": "Apps inschakelen", "omiAppName": "Omi", @@ -4004,6 +4004,49 @@ "reviewProposalSuggested": "Voorgesteld", "reviewProposalConflict": "Dit is veranderd sinds Dream het voorstelde, dus er is niets toegepast.", "itemLinkListTitle": "{count, plural, =0{Geen items} =1{1 item} other{{count} items}}", + "onboardingPermissionTapToFix": "Tik om te herstellen", + "onboardingNoOmiYet": "Heb je je Omi nog niet?", + "onboardingOmiOrdered": "Ik heb hem besteld", + "onboardingOmiNotOrdered": "Nog niet besteld", + "onboardingConnectOmi": "Omi verbinden", + "consentShortMessage": "Omi maakt met AI notities van je gesprekken. Je kunt ze altijd verwijderen.", + "consentAiProviders": "Verwerkt met {providers}.", + "onboardingActIntro": "Probeer Omi eens.", + "onboardingActMicTitle": "Omi heeft je microfoon nodig", + "onboardingActMicBody": "Zodat Omi je hoort als je tegen hem praat.", + "onboardingActMicAllow": "Microfoon toestaan", + "onboardingActDictateTitle": "Vertel Omi over je dag", + "onboardingActDictateBody": "Zeg het op jouw manier. Omi doet de rest.", + "onboardingActDictateExample": "Net geluncht met Sam, ze verhuist in juni naar Lissabon! Herinner me eraan de presentatie vóór vrijdag naar het team te sturen en op weg naar huis melk te halen.", + "onboardingActDictateHint": "Tik op het vinkje als je klaar bent", + "onboardingActPhoneMic": "Telefoonmicrofoon", + "onboardingActWriting": "Samenvatten…", + "onboardingActRemembers": "Omi onthoudt het, zodat jij dat niet hoeft", + "onboardingActNotesLabel": "Je dag, samengevat", + "onboardingActTasksLabel": "Omi heeft je to-do's opgepikt", + "onboardingActYourWords": "Dit zei je", + "onboardingActCongratsTitle": "Je bent klaar, {name}", + "onboardingActRateTitle": "Bevalt Omi tot nu toe?", + "onboardingActRateYes": "Omi beoordelen", + "onboardingWelcomeCardTitle": "Welkom bij Omi", + "onboardingWelcomeCardBody": "Draag Omi en praat zoals altijd. Elk gesprek wordt hier notities, taken en herinneringen.", + "onboardingActDemoTitle": "Lunch met Sam", + "onboardingActDemoSummary": "Lunch met Sam, die in juni naar Lissabon verhuist. Je wilt de presentatie vóór vrijdag naar je team sturen en op weg naar huis melk halen.", + "onboardingActDemoTask1": "Presentatie vóór vrijdag naar het team sturen", + "onboardingActDemoTask2": "Op weg naar huis melk halen", + "onboardingActJustNow": "Zojuist", + "onboardingActSavedToTasks": "Opgeslagen in je taken", + "onboardingConnectTapToConnect": "Tik op je Omi om te verbinden.", + "onboardingActMicFallbackBody": "Je Omi stuurt nog geen geluid. Gebruik voorlopig je telefoon.", + "onboardingActNoAudioOmi": "We horen je Omi niet. Controleer of hij aan staat en dichtbij is.", + "onboardingActNoAudioPhone": "We horen je nog niet. Probeer iets harder te praten.", + "onboardingActDictateBodyOmi": "Praat gewoon. Je Omi luistert.", + "voiceProfileCardDismissed": "Je kunt je stemprofiel altijd instellen via Instellingen.", + "homeNewRecordingButton": "Nieuwe opname", + "chatWelcomeMessage": "Hoi, ik ben Omi. Vraag me alles over waar je het over hebt gehad: wat iemand zei, wat je beloofde of een samenvatting van je dag. Ik kan ook taken toevoegen en dingen voor je onthouden.", + "onboardingStarterTaskAsk": "Vraag Omi naar je dag", + "onboardingStarterTaskRecord": "Neem een gesprek op met Omi", + "onboardingStarterTaskCheck": "Vink deze taak af om hem af te ronden", "paywallRecommended": "Recommended", "paywallNoPlans": "Plans aren't available right now. Try again in a minute.", "paywallOtherPlans": "See all plans", @@ -4014,21 +4057,6 @@ "paywallManageBilling": "Manage billing", "paywallLoadError": "Couldn't load plans. Check your connection and try again.", "paywallPlans": "Plans", - "firstCaptureTitle": "How will you capture?", - "firstCaptureDescription": "Choose how you want to capture your first conversation.", - "firstCaptureOmi": "I Have an Omi", - "firstCapturePhone": "This Phone", - "firstCaptureWatch": "Apple Watch", - "firstCaptureSkip": "Skip for Now", - "firstCapturePrompt": "Tell Omi what you're working on today, or what's on your mind. Talk for about 30 seconds.", - "firstCaptureKeepGoing": "Keep going… 30 seconds is ideal.", - "firstCaptureReady": "You can finish now, or keep talking.", - "firstCaptureStart": "Start Recording", - "firstCaptureFinish": "Finish Recording", - "firstCaptureListening": "Listening… your words will appear here.", - "firstCaptureProcessing": "Creating your first conversation…", - "firstCaptureResult": "Your first conversation", - "firstCaptureEmpty": "I didn't catch enough speech, try again with a few sentences", "watchCaptureTitle": "Record from your Apple Watch", "watchCaptureOpen": "Open Omi on your Apple Watch.", "watchCaptureRecord": "Tap Record. Allow microphone access if asked.", @@ -4040,17 +4068,6 @@ "pairingNotFoundOrDropped": "Couldn't reach your Omi. Make sure it's charged, close to your phone, and its light is blinking, then try again.", "pairingBondRecovery": "Forget Omi in your phone’s Bluetooth settings, then try pairing again.", "pairingHowTo": "How to pair", - "firstCaptureWatchInstructions": "Open Omi on your Apple Watch and tap Record. Allow microphone access if asked. Your capture will sync to Omi on this phone.", - "firstCaptureWatchInstall": "In the Watch app on your iPhone, install Omi on your Apple Watch. Then open Omi on your watch and tap Record.", - "firstCaptureWatchPair": "Pair your Apple Watch with this iPhone using the Watch app, then install and open Omi on your watch.", - "firstCaptureUnavailable": "Recording could not start. Check microphone access and your connection, then try again.", - "firstCaptureDelayed": "Your recording is taking longer to process. You can check your conversations on Home or try again.", - "firstCaptureEmptyTitle": "Let's try that again", - "firstCaptureShortTitle": "Finish This Recording?", - "firstCaptureShortWarning": "A few more sentences help Omi understand you. About 30 seconds is ideal.", - "firstCaptureCancelTitle": "Leave This Recording?", - "firstCaptureCancelWarning": "Going back stops this recording. Keep talking to give Omi a few more sentences.", - "firstCaptureSiriTip": "Tip: Enable Omi in Shortcuts → Siri.", "paywallOutlineHeadline": "Mis nooit meer een woord.", "paywallCancelAnytime": "Opzeggen kan altijd.", "connectAnAgent": "Een agent koppelen", diff --git a/app/lib/l10n/app_no.arb b/app/lib/l10n/app_no.arb index 83743d582f0..10c984d1225 100644 --- a/app/lib/l10n/app_no.arb +++ b/app/lib/l10n/app_no.arb @@ -520,7 +520,7 @@ "dontShowAgain": "Ikke vis igjen", "iUnderstand": "Jeg forstår", "enableBluetooth": "Aktiver Bluetooth", - "bluetoothNeeded": "Omi trenger Bluetooth for å koble til den bærbare enheten din. Aktiver Bluetooth og prøv igjen.", + "bluetoothNeeded": "Omi kobles til telefonen via Bluetooth. Slå det på for å fortsette.", "contactSupport": "Kontakte support?", "connectLater": "Koble til senere", "grantPermissions": "Gi tillatelser", @@ -1501,7 +1501,7 @@ "durationSeconds": "Varighet: {seconds} sekunder", "additionalSpeechSampleRemoved": "Ekstra taleprøve fjernet", "consentDataMessage": "Ved å fortsette vil samtalene, opptakene og den personlige informasjonen din bli lagret sikkert på våre servere. Lydopptakene og transkripsjonene dine behandles av tredjeparts AI-tjenester (inkludert Deepgram for transkripsjon og OpenAI for analyse) for å gi deg AI-drevne innsikter og aktivere alle appfunksjoner.", - "tasksEmptyStateMessage": "Start en samtale for å opprette en oppgave.", + "tasksEmptyStateMessage": "Gjøremål fra samtalene dine vises her.", "clearChatAction": "Tøm chat", "enableApps": "Aktiver apper", "omiAppName": "Omi", @@ -4004,6 +4004,49 @@ "reviewProposalSuggested": "Foreslått", "reviewProposalConflict": "Dette er endret siden Dream foreslo det, så ingenting ble brukt.", "itemLinkListTitle": "{count, plural, =0{Ingen elementer} =1{1 element} other{{count} elementer}}", + "onboardingPermissionTapToFix": "Trykk for å fikse", + "onboardingNoOmiYet": "Har du ikke fått Omi ennå?", + "onboardingOmiOrdered": "Jeg har bestilt den", + "onboardingOmiNotOrdered": "Ikke bestilt ennå", + "onboardingConnectOmi": "Koble til Omi", + "consentShortMessage": "Omi bruker KI til å gjøre samtalene dine om til notater. Du kan slette dem når som helst.", + "consentAiProviders": "Behandles med {providers}.", + "onboardingActIntro": "La oss prøve Omi.", + "onboardingActMicTitle": "Omi trenger mikrofonen din", + "onboardingActMicBody": "Så Omi hører deg når du snakker til den.", + "onboardingActMicAllow": "Tillat mikrofon", + "onboardingActDictateTitle": "Fortell Omi om dagen din", + "onboardingActDictateBody": "Si det på din måte. Omi tar seg av resten.", + "onboardingActDictateExample": "Spiste nettopp lunsj med Sam, hun flytter til Lisboa i juni! Minn meg på å sende presentasjonen til teamet innen fredag og kjøpe melk på vei hjem.", + "onboardingActDictateHint": "Trykk på haken når du er ferdig", + "onboardingActPhoneMic": "Telefonmikrofon", + "onboardingActWriting": "Oppsummerer…", + "onboardingActRemembers": "Omi husker det, så du slipper", + "onboardingActNotesLabel": "Dagen din, oppsummert", + "onboardingActTasksLabel": "Omi fanget opp gjøremålene dine", + "onboardingActYourWords": "Dette sa du", + "onboardingActCongratsTitle": "Alt er klart, {name}", + "onboardingActRateTitle": "Liker du Omi så langt?", + "onboardingActRateYes": "Gi Omi en vurdering", + "onboardingWelcomeCardTitle": "Velkommen til Omi", + "onboardingWelcomeCardBody": "Bruk Omi og snakk som vanlig. Hver samtale blir til notater, oppgaver og minner her.", + "onboardingActDemoTitle": "Lunsj med Sam", + "onboardingActDemoSummary": "Lunsj med Sam, som flytter til Lisboa i juni. Du vil sende presentasjonen til teamet innen fredag og kjøpe melk på vei hjem.", + "onboardingActDemoTask1": "Send presentasjonen til teamet innen fredag", + "onboardingActDemoTask2": "Kjøp melk på vei hjem", + "onboardingActJustNow": "Akkurat nå", + "onboardingActSavedToTasks": "Lagret i oppgavene dine", + "onboardingConnectTapToConnect": "Trykk på Omi-en din for å koble til.", + "onboardingActMicFallbackBody": "Omi-en din sender ikke lyd ennå. Bruk telefonen inntil videre.", + "onboardingActNoAudioOmi": "Hører ikke Omi-en din. Sjekk at den er på og i nærheten.", + "onboardingActNoAudioPhone": "Hører deg ikke ennå. Prøv å snakke litt høyere.", + "onboardingActDictateBodyOmi": "Bare snakk. Omi-en din lytter.", + "voiceProfileCardDismissed": "Du kan sette opp stemmeprofilen når som helst i Innstillinger.", + "homeNewRecordingButton": "Ny opptak", + "chatWelcomeMessage": "Hei, jeg er Omi. Spør meg om alt du har snakket om: hva noen sa, hva du lovet, eller et sammendrag av dagen din. Jeg kan også legge til oppgaver og huske ting for deg.", + "onboardingStarterTaskAsk": "Spør Omi om dagen din", + "onboardingStarterTaskRecord": "Ta opp en samtale med Omi", + "onboardingStarterTaskCheck": "Kryss av denne oppgaven for å fullføre den", "paywallRecommended": "Recommended", "paywallNoPlans": "Plans aren't available right now. Try again in a minute.", "paywallOtherPlans": "See all plans", @@ -4014,21 +4057,6 @@ "paywallManageBilling": "Manage billing", "paywallLoadError": "Couldn't load plans. Check your connection and try again.", "paywallPlans": "Plans", - "firstCaptureTitle": "How will you capture?", - "firstCaptureDescription": "Choose how you want to capture your first conversation.", - "firstCaptureOmi": "I Have an Omi", - "firstCapturePhone": "This Phone", - "firstCaptureWatch": "Apple Watch", - "firstCaptureSkip": "Skip for Now", - "firstCapturePrompt": "Tell Omi what you're working on today, or what's on your mind. Talk for about 30 seconds.", - "firstCaptureKeepGoing": "Keep going… 30 seconds is ideal.", - "firstCaptureReady": "You can finish now, or keep talking.", - "firstCaptureStart": "Start Recording", - "firstCaptureFinish": "Finish Recording", - "firstCaptureListening": "Listening… your words will appear here.", - "firstCaptureProcessing": "Creating your first conversation…", - "firstCaptureResult": "Your first conversation", - "firstCaptureEmpty": "I didn't catch enough speech, try again with a few sentences", "watchCaptureTitle": "Record from your Apple Watch", "watchCaptureOpen": "Open Omi on your Apple Watch.", "watchCaptureRecord": "Tap Record. Allow microphone access if asked.", @@ -4040,17 +4068,6 @@ "pairingNotFoundOrDropped": "Couldn't reach your Omi. Make sure it's charged, close to your phone, and its light is blinking, then try again.", "pairingBondRecovery": "Forget Omi in your phone’s Bluetooth settings, then try pairing again.", "pairingHowTo": "How to pair", - "firstCaptureWatchInstructions": "Open Omi on your Apple Watch and tap Record. Allow microphone access if asked. Your capture will sync to Omi on this phone.", - "firstCaptureWatchInstall": "In the Watch app on your iPhone, install Omi on your Apple Watch. Then open Omi on your watch and tap Record.", - "firstCaptureWatchPair": "Pair your Apple Watch with this iPhone using the Watch app, then install and open Omi on your watch.", - "firstCaptureUnavailable": "Recording could not start. Check microphone access and your connection, then try again.", - "firstCaptureDelayed": "Your recording is taking longer to process. You can check your conversations on Home or try again.", - "firstCaptureEmptyTitle": "Let's try that again", - "firstCaptureShortTitle": "Finish This Recording?", - "firstCaptureShortWarning": "A few more sentences help Omi understand you. About 30 seconds is ideal.", - "firstCaptureCancelTitle": "Leave This Recording?", - "firstCaptureCancelWarning": "Going back stops this recording. Keep talking to give Omi a few more sentences.", - "firstCaptureSiriTip": "Tip: Enable Omi in Shortcuts → Siri.", "paywallOutlineHeadline": "Gå aldri glipp av et ord igjen.", "paywallCancelAnytime": "Avbryt når som helst.", "connectAnAgent": "Koble til en agent", diff --git a/app/lib/l10n/app_pl.arb b/app/lib/l10n/app_pl.arb index ddc2bafe889..7b9ecb2319c 100644 --- a/app/lib/l10n/app_pl.arb +++ b/app/lib/l10n/app_pl.arb @@ -520,7 +520,7 @@ "dontShowAgain": "Nie pokazuj ponownie", "iUnderstand": "Rozumiem", "enableBluetooth": "Włącz Bluetooth", - "bluetoothNeeded": "Omi potrzebuje Bluetooth, aby połączyć się z Twoim urządzeniem noszonym. Włącz Bluetooth i spróbuj ponownie.", + "bluetoothNeeded": "Omi łączy się z telefonem przez Bluetooth. Włącz go, aby kontynuować.", "contactSupport": "Skontaktuj się z pomocą techniczną?", "connectLater": "Połącz później", "grantPermissions": "Przyznaj uprawnienia", @@ -1501,7 +1501,7 @@ "durationSeconds": "Czas trwania: {seconds} sekund", "additionalSpeechSampleRemoved": "Dodatkowa próbka głosowa usunięta", "consentDataMessage": "Kontynuując, Twoje rozmowy, nagrania i dane osobowe będą bezpiecznie przechowywane na naszych serwerach. Twoje nagrania audio i transkrypcje są przetwarzane przez zewnętrzne usługi AI (w tym Deepgram do transkrypcji i OpenAI do analizy), aby dostarczyć Ci wglądy oparte na AI i umożliwić wszystkie funkcje aplikacji.", - "tasksEmptyStateMessage": "Rozpocznij rozmowę, aby utworzyć zadanie.", + "tasksEmptyStateMessage": "Zadania z Twoich rozmów pojawią się tutaj.", "clearChatAction": "Wyczyść czat", "enableApps": "Włącz aplikacje", "omiAppName": "Omi", @@ -4039,6 +4039,49 @@ "reviewProposalSuggested": "Sugerowane", "reviewProposalConflict": "To się zmieniło od czasu sugestii Dream, więc nic nie zostało zastosowane.", "itemLinkListTitle": "{count, plural, =0{Brak elementów} =1{1 element} other{{count} elementów}}", + "onboardingPermissionTapToFix": "Dotknij, aby naprawić", + "onboardingNoOmiYet": "Nie masz jeszcze Omi?", + "onboardingOmiOrdered": "Już zamówiłem/am", + "onboardingOmiNotOrdered": "Jeszcze nie zamówiłem/am", + "onboardingConnectOmi": "Połącz Omi", + "consentShortMessage": "Omi za pomocą AI zamienia Twoje rozmowy w notatki. Możesz je usunąć w każdej chwili.", + "consentAiProviders": "Przetwarzane przez {providers}.", + "onboardingActIntro": "Wypróbujmy Omi.", + "onboardingActMicTitle": "Omi potrzebuje mikrofonu", + "onboardingActMicBody": "Aby Omi słyszał Cię, gdy do niego mówisz.", + "onboardingActMicAllow": "Zezwól na mikrofon", + "onboardingActDictateTitle": "Opowiedz Omi o swoim dniu", + "onboardingActDictateBody": "Powiedz to po swojemu. Resztą zajmie się Omi.", + "onboardingActDictateExample": "Właśnie zjadłem lunch z Sam, w czerwcu przeprowadza się do Lizbony! Przypomnij mi, żeby wysłać prezentację zespołowi do piątku i kupić mleko w drodze do domu.", + "onboardingActDictateHint": "Gdy skończysz, dotknij znacznika", + "onboardingActPhoneMic": "Mikrofon telefonu", + "onboardingActWriting": "Podsumowujemy…", + "onboardingActRemembers": "Omi pamięta, żebyś Ty nie musiał", + "onboardingActNotesLabel": "Twój dzień w skrócie", + "onboardingActTasksLabel": "Omi wyłapał Twoje zadania", + "onboardingActYourWords": "Oto, co powiedziałeś", + "onboardingActCongratsTitle": "Wszystko gotowe, {name}", + "onboardingActRateTitle": "Podoba Ci się Omi?", + "onboardingActRateYes": "Oceń Omi", + "onboardingWelcomeCardTitle": "Witaj w Omi", + "onboardingWelcomeCardBody": "Noś Omi i rozmawiaj jak zwykle. Każda rozmowa zamienia się tutaj w notatki, zadania i wspomnienia.", + "onboardingActDemoTitle": "Lunch z Sam", + "onboardingActDemoSummary": "Lunch z Sam, która w czerwcu przeprowadza się do Lizbony. Chcesz do piątku wysłać zespołowi prezentację i kupić mleko w drodze do domu.", + "onboardingActDemoTask1": "Wysłać prezentację zespołowi do piątku", + "onboardingActDemoTask2": "Kupić mleko w drodze do domu", + "onboardingActJustNow": "Przed chwilą", + "onboardingActSavedToTasks": "Zapisano w Twoich zadaniach", + "onboardingConnectTapToConnect": "Stuknij swoje Omi, aby połączyć.", + "onboardingActMicFallbackBody": "Twoje Omi jeszcze nie przesyła dźwięku. Na razie użyj telefonu.", + "onboardingActNoAudioOmi": "Nie słychać Omi. Sprawdź, czy jest włączone i blisko.", + "onboardingActNoAudioPhone": "Jeszcze Cię nie słychać. Spróbuj mówić trochę głośniej.", + "onboardingActDictateBodyOmi": "Po prostu mów. Twoje Omi słucha.", + "voiceProfileCardDismissed": "Profil głosowy możesz skonfigurować w każdej chwili w Ustawieniach.", + "homeNewRecordingButton": "Nowe nagranie", + "chatWelcomeMessage": "Cześć, jestem Omi. Pytaj mnie o wszystko, o czym rozmawiałeś: co ktoś powiedział, co obiecałeś albo jak minął ci dzień. Mogę też dodawać zadania i zapamiętywać rzeczy za ciebie.", + "onboardingStarterTaskAsk": "Zapytaj Omi o swój dzień", + "onboardingStarterTaskRecord": "Nagraj rozmowę z Omi", + "onboardingStarterTaskCheck": "Odhacz to zadanie, aby je ukończyć", "paywallRecommended": "Recommended", "paywallNoPlans": "Plans aren't available right now. Try again in a minute.", "paywallOtherPlans": "See all plans", @@ -4049,21 +4092,6 @@ "paywallManageBilling": "Manage billing", "paywallLoadError": "Couldn't load plans. Check your connection and try again.", "paywallPlans": "Plans", - "firstCaptureTitle": "How will you capture?", - "firstCaptureDescription": "Choose how you want to capture your first conversation.", - "firstCaptureOmi": "I Have an Omi", - "firstCapturePhone": "This Phone", - "firstCaptureWatch": "Apple Watch", - "firstCaptureSkip": "Skip for Now", - "firstCapturePrompt": "Tell Omi what you're working on today, or what's on your mind. Talk for about 30 seconds.", - "firstCaptureKeepGoing": "Keep going… 30 seconds is ideal.", - "firstCaptureReady": "You can finish now, or keep talking.", - "firstCaptureStart": "Start Recording", - "firstCaptureFinish": "Finish Recording", - "firstCaptureListening": "Listening… your words will appear here.", - "firstCaptureProcessing": "Creating your first conversation…", - "firstCaptureResult": "Your first conversation", - "firstCaptureEmpty": "I didn't catch enough speech, try again with a few sentences", "watchCaptureTitle": "Record from your Apple Watch", "watchCaptureOpen": "Open Omi on your Apple Watch.", "watchCaptureRecord": "Tap Record. Allow microphone access if asked.", @@ -4075,17 +4103,6 @@ "pairingNotFoundOrDropped": "Couldn't reach your Omi. Make sure it's charged, close to your phone, and its light is blinking, then try again.", "pairingBondRecovery": "Forget Omi in your phone’s Bluetooth settings, then try pairing again.", "pairingHowTo": "How to pair", - "firstCaptureWatchInstructions": "Open Omi on your Apple Watch and tap Record. Allow microphone access if asked. Your capture will sync to Omi on this phone.", - "firstCaptureWatchInstall": "In the Watch app on your iPhone, install Omi on your Apple Watch. Then open Omi on your watch and tap Record.", - "firstCaptureWatchPair": "Pair your Apple Watch with this iPhone using the Watch app, then install and open Omi on your watch.", - "firstCaptureUnavailable": "Recording could not start. Check microphone access and your connection, then try again.", - "firstCaptureDelayed": "Your recording is taking longer to process. You can check your conversations on Home or try again.", - "firstCaptureEmptyTitle": "Let's try that again", - "firstCaptureShortTitle": "Finish This Recording?", - "firstCaptureShortWarning": "A few more sentences help Omi understand you. About 30 seconds is ideal.", - "firstCaptureCancelTitle": "Leave This Recording?", - "firstCaptureCancelWarning": "Going back stops this recording. Keep talking to give Omi a few more sentences.", - "firstCaptureSiriTip": "Tip: Enable Omi in Shortcuts → Siri.", "paywallOutlineHeadline": "Nigdy więcej nie przegap ani słowa.", "paywallCancelAnytime": "Anuluj w dowolnym momencie.", "connectAnAgent": "Połącz agenta", diff --git a/app/lib/l10n/app_pt.arb b/app/lib/l10n/app_pt.arb index 249714f9338..34a2d59c535 100644 --- a/app/lib/l10n/app_pt.arb +++ b/app/lib/l10n/app_pt.arb @@ -515,7 +515,7 @@ "dontShowAgain": "Não mostrar novamente", "iUnderstand": "Eu entendo", "enableBluetooth": "Ativar Bluetooth", - "bluetoothNeeded": "Omi precisa de Bluetooth para conectar seu wearable.", + "bluetoothNeeded": "O Omi se conecta ao seu telefone por Bluetooth. Ative-o para continuar.", "contactSupport": "Contatar suporte?", "connectLater": "Conectar mais tarde", "grantPermissions": "Conceder permissões", @@ -1501,7 +1501,7 @@ "durationSeconds": "Duração: {seconds} segundos", "additionalSpeechSampleRemoved": "Amostra de voz adicional removida", "consentDataMessage": "Ao continuar, suas conversas, gravações e informações pessoais serão armazenadas com segurança em nossos servidores. Suas gravações de áudio e transcrições são processadas por serviços de IA de terceiros (incluindo Deepgram para transcrição e OpenAI para análise) para fornecer insights baseados em IA e habilitar todos os recursos do aplicativo.", - "tasksEmptyStateMessage": "Inicie uma conversa para criar uma tarefa.", + "tasksEmptyStateMessage": "As tarefas das suas conversas aparecerão aqui.", "clearChatAction": "Limpar conversa", "enableApps": "Ativar aplicativos", "omiAppName": "Omi", @@ -4039,6 +4039,49 @@ "reviewProposalSuggested": "Sugerido", "reviewProposalConflict": "Isto mudou desde que o Dream sugeriu, então nada foi aplicado.", "itemLinkListTitle": "{count, plural, =0{Sem itens} =1{1 item} other{{count} itens}}", + "onboardingPermissionTapToFix": "Toque para corrigir", + "onboardingNoOmiYet": "Ainda não tem o seu Omi?", + "onboardingOmiOrdered": "Já encomendei", + "onboardingOmiNotOrdered": "Ainda não encomendei", + "onboardingConnectOmi": "Conectar Omi", + "consentShortMessage": "O Omi usa IA para transformar suas conversas em notas. Você pode excluí-las quando quiser.", + "consentAiProviders": "Processado com {providers}.", + "onboardingActIntro": "Vamos testar o Omi.", + "onboardingActMicTitle": "O Omi precisa do seu microfone", + "onboardingActMicBody": "Para o Omi ouvir quando você falar com ele.", + "onboardingActMicAllow": "Permitir microfone", + "onboardingActDictateTitle": "Conte ao Omi sobre o seu dia", + "onboardingActDictateBody": "Fale do seu jeito. O Omi cuida do resto.", + "onboardingActDictateExample": "Acabei de almoçar com a Sam, ela vai se mudar para Lisboa em junho! Me lembra de mandar a apresentação para o time até sexta e de comprar leite na volta para casa.", + "onboardingActDictateHint": "Toque no check quando terminar", + "onboardingActPhoneMic": "Microfone do celular", + "onboardingActWriting": "Resumindo…", + "onboardingActRemembers": "O Omi lembra por você", + "onboardingActNotesLabel": "Seu dia, resumido", + "onboardingActTasksLabel": "O Omi pegou suas tarefas", + "onboardingActYourWords": "Veja o que você disse", + "onboardingActCongratsTitle": "Tudo pronto, {name}", + "onboardingActRateTitle": "Está gostando do Omi?", + "onboardingActRateYes": "Avaliar o Omi", + "onboardingWelcomeCardTitle": "Boas-vindas ao Omi", + "onboardingWelcomeCardBody": "Use o Omi e converse normalmente. Cada conversa vira notas, tarefas e memórias bem aqui.", + "onboardingActDemoTitle": "Almoço com a Sam", + "onboardingActDemoSummary": "Almoço com o Sam, que vai se mudar para Lisboa em junho. Você quer enviar a apresentação para a equipe até sexta e comprar leite no caminho de casa.", + "onboardingActDemoTask1": "Mandar a apresentação para o time até sexta", + "onboardingActDemoTask2": "Comprar leite na volta para casa", + "onboardingActJustNow": "Agora mesmo", + "onboardingActSavedToTasks": "Salvo nas suas tarefas", + "onboardingConnectTapToConnect": "Toque no seu Omi para conectar.", + "onboardingActMicFallbackBody": "Seu Omi ainda não está enviando áudio. Use o celular por enquanto.", + "onboardingActNoAudioOmi": "Não estamos ouvindo seu Omi. Veja se ele está ligado e por perto.", + "onboardingActNoAudioPhone": "Ainda não estamos te ouvindo. Tente falar um pouco mais alto.", + "onboardingActDictateBodyOmi": "É só falar. Seu Omi está ouvindo.", + "voiceProfileCardDismissed": "Você pode configurar o perfil de voz a qualquer momento em Ajustes.", + "homeNewRecordingButton": "Nova gravação", + "chatWelcomeMessage": "Oi, eu sou o Omi. Pergunte qualquer coisa sobre o que você conversou: o que alguém disse, o que você prometeu ou um resumo do seu dia. Também posso adicionar tarefas e lembrar coisas para você.", + "onboardingStarterTaskAsk": "Pergunte ao Omi sobre o seu dia", + "onboardingStarterTaskRecord": "Grave uma conversa com o Omi", + "onboardingStarterTaskCheck": "Marque esta tarefa para concluí-la", "paywallRecommended": "Recommended", "paywallNoPlans": "Plans aren't available right now. Try again in a minute.", "paywallOtherPlans": "See all plans", @@ -4049,21 +4092,6 @@ "paywallManageBilling": "Manage billing", "paywallLoadError": "Couldn't load plans. Check your connection and try again.", "paywallPlans": "Plans", - "firstCaptureTitle": "How will you capture?", - "firstCaptureDescription": "Choose how you want to capture your first conversation.", - "firstCaptureOmi": "I Have an Omi", - "firstCapturePhone": "This Phone", - "firstCaptureWatch": "Apple Watch", - "firstCaptureSkip": "Skip for Now", - "firstCapturePrompt": "Tell Omi what you're working on today, or what's on your mind. Talk for about 30 seconds.", - "firstCaptureKeepGoing": "Keep going… 30 seconds is ideal.", - "firstCaptureReady": "You can finish now, or keep talking.", - "firstCaptureStart": "Start Recording", - "firstCaptureFinish": "Finish Recording", - "firstCaptureListening": "Listening… your words will appear here.", - "firstCaptureProcessing": "Creating your first conversation…", - "firstCaptureResult": "Your first conversation", - "firstCaptureEmpty": "I didn't catch enough speech, try again with a few sentences", "watchCaptureTitle": "Record from your Apple Watch", "watchCaptureOpen": "Open Omi on your Apple Watch.", "watchCaptureRecord": "Tap Record. Allow microphone access if asked.", @@ -4075,17 +4103,6 @@ "pairingNotFoundOrDropped": "Couldn't reach your Omi. Make sure it's charged, close to your phone, and its light is blinking, then try again.", "pairingBondRecovery": "Forget Omi in your phone’s Bluetooth settings, then try pairing again.", "pairingHowTo": "How to pair", - "firstCaptureWatchInstructions": "Open Omi on your Apple Watch and tap Record. Allow microphone access if asked. Your capture will sync to Omi on this phone.", - "firstCaptureWatchInstall": "In the Watch app on your iPhone, install Omi on your Apple Watch. Then open Omi on your watch and tap Record.", - "firstCaptureWatchPair": "Pair your Apple Watch with this iPhone using the Watch app, then install and open Omi on your watch.", - "firstCaptureUnavailable": "Recording could not start. Check microphone access and your connection, then try again.", - "firstCaptureDelayed": "Your recording is taking longer to process. You can check your conversations on Home or try again.", - "firstCaptureEmptyTitle": "Let's try that again", - "firstCaptureShortTitle": "Finish This Recording?", - "firstCaptureShortWarning": "A few more sentences help Omi understand you. About 30 seconds is ideal.", - "firstCaptureCancelTitle": "Leave This Recording?", - "firstCaptureCancelWarning": "Going back stops this recording. Keep talking to give Omi a few more sentences.", - "firstCaptureSiriTip": "Tip: Enable Omi in Shortcuts → Siri.", "paywallOutlineHeadline": "Nunca mais perca uma palavra.", "paywallCancelAnytime": "Cancele quando quiser.", "connectAnAgent": "Conectar um agente", diff --git a/app/lib/l10n/app_ro.arb b/app/lib/l10n/app_ro.arb index 7d2a66f58d5..104ffd67356 100644 --- a/app/lib/l10n/app_ro.arb +++ b/app/lib/l10n/app_ro.arb @@ -520,7 +520,7 @@ "dontShowAgain": "Nu mai afișa", "iUnderstand": "Înțeleg", "enableBluetooth": "Activare Bluetooth", - "bluetoothNeeded": "Omi are nevoie de Bluetooth pentru a se conecta la dispozitivul dvs. Activați Bluetooth și încercați din nou.", + "bluetoothNeeded": "Omi se conectează la telefon prin Bluetooth. Activează-l pentru a continua.", "contactSupport": "Contactează suportul?", "connectLater": "Conectează mai târziu", "grantPermissions": "Acordați permisiuni", @@ -1501,7 +1501,7 @@ "durationSeconds": "Durată: {seconds} secunde", "additionalSpeechSampleRemoved": "Mostră vocală suplimentară eliminată", "consentDataMessage": "Continuând, conversațiile, înregistrările și informațiile dvs. personale vor fi stocate în siguranță pe serverele noastre. Înregistrările audio și transcrierile dvs. sunt procesate de servicii AI terțe (inclusiv Deepgram pentru transcriere și OpenAI pentru analiză) pentru a vă oferi informații bazate pe AI și a activa toate funcțiile aplicației.", - "tasksEmptyStateMessage": "Începe o conversație pentru a crea o sarcină.", + "tasksEmptyStateMessage": "Sarcinile din conversațiile tale vor apărea aici.", "clearChatAction": "Șterge conversația", "enableApps": "Activează aplicațiile", "omiAppName": "Omi", @@ -4004,6 +4004,49 @@ "reviewProposalSuggested": "Sugerat", "reviewProposalConflict": "S-a schimbat de când Dream a sugerat asta, așa că nu s-a aplicat nimic.", "itemLinkListTitle": "{count, plural, =0{Niciun element} =1{1 element} other{{count} elemente}}", + "onboardingPermissionTapToFix": "Atinge pentru a remedia", + "onboardingNoOmiYet": "Nu ai încă Omi?", + "onboardingOmiOrdered": "L-am comandat", + "onboardingOmiNotOrdered": "Încă nu am comandat", + "onboardingConnectOmi": "Conectează Omi", + "consentShortMessage": "Omi folosește AI pentru a transforma conversațiile tale în notițe. Le poți șterge oricând.", + "consentAiProviders": "Procesat cu {providers}.", + "onboardingActIntro": "Hai să încercăm Omi.", + "onboardingActMicTitle": "Omi are nevoie de microfon", + "onboardingActMicBody": "Ca Omi să te audă când vorbești cu el.", + "onboardingActMicAllow": "Permite microfonul", + "onboardingActDictateTitle": "Povestește-i lui Omi despre ziua ta", + "onboardingActDictateBody": "Spune-o în felul tău. Omi se ocupă de rest.", + "onboardingActDictateExample": "Tocmai am luat prânzul cu Sam, se mută la Lisabona în iunie! Amintește-mi să trimit prezentarea echipei până vineri și să iau lapte în drum spre casă.", + "onboardingActDictateHint": "Atinge bifa când ai terminat", + "onboardingActPhoneMic": "Microfonul telefonului", + "onboardingActWriting": "Rezumăm…", + "onboardingActRemembers": "Omi își amintește, ca să nu fie nevoie să o faci tu", + "onboardingActNotesLabel": "Ziua ta, pe scurt", + "onboardingActTasksLabel": "Omi ți-a prins sarcinile", + "onboardingActYourWords": "Iată ce ai spus", + "onboardingActCongratsTitle": "Totul e gata, {name}", + "onboardingActRateTitle": "Îți place Omi până acum?", + "onboardingActRateYes": "Evaluează Omi", + "onboardingWelcomeCardTitle": "Bun venit în Omi", + "onboardingWelcomeCardBody": "Poartă Omi și vorbește ca de obicei. Fiecare conversație devine aici notițe, sarcini și amintiri.", + "onboardingActDemoTitle": "Prânz cu Sam", + "onboardingActDemoSummary": "Prânz cu Sam, care se mută la Lisabona în iunie. Vrei să trimiți prezentarea echipei până vineri și să iei lapte în drum spre casă.", + "onboardingActDemoTask1": "Trimite prezentarea echipei până vineri", + "onboardingActDemoTask2": "Ia lapte în drum spre casă", + "onboardingActJustNow": "Chiar acum", + "onboardingActSavedToTasks": "Salvat în sarcinile tale", + "onboardingConnectTapToConnect": "Atinge Omi-ul tău pentru a te conecta.", + "onboardingActMicFallbackBody": "Omi nu trimite încă sunet. Deocamdată folosește telefonul.", + "onboardingActNoAudioOmi": "Nu se aude Omi. Verifică dacă e pornit și aproape.", + "onboardingActNoAudioPhone": "Încă nu te auzim. Încearcă să vorbești puțin mai tare.", + "onboardingActDictateBodyOmi": "Doar vorbește. Omi te ascultă.", + "voiceProfileCardDismissed": "Poți configura profilul vocal oricând din Setări.", + "homeNewRecordingButton": "Înregistrare nouă", + "chatWelcomeMessage": "Salut, sunt Omi. Întreabă-mă orice despre ce ai vorbit: ce a spus cineva, ce ai promis sau un rezumat al zilei tale. Pot și să adaug sarcini și să țin minte lucruri pentru tine.", + "onboardingStarterTaskAsk": "Întreabă-l pe Omi despre ziua ta", + "onboardingStarterTaskRecord": "Înregistrează o conversație cu Omi", + "onboardingStarterTaskCheck": "Bifează această sarcină ca s-o finalizezi", "paywallRecommended": "Recommended", "paywallNoPlans": "Plans aren't available right now. Try again in a minute.", "paywallOtherPlans": "See all plans", @@ -4014,21 +4057,6 @@ "paywallManageBilling": "Manage billing", "paywallLoadError": "Couldn't load plans. Check your connection and try again.", "paywallPlans": "Plans", - "firstCaptureTitle": "How will you capture?", - "firstCaptureDescription": "Choose how you want to capture your first conversation.", - "firstCaptureOmi": "I Have an Omi", - "firstCapturePhone": "This Phone", - "firstCaptureWatch": "Apple Watch", - "firstCaptureSkip": "Skip for Now", - "firstCapturePrompt": "Tell Omi what you're working on today, or what's on your mind. Talk for about 30 seconds.", - "firstCaptureKeepGoing": "Keep going… 30 seconds is ideal.", - "firstCaptureReady": "You can finish now, or keep talking.", - "firstCaptureStart": "Start Recording", - "firstCaptureFinish": "Finish Recording", - "firstCaptureListening": "Listening… your words will appear here.", - "firstCaptureProcessing": "Creating your first conversation…", - "firstCaptureResult": "Your first conversation", - "firstCaptureEmpty": "I didn't catch enough speech, try again with a few sentences", "watchCaptureTitle": "Record from your Apple Watch", "watchCaptureOpen": "Open Omi on your Apple Watch.", "watchCaptureRecord": "Tap Record. Allow microphone access if asked.", @@ -4040,17 +4068,6 @@ "pairingNotFoundOrDropped": "Couldn't reach your Omi. Make sure it's charged, close to your phone, and its light is blinking, then try again.", "pairingBondRecovery": "Forget Omi in your phone’s Bluetooth settings, then try pairing again.", "pairingHowTo": "How to pair", - "firstCaptureWatchInstructions": "Open Omi on your Apple Watch and tap Record. Allow microphone access if asked. Your capture will sync to Omi on this phone.", - "firstCaptureWatchInstall": "In the Watch app on your iPhone, install Omi on your Apple Watch. Then open Omi on your watch and tap Record.", - "firstCaptureWatchPair": "Pair your Apple Watch with this iPhone using the Watch app, then install and open Omi on your watch.", - "firstCaptureUnavailable": "Recording could not start. Check microphone access and your connection, then try again.", - "firstCaptureDelayed": "Your recording is taking longer to process. You can check your conversations on Home or try again.", - "firstCaptureEmptyTitle": "Let's try that again", - "firstCaptureShortTitle": "Finish This Recording?", - "firstCaptureShortWarning": "A few more sentences help Omi understand you. About 30 seconds is ideal.", - "firstCaptureCancelTitle": "Leave This Recording?", - "firstCaptureCancelWarning": "Going back stops this recording. Keep talking to give Omi a few more sentences.", - "firstCaptureSiriTip": "Tip: Enable Omi in Shortcuts → Siri.", "paywallOutlineHeadline": "Nu mai rata niciun cuvânt.", "paywallCancelAnytime": "Anulează oricând.", "connectAnAgent": "Conectează un agent", diff --git a/app/lib/l10n/app_ru.arb b/app/lib/l10n/app_ru.arb index 8c66afce8e8..544372c2edd 100644 --- a/app/lib/l10n/app_ru.arb +++ b/app/lib/l10n/app_ru.arb @@ -520,7 +520,7 @@ "dontShowAgain": "Больше не показывать", "iUnderstand": "Я понимаю", "enableBluetooth": "Включите Bluetooth", - "bluetoothNeeded": "Omi требуется Bluetooth для подключения к вашему устройству. Пожалуйста, включите Bluetooth и попробуйте снова.", + "bluetoothNeeded": "Omi подключается к телефону по Bluetooth. Включите его, чтобы продолжить.", "contactSupport": "Связаться с поддержкой?", "connectLater": "Подключить позже", "grantPermissions": "Предоставить разрешения", @@ -1501,7 +1501,7 @@ "durationSeconds": "Длительность: {seconds} секунд", "additionalSpeechSampleRemoved": "Дополнительный образец голоса удален", "consentDataMessage": "Продолжая, ваши разговоры, записи и личная информация будут надежно храниться на наших серверах. Ваши аудиозаписи и транскрипции обрабатываются сторонними AI-сервисами (включая Deepgram для транскрипции и OpenAI для анализа), чтобы предоставить вам аналитику на основе ИИ и обеспечить работу всех функций приложения.", - "tasksEmptyStateMessage": "Начните разговор, чтобы создать задачу.", + "tasksEmptyStateMessage": "Задачи из ваших разговоров появятся здесь.", "clearChatAction": "Очистить чат", "enableApps": "Включить приложения", "omiAppName": "Omi", @@ -4039,6 +4039,49 @@ "reviewProposalSuggested": "Предложено", "reviewProposalConflict": "Это изменилось после предложения Dream, поэтому ничего не применено.", "itemLinkListTitle": "{count, plural, =0{Нет элементов} =1{1 элемент} other{{count} элементов}}", + "onboardingPermissionTapToFix": "Нажмите, чтобы исправить", + "onboardingNoOmiYet": "Ещё нет Omi?", + "onboardingOmiOrdered": "Я уже заказал", + "onboardingOmiNotOrdered": "Ещё не заказал", + "onboardingConnectOmi": "Подключите Omi", + "consentShortMessage": "Omi с помощью ИИ превращает ваши разговоры в заметки. Их можно удалить в любой момент.", + "consentAiProviders": "Обработка: {providers}.", + "onboardingActIntro": "Попробуем Omi.", + "onboardingActMicTitle": "Omi нужен микрофон", + "onboardingActMicBody": "Чтобы Omi слышал вас, когда вы с ним говорите.", + "onboardingActMicAllow": "Разрешить микрофон", + "onboardingActDictateTitle": "Расскажите Omi о своём дне", + "onboardingActDictateBody": "Говорите как вам удобно. Остальное сделает Omi.", + "onboardingActDictateExample": "Только что пообедал с Сэм, в июне она переезжает в Лиссабон! Напомни отправить презентацию команде до пятницы и купить молоко по дороге домой.", + "onboardingActDictateHint": "Нажмите галочку, когда закончите", + "onboardingActPhoneMic": "Микрофон телефона", + "onboardingActWriting": "Подводим итоги…", + "onboardingActRemembers": "Omi запоминает, чтобы вам не пришлось", + "onboardingActNotesLabel": "Ваш день вкратце", + "onboardingActTasksLabel": "Omi нашёл ваши дела", + "onboardingActYourWords": "Вот что вы сказали", + "onboardingActCongratsTitle": "Всё готово, {name}", + "onboardingActRateTitle": "Нравится Omi?", + "onboardingActRateYes": "Оценить Omi", + "onboardingWelcomeCardTitle": "Добро пожаловать в Omi", + "onboardingWelcomeCardBody": "Носите Omi и говорите как обычно. Каждый разговор превратится здесь в заметки, задачи и воспоминания.", + "onboardingActDemoTitle": "Обед с Сэм", + "onboardingActDemoSummary": "Обед с Сэм, которая в июне переезжает в Лиссабон. Нужно отправить презентацию команде до пятницы и купить молоко по дороге домой.", + "onboardingActDemoTask1": "Отправить презентацию команде до пятницы", + "onboardingActDemoTask2": "Купить молоко по дороге домой", + "onboardingActJustNow": "Только что", + "onboardingActSavedToTasks": "Сохранено в ваши задачи", + "onboardingConnectTapToConnect": "Нажмите на Omi, чтобы подключить.", + "onboardingActMicFallbackBody": "Omi пока не передаёт звук. Пока используйте телефон.", + "onboardingActNoAudioOmi": "Omi не слышно. Проверьте, что он включён и рядом.", + "onboardingActNoAudioPhone": "Пока вас не слышно. Попробуйте говорить чуть громче.", + "onboardingActDictateBodyOmi": "Просто говорите. Omi слушает.", + "voiceProfileCardDismissed": "Голосовой профиль можно настроить в любое время в Настройках.", + "homeNewRecordingButton": "Новая запись", + "chatWelcomeMessage": "Привет, я Omi. Спрашивайте обо всём, о чём вы говорили: что сказал кто-то, что вы пообещали или как прошёл ваш день. Я также могу добавлять задачи и запоминать важное за вас.", + "onboardingStarterTaskAsk": "Спросите Omi о своём дне", + "onboardingStarterTaskRecord": "Запишите разговор с Omi", + "onboardingStarterTaskCheck": "Отметьте эту задачу, чтобы завершить её", "paywallRecommended": "Recommended", "paywallNoPlans": "Plans aren't available right now. Try again in a minute.", "paywallOtherPlans": "See all plans", @@ -4049,21 +4092,6 @@ "paywallManageBilling": "Manage billing", "paywallLoadError": "Couldn't load plans. Check your connection and try again.", "paywallPlans": "Plans", - "firstCaptureTitle": "How will you capture?", - "firstCaptureDescription": "Choose how you want to capture your first conversation.", - "firstCaptureOmi": "I Have an Omi", - "firstCapturePhone": "This Phone", - "firstCaptureWatch": "Apple Watch", - "firstCaptureSkip": "Skip for Now", - "firstCapturePrompt": "Tell Omi what you're working on today, or what's on your mind. Talk for about 30 seconds.", - "firstCaptureKeepGoing": "Keep going… 30 seconds is ideal.", - "firstCaptureReady": "You can finish now, or keep talking.", - "firstCaptureStart": "Start Recording", - "firstCaptureFinish": "Finish Recording", - "firstCaptureListening": "Listening… your words will appear here.", - "firstCaptureProcessing": "Creating your first conversation…", - "firstCaptureResult": "Your first conversation", - "firstCaptureEmpty": "I didn't catch enough speech, try again with a few sentences", "watchCaptureTitle": "Record from your Apple Watch", "watchCaptureOpen": "Open Omi on your Apple Watch.", "watchCaptureRecord": "Tap Record. Allow microphone access if asked.", @@ -4075,17 +4103,6 @@ "pairingNotFoundOrDropped": "Couldn't reach your Omi. Make sure it's charged, close to your phone, and its light is blinking, then try again.", "pairingBondRecovery": "Forget Omi in your phone’s Bluetooth settings, then try pairing again.", "pairingHowTo": "How to pair", - "firstCaptureWatchInstructions": "Open Omi on your Apple Watch and tap Record. Allow microphone access if asked. Your capture will sync to Omi on this phone.", - "firstCaptureWatchInstall": "In the Watch app on your iPhone, install Omi on your Apple Watch. Then open Omi on your watch and tap Record.", - "firstCaptureWatchPair": "Pair your Apple Watch with this iPhone using the Watch app, then install and open Omi on your watch.", - "firstCaptureUnavailable": "Recording could not start. Check microphone access and your connection, then try again.", - "firstCaptureDelayed": "Your recording is taking longer to process. You can check your conversations on Home or try again.", - "firstCaptureEmptyTitle": "Let's try that again", - "firstCaptureShortTitle": "Finish This Recording?", - "firstCaptureShortWarning": "A few more sentences help Omi understand you. About 30 seconds is ideal.", - "firstCaptureCancelTitle": "Leave This Recording?", - "firstCaptureCancelWarning": "Going back stops this recording. Keep talking to give Omi a few more sentences.", - "firstCaptureSiriTip": "Tip: Enable Omi in Shortcuts → Siri.", "paywallOutlineHeadline": "Больше не упускайте ни слова.", "paywallCancelAnytime": "Отмена в любой момент.", "connectAnAgent": "Подключить агента", diff --git a/app/lib/l10n/app_sk.arb b/app/lib/l10n/app_sk.arb index 8df1d9f85a7..f7428805bb3 100644 --- a/app/lib/l10n/app_sk.arb +++ b/app/lib/l10n/app_sk.arb @@ -520,7 +520,7 @@ "dontShowAgain": "Znovu nezobrazovať", "iUnderstand": "Rozumiem", "enableBluetooth": "Povoliť Bluetooth", - "bluetoothNeeded": "Omi potrebuje Bluetooth na pripojenie k vášmu nositeľnému zariadeniu. Povoľte prosím Bluetooth a skúste to znova.", + "bluetoothNeeded": "Omi sa k telefónu pripája cez Bluetooth. Zapnite ho a pokračujte.", "contactSupport": "Kontaktovať podporu?", "connectLater": "Pripojiť neskôr", "grantPermissions": "Udeliť povolenia", @@ -1501,7 +1501,7 @@ "durationSeconds": "Trvanie: {seconds} sekúnd", "additionalSpeechSampleRemoved": "Ďalšia hlasová vzorka odstránená", "consentDataMessage": "Pokračovaním budú vaše konverzácie, nahrávky a osobné údaje bezpečne uložené na našich serveroch. Vaše audio nahrávky a prepisy sú spracovávané AI službami tretích strán (vrátane Deepgram na prepis a OpenAI na analýzu), aby vám poskytli poznatky založené na AI a umožnili všetky funkcie aplikácie.", - "tasksEmptyStateMessage": "Začnite konverzáciu a vytvorte úlohu.", + "tasksEmptyStateMessage": "Úlohy z vašich rozhovorov sa zobrazia tu.", "clearChatAction": "Vymazať chat", "enableApps": "Povoliť aplikácie", "omiAppName": "Omi", @@ -4004,6 +4004,49 @@ "reviewProposalSuggested": "Navrhnuté", "reviewProposalConflict": "Od návrhu Dream sa to zmenilo, takže sa nič nepoužilo.", "itemLinkListTitle": "{count, plural, =0{Žiadne položky} =1{1 položka} other{{count} položiek}}", + "onboardingPermissionTapToFix": "Ťuknutím opravíte", + "onboardingNoOmiYet": "Ešte nemáte Omi?", + "onboardingOmiOrdered": "Už som ho objednal/a", + "onboardingOmiNotOrdered": "Ešte neobjednané", + "onboardingConnectOmi": "Pripojte Omi", + "consentShortMessage": "Omi pomocou AI mení vaše konverzácie na poznámky. Kedykoľvek ich môžete vymazať.", + "consentAiProviders": "Spracúva {providers}.", + "onboardingActIntro": "Vyskúšajme Omi.", + "onboardingActMicTitle": "Omi potrebuje mikrofón", + "onboardingActMicBody": "Aby vás Omi počul, keď naň hovoríte.", + "onboardingActMicAllow": "Povoliť mikrofón", + "onboardingActDictateTitle": "Povedzte Omi o svojom dni", + "onboardingActDictateBody": "Povedzte to po svojom. Zvyšok zariadi Omi.", + "onboardingActDictateExample": "Práve som obedoval so Sam, v júni sa sťahuje do Lisabonu! Pripomeň mi poslať prezentáciu tímu do piatka a cestou domov kúpiť mlieko.", + "onboardingActDictateHint": "Keď skončíte, ťuknite na fajku", + "onboardingActPhoneMic": "Mikrofón telefónu", + "onboardingActWriting": "Zhrnujeme…", + "onboardingActRemembers": "Omi si to pamätá, aby ste nemuseli vy", + "onboardingActNotesLabel": "Váš deň v skratke", + "onboardingActTasksLabel": "Omi zachytil vaše úlohy", + "onboardingActYourWords": "Toto ste povedali", + "onboardingActCongratsTitle": "Všetko je pripravené, {name}", + "onboardingActRateTitle": "Páči sa vám zatiaľ Omi?", + "onboardingActRateYes": "Ohodnotiť Omi", + "onboardingWelcomeCardTitle": "Vitajte v Omi", + "onboardingWelcomeCardBody": "Noste Omi a hovorte ako zvyčajne. Každý rozhovor sa tu zmení na poznámky, úlohy a spomienky.", + "onboardingActDemoTitle": "Obed so Sam", + "onboardingActDemoSummary": "Obed so Sam, ktorá sa v júni sťahuje do Lisabonu. Do piatku chcete poslať tímu prezentáciu a cestou domov kúpiť mlieko.", + "onboardingActDemoTask1": "Poslať prezentáciu tímu do piatka", + "onboardingActDemoTask2": "Cestou domov kúpiť mlieko", + "onboardingActJustNow": "Práve teraz", + "onboardingActSavedToTasks": "Uložené do vašich úloh", + "onboardingConnectTapToConnect": "Ťuknutím na Omi sa pripojíte.", + "onboardingActMicFallbackBody": "Omi zatiaľ neposiela zvuk. Zatiaľ použite telefón.", + "onboardingActNoAudioOmi": "Omi nepočuť. Skontrolujte, či je zapnuté a blízko.", + "onboardingActNoAudioPhone": "Zatiaľ vás nepočuť. Skúste hovoriť trochu hlasnejšie.", + "onboardingActDictateBodyOmi": "Jednoducho hovorte. Vaše Omi počúva.", + "voiceProfileCardDismissed": "Hlasový profil si môžete kedykoľvek nastaviť v Nastaveniach.", + "homeNewRecordingButton": "Nový záznam", + "chatWelcomeMessage": "Ahoj, som Omi. Opýtaj sa ma na čokoľvek, o čom si hovoril: čo kto povedal, čo si sľúbil alebo zhrnutie tvojho dňa. Viem aj pridávať úlohy a pamätať si veci za teba.", + "onboardingStarterTaskAsk": "Opýtaj sa Omi na svoj deň", + "onboardingStarterTaskRecord": "Nahraj rozhovor s Omi", + "onboardingStarterTaskCheck": "Odškrtni túto úlohu a dokonči ju", "paywallRecommended": "Recommended", "paywallNoPlans": "Plans aren't available right now. Try again in a minute.", "paywallOtherPlans": "See all plans", @@ -4014,21 +4057,6 @@ "paywallManageBilling": "Manage billing", "paywallLoadError": "Couldn't load plans. Check your connection and try again.", "paywallPlans": "Plans", - "firstCaptureTitle": "How will you capture?", - "firstCaptureDescription": "Choose how you want to capture your first conversation.", - "firstCaptureOmi": "I Have an Omi", - "firstCapturePhone": "This Phone", - "firstCaptureWatch": "Apple Watch", - "firstCaptureSkip": "Skip for Now", - "firstCapturePrompt": "Tell Omi what you're working on today, or what's on your mind. Talk for about 30 seconds.", - "firstCaptureKeepGoing": "Keep going… 30 seconds is ideal.", - "firstCaptureReady": "You can finish now, or keep talking.", - "firstCaptureStart": "Start Recording", - "firstCaptureFinish": "Finish Recording", - "firstCaptureListening": "Listening… your words will appear here.", - "firstCaptureProcessing": "Creating your first conversation…", - "firstCaptureResult": "Your first conversation", - "firstCaptureEmpty": "I didn't catch enough speech, try again with a few sentences", "watchCaptureTitle": "Record from your Apple Watch", "watchCaptureOpen": "Open Omi on your Apple Watch.", "watchCaptureRecord": "Tap Record. Allow microphone access if asked.", @@ -4040,17 +4068,6 @@ "pairingNotFoundOrDropped": "Couldn't reach your Omi. Make sure it's charged, close to your phone, and its light is blinking, then try again.", "pairingBondRecovery": "Forget Omi in your phone’s Bluetooth settings, then try pairing again.", "pairingHowTo": "How to pair", - "firstCaptureWatchInstructions": "Open Omi on your Apple Watch and tap Record. Allow microphone access if asked. Your capture will sync to Omi on this phone.", - "firstCaptureWatchInstall": "In the Watch app on your iPhone, install Omi on your Apple Watch. Then open Omi on your watch and tap Record.", - "firstCaptureWatchPair": "Pair your Apple Watch with this iPhone using the Watch app, then install and open Omi on your watch.", - "firstCaptureUnavailable": "Recording could not start. Check microphone access and your connection, then try again.", - "firstCaptureDelayed": "Your recording is taking longer to process. You can check your conversations on Home or try again.", - "firstCaptureEmptyTitle": "Let's try that again", - "firstCaptureShortTitle": "Finish This Recording?", - "firstCaptureShortWarning": "A few more sentences help Omi understand you. About 30 seconds is ideal.", - "firstCaptureCancelTitle": "Leave This Recording?", - "firstCaptureCancelWarning": "Going back stops this recording. Keep talking to give Omi a few more sentences.", - "firstCaptureSiriTip": "Tip: Enable Omi in Shortcuts → Siri.", "paywallOutlineHeadline": "Už vám neunikne ani slovo.", "paywallCancelAnytime": "Zrušte kedykoľvek.", "connectAnAgent": "Pripojiť agenta", diff --git a/app/lib/l10n/app_sl.arb b/app/lib/l10n/app_sl.arb index e3d9b97c7fc..08012577eb5 100644 --- a/app/lib/l10n/app_sl.arb +++ b/app/lib/l10n/app_sl.arb @@ -1586,7 +1586,7 @@ "@enableBluetooth": { "description": "Title for dialog prompting user to enable Bluetooth" }, - "bluetoothNeeded": "Omi potrebuje Bluetooth za povezavo z vašo nosljivo napravo. Prosimo omogočite Bluetooth in poskusite znova.", + "bluetoothNeeded": "Omi se s telefonom poveže prek Bluetootha. Vklopite ga za nadaljevanje.", "@bluetoothNeeded": { "description": "Explanation text for why Bluetooth is required" }, @@ -5782,7 +5782,7 @@ "@consentDataMessage": { "description": "Consent message explaining how user data will be stored and used" }, - "tasksEmptyStateMessage": "Začnite pogovor, da ustvarite nalogo.", + "tasksEmptyStateMessage": "Opravila iz vaših pogovorov se bodo prikazala tukaj.", "@tasksEmptyStateMessage": { "description": "Empty state message shown when there are no tasks, with instruction to tap + button" }, @@ -11430,6 +11430,49 @@ "reviewProposalSuggested": "Predlagano", "reviewProposalConflict": "To se je spremenilo, odkar je Dream to predlagal, zato ni bilo nič uporabljeno.", "itemLinkListTitle": "{count, plural, =0{Ni postavk} =1{1 postavka} other{{count} postavk}}", + "onboardingPermissionTapToFix": "Tapnite za popravilo", + "onboardingNoOmiYet": "Še nimate Omi?", + "onboardingOmiOrdered": "Naročil/a sem ga", + "onboardingOmiNotOrdered": "Še nisem naročil/a", + "onboardingConnectOmi": "Povežite Omi", + "consentShortMessage": "Omi z umetno inteligenco spremeni vaše pogovore v zapiske. Kadar koli jih lahko izbrišete.", + "consentAiProviders": "Obdelano z {providers}.", + "onboardingActIntro": "Preizkusimo Omi.", + "onboardingActMicTitle": "Omi potrebuje mikrofon", + "onboardingActMicBody": "Da vas Omi sliši, ko mu govorite.", + "onboardingActMicAllow": "Dovoli mikrofon", + "onboardingActDictateTitle": "Povejte Omiju o svojem dnevu", + "onboardingActDictateBody": "Povejte po svoje. Za ostalo poskrbi Omi.", + "onboardingActDictateExample": "Pravkar sem kosil s Sam, junija se seli v Lizbono! Opomni me, naj do petka pošljem predstavitev ekipi in na poti domov kupim mleko.", + "onboardingActDictateHint": "Ko končate, tapnite kljukico", + "onboardingActPhoneMic": "Mikrofon telefona", + "onboardingActWriting": "Povzemamo…", + "onboardingActRemembers": "Omi si zapomni namesto vas", + "onboardingActNotesLabel": "Vaš dan na kratko", + "onboardingActTasksLabel": "Omi je ujel vaša opravila", + "onboardingActYourWords": "To ste povedali", + "onboardingActCongratsTitle": "Vse je pripravljeno, {name}", + "onboardingActRateTitle": "Vam je Omi všeč?", + "onboardingActRateYes": "Ocenite Omi", + "onboardingWelcomeCardTitle": "Dobrodošli v Omi", + "onboardingWelcomeCardBody": "Nosite Omi in govorite kot običajno. Vsak pogovor tukaj postane zapiski, naloge in spomini.", + "onboardingActDemoTitle": "Kosilo s Sam", + "onboardingActDemoSummary": "Kosilo s Samom, ki se junija seli v Lizbono. Do petka želite ekipi poslati predstavitev in na poti domov kupiti mleko.", + "onboardingActDemoTask1": "Do petka poslati predstavitev ekipi", + "onboardingActDemoTask2": "Na poti domov kupiti mleko", + "onboardingActJustNow": "Pravkar", + "onboardingActSavedToTasks": "Shranjeno med vaša opravila", + "onboardingConnectTapToConnect": "Tapnite svoj Omi za povezavo.", + "onboardingActMicFallbackBody": "Omi še ne pošilja zvoka. Za zdaj uporabite telefon.", + "onboardingActNoAudioOmi": "Omija ne slišimo. Preverite, ali je vklopljen in blizu.", + "onboardingActNoAudioPhone": "Še vas ne slišimo. Poskusite govoriti malo glasneje.", + "onboardingActDictateBodyOmi": "Samo govorite. Vaš Omi posluša.", + "voiceProfileCardDismissed": "Glasovni profil lahko kadar koli nastavite v Nastavitvah.", + "homeNewRecordingButton": "Novo snemanje", + "chatWelcomeMessage": "Živjo, sem Omi. Vprašaj me karkoli o tem, o čemer si govoril: kaj je kdo rekel, kaj si obljubil ali povzetek tvojega dne. Lahko tudi dodajam opravila in si zapomnim stvari namesto tebe.", + "onboardingStarterTaskAsk": "Vprašaj Omi o svojem dnevu", + "onboardingStarterTaskRecord": "Posnemi pogovor z Omi", + "onboardingStarterTaskCheck": "Odkljukaj to opravilo, da ga končaš", "paywallRecommended": "Recommended", "paywallNoPlans": "Plans aren't available right now. Try again in a minute.", "paywallOtherPlans": "See all plans", @@ -11440,21 +11483,6 @@ "paywallManageBilling": "Manage billing", "paywallLoadError": "Couldn't load plans. Check your connection and try again.", "paywallPlans": "Plans", - "firstCaptureTitle": "How will you capture?", - "firstCaptureDescription": "Choose how you want to capture your first conversation.", - "firstCaptureOmi": "I Have an Omi", - "firstCapturePhone": "This Phone", - "firstCaptureWatch": "Apple Watch", - "firstCaptureSkip": "Skip for Now", - "firstCapturePrompt": "Tell Omi what you're working on today, or what's on your mind. Talk for about 30 seconds.", - "firstCaptureKeepGoing": "Keep going… 30 seconds is ideal.", - "firstCaptureReady": "You can finish now, or keep talking.", - "firstCaptureStart": "Start Recording", - "firstCaptureFinish": "Finish Recording", - "firstCaptureListening": "Listening… your words will appear here.", - "firstCaptureProcessing": "Creating your first conversation…", - "firstCaptureResult": "Your first conversation", - "firstCaptureEmpty": "I didn't catch enough speech, try again with a few sentences", "watchCaptureTitle": "Record from your Apple Watch", "watchCaptureOpen": "Open Omi on your Apple Watch.", "watchCaptureRecord": "Tap Record. Allow microphone access if asked.", @@ -11466,17 +11494,6 @@ "pairingNotFoundOrDropped": "Couldn't reach your Omi. Make sure it's charged, close to your phone, and its light is blinking, then try again.", "pairingBondRecovery": "Forget Omi in your phone’s Bluetooth settings, then try pairing again.", "pairingHowTo": "How to pair", - "firstCaptureWatchInstructions": "Open Omi on your Apple Watch and tap Record. Allow microphone access if asked. Your capture will sync to Omi on this phone.", - "firstCaptureWatchInstall": "In the Watch app on your iPhone, install Omi on your Apple Watch. Then open Omi on your watch and tap Record.", - "firstCaptureWatchPair": "Pair your Apple Watch with this iPhone using the Watch app, then install and open Omi on your watch.", - "firstCaptureUnavailable": "Recording could not start. Check microphone access and your connection, then try again.", - "firstCaptureDelayed": "Your recording is taking longer to process. You can check your conversations on Home or try again.", - "firstCaptureEmptyTitle": "Let's try that again", - "firstCaptureShortTitle": "Finish This Recording?", - "firstCaptureShortWarning": "A few more sentences help Omi understand you. About 30 seconds is ideal.", - "firstCaptureCancelTitle": "Leave This Recording?", - "firstCaptureCancelWarning": "Going back stops this recording. Keep talking to give Omi a few more sentences.", - "firstCaptureSiriTip": "Tip: Enable Omi in Shortcuts → Siri.", "paywallOutlineHeadline": "Nikoli več ne zamudite niti besede.", "paywallCancelAnytime": "Prekličite kadar koli.", "connectAnAgent": "Poveži agenta", diff --git a/app/lib/l10n/app_sr.arb b/app/lib/l10n/app_sr.arb index fe1a9972d9f..9b19cf0f0fc 100644 --- a/app/lib/l10n/app_sr.arb +++ b/app/lib/l10n/app_sr.arb @@ -1586,7 +1586,7 @@ "@enableBluetooth": { "description": "Title for dialog prompting user to enable Bluetooth" }, - "bluetoothNeeded": "Omi треба Bluetooth да би се повезао на вашу носиву. Омогућите Bluetooth и покушајте поново.", + "bluetoothNeeded": "Omi се са телефоном повезује преко Bluetooth-а. Укључите га да наставите.", "@bluetoothNeeded": { "description": "Explanation text for why Bluetooth is required" }, @@ -5782,7 +5782,7 @@ "@consentDataMessage": { "description": "Consent message explaining how user data will be stored and used" }, - "tasksEmptyStateMessage": "Започните разговор да бисте направили задатак.", + "tasksEmptyStateMessage": "Задаци из ваших разговора појавиће се овде.", "@tasksEmptyStateMessage": { "description": "Empty state message shown when there are no tasks, with instruction to tap + button" }, @@ -11430,6 +11430,49 @@ "reviewProposalSuggested": "Predloženo", "reviewProposalConflict": "Ovo se promenilo otkako je Dream to predložio, pa ništa nije primenjeno.", "itemLinkListTitle": "{count, plural, =0{Нема ставки} =1{1 ставка} other{{count} ставки}}", + "onboardingPermissionTapToFix": "Додирните да поправите", + "onboardingNoOmiYet": "Још немате Omi?", + "onboardingOmiOrdered": "Наручио/ла сам га", + "onboardingOmiNotOrdered": "Још нисам наручио/ла", + "onboardingConnectOmi": "Повежите Omi", + "consentShortMessage": "Omi користи AI да ваше разговоре претвори у белешке. Можете их обрисати у било ком тренутку.", + "consentAiProviders": "Обрађује се помоћу {providers}.", + "onboardingActIntro": "Испробајмо Omi.", + "onboardingActMicTitle": "Omi-ју треба микрофон", + "onboardingActMicBody": "Да вас Omi чује када му се обратите.", + "onboardingActMicAllow": "Дозволи микрофон", + "onboardingActDictateTitle": "Испричајте Omi-ју свој дан", + "onboardingActDictateBody": "Реците то на свој начин. Omi ће се побринути за остало.", + "onboardingActDictateExample": "Управо сам ручао са Сем, у јуну се сели у Лисабон! Подсети ме да пошаљем презентацију тиму до петка и да купим млеко на путу кући.", + "onboardingActDictateHint": "Додирните квачицу када завршите", + "onboardingActPhoneMic": "Микрофон телефона", + "onboardingActWriting": "Сажимамо…", + "onboardingActRemembers": "Omi памти уместо вас", + "onboardingActNotesLabel": "Ваш дан, укратко", + "onboardingActTasksLabel": "Omi је ухватио ваше задатке", + "onboardingActYourWords": "Ево шта сте рекли", + "onboardingActCongratsTitle": "Све је спремно, {name}", + "onboardingActRateTitle": "Да ли вам се Omi допада?", + "onboardingActRateYes": "Оцените Omi", + "onboardingWelcomeCardTitle": "Добро дошли у Omi", + "onboardingWelcomeCardBody": "Носите Omi и причајте као и обично. Сваки разговор овде постаје белешке, задаци и сећања.", + "onboardingActDemoTitle": "Ручак са Сем", + "onboardingActDemoSummary": "Ручак са Сем, која се у јуну сели у Лисабон. Желите да пошаљете презентацију тиму до петка и купите млеко на путу кући.", + "onboardingActDemoTask1": "Послати презентацију тиму до петка", + "onboardingActDemoTask2": "Купити млеко на путу кући", + "onboardingActJustNow": "Управо сада", + "onboardingActSavedToTasks": "Сачувано у ваше задатке", + "onboardingConnectTapToConnect": "Додирните свој Omi да бисте се повезали.", + "onboardingActMicFallbackBody": "Omi још не шаље звук. За сада користите телефон.", + "onboardingActNoAudioOmi": "Не чујемо Omi. Проверите да ли је укључен и близу.", + "onboardingActNoAudioPhone": "Још вас не чујемо. Покушајте да говорите мало гласније.", + "onboardingActDictateBodyOmi": "Само причајте. Ваш Omi слуша.", + "voiceProfileCardDismissed": "Гласовни профил можете подесити било када у Подешавањима.", + "homeNewRecordingButton": "Ново снимање", + "chatWelcomeMessage": "Zdravo, ja sam Omi. Pitaj me bilo šta o čemu si pričao: šta je neko rekao, šta si obećao ili rezime tvog dana. Mogu i da dodajem zadatke i pamtim stvari za tebe.", + "onboardingStarterTaskAsk": "Pitaj Omi o svom danu", + "onboardingStarterTaskRecord": "Snimi razgovor uz Omi", + "onboardingStarterTaskCheck": "Označi ovaj zadatak da ga završiš", "paywallRecommended": "Recommended", "paywallNoPlans": "Plans aren't available right now. Try again in a minute.", "paywallOtherPlans": "See all plans", @@ -11440,21 +11483,6 @@ "paywallManageBilling": "Manage billing", "paywallLoadError": "Couldn't load plans. Check your connection and try again.", "paywallPlans": "Plans", - "firstCaptureTitle": "How will you capture?", - "firstCaptureDescription": "Choose how you want to capture your first conversation.", - "firstCaptureOmi": "I Have an Omi", - "firstCapturePhone": "This Phone", - "firstCaptureWatch": "Apple Watch", - "firstCaptureSkip": "Skip for Now", - "firstCapturePrompt": "Tell Omi what you're working on today, or what's on your mind. Talk for about 30 seconds.", - "firstCaptureKeepGoing": "Keep going… 30 seconds is ideal.", - "firstCaptureReady": "You can finish now, or keep talking.", - "firstCaptureStart": "Start Recording", - "firstCaptureFinish": "Finish Recording", - "firstCaptureListening": "Listening… your words will appear here.", - "firstCaptureProcessing": "Creating your first conversation…", - "firstCaptureResult": "Your first conversation", - "firstCaptureEmpty": "I didn't catch enough speech, try again with a few sentences", "watchCaptureTitle": "Record from your Apple Watch", "watchCaptureOpen": "Open Omi on your Apple Watch.", "watchCaptureRecord": "Tap Record. Allow microphone access if asked.", @@ -11466,17 +11494,6 @@ "pairingNotFoundOrDropped": "Couldn't reach your Omi. Make sure it's charged, close to your phone, and its light is blinking, then try again.", "pairingBondRecovery": "Forget Omi in your phone’s Bluetooth settings, then try pairing again.", "pairingHowTo": "How to pair", - "firstCaptureWatchInstructions": "Open Omi on your Apple Watch and tap Record. Allow microphone access if asked. Your capture will sync to Omi on this phone.", - "firstCaptureWatchInstall": "In the Watch app on your iPhone, install Omi on your Apple Watch. Then open Omi on your watch and tap Record.", - "firstCaptureWatchPair": "Pair your Apple Watch with this iPhone using the Watch app, then install and open Omi on your watch.", - "firstCaptureUnavailable": "Recording could not start. Check microphone access and your connection, then try again.", - "firstCaptureDelayed": "Your recording is taking longer to process. You can check your conversations on Home or try again.", - "firstCaptureEmptyTitle": "Let's try that again", - "firstCaptureShortTitle": "Finish This Recording?", - "firstCaptureShortWarning": "A few more sentences help Omi understand you. About 30 seconds is ideal.", - "firstCaptureCancelTitle": "Leave This Recording?", - "firstCaptureCancelWarning": "Going back stops this recording. Keep talking to give Omi a few more sentences.", - "firstCaptureSiriTip": "Tip: Enable Omi in Shortcuts → Siri.", "paywallOutlineHeadline": "Никада више не пропустите ни реч.", "paywallCancelAnytime": "Откажите било када.", "connectAnAgent": "Повежите агента", diff --git a/app/lib/l10n/app_sv.arb b/app/lib/l10n/app_sv.arb index 78ead4589ac..133ac9cc4f0 100644 --- a/app/lib/l10n/app_sv.arb +++ b/app/lib/l10n/app_sv.arb @@ -520,7 +520,7 @@ "dontShowAgain": "Visa inte igen", "iUnderstand": "Jag förstår", "enableBluetooth": "Aktivera Bluetooth", - "bluetoothNeeded": "Omi behöver Bluetooth för att ansluta till din bärbara enhet. Aktivera Bluetooth och försök igen.", + "bluetoothNeeded": "Omi ansluter till din telefon via Bluetooth. Slå på det för att fortsätta.", "contactSupport": "Kontakta support?", "connectLater": "Anslut senare", "grantPermissions": "Bevilja behörigheter", @@ -1501,7 +1501,7 @@ "durationSeconds": "Längd: {seconds} sekunder", "additionalSpeechSampleRemoved": "Ytterligare röstprov borttaget", "consentDataMessage": "Genom att fortsätta kommer dina konversationer, inspelningar och personlig information att lagras säkert på våra servrar. Dina ljudinspelningar och transkriptioner behandlas av AI-tjänster från tredje part (inklusive Deepgram för transkription och OpenAI för analys) för att ge dig AI-drivna insikter och aktivera alla appfunktioner.", - "tasksEmptyStateMessage": "Starta ett samtal för att skapa en uppgift.", + "tasksEmptyStateMessage": "Att göra från dina samtal visas här.", "clearChatAction": "Rensa chatt", "enableApps": "Aktivera appar", "omiAppName": "Omi", @@ -4004,6 +4004,49 @@ "reviewProposalSuggested": "Föreslaget", "reviewProposalConflict": "Det här har ändrats sedan Dream föreslog det, så inget tillämpades.", "itemLinkListTitle": "{count, plural, =0{Inga objekt} =1{1 objekt} other{{count} objekt}}", + "onboardingPermissionTapToFix": "Tryck för att åtgärda", + "onboardingNoOmiYet": "Har du inte fått din Omi än?", + "onboardingOmiOrdered": "Jag har beställt den", + "onboardingOmiNotOrdered": "Inte beställt än", + "onboardingConnectOmi": "Anslut Omi", + "consentShortMessage": "Omi använder AI för att göra dina samtal till anteckningar. Du kan radera dem när som helst.", + "consentAiProviders": "Bearbetas med {providers}.", + "onboardingActIntro": "Vi testar Omi.", + "onboardingActMicTitle": "Omi behöver din mikrofon", + "onboardingActMicBody": "Så att Omi hör dig när du pratar med den.", + "onboardingActMicAllow": "Tillåt mikrofon", + "onboardingActDictateTitle": "Berätta för Omi om din dag", + "onboardingActDictateBody": "Säg det på ditt sätt. Omi sköter resten.", + "onboardingActDictateExample": "Åt precis lunch med Sam, hon flyttar till Lissabon i juni! Påminn mig om att skicka presentationen till teamet senast fredag och att köpa mjölk på vägen hem.", + "onboardingActDictateHint": "Tryck på bocken när du är klar", + "onboardingActPhoneMic": "Telefonens mikrofon", + "onboardingActWriting": "Sammanfattar…", + "onboardingActRemembers": "Omi minns så att du slipper", + "onboardingActNotesLabel": "Din dag, sammanfattad", + "onboardingActTasksLabel": "Omi fångade dina att-göra", + "onboardingActYourWords": "Det här sa du", + "onboardingActCongratsTitle": "Allt är klart, {name}", + "onboardingActRateTitle": "Gillar du Omi hittills?", + "onboardingActRateYes": "Betygsätt Omi", + "onboardingWelcomeCardTitle": "Välkommen till Omi", + "onboardingWelcomeCardBody": "Bär Omi och prata som vanligt. Varje samtal blir här till anteckningar, uppgifter och minnen.", + "onboardingActDemoTitle": "Lunch med Sam", + "onboardingActDemoSummary": "Lunch med Sam, som flyttar till Lissabon i juni. Du vill skicka presentationen till teamet senast fredag och köpa mjölk på vägen hem.", + "onboardingActDemoTask1": "Skicka presentationen till teamet senast fredag", + "onboardingActDemoTask2": "Köp mjölk på vägen hem", + "onboardingActJustNow": "Precis nu", + "onboardingActSavedToTasks": "Sparat i dina uppgifter", + "onboardingConnectTapToConnect": "Tryck på din Omi för att ansluta.", + "onboardingActMicFallbackBody": "Din Omi skickar inget ljud än. Använd telefonen så länge.", + "onboardingActNoAudioOmi": "Hör inte din Omi. Kontrollera att den är på och nära.", + "onboardingActNoAudioPhone": "Hör dig inte än. Försök prata lite högre.", + "onboardingActDictateBodyOmi": "Prata bara. Din Omi lyssnar.", + "voiceProfileCardDismissed": "Du kan ställa in din röstprofil när som helst i Inställningar.", + "homeNewRecordingButton": "Ny inspelning", + "chatWelcomeMessage": "Hej, jag är Omi. Fråga mig om allt du har pratat om: vad någon sa, vad du lovade eller en sammanfattning av din dag. Jag kan också lägga till uppgifter och komma ihåg saker åt dig.", + "onboardingStarterTaskAsk": "Fråga Omi om din dag", + "onboardingStarterTaskRecord": "Spela in ett samtal med Omi", + "onboardingStarterTaskCheck": "Bocka av den här uppgiften för att slutföra den", "paywallRecommended": "Recommended", "paywallNoPlans": "Plans aren't available right now. Try again in a minute.", "paywallOtherPlans": "See all plans", @@ -4014,21 +4057,6 @@ "paywallManageBilling": "Manage billing", "paywallLoadError": "Couldn't load plans. Check your connection and try again.", "paywallPlans": "Plans", - "firstCaptureTitle": "How will you capture?", - "firstCaptureDescription": "Choose how you want to capture your first conversation.", - "firstCaptureOmi": "I Have an Omi", - "firstCapturePhone": "This Phone", - "firstCaptureWatch": "Apple Watch", - "firstCaptureSkip": "Skip for Now", - "firstCapturePrompt": "Tell Omi what you're working on today, or what's on your mind. Talk for about 30 seconds.", - "firstCaptureKeepGoing": "Keep going… 30 seconds is ideal.", - "firstCaptureReady": "You can finish now, or keep talking.", - "firstCaptureStart": "Start Recording", - "firstCaptureFinish": "Finish Recording", - "firstCaptureListening": "Listening… your words will appear here.", - "firstCaptureProcessing": "Creating your first conversation…", - "firstCaptureResult": "Your first conversation", - "firstCaptureEmpty": "I didn't catch enough speech, try again with a few sentences", "watchCaptureTitle": "Record from your Apple Watch", "watchCaptureOpen": "Open Omi on your Apple Watch.", "watchCaptureRecord": "Tap Record. Allow microphone access if asked.", @@ -4040,17 +4068,6 @@ "pairingNotFoundOrDropped": "Couldn't reach your Omi. Make sure it's charged, close to your phone, and its light is blinking, then try again.", "pairingBondRecovery": "Forget Omi in your phone’s Bluetooth settings, then try pairing again.", "pairingHowTo": "How to pair", - "firstCaptureWatchInstructions": "Open Omi on your Apple Watch and tap Record. Allow microphone access if asked. Your capture will sync to Omi on this phone.", - "firstCaptureWatchInstall": "In the Watch app on your iPhone, install Omi on your Apple Watch. Then open Omi on your watch and tap Record.", - "firstCaptureWatchPair": "Pair your Apple Watch with this iPhone using the Watch app, then install and open Omi on your watch.", - "firstCaptureUnavailable": "Recording could not start. Check microphone access and your connection, then try again.", - "firstCaptureDelayed": "Your recording is taking longer to process. You can check your conversations on Home or try again.", - "firstCaptureEmptyTitle": "Let's try that again", - "firstCaptureShortTitle": "Finish This Recording?", - "firstCaptureShortWarning": "A few more sentences help Omi understand you. About 30 seconds is ideal.", - "firstCaptureCancelTitle": "Leave This Recording?", - "firstCaptureCancelWarning": "Going back stops this recording. Keep talking to give Omi a few more sentences.", - "firstCaptureSiriTip": "Tip: Enable Omi in Shortcuts → Siri.", "paywallOutlineHeadline": "Missa aldrig ett ord igen.", "paywallCancelAnytime": "Avsluta när som helst.", "connectAnAgent": "Anslut en agent", diff --git a/app/lib/l10n/app_ta.arb b/app/lib/l10n/app_ta.arb index c1d2d1878de..56f790afd77 100644 --- a/app/lib/l10n/app_ta.arb +++ b/app/lib/l10n/app_ta.arb @@ -1586,7 +1586,7 @@ "@enableBluetooth": { "description": "Title for dialog prompting user to enable Bluetooth" }, - "bluetoothNeeded": "Omi உங்கள் உணர்வுமான சாதனத்துடன் இணைக்க Bluetooth தேவை. Bluetooth ஐ இயக்கவும் மற்றும் மீண்டும் முயற்சி செய்யவும்.", + "bluetoothNeeded": "Omi உங்கள் போனுடன் Bluetooth மூலம் இணைகிறது. தொடர அதை இயக்கவும்.", "@bluetoothNeeded": { "description": "Explanation text for why Bluetooth is required" }, @@ -5782,7 +5782,7 @@ "@consentDataMessage": { "description": "Consent message explaining how user data will be stored and used" }, - "tasksEmptyStateMessage": "ஒரு பணியை உருவாக்க உரையாடலைத் தொடங்குங்கள்.", + "tasksEmptyStateMessage": "உங்கள் உரையாடல்களில் உள்ள பணிகள் இங்கே தோன்றும்.", "@tasksEmptyStateMessage": { "description": "Empty state message shown when there are no tasks, with instruction to tap + button" }, @@ -11430,6 +11430,49 @@ "reviewProposalSuggested": "பரிந்துரைக்கப்பட்டது", "reviewProposalConflict": "Dream பரிந்துரைத்த பிறகு இது மாறியுள்ளது, அதனால் எதுவும் பயன்படுத்தப்படவில்லை.", "itemLinkListTitle": "{count, plural, =0{உருப்படிகள் இல்லை} =1{1 உருப்படி} other{{count} உருப்படிகள்}}", + "onboardingPermissionTapToFix": "சரிசெய்ய தட்டவும்", + "onboardingNoOmiYet": "இன்னும் உங்கள் Omi இல்லையா?", + "onboardingOmiOrdered": "நான் ஆர்டர் செய்துவிட்டேன்", + "onboardingOmiNotOrdered": "இன்னும் ஆர்டர் செய்யவில்லை", + "onboardingConnectOmi": "Omi-ஐ இணைக்கவும்", + "consentShortMessage": "Omi AI மூலம் உங்கள் உரையாடல்களைக் குறிப்புகளாக மாற்றுகிறது. எப்போது வேண்டுமானாலும் நீக்கலாம்.", + "consentAiProviders": "{providers} மூலம் செயலாக்கப்படுகிறது.", + "onboardingActIntro": "Omi-ஐ முயற்சிப்போம்.", + "onboardingActMicTitle": "Omi-க்கு உங்கள் மைக்ரோஃபோன் தேவை", + "onboardingActMicBody": "நீங்கள் பேசும்போது Omi கேட்க.", + "onboardingActMicAllow": "மைக்ரோஃபோனை அனுமதி", + "onboardingActDictateTitle": "உங்கள் நாளைப் பற்றி Omi-யிடம் சொல்லுங்கள்", + "onboardingActDictateBody": "உங்கள் பாணியில் சொல்லுங்கள். மீதியை Omi பார்த்துக்கொள்ளும்.", + "onboardingActDictateExample": "இப்போதுதான் சாமுடன் மதிய உணவு சாப்பிட்டேன், அவள் ஜூனில் லிஸ்பனுக்குப் போகிறாள்! வெள்ளிக்குள் குழுவுக்கு டெக் அனுப்பவும், வீட்டுக்குப் போகும் வழியில் பால் வாங்கவும் நினைவூட்டு.", + "onboardingActDictateHint": "முடிந்ததும் டிக்கைத் தட்டவும்", + "onboardingActPhoneMic": "தொலைபேசி மைக்ரோஃபோன்", + "onboardingActWriting": "சுருக்கப்படுகிறது…", + "onboardingActRemembers": "நீங்கள் நினைவில் வைக்க வேண்டியதில்லை, Omi வைத்துக்கொள்ளும்", + "onboardingActNotesLabel": "உங்கள் நாள், சுருக்கமாக", + "onboardingActTasksLabel": "Omi உங்கள் வேலைகளைக் கண்டறிந்தது", + "onboardingActYourWords": "நீங்கள் சொன்னது இது", + "onboardingActCongratsTitle": "எல்லாம் தயார், {name}", + "onboardingActRateTitle": "இதுவரை Omi பிடித்திருக்கிறதா?", + "onboardingActRateYes": "Omi-ஐ மதிப்பிடுங்கள்", + "onboardingWelcomeCardTitle": "Omi-க்கு வரவேற்கிறோம்", + "onboardingWelcomeCardBody": "Omi-ஐ அணிந்து வழக்கம்போல் பேசுங்கள். ஒவ்வொரு உரையாடலும் இங்கேயே குறிப்புகள், பணிகள், நினைவுகளாகும்.", + "onboardingActDemoTitle": "சாமுடன் மதிய உணவு", + "onboardingActDemoSummary": "ஜூனில் லிஸ்பனுக்குச் செல்லும் சாமுடன் மதிய உணவு. வெள்ளிக்குள் குழுவுக்கு டெக் அனுப்பவும், வீட்டுக்குப் போகும் வழியில் பால் வாங்கவும் வேண்டும்.", + "onboardingActDemoTask1": "வெள்ளிக்குள் குழுவுக்கு டெக் அனுப்புவது", + "onboardingActDemoTask2": "வீட்டுக்குப் போகும் வழியில் பால் வாங்குவது", + "onboardingActJustNow": "இப்போதுதான்", + "onboardingActSavedToTasks": "உங்கள் பணிகளில் சேமிக்கப்பட்டது", + "onboardingConnectTapToConnect": "இணைக்க உங்கள் Omi-ஐத் தட்டவும்.", + "onboardingActMicFallbackBody": "உங்கள் Omi இன்னும் ஒலியை அனுப்பவில்லை. இப்போதைக்கு உங்கள் போனைப் பயன்படுத்துங்கள்.", + "onboardingActNoAudioOmi": "உங்கள் Omi கேட்கவில்லை. அது இயக்கத்தில் உள்ளதா, அருகில் உள்ளதா எனப் பாருங்கள்.", + "onboardingActNoAudioPhone": "இன்னும் உங்கள் குரல் கேட்கவில்லை. சற்று சத்தமாகப் பேசிப் பாருங்கள்.", + "onboardingActDictateBodyOmi": "பேசுங்கள் போதும். உங்கள் Omi கேட்டுக்கொண்டிருக்கிறது.", + "voiceProfileCardDismissed": "அமைப்புகளில் எப்போது வேண்டுமானாலும் குரல் சுயவிவரத்தை அமைக்கலாம்.", + "homeNewRecordingButton": "புதிய பதிவு", + "chatWelcomeMessage": "வணக்கம், நான் Omi. நீங்கள் பேசிய எதைப் பற்றியும் கேளுங்கள்: யார் என்ன சொன்னார்கள், நீங்கள் என்ன வாக்குறுதி அளித்தீர்கள் அல்லது உங்கள் நாளின் சுருக்கம். பணிகளைச் சேர்க்கவும் உங்களுக்காக விஷயங்களை நினைவில் வைக்கவும் என்னால் முடியும்.", + "onboardingStarterTaskAsk": "உங்கள் நாளைப் பற்றி Omi-யிடம் கேளுங்கள்", + "onboardingStarterTaskRecord": "Omi மூலம் ஒரு உரையாடலைப் பதிவுசெய்யுங்கள்", + "onboardingStarterTaskCheck": "முடிக்க இந்தப் பணியைக் குறியிடுங்கள்", "paywallRecommended": "Recommended", "paywallNoPlans": "Plans aren't available right now. Try again in a minute.", "paywallOtherPlans": "See all plans", @@ -11440,21 +11483,6 @@ "paywallManageBilling": "Manage billing", "paywallLoadError": "Couldn't load plans. Check your connection and try again.", "paywallPlans": "Plans", - "firstCaptureTitle": "How will you capture?", - "firstCaptureDescription": "Choose how you want to capture your first conversation.", - "firstCaptureOmi": "I Have an Omi", - "firstCapturePhone": "This Phone", - "firstCaptureWatch": "Apple Watch", - "firstCaptureSkip": "Skip for Now", - "firstCapturePrompt": "Tell Omi what you're working on today, or what's on your mind. Talk for about 30 seconds.", - "firstCaptureKeepGoing": "Keep going… 30 seconds is ideal.", - "firstCaptureReady": "You can finish now, or keep talking.", - "firstCaptureStart": "Start Recording", - "firstCaptureFinish": "Finish Recording", - "firstCaptureListening": "Listening… your words will appear here.", - "firstCaptureProcessing": "Creating your first conversation…", - "firstCaptureResult": "Your first conversation", - "firstCaptureEmpty": "I didn't catch enough speech, try again with a few sentences", "watchCaptureTitle": "Record from your Apple Watch", "watchCaptureOpen": "Open Omi on your Apple Watch.", "watchCaptureRecord": "Tap Record. Allow microphone access if asked.", @@ -11466,17 +11494,6 @@ "pairingNotFoundOrDropped": "Couldn't reach your Omi. Make sure it's charged, close to your phone, and its light is blinking, then try again.", "pairingBondRecovery": "Forget Omi in your phone’s Bluetooth settings, then try pairing again.", "pairingHowTo": "How to pair", - "firstCaptureWatchInstructions": "Open Omi on your Apple Watch and tap Record. Allow microphone access if asked. Your capture will sync to Omi on this phone.", - "firstCaptureWatchInstall": "In the Watch app on your iPhone, install Omi on your Apple Watch. Then open Omi on your watch and tap Record.", - "firstCaptureWatchPair": "Pair your Apple Watch with this iPhone using the Watch app, then install and open Omi on your watch.", - "firstCaptureUnavailable": "Recording could not start. Check microphone access and your connection, then try again.", - "firstCaptureDelayed": "Your recording is taking longer to process. You can check your conversations on Home or try again.", - "firstCaptureEmptyTitle": "Let's try that again", - "firstCaptureShortTitle": "Finish This Recording?", - "firstCaptureShortWarning": "A few more sentences help Omi understand you. About 30 seconds is ideal.", - "firstCaptureCancelTitle": "Leave This Recording?", - "firstCaptureCancelWarning": "Going back stops this recording. Keep talking to give Omi a few more sentences.", - "firstCaptureSiriTip": "Tip: Enable Omi in Shortcuts → Siri.", "paywallOutlineHeadline": "இனி ஒரு வார்த்தையையும் தவறவிடாதீர்கள்.", "paywallCancelAnytime": "எப்போது வேண்டுமானாலும் ரத்து செய்யலாம்.", "connectAnAgent": "ஒரு ஏஜென்டை இணைக்கவும்", diff --git a/app/lib/l10n/app_te.arb b/app/lib/l10n/app_te.arb index b33ed7f2478..4d68f8b4cbd 100644 --- a/app/lib/l10n/app_te.arb +++ b/app/lib/l10n/app_te.arb @@ -1586,7 +1586,7 @@ "@enableBluetooth": { "description": "Title for dialog prompting user to enable Bluetooth" }, - "bluetoothNeeded": "మీ ధరించదగిన పరికరానికి కనెక్ట్ చేయడానికి Omi కు బ్లూటూత్ అవసరం. దయచేసి బ్లూటూత్‌ను ప్రారంభించి మళ్లీ ప్రయత్నించండి.", + "bluetoothNeeded": "Omi మీ ఫోన్‌కు Bluetooth ద్వారా కనెక్ట్ అవుతుంది. కొనసాగడానికి దాన్ని ఆన్ చేయండి.", "@bluetoothNeeded": { "description": "Explanation text for why Bluetooth is required" }, @@ -5782,7 +5782,7 @@ "@consentDataMessage": { "description": "Consent message explaining how user data will be stored and used" }, - "tasksEmptyStateMessage": "పనిని సృష్టించడానికి సంభాషణను ప్రారంభించండి.", + "tasksEmptyStateMessage": "మీ సంభాషణల నుండి పనులు ఇక్కడ కనిపిస్తాయి.", "@tasksEmptyStateMessage": { "description": "Empty state message shown when there are no tasks, with instruction to tap + button" }, @@ -11430,6 +11430,49 @@ "reviewProposalSuggested": "సూచించినది", "reviewProposalConflict": "Dream సూచించిన తర్వాత ఇది మారింది, కాబట్టి ఏదీ వర్తింపజేయలేదు.", "itemLinkListTitle": "{count, plural, =0{అంశాలు లేవు} =1{1 అంశం} other{{count} అంశాలు}}", + "onboardingPermissionTapToFix": "సరిచేయడానికి నొక్కండి", + "onboardingNoOmiYet": "ఇంకా మీ Omi లేదా?", + "onboardingOmiOrdered": "నేను ఆర్డర్ చేశాను", + "onboardingOmiNotOrdered": "ఇంకా ఆర్డర్ చేయలేదు", + "onboardingConnectOmi": "Omiని కనెక్ట్ చేయండి", + "consentShortMessage": "Omi AI సహాయంతో మీ సంభాషణలను నోట్స్‌గా మారుస్తుంది. వాటిని ఎప్పుడైనా తొలగించవచ్చు.", + "consentAiProviders": "{providers} ద్వారా ప్రాసెస్ చేయబడుతుంది.", + "onboardingActIntro": "Omi ని ప్రయత్నిద్దాం.", + "onboardingActMicTitle": "Omiకి మీ మైక్రోఫోన్ కావాలి", + "onboardingActMicBody": "మీరు మాట్లాడినప్పుడు Omi వినడానికి.", + "onboardingActMicAllow": "మైక్రోఫోన్‌ను అనుమతించండి", + "onboardingActDictateTitle": "మీ రోజు గురించి Omiకి చెప్పండి", + "onboardingActDictateBody": "మీ శైలిలో చెప్పండి. మిగతాది Omi చూసుకుంటుంది.", + "onboardingActDictateExample": "ఇప్పుడే సామ్‌తో లంచ్ చేశా, తను జూన్‌లో లిస్బన్‌కి వెళ్తోంది! శుక్రవారంలోగా టీమ్‌కి డెక్ పంపాలని, ఇంటికి వెళ్లేటప్పుడు పాలు తీసుకోవాలని గుర్తు చేయి.", + "onboardingActDictateHint": "అయ్యాక టిక్ నొక్కండి", + "onboardingActPhoneMic": "ఫోన్ మైక్రోఫోన్", + "onboardingActWriting": "సారాంశం తయారవుతోంది…", + "onboardingActRemembers": "మీరు గుర్తుంచుకోనవసరం లేదు, Omi గుర్తుంచుకుంటుంది", + "onboardingActNotesLabel": "మీ రోజు, సంక్షిప్తంగా", + "onboardingActTasksLabel": "Omi మీ పనులను గుర్తించింది", + "onboardingActYourWords": "మీరు చెప్పింది ఇది", + "onboardingActCongratsTitle": "అంతా సిద్ధం, {name}", + "onboardingActRateTitle": "ఇప్పటివరకు Omi నచ్చిందా?", + "onboardingActRateYes": "Omiని రేట్ చేయండి", + "onboardingWelcomeCardTitle": "Omiకి స్వాగతం", + "onboardingWelcomeCardBody": "Omi ధరించి మామూలుగా మాట్లాడండి. ప్రతి సంభాషణ ఇక్కడే నోట్స్, పనులు, జ్ఞాపకాలుగా మారుతుంది.", + "onboardingActDemoTitle": "సామ్‌తో లంచ్", + "onboardingActDemoSummary": "జూన్‌లో లిస్బన్‌కు వెళ్తున్న సామ్‌తో లంచ్. శుక్రవారం లోపు టీమ్‌కు డెక్ పంపాలి, ఇంటికి వెళ్తూ పాలు కొనాలి.", + "onboardingActDemoTask1": "శుక్రవారంలోగా టీమ్‌కి డెక్ పంపడం", + "onboardingActDemoTask2": "ఇంటికి వెళ్లేటప్పుడు పాలు తీసుకోవడం", + "onboardingActJustNow": "ఇప్పుడే", + "onboardingActSavedToTasks": "మీ పనుల్లో సేవ్ అయింది", + "onboardingConnectTapToConnect": "కనెక్ట్ చేయడానికి మీ Omi ని ట్యాప్ చేయండి.", + "onboardingActMicFallbackBody": "మీ Omi ఇంకా ఆడియో పంపడం లేదు. ప్రస్తుతానికి మీ ఫోన్ వాడండి.", + "onboardingActNoAudioOmi": "మీ Omi వినిపించడం లేదు. అది ఆన్‌లో ఉందో, దగ్గర్లో ఉందో చూడండి.", + "onboardingActNoAudioPhone": "ఇంకా మీ గొంతు వినిపించడం లేదు. కొంచెం గట్టిగా మాట్లాడి చూడండి.", + "onboardingActDictateBodyOmi": "మాట్లాడండి చాలు. మీ Omi వింటోంది.", + "voiceProfileCardDismissed": "సెట్టింగ్‌లలో ఎప్పుడైనా వాయిస్ ప్రొఫైల్ సెటప్ చేయవచ్చు.", + "homeNewRecordingButton": "కొత్త రికార్డింగ్", + "chatWelcomeMessage": "హాయ్, నేను Omi. మీరు మాట్లాడిన దేని గురించైనా అడగండి: ఎవరు ఏమన్నారు, మీరు ఏమి మాట ఇచ్చారు లేదా మీ రోజు సారాంశం. నేను పనులను జోడించగలను, మీ కోసం విషయాలు గుర్తుంచుకోగలను.", + "onboardingStarterTaskAsk": "మీ రోజు గురించి Omi ని అడగండి", + "onboardingStarterTaskRecord": "Omi తో ఒక సంభాషణను రికార్డ్ చేయండి", + "onboardingStarterTaskCheck": "పూర్తి చేయడానికి ఈ పనిని టిక్ చేయండి", "paywallRecommended": "Recommended", "paywallNoPlans": "Plans aren't available right now. Try again in a minute.", "paywallOtherPlans": "See all plans", @@ -11440,21 +11483,6 @@ "paywallManageBilling": "Manage billing", "paywallLoadError": "Couldn't load plans. Check your connection and try again.", "paywallPlans": "Plans", - "firstCaptureTitle": "How will you capture?", - "firstCaptureDescription": "Choose how you want to capture your first conversation.", - "firstCaptureOmi": "I Have an Omi", - "firstCapturePhone": "This Phone", - "firstCaptureWatch": "Apple Watch", - "firstCaptureSkip": "Skip for Now", - "firstCapturePrompt": "Tell Omi what you're working on today, or what's on your mind. Talk for about 30 seconds.", - "firstCaptureKeepGoing": "Keep going… 30 seconds is ideal.", - "firstCaptureReady": "You can finish now, or keep talking.", - "firstCaptureStart": "Start Recording", - "firstCaptureFinish": "Finish Recording", - "firstCaptureListening": "Listening… your words will appear here.", - "firstCaptureProcessing": "Creating your first conversation…", - "firstCaptureResult": "Your first conversation", - "firstCaptureEmpty": "I didn't catch enough speech, try again with a few sentences", "watchCaptureTitle": "Record from your Apple Watch", "watchCaptureOpen": "Open Omi on your Apple Watch.", "watchCaptureRecord": "Tap Record. Allow microphone access if asked.", @@ -11466,17 +11494,6 @@ "pairingNotFoundOrDropped": "Couldn't reach your Omi. Make sure it's charged, close to your phone, and its light is blinking, then try again.", "pairingBondRecovery": "Forget Omi in your phone’s Bluetooth settings, then try pairing again.", "pairingHowTo": "How to pair", - "firstCaptureWatchInstructions": "Open Omi on your Apple Watch and tap Record. Allow microphone access if asked. Your capture will sync to Omi on this phone.", - "firstCaptureWatchInstall": "In the Watch app on your iPhone, install Omi on your Apple Watch. Then open Omi on your watch and tap Record.", - "firstCaptureWatchPair": "Pair your Apple Watch with this iPhone using the Watch app, then install and open Omi on your watch.", - "firstCaptureUnavailable": "Recording could not start. Check microphone access and your connection, then try again.", - "firstCaptureDelayed": "Your recording is taking longer to process. You can check your conversations on Home or try again.", - "firstCaptureEmptyTitle": "Let's try that again", - "firstCaptureShortTitle": "Finish This Recording?", - "firstCaptureShortWarning": "A few more sentences help Omi understand you. About 30 seconds is ideal.", - "firstCaptureCancelTitle": "Leave This Recording?", - "firstCaptureCancelWarning": "Going back stops this recording. Keep talking to give Omi a few more sentences.", - "firstCaptureSiriTip": "Tip: Enable Omi in Shortcuts → Siri.", "paywallOutlineHeadline": "ఇకపై ఒక్క మాట కూడా మిస్ అవ్వకండి.", "paywallCancelAnytime": "ఎప్పుడైనా రద్దు చేసుకోవచ్చు.", "connectAnAgent": "ఏజెంట్‌ను కనెక్ట్ చేయండి", diff --git a/app/lib/l10n/app_th.arb b/app/lib/l10n/app_th.arb index 28d45fff403..cf137d3f717 100644 --- a/app/lib/l10n/app_th.arb +++ b/app/lib/l10n/app_th.arb @@ -520,7 +520,7 @@ "dontShowAgain": "ไม่ต้องแสดงอีก", "iUnderstand": "เข้าใจแล้ว", "enableBluetooth": "เปิดใช้งาน Bluetooth", - "bluetoothNeeded": "Omi ต้องการ Bluetooth เพื่อเชื่อมต่อกับอุปกรณ์สวมใส่ของคุณ กรุณาเปิดใช้งาน Bluetooth แล้วลองอีกครั้ง", + "bluetoothNeeded": "Omi เชื่อมต่อกับโทรศัพท์ผ่าน Bluetooth เปิดเพื่อดำเนินการต่อ", "contactSupport": "ติดต่อฝ่ายสนับสนุน?", "connectLater": "เชื่อมต่อภายหลัง", "grantPermissions": "ให้สิทธิ์", @@ -1501,7 +1501,7 @@ "durationSeconds": "ระยะเวลา: {seconds} วินาที", "additionalSpeechSampleRemoved": "ลบตัวอย่างเสียงเพิ่มเติมแล้ว", "consentDataMessage": "เมื่อดำเนินการต่อ การสนทนา การบันทึก และข้อมูลส่วนบุคคลของคุณจะถูกจัดเก็บอย่างปลอดภัยบนเซิร์ฟเวอร์ของเรา การบันทึกเสียงและการถอดความของคุณจะถูกประมวลผลโดยบริการ AI ของบุคคลที่สาม (รวมถึง Deepgram สำหรับการถอดความ และ OpenAI สำหรับการวิเคราะห์) เพื่อมอบข้อมูลเชิงลึกที่ขับเคลื่อนด้วย AI และเปิดใช้งานคุณสมบัติทั้งหมดของแอป", - "tasksEmptyStateMessage": "เริ่มการสนทนาเพื่อสร้างงาน", + "tasksEmptyStateMessage": "สิ่งที่ต้องทำจากบทสนทนาของคุณจะแสดงที่นี่", "clearChatAction": "ล้างแชท", "enableApps": "เปิดใช้งานแอป", "omiAppName": "Omi", @@ -4004,6 +4004,49 @@ "reviewProposalSuggested": "ที่แนะนำ", "reviewProposalConflict": "รายการนี้เปลี่ยนไปหลังจาก Dream แนะนำ จึงไม่มีการใช้การแก้ไขใด ๆ", "itemLinkListTitle": "{count, plural, =0{ไม่มีรายการ} =1{1 รายการ} other{{count} รายการ}}", + "onboardingPermissionTapToFix": "แตะเพื่อแก้ไข", + "onboardingNoOmiYet": "ยังไม่มี Omi ใช่ไหม", + "onboardingOmiOrdered": "สั่งซื้อแล้ว", + "onboardingOmiNotOrdered": "ยังไม่ได้สั่งซื้อ", + "onboardingConnectOmi": "เชื่อมต่อ Omi", + "consentShortMessage": "Omi ใช้ AI เปลี่ยนบทสนทนาของคุณเป็นโน้ต คุณลบได้ทุกเมื่อ", + "consentAiProviders": "ประมวลผลด้วย {providers}", + "onboardingActIntro": "มาลองใช้ Omi กัน", + "onboardingActMicTitle": "Omi ต้องใช้ไมโครโฟน", + "onboardingActMicBody": "เพื่อให้ Omi ได้ยินเมื่อคุณพูดด้วย", + "onboardingActMicAllow": "อนุญาตไมโครโฟน", + "onboardingActDictateTitle": "เล่าวันของคุณให้ Omi ฟัง", + "onboardingActDictateBody": "พูดแบบที่คุณถนัด ที่เหลือ Omi จัดการเอง", + "onboardingActDictateExample": "เพิ่งกินข้าวกลางวันกับแซม เธอจะย้ายไปลิสบอนเดือนมิถุนายน! เตือนฉันส่งสไลด์ให้ทีมภายในวันศุกร์ แล้วก็ซื้อนมตอนกลับบ้านด้วย", + "onboardingActDictateHint": "แตะเครื่องหมายถูกเมื่อพูดเสร็จ", + "onboardingActPhoneMic": "ไมโครโฟนโทรศัพท์", + "onboardingActWriting": "กำลังสรุป…", + "onboardingActRemembers": "Omi จำให้ คุณไม่ต้องจำเอง", + "onboardingActNotesLabel": "สรุปวันของคุณ", + "onboardingActTasksLabel": "Omi จับสิ่งที่ต้องทำให้แล้ว", + "onboardingActYourWords": "สิ่งที่คุณพูด", + "onboardingActCongratsTitle": "พร้อมแล้ว {name}", + "onboardingActRateTitle": "ชอบ Omi ไหม", + "onboardingActRateYes": "ให้คะแนน Omi", + "onboardingWelcomeCardTitle": "ยินดีต้อนรับสู่ Omi", + "onboardingWelcomeCardBody": "สวม Omi แล้วคุยตามปกติ ทุกบทสนทนาจะกลายเป็นโน้ต งาน และความทรงจำตรงนี้", + "onboardingActDemoTitle": "มื้อกลางวันกับแซม", + "onboardingActDemoSummary": "ทานข้าวกลางวันกับแซมที่จะย้ายไปลิสบอนเดือนมิถุนายน คุณต้องส่งสไลด์ให้ทีมภายในวันศุกร์ และซื้อนมระหว่างทางกลับบ้าน", + "onboardingActDemoTask1": "ส่งสไลด์ให้ทีมภายในวันศุกร์", + "onboardingActDemoTask2": "ซื้อนมตอนกลับบ้าน", + "onboardingActJustNow": "เมื่อสักครู่", + "onboardingActSavedToTasks": "บันทึกไว้ในงานของคุณแล้ว", + "onboardingConnectTapToConnect": "แตะ Omi ของคุณเพื่อเชื่อมต่อ", + "onboardingActMicFallbackBody": "Omi ยังไม่ส่งเสียงมา ใช้โทรศัพท์ไปก่อนนะ", + "onboardingActNoAudioOmi": "ไม่ได้ยินเสียงจาก Omi ตรวจดูว่าเปิดอยู่และอยู่ใกล้ๆ", + "onboardingActNoAudioPhone": "ยังไม่ได้ยินเสียงคุณ ลองพูดดังขึ้นอีกนิด", + "onboardingActDictateBodyOmi": "พูดได้เลย Omi กำลังฟังอยู่", + "voiceProfileCardDismissed": "คุณตั้งค่าโปรไฟล์เสียงได้ทุกเมื่อในการตั้งค่า", + "homeNewRecordingButton": "บันทึกใหม่", + "chatWelcomeMessage": "สวัสดี ฉันคือ Omi ถามอะไรก็ได้เกี่ยวกับสิ่งที่คุณคุยไว้ เช่น ใครพูดอะไร คุณสัญญาอะไรไว้ หรือสรุปวันของคุณ ฉันยังเพิ่มสิ่งที่ต้องทำและจำเรื่องต่าง ๆ ให้คุณได้ด้วย", + "onboardingStarterTaskAsk": "ถาม Omi เกี่ยวกับวันของคุณ", + "onboardingStarterTaskRecord": "บันทึกบทสนทนาด้วย Omi", + "onboardingStarterTaskCheck": "ติ๊กงานนี้เพื่อทำให้เสร็จ", "paywallRecommended": "Recommended", "paywallNoPlans": "Plans aren't available right now. Try again in a minute.", "paywallOtherPlans": "See all plans", @@ -4014,21 +4057,6 @@ "paywallManageBilling": "Manage billing", "paywallLoadError": "Couldn't load plans. Check your connection and try again.", "paywallPlans": "Plans", - "firstCaptureTitle": "How will you capture?", - "firstCaptureDescription": "Choose how you want to capture your first conversation.", - "firstCaptureOmi": "I Have an Omi", - "firstCapturePhone": "This Phone", - "firstCaptureWatch": "Apple Watch", - "firstCaptureSkip": "Skip for Now", - "firstCapturePrompt": "Tell Omi what you're working on today, or what's on your mind. Talk for about 30 seconds.", - "firstCaptureKeepGoing": "Keep going… 30 seconds is ideal.", - "firstCaptureReady": "You can finish now, or keep talking.", - "firstCaptureStart": "Start Recording", - "firstCaptureFinish": "Finish Recording", - "firstCaptureListening": "Listening… your words will appear here.", - "firstCaptureProcessing": "Creating your first conversation…", - "firstCaptureResult": "Your first conversation", - "firstCaptureEmpty": "I didn't catch enough speech, try again with a few sentences", "watchCaptureTitle": "Record from your Apple Watch", "watchCaptureOpen": "Open Omi on your Apple Watch.", "watchCaptureRecord": "Tap Record. Allow microphone access if asked.", @@ -4040,17 +4068,6 @@ "pairingNotFoundOrDropped": "Couldn't reach your Omi. Make sure it's charged, close to your phone, and its light is blinking, then try again.", "pairingBondRecovery": "Forget Omi in your phone’s Bluetooth settings, then try pairing again.", "pairingHowTo": "How to pair", - "firstCaptureWatchInstructions": "Open Omi on your Apple Watch and tap Record. Allow microphone access if asked. Your capture will sync to Omi on this phone.", - "firstCaptureWatchInstall": "In the Watch app on your iPhone, install Omi on your Apple Watch. Then open Omi on your watch and tap Record.", - "firstCaptureWatchPair": "Pair your Apple Watch with this iPhone using the Watch app, then install and open Omi on your watch.", - "firstCaptureUnavailable": "Recording could not start. Check microphone access and your connection, then try again.", - "firstCaptureDelayed": "Your recording is taking longer to process. You can check your conversations on Home or try again.", - "firstCaptureEmptyTitle": "Let's try that again", - "firstCaptureShortTitle": "Finish This Recording?", - "firstCaptureShortWarning": "A few more sentences help Omi understand you. About 30 seconds is ideal.", - "firstCaptureCancelTitle": "Leave This Recording?", - "firstCaptureCancelWarning": "Going back stops this recording. Keep talking to give Omi a few more sentences.", - "firstCaptureSiriTip": "Tip: Enable Omi in Shortcuts → Siri.", "paywallOutlineHeadline": "ไม่พลาดแม้แต่คำเดียวอีกต่อไป", "paywallCancelAnytime": "ยกเลิกได้ทุกเมื่อ", "connectAnAgent": "เชื่อมต่อเอเจนต์", diff --git a/app/lib/l10n/app_tl.arb b/app/lib/l10n/app_tl.arb index b047dc54ed5..8690d7b7e98 100644 --- a/app/lib/l10n/app_tl.arb +++ b/app/lib/l10n/app_tl.arb @@ -1586,7 +1586,7 @@ "@enableBluetooth": { "description": "Title for dialog prompting user to enable Bluetooth" }, - "bluetoothNeeded": "Ang Omi ay kailangan ng Bluetooth upang kumonekta sa iyong wearable. Mangyaring paganahin ang Bluetooth at subukan muli.", + "bluetoothNeeded": "Kumokonekta ang Omi sa iyong phone gamit ang Bluetooth. I-on ito para magpatuloy.", "@bluetoothNeeded": { "description": "Explanation text for why Bluetooth is required" }, @@ -5782,7 +5782,7 @@ "@consentDataMessage": { "description": "Consent message explaining how user data will be stored and used" }, - "tasksEmptyStateMessage": "Magsimula ng pag-uusap para gumawa ng gawain.", + "tasksEmptyStateMessage": "Lalabas dito ang mga gagawin mula sa iyong mga usapan.", "@tasksEmptyStateMessage": { "description": "Empty state message shown when there are no tasks, with instruction to tap + button" }, @@ -11430,6 +11430,49 @@ "reviewProposalSuggested": "Iminungkahi", "reviewProposalConflict": "Nagbago ito mula nang imungkahi ng Dream, kaya walang nailapat.", "itemLinkListTitle": "{count, plural, =0{Walang Item} =1{1 Item} other{{count} Item}}", + "onboardingPermissionTapToFix": "I-tap para ayusin", + "onboardingNoOmiYet": "Wala ka pang Omi?", + "onboardingOmiOrdered": "Na-order ko na", + "onboardingOmiNotOrdered": "Hindi pa naka-order", + "onboardingConnectOmi": "Ikonekta ang Omi", + "consentShortMessage": "Ginagamit ng Omi ang AI para gawing notes ang iyong mga usapan. Puwede mo itong burahin anumang oras.", + "consentAiProviders": "Pinoproseso gamit ang {providers}.", + "onboardingActIntro": "Subukan natin ang Omi.", + "onboardingActMicTitle": "Kailangan ng Omi ang mikropono mo", + "onboardingActMicBody": "Para marinig ka ng Omi kapag kinakausap mo ito.", + "onboardingActMicAllow": "Payagan ang Mikropono", + "onboardingActDictateTitle": "Ikuwento sa Omi ang araw mo", + "onboardingActDictateBody": "Sabihin mo sa sarili mong paraan. Ang Omi na ang bahala.", + "onboardingActDictateExample": "Kakatapos ko lang mag-lunch kasama si Sam, lilipat na siya sa Lisbon sa Hunyo! Ipaalala mo sa akin na ipadala ang deck sa team bago mag-Biyernes, at bumili ng gatas pauwi.", + "onboardingActDictateHint": "I-tap ang check kapag tapos ka na", + "onboardingActPhoneMic": "Mikropono ng phone", + "onboardingActWriting": "Binubuod…", + "onboardingActRemembers": "Tinatandaan ng Omi para hindi mo na kailangan", + "onboardingActNotesLabel": "Ang araw mo, buod", + "onboardingActTasksLabel": "Nahuli ng Omi ang mga gagawin mo", + "onboardingActYourWords": "Ito ang sinabi mo", + "onboardingActCongratsTitle": "Handa ka na, {name}", + "onboardingActRateTitle": "Gusto mo ba ang Omi sa ngayon?", + "onboardingActRateYes": "I-rate ang Omi", + "onboardingWelcomeCardTitle": "Maligayang pagdating sa Omi", + "onboardingWelcomeCardBody": "Isuot ang Omi at makipag-usap gaya ng dati. Nagiging tala, gawain, at alaala dito ang bawat usapan.", + "onboardingActDemoTitle": "Lunch kasama si Sam", + "onboardingActDemoSummary": "Tanghalian kasama si Sam, na lilipat sa Lisbon sa Hunyo. Gusto mong ipadala ang deck sa team bago mag-Biyernes at bumili ng gatas pauwi.", + "onboardingActDemoTask1": "Ipadala ang deck sa team bago mag-Biyernes", + "onboardingActDemoTask2": "Bumili ng gatas pauwi", + "onboardingActJustNow": "Ngayon lang", + "onboardingActSavedToTasks": "Na-save sa mga gawain mo", + "onboardingConnectTapToConnect": "I-tap ang iyong Omi para kumonekta.", + "onboardingActMicFallbackBody": "Hindi pa nagpapadala ng audio ang Omi mo. Gamitin muna ang iyong phone.", + "onboardingActNoAudioOmi": "Hindi marinig ang Omi mo. Siguraduhing naka-on ito at malapit.", + "onboardingActNoAudioPhone": "Hindi ka pa marinig. Subukang magsalita nang medyo malakas.", + "onboardingActDictateBodyOmi": "Magsalita ka lang. Nakikinig ang Omi mo.", + "voiceProfileCardDismissed": "Maaari mong i-set up ang Voice Profile anumang oras sa Settings.", + "homeNewRecordingButton": "Bagong Recording", + "chatWelcomeMessage": "Hi, ako si Omi. Itanong mo sa akin ang kahit ano tungkol sa mga napag-usapan mo: ang sinabi ng isang tao, ang ipinangako mo, o buod ng araw mo. Kaya ko ring magdagdag ng mga gawain at tandaan ang mga bagay para sa iyo.", + "onboardingStarterTaskAsk": "Tanungin si Omi tungkol sa araw mo", + "onboardingStarterTaskRecord": "Mag-record ng usapan gamit ang Omi", + "onboardingStarterTaskCheck": "Lagyan ng tsek ang gawaing ito para matapos", "paywallRecommended": "Recommended", "paywallNoPlans": "Plans aren't available right now. Try again in a minute.", "paywallOtherPlans": "See all plans", @@ -11440,21 +11483,6 @@ "paywallManageBilling": "Manage billing", "paywallLoadError": "Couldn't load plans. Check your connection and try again.", "paywallPlans": "Plans", - "firstCaptureTitle": "How will you capture?", - "firstCaptureDescription": "Choose how you want to capture your first conversation.", - "firstCaptureOmi": "I Have an Omi", - "firstCapturePhone": "This Phone", - "firstCaptureWatch": "Apple Watch", - "firstCaptureSkip": "Skip for Now", - "firstCapturePrompt": "Tell Omi what you're working on today, or what's on your mind. Talk for about 30 seconds.", - "firstCaptureKeepGoing": "Keep going… 30 seconds is ideal.", - "firstCaptureReady": "You can finish now, or keep talking.", - "firstCaptureStart": "Start Recording", - "firstCaptureFinish": "Finish Recording", - "firstCaptureListening": "Listening… your words will appear here.", - "firstCaptureProcessing": "Creating your first conversation…", - "firstCaptureResult": "Your first conversation", - "firstCaptureEmpty": "I didn't catch enough speech, try again with a few sentences", "watchCaptureTitle": "Record from your Apple Watch", "watchCaptureOpen": "Open Omi on your Apple Watch.", "watchCaptureRecord": "Tap Record. Allow microphone access if asked.", @@ -11466,17 +11494,6 @@ "pairingNotFoundOrDropped": "Couldn't reach your Omi. Make sure it's charged, close to your phone, and its light is blinking, then try again.", "pairingBondRecovery": "Forget Omi in your phone’s Bluetooth settings, then try pairing again.", "pairingHowTo": "How to pair", - "firstCaptureWatchInstructions": "Open Omi on your Apple Watch and tap Record. Allow microphone access if asked. Your capture will sync to Omi on this phone.", - "firstCaptureWatchInstall": "In the Watch app on your iPhone, install Omi on your Apple Watch. Then open Omi on your watch and tap Record.", - "firstCaptureWatchPair": "Pair your Apple Watch with this iPhone using the Watch app, then install and open Omi on your watch.", - "firstCaptureUnavailable": "Recording could not start. Check microphone access and your connection, then try again.", - "firstCaptureDelayed": "Your recording is taking longer to process. You can check your conversations on Home or try again.", - "firstCaptureEmptyTitle": "Let's try that again", - "firstCaptureShortTitle": "Finish This Recording?", - "firstCaptureShortWarning": "A few more sentences help Omi understand you. About 30 seconds is ideal.", - "firstCaptureCancelTitle": "Leave This Recording?", - "firstCaptureCancelWarning": "Going back stops this recording. Keep talking to give Omi a few more sentences.", - "firstCaptureSiriTip": "Tip: Enable Omi in Shortcuts → Siri.", "paywallOutlineHeadline": "Huwag nang palampasin ang kahit isang salita.", "paywallCancelAnytime": "Kanselahin anumang oras.", "connectAnAgent": "Ikonekta ang isang Agent", diff --git a/app/lib/l10n/app_tr.arb b/app/lib/l10n/app_tr.arb index 11d49f7ca4a..5613d29e3bb 100644 --- a/app/lib/l10n/app_tr.arb +++ b/app/lib/l10n/app_tr.arb @@ -520,7 +520,7 @@ "dontShowAgain": "Tekrar gösterme", "iUnderstand": "Anlıyorum", "enableBluetooth": "Bluetooth'u Etkinleştir", - "bluetoothNeeded": "Omi'nin giyilebilir cihazınıza bağlanması için Bluetooth gereklidir. Lütfen Bluetooth'u etkinleştirin ve tekrar deneyin.", + "bluetoothNeeded": "Omi telefonuna Bluetooth ile bağlanır. Devam etmek için açın.", "contactSupport": "Desteğe Başvur?", "connectLater": "Sonra Bağlan", "grantPermissions": "İzinleri ver", @@ -1501,7 +1501,7 @@ "durationSeconds": "Süre: {seconds} saniye", "additionalSpeechSampleRemoved": "Ek ses örneği kaldırıldı", "consentDataMessage": "Devam ederek, konuşmalarınız, kayıtlarınız ve kişisel bilgileriniz sunucularımızda güvenli bir şekilde saklanacaktır. Ses kayıtlarınız ve transkriptleriniz, size yapay zeka destekli içgörüler sağlamak ve tüm uygulama özelliklerini etkinleştirmek için üçüncü taraf yapay zeka hizmetleri (transkripsiyon için Deepgram ve analiz için OpenAI dahil) tarafından işlenir.", - "tasksEmptyStateMessage": "Görev oluşturmak için bir konuşma başlatın.", + "tasksEmptyStateMessage": "Konuşmalarındaki yapılacaklar burada görünecek.", "clearChatAction": "Sohbeti temizle", "enableApps": "Uygulamaları etkinleştir", "omiAppName": "Omi", @@ -4039,6 +4039,49 @@ "reviewProposalSuggested": "Önerilen", "reviewProposalConflict": "Bu, Dream önerdikten sonra değişti; bu yüzden hiçbir şey uygulanmadı.", "itemLinkListTitle": "{count, plural, =0{Öğe Yok} =1{1 Öğe} other{{count} Öğe}}", + "onboardingPermissionTapToFix": "Düzeltmek için dokunun", + "onboardingNoOmiYet": "Omi'n henüz yok mu?", + "onboardingOmiOrdered": "Sipariş verdim", + "onboardingOmiNotOrdered": "Henüz sipariş vermedim", + "onboardingConnectOmi": "Omi'yi bağla", + "consentShortMessage": "Omi, konuşmalarını yapay zekâ ile notlara dönüştürür. İstediğin zaman silebilirsin.", + "consentAiProviders": "{providers} ile işlenir.", + "onboardingActIntro": "Hadi Omi'yi deneyelim.", + "onboardingActMicTitle": "Omi'nin mikrofonuna ihtiyacı var", + "onboardingActMicBody": "Omi'yle konuştuğunda seni duyabilmesi için.", + "onboardingActMicAllow": "Mikrofona İzin Ver", + "onboardingActDictateTitle": "Omi'ye gününü anlat", + "onboardingActDictateBody": "Kendi tarzınla söyle. Gerisini Omi halleder.", + "onboardingActDictateExample": "Az önce Sam'le öğle yemeği yedim, haziranda Lizbon'a taşınıyor! Cumaya kadar sunumu ekibe göndermemi ve eve dönerken süt almamı hatırlat.", + "onboardingActDictateHint": "Bitince onay işaretine dokun", + "onboardingActPhoneMic": "Telefon mikrofonu", + "onboardingActWriting": "Özetleniyor…", + "onboardingActRemembers": "Omi hatırlar, senin hatırlamana gerek kalmaz", + "onboardingActNotesLabel": "Günün, özetle", + "onboardingActTasksLabel": "Omi yapılacaklarını yakaladı", + "onboardingActYourWords": "Söylediklerin", + "onboardingActCongratsTitle": "Her şey hazır, {name}", + "onboardingActRateTitle": "Omi'yi beğendin mi?", + "onboardingActRateYes": "Omi'yi Puanla", + "onboardingWelcomeCardTitle": "Omi'ye hoş geldin", + "onboardingWelcomeCardBody": "Omi'yi tak ve her zamanki gibi konuş. Her konuşma burada nota, göreve ve anıya dönüşür.", + "onboardingActDemoTitle": "Sam'le öğle yemeği", + "onboardingActDemoSummary": "Haziranda Lizbon'a taşınan Sam'le öğle yemeği. Cumaya kadar sunumu ekibine göndermek ve eve dönerken süt almak istiyorsun.", + "onboardingActDemoTask1": "Cumaya kadar sunumu ekibe gönder", + "onboardingActDemoTask2": "Eve dönerken süt al", + "onboardingActJustNow": "Az önce", + "onboardingActSavedToTasks": "Görevlerine kaydedildi", + "onboardingConnectTapToConnect": "Bağlamak için Omi'ne dokun.", + "onboardingActMicFallbackBody": "Omi'n henüz ses göndermiyor. Şimdilik telefonunu kullan.", + "onboardingActNoAudioOmi": "Omi'n duyulmuyor. Açık ve yakında olduğundan emin ol.", + "onboardingActNoAudioPhone": "Henüz seni duyamıyoruz. Biraz daha yüksek sesle konuşmayı dene.", + "onboardingActDictateBodyOmi": "Sadece konuş. Omi'n dinliyor.", + "voiceProfileCardDismissed": "Ses profilini istediğin zaman Ayarlar'dan kurabilirsin.", + "homeNewRecordingButton": "Yeni Kayıt", + "chatWelcomeMessage": "Merhaba, ben Omi. Konuştuğun her şeyi bana sorabilirsin: birinin ne dediğini, ne söz verdiğini ya da gününün özetini. Görev de ekleyebilir ve senin için bir şeyleri hatırlayabilirim.", + "onboardingStarterTaskAsk": "Omi'ye gününü sor", + "onboardingStarterTaskRecord": "Omi ile bir konuşma kaydet", + "onboardingStarterTaskCheck": "Bu görevi tamamlamak için işaretle", "paywallRecommended": "Recommended", "paywallNoPlans": "Plans aren't available right now. Try again in a minute.", "paywallOtherPlans": "See all plans", @@ -4049,21 +4092,6 @@ "paywallManageBilling": "Manage billing", "paywallLoadError": "Couldn't load plans. Check your connection and try again.", "paywallPlans": "Plans", - "firstCaptureTitle": "How will you capture?", - "firstCaptureDescription": "Choose how you want to capture your first conversation.", - "firstCaptureOmi": "I Have an Omi", - "firstCapturePhone": "This Phone", - "firstCaptureWatch": "Apple Watch", - "firstCaptureSkip": "Skip for Now", - "firstCapturePrompt": "Tell Omi what you're working on today, or what's on your mind. Talk for about 30 seconds.", - "firstCaptureKeepGoing": "Keep going… 30 seconds is ideal.", - "firstCaptureReady": "You can finish now, or keep talking.", - "firstCaptureStart": "Start Recording", - "firstCaptureFinish": "Finish Recording", - "firstCaptureListening": "Listening… your words will appear here.", - "firstCaptureProcessing": "Creating your first conversation…", - "firstCaptureResult": "Your first conversation", - "firstCaptureEmpty": "I didn't catch enough speech, try again with a few sentences", "watchCaptureTitle": "Record from your Apple Watch", "watchCaptureOpen": "Open Omi on your Apple Watch.", "watchCaptureRecord": "Tap Record. Allow microphone access if asked.", @@ -4075,17 +4103,6 @@ "pairingNotFoundOrDropped": "Couldn't reach your Omi. Make sure it's charged, close to your phone, and its light is blinking, then try again.", "pairingBondRecovery": "Forget Omi in your phone’s Bluetooth settings, then try pairing again.", "pairingHowTo": "How to pair", - "firstCaptureWatchInstructions": "Open Omi on your Apple Watch and tap Record. Allow microphone access if asked. Your capture will sync to Omi on this phone.", - "firstCaptureWatchInstall": "In the Watch app on your iPhone, install Omi on your Apple Watch. Then open Omi on your watch and tap Record.", - "firstCaptureWatchPair": "Pair your Apple Watch with this iPhone using the Watch app, then install and open Omi on your watch.", - "firstCaptureUnavailable": "Recording could not start. Check microphone access and your connection, then try again.", - "firstCaptureDelayed": "Your recording is taking longer to process. You can check your conversations on Home or try again.", - "firstCaptureEmptyTitle": "Let's try that again", - "firstCaptureShortTitle": "Finish This Recording?", - "firstCaptureShortWarning": "A few more sentences help Omi understand you. About 30 seconds is ideal.", - "firstCaptureCancelTitle": "Leave This Recording?", - "firstCaptureCancelWarning": "Going back stops this recording. Keep talking to give Omi a few more sentences.", - "firstCaptureSiriTip": "Tip: Enable Omi in Shortcuts → Siri.", "paywallOutlineHeadline": "Bir daha tek kelimeyi bile kaçırma.", "paywallCancelAnytime": "İstediğin zaman iptal et.", "connectAnAgent": "Bir Ajan Bağla", diff --git a/app/lib/l10n/app_uk.arb b/app/lib/l10n/app_uk.arb index 104a062fcb7..51dbe2fbc42 100644 --- a/app/lib/l10n/app_uk.arb +++ b/app/lib/l10n/app_uk.arb @@ -520,7 +520,7 @@ "dontShowAgain": "Більше не показувати", "iUnderstand": "Я розумію", "enableBluetooth": "Увімкнути Bluetooth", - "bluetoothNeeded": "Omi потребує Bluetooth для підключення до вашого носимого пристрою. Будь ласка, увімкніть Bluetooth та спробуйте ще раз.", + "bluetoothNeeded": "Omi підключається до телефона через Bluetooth. Увімкніть його, щоб продовжити.", "contactSupport": "Зв'язатися з підтримкою?", "connectLater": "Підключити пізніше", "grantPermissions": "Надати дозволи", @@ -1501,7 +1501,7 @@ "durationSeconds": "Тривалість: {seconds} секунд", "additionalSpeechSampleRemoved": "Додатковий зразок голосу видалено", "consentDataMessage": "Продовжуючи, ваші розмови, записи та особиста інформація будуть надійно зберігатися на наших серверах. Ваші аудіозаписи та транскрипції обробляються сторонніми AI-сервісами (включаючи Deepgram для транскрипції та OpenAI для аналізу), щоб надати вам аналітику на основі ШІ та увімкнути всі функції додатку.", - "tasksEmptyStateMessage": "Почніть розмову, щоб створити завдання.", + "tasksEmptyStateMessage": "Завдання з ваших розмов з'являться тут.", "clearChatAction": "Очистити чат", "enableApps": "Увімкнути додатки", "omiAppName": "Omi", @@ -4004,6 +4004,49 @@ "reviewProposalSuggested": "Запропоновано", "reviewProposalConflict": "Це змінилося після пропозиції Dream, тому нічого не застосовано.", "itemLinkListTitle": "{count, plural, =0{Немає елементів} =1{1 елемент} other{{count} елементів}}", + "onboardingPermissionTapToFix": "Торкніться, щоб виправити", + "onboardingNoOmiYet": "Ще немає Omi?", + "onboardingOmiOrdered": "Я вже замовив", + "onboardingOmiNotOrdered": "Ще не замовив", + "onboardingConnectOmi": "Підключіть Omi", + "consentShortMessage": "Omi за допомогою ШІ перетворює ваші розмови на нотатки. Їх можна видалити будь-коли.", + "consentAiProviders": "Обробка: {providers}.", + "onboardingActIntro": "Спробуймо Omi.", + "onboardingActMicTitle": "Omi потрібен мікрофон", + "onboardingActMicBody": "Щоб Omi чув вас, коли ви з ним говорите.", + "onboardingActMicAllow": "Дозволити мікрофон", + "onboardingActDictateTitle": "Розкажіть Omi про свій день", + "onboardingActDictateBody": "Кажіть по-своєму. Решту зробить Omi.", + "onboardingActDictateExample": "Щойно пообідав із Сем, у червні вона переїжджає до Лісабона! Нагадай надіслати презентацію команді до п'ятниці й купити молоко дорогою додому.", + "onboardingActDictateHint": "Торкніться галочки, коли закінчите", + "onboardingActPhoneMic": "Мікрофон телефона", + "onboardingActWriting": "Підсумовуємо…", + "onboardingActRemembers": "Omi пам'ятає, щоб вам не доводилося", + "onboardingActNotesLabel": "Ваш день коротко", + "onboardingActTasksLabel": "Omi помітив ваші справи", + "onboardingActYourWords": "Ось що ви сказали", + "onboardingActCongratsTitle": "Усе готово, {name}", + "onboardingActRateTitle": "Подобається Omi?", + "onboardingActRateYes": "Оцінити Omi", + "onboardingWelcomeCardTitle": "Вітаємо в Omi", + "onboardingWelcomeCardBody": "Носіть Omi й говоріть як завжди. Кожна розмова стане тут нотатками, завданнями й спогадами.", + "onboardingActDemoTitle": "Обід із Сем", + "onboardingActDemoSummary": "Обід із Сем, яка в червні переїжджає до Лісабона. Треба надіслати презентацію команді до пʼятниці й купити молоко дорогою додому.", + "onboardingActDemoTask1": "Надіслати презентацію команді до п'ятниці", + "onboardingActDemoTask2": "Купити молоко дорогою додому", + "onboardingActJustNow": "Щойно", + "onboardingActSavedToTasks": "Збережено у ваші завдання", + "onboardingConnectTapToConnect": "Торкніться Omi, щоб підключити.", + "onboardingActMicFallbackBody": "Omi ще не передає звук. Поки що скористайтеся телефоном.", + "onboardingActNoAudioOmi": "Omi не чути. Перевірте, що він увімкнений і поруч.", + "onboardingActNoAudioPhone": "Поки вас не чути. Спробуйте говорити трохи голосніше.", + "onboardingActDictateBodyOmi": "Просто говоріть. Omi слухає.", + "voiceProfileCardDismissed": "Голосовий профіль можна налаштувати будь-коли в Налаштуваннях.", + "homeNewRecordingButton": "Новий запис", + "chatWelcomeMessage": "Привіт, я Omi. Питайте про все, про що ви говорили: що хтось сказав, що ви пообіцяли або як минув ваш день. Я також можу додавати завдання й запам'ятовувати важливе за вас.", + "onboardingStarterTaskAsk": "Запитайте Omi про свій день", + "onboardingStarterTaskRecord": "Запишіть розмову з Omi", + "onboardingStarterTaskCheck": "Позначте це завдання, щоб завершити його", "paywallRecommended": "Recommended", "paywallNoPlans": "Plans aren't available right now. Try again in a minute.", "paywallOtherPlans": "See all plans", @@ -4014,21 +4057,6 @@ "paywallManageBilling": "Manage billing", "paywallLoadError": "Couldn't load plans. Check your connection and try again.", "paywallPlans": "Plans", - "firstCaptureTitle": "How will you capture?", - "firstCaptureDescription": "Choose how you want to capture your first conversation.", - "firstCaptureOmi": "I Have an Omi", - "firstCapturePhone": "This Phone", - "firstCaptureWatch": "Apple Watch", - "firstCaptureSkip": "Skip for Now", - "firstCapturePrompt": "Tell Omi what you're working on today, or what's on your mind. Talk for about 30 seconds.", - "firstCaptureKeepGoing": "Keep going… 30 seconds is ideal.", - "firstCaptureReady": "You can finish now, or keep talking.", - "firstCaptureStart": "Start Recording", - "firstCaptureFinish": "Finish Recording", - "firstCaptureListening": "Listening… your words will appear here.", - "firstCaptureProcessing": "Creating your first conversation…", - "firstCaptureResult": "Your first conversation", - "firstCaptureEmpty": "I didn't catch enough speech, try again with a few sentences", "watchCaptureTitle": "Record from your Apple Watch", "watchCaptureOpen": "Open Omi on your Apple Watch.", "watchCaptureRecord": "Tap Record. Allow microphone access if asked.", @@ -4040,17 +4068,6 @@ "pairingNotFoundOrDropped": "Couldn't reach your Omi. Make sure it's charged, close to your phone, and its light is blinking, then try again.", "pairingBondRecovery": "Forget Omi in your phone’s Bluetooth settings, then try pairing again.", "pairingHowTo": "How to pair", - "firstCaptureWatchInstructions": "Open Omi on your Apple Watch and tap Record. Allow microphone access if asked. Your capture will sync to Omi on this phone.", - "firstCaptureWatchInstall": "In the Watch app on your iPhone, install Omi on your Apple Watch. Then open Omi on your watch and tap Record.", - "firstCaptureWatchPair": "Pair your Apple Watch with this iPhone using the Watch app, then install and open Omi on your watch.", - "firstCaptureUnavailable": "Recording could not start. Check microphone access and your connection, then try again.", - "firstCaptureDelayed": "Your recording is taking longer to process. You can check your conversations on Home or try again.", - "firstCaptureEmptyTitle": "Let's try that again", - "firstCaptureShortTitle": "Finish This Recording?", - "firstCaptureShortWarning": "A few more sentences help Omi understand you. About 30 seconds is ideal.", - "firstCaptureCancelTitle": "Leave This Recording?", - "firstCaptureCancelWarning": "Going back stops this recording. Keep talking to give Omi a few more sentences.", - "firstCaptureSiriTip": "Tip: Enable Omi in Shortcuts → Siri.", "paywallOutlineHeadline": "Більше не пропускайте жодного слова.", "paywallCancelAnytime": "Скасовуйте будь-коли.", "connectAnAgent": "Підключити агента", diff --git a/app/lib/l10n/app_ur.arb b/app/lib/l10n/app_ur.arb index 2c03b5ca778..d8dbc50d5fe 100644 --- a/app/lib/l10n/app_ur.arb +++ b/app/lib/l10n/app_ur.arb @@ -1586,7 +1586,7 @@ "@enableBluetooth": { "description": "Title for dialog prompting user to enable Bluetooth" }, - "bluetoothNeeded": "Omi کو اپنے قابل پہننے کے آلے سے جڑنے کے لیے Bluetooth کی ضرورت ہے۔ براہ کرم Bluetooth فعال کریں اور دوبارہ کوشش کریں۔", + "bluetoothNeeded": "Omi آپ کے فون سے Bluetooth کے ذریعے جڑتا ہے۔ جاری رکھنے کے لیے اسے آن کریں۔", "@bluetoothNeeded": { "description": "Explanation text for why Bluetooth is required" }, @@ -5782,7 +5782,7 @@ "@consentDataMessage": { "description": "Consent message explaining how user data will be stored and used" }, - "tasksEmptyStateMessage": "ٹاسک بنانے کے لیے گفتگو شروع کریں۔", + "tasksEmptyStateMessage": "آپ کی گفتگو سے کام یہاں ظاہر ہوں گے۔", "@tasksEmptyStateMessage": { "description": "Empty state message shown when there are no tasks, with instruction to tap + button" }, @@ -11430,6 +11430,49 @@ "reviewProposalSuggested": "تجویز کردہ", "reviewProposalConflict": "Dream کی تجویز کے بعد یہ بدل گیا، اس لیے کچھ بھی لاگو نہیں کیا گیا۔", "itemLinkListTitle": "{count, plural, =0{کوئی آئٹم نہیں} =1{1 آئٹم} other{{count} آئٹمز}}", + "onboardingPermissionTapToFix": "ٹھیک کرنے کے لیے ٹیپ کریں", + "onboardingNoOmiYet": "ابھی تک Omi نہیں ہے؟", + "onboardingOmiOrdered": "میں نے آرڈر کر دیا ہے", + "onboardingOmiNotOrdered": "ابھی آرڈر نہیں کیا", + "onboardingConnectOmi": "Omi کو جوڑیں", + "consentShortMessage": "Omi AI کی مدد سے آپ کی گفتگو کو نوٹس میں بدلتا ہے۔ آپ انہیں کسی بھی وقت حذف کر سکتے ہیں۔", + "consentAiProviders": "{providers} کے ذریعے پروسیس کیا جاتا ہے۔", + "onboardingActIntro": "آئیے Omi آزمائیں۔", + "onboardingActMicTitle": "Omi کو آپ کا مائیکروفون چاہیے", + "onboardingActMicBody": "تاکہ جب آپ بات کریں تو Omi سن سکے۔", + "onboardingActMicAllow": "مائیکروفون کی اجازت دیں", + "onboardingActDictateTitle": "Omi کو اپنے دن کے بارے میں بتائیں", + "onboardingActDictateBody": "اپنے انداز میں کہیں۔ باقی Omi سنبھال لے گا۔", + "onboardingActDictateExample": "ابھی سیم کے ساتھ لنچ کیا، وہ جون میں لزبن جا رہی ہے! مجھے یاد دلانا کہ جمعہ تک ٹیم کو ڈیک بھیجنا ہے، اور گھر جاتے ہوئے دودھ لینا ہے۔", + "onboardingActDictateHint": "ختم ہونے پر ٹک پر ٹیپ کریں", + "onboardingActPhoneMic": "فون کا مائیکروفون", + "onboardingActWriting": "خلاصہ بن رہا ہے…", + "onboardingActRemembers": "Omi یاد رکھتا ہے تاکہ آپ کو نہ رکھنا پڑے", + "onboardingActNotesLabel": "آپ کا دن، مختصراً", + "onboardingActTasksLabel": "Omi نے آپ کے کام پکڑ لیے", + "onboardingActYourWords": "آپ نے یہ کہا", + "onboardingActCongratsTitle": "سب تیار ہے، {name}", + "onboardingActRateTitle": "اب تک Omi پسند آیا؟", + "onboardingActRateYes": "Omi کو ریٹ کریں", + "onboardingWelcomeCardTitle": "Omi میں خوش آمدید", + "onboardingWelcomeCardBody": "Omi پہنیں اور معمول کے مطابق بات کریں۔ ہر گفتگو یہیں نوٹس، کاموں اور یادوں میں بدل جاتی ہے۔", + "onboardingActDemoTitle": "سیم کے ساتھ لنچ", + "onboardingActDemoSummary": "سام کے ساتھ لنچ، جو جون میں لزبن جا رہی ہے۔ آپ کو جمعہ تک ٹیم کو ڈیک بھیجنا ہے اور گھر جاتے ہوئے دودھ لینا ہے۔", + "onboardingActDemoTask1": "جمعہ تک ٹیم کو ڈیک بھیجنا", + "onboardingActDemoTask2": "گھر جاتے ہوئے دودھ لینا", + "onboardingActJustNow": "ابھی ابھی", + "onboardingActSavedToTasks": "آپ کے کاموں میں محفوظ", + "onboardingConnectTapToConnect": "کنیکٹ کرنے کے لیے اپنے Omi پر ٹیپ کریں۔", + "onboardingActMicFallbackBody": "آپ کا Omi ابھی آڈیو نہیں بھیج رہا۔ فی الحال اپنا فون استعمال کریں۔", + "onboardingActNoAudioOmi": "آپ کا Omi سنائی نہیں دے رہا۔ دیکھیں کہ وہ آن اور قریب ہو۔", + "onboardingActNoAudioPhone": "ابھی آپ کی آواز نہیں آ رہی۔ تھوڑا اونچا بول کر دیکھیں۔", + "onboardingActDictateBodyOmi": "بس بولیں۔ آپ کا Omi سن رہا ہے۔", + "voiceProfileCardDismissed": "آپ کسی بھی وقت سیٹنگز میں وائس پروفائل سیٹ کر سکتے ہیں۔", + "homeNewRecordingButton": "نئی ریکارڈنگ", + "chatWelcomeMessage": "ہیلو، میں Omi ہوں۔ آپ نے جس بارے میں بات کی، اس کے بارے میں کچھ بھی پوچھیں: کسی نے کیا کہا، آپ نے کیا وعدہ کیا یا آپ کے دن کا خلاصہ۔ میں کام بھی شامل کر سکتا ہوں اور آپ کے لیے باتیں یاد بھی رکھ سکتا ہوں۔", + "onboardingStarterTaskAsk": "Omi سے اپنے دن کے بارے میں پوچھیں", + "onboardingStarterTaskRecord": "Omi کے ساتھ ایک گفتگو ریکارڈ کریں", + "onboardingStarterTaskCheck": "مکمل کرنے کے لیے اس کام پر نشان لگائیں", "paywallRecommended": "Recommended", "paywallNoPlans": "Plans aren't available right now. Try again in a minute.", "paywallOtherPlans": "See all plans", @@ -11440,21 +11483,6 @@ "paywallManageBilling": "Manage billing", "paywallLoadError": "Couldn't load plans. Check your connection and try again.", "paywallPlans": "Plans", - "firstCaptureTitle": "How will you capture?", - "firstCaptureDescription": "Choose how you want to capture your first conversation.", - "firstCaptureOmi": "I Have an Omi", - "firstCapturePhone": "This Phone", - "firstCaptureWatch": "Apple Watch", - "firstCaptureSkip": "Skip for Now", - "firstCapturePrompt": "Tell Omi what you're working on today, or what's on your mind. Talk for about 30 seconds.", - "firstCaptureKeepGoing": "Keep going… 30 seconds is ideal.", - "firstCaptureReady": "You can finish now, or keep talking.", - "firstCaptureStart": "Start Recording", - "firstCaptureFinish": "Finish Recording", - "firstCaptureListening": "Listening… your words will appear here.", - "firstCaptureProcessing": "Creating your first conversation…", - "firstCaptureResult": "Your first conversation", - "firstCaptureEmpty": "I didn't catch enough speech, try again with a few sentences", "watchCaptureTitle": "Record from your Apple Watch", "watchCaptureOpen": "Open Omi on your Apple Watch.", "watchCaptureRecord": "Tap Record. Allow microphone access if asked.", @@ -11466,17 +11494,6 @@ "pairingNotFoundOrDropped": "Couldn't reach your Omi. Make sure it's charged, close to your phone, and its light is blinking, then try again.", "pairingBondRecovery": "Forget Omi in your phone’s Bluetooth settings, then try pairing again.", "pairingHowTo": "How to pair", - "firstCaptureWatchInstructions": "Open Omi on your Apple Watch and tap Record. Allow microphone access if asked. Your capture will sync to Omi on this phone.", - "firstCaptureWatchInstall": "In the Watch app on your iPhone, install Omi on your Apple Watch. Then open Omi on your watch and tap Record.", - "firstCaptureWatchPair": "Pair your Apple Watch with this iPhone using the Watch app, then install and open Omi on your watch.", - "firstCaptureUnavailable": "Recording could not start. Check microphone access and your connection, then try again.", - "firstCaptureDelayed": "Your recording is taking longer to process. You can check your conversations on Home or try again.", - "firstCaptureEmptyTitle": "Let's try that again", - "firstCaptureShortTitle": "Finish This Recording?", - "firstCaptureShortWarning": "A few more sentences help Omi understand you. About 30 seconds is ideal.", - "firstCaptureCancelTitle": "Leave This Recording?", - "firstCaptureCancelWarning": "Going back stops this recording. Keep talking to give Omi a few more sentences.", - "firstCaptureSiriTip": "Tip: Enable Omi in Shortcuts → Siri.", "paywallOutlineHeadline": "اب ایک لفظ بھی نہ چھوڑیں۔", "paywallCancelAnytime": "کسی بھی وقت منسوخ کریں۔", "connectAnAgent": "ایجنٹ کو جوڑیں", diff --git a/app/lib/l10n/app_vi.arb b/app/lib/l10n/app_vi.arb index 5874012427d..8bd1a4b225b 100644 --- a/app/lib/l10n/app_vi.arb +++ b/app/lib/l10n/app_vi.arb @@ -520,7 +520,7 @@ "dontShowAgain": "Không hiển thị lại", "iUnderstand": "Tôi hiểu", "enableBluetooth": "Bật Bluetooth", - "bluetoothNeeded": "Omi cần Bluetooth để kết nối với thiết bị đeo của bạn. Vui lòng bật Bluetooth và thử lại.", + "bluetoothNeeded": "Omi kết nối với điện thoại qua Bluetooth. Hãy bật để tiếp tục.", "contactSupport": "Liên hệ hỗ trợ?", "connectLater": "Kết nối sau", "grantPermissions": "Cấp quyền", @@ -1501,7 +1501,7 @@ "durationSeconds": "Thời lượng: {seconds} giây", "additionalSpeechSampleRemoved": "Đã xóa mẫu giọng nói bổ sung", "consentDataMessage": "Bằng cách tiếp tục, các cuộc trò chuyện, bản ghi âm và thông tin cá nhân của bạn sẽ được lưu trữ an toàn trên máy chủ của chúng tôi. Bản ghi âm và bản phiên âm của bạn được xử lý bởi các dịch vụ AI bên thứ ba (bao gồm Deepgram cho phiên âm và OpenAI cho phân tích) để cung cấp cho bạn thông tin chi tiết được hỗ trợ bởi AI và kích hoạt tất cả các tính năng ứng dụng.", - "tasksEmptyStateMessage": "Bắt đầu cuộc trò chuyện để tạo nhiệm vụ.", + "tasksEmptyStateMessage": "Việc cần làm từ các cuộc trò chuyện của bạn sẽ hiện ở đây.", "clearChatAction": "Xóa cuộc trò chuyện", "enableApps": "Kích hoạt ứng dụng", "omiAppName": "Omi", @@ -4004,6 +4004,49 @@ "reviewProposalSuggested": "Đề xuất", "reviewProposalConflict": "Mục này đã thay đổi sau khi Dream đề xuất nên không có gì được áp dụng.", "itemLinkListTitle": "{count, plural, =0{Không có mục nào} =1{1 mục} other{{count} mục}}", + "onboardingPermissionTapToFix": "Nhấn để khắc phục", + "onboardingNoOmiYet": "Bạn chưa có Omi?", + "onboardingOmiOrdered": "Tôi đã đặt hàng", + "onboardingOmiNotOrdered": "Chưa đặt hàng", + "onboardingConnectOmi": "Kết nối Omi", + "consentShortMessage": "Omi dùng AI để biến các cuộc trò chuyện của bạn thành ghi chú. Bạn có thể xóa chúng bất cứ lúc nào.", + "consentAiProviders": "Được xử lý bằng {providers}.", + "onboardingActIntro": "Cùng thử Omi nhé.", + "onboardingActMicTitle": "Omi cần micrô của bạn", + "onboardingActMicBody": "Để Omi nghe được khi bạn nói.", + "onboardingActMicAllow": "Cho phép micrô", + "onboardingActDictateTitle": "Kể cho Omi nghe về ngày của bạn", + "onboardingActDictateBody": "Cứ nói theo cách của bạn. Omi lo phần còn lại.", + "onboardingActDictateExample": "Vừa ăn trưa với Sam, tháng Sáu cô ấy chuyển đến Lisbon! Nhắc mình gửi slide cho nhóm trước thứ Sáu, và mua sữa trên đường về nhà.", + "onboardingActDictateHint": "Nhấn dấu tích khi bạn nói xong", + "onboardingActPhoneMic": "Micrô điện thoại", + "onboardingActWriting": "Đang tóm tắt…", + "onboardingActRemembers": "Omi ghi nhớ để bạn không phải nhớ", + "onboardingActNotesLabel": "Ngày của bạn, tóm tắt", + "onboardingActTasksLabel": "Omi đã nắm được việc cần làm", + "onboardingActYourWords": "Đây là những gì bạn nói", + "onboardingActCongratsTitle": "Mọi thứ đã sẵn sàng, {name}", + "onboardingActRateTitle": "Bạn thích Omi chứ?", + "onboardingActRateYes": "Đánh giá Omi", + "onboardingWelcomeCardTitle": "Chào mừng đến với Omi", + "onboardingWelcomeCardBody": "Đeo Omi và nói chuyện như bình thường. Mỗi cuộc trò chuyện sẽ thành ghi chú, việc cần làm và ký ức ngay tại đây.", + "onboardingActDemoTitle": "Ăn trưa với Sam", + "onboardingActDemoSummary": "Ăn trưa với Sam, người sẽ chuyển đến Lisbon vào tháng Sáu. Bạn muốn gửi bản trình bày cho nhóm trước thứ Sáu và mua sữa trên đường về.", + "onboardingActDemoTask1": "Gửi slide cho nhóm trước thứ Sáu", + "onboardingActDemoTask2": "Mua sữa trên đường về nhà", + "onboardingActJustNow": "Vừa xong", + "onboardingActSavedToTasks": "Đã lưu vào việc cần làm", + "onboardingConnectTapToConnect": "Chạm vào Omi của bạn để kết nối.", + "onboardingActMicFallbackBody": "Omi chưa gửi âm thanh. Tạm thời hãy dùng điện thoại.", + "onboardingActNoAudioOmi": "Không nghe thấy Omi. Hãy kiểm tra xem nó đã bật và ở gần chưa.", + "onboardingActNoAudioPhone": "Chưa nghe thấy bạn. Hãy thử nói to hơn một chút.", + "onboardingActDictateBodyOmi": "Cứ nói thôi. Omi đang lắng nghe.", + "voiceProfileCardDismissed": "Bạn có thể thiết lập Hồ sơ giọng nói bất cứ lúc nào trong Cài đặt.", + "homeNewRecordingButton": "Ghi âm mới", + "chatWelcomeMessage": "Chào bạn, mình là Omi. Hỏi mình bất cứ điều gì bạn đã nói: ai đã nói gì, bạn đã hứa gì hay tóm tắt ngày của bạn. Mình cũng có thể thêm việc cần làm và ghi nhớ mọi thứ giúp bạn.", + "onboardingStarterTaskAsk": "Hỏi Omi về ngày của bạn", + "onboardingStarterTaskRecord": "Ghi âm một cuộc trò chuyện với Omi", + "onboardingStarterTaskCheck": "Đánh dấu việc này để hoàn thành", "paywallRecommended": "Recommended", "paywallNoPlans": "Plans aren't available right now. Try again in a minute.", "paywallOtherPlans": "See all plans", @@ -4014,21 +4057,6 @@ "paywallManageBilling": "Manage billing", "paywallLoadError": "Couldn't load plans. Check your connection and try again.", "paywallPlans": "Plans", - "firstCaptureTitle": "How will you capture?", - "firstCaptureDescription": "Choose how you want to capture your first conversation.", - "firstCaptureOmi": "I Have an Omi", - "firstCapturePhone": "This Phone", - "firstCaptureWatch": "Apple Watch", - "firstCaptureSkip": "Skip for Now", - "firstCapturePrompt": "Tell Omi what you're working on today, or what's on your mind. Talk for about 30 seconds.", - "firstCaptureKeepGoing": "Keep going… 30 seconds is ideal.", - "firstCaptureReady": "You can finish now, or keep talking.", - "firstCaptureStart": "Start Recording", - "firstCaptureFinish": "Finish Recording", - "firstCaptureListening": "Listening… your words will appear here.", - "firstCaptureProcessing": "Creating your first conversation…", - "firstCaptureResult": "Your first conversation", - "firstCaptureEmpty": "I didn't catch enough speech, try again with a few sentences", "watchCaptureTitle": "Record from your Apple Watch", "watchCaptureOpen": "Open Omi on your Apple Watch.", "watchCaptureRecord": "Tap Record. Allow microphone access if asked.", @@ -4040,17 +4068,6 @@ "pairingNotFoundOrDropped": "Couldn't reach your Omi. Make sure it's charged, close to your phone, and its light is blinking, then try again.", "pairingBondRecovery": "Forget Omi in your phone’s Bluetooth settings, then try pairing again.", "pairingHowTo": "How to pair", - "firstCaptureWatchInstructions": "Open Omi on your Apple Watch and tap Record. Allow microphone access if asked. Your capture will sync to Omi on this phone.", - "firstCaptureWatchInstall": "In the Watch app on your iPhone, install Omi on your Apple Watch. Then open Omi on your watch and tap Record.", - "firstCaptureWatchPair": "Pair your Apple Watch with this iPhone using the Watch app, then install and open Omi on your watch.", - "firstCaptureUnavailable": "Recording could not start. Check microphone access and your connection, then try again.", - "firstCaptureDelayed": "Your recording is taking longer to process. You can check your conversations on Home or try again.", - "firstCaptureEmptyTitle": "Let's try that again", - "firstCaptureShortTitle": "Finish This Recording?", - "firstCaptureShortWarning": "A few more sentences help Omi understand you. About 30 seconds is ideal.", - "firstCaptureCancelTitle": "Leave This Recording?", - "firstCaptureCancelWarning": "Going back stops this recording. Keep talking to give Omi a few more sentences.", - "firstCaptureSiriTip": "Tip: Enable Omi in Shortcuts → Siri.", "paywallOutlineHeadline": "Không bao giờ bỏ lỡ một lời nào nữa.", "paywallCancelAnytime": "Hủy bất cứ lúc nào.", "connectAnAgent": "Kết nối tác nhân", diff --git a/app/lib/l10n/app_zh.arb b/app/lib/l10n/app_zh.arb index 9f0b8b55626..1804846af6c 100644 --- a/app/lib/l10n/app_zh.arb +++ b/app/lib/l10n/app_zh.arb @@ -515,7 +515,7 @@ "dontShowAgain": "不再显示", "iUnderstand": "我理解", "enableBluetooth": "启用蓝牙", - "bluetoothNeeded": "Omi 需要蓝牙来连接您的穿戴设备。", + "bluetoothNeeded": "Omi 通过蓝牙连接你的手机。打开蓝牙以继续。", "contactSupport": "联系支持?", "connectLater": "稍后连接", "grantPermissions": "授予权限", @@ -1504,7 +1504,7 @@ "durationSeconds": "时长: {seconds} 秒", "additionalSpeechSampleRemoved": "已删除附加语音样本", "consentDataMessage": "继续即表示您的对话、录音和个人信息将安全存储在我们的服务器上。您的音频录音和转录由第三方AI服务处理(包括用于转录的Deepgram和用于分析的OpenAI),以为您提供AI驱动的洞察并启用所有应用功能。", - "tasksEmptyStateMessage": "开始对话以创建任务。", + "tasksEmptyStateMessage": "对话中的待办事项会显示在这里。", "clearChatAction": "清除聊天", "enableApps": "启用应用", "omiAppName": "Omi", @@ -4004,6 +4004,49 @@ "reviewProposalSuggested": "建议", "reviewProposalConflict": "Dream 提出建议后内容已有变化,因此未应用任何更改。", "itemLinkListTitle": "{count, plural, =0{无项目} =1{1 个项目} other{{count} 个项目}}", + "onboardingPermissionTapToFix": "点按以修复", + "onboardingNoOmiYet": "还没有 Omi?", + "onboardingOmiOrdered": "我已下单", + "onboardingOmiNotOrdered": "还没下单", + "onboardingConnectOmi": "连接 Omi", + "consentShortMessage": "Omi 用 AI 把你的对话整理成笔记。你可以随时删除。", + "consentAiProviders": "由 {providers} 处理。", + "onboardingActIntro": "来试试 Omi 吧。", + "onboardingActMicTitle": "Omi 需要使用麦克风", + "onboardingActMicBody": "这样你说话时 Omi 才能听到。", + "onboardingActMicAllow": "允许使用麦克风", + "onboardingActDictateTitle": "跟 Omi 说说你的一天", + "onboardingActDictateBody": "随便说,剩下的交给 Omi。", + "onboardingActDictateExample": "刚和 Sam 吃完午饭,她六月要搬去里斯本!提醒我周五前把演示文稿发给团队,回家路上顺便买牛奶。", + "onboardingActDictateHint": "说完后点对勾", + "onboardingActPhoneMic": "手机麦克风", + "onboardingActWriting": "正在总结…", + "onboardingActRemembers": "Omi 帮你记住,你无需费心", + "onboardingActNotesLabel": "你的一天,总结好了", + "onboardingActTasksLabel": "Omi 记下了你的待办", + "onboardingActYourWords": "你说的内容", + "onboardingActCongratsTitle": "一切就绪,{name}", + "onboardingActRateTitle": "目前喜欢 Omi 吗?", + "onboardingActRateYes": "给 Omi 评分", + "onboardingWelcomeCardTitle": "欢迎使用 Omi", + "onboardingWelcomeCardBody": "戴上 Omi,照常说话。每段对话都会在这里变成笔记、待办和记忆。", + "onboardingActDemoTitle": "和 Sam 吃午饭", + "onboardingActDemoSummary": "和 Sam 吃了午饭,她六月要搬去里斯本。你打算周五前把演示稿发给团队,回家路上买牛奶。", + "onboardingActDemoTask1": "周五前把演示文稿发给团队", + "onboardingActDemoTask2": "回家路上买牛奶", + "onboardingActJustNow": "刚刚", + "onboardingActSavedToTasks": "已保存到你的任务", + "onboardingConnectTapToConnect": "轻点你的 Omi 进行连接。", + "onboardingActMicFallbackBody": "你的 Omi 还没有传来声音,先用手机吧。", + "onboardingActNoAudioOmi": "听不到你的 Omi。请确认它已开启并在附近。", + "onboardingActNoAudioPhone": "还听不到你的声音,试着大声一点。", + "onboardingActDictateBodyOmi": "直接说就好,你的 Omi 正在听。", + "voiceProfileCardDismissed": "你可以随时在设置中设置声音档案。", + "homeNewRecordingButton": "新录音", + "chatWelcomeMessage": "你好,我是 Omi。关于你聊过的任何事都可以问我:某人说了什么、你答应过什么,或者今天的总结。我还能帮你添加待办、记住重要的事。", + "onboardingStarterTaskAsk": "问问 Omi 你今天过得怎么样", + "onboardingStarterTaskRecord": "用 Omi 录一段对话", + "onboardingStarterTaskCheck": "勾选这项任务来完成它", "paywallRecommended": "Recommended", "paywallNoPlans": "Plans aren't available right now. Try again in a minute.", "paywallOtherPlans": "See all plans", @@ -4014,21 +4057,6 @@ "paywallManageBilling": "Manage billing", "paywallLoadError": "Couldn't load plans. Check your connection and try again.", "paywallPlans": "Plans", - "firstCaptureTitle": "How will you capture?", - "firstCaptureDescription": "Choose how you want to capture your first conversation.", - "firstCaptureOmi": "I Have an Omi", - "firstCapturePhone": "This Phone", - "firstCaptureWatch": "Apple Watch", - "firstCaptureSkip": "Skip for Now", - "firstCapturePrompt": "Tell Omi what you're working on today, or what's on your mind. Talk for about 30 seconds.", - "firstCaptureKeepGoing": "Keep going… 30 seconds is ideal.", - "firstCaptureReady": "You can finish now, or keep talking.", - "firstCaptureStart": "Start Recording", - "firstCaptureFinish": "Finish Recording", - "firstCaptureListening": "Listening… your words will appear here.", - "firstCaptureProcessing": "Creating your first conversation…", - "firstCaptureResult": "Your first conversation", - "firstCaptureEmpty": "I didn't catch enough speech, try again with a few sentences", "watchCaptureTitle": "Record from your Apple Watch", "watchCaptureOpen": "Open Omi on your Apple Watch.", "watchCaptureRecord": "Tap Record. Allow microphone access if asked.", @@ -4040,17 +4068,6 @@ "pairingNotFoundOrDropped": "Couldn't reach your Omi. Make sure it's charged, close to your phone, and its light is blinking, then try again.", "pairingBondRecovery": "Forget Omi in your phone’s Bluetooth settings, then try pairing again.", "pairingHowTo": "How to pair", - "firstCaptureWatchInstructions": "Open Omi on your Apple Watch and tap Record. Allow microphone access if asked. Your capture will sync to Omi on this phone.", - "firstCaptureWatchInstall": "In the Watch app on your iPhone, install Omi on your Apple Watch. Then open Omi on your watch and tap Record.", - "firstCaptureWatchPair": "Pair your Apple Watch with this iPhone using the Watch app, then install and open Omi on your watch.", - "firstCaptureUnavailable": "Recording could not start. Check microphone access and your connection, then try again.", - "firstCaptureDelayed": "Your recording is taking longer to process. You can check your conversations on Home or try again.", - "firstCaptureEmptyTitle": "Let's try that again", - "firstCaptureShortTitle": "Finish This Recording?", - "firstCaptureShortWarning": "A few more sentences help Omi understand you. About 30 seconds is ideal.", - "firstCaptureCancelTitle": "Leave This Recording?", - "firstCaptureCancelWarning": "Going back stops this recording. Keep talking to give Omi a few more sentences.", - "firstCaptureSiriTip": "Tip: Enable Omi in Shortcuts → Siri.", "paywallOutlineHeadline": "再也不错过任何一句话。", "paywallCancelAnytime": "随时取消。", "connectAnAgent": "连接智能体", diff --git a/app/lib/main.dart b/app/lib/main.dart index 649e890a906..63b5323be30 100644 --- a/app/lib/main.dart +++ b/app/lib/main.dart @@ -21,6 +21,7 @@ import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:omi/gen/pigeon_communicator.g.dart'; import 'package:omi/services/bridges/ble_bridge.dart'; import 'package:omi/services/app_review_service.dart'; +import 'package:omi/services/superwall_pilot.dart'; import 'package:omi/services/account_cutover/account_cutover_runtime.dart'; import 'package:omi/widgets/bluetooth_guidance_listener.dart'; import 'package:flutter_foreground_task/flutter_foreground_task.dart'; @@ -289,6 +290,7 @@ Future _init() async { } await PhysicalQualification.startupStage('platform_services', PlatformManager.initializeServices); + SuperwallPilot.instance.configure(); await PhysicalQualification.startupStage('notification_locale', NotificationChannelStrings.loadAppLocale); await PhysicalQualification.startupStage('notification_service', NotificationService.instance.initialize); @@ -342,6 +344,7 @@ Future _init() async { if (isAuth) { OnboardingSyncRuntime.wake(); final firebaseUser = FirebaseAuth.instance.currentUser; + await SuperwallPilot.instance.syncUser(firebaseUser != null && !firebaseUser.isAnonymous ? firebaseUser.uid : null); PlatformManager.instance.analytics.identify( authMethod: firebaseUser == null || firebaseUser.providerData.isEmpty ? null : firebaseUser.providerData.first.providerId, diff --git a/app/lib/mobile/mobile_app.dart b/app/lib/mobile/mobile_app.dart index 33e210dfdd9..370b2efce2c 100644 --- a/app/lib/mobile/mobile_app.dart +++ b/app/lib/mobile/mobile_app.dart @@ -4,7 +4,6 @@ import 'package:provider/provider.dart'; import 'package:omi/backend/preferences.dart'; import 'package:omi/pages/home/page.dart'; -import 'package:omi/pages/onboarding/device_selection.dart'; import 'package:omi/pages/onboarding/permissions/permissions_checker.dart'; import 'package:omi/pages/onboarding/wrapper.dart'; import 'package:omi/providers/auth_provider.dart'; @@ -72,7 +71,8 @@ class _MobileAppState extends State { }, ); } else { - return const DeviceSelectionPage(); + // Signed out: straight to sign-in, the first onboarding step. + return const OnboardingWrapper(); } }, ); diff --git a/app/lib/pages/action_items/action_items_page.dart b/app/lib/pages/action_items/action_items_page.dart index 38d038090df..ea052c25d01 100644 --- a/app/lib/pages/action_items/action_items_page.dart +++ b/app/lib/pages/action_items/action_items_page.dart @@ -2,7 +2,6 @@ import 'package:omi/utils/platform/platform_manager.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -import 'package:pull_down_button/pull_down_button.dart'; import 'package:omi/backend/http/action_items_api_contract.dart'; import 'package:omi/backend/http/api_presentation.dart'; @@ -59,8 +58,13 @@ class _ActionItemsPageState extends State with AutomaticKeepAli bool _noDeadlineExpanded = true; - /// Group the list by project instead of by due date (offered once projects exist). - bool _groupByProject = false; + /// Tasks just ticked (or reopened) that are about to leave this view: shown in their new state + /// at once ([_completing] maps id → new completed value), then folded away ([_folding]) before the + /// provider moves them, so the change reads as an action instead of a row vanishing. + final Map _completing = {}; + final Set _folding = {}; + static const _showDone = Duration(milliseconds: 450); + static const _fold = Duration(milliseconds: 320); // Search header lifecycle objects. final TextEditingController _searchController = TextEditingController(); @@ -199,7 +203,7 @@ class _ActionItemsPageState extends State with AutomaticKeepAli Widget _buildPageHeader(ActionItemsProvider provider) { return Padding( - padding: const EdgeInsets.fromLTRB(16, 8, 12, 4), + padding: const EdgeInsets.fromLTRB(16, 8, 16, 4), child: Row( children: [ Expanded( @@ -219,76 +223,11 @@ class _ActionItemsPageState extends State with AutomaticKeepAli }, ), ), - const SizedBox(width: 4), - _buildOverflowMenu(provider), ], ), ); } - Widget _buildOverflowMenu(ActionItemsProvider provider) { - final showingCompleted = provider.showCompletedView; - final hasItems = provider.actionItems.isNotEmpty; - final allSelected = hasItems && provider.selectedCount == provider.actionItems.length; - - return PullDownButton( - itemBuilder: (context) => [ - PullDownMenuItem( - title: context.l10n.selectActionItems, - iconWidget: const Icon(Icons.check_box_outlined, size: 18), - onTap: () { - OmiHaptics.light(); - _searchFocusNode.unfocus(); - provider.startSelection(); - }, - ), - PullDownMenuItem( - title: allSelected ? context.l10n.deselectAllTasksMenu : context.l10n.selectAllTasksMenu, - iconWidget: Icon(allSelected ? Icons.deselect_rounded : Icons.select_all_rounded, size: 18), - onTap: () { - OmiHaptics.light(); - _searchFocusNode.unfocus(); - if (allSelected) { - provider.clearSelection(); - } else { - if (!provider.isSelectionMode) provider.startSelection(); - provider.selectAllItems(); - } - }, - ), - if (context.read()?.isOn ?? false) - PullDownMenuItem( - title: _groupByProject ? context.l10n.tasksGroupByDate : context.l10n.tasksGroupByProject, - iconWidget: Icon(_groupByProject ? Icons.event_outlined : Icons.folder_outlined, size: 18), - onTap: () { - OmiHaptics.light(); - final review = context.read(); - if (!_groupByProject && review != null && review.projects.isEmpty) review.loadProjects(); - setState(() => _groupByProject = !_groupByProject); - }, - ), - PullDownMenuItem( - title: showingCompleted ? context.l10n.hideCompletedTasks : context.l10n.showCompletedTasks, - iconWidget: Icon(showingCompleted ? Icons.visibility_off_outlined : Icons.visibility_outlined, size: 18), - onTap: () { - OmiHaptics.light(); - provider.toggleShowCompletedView(); - }, - ), - ], - buttonBuilder: (context, showMenu) => OmiIconButton.filled( - icon: const Icon(Icons.more_horiz_rounded), - label: context.l10n.moreOptions, - color: OmiColors.textSecondary, - diameter: 40, - onPressed: () { - OmiHaptics.selection(); - showMenu(); - }, - ), - ); - } - Widget _buildNoSearchResultsContent() { return OmiEmptyState(icon: Icons.search_off_rounded, title: context.l10n.noResultsFound); } @@ -467,6 +406,7 @@ class _ActionItemsPageState extends State with AutomaticKeepAli excludeFromSemantics: true, onTap: () {}, child: RefreshIndicator( + notificationPredicate: OmiRefreshControl.materialPredicate, onRefresh: () async { OmiHaptics.medium(); return provider.forceRefreshActionItems(); @@ -478,6 +418,7 @@ class _ActionItemsPageState extends State with AutomaticKeepAli controller: _scrollController, physics: const AlwaysScrollableScrollPhysics(), slivers: [ + if (OmiRefreshControl.supported) OmiRefreshControl(onRefresh: _pullRefresh), SliverFillRemaining( hasScrollBody: false, child: Center(child: ActionItemsApiStatus(provider: provider)), @@ -501,6 +442,9 @@ class _ActionItemsPageState extends State with AutomaticKeepAli ); } + /// The iOS pull-to-refresh ([OmiRefreshControl]); Android keeps the Material indicator. + Future _pullRefresh() => context.read().forceRefreshActionItems(); + Widget _buildLoadingState() { return CustomScrollView( controller: _scrollController, @@ -517,7 +461,10 @@ class _ActionItemsPageState extends State with AutomaticKeepAli return CustomScrollView( controller: _scrollController, physics: const AlwaysScrollableScrollPhysics(), - slivers: [SliverFillRemaining(hasScrollBody: false, child: Center(child: _buildEmptyTasksContent()))], + slivers: [ + if (OmiRefreshControl.supported) OmiRefreshControl(onRefresh: _pullRefresh), + SliverFillRemaining(hasScrollBody: false, child: Center(child: _buildEmptyTasksContent())), + ], ); } @@ -527,7 +474,6 @@ class _ActionItemsPageState extends State with AutomaticKeepAli child: KeyedSubtree( key: const ValueKey('omi.action_items.empty'), child: OmiEmptyState( - icon: Icons.task_alt_rounded, title: context.l10n.noTasksYet, titleLayoutReference: context.l10n.noConversationsYet, message: context.l10n.tasksEmptyStateMessage, @@ -547,6 +493,7 @@ class _ActionItemsPageState extends State with AutomaticKeepAli controller: _scrollController, physics: const AlwaysScrollableScrollPhysics(), slivers: [ + if (OmiRefreshControl.supported) OmiRefreshControl(onRefresh: _pullRefresh), const SliverPadding(padding: EdgeInsets.only(top: 8)), SliverToBoxAdapter(child: _buildPageHeader(provider)), @@ -568,7 +515,7 @@ class _ActionItemsPageState extends State with AutomaticKeepAli ); }, childCount: filteredItems.length), ), - ] else if (_groupByProject) ...[ + ] else if (provider.groupByProject) ...[ const SliverPadding(padding: EdgeInsets.only(top: 6)), ...projectTaskSections( context: context, @@ -1047,45 +994,84 @@ class _ActionItemsPageState extends State with AutomaticKeepAli // One meaning on every row, at every indent level: swipe right completes (or reopens), swipe // left deletes with Undo. Indenting lives in the long-press menu and in drag. - return Dismissible( - key: Key('dismiss_${item.id}'), - direction: DismissDirection.horizontal, - dismissThresholds: const {DismissDirection.startToEnd: 0.3, DismissDirection.endToStart: 0.3}, - confirmDismiss: (direction) async { - if (direction == DismissDirection.startToEnd) { - await _toggleCompleted(provider, item); - return false; - } - return true; - }, - background: Container( - alignment: Alignment.centerLeft, - padding: const EdgeInsets.only(left: 20.0), - decoration: BoxDecoration( - color: item.completed ? OmiColors.surface3 : OmiColors.success, - borderRadius: OmiRadius.smAll, - ), - child: Icon(item.completed ? Icons.undo : Icons.check, color: OmiColors.textPrimary), - ), - secondaryBackground: Container( - alignment: Alignment.centerRight, - padding: const EdgeInsets.only(right: 20.0), - decoration: BoxDecoration(color: OmiColors.danger, borderRadius: OmiRadius.smAll), - child: Icon(Icons.delete_outline, color: OmiColors.textPrimary), - ), - onDismissed: (direction) { - if (direction == DismissDirection.endToStart) { - _deleteTask(item); - } - }, - child: draggable, - ); + return _foldable( + item, + Dismissible( + key: Key('dismiss_${item.id}'), + direction: DismissDirection.horizontal, + dismissThresholds: const {DismissDirection.startToEnd: 0.3, DismissDirection.endToStart: 0.3}, + confirmDismiss: (direction) async { + if (direction == DismissDirection.startToEnd) { + await _toggleCompleted(provider, item); + return false; + } + return true; + }, + background: Container( + alignment: Alignment.centerLeft, + padding: const EdgeInsets.only(left: 20.0), + decoration: BoxDecoration( + color: item.completed ? OmiColors.surface3 : OmiColors.success, + borderRadius: OmiRadius.smAll, + ), + child: Icon(item.completed ? Icons.undo : Icons.check, color: OmiColors.textPrimary), + ), + secondaryBackground: Container( + alignment: Alignment.centerRight, + padding: const EdgeInsets.only(right: 20.0), + decoration: BoxDecoration(color: OmiColors.danger, borderRadius: OmiRadius.smAll), + child: Icon(Icons.delete_outline, color: OmiColors.textPrimary), + ), + onDismissed: (direction) { + if (direction == DismissDirection.endToStart) { + _deleteTask(item); + } + }, + child: draggable, + )); } Future _toggleCompleted(ActionItemsProvider provider, ActionItemWithMetadata item) async { OmiHaptics.light(); - await provider.updateActionItemState(item, !item.completed); - if (!item.completed) _onActionItemCompleted(); + final completing = !item.completed; + // The open view drops completed tasks and the completed view drops reopened ones: either way + // the row is about to leave, so it shows its new state and folds away first. + final leaves = completing != provider.showCompletedView; + final animate = leaves && OmiMotion.of(context).emphasized != Duration.zero; + if (animate) { + if (_completing.containsKey(item.id)) return; + setState(() => _completing[item.id] = completing); + await Future.delayed(_showDone); + if (!mounted) return; + setState(() => _folding.add(item.id)); + await Future.delayed(_fold); + if (!mounted) return; + } + await provider.updateActionItemState(item, completing); + if (mounted && animate) { + setState(() { + _completing.remove(item.id); + _folding.remove(item.id); + }); + } + if (completing) _onActionItemCompleted(); + } + + /// Folds a just-completed row closed (height and opacity) before it leaves the list. + Widget _foldable(ActionItemWithMetadata item, Widget row) { + final folding = _folding.contains(item.id); + return AnimatedSize( + // Per task, so the next row never inherits a folded slot and grows back in. + key: ValueKey('fold_${item.id}'), + duration: _fold, + curve: Curves.easeInOutCubic, + alignment: Alignment.topCenter, + child: AnimatedOpacity( + duration: _fold, + opacity: folding ? 0 : 1, + child: folding ? const SizedBox(width: double.infinity, height: 0) : row, + ), + ); } /// Long-press menu: the same shape as memories and conversations, with Select for multi-select @@ -1147,6 +1133,8 @@ class _ActionItemsPageState extends State with AutomaticKeepAli final indentLevel = _getIndentLevel(item); final goalTitle = _getGoalTitleForTask(item); final isSelected = provider.isSelectionMode && provider.isItemSelected(item.id); + // Ticked and about to fold away: already looks done. + final done = _completing[item.id] ?? item.completed; return GestureDetector( behavior: HitTestBehavior.opaque, @@ -1192,7 +1180,17 @@ class _ActionItemsPageState extends State with AutomaticKeepAli child: SizedBox( width: 44, height: 44, - child: Center(child: TaskCompletionMark(completed: item.completed)), + child: Center( + child: AnimatedSwitcher( + duration: const Duration(milliseconds: 220), + transitionBuilder: (child, a) => ScaleTransition( + scale: Tween(begin: 0.6, end: 1.0) + .animate(CurvedAnimation(parent: a, curve: Curves.easeOutBack)), + child: FadeTransition(opacity: a, child: child), + ), + child: TaskCompletionMark(key: ValueKey(done), completed: done), + ), + ), ), ), ), @@ -1207,10 +1205,10 @@ class _ActionItemsPageState extends State with AutomaticKeepAli Text( item.description, style: OmiType.callout.copyWith( - color: item.completed ? OmiColors.textTertiary : OmiColors.textPrimary, + color: done ? OmiColors.textTertiary : OmiColors.textPrimary, fontWeight: FontWeight.w500, letterSpacing: -0.2, - decoration: item.completed ? TextDecoration.lineThrough : null, + decoration: done ? TextDecoration.lineThrough : null, decorationColor: OmiColors.textTertiary, ), ), diff --git a/app/lib/pages/action_items/widgets/tasks_more_menu.dart b/app/lib/pages/action_items/widgets/tasks_more_menu.dart new file mode 100644 index 00000000000..0d26618ea2d --- /dev/null +++ b/app/lib/pages/action_items/widgets/tasks_more_menu.dart @@ -0,0 +1,97 @@ +import 'package:flutter/material.dart'; + +import 'package:provider/provider.dart'; +import 'package:pull_down_button/pull_down_button.dart'; + +import 'package:omi/pages/settings/task_integrations_page.dart'; +import 'package:omi/providers/action_items_provider.dart'; +import 'package:omi/providers/review_provider.dart'; +import 'package:omi/ui/ui.dart'; +import 'package:omi/utils/l10n_extensions.dart'; +import 'package:omi/utils/other/temp.dart'; +import 'package:omi/utils/platform/platform_manager.dart'; + +/// Tasks' one page action: a bare "⋯" beside the Home / Tasks switcher. It holds selection, +/// grouping, the completed view and export, so the shell header stays the same on both pages. +class TasksMoreMenu extends StatelessWidget { + const TasksMoreMenu({super.key}); + + static const buttonKey = ValueKey('tasks_more_button'); + + @override + Widget build(BuildContext context) { + final provider = context.watch(); + final l10n = context.l10n; + final showingCompleted = provider.showCompletedView; + final groupByProject = provider.groupByProject; + final hasItems = provider.actionItems.isNotEmpty; + final allSelected = hasItems && provider.selectedCount == provider.actionItems.length; + + return PullDownButton( + itemBuilder: (context) => [ + if (hasItems) ...[ + PullDownMenuItem( + title: l10n.selectActionItems, + iconWidget: const Icon(Icons.check_box_outlined, size: 18), + onTap: () { + OmiHaptics.light(); + primaryFocus?.unfocus(); + provider.startSelection(); + }, + ), + PullDownMenuItem( + title: allSelected ? l10n.deselectAllTasksMenu : l10n.selectAllTasksMenu, + iconWidget: Icon(allSelected ? Icons.deselect_rounded : Icons.select_all_rounded, size: 18), + onTap: () { + OmiHaptics.light(); + primaryFocus?.unfocus(); + if (allSelected) { + provider.clearSelection(); + } else { + if (!provider.isSelectionMode) provider.startSelection(); + provider.selectAllItems(); + } + }, + ), + ], + if (context.read()?.isOn ?? false) + PullDownMenuItem( + title: groupByProject ? l10n.tasksGroupByDate : l10n.tasksGroupByProject, + iconWidget: Icon(groupByProject ? Icons.event_outlined : Icons.folder_outlined, size: 18), + onTap: () { + OmiHaptics.light(); + final review = context.read(); + if (!groupByProject && review != null && review.projects.isEmpty) review.loadProjects(); + provider.toggleGroupByProject(); + }, + ), + PullDownMenuItem( + title: showingCompleted ? l10n.hideCompletedTasks : l10n.showCompletedTasks, + iconWidget: Icon(showingCompleted ? Icons.visibility_off_outlined : Icons.visibility_outlined, size: 18), + onTap: () { + OmiHaptics.light(); + provider.toggleShowCompletedView(); + }, + ), + PullDownMenuItem( + title: l10n.exportButton, + iconWidget: const Icon(Icons.ios_share_rounded, size: 18), + onTap: () { + PlatformManager.instance.analytics.exportTasksBannerClicked(); + routeToPage(context, const TaskIntegrationsPage()); + }, + ), + ], + buttonBuilder: (context, showMenu) => OmiIconButton( + key: buttonKey, + icon: const Icon(Icons.more_horiz_rounded), + label: l10n.moreOptions, + color: OmiColors.textSecondary, + onPressed: () { + OmiHaptics.selection(); + showMenu(); + }, + ), + ); + } +} diff --git a/app/lib/pages/apps/widgets/omi_web_page.dart b/app/lib/pages/apps/widgets/omi_web_page.dart index d5d6da38978..d0c1414a5bb 100644 --- a/app/lib/pages/apps/widgets/omi_web_page.dart +++ b/app/lib/pages/apps/widgets/omi_web_page.dart @@ -12,7 +12,7 @@ import 'package:omi/utils/l10n_extensions.dart'; /// skip past it.) /// * First load shows a spinner; a failed main-frame load shows the reason and **Try Again**. class OmiWebPage extends StatefulWidget { - const OmiWebPage({super.key, required this.title, required this.url, this.userAgent}); + const OmiWebPage({super.key, required this.title, required this.url, this.userAgent, this.showClose = false}); final String title; final Uri url; @@ -20,6 +20,10 @@ class OmiWebPage extends StatefulWidget { /// Overrides the web view's user agent (some app home pages sniff it). final String? userAgent; + /// A Close (×) that dismisses the whole page, for a page presented as a sheet (the Omi store). + /// The default back button steps back through the web history first. + final bool showClose; + @override State createState() => _OmiWebPageState(); } @@ -75,7 +79,8 @@ class _OmiWebPageState extends State { @override Widget build(BuildContext context) { return PopScope( - canPop: !_canGoBack || _failed, + // A sheet closes as a whole; a pushed page walks back through its web history first. + canPop: widget.showClose || !_canGoBack || _failed, onPopInvokedWithResult: (didPop, _) async { if (didPop) return; await _controller.goBack(); @@ -84,7 +89,14 @@ class _OmiWebPageState extends State { }, child: Scaffold( appBar: AppBar( - leading: const OmiBackButton(), + leading: widget.showClose + ? OmiIconButton( + key: const Key('omi_web_page_close'), + icon: const Icon(Icons.close), + label: context.l10n.close, + onPressed: () => Navigator.of(context).pop(), + ) + : const OmiBackButton(), title: Text(widget.title, maxLines: 1, overflow: TextOverflow.ellipsis), ), body: Stack( diff --git a/app/lib/pages/capture/connect.dart b/app/lib/pages/capture/connect.dart index 5917a6df62d..d5f5a59f143 100644 --- a/app/lib/pages/capture/connect.dart +++ b/app/lib/pages/capture/connect.dart @@ -25,10 +25,7 @@ Future openOmiStore({Future Function(Uri)? launcher}) async { } class ConnectDevicePage extends StatefulWidget { - const ConnectDevicePage({super.key, this.onConnected}); - - /// Onboarding returns to its completion owner before Home can be entered. - final VoidCallback? onConnected; + const ConnectDevicePage({super.key}); @override State createState() => _ConnectDevicePageState(); @@ -73,11 +70,9 @@ class _ConnectDevicePageState extends State { if (!onboardingProvider.isConnected) ScanningRippleWidget( isScanning: !onboardingProvider.isConnected, - size: (MediaQuery.sizeOf(context).height <= 700 ? 280.0 : 360.0) * - (onboardingProvider.pairingFailureReason == null ? 1 : 0.5), + size: MediaQuery.sizeOf(context).height <= 700 ? 280 : 360, ), DeviceAnimationWidget( - sizeMultiplier: onboardingProvider.pairingFailureReason == null ? 1 : 0.5, isConnected: onboardingProvider.isConnected, deviceName: onboardingProvider.deviceName, deviceType: onboardingProvider.deviceType, @@ -86,13 +81,9 @@ class _ConnectDevicePageState extends State { ], ), FindDevicesPage( - isFromOnboarding: widget.onConnected != null, + isFromOnboarding: false, goNext: () { Logger.debug('onConnected from FindDevicesPage'); - if (widget.onConnected != null) { - widget.onConnected!(); - return; - } // Back to the Home already underneath, not a second Home on top of it. HomeNavigation.returnHome(context); }, @@ -110,18 +101,12 @@ class _ConnectDevicePageState extends State { child: Column( mainAxisSize: MainAxisSize.min, children: [ - OmiButton( - variant: OmiButtonVariant.tertiary, - labelStyle: OmiType.callout.copyWith( - fontWeight: FontWeight.w600, fontFamily: Theme.of(context).textTheme.labelLarge?.fontFamily), + OmiButton.tertiary( key: const Key('get_omi_device_button'), label: context.l10n.getOmiDevice, onPressed: openOmiStore, ), - OmiButton( - variant: OmiButtonVariant.tertiary, - labelStyle: OmiType.callout.copyWith( - fontWeight: FontWeight.w600, fontFamily: Theme.of(context).textTheme.labelLarge?.fontFamily), + OmiButton.tertiary( key: const Key('connection_guide_button'), label: context.l10n.connectionGuide, icon: Icons.info_outline, diff --git a/app/lib/pages/capture/widgets/widgets.dart b/app/lib/pages/capture/widgets/widgets.dart index 7a9828f9076..956d529e8b3 100644 --- a/app/lib/pages/capture/widgets/widgets.dart +++ b/app/lib/pages/capture/widgets/widgets.dart @@ -21,11 +21,29 @@ import 'package:omi/widgets/conversation_photo_image.dart'; import 'package:omi/widgets/photos_grid.dart'; import 'package:omi/widgets/transcript.dart'; -class SpeechProfileCardWidget extends StatelessWidget { +/// "Teach Omi your voice" on Home, for readers who have no voice profile yet (onboarding no longer +/// records one). Closing it hides it for good and points to Settings > Voice Profile. +class SpeechProfileCardWidget extends StatefulWidget { const SpeechProfileCardWidget({super.key}); + @override + State createState() => _SpeechProfileCardWidgetState(); +} + +class _SpeechProfileCardWidgetState extends State { + bool _dismissed = SharedPreferencesUtil().voiceProfileCardDismissed; + + void _dismiss() { + OmiHaptics.light(); + PlatformManager.instance.analytics.track('Voice Profile Card Dismissed'); + SharedPreferencesUtil().voiceProfileCardDismissed = true; + setState(() => _dismissed = true); + OmiFeedback.info(context, context.l10n.voiceProfileCardDismissed); + } + @override Widget build(BuildContext context) { + if (_dismissed) return const SizedBox(); return Consumer( builder: (context, provider, child) { if (provider.isLoading) return const SizedBox(); @@ -43,6 +61,7 @@ class SpeechProfileCardWidget extends StatelessWidget { label: context.l10n.teachOmiYourVoice, // A dot marks a setup step still to do. badge: true, + onDismiss: _dismiss, onTap: () async { PlatformManager.instance.analytics.pageOpened('Speech Profile Memories'); bool hasSpeakerProfile = SharedPreferencesUtil().hasSpeakerProfile; @@ -65,44 +84,63 @@ class SpeechProfileCardWidget extends StatelessWidget { /// A tappable card row on the capture surfaces: icon, label, chevron; announced as a button. class _CardRow extends StatelessWidget { - const _CardRow({required this.icon, required this.label, required this.onTap, this.badge = false}); + const _CardRow({required this.icon, required this.label, required this.onTap, this.badge = false, this.onDismiss}); final IconData icon; final String label; final VoidCallback onTap; final bool badge; + /// Shows a close button in place of the chevron. + final VoidCallback? onDismiss; + @override Widget build(BuildContext context) { + final open = Semantics( + button: true, + label: label, + excludeSemantics: true, + child: InkWell( + onTap: onTap, + child: Padding( + padding: + EdgeInsets.fromLTRB(OmiSpacing.md, OmiSpacing.md, onDismiss == null ? OmiSpacing.md : 0, OmiSpacing.md), + child: Row( + children: [ + Icon(icon, color: OmiColors.textPrimary), + const SizedBox(width: OmiSpacing.md), + Expanded(child: Text(label, style: OmiType.callout)), + if (badge) ...[ + Icon(Icons.fiber_manual_record, color: OmiColors.danger, size: 10), + const SizedBox(width: OmiSpacing.xs), + ], + if (onDismiss == null) Icon(Icons.arrow_forward_ios, color: OmiColors.textPrimary, size: 16), + ], + ), + ), + ), + ); return Padding( padding: const EdgeInsets.fromLTRB(OmiSpacing.md, OmiSpacing.md, OmiSpacing.md, 0), - child: Semantics( - button: true, - label: label, - excludeSemantics: true, - child: Material( - color: OmiColors.surface1, - borderRadius: OmiRadius.xlAll, - clipBehavior: Clip.antiAlias, - child: InkWell( - onTap: onTap, - child: Padding( - padding: const EdgeInsets.all(OmiSpacing.md), - child: Row( + child: Material( + color: OmiColors.surface1, + borderRadius: OmiRadius.xlAll, + clipBehavior: Clip.antiAlias, + child: onDismiss == null + ? open + : Row( children: [ - Icon(icon, color: OmiColors.textPrimary), - const SizedBox(width: OmiSpacing.md), - Expanded(child: Text(label, style: OmiType.callout)), - if (badge) ...[ - Icon(Icons.fiber_manual_record, color: OmiColors.danger, size: 10), - const SizedBox(width: OmiSpacing.xs), - ], - Icon(Icons.arrow_forward_ios, color: OmiColors.textPrimary, size: 16), + Expanded(child: open), + OmiIconButton( + key: const ValueKey('voice_profile_card_dismiss'), + icon: const Icon(Icons.close_rounded, size: 18), + label: context.l10n.dismiss, + color: OmiColors.textSecondary, + onPressed: onDismiss!, + ), + const SizedBox(width: OmiSpacing.xxs), ], ), - ), - ), - ), ), ); } diff --git a/app/lib/pages/chat/page.dart b/app/lib/pages/chat/page.dart index f4c1b8fe484..3ca34da3334 100644 --- a/app/lib/pages/chat/page.dart +++ b/app/lib/pages/chat/page.dart @@ -21,8 +21,6 @@ import 'package:omi/pages/apps/widgets/capability_apps_page.dart'; import 'package:omi/pages/chat/chat_scroll_policy.dart'; import 'package:omi/pages/chat/widgets/ai_message.dart'; import 'package:omi/pages/chat/widgets/jump_to_latest_button.dart'; -import 'package:omi/pages/paywall/paywall_router.dart'; -import 'package:omi/pages/settings/widgets/plans_sheet.dart'; import 'package:omi/pages/chat/widgets/user_message.dart'; import 'package:omi/pages/chat/widgets/voice_recorder_widget.dart'; import 'package:omi/pages/settings/integrations_page.dart'; @@ -45,6 +43,8 @@ import 'package:omi/pages/chat/widgets/chat_chrome.dart'; import 'package:omi/pages/chat/widgets/chat_entrance.dart'; import 'package:omi/pages/chat/widgets/chat_followup_chip.dart'; import 'package:omi/ui/ui.dart'; +import 'package:omi/pages/settings/widgets/upgrade_paywall.dart'; +import 'package:omi/services/superwall_pilot.dart'; class ChatPage extends StatefulWidget { final bool isPivotBottom; @@ -306,8 +306,13 @@ class ChatPageState extends State with AutomaticKeepAliveClientMixin, : provider.isClearingChat ? OmiLoadingState(label: context.l10n.deletingMessages) : (provider.messages.isEmpty) - ? ChatGreeting(isConnected: connectivityProvider.isConnected, name: prefs.givenName) - : _buildTranscript(provider), + ? ChatGreeting( + isConnected: connectivityProvider.isConnected, + name: prefs.givenName, + welcome: ChatWelcome.pending, + animateWelcome: !ChatWelcome.shown, + ) + : _transcriptMarkingWelcomed(provider), ), _buildComposer(context, provider, connectivityProvider), ], @@ -693,7 +698,15 @@ class ChatPageState extends State with AutomaticKeepAliveClientMixin, ); } + /// Anyone who already has a chat history is past the welcome, so clearing it later doesn't bring + /// the welcome back. + Widget _transcriptMarkingWelcomed(MessageProvider provider) { + if (ChatWelcome.pending) ChatWelcome.markDone(); + return _buildTranscript(provider); + } + _sendMessageUtil(String text) async { + ChatWelcome.markDone(); var provider = context.read(); // Guard against re-entry (rapid double-tap of send, voice→transcribeSuccess // race firing onTranscriptReady twice, etc.). Without this the chat could @@ -740,23 +753,9 @@ class ChatPageState extends State with AutomaticKeepAliveClientMixin, Future _showPlansSheetOnQuotaExceeded() async { if (!mounted) return; - final usage = context.read(); // Refresh subscription data so the plans sheet is up-to-date - usage.fetchSubscription(); - if (await PaywallRouter.openVariant(context, source: 'chat_quota_exceeded')) return; - if (!mounted) return; - PlatformManager.instance.analytics.paywallOpened( - 'chat_quota_exceeded', - currentPlan: usage.subscription?.subscription.plan.wireName ?? 'unknown', - ); - final title = await plansSheetTitle(context); - if (!mounted) return; - showOmiSheet( - context: context, - title: title, - padding: EdgeInsets.zero, - builder: (_) => const _PlansSheetWrapper(analyticsSource: 'chat_quota_exceeded'), - ); + context.read().fetchSubscription(); + openUpgradePaywall(context, placement: SuperwallPilot.chatQuotaPlacement, source: 'chat_quota_exceeded'); } sendInitialAppMessage(App? app) async { @@ -1153,53 +1152,6 @@ class _SelectedTextChip extends StatelessWidget { } } -class _PlansSheetWrapper extends StatefulWidget { - const _PlansSheetWrapper({this.analyticsSource = 'chat_quota_exceeded'}); - - final String analyticsSource; - - @override - State<_PlansSheetWrapper> createState() => _PlansSheetWrapperState(); -} - -class _PlansSheetWrapperState extends State<_PlansSheetWrapper> with TickerProviderStateMixin { - late AnimationController _waveController; - late AnimationController _arrowController; - late AnimationController _notesController; - late Animation _arrowAnimation; - - @override - void initState() { - super.initState(); - _waveController = AnimationController(vsync: this, duration: const Duration(seconds: 2))..repeat(); - _arrowController = AnimationController(vsync: this, duration: const Duration(milliseconds: 800))..repeat(); - _notesController = AnimationController(vsync: this, duration: const Duration(seconds: 3))..repeat(); - _arrowAnimation = Tween( - begin: 0, - end: 10, - ).animate(CurvedAnimation(parent: _arrowController, curve: Curves.easeInOut)); - } - - @override - void dispose() { - _waveController.dispose(); - _arrowController.dispose(); - _notesController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return PlansSheet( - waveController: _waveController, - notesController: _notesController, - arrowController: _arrowController, - arrowAnimation: _arrowAnimation, - analyticsSource: widget.analyticsSource, - ); - } -} - /// Row identity for the transcript sliver. Persisted messages use their id. /// Placeholder replies all keep id `0000`, so those rows use the message object. Key _chatMessageRowKey(ServerMessage message) => message.isEmpty ? ObjectKey(message) : ValueKey(message.id); diff --git a/app/lib/pages/chat/widgets/chat_starters.dart b/app/lib/pages/chat/widgets/chat_starters.dart index 2d217a9d1a7..0d27c77f67d 100644 --- a/app/lib/pages/chat/widgets/chat_starters.dart +++ b/app/lib/pages/chat/widgets/chat_starters.dart @@ -1,8 +1,12 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; +import 'package:omi/backend/preferences.dart'; import 'package:omi/backend/schema/conversation.dart'; import 'package:omi/pages/chat/widgets/chat_entrance.dart'; +import 'package:omi/pages/chat/widgets/typing_indicator.dart'; import 'package:omi/providers/conversation_provider.dart'; import 'package:omi/ui/ui.dart'; import 'package:omi/utils/l10n_extensions.dart'; @@ -15,13 +19,45 @@ int countConversationsForLocalDay(Iterable conversations, Da }).length; } +/// Omi's first chat message: shown in the empty chat until the person sends their first message, +/// typed in the first time it appears. Remembered per account. +abstract final class ChatWelcome { + static String _key(String uid) => 'chat/welcome/$uid'; + + /// Still to be answered: the person has not sent a chat message since it was introduced. + static bool get pending => SharedPreferencesUtil().getString(_key(SharedPreferencesUtil().uid)) != 'done'; + + /// It has been typed in once; later visits show it settled. + static bool get shown => SharedPreferencesUtil().getString(_key(SharedPreferencesUtil().uid)) != ''; + + static void markShown() { + if (!shown) SharedPreferencesUtil().saveString(_key(SharedPreferencesUtil().uid), 'shown'); + } + + static void markDone() => SharedPreferencesUtil().saveString(_key(SharedPreferencesUtil().uid), 'done'); +} + class ChatGreeting extends StatelessWidget { - const ChatGreeting({super.key, required this.isConnected, this.name = '', this.hour, this.todayCount}); + const ChatGreeting({ + super.key, + required this.isConnected, + this.name = '', + this.hour, + this.todayCount, + this.welcome = false, + this.animateWelcome = true, + }); final bool isConnected; final String name; final int? hour; final int? todayCount; + /// Show Omi's welcome message in place of "What do you want to know?". + final bool welcome; + + /// Type the welcome in (the first time); otherwise it appears settled. + final bool animateWelcome; + @override Widget build(BuildContext context) { final l10n = context.l10n; @@ -58,19 +94,22 @@ class ChatGreeting extends StatelessWidget { child: _ConversationCount(count: count, style: heading), ), const SizedBox(height: 10), - ChatRise( - key: const Key('chat_question_rise'), - interval: ChatIntro.question, - child: Text( - l10n.whatDoYouWantToKnow, - style: OmiType.title3.copyWith( - fontWeight: FontWeight.w500, - letterSpacing: -0.2, - height: 1.2, - color: OmiColors.textSecondary, + if (welcome) + _WelcomeMessage(text: l10n.chatWelcomeMessage, animate: animateWelcome) + else + ChatRise( + key: const Key('chat_question_rise'), + interval: ChatIntro.question, + child: Text( + l10n.whatDoYouWantToKnow, + style: OmiType.title3.copyWith( + fontWeight: FontWeight.w500, + letterSpacing: -0.2, + height: 1.2, + color: OmiColors.textSecondary, + ), ), ), - ), ], ), ), @@ -157,3 +196,90 @@ class ChatSuggestions extends StatelessWidget { ); } } + +/// Omi "texting" its first message: the typing dots, then the words arriving like a streamed reply, +/// in the same plain style as Omi's chat answers. +class _WelcomeMessage extends StatefulWidget { + const _WelcomeMessage({required this.text, required this.animate}); + + final String text; + final bool animate; + + @override + State<_WelcomeMessage> createState() => _WelcomeMessageState(); +} + +class _WelcomeMessageState extends State<_WelcomeMessage> { + late final List _words = widget.text.split(' '); + late int _shown = widget.animate ? 0 : _words.length; + bool _typing = false; + Timer? _timer; + + @override + void initState() { + super.initState(); + if (!widget.animate) return; + _typing = true; + _timer = Timer(const Duration(milliseconds: 1300), () { + if (!mounted) return; + setState(() => _typing = false); + _timer = Timer.periodic(const Duration(milliseconds: 45), (timer) { + if (!mounted) return timer.cancel(); + setState(() => _shown++); + if (_shown >= _words.length) { + timer.cancel(); + ChatWelcome.markShown(); + } + }); + }); + } + + @override + void dispose() { + _timer?.cancel(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final reduceMotion = MediaQuery.disableAnimationsOf(context); + final shown = reduceMotion ? _words.length : _shown; + if (_typing && !reduceMotion) { + return const Padding( + key: Key('chat_welcome_typing'), + padding: EdgeInsets.symmetric(vertical: OmiSpacing.sm), + child: Row(mainAxisSize: MainAxisSize.min, children: [TypingIndicator()]), + ); + } + return Semantics( + liveRegion: true, + label: widget.text, + excludeSemantics: true, + child: Text( + // The whole message lays out from the start, so lines don't jump as words arrive. + widget.text, + key: const Key('chat_welcome_message'), + style: OmiType.body.copyWith(height: 1.45, color: OmiColors.textPrimary), + ).revealedUpTo(shown, _words), + ); + } +} + +extension on Text { + /// The same text, with the words after [count] drawn transparent so the layout never shifts. + Widget revealedUpTo(int count, List words) { + if (count >= words.length) return this; + final visible = words.take(count).join(' '); + final hidden = words.skip(count).join(' '); + return Text.rich( + key: key, + TextSpan( + style: style, + children: [ + TextSpan(text: visible), + TextSpan(text: count == 0 ? hidden : ' $hidden', style: const TextStyle(color: Colors.transparent)), + ], + ), + ); + } +} diff --git a/app/lib/pages/conversations/conversations_page.dart b/app/lib/pages/conversations/conversations_page.dart index 459cdd6984b..3d727919be8 100644 --- a/app/lib/pages/conversations/conversations_page.dart +++ b/app/lib/pages/conversations/conversations_page.dart @@ -9,6 +9,7 @@ import 'package:visibility_detector/visibility_detector.dart'; import 'package:omi/backend/schema/conversation.dart'; import 'package:omi/pages/capture/widgets/widgets.dart'; +import 'package:omi/pages/conversations/widgets/onboarding_welcome_card.dart'; import 'package:omi/pages/conversations/widgets/capture_recovery_banner.dart'; import 'package:omi/pages/conversations/widgets/processing_capture.dart'; import 'package:omi/pages/conversations/widgets/speaker_tag_prompt_card.dart'; @@ -521,16 +522,21 @@ class _ConversationsPageState extends State with AutomaticKee processingByDate: processingByDate, ); + Future refresh() async { + _lastLoadMoreRequestKey = null; + Provider.of(context, listen: false).refreshInProgressConversations(); + await Future.wait([ + convoProvider.getInitialConversations(), + Provider.of(context, listen: false).refresh(), + if (_recapsKey.currentState != null) _recapsKey.currentState!.refresh(), + ]); + } + return RefreshIndicator( + notificationPredicate: OmiRefreshControl.materialPredicate, onRefresh: () async { HapticFeedback.mediumImpact(); - _lastLoadMoreRequestKey = null; - Provider.of(context, listen: false).refreshInProgressConversations(); - await Future.wait([ - convoProvider.getInitialConversations(), - Provider.of(context, listen: false).refresh(), - if (_recapsKey.currentState != null) _recapsKey.currentState!.refresh(), - ]); + await refresh(); }, color: OmiColors.onAccent, backgroundColor: OmiColors.accent, @@ -538,8 +544,11 @@ class _ConversationsPageState extends State with AutomaticKee controller: _scrollController, physics: const AlwaysScrollableScrollPhysics(), slivers: [ + if (OmiRefreshControl.supported) OmiRefreshControl(onRefresh: refresh), // The live capture row: recording, a call, or a conversation being captured. - const SliverToBoxAdapter(child: ConversationCaptureWidget(showsCall: true)), + const SliverToBoxAdapter( + child: ConversationCaptureWidget(showsCall: true, deviceStatusInBottomBar: true)), + const SliverToBoxAdapter(child: OnboardingWelcomeCard()), const SliverToBoxAdapter(child: SpeechProfileCardWidget()), const SliverToBoxAdapter(child: UpdateFirmwareCardWidget()), // Review absorbs the voice prompts; the old card shows only while Review is off. diff --git a/app/lib/pages/conversations/widgets/conversation_list_item.dart b/app/lib/pages/conversations/widgets/conversation_list_item.dart index 96213ff55ee..2c8ea000753 100644 --- a/app/lib/pages/conversations/widgets/conversation_list_item.dart +++ b/app/lib/pages/conversations/widgets/conversation_list_item.dart @@ -15,7 +15,8 @@ import 'package:omi/pages/conversation_detail/page.dart'; import 'package:omi/widgets/conversation_bottom_bar.dart' show ConversationTab; import 'package:omi/pages/conversations/conversation_action_analytics.dart'; import 'package:omi/pages/conversations/conversation_actions.dart'; -import 'package:omi/pages/settings/usage_page.dart'; +import 'package:omi/pages/settings/widgets/upgrade_paywall.dart'; +import 'package:omi/services/superwall_pilot.dart'; import 'package:omi/providers/conversation_provider.dart'; import 'package:omi/providers/usage_provider.dart'; import 'package:omi/ui/ui.dart'; @@ -176,11 +177,11 @@ class _ConversationListItemState extends State { Future _open(BuildContext context, ConversationProvider provider) async { if (widget.conversation.isLocked) { if (!context.read().showSubscriptionUI) return; - PlatformManager.instance.analytics.paywallOpened( - 'locked_conversation', - currentPlan: context.read().subscription?.subscription.plan.wireName ?? 'unknown', + await openUpgradePaywall( + context, + placement: SuperwallPilot.lockedContentPlacement, + source: 'locked_conversation', ); - routeToPage(context, const UsagePage(showUpgradeDialog: true, paywallSource: 'locked_conversation')); return; } HapticFeedback.selectionClick(); @@ -854,11 +855,11 @@ class LockedConversationRun extends StatelessWidget { return; } if (!context.read().showSubscriptionUI) return; - PlatformManager.instance.analytics.paywallOpened( - 'locked_conversation', - currentPlan: context.read().subscription?.subscription.plan.wireName ?? 'unknown', + await openUpgradePaywall( + context, + placement: SuperwallPilot.lockedContentPlacement, + source: 'locked_conversation', ); - routeToPage(context, const UsagePage(showUpgradeDialog: true, paywallSource: 'locked_conversation')); } @override diff --git a/app/lib/pages/conversations/widgets/empty_conversations.dart b/app/lib/pages/conversations/widgets/empty_conversations.dart index eeaf6d90fee..cf4d7be4180 100644 --- a/app/lib/pages/conversations/widgets/empty_conversations.dart +++ b/app/lib/pages/conversations/widgets/empty_conversations.dart @@ -17,7 +17,6 @@ class NoConversationsHero extends StatelessWidget { return Padding( padding: const EdgeInsets.only(bottom: 120), child: OmiEmptyState( - icon: Icons.forum_rounded, title: context.l10n.noConversationsYet, titleLayoutReference: context.l10n.noTasksYet, message: message, @@ -47,7 +46,7 @@ class EmptyConversationsWidget extends StatelessWidget { title: l10n.noStarredConversations, message: l10n.starConversationHint, ) - : OmiEmptyState(icon: Icons.forum_rounded, title: l10n.noConversationsYet), + : OmiEmptyState(title: l10n.noConversationsYet), ); } } diff --git a/app/lib/pages/conversations/widgets/onboarding_welcome_card.dart b/app/lib/pages/conversations/widgets/onboarding_welcome_card.dart new file mode 100644 index 00000000000..76630e70f0b --- /dev/null +++ b/app/lib/pages/conversations/widgets/onboarding_welcome_card.dart @@ -0,0 +1,71 @@ +import 'package:flutter/material.dart'; + +import 'package:provider/provider.dart'; + +import 'package:omi/backend/preferences.dart'; +import 'package:omi/pages/onboarding/activation/activation_flow.dart'; +import 'package:omi/providers/conversation_provider.dart'; +import 'package:omi/ui/ui.dart'; +import 'package:omi/utils/l10n_extensions.dart'; + +/// For people who skipped the first conversation in onboarding: a short welcome on Home that says +/// how Omi works. Gone once they have a conversation, or when they close it. +class OnboardingWelcomeCard extends StatefulWidget { + const OnboardingWelcomeCard({super.key}); + + @override + State createState() => _OnboardingWelcomeCardState(); +} + +class _OnboardingWelcomeCardState extends State { + late bool _show = SharedPreferencesUtil().getBool(ActivationFlow.welcomeCardKey); + + void _close() { + OmiHaptics.selection(); + SharedPreferencesUtil().saveBool(ActivationFlow.welcomeCardKey, false); + setState(() => _show = false); + } + + @override + Widget build(BuildContext context) { + if (!_show) return const SizedBox.shrink(); + final hasConversations = context.select((p) => p.conversations.isNotEmpty); + if (hasConversations) return const SizedBox.shrink(); + final l10n = context.l10n; + return Padding( + padding: const EdgeInsets.fromLTRB(OmiSpacing.md, OmiSpacing.sm, OmiSpacing.md, 0), + child: Container( + key: const Key('onboarding_welcome_card'), + padding: const EdgeInsets.fromLTRB(OmiSpacing.lg, OmiSpacing.md, OmiSpacing.xs, OmiSpacing.lg), + decoration: BoxDecoration(color: OmiColors.surface1, borderRadius: OmiRadius.lgAll), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Padding( + padding: const EdgeInsets.only(top: OmiSpacing.xs), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('👋 ${l10n.onboardingWelcomeCardTitle}', style: OmiType.headline), + const SizedBox(height: OmiSpacing.xs), + Text( + l10n.onboardingWelcomeCardBody, + style: OmiType.subhead.copyWith(color: OmiColors.textSecondary, height: 1.4), + ), + ], + ), + ), + ), + OmiIconButton( + key: const Key('onboarding_welcome_card_close'), + icon: const Icon(Icons.close_rounded, size: 20), + label: l10n.close, + onPressed: _close, + ), + ], + ), + ), + ); + } +} diff --git a/app/lib/pages/conversations/widgets/processing_capture.dart b/app/lib/pages/conversations/widgets/processing_capture.dart index d4b65fd1aeb..f68cc6c2483 100644 --- a/app/lib/pages/conversations/widgets/processing_capture.dart +++ b/app/lib/pages/conversations/widgets/processing_capture.dart @@ -25,16 +25,22 @@ import 'package:omi/utils/other/temp.dart'; import 'package:omi/utils/processing_timeout.dart'; import 'package:omi/backend/schema/phone_call.dart'; import 'package:omi/providers/phone_call_provider.dart'; +import 'package:omi/pages/conversations/widgets/conversation_list_item.dart'; import 'package:omi/pages/conversations/widgets/live_capture_card.dart'; import 'package:omi/pages/phone_calls/active_call_page.dart'; import 'package:omi/ui/ui.dart'; class ConversationCaptureWidget extends StatefulWidget { - const ConversationCaptureWidget({super.key, this.showsCall = false}); + const ConversationCaptureWidget({super.key, this.showsCall = false, this.deviceStatusInBottomBar = false}); /// Home shows an Omi call on this card; the Conversations tab has its own call banner. final bool showsCall; + /// Home's bottom pill already says the connected pendant is listening, so the card stays out + /// of the way then; it still shows whenever there is more to say (paused, reconnecting, saving + /// for later, a dropped pendant) or the phone is the one recording. + final bool deviceStatusInBottomBar; + @override State createState() => _ConversationCaptureWidgetState(); } @@ -137,6 +143,12 @@ class _ConversationCaptureWidgetState extends State { if (provider.liveCaptureSource == null && !phoneLive && !batch) { return const SizedBox.shrink(); } + if (widget.deviceStatusInBottomBar && !phoneLive && !batch && _pendantJustListening(provider)) { + return const SizedBox.shrink(); + } + if (widget.deviceStatusInBottomBar && phoneLive && !batch && _phoneJustListening(provider)) { + return const SizedBox.shrink(); + } return GestureDetector( onTap: () async { @@ -175,6 +187,26 @@ class _ConversationCaptureWidgetState extends State { ); } + /// The pendant is capturing: Home's bottom bar already says so (listening, still connecting to + /// transcription, buffering), so no separate "Listening" card. The card stays for what needs the + /// person: paused, or transcription that has failed. + static bool _pendantJustListening(CaptureProvider provider) { + final source = provider.liveCaptureSource; + return source != null && source != 'phone' && !provider.isPaused && provider.terminalTranscriptionFailure == null; + } + + /// The phone is recording without trouble: Home's bottom bar shows it (status, newest words, the + /// live transcript on tap, Stop), so no card — including the moment it is starting, or the old + /// card flashes before the bar takes over. It stays for paused, the microphone taken or + /// recovering, and transcription that has failed. + static bool _phoneJustListening(CaptureProvider provider) { + return (provider.recordingState == RecordingState.record || + provider.recordingState == RecordingState.initialising) && + !provider.isPaused && + !provider.isPhoneMicPaused && + provider.terminalTranscriptionFailure == null; + } + /// The live card's glyph and 44pt Pause target carry their own air, so its edges are tighter /// than the Transcribe Later card's; the status line keeps the width it needs on a 320pt phone. static const _liveCardPadding = EdgeInsets.fromLTRB(14, 10, 8, 12); @@ -668,8 +700,15 @@ class ProcessingConversationWidget extends StatefulWidget { State createState() => _ProcessingConversationWidgetState(); } -class _ProcessingConversationWidgetState extends State { +class _ProcessingConversationWidgetState extends State + with SingleTickerProviderStateMixin { Timer? _timeoutTicker; + + /// About half a minute of motion — a typical summary — then the row rests, so a slow one does not + /// animate for minutes (after the timeout it says so instead). + static const _cycles = 18; + late final AnimationController _motion = + AnimationController(vsync: this, duration: const Duration(milliseconds: 1600))..repeat(count: _cycles); bool _timedOut = false; bool _retrying = false; @@ -713,6 +752,7 @@ class _ProcessingConversationWidgetState extends State 0) OmiDuration.compact(seconds, l10n), + ].join(' • '); + final snippet = conversationSnippet(conversation, maxChars: 140); + final metaStyle = TextStyle(color: OmiColors.textTertiary, fontSize: 14); + // The saved row's layout (glyph, title, time and length), so the finished conversation takes + // its place without the card changing shape: the emoji arrives where the glyph is. return GestureDetector( - onTap: () async { - routeToPage(context, ProcessingConversationPage(conversation: widget.conversation)); - }, + onTap: () => routeToPage(context, ProcessingConversationPage(conversation: conversation)), child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6), + padding: const EdgeInsets.only(top: 8, left: 16, right: 16), child: Container( + key: const ValueKey('processing_conversation_card'), width: double.maxFinite, - decoration: BoxDecoration(color: OmiColors.surface1, borderRadius: BorderRadius.circular(24.0)), - // Static skeleton - no animation to save CPU/battery - child: Padding( - padding: const EdgeInsets.all(16), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Header row - Row( + padding: const EdgeInsets.all(14), + decoration: BoxDecoration(color: OmiColors.surface1, borderRadius: OmiRadius.xlAll), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _CondensingGlyph(motion: _motion, still: still), + const SizedBox(width: 12), + Expanded( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, children: [ - // Icon placeholder - Container( - width: 24, - height: 24, - decoration: BoxDecoration( - color: OmiColors.surface2, - borderRadius: BorderRadius.circular(12), - ), - ), - const SizedBox(width: 8), - // Processing label - Container( - decoration: BoxDecoration( - color: OmiColors.categorySurface, - borderRadius: BorderRadius.circular(16), - ), - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), - child: Text( - captureStateLabel(context.l10n, CaptureDisplayState.processing), - style: OmiType.subhead.copyWith(fontWeight: FontWeight.w500), - ), - ), - const Spacer(), - // The real start time, not a placeholder bar (hub audit #25). Text( - OmiDateFormat.of(context).time(widget.conversation.startedAt ?? widget.conversation.createdAt), - style: OmiType.footnote.copyWith(color: OmiColors.textTertiary), + captureStateLabel(l10n, CaptureDisplayState.processing), + style: Theme.of(context).textTheme.titleMedium, + maxLines: 1, + overflow: TextOverflow.ellipsis, ), - ], - ), - const SizedBox(height: 12), - // Title placeholder - Container( - width: double.maxFinite, - height: 16, - decoration: BoxDecoration(color: OmiColors.surface2, borderRadius: BorderRadius.circular(4)), - ), - if (_timedOut) ...[ - const SizedBox(height: 12), - Text( - context.l10n.processingTakingLonger, - style: TextStyle(color: OmiColors.textTertiary, fontSize: 13, height: 1.3), - ), - const SizedBox(height: 10), - Align( - alignment: Alignment.centerLeft, - child: GestureDetector( - onTap: () {}, // absorb so the card's open-on-tap does not fire - child: TextButton( + const SizedBox(height: 3), + Text(meta, style: metaStyle, maxLines: 1, overflow: TextOverflow.ellipsis), + if (_timedOut) ...[ + const SizedBox(height: OmiSpacing.sm), + Text( + l10n.processingTakingLonger, + style: OmiType.footnote.copyWith(color: OmiColors.textSecondary, height: 1.35), + ), + const SizedBox(height: OmiSpacing.sm), + OmiButton.secondary( key: const Key('processing_conversation_retry_button'), + label: l10n.tryAgain, + icon: Icons.refresh_rounded, + size: OmiButtonSize.compact, + isLoading: _retrying, onPressed: _retrying ? null : _onRetry, - style: TextButton.styleFrom( - foregroundColor: OmiColors.textPrimary, - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), - minimumSize: const Size(44, 44), - tapTargetSize: MaterialTapTargetSize.shrinkWrap, + ), + ] else if (snippet.isNotEmpty) ...[ + const SizedBox(height: OmiSpacing.sm), + // What was said, with a light passing over it while Omi reads it. + _ReadingSweep( + motion: _motion, + still: still, + child: Text( + snippet, + style: OmiType.footnote.copyWith(color: OmiColors.textTertiary, height: 1.35), + maxLines: 2, + overflow: TextOverflow.ellipsis, ), - child: _retrying ? const OmiSpinner(size: OmiSpinnerSize.small) : Text(context.l10n.tryAgain), ), - ), - ), - ], - ], - ), + ], + ], + ), + ), + ], + ), + ), + ), + ); + } +} + +/// The processing row's glyph, in the emoji's square: three transcript lines draw together into +/// one note and spread back out. Still, it is the three lines. +class _CondensingGlyph extends StatelessWidget { + const _CondensingGlyph({required this.motion, required this.still}); + + final Animation motion; + final bool still; + + @override + Widget build(BuildContext context) { + return Container( + width: 36, + height: 36, + decoration: BoxDecoration(color: OmiColors.surface2, borderRadius: OmiRadius.mdAll), + child: RepaintBoundary( + child: AnimatedBuilder( + animation: motion, + builder: (context, _) => CustomPaint( + painter: _CondensingLinesPainter(t: still ? 0 : motion.value, color: OmiColors.textSecondary), ), ), ), ); } } + +class _CondensingLinesPainter extends CustomPainter { + _CondensingLinesPainter({required this.t, required this.color}); + + final double t; + final Color color; + + @override + void paint(Canvas canvas, Size size) { + // 0→0.5: the lines gather into one short line; 0.5→1: they spread back out. + final phase = t < 0.5 ? Curves.easeInOutCubic.transform(t * 2) : 1 - Curves.easeInOutCubic.transform((t - 0.5) * 2); + final c = size.center(Offset.zero); + final paint = Paint() + ..strokeCap = StrokeCap.round + ..strokeWidth = 2; + const widths = [16.0, 12.0, 8.0]; + for (var k = 0; k < 3; k++) { + final y = c.dy + (k - 1) * 5 * (1 - phase); + final w = widths[k] * (1 - phase) + 11 * phase; + // Each line keeps its left edge, like text, until they meet in the middle. + final left = c.dx - widths[0] / 2 * (1 - phase) - w / 2 * phase; + paint.color = color.withValues(alpha: k == 1 ? 1 : 1 - 0.6 * phase); + canvas.drawLine(Offset(left, y), Offset(left + w, y), paint); + } + } + + @override + bool shouldRepaint(covariant _CondensingLinesPainter old) => old.t != t || old.color != color; +} + +/// A soft band of light that moves across [child] once per cycle of [motion]. +class _ReadingSweep extends StatelessWidget { + const _ReadingSweep({required this.motion, required this.still, required this.child}); + + final Animation motion; + final bool still; + final Widget child; + + @override + Widget build(BuildContext context) { + if (still) return child; + return RepaintBoundary( + child: AnimatedBuilder( + animation: motion, + child: child, + builder: (context, child) { + final x = -0.3 + 1.6 * Curves.easeInOut.transform(motion.value); + return ShaderMask( + blendMode: BlendMode.srcIn, + shaderCallback: (rect) => LinearGradient( + colors: [OmiColors.textTertiary, OmiColors.textPrimary, OmiColors.textTertiary], + stops: [(x - 0.2).clamp(0.0, 1.0), x.clamp(0.0, 1.0), (x + 0.2).clamp(0.0, 1.0)], + ).createShader(rect), + child: child, + ); + }, + ), + ); + } +} diff --git a/app/lib/pages/home/page.dart b/app/lib/pages/home/page.dart index 17a96bf029d..1501db0c2a4 100644 --- a/app/lib/pages/home/page.dart +++ b/app/lib/pages/home/page.dart @@ -21,7 +21,10 @@ import 'package:omi/pages/chat/chat_route.dart'; import 'package:omi/pages/chat/page.dart'; import 'package:omi/pages/action_items/widgets/task_selection_action_bar.dart'; import 'package:omi/pages/conversations/widgets/merge_action_bar.dart'; +import 'package:omi/pages/action_items/widgets/tasks_more_menu.dart'; import 'package:omi/pages/home/home_content.dart'; +import 'package:omi/pages/home/widgets/home_bottom_controls.dart'; +import 'package:omi/pages/home/widgets/home_page_stack.dart'; import 'package:omi/pages/home/widgets/home_tab_switcher.dart'; import 'package:omi/pages/home/home_widgets_publisher.dart'; import 'package:omi/pages/phone_calls/active_call_banner.dart'; @@ -65,6 +68,7 @@ import 'package:omi/widgets/calendar_date_picker_sheet.dart'; import 'package:omi/widgets/header_circle_button.dart'; import 'package:omi/pages/onboarding/interactive_device_onboarding/interactive_device_onboarding_wrapper.dart'; import 'package:omi/services/sockets/listen_client_state.dart'; +import 'package:omi/pages/home/paywall_reask_prompt.dart'; import 'package:omi/ui/ui.dart'; import 'home_deep_links.dart'; import 'home_navigation.dart'; @@ -368,6 +372,7 @@ class _HomePageState extends State with WidgetsBindingObserver, Ticker // Reload convos if (mounted) { + unawaited(queuePaywallReask(context)); Provider.of(context, listen: false).refreshConversations(); final captureProvider = Provider.of(context, listen: false); captureProvider.setMetricsAppActive(true); @@ -473,6 +478,7 @@ class _HomePageState extends State with WidgetsBindingObserver, Ticker listen: false, ).streamDeviceRecording(device: Provider.of(context, listen: false).capabilityNormalizedDevice); } + if (mounted) context.read().showDeferredFirmwareUpdatePrompt(context); if (!mounted || initialLink == null) return; await openHomeDeepLink(context, initialLink, openSettings: _openSettings, openSearch: _openSearch); @@ -563,6 +569,8 @@ class _HomePageState extends State with WidgetsBindingObserver, Ticker }, ); + unawaited(queuePaywallReask(context)); + // Register callback for device connection to check firmware announcements and device onboarding deviceProvider.onDeviceConnected = (BtDevice device) { _onDeviceConnectedForAnnouncements(device); @@ -775,7 +783,7 @@ class _HomePageState extends State with WidgetsBindingObserver, Ticker child: Scaffold( backgroundColor: OmiColors.surface0, resizeToAvoidBottomInset: false, - appBar: _buildAppBar(context, onHome), + appBar: _buildAppBar(context), body: GestureDetector( onTap: () => primaryFocus?.unfocus(), child: Stack( @@ -783,7 +791,7 @@ class _HomePageState extends State with WidgetsBindingObserver, Ticker Column( children: [ HomeTabSwitcher( - trailing: onHome ? null : _buildTasksActions(context), + trailing: _buildPageActions(context, onHome), onTabTap: (index, isRepeat) { if (isRepeat) { _scrollToTop(index); @@ -819,7 +827,7 @@ class _HomePageState extends State with WidgetsBindingObserver, Ticker // Home shows an active call in its capture row; Tasks gets the slim bar. if (!onHome) const ActiveCallTopBar(), Expanded( - child: IndexedStack(index: selectedIndex, children: _buildPages(selectedIndex)), + child: HomePageStack(index: selectedIndex, children: _buildPages(selectedIndex)), ), ], ), @@ -840,9 +848,9 @@ class _HomePageState extends State with WidgetsBindingObserver, Ticker bottom: homeChatBarOffset(context), child: Row( children: [ - Expanded(child: _buildChatBar(context)), + HomeChatButton(onTap: _openChat, onLongPress: () => _openChat(voice: true)), const SizedBox(width: 10), - const HomeRecordButton(), + const Expanded(child: HomeRecordButton()), ], ), ), @@ -878,99 +886,23 @@ class _HomePageState extends State with WidgetsBindingObserver, Ticker openChatSheet(context, ChatPage(isPivotBottom: false, autoStartVoice: voice)); } - Widget _buildChatBar(BuildContext context) { - return Semantics( - container: true, - button: true, - label: context.l10n.askOmi, - onTap: _openChat, - child: GestureDetector( - key: const ValueKey('home_ask_omi_bar'), - behavior: HitTestBehavior.opaque, - onTap: _openChat, - child: Container( - height: kHomeChatBarHeight, - decoration: BoxDecoration( - color: OmiColors.surface1, - borderRadius: OmiRadius.pillAll, - border: Border.all(color: OmiColors.border, width: 1), - ), - child: Row( - children: [ - const SizedBox(width: 18), - Expanded( - child: ExcludeSemantics( - child: Text( - context.l10n.askOmi, - style: OmiType.subhead.copyWith(color: OmiColors.textTertiary), - overflow: TextOverflow.ellipsis, - ), - ), - ), - GestureDetector( - // The mic sits inside the chat bar's own tap target, so a near miss - // does not do nothing: it opens text chat instead of voice. Own the - // bar's full height and its rounded end, not just the 42pt circle. - behavior: HitTestBehavior.opaque, - onTap: () => _openChat(voice: true), - child: Semantics( - container: true, - button: true, - label: context.l10n.voiceMode, - child: Container( - height: kHomeChatBarHeight, - padding: const EdgeInsets.only(left: 8, right: 6), - alignment: Alignment.center, - child: Container( - width: 42, - height: 42, - alignment: Alignment.center, - decoration: BoxDecoration(color: OmiColors.accent, shape: BoxShape.circle), - child: FaIcon(FontAwesomeIcons.microphone, size: 15, color: OmiColors.onAccent), - ), - ), - ), - ), - ], - ), - ), - ), - ); - } - - /// Export and the completed toggle, beside the switcher while Tasks is showing. - Widget _buildTasksActions(BuildContext context) { - return Consumer( - builder: (context, actionItemsProvider, _) { - final showCompleted = actionItemsProvider.showCompletedView; - return Row( - mainAxisSize: MainAxisSize.min, - children: [ - HeaderCircleButton( - semanticLabel: context.l10n.exportButton, - icon: FaIcon(FontAwesomeIcons.arrowUpFromBracket, size: 16, color: OmiColors.textSecondary), - onTap: () { + /// The selected page's one action, bare beside the switcher: Home filters by date, Tasks opens + /// its menu (select, group, completed, export). Cross-fades when the page changes. + Widget _buildPageActions(BuildContext context, bool onHome) { + return AnimatedSwitcher( + duration: OmiMotion.of(context).quick, + child: onHome + ? OmiIconButton( + key: const ValueKey('home_calendar_button'), + icon: const FaIcon(FontAwesomeIcons.calendar, size: 18), + label: context.l10n.filterByDate, + color: OmiColors.textSecondary, + onPressed: () { OmiHaptics.selection(); - PlatformManager.instance.analytics.exportTasksBannerClicked(); - routeToPage(context, const TaskIntegrationsPage()); + unawaited(_openCalendar(context)); }, - ), - HeaderCircleButton( - semanticLabel: context.l10n.completed, - color: showCompleted ? OmiColors.surface3 : OmiColors.surface1, - icon: FaIcon( - FontAwesomeIcons.solidCircleCheck, - size: 16, - color: showCompleted ? OmiColors.textPrimary : OmiColors.textSecondary, - ), - onTap: () { - OmiHaptics.light(); - actionItemsProvider.toggleShowCompletedView(); - }, - ), - ], - ); - }, + ) + : const TasksMoreMenu(), ); } @@ -991,7 +923,9 @@ class _HomePageState extends State with WidgetsBindingObserver, Ticker ); } - PreferredSizeWidget _buildAppBar(BuildContext context, bool onHome) { + /// The shell header: device, search and settings, the same on Home and Tasks. Page actions sit + /// beside the switcher instead (see [_buildPageActions]). + PreferredSizeWidget _buildAppBar(BuildContext context) { return AppBar( automaticallyImplyLeading: false, backgroundColor: Theme.of(context).colorScheme.surface, @@ -1009,16 +943,6 @@ class _HomePageState extends State with WidgetsBindingObserver, Ticker ), Row( children: [ - if (onHome) - HeaderCircleButton( - key: const ValueKey('home_calendar_button'), - semanticLabel: context.l10n.filterByDate, - icon: FaIcon(FontAwesomeIcons.calendar, size: 16, color: OmiColors.textSecondary), - onTap: () { - OmiHaptics.selection(); - unawaited(_openCalendar(context)); - }, - ), // Search: conversations, recaps, tasks and memories, from any page of the shell. HeaderCircleButton( key: const ValueKey('home_search_button'), diff --git a/app/lib/pages/home/paywall_reask_prompt.dart b/app/lib/pages/home/paywall_reask_prompt.dart new file mode 100644 index 00000000000..055da04ab2f --- /dev/null +++ b/app/lib/pages/home/paywall_reask_prompt.dart @@ -0,0 +1,41 @@ +import 'package:flutter/material.dart'; + +import 'package:provider/provider.dart'; + +import 'package:omi/models/subscription.dart'; +import 'package:omi/pages/settings/widgets/upgrade_paywall.dart'; +import 'package:omi/providers/usage_provider.dart'; +import 'package:omi/services/experiments/paywall_reask.dart'; +import 'package:omi/services/superwall_pilot.dart'; +import 'package:omi/ui/prompts/prompt_queue.dart'; + +bool _checking = false; + +bool _isFree(UsageProvider usage) => + usage.showSubscriptionUI && usage.subscription?.subscription.plan == PlanType.basic; + +/// On app open (launch or back from the background): when [PaywallReask] says a re-ask is due for +/// this free reader and their arm shows it, queue the paywall. [PromptQueue] holds it while the +/// reader records, is on a call or another prompt is up, and drops it if they subscribed meanwhile. +Future queuePaywallReask(BuildContext context) async { + if (_checking) return; + _checking = true; + try { + final usage = context.read(); + if (usage.subscription == null) await usage.fetchSubscription(); + if (!context.mounted || !_isFree(usage)) return; + if (!await PaywallReask.checkDue() || !context.mounted) return; + PromptQueue.instance.enqueue( + PaywallReask.promptId, + PromptPriority.low, + canShowNow: () => context.mounted && _isFree(context.read()), + show: (promptContext) => openUpgradePaywall( + promptContext, + placement: SuperwallPilot.appOpenReaskPlacement, + source: 'app_open_reask', + ), + ); + } finally { + _checking = false; + } +} diff --git a/app/lib/pages/home/widgets/battery_info_widget.dart b/app/lib/pages/home/widgets/battery_info_widget.dart index 287636cf480..3f63cf8e810 100644 --- a/app/lib/pages/home/widgets/battery_info_widget.dart +++ b/app/lib/pages/home/widgets/battery_info_widget.dart @@ -1,6 +1,7 @@ import 'package:omi/pages/phone_calls/phone_calls_feature.dart'; import 'package:omi/utils/platform/platform_manager.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/semantics.dart'; import 'package:provider/provider.dart'; @@ -10,6 +11,8 @@ import 'package:omi/gen/assets.gen.dart'; import 'package:omi/pages/capture/connect.dart'; import 'package:omi/pages/conversation_capturing/page.dart'; import 'package:omi/pages/home/device.dart'; +import 'package:omi/pages/home/widgets/home_bottom_controls.dart'; +import 'package:omi/pages/home/widgets/grow_route.dart'; import 'package:omi/pages/phone_calls/phone_calls_page.dart'; import 'package:omi/providers/capture_provider.dart'; import 'package:omi/providers/device_provider.dart'; @@ -33,8 +36,6 @@ const EdgeInsets _pillTargetMargin = EdgeInsets.symmetric(vertical: (kMinTapTarg /// Battery at or below this shows a low-battery glyph and a warning colour, not just a dot. const int kLowBatteryPercent = 20; -const _stopGlyphRadius = BorderRadius.all(Radius.circular(4)); // omi-ux-allow: radius-literal -- a glyph - class BatteryInfoWidget extends StatefulWidget { const BatteryInfoWidget({super.key}); @@ -175,12 +176,9 @@ class _BatteryInfoWidgetState extends State { children: [ // The asset is white; tint it so it shows on the light pill too. Image.asset(Assets.images.logoTransparent.path, width: 16, height: 16, color: OmiColors.textPrimary), - // Home has room for the word; the other tabs keep the logo alone (it still has a - // spoken label). - if (isMemoriesPage) ...[ - const SizedBox(width: 6), - Text(label, style: OmiType.caption.copyWith(fontSize: 12)), - ], + // The word stays on every page: the logo alone reads as a loading spinner. + const SizedBox(width: 6), + Text(label, style: OmiType.caption.copyWith(fontSize: 12)), ], ); } @@ -222,10 +220,13 @@ class _DevicePill extends StatelessWidget { } } -/// Circular phone-mic record button shown to the right of the home chat bar. -/// Tap starts/stops recording; long-press opens the record options sheet (phone call). The options -/// are announced as the long-press action, and a one-time tip points them out after the first -/// recording. +/// Home's main bottom control, a light pill beside Ask Omi: +/// - "+ New" starts a recording with the phone, and the live transcript grows out of the pill; +/// - "Stop" while the phone records (its status is the card at the top of Home); +/// - with a device connected, the device itself: its picture, what it is doing and its battery. +/// Tap opens what it is capturing; hold offers the phone instead. +/// Long-press on "New" opens the record options sheet (phone call) when calls are offered, and a +/// one-time tip points that out after the first recording. class HomeRecordButton extends StatefulWidget { const HomeRecordButton({super.key}); @@ -236,6 +237,17 @@ class HomeRecordButton extends StatefulWidget { class _HomeRecordButtonState extends State { static const _optionsTipKey = 'v2/homeRecordOptionsTipShown'; + /// Finishing takes a moment (the last audio is flushed): the pill says so at once. + bool _stopping = false; + + /// The phone recording is this pill's own: live, paused, or reconnecting to transcription. + static bool _phoneOwnsCapture(CaptureProvider capture) { + final state = capture.recordingState; + return state == RecordingState.record || + capture.isPhoneMicPaused || + (state == RecordingState.interrupted && capture.liveCaptureSource == 'phone'); + } + void _showRecordOptions(BuildContext context) { OmiHaptics.light(); SharedPreferencesUtil().saveBool(_optionsTipKey, true); @@ -322,10 +334,16 @@ class _HomeRecordButtonState extends State { Future _startPhoneRecording(BuildContext context) async { final captureProvider = context.read(); OmiHaptics.medium(); - if (captureProvider.recordingState == RecordingState.record || captureProvider.isPhoneMicPaused) { + if (_phoneOwnsCapture(captureProvider)) { // The phone recording is this button's own: finish it (processed, except Transcribe Later, // whose local file is finalized on stop), then any pendant it paused resumes. - await captureProvider.finishCapture(); + if (_stopping) return; + setState(() => _stopping = true); + try { + await captureProvider.finishCapture(); + } finally { + if (mounted) setState(() => _stopping = false); + } PlatformManager.instance.analytics.phoneMicRecordingStopped(); if (context.mounted) _maybeShowOptionsTip(context); return; @@ -354,104 +372,125 @@ class _HomeRecordButtonState extends State { } return; } - if (context.mounted) { - routeToPage(context, ConversationCapturingPage(topConversationId: captureProvider.topConversationId)); + if (context.mounted) _openTranscript(context); + } + + final GlobalKey _pillKey = GlobalKey(); + + /// The live transcript, grown out of this pill. + void _openTranscript(BuildContext context) { + final capture = context.read(); + pushGrowingFrom( + context, + from: _pillKey, + page: ConversationCapturingPage(topConversationId: capture.topConversationId), + ); + } + + /// The capturing pendant's pill: its live transcript, or the call while an Omi call owns it. + void _openDevice(BuildContext context) { + OmiHaptics.selection(); + if (_callInProgress(context)) { + routeToPage(context, const ActiveCallPage()); + return; } + final capture = context.read(); + PlatformManager.instance.analytics.liveTranscriptCardClicked( + hasSegments: capture.segments.isNotEmpty, + hasPhotos: capture.photos.isNotEmpty, + segmentCount: capture.segments.length, + photoCount: capture.photos.length, + ); + _openTranscript(context); } @override Widget build(BuildContext context) { - return Consumer( - builder: (context, captureProvider, _) { - // The phone recording is this button's own (live or paused); anything else is idle here. - final isRecording = captureProvider.recordingState == RecordingState.record || captureProvider.isPhoneMicPaused; - final isInitialising = captureProvider.recordingState == RecordingState.initialising; - // The options sheet only adds a phone call; with calls hidden there is nothing else to offer. - final canShowOptions = PhoneCallsFeature.visible && !isRecording && !isInitialising; - final l10n = context.l10n; - final circle = Semantics( - button: true, - label: isRecording ? l10n.stopRecording : l10n.startRecording, - enabled: !isInitialising, - // The child's gestures are excluded below, so the actions (tap, and long-press for the - // record options) are declared here where assistive tech can reach them. - onTap: isInitialising ? null : () => _startRecording(context), - onLongPress: canShowOptions ? () => _showRecordOptions(context) : null, - onLongPressHint: canShowOptions ? l10n.moreOptions : null, - excludeSemantics: true, - child: GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: () => _startRecording(context), - onLongPress: canShowOptions ? () => _showRecordOptions(context) : null, - // "Record with this phone": a neutral circle with a white dot, a stop square while the - // phone records. The ⌄ badge opens the other ways to record. - child: Container( - width: 62, - height: 62, - alignment: Alignment.center, - decoration: BoxDecoration( - color: OmiColors.surface1, - shape: BoxShape.circle, - border: Border.all(color: OmiColors.border, width: 1), - ), - child: isRecording - ? Container( - width: 18, - height: 18, - decoration: BoxDecoration( - color: OmiColors.textPrimary, - borderRadius: _stopGlyphRadius, - ), - ) - : isInitialising - ? const OmiSpinner(size: OmiSpinnerSize.small) - : Container( - width: 20, - height: 20, - decoration: BoxDecoration(color: OmiColors.textPrimary, shape: BoxShape.circle), - ), - ), - ), - ); - // The badge is its own control (and its own accessibility node) beside the circle's. - return SizedBox( - width: 62, - height: 62, - child: Stack(clipBehavior: Clip.none, children: [ - circle, - if (canShowOptions) - Positioned( - right: 0, - bottom: 0, - child: Semantics( - button: true, - label: l10n.moreWaysToRecord, - child: GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: () => _showRecordOptions(context), - // A 30pt hit area around the 22pt badge, inside the button's 62pt box (a Stack - // does not hit-test outside its bounds, and 44pt would cover the circle's - // centre). Screen readers also get the options as the circle's long-press action. - child: Padding( - padding: const EdgeInsets.all(4), - child: Container( - width: 22, - height: 22, - alignment: Alignment.center, - decoration: BoxDecoration( - color: OmiColors.surface3, - shape: BoxShape.circle, - border: Border.all(color: OmiColors.surface0, width: 2), - ), - child: Icon(Icons.keyboard_arrow_down_rounded, size: 16, color: OmiColors.textPrimary), - ), - ), - ), - ), - ), - ]), - ); - }, + final l10n = context.l10n; + final capture = context.watch(); + final device = context.select((d) => d.connectedDevice); + final starting = capture.recordingState == RecordingState.initialising; + // The pendant stands in for "New" only while it is capturing live; connected but idle (or + // saving for Transcribe Later), the phone is still one tap away and the header shows the device. + final pendantCapturing = device != null && _pendantHasCapture(capture); + // A phone recording that is starting already looks live, so the pill changes the moment it is + // tapped instead of after the microphone is up (Home shows no separate card meanwhile). + final phoneRecording = _phoneOwnsCapture(capture) || (starting && !pendantCapturing); + // The options sheet only adds a phone call; with calls hidden there is nothing else to offer. + final canShowOptions = PhoneCallsFeature.visible && !phoneRecording && !starting; + + final Widget pill; + if (phoneRecording) { + void stop() => _startRecording(context); + final paused = capture.isPhoneMicPaused || capture.isPaused; + final status = paused ? l10n.paused : l10n.listening; + // Transcribe Later has no live transcript to open: the whole pill stops it. + final batch = capture.isPhoneMicBatchRecording; + pill = HomeControlSurface( + key: const ValueKey('home_record_pill_stop'), + semanticsLabel: batch ? l10n.stopRecording : status, + semanticsHint: batch ? null : l10n.liveTranscript, + semanticsActions: batch || _stopping ? null : {CustomSemanticsAction(label: l10n.stopRecording): stop}, + onTap: _stopping + ? null + : batch + ? stop + : () { + OmiHaptics.selection(); + _openTranscript(context); + }, + child: HomeLivePillContent( + status: status, + lastLine: batch ? null : capture.segments.lastOrNull?.text, + live: !paused, + stopping: _stopping, + onStop: stop, + ), + ); + } else if (pendantCapturing && !starting) { + final paused = capture.isPaused; + final status = paused ? l10n.paused : l10n.listening; + pill = HomeControlSurface( + key: const ValueKey('home_record_pill_device'), + semanticsLabel: '${device.name}, $status', + semanticsHint: l10n.liveTranscript, + onTap: () => _openDevice(context), + // Holding it offers the phone instead (or a call), as tapping record used to. + onLongPress: () => _showPendantListening(context), + longPressHint: l10n.moreOptions, + child: HomeDevicePillContent( + device: device, + status: status, + live: !paused, + lastLine: capture.segments.lastOrNull?.text, + ), + ); + } else { + pill = HomeControlSurface( + key: const ValueKey('home_record_pill_new'), + semanticsLabel: l10n.newRecording, + onTap: starting ? null : () => _startRecording(context), + onLongPress: canShowOptions ? () => _showRecordOptions(context) : null, + longPressHint: l10n.moreWaysToRecord, + child: HomeNewPillContent(starting: starting), + ); + } + + return KeyedSubtree( + key: _pillKey, + child: AnimatedSwitcher( + duration: OmiMotion.of(context).standard, + switchInCurve: Curves.easeOut, + switchOutCurve: Curves.easeIn, + // Cross-fade with a slight rise, so a change of state reads as the same control updating. + transitionBuilder: (child, animation) => FadeTransition( + opacity: animation, + child: ScaleTransition(scale: Tween(begin: 0.98, end: 1.0).animate(animation), child: child), + ), + layoutBuilder: (current, previous) => + Stack(alignment: Alignment.center, children: [...previous, if (current != null) current]), + child: pill, + ), ); } } diff --git a/app/lib/pages/home/widgets/grow_route.dart b/app/lib/pages/home/widgets/grow_route.dart new file mode 100644 index 00000000000..be132f370ec --- /dev/null +++ b/app/lib/pages/home/widgets/grow_route.dart @@ -0,0 +1,157 @@ +import 'dart:ui' show lerpDouble; + +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; + +import 'package:omi/ui/ui.dart'; + +/// Opens [page] by growing it out of the control that asked for it: the page's shape starts as +/// that control (its rect and pill corners, in [fromColor]) and widens to the full screen while +/// the page fades in over it. A back tap runs it back into the control; the iOS edge swipe still +/// slides it away like any page. Under Reduce Motion it is a plain fade. +/// +/// The page is laid out at full size the whole time and only clipped, so nothing inside it +/// reflows while it grows. +Future pushGrowingFrom(BuildContext context, {required GlobalKey from, required Widget page, Color? fromColor}) { + final box = from.currentContext?.findRenderObject() as RenderBox?; + final origin = box != null && box.hasSize ? box.localToGlobal(Offset.zero) & box.size : null; + return Navigator.of(context).push( + _GrowRoute( + page: page, + origin: origin, + fromColor: fromColor ?? OmiColors.surface1, + reduceMotion: OmiMotion.of(context).standard == Duration.zero, + ), + ); +} + +class _GrowRoute extends PageRoute with CupertinoRouteTransitionMixin { + _GrowRoute({required this.page, required this.origin, required this.fromColor, required this.reduceMotion}); + + final Widget page; + final Rect? origin; + final Color fromColor; + final bool reduceMotion; + + /// Set once an edge swipe starts, so the rest of that dismissal stays a slide instead of + /// switching to the shrink halfway through. + bool _swiping = false; + + @override + Widget buildContent(BuildContext context) => page; + + @override + String? get title => null; + + @override + bool get maintainState => true; + + @override + Duration get transitionDuration => reduceMotion ? OmiMotion.quickDuration : const Duration(milliseconds: 460); + + @override + Duration get reverseTransitionDuration => reduceMotion ? OmiMotion.quickDuration : const Duration(milliseconds: 380); + + @override + Widget buildTransitions( + BuildContext context, + Animation animation, + Animation secondaryAnimation, + Widget child, + ) { + if (popGestureInProgress) _swiping = true; + if (animation.status == AnimationStatus.completed) _swiping = false; + // One widget structure in every mode, so the page keeps its state when a swipe takes over: + // the Cupertino layer (back swipe, parallax under later pages) holds still until a swipe + // starts, and the grow layer steps aside while one runs. + return _Grow( + animation: animation, + origin: origin, + fromColor: fromColor, + mode: _swiping + ? _GrowMode.off + : reduceMotion || origin == null + ? _GrowMode.fade + : _GrowMode.grow, + child: super.buildTransitions( + context, + _swiping ? animation : kAlwaysCompleteAnimation, + secondaryAnimation, + child, + ), + ); + } +} + +enum _GrowMode { grow, fade, off } + +class _Grow extends StatelessWidget { + const _Grow({ + required this.animation, + required this.origin, + required this.fromColor, + required this.mode, + required this.child, + }); + + final Animation animation; + final Rect? origin; + final Color fromColor; + final _GrowMode mode; + final Widget child; + + @override + Widget build(BuildContext context) { + final screen = Offset.zero & MediaQuery.sizeOf(context); + // The shape moves on an emphasized curve that leaves and lands softly (mirrored on the way + // back); the page fades in only once there is room to read it. + final shape = CurvedAnimation( + parent: animation, + curve: Curves.easeInOutCubicEmphasized, + reverseCurve: Curves.easeInOutCubicEmphasized.flipped, + ); + final content = mode == _GrowMode.grow + ? CurvedAnimation(parent: animation, curve: const Interval(0.3, 0.85, curve: Curves.easeOut)) + : mode == _GrowMode.fade + ? animation + : kAlwaysCompleteAnimation; + final pageColor = Theme.of(context).scaffoldBackgroundColor; + final faded = FadeTransition(opacity: content, child: child); + // The shape itself fades in over the first stretch (out over the last, on close), so the real + // pill underneath shows through where the two overlap instead of a blank copy of it. + final handoff = mode == _GrowMode.grow + ? CurvedAnimation(parent: animation, curve: const Interval(0, 0.18, curve: Curves.easeOut)) + : kAlwaysCompleteAnimation; + final shaped = AnimatedBuilder( + animation: animation, + child: faded, + builder: (context, faded) { + final growing = mode == _GrowMode.grow; + final t = growing ? shape.value : 1.0; + final from = origin ?? screen; + final rect = Rect.lerp(from, screen, t)!; + final radius = lerpDouble(from.shortestSide / 2, 0, t)!; + return ClipRRect( + clipper: _RectClipper(RRect.fromRectAndRadius(rect, Radius.circular(radius))), + child: ColoredBox( + color: growing ? Color.lerp(fromColor, pageColor, content.value)! : Colors.transparent, + child: faded, + ), + ); + }, + ); + return FadeTransition(opacity: handoff, child: shaped); + } +} + +class _RectClipper extends CustomClipper { + const _RectClipper(this.rrect); + + final RRect rrect; + + @override + RRect getClip(Size size) => rrect; + + @override + bool shouldReclip(_RectClipper oldClipper) => oldClipper.rrect != rrect; +} diff --git a/app/lib/pages/home/widgets/home_bottom_controls.dart b/app/lib/pages/home/widgets/home_bottom_controls.dart new file mode 100644 index 00000000000..f052925dbeb --- /dev/null +++ b/app/lib/pages/home/widgets/home_bottom_controls.dart @@ -0,0 +1,423 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/semantics.dart'; + +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; + +import 'package:omi/backend/schema/bt_device/bt_device.dart'; +import 'package:omi/ui/ui.dart'; +import 'package:omi/utils/device.dart'; +import 'package:omi/utils/l10n_extensions.dart'; +import 'package:omi/widgets/home_bottom_bar.dart'; + +/// The light surface Home's bottom controls share: white on the light canvas, a step up from the +/// page in dark mode. It presses in slightly under the finger. +class HomeControlSurface extends StatefulWidget { + const HomeControlSurface({ + super.key, + required this.child, + required this.semanticsLabel, + this.semanticsHint, + this.onTap, + this.onLongPress, + this.longPressHint, + this.width, + this.semanticsActions, + }); + + final Widget child; + final String semanticsLabel; + final String? semanticsHint; + final VoidCallback? onTap; + final VoidCallback? onLongPress; + final String? longPressHint; + + /// Fixed width (a circle); null fills the space it is given (the pill). + final double? width; + + /// Actions of controls inside the surface, which its single label otherwise hides (the live + /// pill's stop button). + final Map? semanticsActions; + + // White on the canvas like Home's cards, lifted by a soft, wide shadow because the controls + // float over the list (a hairline would read as a form field). + static BoxDecoration decoration() => BoxDecoration( + color: OmiColors.surface1, + borderRadius: OmiRadius.pillAll, + boxShadow: [ + BoxShadow(color: Colors.black.withValues(alpha: 0.06), blurRadius: 24, offset: const Offset(0, 6)), + BoxShadow(color: Colors.black.withValues(alpha: 0.04), blurRadius: 4, offset: const Offset(0, 1)), + ], + ); + + @override + State createState() => _HomeControlSurfaceState(); +} + +class _HomeControlSurfaceState extends State { + bool _pressed = false; + + void _press(bool down) { + if (_pressed != down) setState(() => _pressed = down); + } + + @override + Widget build(BuildContext context) { + final motion = OmiMotion.of(context); + return Semantics( + container: true, + button: true, + enabled: widget.onTap != null, + label: widget.semanticsLabel, + hint: widget.semanticsHint, + onTap: widget.onTap, + onLongPress: widget.onLongPress, + onLongPressHint: widget.onLongPress == null ? null : widget.longPressHint, + customSemanticsActions: widget.semanticsActions, + excludeSemantics: true, + child: GestureDetector( + behavior: HitTestBehavior.opaque, + onTapDown: widget.onTap == null ? null : (_) => _press(true), + onTapUp: (_) => _press(false), + onTapCancel: () => _press(false), + onTap: widget.onTap, + onLongPress: widget.onLongPress, + child: AnimatedScale( + scale: _pressed ? 0.96 : 1, + duration: motion.quick, + curve: Curves.easeOut, + child: Container( + width: widget.width, + height: kHomeChatBarHeight, + decoration: HomeControlSurface.decoration(), + child: widget.child, + ), + ), + ), + ); + } +} + +/// Ask Omi: a quiet circle at the left of the record pill. Tap to type, hold to talk. +class HomeChatButton extends StatelessWidget { + const HomeChatButton({super.key, required this.onTap, required this.onLongPress}); + + final VoidCallback onTap; + final VoidCallback onLongPress; + + @override + Widget build(BuildContext context) { + final l10n = context.l10n; + return HomeControlSurface( + key: const ValueKey('home_ask_omi_button'), + width: kHomeChatBarHeight, + semanticsLabel: l10n.askOmi, + onTap: onTap, + onLongPress: onLongPress, + longPressHint: l10n.voiceMode, + child: Center(child: FaIcon(FontAwesomeIcons.solidComment, size: 22, color: OmiColors.textPrimary)), + ); + } +} + +/// A 28pt ink disc holding a glyph: the pill's one point of contrast, so it reads as the +/// primary control without turning the whole bar dark. +class _Disc extends StatelessWidget { + const _Disc({required this.child, this.color}); + + final Widget child; + final Color? color; + + @override + Widget build(BuildContext context) { + return Container( + width: 28, + height: 28, + alignment: Alignment.center, + decoration: BoxDecoration(color: color ?? OmiColors.accent, shape: BoxShape.circle), + child: child, + ); + } +} + +TextStyle _pillLabel() => OmiType.headline.copyWith(fontWeight: FontWeight.w500, color: OmiColors.textPrimary); + +/// "+ New Recording": start a recording with this phone. While the microphone opens, a spinner +/// takes the plus's place. +class HomeNewPillContent extends StatelessWidget { + const HomeNewPillContent({super.key, required this.starting}); + + final bool starting; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: OmiSpacing.md), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SizedBox( + width: 24, + height: 24, + child: Center( + child: starting + ? OmiSpinner(size: OmiSpinnerSize.small, color: OmiColors.textPrimary) + : Icon(Icons.add_rounded, size: 26, color: OmiColors.textPrimary), + ), + ), + const SizedBox(width: 8), + Flexible( + child: Text( + context.l10n.homeNewRecordingButton, + style: _pillLabel(), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + // Optical centre: the plus is lighter than the words, so the pair sits a hair right. + const SizedBox(width: 4), + ], + ), + ); + } +} + +/// The phone recording, in place of "New": a breathing dot, what it is doing and the newest words +/// (the pill opens the live transcript), with its own stop button at the end. While the recording +/// finishes, the square becomes a spinner. +class HomeLivePillContent extends StatelessWidget { + const HomeLivePillContent({ + super.key, + required this.status, + required this.onStop, + this.lastLine, + this.live = true, + this.stopping = false, + }); + + final String status; + final String? lastLine; + final bool live; + final bool stopping; + final VoidCallback? onStop; + + @override + Widget build(BuildContext context) { + return Row( + children: [ + const SizedBox(width: OmiSpacing.md + 2), + HomeLiveDot(live: live), + const SizedBox(width: OmiSpacing.sm), + Expanded(child: _StatusAndWords(status: status, lastLine: lastLine)), + Semantics( + button: true, + label: context.l10n.stopRecording, + child: GestureDetector( + key: const ValueKey('home_record_pill_stop_button'), + behavior: HitTestBehavior.opaque, + onTap: stopping ? null : onStop, + child: SizedBox( + width: kHomeChatBarHeight, + height: kHomeChatBarHeight, + child: Center( + child: _Disc( + color: OmiColors.danger, + child: stopping + ? const OmiSpinner(size: OmiSpinnerSize.small, color: Colors.white) + : Container( + width: 10, + height: 10, + decoration: const BoxDecoration( + color: Colors.white, + borderRadius: + BorderRadius.all(Radius.circular(2.5)), // omi-ux-allow: radius-literal -- a glyph + ), + ), + ), + ), + ), + ), + ), + ], + ); + } +} + +/// One line of status, or, once words arrive, the status small above the newest words. The words +/// run in from the right and fade out at the left, so the latest is always what shows. +class _StatusAndWords extends StatelessWidget { + const _StatusAndWords({required this.status, this.lastLine, this.title}); + + final String status; + final String? lastLine; + + /// The device name, before the status. + final String? title; + + @override + Widget build(BuildContext context) { + final words = lastLine?.trim() ?? ''; + final secondary = OmiType.headline.copyWith(fontWeight: FontWeight.w400, color: OmiColors.textSecondary); + if (words.isEmpty) { + return Text.rich( + TextSpan(children: [ + if (title != null) TextSpan(text: '$title ', style: _pillLabel()), + TextSpan(text: status, style: title == null ? _pillLabel() : secondary), + ]), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ); + } + return Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title == null ? status : '$title · $status', + style: OmiType.caption.copyWith(color: OmiColors.textSecondary), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + SizedBox( + height: 20, + child: ShaderMask( + shaderCallback: (rect) => const LinearGradient( + colors: [Colors.transparent, Colors.black], + stops: [0, 0.16], + ).createShader(rect), + blendMode: BlendMode.dstIn, + child: ClipRect( + child: OverflowBox( + maxWidth: double.infinity, + alignment: Alignment.centerRight, + child: Text( + words, + key: const ValueKey('home_pill_last_words'), + style: OmiType.callout.copyWith(color: OmiColors.textPrimary), + maxLines: 1, + softWrap: false, + ), + ), + ), + ), + ), + ], + ); + } +} + +/// The connected device in place of "New": its picture with a live dot, its name and what it is +/// doing. (Its battery is in the header pill.) +class HomeDevicePillContent extends StatelessWidget { + const HomeDevicePillContent({ + super.key, + required this.device, + required this.status, + required this.live, + this.lastLine, + }); + + final BtDevice device; + final String status; + final String? lastLine; + + /// Capturing now: the dot is red and breathes; otherwise it is a still grey dot. + final bool live; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: OmiSpacing.md), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SizedBox( + width: 28, + height: 28, + child: Stack( + clipBehavior: Clip.none, + children: [ + Positioned.fill( + child: Image.asset( + DeviceUtils.getDeviceImagePath( + deviceType: device.type, + modelNumber: device.modelNumber, + deviceName: device.name, + ), + fit: BoxFit.contain, + ), + ), + Positioned(right: -2, bottom: -1, child: HomeLiveDot(live: live)), + ], + ), + ), + const SizedBox(width: OmiSpacing.sm), + Flexible(child: _StatusAndWords(title: device.name, status: status, lastLine: lastLine)), + ], + ), + ); + } +} + +/// A 10pt status dot ringed in the pill's colour so it reads on top of the device picture. Live: +/// red, breathing (still under Reduce Motion). Idle: grey. +class HomeLiveDot extends StatefulWidget { + const HomeLiveDot({super.key, required this.live}); + + final bool live; + + @override + State createState() => _HomeLiveDotState(); +} + +class _HomeLiveDotState extends State with SingleTickerProviderStateMixin { + late final AnimationController _breath = + AnimationController(vsync: this, duration: const Duration(milliseconds: 1400), lowerBound: 0.4, value: 1); + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + _sync(); + } + + @override + void didUpdateWidget(HomeLiveDot oldWidget) { + super.didUpdateWidget(oldWidget); + _sync(); + } + + void _sync() { + final breathe = widget.live && OmiMotion.of(context).standard != Duration.zero; + if (breathe && !_breath.isAnimating) { + _breath.repeat(reverse: true); + } else if (!breathe) { + _breath + ..stop() + ..value = 1; + } + } + + @override + void dispose() { + _breath.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Container( + width: 12, + height: 12, + padding: const EdgeInsets.all(2), + decoration: BoxDecoration(color: OmiColors.surface1, shape: BoxShape.circle), + child: FadeTransition( + opacity: _breath, + child: DecoratedBox( + decoration: BoxDecoration( + color: widget.live ? OmiColors.danger : OmiColors.textTertiary, + shape: BoxShape.circle, + ), + ), + ), + ); + } +} diff --git a/app/lib/pages/home/widgets/home_page_stack.dart b/app/lib/pages/home/widgets/home_page_stack.dart new file mode 100644 index 00000000000..db9a832cfb6 --- /dev/null +++ b/app/lib/pages/home/widgets/home_page_stack.dart @@ -0,0 +1,60 @@ +import 'package:flutter/material.dart'; + +import 'package:omi/ui/ui.dart'; + +/// Home and Tasks as an [IndexedStack] (both keep their state and scroll position), with the +/// page that comes in fading and sliding a short way from the side of its tab: Tasks from the +/// right, Home from the left. Reduce Motion switches without the animation. +class HomePageStack extends StatefulWidget { + const HomePageStack({super.key, required this.index, required this.children}); + + final int index; + final List children; + + @override + State createState() => _HomePageStackState(); +} + +class _HomePageStackState extends State with SingleTickerProviderStateMixin { + /// How far the incoming page travels, as a fraction of its width. + static const _travel = 0.06; + + late final AnimationController _controller = AnimationController(vsync: this, value: 1); + late final Animation _fade = CurvedAnimation(parent: _controller, curve: OmiMotion.emphasizedCurve); + late Animation _slide = const AlwaysStoppedAnimation(Offset.zero); + + @override + void didUpdateWidget(HomePageStack oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.index == oldWidget.index) return; + final fromRight = widget.index > oldWidget.index; + _slide = Tween(begin: Offset(fromRight ? _travel : -_travel, 0), end: Offset.zero).animate(_fade); + _controller.duration = OmiMotion.of(context).standard; + _controller.forward(from: 0); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + // Every child keeps the same wrapper types whether or not it is selected, so switching + // never rebuilds a page's subtree; only the selected one is driven by the animation. + return IndexedStack( + index: widget.index, + children: [ + for (var i = 0; i < widget.children.length; i++) + FadeTransition( + opacity: i == widget.index ? _fade : kAlwaysCompleteAnimation, + child: SlideTransition( + position: i == widget.index ? _slide : const AlwaysStoppedAnimation(Offset.zero), + child: widget.children[i], + ), + ), + ], + ); + } +} diff --git a/app/lib/pages/home/widgets/home_tab_switcher.dart b/app/lib/pages/home/widgets/home_tab_switcher.dart index 89a02be1c8d..488511bc91e 100644 --- a/app/lib/pages/home/widgets/home_tab_switcher.dart +++ b/app/lib/pages/home/widgets/home_tab_switcher.dart @@ -3,12 +3,21 @@ import 'package:flutter/services.dart'; import 'package:provider/provider.dart'; +import 'package:omi/backend/preferences.dart'; import 'package:omi/providers/home_provider.dart'; import 'package:omi/services/dev_controls/addressability_catalog.dart'; import 'package:omi/ui/ui.dart'; import 'package:omi/utils/l10n_extensions.dart'; import 'package:omi/utils/platform/platform_manager.dart'; +/// A dot on Tasks after onboarding saved the person's first to-dos, until they open Tasks. +abstract final class NewTasksDot { + static String _key(String uid) => 'home/newTasksDot/$uid'; + static bool get showing => SharedPreferencesUtil().getBool(_key(SharedPreferencesUtil().uid)); + static void show() => SharedPreferencesUtil().saveBool(_key(SharedPreferencesUtil().uid), true); + static void clear() => SharedPreferencesUtil().remove(_key(SharedPreferencesUtil().uid)); +} + /// Home and Tasks as two large words at the top of the shell; the selected one is in full ink, the /// other muted. It replaces the bottom tab bar. [trailing] holds the selected page's own actions. class HomeTabSwitcher extends StatelessWidget { @@ -32,7 +41,8 @@ class HomeTabSwitcher extends StatelessWidget { child: _tab(context, selected, HomeProvider.homeTab, l10n.home, 'Home', OmiKeys.homeTabHome), ), const SizedBox(width: OmiSpacing.sm), - _tab(context, selected, HomeProvider.tasksTab, l10n.tasks, 'Tasks', OmiKeys.homeTabTasks), + _tab(context, selected, HomeProvider.tasksTab, l10n.tasks, 'Tasks', OmiKeys.homeTabTasks, + dot: selected != HomeProvider.tasksTab && NewTasksDot.showing), const Spacer(), if (trailing != null) trailing!, ], @@ -41,9 +51,11 @@ class HomeTabSwitcher extends StatelessWidget { } /// [analyticsName] is the stable event name; [label] is what the reader sees and hears. - Widget _tab(BuildContext context, int selected, int index, String label, String analyticsName, Key key) { + Widget _tab(BuildContext context, int selected, int index, String label, String analyticsName, Key key, + {bool dot = false}) { final isSelected = selected == index; void select() { + if (index == HomeProvider.tasksTab) NewTasksDot.clear(); onTabTap(index, isSelected); primaryFocus?.unfocus(); WidgetsBinding.instance.addPostFrameCallback((_) { @@ -73,7 +85,20 @@ class HomeTabSwitcher extends StatelessWidget { curve: Curves.easeOut, style: OmiType.title2.copyWith( fontWeight: FontWeight.w700, color: isSelected ? OmiColors.textPrimary : OmiColors.textTertiary), - child: Text(label), + child: dot + ? Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(label), + Padding( + key: const Key('home_tasks_new_dot'), + padding: const EdgeInsets.only(left: 3, top: 4), + child: Icon(Icons.fiber_manual_record, color: OmiColors.danger, size: 9), + ), + ], + ) + : Text(label), ), ), ), diff --git a/app/lib/pages/memories/widgets/memory_item.dart b/app/lib/pages/memories/widgets/memory_item.dart index 89650734198..64cb7d4ee43 100644 --- a/app/lib/pages/memories/widgets/memory_item.dart +++ b/app/lib/pages/memories/widgets/memory_item.dart @@ -1,6 +1,5 @@ import 'dart:ui'; -import 'package:omi/utils/platform/platform_manager.dart'; import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; @@ -12,7 +11,8 @@ import 'package:omi/backend/schema/memory.dart'; import 'package:omi/pages/conversation_detail/conversation_detail_provider.dart'; import 'package:omi/pages/conversation_detail/page.dart'; import 'package:omi/services/client_device_service.dart'; -import 'package:omi/pages/settings/usage_page.dart'; +import 'package:omi/pages/settings/widgets/upgrade_paywall.dart'; +import 'package:omi/services/superwall_pilot.dart'; import 'package:omi/providers/conversation_provider.dart'; import 'package:omi/providers/memories_provider.dart'; import 'package:omi/providers/usage_provider.dart'; @@ -186,12 +186,11 @@ class MemoryItem extends StatelessWidget { showMemoryQuickEditSheet(context, memory, provider, readOnly: true); return; } - PlatformManager.instance.analytics.paywallOpened( - 'locked_memory', - currentPlan: - context.read().subscription?.subscription.plan.wireName ?? 'unknown', + openUpgradePaywall( + context, + placement: SuperwallPilot.lockedContentPlacement, + source: 'locked_memory', ); - routeToPage(context, const UsagePage(showUpgradeDialog: true, paywallSource: 'locked_memory')); return; }, child: Container( diff --git a/app/lib/pages/onboarding/activation/activation_capture.dart b/app/lib/pages/onboarding/activation/activation_capture.dart new file mode 100644 index 00000000000..877b0d5b12f --- /dev/null +++ b/app/lib/pages/onboarding/activation/activation_capture.dart @@ -0,0 +1,406 @@ +import 'dart:async'; +import 'dart:math' as math; +import 'dart:io' show File; +import 'dart:typed_data'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; + +import 'package:path_provider/path_provider.dart'; +import 'package:permission_handler/permission_handler.dart'; + +import 'package:provider/provider.dart'; + +import 'package:omi/backend/http/api/speech_profile.dart'; +import 'package:omi/backend/preferences.dart'; +import 'package:omi/backend/schema/conversation.dart'; +import 'package:omi/providers/capture_provider.dart'; +import 'package:omi/providers/conversation_provider.dart'; +import 'package:omi/providers/home_provider.dart'; +import 'package:omi/services/capture/optimistic_processing.dart'; +import 'package:omi/utils/enums.dart'; +import 'package:omi/utils/audio/wav_bytes.dart'; +import 'package:omi/utils/logger.dart'; +import 'package:omi/utils/platform/platform_manager.dart'; + +/// What the first-run dictation became: the conversation Omi wrote from it, or (when Omi could not +/// make one in time, or judged it not worth keeping) just the words the reader said. +@immutable +class ActivationResult { + const ActivationResult({ + required this.transcript, + this.emoji = '', + this.title = '', + this.overview = '', + this.tasks = const [], + this.notesPending = false, + }); + + final String transcript; + final String emoji; + final String title; + final String overview; + final List tasks; + + /// Omi had not finished its notes when the reader was moved on; they land on Home later. + final bool notesPending; + + /// Omi wrote notes from it; otherwise only [transcript] is shown. + bool get hasNotes => title.isNotEmpty || overview.isNotEmpty; +} + +/// How starting to listen went. +enum ActivationStart { + listening, + + /// The reader's Omi is not streaming and the phone microphone is not allowed yet: ask for it first. + needsMicrophone, + failed, +} + +/// The recording behind the first-run "Tell Omi about your day" step. +/// +/// Uses the app's own capture pipeline, so the dictation becomes a real conversation that is first +/// on Home: through the reader's Omi when it is already streaming, otherwise through the phone +/// microphone. Replaceable in tests. +abstract class ActivationCapture { + /// Whether the words come from the reader's Omi (true) or the phone microphone. + bool get usesDevice; + + /// What has been heard since [start], updated live. + ValueListenable get liveText; + + /// How loud the audio coming in is, 0–1, updated live: Omi visibly hears the person before any + /// words come back. + ValueListenable get level; + + /// Starts listening. + Future start(); + + /// Stops listening and waits (up to [timeout]) for Omi's notes. + Future finish({Duration timeout}); + + /// Stops listening without making notes. + Future cancel(); +} + +class PipelineActivationCapture implements ActivationCapture { + PipelineActivationCapture(BuildContext context, {this.expectDevice = false}) + : _capture = context.read(), + _conversations = context.read(), + _home = context.read(); + + /// The reader connected an Omi: wait for it to stream rather than taking over with the phone. + final bool expectDevice; + + final CaptureProvider _capture; + final ConversationProvider _conversations; + final HomeProvider _home; + final ValueNotifier _live = ValueNotifier(''); + final ValueNotifier _level = ValueNotifier(0); + int _startIndex = 0; + bool _device = false; + bool _phoneStarted = false; + bool _listening = false; + + /// Set by [cancel]. [start] checks it after each wait, so a Skip while it is still starting does + /// not leave the phone microphone recording behind the next screen. + bool _cancelled = false; + + /// When the dictation began, to tell its conversation from the audio before it. + DateTime? _listeningSince; + + /// The phone dictation's own audio, kept to enroll the reader's voice (the old voice step did this; + /// without it Omi cannot tell the reader apart from the people they talk to). + final BytesBuilder _voice = BytesBuilder(copy: false); + + // 16 kHz mono PCM16. The server takes 5–180 s; a few sentences is plenty. + static const _bytesPerSecond = 16000 * 2; + static const _minVoiceSeconds = 6; + + /// Words the reader must have said for the audio to be worth a voiceprint: silence, a TV or + /// someone else talking makes few or no words of the reader's own. + static const _minVoiceWords = 10; + static const _maxVoiceSeconds = 170; + + @override + bool get usesDevice => _device; + + @override + ValueListenable get liveText => _live; + + @override + ValueListenable get level => _level; + + /// Eases toward [target] so the meter moves with speech without flickering. + void _meter(double target) { + final next = _level.value * 0.6 + target.clamp(0.0, 1.0) * 0.4; + if ((next - _level.value).abs() > 0.01) _level.value = next; + } + + /// Loudness of 16-bit little-endian PCM: speech sits roughly between 500 and 6000 RMS. + static double _pcmLevel(List pcm) { + var sum = 0.0; + var n = 0; + for (var i = 0; i + 1 < pcm.length; i += 2) { + var v = pcm[i] | (pcm[i + 1] << 8); + if (v >= 0x8000) v -= 0x10000; + sum += v * v; + n++; + } + if (n == 0) return 0; + return ((math.sqrt(sum / n) - 300) / 4000).clamp(0.0, 1.0); + } + + /// The pendant's packets are compressed: louder speech makes bigger packets, silence small ones. + static double _packetLevel(List packet) => ((packet.length - 30) / 110).clamp(0.0, 1.0); + + void _onCapture() { + if (!_listening) return; + final segments = _capture.segments; + // Fewer segments than when we started means the conversation was reset: all of them are new. + if (segments.length < _startIndex) _startIndex = 0; + final start = _startIndex; + _live.value = segments.skip(start).map((s) => s.text.trim()).where((t) => t.isNotEmpty).join(' '); + } + + @override + Future start() async { + try { + // Live words need the streaming transport, not "transcribe later". + await _capture.suspendBatchModeForOnboarding(); + // A just-connected Omi can take a moment to start streaming. + for (var waited = 0; expectDevice && !_isDeviceRecording && waited < 5000; waited += 250) { + await Future.delayed(const Duration(milliseconds: 250)); + if (_cancelled) return ActivationStart.failed; + } + if (_cancelled) return ActivationStart.failed; + _device = _isDeviceRecording; + if (_device) { + _capture.deviceAudioTap = (packet) => _meter(_packetLevel(packet)); + // The Omi has been listening since it connected (unboxing, the screens before this one). + // Close that first, so the dictation is a conversation of its own. + if (_capture.segments.isNotEmpty) await _capture.forceProcessingCurrentConversation(); + } else { + if (!await Permission.microphone.isGranted) return ActivationStart.needsMicrophone; + if (_cancelled) return ActivationStart.failed; + _phoneStarted = true; + final keepVoice = !SharedPreferencesUtil().hasSpeakerProfile; + _capture.phoneMicAudioTap = (pcm) { + _meter(_pcmLevel(pcm)); + if (keepVoice && _voice.length < _maxVoiceSeconds * _bytesPerSecond) _voice.add(pcm); + }; + await _capture.streamRecording(); + // Skipped while the microphone was starting: cancel()'s stop may have run before it was on, + // so stop it again now that it is. + if (_cancelled) { + _phoneStarted = true; + await _stopPhone(); + return ActivationStart.failed; + } + } + if (_cancelled) return ActivationStart.failed; + _startIndex = _capture.segments.length; + _listeningSince = DateTime.now(); + _listening = true; + _capture.addListener(_onCapture); + return ActivationStart.listening; + } catch (e) { + Logger.debug('Activation capture could not start: $e'); + return ActivationStart.failed; + } + } + + bool get _isDeviceRecording => _capture.recordingState == RecordingState.deviceRecord; + + static final _word = RegExp(r"[\p{L}\p{N}']+", unicode: true); + + static Iterable _wordsOf(String text) => _word.allMatches(text.toLowerCase()).map((m) => m.group(0)!); + + /// Whether [conversation] holds the dictation. The audio from before it (the unboxing, the screens + /// before this one) can become a conversation in the same moment, so: it must not have begun well + /// before the dictation, and its transcript must share most of the reader's first real words + /// (whole words; "a", "to" and the like match anything). + bool _isDictation(ServerConversation conversation, String heard) { + final began = conversation.startedAt; + final since = _listeningSince; + // Server and phone clocks can differ a little; the earlier audio began minutes before. + if (began != null && since != null && began.isBefore(since.subtract(const Duration(seconds: 20)))) { + return false; + } + final words = _wordsOf(heard).where((w) => w.length > 2).take(6).toList(); + if (words.isEmpty) return true; + final said = _wordsOf(conversation.transcriptSegments.map((s) => s.text).join(' ')).toSet(); + // Not loaded with the list: trust it only when its start time says it is the dictation. + if (said.isEmpty) return began != null && since != null; + return words.where(said.contains).length >= (words.length / 2).ceil(); + } + + void _stopListening() { + _listening = false; + _capture.removeListener(_onCapture); + _capture.phoneMicAudioTap = null; + _capture.deviceAudioTap = null; + _level.value = 0; + } + + /// Uploads the dictation as the reader's voice profile, in the background: it never holds up the + /// notes, and a failure only means the Home card or Settings asks for it later. + Future _enrollVoice(Uint8List pcm, String heard) async { + if (pcm.length < _minVoiceSeconds * _bytesPerSecond || SharedPreferencesUtil().hasSpeakerProfile) return; + if (_wordsOf(heard).length < _minVoiceWords) return; + File? file; + try { + final directory = await getTemporaryDirectory(); + file = await File('${directory.path}/omi-first-voice-${DateTime.now().millisecondsSinceEpoch}.wav') + .writeAsBytes(WavBytes.fromPcm(pcm, sampleRate: 16000, numChannels: 1).asBytes()); + if (await uploadProfile(file).timeout(const Duration(seconds: 45))) { + SharedPreferencesUtil().hasSpeakerProfile = true; + _home.setSpeakerProfile(true); + PlatformManager.instance.analytics.speechProfileUploadSucceeded(); + PlatformManager.instance.analytics.speechProfileEmbeddingStored(); + } + } on SpeechProfileUploadException catch (e) { + PlatformManager.instance.analytics.speechProfileUploadFailed( + reason: 'http_${e.statusCode}', + statusCode: e.statusCode, + ); + } catch (e) { + Logger.debug('Activation voice enrollment failed: $e'); + PlatformManager.instance.analytics.speechProfileUploadFailed(reason: 'activation_upload_failed'); + } finally { + if (file != null && await file.exists()) await file.delete(); + } + } + + @override + Future finish({Duration timeout = const Duration(seconds: 30)}) async { + final transcript = _live.value; + _stopListening(); + final known = _conversations.conversations.map((c) => c.id).toSet(); + final created = Completer(); + void watch() { + for (final conversation in _conversations.conversations) { + if (conversation.id == OptimisticProcessingPlaceholder.id || known.contains(conversation.id)) continue; + // The audio from before the dictation may land here too; only the dictation's own counts. + if (!_isDictation(conversation, transcript)) { + known.add(conversation.id); + continue; + } + if (!created.isCompleted) created.complete(conversation); + return; + } + } + + _conversations.addListener(watch); + try { + if (_phoneStarted) { + // If an Omi is connected but was not streaming, give it back the recording. + await _capture.stopStreamRecording(resumeHandedOffPendant: expectDevice); + _phoneStarted = false; + } + unawaited(_enrollVoice(_voice.takeBytes(), transcript)); + // Ends the conversation now instead of after the usual silence, and adds it to Home. + await _capture.forceProcessingCurrentConversation(); + final conversation = await created.future.timeout(timeout); + final structured = conversation.structured; + if (conversation.discarded) return ActivationResult(transcript: transcript); + return ActivationResult( + transcript: transcript, + emoji: '${structured.getEmoji()}', + title: structured.title.trim(), + overview: structured.overview.trim(), + tasks: structured.actionItems + .where((a) => !a.deleted) + .map((a) => a.description.trim()) + .where((d) => d.isNotEmpty) + .take(4) + .toList(), + ); + } catch (e) { + Logger.debug('Activation notes not ready: $e'); + return ActivationResult(transcript: transcript); + } finally { + _conversations.removeListener(watch); + } + } + + @override + Future cancel() async { + _cancelled = true; + _stopListening(); + _voice.clear(); + await _stopPhone(); + } + + Future _stopPhone() async { + if (!_phoneStarted) return; + _phoneStarted = false; + try { + await _capture.stopStreamRecording(resumeHandedOffPendant: expectDevice); + } catch (_) {} + } +} + +/// Debug-only stand-in for local servers without speech-to-text: "hears" [script] word by word and +/// returns sample notes, so the activation can be reviewed end to end. Turn on with +/// `--dart-define=OMI_ACTIVATION_DEMO=1`, or `=device` to also act as if an Omi were connected +/// (simulators have no Bluetooth). Never active in release builds. +class DemoActivationCapture implements ActivationCapture { + DemoActivationCapture({required this.script, required this.result}); + + static const _flag = String.fromEnvironment('OMI_ACTIVATION_DEMO'); + static bool get enabled => kDebugMode && (_flag == '1' || _flag == 'device'); + + /// Review the device owner's path without a device. + static bool get pretendsDevice => kDebugMode && _flag == 'device'; + + final String script; + final ActivationResult result; + final ValueNotifier _live = ValueNotifier(''); + final ValueNotifier _level = ValueNotifier(0); + Timer? _timer; + int _beat = 0; + + @override + bool get usesDevice => pretendsDevice; + + @override + ValueListenable get liveText => _live; + + @override + ValueListenable get level => _level; + + @override + Future start() async { + final words = script.split(' '); + var shown = 0; + _timer = Timer.periodic(const Duration(milliseconds: 260), (timer) { + if (shown >= words.length) { + _level.value = 0; + return timer.cancel(); + } + shown++; + _live.value = words.take(shown).join(' '); + // A speaking rhythm for the meter. + _level.value = 0.35 + 0.5 * ((_beat++ * 7) % 10) / 10; + }); + return ActivationStart.listening; + } + + @override + Future finish({Duration timeout = const Duration(seconds: 30)}) async { + _timer?.cancel(); + await Future.delayed(const Duration(seconds: 2)); + return ActivationResult( + transcript: _live.value, + emoji: result.emoji, + title: result.title, + overview: result.overview, + tasks: result.tasks, + ); + } + + @override + Future cancel() async => _timer?.cancel(); +} diff --git a/app/lib/pages/onboarding/activation/activation_flow.dart b/app/lib/pages/onboarding/activation/activation_flow.dart new file mode 100644 index 00000000000..4eeb5b60951 --- /dev/null +++ b/app/lib/pages/onboarding/activation/activation_flow.dart @@ -0,0 +1,1296 @@ +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +import 'package:in_app_review/in_app_review.dart'; +import 'package:permission_handler/permission_handler.dart'; + +import 'package:omi/backend/preferences.dart'; +import 'package:omi/l10n/app_localizations.dart'; +import 'package:omi/pages/home/widgets/home_tab_switcher.dart'; +import 'package:omi/pages/onboarding/activation/activation_capture.dart'; +import 'package:omi/pages/onboarding/activation/activation_motion.dart'; +import 'package:omi/pages/onboarding/activation/activation_visuals.dart'; +import 'package:omi/ui/ui.dart'; +import 'package:omi/utils/l10n_extensions.dart'; +import 'package:omi/utils/platform/platform_manager.dart'; +import 'package:omi/widgets/scanning_ripple.dart'; +import 'package:omi/pages/onboarding/widgets/onboarding_card.dart'; + +/// Height of the row above every title: Skip sits in it on the dictation step. +const double _kTopRowHeight = 44; + +/// Space between that row and every title: low enough to feel centred, high enough that the title +/// stays clear of the system prompts (microphone, App Store rating) that open mid-screen. +const double _kHeadingGap = 76; + +/// The area under the card on dictation, "Summarizing…", the summary and the tasks. One height +/// for all four, so the card stays in the same place while the words turn into notes. +const double _kControlHeight = 172; + +/// The stages of the first-run activation, in order. Some are skipped: [microphone] with an Omi (or +/// when already allowed), [taskList] when Omi found no tasks. How to wear and use an Omi is the +/// device tutorial Home shows after onboarding. +enum ActivationStage { intro, microphone, dictate, writing, summary, taskList, congrats, rating } + +/// Shows people what Omi does with their own words, then hands over to the paywall and Home. +/// +/// Everyone sees "Let's try Omi." and then says a few +/// sentences. Omi turns them into a summary and to-dos (a real conversation, first on Home), then +/// come a congratulations screen, the rating ask and the paywall. Skipping the dictation shows a +/// welcome card on Home instead. +class ActivationFlow extends StatefulWidget { + const ActivationFlow({ + super.key, + required this.hasDevice, + required this.onFinished, + this.showPaywall, + this.capture, + this.microphoneGranted, + this.requestMicrophone, + this.requestReview, + this.notesDeadline = const Duration(seconds: 12), + }); + + /// The reader connected a wearable in onboarding: they dictate through it. + final bool hasDevice; + + /// Called once at the very end; Home follows. + /// Saves onboarding as done and leaves for Home; false when the save failed (the reader can retry). + final Future Function() onFinished; + + /// Shows the paywall after the rating ask, last before Home; completes when it is closed. + final Future Function()? showPaywall; + + /// Defaults to the app's capture pipeline; replaceable in tests. + final ActivationCapture Function(BuildContext context)? capture; + final Future Function()? microphoneGranted; + final Future Function()? requestMicrophone; + final Future Function()? requestReview; + + /// How long "Summarizing…" waits for Omi's notes. A few sentences usually take seconds; past this + /// the reader moves on with their own words, and the notes arrive on Home. + final Duration notesDeadline; + + /// Set when the reader skipped the dictation, so Home shows the welcome card. + static const welcomeCardKey = 'onboarding/showWelcomeCard'; + + @override + State createState() => _ActivationFlowState(); +} + +class _ActivationFlowState extends State { + ActivationStage _stage = ActivationStage.intro; + ActivationCapture? _capture; + + /// The connected wearable was not streaming, so this dictation uses the phone microphone. + bool _phoneInstead = false; + String _heard = ''; + ActivationResult? _result; + bool _finished = false; + bool _paywallShown = false; + + void _track(String step) => + PlatformManager.instance.analytics.track('Onboarding Activation Step', properties: {'step': step}); + + void _go(ActivationStage next) { + if (!mounted) return; + _track(next.name); + // Each visit to the dictation listens afresh. + if (next == ActivationStage.dictate) _capture = null; + // The to-dos are saved: Home marks Tasks with a dot until the person opens it. + if (next == ActivationStage.taskList) NewTasksDot.show(); + setState(() => _stage = next); + } + + /// The wearable was not streaming and the phone microphone is not allowed yet. + void _needsMicrophone() { + _phoneInstead = true; + _track('device_not_streaming'); + _go(ActivationStage.microphone); + } + + Future _afterIntro() async { + if (widget.hasDevice) return _go(ActivationStage.dictate); + final granted = await (widget.microphoneGranted ?? () => Permission.microphone.isGranted)(); + _go(granted ? ActivationStage.dictate : ActivationStage.microphone); + } + + Future _allowMicrophone() async { + final granted = await (widget.requestMicrophone ?? () async => (await Permission.microphone.request()).isGranted)(); + if (granted) { + OmiHaptics.light(); + _go(ActivationStage.dictate); + } else { + _skipped(); + } + } + + void _skipped() { + SharedPreferencesUtil().saveBool(ActivationFlow.welcomeCardKey, true); + _track('skipped'); + _go(ActivationStage.congrats); + } + + void _dictated(String heard, Future result) { + SharedPreferencesUtil().saveBool(ActivationFlow.welcomeCardKey, false); + _heard = heard; + _go(ActivationStage.writing); + // Keep "Summarizing…" up long enough to see the words being read, even when Omi is quick. + final notes = result.timeout( + widget.notesDeadline, + onTimeout: () => ActivationResult(transcript: heard, notesPending: true), + ); + Future.wait([notes, Future.delayed(const Duration(milliseconds: 1800))]).then((values) { + _result = values.first as ActivationResult; + _go(ActivationStage.summary); + }); + } + + ActivationCapture _newCapture(BuildContext context) { + if (widget.capture != null) return widget.capture!(context); + if (DemoActivationCapture.enabled) { + final l10n = context.l10n; + return DemoActivationCapture( + script: l10n.onboardingActDictateExample, + result: ActivationResult( + transcript: '', + emoji: '🥗', + title: l10n.onboardingActDemoTitle, + overview: l10n.onboardingActDemoSummary, + tasks: [l10n.onboardingActDemoTask1, l10n.onboardingActDemoTask2], + ), + ); + } + return PipelineActivationCapture(context, expectDevice: widget.hasDevice && !_phoneInstead); + } + + Future _afterRating() async { + if (_finished) return; + _finished = true; + if (!_paywallShown) { + _paywallShown = true; + await widget.showPaywall?.call(); + } + // A failed save (the wrapper says so) must not strand the reader: Continue tries again. + if (!await widget.onFinished()) _finished = false; + } + + @override + Widget build(BuildContext context) { + final l10n = context.l10n; + final Widget child = switch (_stage) { + ActivationStage.intro => _IntroLine( + key: const ValueKey('intro'), + text: l10n.onboardingActIntro, + onDone: _afterIntro, + ), + ActivationStage.microphone => _Stage( + key: const ValueKey('microphone'), + title: l10n.onboardingActMicTitle, + subtitle: _phoneInstead ? l10n.onboardingActMicFallbackBody : l10n.onboardingActMicBody, + body: const _MicVisual(), + bottom: _ContinueButton( + stage: 'microphone', + label: l10n.onboardingActMicAllow, + onPressed: _allowMicrophone, + ), + ), + ActivationStage.dictate => _DictationStep( + key: const ValueKey('dictate'), + capture: _capture ??= _newCapture(context), + onDone: _dictated, + onSkip: _skipped, + onNeedsMicrophone: _needsMicrophone, + throughOmi: widget.hasDevice && !_phoneInstead, + ), + ActivationStage.writing => _Stage( + key: const ValueKey('writing'), + title: l10n.onboardingActWriting, + // The dictation's subtitle keeps its space (unseen), so the card doesn't move between the two. + subtitle: + widget.hasDevice && !_phoneInstead ? l10n.onboardingActDictateBodyOmi : l10n.onboardingActDictateBody, + subtitleHidden: true, + body: CompactingCard(text: _heard, style: _wordsStyle), + bottom: const CondensingControl(), + bottomHeight: _kControlHeight, + ), + ActivationStage.summary => _Stage( + key: const ValueKey('summary'), + eyebrow: l10n.onboardingActRemembers, + title: _result!.hasNotes ? l10n.onboardingActNotesLabel : l10n.onboardingActYourWords, + body: SummaryReveal(child: _SummaryCard(result: _result!, heard: _heard)), + bottom: _ContinueButton( + stage: 'summary', + onPressed: () => _go( + _result!.hasNotes && _result!.tasks.isNotEmpty ? ActivationStage.taskList : ActivationStage.congrats, + ), + ), + bottomHeight: _kControlHeight, + ), + ActivationStage.taskList => _Stage( + key: const ValueKey('taskList'), + title: l10n.onboardingActTasksLabel, + body: _TasksCard(tasks: _result!.tasks), + bottom: _ContinueButton(stage: 'tasks', onPressed: () => _go(ActivationStage.congrats)), + bottomHeight: _kControlHeight, + ), + ActivationStage.congrats => + _Hooray(key: const ValueKey('congrats'), onContinue: () => _go(ActivationStage.rating)), + ActivationStage.rating => _RatingAsk( + key: const ValueKey('rating'), + requestReview: widget.requestReview ?? InAppReview.instance.requestReview, + onDone: _afterRating, + ), + }; + final reduce = OmiMotion.of(context).emphasized == Duration.zero; + return AnimatedSwitcher( + duration: reduce ? Duration.zero : const Duration(milliseconds: 450), + switchInCurve: Curves.easeOutCubic, + switchOutCurve: Curves.easeInCubic, + layoutBuilder: (current, previous) => + Stack(fit: StackFit.expand, children: [...previous, if (current != null) current]), + transitionBuilder: (child, animation) => FadeTransition(opacity: animation, child: child), + child: child, + ); + } +} + +/// Every titled activation screen. The title has one size and one height throughout, so moving from +/// step to step only changes what is under it. [bottomHeight] fixes the area under the body (so the +/// body sits in the same place on screens that share it); [bodyAtTop] puts the body right under +/// the title instead of in the middle. +class _Stage extends StatelessWidget { + const _Stage({ + super.key, + required this.title, + this.eyebrow, + this.subtitle, + this.subtitleHidden = false, + required this.body, + this.bottom, + this.topRight, + this.bottomHeight, + this.bodyAtTop = false, + }); + + final String title; + + /// A quiet line above the title. + final String? eyebrow; + final String? subtitle; + + /// Keeps the subtitle's space without showing it, so the body sits where it did on the step before. + final bool subtitleHidden; + final Widget body; + final Widget? bottom; + final Widget? topRight; + final double? bottomHeight; + final bool bodyAtTop; + + @override + Widget build(BuildContext context) { + return SafeArea( + child: Padding( + padding: const EdgeInsets.fromLTRB(OmiSpacing.xl, 0, OmiSpacing.xl, kOnboardingButtonBottomGap), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + SizedBox( + height: _kTopRowHeight, + child: Align(alignment: Alignment.centerRight, child: topRight), + ), + // The eyebrow takes its line out of the gap, so the title stays at the same height. + SizedBox(height: eyebrow == null ? _kHeadingGap : _kHeadingGap - 26), + if (eyebrow != null) + SizedBox( + height: 26, + child: Text( + eyebrow!, + textAlign: TextAlign.center, + style: OmiType.subhead.copyWith(color: OmiColors.textSecondary), + ), + ), + Semantics(header: true, child: Text(title, style: OmiType.title1, textAlign: TextAlign.center)), + if (subtitle != null) ...[ + const SizedBox(height: OmiSpacing.xs), + Opacity( + opacity: subtitleHidden ? 0 : 1, + child: Text( + subtitle!, + style: OmiType.subhead.copyWith(color: OmiColors.textSecondary), + textAlign: TextAlign.center, + ), + ), + ], + Expanded( + child: bodyAtTop + ? Padding( + padding: const EdgeInsets.only(top: OmiSpacing.md), + child: Align(alignment: Alignment.topCenter, child: body), + ) + : LayoutBuilder( + builder: (context, constraints) => SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints(minHeight: constraints.maxHeight), + child: Center(child: body), + ), + ), + ), + ), + if (bottom != null) + bottomHeight == null + ? Padding(padding: const EdgeInsets.only(top: OmiSpacing.md), child: bottom) + : SizedBox(height: bottomHeight, child: Align(alignment: Alignment.bottomCenter, child: bottom)), + ], + ), + ), + ); + } +} + +class _ContinueButton extends StatelessWidget { + const _ContinueButton({required this.stage, required this.onPressed, this.label}); + + final String stage; + final String? label; + final VoidCallback onPressed; + + @override + Widget build(BuildContext context) { + return OmiButton( + key: Key('activation_continue_$stage'), + label: label ?? context.l10n.continueButton, + expand: true, + onPressed: () { + OmiHaptics.selection(); + onPressed(); + }, + ); + } +} + +/// "Let's try Omi." Word by word, then moves on by itself. Nothing to tap. +class _IntroLine extends StatefulWidget { + const _IntroLine({super.key, required this.text, required this.onDone}); + + final String text; + final VoidCallback onDone; + + @override + State<_IntroLine> createState() => _IntroLineState(); +} + +class _IntroLineState extends State<_IntroLine> { + Timer? _timer; + + @override + void initState() { + super.initState(); + _timer = Timer(const Duration(milliseconds: 3000), widget.onDone); + } + + @override + void dispose() { + _timer?.cancel(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final words = widget.text.split(' '); + return Center( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: OmiSpacing.xl), + child: OneShot( + duration: const Duration(milliseconds: 1400), + builder: (context, t) => Wrap( + alignment: WrapAlignment.center, + children: [ + for (var i = 0; i < words.length; i++) + rise( + segment(t, i / (words.length + 1), (i + 2) / (words.length + 1)), + Text('${words[i]} ', style: OmiType.largeTitle), + distance: 14, + ), + ], + ), + ), + ), + ); + } +} + +/// The microphone ask: the dark round control and soft ripple of the listening screen that follows, +/// so allowing the microphone feels like the first step of talking to Omi. +class _MicVisual extends StatelessWidget { + const _MicVisual(); + + static const double _size = 180; + + @override + Widget build(BuildContext context) { + return OneShot( + duration: const Duration(milliseconds: 900), + builder: (context, t) => rise( + t, + SizedBox( + width: _size, + height: _size, + child: Stack( + alignment: Alignment.center, + children: [ + ScanningRippleWidget(isScanning: true, size: _size, color: OmiColors.textTertiary), + Container( + width: 112, + height: 112, + decoration: BoxDecoration(color: OmiColors.textPrimary.withValues(alpha: 0.06), shape: BoxShape.circle), + ), + Container( + width: 80, + height: 80, + decoration: BoxDecoration(color: OmiColors.accent, shape: BoxShape.circle), + child: Icon(Icons.mic_rounded, size: 36, color: OmiColors.onAccent), + ), + ], + ), + ), + distance: 16, + ), + ); + } +} + +/// The card that holds the reader's words, the same on the dictation and "Summarizing…" screens so +/// it reads as one card. +class _WordsCard extends StatelessWidget { + const _WordsCard({super.key, required this.child}); + + final Widget child; + + @override + Widget build(BuildContext context) => NoteCard(minHeight: 150, child: child); +} + +TextStyle get _wordsStyle => OmiType.body.copyWith(height: 1.45); + +/// "Tell Omi about your day": a card that fills with the reader's words, Skip (grey, top right) and +/// a round ✓ (done, enabled once Omi has heard something). +class _DictationStep extends StatefulWidget { + const _DictationStep({ + super.key, + required this.capture, + required this.onDone, + required this.onSkip, + required this.onNeedsMicrophone, + this.throughOmi = false, + }); + + final ActivationCapture capture; + final void Function(String heard, Future result) onDone; + final VoidCallback onSkip; + final VoidCallback onNeedsMicrophone; + + /// The reader dictates through their Omi, not the phone. + final bool throughOmi; + + @override + State<_DictationStep> createState() => _DictationStepState(); +} + +class _DictationStepState extends State<_DictationStep> { + bool _started = false; + bool _failed = false; + bool _leaving = false; + + /// Nothing heard for a while after listening started: say so, instead of listening in silence. + bool _quiet = false; + Timer? _quietTimer; + + /// Still nothing heard well after the hint: offer a plain way on instead of a grey ✓ and a corner + /// Skip (the microphone, the Omi or transcription may simply not be working right now). + bool _stuck = false; + Timer? _stuckTimer; + + @override + void initState() { + super.initState(); + widget.capture.liveText.addListener(_onWords); + // From the moment the step opens, so a start that hangs (waiting for the Omi, the microphone) + // still ends in Continue. + _stuckTimer = Timer(const Duration(seconds: 25), () { + if (mounted && widget.capture.liveText.value.trim().isEmpty) setState(() => _stuck = true); + }); + WidgetsBinding.instance.addPostFrameCallback((_) async { + final start = await widget.capture.start(); + if (!mounted) return; + if (start == ActivationStart.needsMicrophone) { + _leaving = true; + widget.onNeedsMicrophone(); + return; + } + setState(() { + _started = start == ActivationStart.listening; + _failed = !_started; + }); + if (_started) { + _quietTimer = Timer(const Duration(seconds: 12), () { + if (mounted && widget.capture.liveText.value.trim().isEmpty) setState(() => _quiet = true); + }); + } + }); + } + + bool _heardOnce = false; + + /// A light tap when the first words land: the moment Omi visibly understood. + void _onWords() { + if (_heardOnce || widget.capture.liveText.value.trim().isEmpty) return; + _heardOnce = true; + OmiHaptics.light(); + } + + @override + void dispose() { + widget.capture.liveText.removeListener(_onWords); + _quietTimer?.cancel(); + _stuckTimer?.cancel(); + if (!_leaving) unawaited(widget.capture.cancel()); + super.dispose(); + } + + void _done() { + if (_leaving) return; + _leaving = true; + OmiHaptics.medium(); + widget.onDone(widget.capture.liveText.value, widget.capture.finish()); + } + + Future _skip() async { + if (_leaving) return; + _leaving = true; + OmiHaptics.selection(); + await widget.capture.cancel(); + widget.onSkip(); + } + + @override + Widget build(BuildContext context) { + final l10n = context.l10n; + final source = widget.capture.usesDevice ? 'Omi' : l10n.onboardingActPhoneMic; + return ValueListenableBuilder( + valueListenable: widget.capture.liveText, + builder: (context, text, _) { + final heard = text.trim().isNotEmpty; + return _Stage( + title: l10n.onboardingActDictateTitle, + subtitle: widget.throughOmi ? l10n.onboardingActDictateBodyOmi : l10n.onboardingActDictateBody, + topRight: _QuietSkip(label: l10n.skip, onTap: _skip), + body: ReadAlongCard( + key: const Key('activation_dictation_card'), + script: l10n.onboardingActDictateExample, + live: widget.capture.liveText, + level: widget.capture.level, + listening: _started && !_failed, + levelDriven: !widget.capture.usesDevice, + style: _wordsStyle, + ), + // Words that arrive late still bring the ✓ back. + bottom: !heard && (_failed || _stuck) + ? _WayOn( + status: _failed + ? l10n.somethingWentWrong + : (widget.capture.usesDevice ? l10n.onboardingActNoAudioOmi : l10n.onboardingActNoAudioPhone), + onContinue: _skip, + ) + : _ListeningControl( + level: widget.capture.level, + heard: heard, + listening: _started, + failed: _failed, + status: _failed + ? l10n.somethingWentWrong + : heard + ? l10n.onboardingActDictateHint + : !_started + ? l10n.listening + : _quiet + ? (widget.capture.usesDevice + ? l10n.onboardingActNoAudioOmi + : l10n.onboardingActNoAudioPhone) + : '${l10n.listening} · $source', + onDone: _done, + ), + bottomHeight: _kControlHeight, + ); + }, + ); + } +} + +/// The dictation's bottom when listening failed or nothing came through: what went wrong, in one +/// quiet line, and Continue (the same as Skip: the hooray, with a welcome card left for Home). +class _WayOn extends StatelessWidget { + const _WayOn({required this.status, required this.onContinue}); + + final String status; + final VoidCallback onContinue; + + @override + Widget build(BuildContext context) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + status, + key: const Key('activation_dictation_status'), + textAlign: TextAlign.center, + style: OmiType.footnote.copyWith(color: OmiColors.textSecondary), + ), + const SizedBox(height: OmiSpacing.md), + _ContinueButton(stage: 'dictation', onPressed: onContinue), + ], + ); + } +} + +/// The bottom of the dictation step, in the style of Connect Omi: soft rings around one round ✓ +/// while Omi listens, and one quiet line under it. ✓ stays grey until Omi has heard something. +class _ListeningControl extends StatelessWidget { + const _ListeningControl({ + required this.heard, + required this.listening, + required this.failed, + required this.status, + required this.onDone, + required this.level, + }); + + /// How loud the person is right now: the ring around ✓ and the meter follow it, so it is + /// obvious Omi hears them even before words come back. + final ValueListenable level; + final bool heard; + final bool listening; + final bool failed; + final String status; + final VoidCallback onDone; + + @override + Widget build(BuildContext context) { + final l10n = context.l10n; + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox( + width: 140, + height: 140, + child: Stack( + alignment: Alignment.center, + children: [ + if (listening && !failed) + ScanningRippleWidget(isScanning: true, size: 140, color: OmiColors.textTertiary), + if (listening && !failed) + ValueListenableBuilder( + valueListenable: level, + builder: (context, v, _) => AnimatedContainer( + key: const Key('activation_voice_ring'), + duration: const Duration(milliseconds: 90), + curve: Curves.easeOut, + width: 76 + 60 * v, + height: 76 + 60 * v, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: OmiColors.textPrimary.withValues(alpha: 0.05 + 0.09 * v), + ), + ), + ), + Semantics( + button: true, + enabled: heard, + label: l10n.done, + excludeSemantics: true, + child: GestureDetector( + key: const Key('activation_dictation_done'), + onTap: heard ? onDone : null, + child: AnimatedContainer( + duration: const Duration(milliseconds: 250), + curve: Curves.easeOut, + width: 68, + height: 68, + decoration: BoxDecoration( + color: heard ? OmiColors.accent : OmiColors.surface3, + shape: BoxShape.circle, + ), + child: Icon( + Icons.check_rounded, + size: 32, + color: heard ? OmiColors.onAccent : OmiColors.textTertiary, + ), + ), + ), + ), + ], + ), + ), + const SizedBox(height: OmiSpacing.xs), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (listening && !failed) ...[ + _LevelMeter(level: level), + const SizedBox(width: OmiSpacing.xs), + ], + Flexible( + child: AnimatedSwitcher( + duration: const Duration(milliseconds: 250), + child: Text( + status, + key: ValueKey(status), + textAlign: TextAlign.center, + style: OmiType.footnote.copyWith(color: OmiColors.textSecondary), + ), + ), + ), + ], + ), + ], + ); + } +} + +/// Four small bars that move with the voice, next to "Listening". +class _LevelMeter extends StatelessWidget { + const _LevelMeter({required this.level}); + + final ValueListenable level; + + static const _shape = [0.55, 1.0, 0.75, 0.4]; + + @override + Widget build(BuildContext context) { + return ExcludeSemantics( + child: ValueListenableBuilder( + valueListenable: level, + builder: (context, v, _) => Row( + key: const Key('activation_level_meter'), + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + for (final s in _shape) + AnimatedContainer( + duration: const Duration(milliseconds: 90), + margin: const EdgeInsets.symmetric(horizontal: 1), + width: 3, + height: 3 + 11 * (v * s).clamp(0.0, 1.0), + decoration: BoxDecoration( + color: v > 0.05 ? OmiColors.textPrimary : OmiColors.textTertiary, + borderRadius: OmiRadius.pillAll, + ), + ), + ], + ), + ), + ); + } +} + +/// What Omi made of the words, built in front of the reader the way it appears on Home: the emoji +/// pops in, the title and "Just now" rise, a line draws across, the summary writes itself, and the +/// reader's own words wait underneath behind "Transcript". +/// +/// When Omi could not make notes, the card holds the reader's words alone. +class _SummaryCard extends StatefulWidget { + const _SummaryCard({required this.result, required this.heard}); + + final ActivationResult result; + final String heard; + + @override + State<_SummaryCard> createState() => _SummaryCardState(); +} + +class _SummaryCardState extends State<_SummaryCard> { + final List _beats = []; + bool _showTranscript = false; + + ActivationResult get _r => widget.result; + String get _words => _r.transcript.isNotEmpty ? _r.transcript : widget.heard; + String get _summary => _r.overview.isNotEmpty ? _r.overview : _words; + + Duration get _duration => Duration(milliseconds: (900 + 55 * _summary.split(' ').length).clamp(1600, 4600)); + + @override + void initState() { + super.initState(); + // A light tap as the emoji lands. + if (_r.hasNotes) { + _beats.add(Timer(Duration(milliseconds: (_duration.inMilliseconds * 0.12).round()), OmiHaptics.light)); + } + } + + @override + void dispose() { + for (final beat in _beats) { + beat.cancel(); + } + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final l10n = context.l10n; + final bodyStyle = OmiType.body.copyWith(color: OmiColors.textPrimary, height: 1.45); + return OneShot( + duration: _duration, + builder: (context, t) { + if (!_r.hasNotes) { + final card = _WordsCard( + key: const Key('activation_summary_card'), + child: WordReveal(text: _words, t: segment(t, 0, 1, Curves.linear), style: _wordsStyle), + ); + return card; + } + return NoteCard( + key: const Key('activation_summary_card'), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Row( + children: [ + Transform.scale( + scale: Curves.elasticOut.transform(segment(t, 0.02, 0.3, Curves.linear)), + child: Container( + width: 44, + height: 44, + alignment: Alignment.center, + decoration: BoxDecoration(color: OmiColors.surface2, borderRadius: OmiRadius.mdAll), + child: Text(_r.emoji, + style: const TextStyle( + fontSize: 22)), // omi-ux-allow: font-size-literal -- emoji glyph sized to its 44 pt tile + ), + ), + const SizedBox(width: OmiSpacing.sm), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + rise(segment(t, 0.08, 0.26), Text(_r.title, style: OmiType.headline), distance: 8), + const SizedBox(height: 2), + rise( + segment(t, 0.14, 0.32), + Text(l10n.onboardingActJustNow, + style: OmiType.footnote.copyWith(color: OmiColors.textTertiary)), + distance: 6, + ), + ], + ), + ), + ], + ), + const SizedBox(height: OmiSpacing.md), + Align( + alignment: Alignment.centerLeft, + child: FractionallySizedBox( + widthFactor: segment(t, 0.2, 0.4), + child: Container(height: 1, color: OmiColors.border), + ), + ), + const SizedBox(height: OmiSpacing.md), + SummaryText(text: _summary, t: segment(t, 0.3, 0.9, Curves.linear), style: bodyStyle), + if (_words.isNotEmpty) ...[ + const SizedBox(height: OmiSpacing.md), + rise(segment(t, 0.88, 1), _transcriptToggle(l10n), distance: 6), + ], + ], + ), + ); + }, + ); + } + + Widget _transcriptToggle(AppLocalizations l10n) { + final quiet = OmiType.footnote.copyWith(color: OmiColors.textSecondary); + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Semantics( + button: true, + expanded: _showTranscript, + child: GestureDetector( + key: const Key('activation_summary_transcript'), + behavior: HitTestBehavior.opaque, + onTap: () { + OmiHaptics.selection(); + setState(() => _showTranscript = !_showTranscript); + }, + child: SizedBox( + height: 32, + child: Row( + children: [ + Icon(Icons.graphic_eq_rounded, size: 16, color: OmiColors.textTertiary), + const SizedBox(width: OmiSpacing.xs), + Expanded(child: Text(l10n.transcript, style: quiet)), + AnimatedRotation( + turns: _showTranscript ? 0.5 : 0, + duration: const Duration(milliseconds: 200), + child: Icon(Icons.expand_more_rounded, size: 18, color: OmiColors.textTertiary), + ), + ], + ), + ), + ), + ), + AnimatedSize( + duration: const Duration(milliseconds: 220), + curve: Curves.easeOutCubic, + alignment: Alignment.topCenter, + child: _showTranscript + ? Padding( + padding: const EdgeInsets.only(top: OmiSpacing.xs), + child: Text(_words, style: quiet.copyWith(color: OmiColors.textTertiary, height: 1.4)), + ) + : const SizedBox(width: double.infinity), + ), + ], + ); + } +} + +/// The to-dos Omi caught, sliding in one by one with a tap each, then "Saved to your tasks". +class _TasksCard extends StatefulWidget { + const _TasksCard({required this.tasks}); + + final List tasks; + + @override + State<_TasksCard> createState() => _TasksCardState(); +} + +class _TasksCardState extends State<_TasksCard> { + final List _beats = []; + static const _first = 300; + static const _gap = 520; + + int get _n => widget.tasks.length; + Duration get _duration => Duration(milliseconds: _first + _gap * _n + 900); + + /// When task [i] arrives, as a share of [_duration]. + double _at(int i) => (_first + _gap * i) / _duration.inMilliseconds; + + @override + void initState() { + super.initState(); + for (var i = 0; i < _n; i++) { + _beats.add(Timer(Duration(milliseconds: _first + _gap * i + 120), OmiHaptics.light)); + } + _beats.add(Timer(Duration(milliseconds: _first + _gap * _n + 200), OmiHaptics.success)); + } + + @override + void dispose() { + for (final beat in _beats) { + beat.cancel(); + } + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final l10n = context.l10n; + final span = _gap / _duration.inMilliseconds; + return OneShot( + duration: _duration, + builder: (context, t) { + final saved = segment(t, _at(_n), _at(_n) + span); + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + NoteCard( + key: const Key('activation_tasks_card'), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + for (var i = 0; i < _n; i++) ...[ + if (i > 0) Container(height: 1, color: OmiColors.border), + _arriving(segment(t, _at(i), _at(i) + span * 1.4, Curves.linear), widget.tasks[i]), + ], + ], + ), + ), + const SizedBox(height: OmiSpacing.md), + Opacity( + opacity: saved, + child: Transform.scale( + scale: 0.9 + 0.1 * Curves.easeOutBack.transform(saved), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: OmiSpacing.md, vertical: OmiSpacing.xs), + decoration: BoxDecoration(color: OmiColors.surface1, borderRadius: OmiRadius.pillAll), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.check_circle_rounded, size: 16, color: OmiColors.success), + const SizedBox(width: OmiSpacing.xs), + Text(l10n.onboardingActSavedToTasks, + style: OmiType.footnote.copyWith(color: OmiColors.textSecondary)), + ], + ), + ), + ), + ), + ], + ); + }, + ); + } + + /// One task sliding in from the right with a brief highlight behind it as it lands. + Widget _arriving(double p, String text) { + final move = Curves.easeOutBack.transform(p.clamp(0.0, 1.0)); + final glow = p <= 0 ? 0.0 : (1 - p).clamp(0.0, 1.0); + return Opacity( + opacity: segment(p, 0, 0.4), + child: Transform.translate( + offset: Offset((1 - move) * 36, 0), + child: Container( + decoration: BoxDecoration( + color: OmiColors.accent.withValues(alpha: 0.06 * glow), + borderRadius: OmiRadius.smAll, + ), + padding: const EdgeInsets.symmetric(vertical: OmiSpacing.xxs), + child: TaskRow(text: text), + ), + ), + ); + } +} + +/// Skip in the top right, in grey: there if needed, without competing with the dictation. +class _QuietSkip extends StatelessWidget { + const _QuietSkip({required this.label, required this.onTap}); + + final String label; + final VoidCallback onTap; + + @override + Widget build(BuildContext context) { + return Semantics( + button: true, + label: label, + excludeSemantics: true, + child: GestureDetector( + key: const Key('activation_dictation_skip'), + behavior: HitTestBehavior.opaque, + onTap: onTap, + child: ConstrainedBox( + constraints: const BoxConstraints(minWidth: 44, minHeight: 44), + child: Center( + widthFactor: 1, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: OmiSpacing.xs), + child: Text(label, style: OmiType.callout.copyWith(color: OmiColors.textTertiary)), + ), + ), + ), + ), + ); + } +} + +/// "You're all set": the title in its usual place, and close under it a check that pops in with a +/// burst of confetti. One Continue. +class _Hooray extends StatefulWidget { + const _Hooray({super.key, required this.onContinue}); + + final VoidCallback onContinue; + + @override + State<_Hooray> createState() => _HoorayState(); +} + +class _HoorayState extends State<_Hooray> { + Timer? _haptic; + + @override + void initState() { + super.initState(); + // In step with the check popping in. + _haptic = Timer(const Duration(milliseconds: 380), OmiHaptics.success); + } + + @override + void dispose() { + _haptic?.cancel(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final l10n = context.l10n; + final name = SharedPreferencesUtil().givenName.trim(); + return _Stage( + title: name.isEmpty ? l10n.youreAllSet : l10n.onboardingActCongratsTitle(name), + // Lifted above the middle so the check sits close under the title. + body: Padding( + padding: const EdgeInsets.only(bottom: 120), + child: OneShot( + duration: const Duration(milliseconds: 2000), + builder: (context, t) => SizedBox( + width: 280, + height: 280, + child: Stack( + alignment: Alignment.center, + children: [ + Positioned.fill(child: CustomPaint(painter: ConfettiPainter(t: segment(t, 0.2, 1, Curves.linear)))), + Transform.scale( + scale: Curves.elasticOut.transform(segment(t, 0.12, 0.55, Curves.linear)), + child: Container( + width: 104, + height: 104, + decoration: BoxDecoration(color: OmiColors.accent, shape: BoxShape.circle), + child: Icon(Icons.check_rounded, size: 56, color: OmiColors.onAccent), + ), + ), + ], + ), + ), + ), + ), + bottom: _ContinueButton(stage: 'congrats', onPressed: widget.onContinue), + ); + } +} + +/// After the celebration: "Enjoying Omi so far?" with five gold stars popping in one by one under +/// it, both above where the App Store rating pop-up opens by itself. The paywall follows once the +/// pop-up is closed (or Continue, if it never showed). +class _RatingAsk extends StatefulWidget { + const _RatingAsk({super.key, required this.requestReview, required this.onDone}); + + final Future Function() requestReview; + final VoidCallback onDone; + + @override + State<_RatingAsk> createState() => _RatingAskState(); +} + +class _RatingAskState extends State<_RatingAsk> with WidgetsBindingObserver { + static const _gold = Color(0xFFFFC53D); // omi-ux-allow: color-literal -- rating stars in App Store gold + Timer? _ask; + Timer? _starsLanded; + Timer? _offerContinue; + bool _promptShowing = false; + // Continue stays hidden until the App Store pop-up has had time to appear, so a quick tap can't + // skip past it. + bool _canContinue = false; + bool _asked = false; + bool _done = false; + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addObserver(this); + // A light tap as the stars land, then the App Store prompt. + _starsLanded = Timer(const Duration(milliseconds: 650), OmiHaptics.light); + _ask = Timer(const Duration(milliseconds: 1500), _requestReview); + } + + @override + void dispose() { + _ask?.cancel(); + _starsLanded?.cancel(); + _offerContinue?.cancel(); + WidgetsBinding.instance.removeObserver(this); + super.dispose(); + } + + Future _requestReview() async { + PlatformManager.instance.analytics.track('Onboarding Rating Asked'); + try { + await widget.requestReview(); + } catch (_) {} + _asked = true; + // iOS gives no callback when its pop-up closes (and on many builds the app never goes + // inactive under it), so don't wait for it: once it is up, move on underneath it. Closing it + // then lands on the paywall instead of back on this screen. + _offerContinue = Timer(const Duration(milliseconds: 900), () { + if (!mounted) return; + // Continue stays as the way on if leaving fails (a failed save is retried from here). + setState(() => _canContinue = true); + _leave(); + }); + } + + /// iOS gives no callback for its rating pop-up, but the app goes inactive while it is up and + /// becomes active again when it closes: that return is the cue to move on. + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + if (state == AppLifecycleState.inactive) _promptShowing = true; + if (state == AppLifecycleState.resumed && _promptShowing) _leave(); + } + + void _leave() { + if (_done || !mounted) return; + _done = true; + widget.onDone(); + } + + /// Continue always reaches [widget.onDone], so a failed save can be retried. + void _continue() { + if (!mounted) return; + _done = true; + widget.onDone(); + } + + @override + Widget build(BuildContext context) { + // Not every iOS build makes the app inactive under the pop-up (the simulator never does). The + // pop-up takes every touch while it is up, so the first tap here after asking means it closed. + return Listener( + behavior: HitTestBehavior.translucent, + // After the tap has been handled: a tap on Continue has then already moved on (or retried a + // failed save), and this cue must not count the same tap twice. + onPointerUp: (_) { + if (_asked && _canContinue) Future(_leave); + }, + child: _buildStage(context), + ); + } + + Widget _buildStage(BuildContext context) { + return _Stage( + title: context.l10n.onboardingActRateTitle, + bodyAtTop: true, + body: OneShot( + duration: const Duration(milliseconds: 1200), + builder: (context, t) => Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + for (var i = 0; i < 5; i++) + Transform.scale( + scale: Curves.elasticOut.transform(segment(t, i * 0.1, 0.45 + i * 0.1, Curves.linear)), + child: const Padding( + padding: EdgeInsets.symmetric(horizontal: 3), + child: Icon(Icons.star_rounded, size: 46, color: _gold), + ), + ), + ], + ), + ), + bottom: IgnorePointer( + ignoring: !_canContinue, + child: AnimatedOpacity( + opacity: _canContinue ? 1 : 0, + duration: const Duration(milliseconds: 300), + child: OmiButton( + key: const Key('activation_continue_rating'), + label: context.l10n.continueButton, + expand: true, + onPressed: _continue, + ), + ), + ), + ); + } +} diff --git a/app/lib/pages/onboarding/activation/activation_motion.dart b/app/lib/pages/onboarding/activation/activation_motion.dart new file mode 100644 index 00000000000..0b1a96cd6ec --- /dev/null +++ b/app/lib/pages/onboarding/activation/activation_motion.dart @@ -0,0 +1,715 @@ +import 'dart:async'; +import 'dart:math' as math; +import 'dart:ui' show ImageFilter; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/scheduler.dart'; + +import 'package:omi/pages/onboarding/activation/activation_visuals.dart'; +import 'package:omi/ui/ui.dart'; + +/// Lower-case words without punctuation, for comparing what was said with the script. +List activationTokens(String text) => text + .toLowerCase() + .split(RegExp(r'\s+')) + .map((w) => w.replaceAll(RegExp(r"[^\p{L}\p{N}']", unicode: true), '')) + .where((w) => w.isNotEmpty) + .toList(); + +/// How much of [script] the reader has said, judging by [heard]: the script index just past the last +/// heard word found in order, and the share of heard words that were found. A low share means the +/// reader is saying something else. +({int reached, double match}) readAlongProgress(List script, List heard) { + if (heard.isEmpty) return (reached: 0, match: 1); + var i = 0; + var found = 0; + var reached = 0; + for (final word in heard) { + // Look a few words ahead, so a skipped or misheard word doesn't lose the place. + for (var j = i; j < script.length && j < i + 4; j++) { + if (script[j] == word) { + found++; + i = j + 1; + reached = i; + break; + } + } + } + return (reached: reached, match: found / heard.length); +} + +/// The dictation card. Before Omi's words come back (they come a sentence at a time), the script +/// lights up word by word while the reader's voice is heard, with a soft tick per word, and waits +/// when they pause. When a sentence arrives it either confirms the script (the lit words turn solid) +/// or, when the reader said something else, their own words ink in instead. A waveform along the +/// bottom moves with the voice from the first moment. +class ReadAlongCard extends StatefulWidget { + const ReadAlongCard({ + super.key, + required this.script, + required this.live, + required this.level, + required this.listening, + required this.style, + this.levelDriven = true, + }); + + final String script; + + /// What Omi heard so far (whole sentences). + final ValueListenable live; + + /// Voice loudness, 0..1. + final ValueListenable level; + final bool listening; + final TextStyle style; + + /// False when the voice level is not available (talking through the Omi): the highlight then + /// moves at speaking pace, a few words ahead of what Omi confirmed at most. + final bool levelDriven; + + @override + State createState() => _ReadAlongCardState(); +} + +class _ReadAlongCardState extends State with SingleTickerProviderStateMixin { + static const _wordsPerSecond = 2.6; + static const _voiceThreshold = 0.1; + static const _bars = 32; + + late final List _scriptWords = widget.script.split(RegExp(r'\s+')); + late final List _scriptTokens = activationTokens(widget.script); + late final Ticker _ticker = createTicker(_tick); + Duration _last = Duration.zero; + + /// Words lit so far (fractional while one is lighting). + double _cursor = 0; + int _confirmed = 0; + bool _ownWords = false; + DateTime _lastTick = DateTime.fromMillisecondsSinceEpoch(0); + final List _history = List.filled(_bars, 0, growable: true); + double _sinceSample = 0; + + /// When each word was heard by Omi (script index, or own-word index): drives its flash. + final Map _heardAt = {}; + int _ownSeen = 0; + static const _flash = Duration(milliseconds: 750); + + @override + void initState() { + super.initState(); + widget.live.addListener(_onLive); + _ticker.start(); + } + + @override + void dispose() { + widget.live.removeListener(_onLive); + _ticker.dispose(); + super.dispose(); + } + + void _onLive() { + final heard = activationTokens(widget.live.value); + final p = readAlongProgress(_scriptTokens, heard); + final now = DateTime.now(); + var landed = false; + setState(() { + if (heard.length >= 3 && p.match < 0.5) { + _ownWords = true; + } + if (_ownWords) { + final count = widget.live.value.trim().split(RegExp(r'\s+')).length; + for (var i = _ownSeen; i < count; i++) { + _heardAt[i] = now.add(Duration(milliseconds: 45 * (i - _ownSeen))); + } + landed = count > _ownSeen; + _ownSeen = math.max(_ownSeen, count); + } else if (p.reached > _confirmed) { + // Omi's words landed: each newly heard word flashes in turn. + for (var i = _confirmed; i < p.reached; i++) { + _heardAt[i] = now.add(Duration(milliseconds: 45 * (i - _confirmed))); + } + _confirmed = p.reached; + _cursor = math.max(_cursor, _confirmed.toDouble()); + landed = true; + } + }); + if (landed) OmiHaptics.light(); + } + + /// 1 the moment a word is heard by Omi, easing to 0 over [_flash]; 0 when not flashing. + double _flashOf(int i) { + final at = _heardAt[i]; + if (at == null) return 0; + final ms = DateTime.now().difference(at).inMilliseconds; + if (ms < 0) return 0; + return 1 - Curves.easeOut.transform((ms / _flash.inMilliseconds).clamp(0.0, 1.0)); + } + + bool get _flashing { + final now = DateTime.now(); + return _heardAt.values.any((at) => now.difference(at) < _flash + const Duration(milliseconds: 600)); + } + + void _tick(Duration now) { + final dt = (now - _last).inMicroseconds / 1e6; + _last = now; + if (dt <= 0 || dt > 0.25) return; + final level = widget.level.value; + _sinceSample += dt; + var changed = false; + if (_sinceSample >= 0.06) { + _sinceSample = 0; + _history.removeAt(0); + _history.add(widget.listening ? level : 0); + changed = true; + } + if (widget.listening && !_ownWords) { + final speaking = widget.levelDriven ? level > _voiceThreshold : true; + final cap = widget.levelDriven + ? _scriptWords.length.toDouble() + : math.min(_scriptWords.length.toDouble(), _confirmed + 6.0); + if (speaking && _cursor < cap) { + final before = _cursor.floor(); + _cursor = math.min(cap, _cursor + dt * _wordsPerSecond); + if (_cursor.floor() > before) _wordTick(); + changed = true; + } + } + if ((changed || _flashing) && mounted) setState(() {}); + } + + void _wordTick() { + final now = DateTime.now(); + if (now.difference(_lastTick).inMilliseconds < 80) return; + _lastTick = now; + OmiHaptics.selection(); + } + + @override + Widget build(BuildContext context) { + final reduce = OmiMotion.of(context).emphasized == Duration.zero; + return NoteCard( + minHeight: 150, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + mainAxisSize: MainAxisSize.min, + children: [ + AnimatedSwitcher( + duration: Duration(milliseconds: reduce ? 0 : 280), + child: _ownWords ? _own(reduce) : _scripted(reduce), + ), + const SizedBox(height: OmiSpacing.md), + SizedBox(height: 22, child: CustomPaint(painter: _Waveform(_history, OmiColors.textPrimary))), + ], + ), + ); + } + + Widget _scripted(bool reduce) { + return Wrap( + key: const ValueKey('script'), + children: [ + for (var i = 0; i < _scriptWords.length; i++) _word(i, reduce), + ], + ); + } + + /// Three clear states: not said yet (faint), heard by the microphone but not yet by Omi (grey), + /// and heard by Omi (full ink, arriving with a flash behind it). + Widget _word(int i, bool reduce) { + final lit = (_cursor - i).clamp(0.0, 1.0); + final confirmed = i < _confirmed; + final color = confirmed + ? OmiColors.textPrimary + : Color.lerp(OmiColors.textTertiary.withValues(alpha: 0.55), OmiColors.textSecondary, lit)!; + // A word lifts a touch as the voice lights it, then settles. + final lift = reduce || confirmed ? 0.0 : -2.0 * math.sin(math.pi * lit); + return _flashWord(Text('${_scriptWords[i]} ', style: widget.style.copyWith(color: color)), reduce ? 0 : _flashOf(i), + lift: lift); + } + + /// The moment Omi hears a word: a soft highlight behind it and a small pop, fading out. + Widget _flashWord(Widget word, double f, {double lift = 0}) { + return Transform.translate( + offset: Offset(0, lift), + child: Transform.scale( + scale: 1 + 0.07 * f, + child: DecoratedBox( + decoration: BoxDecoration( + color: OmiColors.textPrimary.withValues(alpha: 0.14 * f), + borderRadius: OmiRadius.smAll, + ), + child: word, + ), + ), + ); + } + + Widget _own(bool reduce) { + final words = widget.live.value.trim().split(RegExp(r'\s+')); + return Wrap( + key: const ValueKey('own'), + children: [ + for (var i = 0; i < words.length; i++) + TweenAnimationBuilder( + key: ValueKey('own_$i'), + tween: Tween(begin: reduce ? 1 : 0, end: 1), + duration: Duration(milliseconds: 260 + 30 * (i % 6)), + curve: Curves.easeOutCubic, + builder: (context, t, child) => Opacity( + opacity: t, + child: Transform.translate(offset: Offset(0, 6 * (1 - t)), child: child), + ), + child: _flashWord( + Text('${words[i]} ', style: widget.style.copyWith(color: OmiColors.textPrimary)), + reduce ? 0 : _flashOf(i), + ), + ), + ], + ); + } +} + +/// Rounded bars of the last couple of seconds of voice, newest on the right. +class _Waveform extends CustomPainter { + _Waveform(List history, this.color) : history = List.of(history); + + final List history; + final Color color; + + @override + void paint(Canvas canvas, Size size) { + final n = history.length; + final gap = size.width / n; + final w = math.min(3.0, gap * 0.5); + for (var i = 0; i < n; i++) { + final v = history[i].clamp(0.0, 1.0); + final h = 3 + (size.height - 3) * v; + final x = gap * i + gap / 2; + final paint = Paint()..color = color.withValues(alpha: 0.18 + 0.7 * v); + canvas.drawRRect( + RRect.fromRectAndRadius( + Rect.fromCenter(center: Offset(x, size.height / 2), width: w, height: h), Radius.circular(w)), + paint, + ); + } + } + + @override + bool shouldRepaint(covariant _Waveform old) => !listEquals(old.history, history) || old.color != color; +} + +const _fillerWords = { + 'a', + 'an', + 'the', + 'and', + 'or', + 'but', + 'so', + 'to', + 'of', + 'in', + 'on', + 'at', + 'by', + 'for', + 'with', + 'from', + 'just', + 'had', + 'have', + 'has', + 'is', + 'was', + 'are', + 'be', + 'me', + 'my', + 'i', + 'im', + "i'm", + 'she', + 'he', + 'they', + 'we', + 'you', + 'it', + 'its', + "it's", + "she's", + "he's", + 'that', + 'this', + 'then', + 'um', + 'uh', + 'like', + 'really', + 'also', + 'way', + 'up', + 'out', + 'about', + 'remind', + 'grab', + 'get', + 'pick', +}; + +/// "Summarizing…": the reader's words stay in the same card and visibly condense. Filler words fade +/// and fold away, the words that matter draw together and darken, and the card shrinks around them; +/// then a light passes over what is left while Omi finishes. +class CompactingCard extends StatefulWidget { + const CompactingCard({super.key, required this.text, required this.style}); + + final String text; + final TextStyle style; + + @override + State createState() => _CompactingCardState(); +} + +class _CompactingCardState extends State with TickerProviderStateMixin { + late final AnimationController _fold = AnimationController(vsync: this, duration: const Duration(milliseconds: 1500)) + ..forward(); + late final AnimationController _sweep = + AnimationController(vsync: this, duration: const Duration(milliseconds: 1400)); + late final List _words = widget.text.trim().split(RegExp(r'\s+')); + late final List _keep = [ + for (final w in _words) + () { + final t = activationTokens(w); + return t.isNotEmpty && !_fillerWords.contains(t.first) && t.first.length > 2; + }(), + ]; + + @override + void initState() { + super.initState(); + _fold.addStatusListener((s) { + if (s == AnimationStatus.completed && mounted) _sweep.repeat(); + }); + } + + @override + void dispose() { + _fold.dispose(); + _sweep.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final reduce = OmiMotion.of(context).emphasized == Duration.zero; + if (reduce) { + return NoteCard( + minHeight: 150, + child: Text( + [ + for (var i = 0; i < _words.length; i++) + if (_keep[i]) _words[i] + ].join(' '), + style: widget.style.copyWith(color: OmiColors.textPrimary), + ), + ); + } + return AnimatedBuilder( + animation: Listenable.merge([_fold, _sweep]), + builder: (context, _) { + final f = Curves.easeInOutCubic.transform(_fold.value); + final words = Wrap( + children: [ + for (var i = 0; i < _words.length; i++) + if (_keep[i]) _kept(i, f) else _dropped(i, f), + ], + ); + return AnimatedSize( + duration: const Duration(milliseconds: 300), + curve: Curves.easeOutCubic, + alignment: Alignment.topCenter, + child: NoteCard( + // The card lets go of its minimum height as the words fold, so it shrinks around them. + minHeight: 150 * (1 - f) + 60 * f, + child: _fold.isCompleted ? _withSweep(words) : words, + ), + ); + }, + ); + } + + /// Each dropped word starts going a little after the one before, so the folding travels along. + Widget _dropped(int i, double f) { + final start = (i / _words.length) * 0.5; + final t = ((f - start) / 0.5).clamp(0.0, 1.0); + return ClipRect( + child: Align( + alignment: Alignment.centerLeft, + widthFactor: 1 - t, + heightFactor: 1, + child: Opacity( + opacity: (1 - t * 1.6).clamp(0.0, 1.0), + child: Text('${_words[i]} ', style: widget.style.copyWith(color: OmiColors.textTertiary)), + ), + ), + ); + } + + Widget _kept(int i, double f) { + final color = Color.lerp(OmiColors.textSecondary, OmiColors.textPrimary, f)!; + return Text( + '${_words[i]} ', + style: widget.style.copyWith(color: color, fontWeight: FontWeight.lerp(FontWeight.w400, FontWeight.w600, f)), + ); + } + + Widget _withSweep(Widget child) { + final x = -1.0 + 3.0 * _sweep.value; + return ShaderMask( + blendMode: BlendMode.srcATop, + shaderCallback: (bounds) => LinearGradient( + begin: Alignment(x - 1, -0.2), + end: Alignment(x + 1, 0.2), + colors: [ + OmiColors.surface1.withValues(alpha: 0), + OmiColors.surface1.withValues(alpha: 0.75), + OmiColors.surface1.withValues(alpha: 0) + ], + stops: const [0.35, 0.5, 0.65], + ).createShader(bounds), + child: child, + ); + } +} + +/// Where the ✓ was while Omi writes: the same dark circle, holding three lines that shorten into +/// one and grow back, over and over — notes condensing. +class CondensingControl extends StatefulWidget { + const CondensingControl({super.key}); + + @override + State createState() => _CondensingControlState(); +} + +class _CondensingControlState extends State with SingleTickerProviderStateMixin { + late final AnimationController _c = AnimationController(vsync: this, duration: const Duration(milliseconds: 1600)) + ..repeat(); + + @override + void dispose() { + _c.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final reduce = OmiMotion.of(context).emphasized == Duration.zero; + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox( + width: 140, + height: 140, + child: Center( + child: Container( + width: 68, + height: 68, + decoration: BoxDecoration(color: OmiColors.accent, shape: BoxShape.circle), + child: AnimatedBuilder( + animation: _c, + builder: (context, _) => CustomPaint( + painter: _CondensingLines(t: reduce ? 0.5 : _c.value, color: OmiColors.onAccent), + ), + ), + ), + ), + ), + // Keeps the circle level with the ✓ it replaces (which has a status line under it). + const SizedBox(height: OmiSpacing.xs + 20), + ], + ); + } +} + +class _CondensingLines extends CustomPainter { + _CondensingLines({required this.t, required this.color}); + + final double t; + final Color color; + + @override + void paint(Canvas canvas, Size size) { + // 0→0.5: three lines draw together into one short line; 0.5→1: they spread back out. + final phase = t < 0.5 ? Curves.easeInOutCubic.transform(t * 2) : 1 - Curves.easeInOutCubic.transform((t - 0.5) * 2); + final c = size.center(Offset.zero); + final paint = Paint() + ..color = color + ..strokeCap = StrokeCap.round + ..strokeWidth = 3; + const widths = [26.0, 20.0, 14.0]; + for (var k = 0; k < 3; k++) { + final y = c.dy + (k - 1) * 8 * (1 - phase); + final w = widths[k] * (1 - phase) + 18 * phase; + canvas.drawLine(Offset(c.dx - w / 2, y), Offset(c.dx + w / 2, y), + paint..color = color.withValues(alpha: k == 1 ? 1 : 1 - 0.6 * phase)); + } + } + + @override + bool shouldRepaint(covariant _CondensingLines old) => old.t != t || old.color != color; +} + +/// The summary's entrance: the card comes up out of a soft blur, then a light passes across it once. +class SummaryReveal extends StatefulWidget { + const SummaryReveal({super.key, required this.child}); + + final Widget child; + + @override + State createState() => _SummaryRevealState(); +} + +class _SummaryRevealState extends State with SingleTickerProviderStateMixin { + late final AnimationController _c = AnimationController(vsync: this, duration: const Duration(milliseconds: 1500)) + ..forward(); + Timer? _done; + + @override + void initState() { + super.initState(); + _done = Timer(const Duration(milliseconds: 1250), OmiHaptics.medium); + } + + @override + void dispose() { + _done?.cancel(); + _c.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + if (OmiMotion.of(context).emphasized == Duration.zero) return widget.child; + return AnimatedBuilder( + animation: _c, + child: widget.child, + builder: (context, child) { + final v = _c.value; + final appear = Curves.easeOutCubic.transform((v / 0.45).clamp(0.0, 1.0)); + final sweep = ((v - 0.55) / 0.45).clamp(0.0, 1.0); + Widget out = Opacity( + opacity: appear, + child: Transform.scale( + scale: 0.94 + 0.06 * appear, + child: ImageFiltered( + imageFilter: ImageFilter.blur(sigmaX: 12 * (1 - appear), sigmaY: 12 * (1 - appear)), + child: child, + ), + ), + ); + if (sweep > 0 && sweep < 1) { + final x = -1.5 + 3.0 * Curves.easeInOut.transform(sweep); + out = ShaderMask( + blendMode: BlendMode.srcATop, + shaderCallback: (bounds) => LinearGradient( + begin: Alignment(x - 0.6, -1), + end: Alignment(x + 0.6, 1), + colors: [ + OmiColors.surface1.withValues(alpha: 0), + OmiColors.surface1.withValues(alpha: 0.6), + OmiColors.surface1.withValues(alpha: 0) + ], + ).createShader(bounds), + child: out, + ); + } + return out; + }, + ); + } +} + +/// One line of a summary: a heading (`## …`), a bullet (`- …`), or plain text. +@visibleForTesting +typedef SummaryLine = ({String text, bool heading, bool bullet}); + +/// Omi's notes arrive as light markdown; the onboarding card shows them as text: headings become +/// short bold lines, `-`/`*` items become bullets, emphasis markers go, blank lines collapse. +@visibleForTesting +List summaryLines(String markdown) { + final out = []; + for (final raw in markdown.split('\n')) { + var line = raw.trim(); + if (line.isEmpty) continue; + line = line.replaceAll('**', '').replaceAll('__', ''); + final heading = RegExp(r'^#{1,6}\s+').firstMatch(line); + final bullet = RegExp(r'^([-*•]|\d+[.)])\s+').firstMatch(line); + if (heading != null) { + out.add((text: line.substring(heading.end).trim(), heading: true, bullet: false)); + } else if (bullet != null) { + out.add((text: line.substring(bullet.end).trim(), heading: false, bullet: true)); + } else { + out.add((text: line, heading: false, bullet: false)); + } + } + return out; +} + +/// The summary in the onboarding card, written in word by word ([t] 0→1) across its lines, in a +/// box that never grows past [maxHeight]: longer notes scroll inside it, with a fade at the bottom. +class SummaryText extends StatelessWidget { + const SummaryText({super.key, required this.text, required this.t, required this.style, this.maxHeight = 190}); + + final String text; + final double t; + final TextStyle style; + final double maxHeight; + + @override + Widget build(BuildContext context) { + final lines = summaryLines(text); + final total = lines.fold(0, (n, l) => n + l.text.split(' ').length); + final shown = (total * t).ceil(); + var index = 0; + final children = []; + for (var k = 0; k < lines.length; k++) { + final line = lines[k]; + final lineStyle = line.heading ? style.copyWith(fontWeight: FontWeight.w600) : style; + final words = line.text.split(' '); + final spans = [ + if (line.bullet) + TextSpan(text: '• ', style: lineStyle.copyWith(color: index < shown ? null : Colors.transparent)), + for (var i = 0; i < words.length; i++) + TextSpan( + text: i == words.length - 1 ? words[i] : '${words[i]} ', + style: lineStyle.copyWith( + color: index + i < shown ? lineStyle.color : lineStyle.color?.withValues(alpha: 0), + ), + ), + ]; + index += words.length; + children.add(Padding( + padding: EdgeInsets.only(top: k == 0 ? 0 : (line.heading ? OmiSpacing.md : OmiSpacing.xs)), + child: Text.rich(TextSpan(children: spans)), + )); + } + final column = + Column(crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: children); + return ConstrainedBox( + constraints: BoxConstraints(maxHeight: maxHeight), + child: ShaderMask( + blendMode: BlendMode.dstIn, + shaderCallback: (bounds) => LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: const [Colors.black, Colors.black, Colors.transparent], + stops: [0, bounds.height >= maxHeight - 1 ? 0.82 : 1, 1], + ).createShader(bounds), + child: SingleChildScrollView(physics: const ClampingScrollPhysics(), child: column), + ), + ); + } +} diff --git a/app/lib/pages/onboarding/activation/activation_visuals.dart b/app/lib/pages/onboarding/activation/activation_visuals.dart new file mode 100644 index 00000000000..c97f56b1d11 --- /dev/null +++ b/app/lib/pages/onboarding/activation/activation_visuals.dart @@ -0,0 +1,206 @@ +import 'dart:math' as math; + +import 'package:flutter/material.dart'; + +import 'package:omi/ui/ui.dart'; + +const _cardShadow = BoxShadow( + color: Color(0x14000000), blurRadius: 24, offset: Offset(0, 8)); // omi-ux-allow: color-literal -- soft card shadow + +/// Runs [builder] with a 0→1 value once over [duration] (instant under Reduce Motion). +class OneShot extends StatefulWidget { + const OneShot({super.key, required this.duration, required this.builder, this.delay = Duration.zero}); + + final Duration duration; + final Duration delay; + final Widget Function(BuildContext context, double t) builder; + + @override + State createState() => _OneShotState(); +} + +class _OneShotState extends State with SingleTickerProviderStateMixin { + late final AnimationController _c = AnimationController(vsync: this, duration: widget.duration); + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + if (_c.status != AnimationStatus.dismissed) return; + if (OmiMotion.of(context).emphasized == Duration.zero) { + _c.value = 1; + } else { + Future.delayed(widget.delay, () { + if (mounted) _c.forward(); + }); + } + } + + @override + void dispose() { + _c.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) => + AnimatedBuilder(animation: _c, builder: (context, _) => widget.builder(context, _c.value)); +} + +/// The part of [t] that falls inside [start]..[end], eased, as 0→1. +double segment(double t, double start, double end, [Curve curve = Curves.easeOutCubic]) => + curve.transform(((t - start) / (end - start)).clamp(0.0, 1.0)); + +/// Fades and lifts [child] in as [t] goes 0→1. +Widget rise(double t, Widget child, {double distance = 10}) => Opacity( + opacity: t, + child: Transform.translate(offset: Offset(0, (1 - t) * distance), child: child), + ); + +/// A note card in the app's style: optional emoji + title, then body text. +class NoteCard extends StatelessWidget { + const NoteCard({super.key, this.emoji, this.title, this.body, this.child, this.minHeight = 0}); + + final String? emoji; + final String? title; + final String? body; + final Widget? child; + final double minHeight; + + @override + Widget build(BuildContext context) { + return Container( + width: double.infinity, + constraints: BoxConstraints(minHeight: minHeight), + padding: const EdgeInsets.all(OmiSpacing.lg), + decoration: BoxDecoration( + color: OmiColors.surface1, + borderRadius: OmiRadius.lgAll, + boxShadow: const [_cardShadow], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + if (title != null) + Text( + emoji == null || emoji!.isEmpty ? title! : '${emoji!} ${title!}', + style: OmiType.headline, + ), + if (title != null && (body != null || child != null)) const SizedBox(height: OmiSpacing.xs), + if (body != null) Text(body!, style: OmiType.subhead.copyWith(color: OmiColors.textSecondary, height: 1.4)), + if (child != null) child!, + ], + ), + ); + } +} + +/// One task row: a round check that fills at [check] (0→1) and the task text. +class TaskRow extends StatelessWidget { + const TaskRow({super.key, required this.text, this.check = 0}); + + final String text; + final double check; + + @override + Widget build(BuildContext context) { + final fill = Color.lerp(Colors.transparent, OmiColors.accent, check)!; + return Padding( + padding: const EdgeInsets.symmetric(vertical: OmiSpacing.xs), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: 22, + height: 22, + margin: const EdgeInsets.only(top: 1), + decoration: BoxDecoration( + shape: BoxShape.circle, + color: fill, + border: Border.all(color: Color.lerp(OmiColors.textTertiary, OmiColors.accent, check)!, width: 1.5), + ), + child: check > 0.5 + ? Icon(Icons.check_rounded, size: 15, color: OmiColors.onAccent.withValues(alpha: (check - 0.5) * 2)) + : null, + ), + const SizedBox(width: OmiSpacing.sm), + Expanded(child: Text(text, style: OmiType.callout.copyWith(height: 1.35))), + ], + ), + ); + } +} + +/// Text that appears word by word as [t] goes 0→1. +class WordReveal extends StatelessWidget { + const WordReveal({super.key, required this.text, required this.t, this.style, this.textAlign}); + + final String text; + final double t; + final TextStyle? style; + final TextAlign? textAlign; + + @override + Widget build(BuildContext context) { + final words = text.split(' '); + final shown = (words.length * t).ceil().clamp(0, words.length); + final base = style ?? DefaultTextStyle.of(context).style; + return Text.rich( + TextSpan( + children: [ + for (var i = 0; i < words.length; i++) + TextSpan( + text: i == words.length - 1 ? words[i] : '${words[i]} ', + style: base.copyWith(color: i < shown ? base.color : base.color?.withValues(alpha: 0)), + ), + ], + ), + textAlign: textAlign, + ); + } +} + +/// A burst of confetti from the centre: pieces fly out, turn and fade as [t] goes 0→1. +class ConfettiPainter extends CustomPainter { + ConfettiPainter({required this.t}); + + final double t; + + static const _colors = [ + Color(0xFFFFC53D), // omi-ux-allow: color-literal -- celebration confetti palette + Color(0xFFFF6B6B), // omi-ux-allow: color-literal -- celebration confetti palette + Color(0xFF4DABF7), // omi-ux-allow: color-literal -- celebration confetti palette + Color(0xFF51CF66), // omi-ux-allow: color-literal -- celebration confetti palette + Color(0xFF38D9A9), // omi-ux-allow: color-literal -- celebration confetti palette + Color(0xFFFF922B), // omi-ux-allow: color-literal -- celebration confetti palette + ]; + + @override + void paint(Canvas canvas, Size size) { + if (t <= 0 || t >= 1) return; + final center = size.center(Offset.zero); + final random = math.Random(7); + final travel = Curves.easeOutCubic.transform(t); + final fade = t < 0.7 ? 1.0 : (1 - (t - 0.7) / 0.3); + for (var i = 0; i < 36; i++) { + final angle = random.nextDouble() * 2 * math.pi; + final reach = size.width * (0.28 + random.nextDouble() * 0.24); + final fall = 40 * t * t; + final position = center + Offset(math.cos(angle) * reach * travel, math.sin(angle) * reach * travel + fall); + final paint = Paint()..color = _colors[i % _colors.length].withValues(alpha: fade.clamp(0.0, 1.0)); + canvas.save(); + canvas.translate(position.dx, position.dy); + canvas.rotate(angle + t * 6 * (random.nextBool() ? 1 : -1)); + final w = 5 + random.nextDouble() * 4; + canvas.drawRRect( + RRect.fromRectAndRadius( + Rect.fromCenter(center: Offset.zero, width: w, height: w * 1.8), const Radius.circular(1.5)), + paint, + ); + canvas.restore(); + } + } + + @override + bool shouldRepaint(ConfettiPainter oldDelegate) => oldDelegate.t != t; +} diff --git a/app/lib/pages/onboarding/activation/onboarding_plans_sheet.dart b/app/lib/pages/onboarding/activation/onboarding_plans_sheet.dart new file mode 100644 index 00000000000..b95080243f4 --- /dev/null +++ b/app/lib/pages/onboarding/activation/onboarding_plans_sheet.dart @@ -0,0 +1,39 @@ +import 'package:flutter/material.dart'; + +import 'package:omi/pages/settings/widgets/plans_sheet.dart'; + +/// The plans sheet shown at the end of onboarding (until the Superwall paywall replaces it). +class OnboardingPlansSheet extends StatefulWidget { + const OnboardingPlansSheet({super.key}); + + @override + State createState() => _OnboardingPlansSheetState(); +} + +class _OnboardingPlansSheetState extends State with TickerProviderStateMixin { + late final AnimationController _wave = AnimationController(duration: const Duration(milliseconds: 18000), vsync: this) + ..repeat(); + late final AnimationController _arrow = AnimationController(duration: const Duration(milliseconds: 800), vsync: this) + ..repeat(reverse: true); + late final Animation _arrowOffset = + Tween(begin: 0, end: 3).animate(CurvedAnimation(parent: _arrow, curve: Curves.easeInOut)); + + @override + void dispose() { + _wave.dispose(); + _arrow.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return PlansSheet( + waveController: _wave, + notesController: _wave, + arrowController: _arrow, + arrowAnimation: _arrowOffset, + // Not yet a registered paywall source; reported as "unknown" until added to the registry. + analyticsSource: 'onboarding', + ); + } +} diff --git a/app/lib/pages/onboarding/ai_consent_widget.dart b/app/lib/pages/onboarding/ai_consent_widget.dart index ab542ef98cf..ef21bc424dd 100644 --- a/app/lib/pages/onboarding/ai_consent_widget.dart +++ b/app/lib/pages/onboarding/ai_consent_widget.dart @@ -41,50 +41,68 @@ class _AiConsentWidgetState extends State { super.dispose(); } + /// The third-party AI services that receive audio and transcripts. Brand names, not translated. + /// Keep in step with the privacy policy and the serving configuration (Apple 5.1.2(i): name who + /// receives the data). + static const _aiProviders = 'Deepgram & OpenAI'; + @override Widget build(BuildContext context) { - final linkStyle = OmiType.footnote.copyWith(color: OmiColors.textPrimary, decoration: TextDecoration.underline); - return OnboardingStep( - card: OnboardingCard( - crossAxisAlignment: CrossAxisAlignment.start, - content: [ - Semantics(header: true, child: Text(context.l10n.dataAndPrivacy, style: OmiType.title1)), - const SizedBox(height: OmiSpacing.md), - Text(context.l10n.consentDataMessage, style: OmiType.subhead.copyWith(height: 1.5)), - const SizedBox(height: OmiSpacing.md), - RichText( - text: TextSpan( - style: OmiType.footnote.copyWith(color: OmiColors.textSecondary, height: 1.4), - children: [ - TextSpan(text: context.l10n.yourDataIsProtected), - TextSpan(text: context.l10n.privacyPolicy, style: linkStyle, recognizer: _privacyRecognizer), - TextSpan(text: context.l10n.and), - TextSpan(text: context.l10n.termsOfService, style: linkStyle, recognizer: _termsRecognizer), - const TextSpan(text: '.'), - ], + final l10n = context.l10n; + final p = OnboardingHero.palette; + final body = OmiType.callout.copyWith(color: p.textSecondary, height: 1.45); + final fine = OmiType.caption.copyWith(color: p.textTertiary, height: 1.3); + final link = fine.copyWith(color: p.textSecondary, decoration: TextDecoration.underline); + return OnboardingHeroStep( + headline: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Semantics( + header: true, + child: Text( + l10n.dataAndPrivacy, + style: OmiType.title1.copyWith(color: p.textPrimary), + textAlign: TextAlign.center, ), ), - ], - footer: [ - const SizedBox(height: OmiSpacing.xl), - OmiButton( - key: const Key('ai_consent_agree'), - label: context.l10n.agreeAndContinue, - expand: true, - onPressed: () { - OmiHaptics.selection(); - widget.onAgree(); - }, + const SizedBox(height: OmiSpacing.xs), + ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 320), + child: Text(l10n.consentShortMessage, style: body, textAlign: TextAlign.center), ), - const SizedBox(height: OmiSpacing.xxs), - OmiButton.tertiary( - key: const Key('ai_consent_use_different_account'), - label: context.l10n.useDifferentAccount, - expand: true, - onPressed: widget.onUseDifferentAccount, + const SizedBox(height: OmiSpacing.xs), + Text( + l10n.consentAiProviders(_aiProviders), + key: const Key('ai_consent_providers'), + style: OmiType.footnote.copyWith(color: p.textTertiary), + textAlign: TextAlign.center, ), ], ), + buttons: [ + OnboardingHeroButton( + key: const Key('ai_consent_agree'), + label: l10n.continueButton, + onPressed: () { + OmiHaptics.selection(); + widget.onAgree(); + }, + ), + ], + // One quiet line where it fits; long translations wrap to a second line. + legal: Text.rich( + TextSpan( + style: fine, + children: [ + TextSpan(text: l10n.byContinuingYouAgree), + TextSpan(text: l10n.termsOfUse, style: link, recognizer: _termsRecognizer), + TextSpan(text: l10n.and), + TextSpan(text: l10n.privacyPolicy, style: link, recognizer: _privacyRecognizer), + const TextSpan(text: '.'), + ], + ), + textAlign: TextAlign.center, + ), ); } } diff --git a/app/lib/pages/onboarding/auth.dart b/app/lib/pages/onboarding/auth.dart index 30818162a41..8ead6825170 100644 --- a/app/lib/pages/onboarding/auth.dart +++ b/app/lib/pages/onboarding/auth.dart @@ -6,6 +6,7 @@ import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:provider/provider.dart'; +import 'package:omi/gen/assets.gen.dart'; import 'package:omi/pages/onboarding/widgets/onboarding_card.dart'; import 'package:omi/providers/auth_provider.dart'; import 'package:omi/ui/ui.dart'; @@ -25,152 +26,89 @@ class _AuthComponentState extends State { Widget build(BuildContext context) { return Consumer( builder: (context, provider, child) { - return OnboardingStep( - card: OnboardingCard( - padding: const EdgeInsets.fromLTRB(OmiSpacing.xxl, 26, OmiSpacing.xxl, OmiSpacing.xs), - content: [ - // Loading indicator or spacing - SizedBox( - height: 20, - child: provider.loading ? const Center(child: OmiSpinner(size: OmiSpinnerSize.small)) : null, + final p = OnboardingHero.palette; + final legalStyle = OmiType.caption.copyWith(color: p.textTertiary, height: 1.3); + return OnboardingHeroStep( + headline: Column( + mainAxisSize: MainAxisSize.min, + children: [ + // The omi wordmark is the headline; the promise sits under it, quieter. + Semantics( + header: true, + label: 'Omi', + child: Assets.images.logoTextWhite.image( + key: const Key('onboarding_wordmark'), + height: 56, + color: p.textPrimary, + ), ), - - // Title text + const SizedBox(height: OmiSpacing.sm), Text( context.l10n.speakTranscribeSummarize, - style: TextStyle( - color: OmiColors.textPrimary, - fontSize: 32, - fontWeight: FontWeight.bold, - height: 1.2, - fontFamily: 'Manrope', - ), + style: OmiType.title3.copyWith(color: p.textSecondary, fontWeight: FontWeight.w500), textAlign: TextAlign.center, ), - - const SizedBox(height: 32), - - // Sign in buttons - if (!provider.isLocalDevProfile && (Platform.isIOS || Platform.isAndroid)) ...[ - SizedBox( - width: double.infinity, - height: 56, - child: ElevatedButton( - onPressed: () { - OmiHaptics.selection(); - provider.onAppleSignIn(widget.onSignIn); - }, - style: ElevatedButton.styleFrom( - backgroundColor: OmiColors.accent, - foregroundColor: OmiColors.onAccent, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(28)), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const FaIcon(FontAwesomeIcons.apple, size: 24), - const SizedBox(width: 8), - Text( - context.l10n.signInWithApple, - style: const TextStyle( - fontSize: 18, - fontWeight: FontWeight.w600, - fontFamily: 'Manrope', - ), - ), - ], - ), - ), - ), - const SizedBox(height: 16), - ], - - // Google sign in button - if (!provider.isLocalDevProfile) - SizedBox( - width: double.infinity, - height: 56, - child: ElevatedButton( - onPressed: () { - OmiHaptics.selection(); - provider.onGoogleSignIn(widget.onSignIn); - }, - style: ElevatedButton.styleFrom( - backgroundColor: OmiColors.accent, - foregroundColor: OmiColors.onAccent, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(28)), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const FaIcon(FontAwesomeIcons.google, size: 20), - const SizedBox(width: 8), - Text( - context.l10n.signInWithGoogle, - style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w600, fontFamily: 'Manrope'), - ), - ], - ), - ), + SizedBox( + height: 28, + child: provider.loading ? const Center(child: OmiSpinner(size: OmiSpinnerSize.small)) : null, + ), + ], + ), + buttons: [ + if (!provider.isLocalDevProfile && (Platform.isIOS || Platform.isAndroid)) ...[ + OnboardingHeroButton( + label: context.l10n.signInWithApple, + icon: const FaIcon(FontAwesomeIcons.apple), + onPressed: () { + OmiHaptics.selection(); + provider.onAppleSignIn(widget.onSignIn); + }, + ), + const SizedBox(height: OmiSpacing.sm), + ], + if (!provider.isLocalDevProfile) + OnboardingHeroButton( + label: context.l10n.signInWithGoogle, + icon: const FaIcon(FontAwesomeIcons.google), + onPressed: () { + OmiHaptics.selection(); + provider.onGoogleSignIn(widget.onSignIn); + }, + ), + // Local development sign-in. Only rendered for a local_dev build: community builds cannot + // complete a real OAuth flow, because provider OAuth clients are bound to the official + // bundle id and a community build is signed with a suffixed one. Never shown in a + // production-family build. + if (provider.isLocalDevProfile) + OnboardingHeroButton( + label: 'Sign in (local dev)', // omi-ux-allow: hardcoded-text -- local_dev builds only + outlined: true, + onPressed: () { + OmiHaptics.selection(); + provider.onLocalDevSignIn(widget.onSignIn); + }, + ), + ], + legal: Text.rich( + TextSpan( + style: legalStyle, + children: [ + TextSpan(text: context.l10n.byContinuingAgree), + TextSpan( + text: context.l10n.privacyPolicy, + style: legalStyle.copyWith(color: p.textSecondary, decoration: TextDecoration.underline), + recognizer: TapGestureRecognizer()..onTap = provider.openPrivacyPolicy, ), - - // Local development sign-in. Only rendered for a local_dev - // build: community builds cannot complete a real OAuth flow, - // because provider OAuth clients are bound to the official - // bundle id and a community build is signed with a suffixed - // one. Never shown in a production-family build. - if (provider.isLocalDevProfile) ...[ - const SizedBox(height: 12), - SizedBox( - width: double.infinity, - height: 56, - child: OutlinedButton( - onPressed: () { - OmiHaptics.selection(); - provider.onLocalDevSignIn(widget.onSignIn); - }, - style: OutlinedButton.styleFrom( - foregroundColor: OmiColors.textPrimary, - side: BorderSide(color: OmiColors.textPrimary.withValues(alpha: 0.4)), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(28)), - ), - child: const Text( - 'Sign in (local dev)', - style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, fontFamily: 'Manrope'), - ), - ), + const TextSpan(text: ' & '), + TextSpan( + text: context.l10n.termsOfUse, + style: legalStyle.copyWith(color: p.textSecondary, decoration: TextDecoration.underline), + recognizer: TapGestureRecognizer()..onTap = provider.openTermsOfService, ), + const TextSpan(text: '.'), ], - - const SizedBox(height: 24), - - // Privacy policy text (same as welcome page) - RichText( - textAlign: TextAlign.center, - text: TextSpan( - style: TextStyle( - color: OmiColors.textPrimary.withValues(alpha: 0.6), - fontSize: 11, - fontFamily: 'Manrope', - ), - children: [ - TextSpan(text: context.l10n.byContinuingAgree), - TextSpan( - text: context.l10n.privacyPolicy, - style: const TextStyle(decoration: TextDecoration.underline), - recognizer: TapGestureRecognizer()..onTap = provider.openPrivacyPolicy, - ), - const TextSpan(text: ' & '), - TextSpan( - text: context.l10n.termsOfUse, - style: const TextStyle(decoration: TextDecoration.underline), - recognizer: TapGestureRecognizer()..onTap = provider.openTermsOfService, - ), - const TextSpan(text: '.'), - ], - ), - ), - ], + ), + textAlign: TextAlign.center, ), ); }, diff --git a/app/lib/pages/onboarding/complete_screen.dart b/app/lib/pages/onboarding/complete_screen.dart index c9b2112db17..0dbaf71e841 100644 --- a/app/lib/pages/onboarding/complete_screen.dart +++ b/app/lib/pages/onboarding/complete_screen.dart @@ -1,240 +1,167 @@ import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; -import 'package:omi/backend/preferences.dart'; -import 'package:omi/backend/schema/bt_device/bt_device.dart'; -import 'package:omi/gen/pigeon_communicator.g.dart'; -import 'package:omi/pages/capture/connect.dart'; -import 'package:omi/providers/capture_provider.dart'; -import 'package:omi/providers/onboarding_provider.dart'; + +import 'package:omi/services/siri_integration.dart'; import 'package:omi/ui/ui.dart'; -import 'package:omi/utils/analytics/registry/events.g.dart'; -import 'package:omi/utils/analytics/registry/typed_events.dart'; + import 'package:omi/utils/l10n_extensions.dart'; -import 'package:omi/utils/platform/platform_manager.dart'; -import 'first_capture_controller.dart'; -import 'first_capture_io.dart'; -import 'first_capture_page.dart'; -import 'first_capture_layout.dart'; - -/// Keeps completion at persisted index 9. Child capture routes never mark -/// onboarding complete until the user sees a result, pairs, or explicitly skips. +import 'package:omi/utils/platform/platform_service.dart'; + class OnboardingCompleteScreen extends StatefulWidget { - const OnboardingCompleteScreen( - {super.key, - required this.onComplete, - this.emit, - this.setProperty, - this.phoneControllerFactory, - this.pairDevice, - this.watchStateProbe, - this.showWatch, - this.onSkipped}); final VoidCallback onComplete; - final VoidCallback? onSkipped; - final void Function(RegisteredEvent)? emit; - final void Function(String, Object)? setProperty; - final FirstCaptureController Function()? phoneControllerFactory; - final Future Function(BuildContext)? pairDevice; - final Future<({bool paired, bool installed})> Function()? watchStateProbe; - final bool? showWatch; + + /// Overrides the iOS 16 App Shortcuts gate for tests; null defers to the platform. + final bool? showSiriHint; + @visibleForTesting + final Future Function()? appShortcutsAvailabilityProbe; + + const OnboardingCompleteScreen({ + super.key, + required this.onComplete, + this.showSiriHint, + this.appShortcutsAvailabilityProbe, + }); @override State createState() => _OnboardingCompleteScreenState(); } -class _OnboardingCompleteScreenState extends State { - bool _routing = false; - bool _pairedOmi = false; - void _emit(RegisteredEvent event) => (widget.emit ?? const TypedEvents().emit)(event); - void _property(String key, Object value) => - (widget.setProperty ?? PlatformManager.instance.analytics.setUserProperty)(key, value); +class _OnboardingCompleteScreenState extends State with SingleTickerProviderStateMixin { + late AnimationController _fadeController; + late Animation _fadeAnimation; + late Animation _scaleAnimation; + late Animation _slideAnimation; + + // App Shortcuts phrases require iOS 16 and a Siri-enabled app build. + late final bool _siriHintVersionSupported = widget.showSiriHint ?? PlatformService.isIOSAtLeast(16); + bool _appShortcutsAvailable = false; + + Future _loadAppShortcutsAvailability() async { + try { + final available = + await (widget.appShortcutsAvailabilityProbe ?? SiriIntegration.instance.appShortcutsAvailable)(); + if (mounted) setState(() => _appShortcutsAvailable = available); + } catch (_) { + // Fail closed: keep the hint hidden when the bridge cannot answer. + } + } @override void initState() { super.initState(); - WidgetsBinding.instance.addPostFrameCallback((_) { - if (mounted) _emit(const OnboardingFirstCaptureShown()); - }); - } - void _choose(OnboardingFirstCaptureChoiceSelectedChoice choice) { - _property('capture_choice', choice.wireName); - // Pairing evidence owns the device property; a selected route is only intent. - final hasOmi = _pairedOmi || - SharedPreferencesUtil().btDevices.any((device) => device.id.isNotEmpty && device.type == DeviceType.omi); - _property('has_omi_device', hasOmi); - _emit(OnboardingFirstCaptureChoiceSelected(choice: choice)); - } + if (_siriHintVersionSupported) _loadAppShortcutsAvailability(); - void _complete() { - // Completion belongs to the persisted wrapper route, even when invoked - // from a phone/Watch child. Replace that owner with Home, not the child. - final owner = ModalRoute.of(context); - if (owner != null) Navigator.of(context).popUntil((route) => route == owner); - widget.onComplete(); - } + _fadeController = AnimationController(duration: const Duration(milliseconds: 800), vsync: this); - void _skip() { - _choose(OnboardingFirstCaptureChoiceSelectedChoice.skipped); - _emit(const OnboardingFirstCaptureSkipped()); - widget.onSkipped?.call(); - _complete(); - } + _fadeAnimation = Tween( + begin: 0.0, + end: 1.0, + ).animate(CurvedAnimation(parent: _fadeController, curve: Curves.easeOut)); - Future _omi() async { - if (_routing) return; - _choose(OnboardingFirstCaptureChoiceSelectedChoice.omi); - setState(() => _routing = true); - bool paired; - if (widget.pairDevice != null) { - paired = await widget.pairDevice!(context); - } else { - final connected = await Navigator.of(context).push(omiPageRoute( - builder: (routeContext) => ConnectDevicePage( - onConnected: () => Navigator.of(routeContext).pop(true), - ), - )); - if (!mounted) return; - paired = connected == true && - context.read().isConnected && - context.read().deviceType == DeviceType.omi; - } - if (!mounted) return; - setState(() => _routing = false); - if (paired) { - _pairedOmi = true; - _property('has_omi_device', true); - _complete(); - } // Home owns the existing CV1 tutorial gate. - } + _scaleAnimation = Tween( + begin: 0.5, + end: 1.0, + ).animate(CurvedAnimation(parent: _fadeController, curve: Curves.elasticOut)); - Future _phone() async { - if (_routing) return; - _choose(OnboardingFirstCaptureChoiceSelectedChoice.phone); - setState(() => _routing = true); - final controller = widget.phoneControllerFactory?.call() ?? - FirstCaptureController(PhoneFirstCaptureIO(context.read()), emit: _emit); - await Navigator.of(context).push( - omiPageRoute(builder: (_) => FirstCapturePage(controller: controller, onComplete: _complete, onSkip: _skip))); - await controller.leave(); - controller.dispose(); - if (mounted) setState(() => _routing = false); - } + _slideAnimation = Tween( + begin: 50.0, + end: 0.0, + ).animate(CurvedAnimation(parent: _fadeController, curve: Curves.easeOut)); - Future _watch() async { - if (_routing) return; - _choose(OnboardingFirstCaptureChoiceSelectedChoice.appleWatch); - setState(() => _routing = true); - ({bool paired, bool installed})? state; - try { - state = await (widget.watchStateProbe ?? - () async { - final host = WatchRecorderHostAPI(); - return (paired: await host.isWatchPaired(), installed: await host.isWatchAppInstalled()); - })() - .timeout(const Duration(seconds: 2)); - } catch (_) {/* Instructions are useful even when WatchConnectivity is unavailable. */} - if (!mounted) return; - await Navigator.of(context).push(omiPageRoute( - builder: (_) => - FirstCaptureWatchInstructions(paired: state?.paired, installed: state?.installed, onComplete: _complete))); - if (mounted) setState(() => _routing = false); + Future.delayed(const Duration(milliseconds: 200), () { + if (mounted) _fadeController.forward(); + }); } @override - Widget build(BuildContext context) { - final watch = widget.showWatch ?? Theme.of(context).platform == TargetPlatform.iOS; - return ColoredBox( - color: OmiColors.surface0, - child: FirstCaptureLayout(content: [ - Icon(Icons.mic_none, size: 64, color: OmiColors.textPrimary), - const SizedBox(height: OmiSpacing.lg), - Text(context.l10n.firstCaptureTitle, style: OmiType.title1, textAlign: TextAlign.center), - const SizedBox(height: OmiSpacing.md), - Text(context.l10n.firstCaptureDescription, - style: OmiType.body.copyWith(color: OmiColors.textSecondary), textAlign: TextAlign.center), - const SizedBox(height: OmiSpacing.xxl), - ], actions: [ - OmiButton( - variant: OmiButtonVariant.secondary, - labelStyle: OmiType.callout.copyWith( - fontWeight: FontWeight.w600, fontFamily: Theme.of(context).textTheme.labelLarge?.fontFamily), - key: const Key('first_capture_omi'), - label: context.l10n.firstCaptureOmi, - icon: Icons.bluetooth, - expand: true, - onPressed: _routing ? null : _omi), - const SizedBox(height: OmiSpacing.md), - if (watch) ...[ - OmiButton( - variant: OmiButtonVariant.secondary, - labelStyle: OmiType.callout.copyWith( - fontWeight: FontWeight.w600, fontFamily: Theme.of(context).textTheme.labelLarge?.fontFamily), - key: const Key('first_capture_watch'), - label: context.l10n.firstCaptureWatch, - icon: Icons.watch_outlined, - expand: true, - onPressed: _routing ? null : _watch), - const SizedBox(height: OmiSpacing.md), - ], - OmiButton( - labelStyle: OmiType.callout.copyWith( - fontWeight: FontWeight.w600, fontFamily: Theme.of(context).textTheme.labelLarge?.fontFamily), - key: const Key('first_capture_phone'), - label: context.l10n.firstCapturePhone, - icon: Icons.phone_iphone, - expand: true, - onPressed: _routing ? null : _phone), - const SizedBox(height: OmiSpacing.lg), - OmiButton( - variant: OmiButtonVariant.tertiary, - labelStyle: OmiType.callout.copyWith( - fontWeight: FontWeight.w600, fontFamily: Theme.of(context).textTheme.labelLarge?.fontFamily), - key: const Key('onboarding_complete_start'), - label: context.l10n.firstCaptureSkip, - onPressed: _routing ? null : _skip), - ])); + void dispose() { + _fadeController.dispose(); + super.dispose(); } -} -class FirstCaptureWatchInstructions extends StatelessWidget { - const FirstCaptureWatchInstructions({super.key, this.paired, this.installed, required this.onComplete}); - final bool? paired; - final bool? installed; - final VoidCallback onComplete; @override - Widget build(BuildContext context) => Scaffold( - appBar: AppBar(leading: const OmiBackButton(), title: Text(context.l10n.firstCaptureWatch)), - body: FirstCaptureLayout(content: [ - Icon(Icons.watch_outlined, size: 64, color: OmiColors.textPrimary), - const SizedBox(height: OmiSpacing.lg), - Text(context.l10n.watchCaptureTitle, style: OmiType.title2, textAlign: TextAlign.center), - const SizedBox(height: OmiSpacing.xl), - if (paired == false || installed == false) - Text(paired == false ? context.l10n.firstCaptureWatchPair : context.l10n.firstCaptureWatchInstall, - style: OmiType.body.copyWith(color: OmiColors.textSecondary), textAlign: TextAlign.center) - else - for (final step in [ - context.l10n.watchCaptureOpen, - context.l10n.watchCaptureRecord, - context.l10n.watchCaptureSync - ].indexed) - Padding( - padding: const EdgeInsets.only(bottom: OmiSpacing.md), - child: Row(crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text('${step.$1 + 1}.', style: OmiType.body), - const SizedBox(width: OmiSpacing.sm), - Expanded(child: Text(step.$2, style: OmiType.body.copyWith(color: OmiColors.textSecondary))), - ]), + Widget build(BuildContext context) { + return Container( + color: OmiColors.surface0, + width: double.infinity, + height: double.infinity, + child: SafeArea( + child: AnimatedBuilder( + animation: _fadeController, + builder: (context, child) { + return FadeTransition( + opacity: _fadeAnimation, + child: Transform.translate( + offset: Offset(0, _slideAnimation.value), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 40), + // Scrollable so large text scales never push the CTA off-screen; the + // ConstrainedBox+IntrinsicHeight pair keeps the centered Spacer layout + // when content is shorter than the viewport (ux-contract §16). + child: LayoutBuilder( + builder: (context, constraints) => SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints(minHeight: constraints.maxHeight), + child: IntrinsicHeight( + child: Column( + children: [ + const Spacer(flex: 3), + ScaleTransition( + scale: _scaleAnimation, + child: Container( + width: 72, + height: 72, + decoration: BoxDecoration(color: OmiColors.surface1, borderRadius: OmiRadius.xlAll), + child: Icon(Icons.check_rounded, color: OmiColors.textPrimary, size: 36), + ), + ), + const SizedBox(height: OmiSpacing.xxl), + Semantics( + header: true, + child: Text( + context.l10n.onboardingYoureAllSet, + style: OmiType.title1.copyWith(height: 1.2), + textAlign: TextAlign.center, + ), + ), + const SizedBox(height: OmiSpacing.md), + Text( + context.l10n.onboardingCompleteMessage, + textAlign: TextAlign.center, + style: OmiType.body.copyWith(color: OmiColors.textSecondary, height: 1.5), + ), + if (_siriHintVersionSupported && _appShortcutsAvailable) ...[ + const SizedBox(height: OmiSpacing.md), + Text( + context.l10n.siriShortcutsSetupHint('Ask Omi', 'Question for Omi'), + key: const Key('onboarding_siri_hint'), + textAlign: TextAlign.center, + style: OmiType.body.copyWith(color: OmiColors.textSecondary), + ), + ], + const Spacer(flex: 3), + OmiButton( + key: const Key('onboarding_complete_start'), + label: context.l10n.startUsingOmi, + expand: true, + onPressed: () { + OmiHaptics.success(); + widget.onComplete(); + }, + ), + const SizedBox(height: OmiSpacing.xxl), + ], + ), + ), + ), + ), + ), + ), ), - ], actions: [ - OmiButton( - key: const Key('watch_capture_continue'), - labelStyle: OmiType.callout.copyWith( - fontWeight: FontWeight.w600, fontFamily: Theme.of(context).textTheme.labelLarge?.fontFamily), - label: context.l10n.startUsingOmi, - expand: true, - onPressed: onComplete), - ]), - ); + ); + }, + ), + ), + ); + } } diff --git a/app/lib/pages/onboarding/connect_omi/connect_omi_step.dart b/app/lib/pages/onboarding/connect_omi/connect_omi_step.dart new file mode 100644 index 00000000000..38f0b8a222d --- /dev/null +++ b/app/lib/pages/onboarding/connect_omi/connect_omi_step.dart @@ -0,0 +1,493 @@ +import 'dart:async'; +import 'dart:math' as math; + +import 'package:flutter/material.dart'; + +import 'package:provider/provider.dart'; + +import 'package:omi/gen/assets.gen.dart'; +import 'package:omi/pages/onboarding/find_device/found_devices.dart'; +import 'package:omi/pages/shop/omi_shop.dart'; +import 'package:omi/providers/onboarding_provider.dart'; +import 'package:omi/services/devices/bluetooth_readiness.dart'; +import 'package:omi/ui/ui.dart'; +import 'package:omi/utils/l10n_extensions.dart'; +import 'package:omi/utils/platform/platform_manager.dart'; +import 'package:omi/widgets/connection_guide_sheet.dart'; +import 'package:omi/widgets/device_widget.dart'; +import 'package:omi/widgets/scanning_ripple.dart'; +import 'package:omi/pages/onboarding/widgets/onboarding_card.dart'; + +/// What the reader told the connect step about their Omi. Sent as `choice` on +/// `Onboarding Device Choice`. +enum OnboardingDeviceChoice { + connected('connected'), + ordered('ordered'), + notOrdered('not_ordered'); + + const OnboardingDeviceChoice(this.wireValue); + final String wireValue; +} + +/// First-run "Connect your Omi" step. +/// +/// The search started in the background right after sign-in, so a reader whose Omi is on and nearby +/// sees it revealed as the step opens. Otherwise the step searches with instructions, and readers +/// without their Omi say whether it is on its way or not ordered yet. +class ConnectOmiStep extends StatefulWidget { + const ConnectOmiStep({super.key, required this.onDone, this.openShop = openOmiShop}); + + /// Called once with how the reader left the step. + final void Function(OnboardingDeviceChoice choice) onDone; + + /// Opens the Omi store for "Not ordered yet"; replaceable in tests (no web view there). + final Future Function(BuildContext context, {required String source}) openShop; + + @override + State createState() => _ConnectOmiStepState(); +} + +class _ConnectOmiStepState extends State with TickerProviderStateMixin { + OnboardingProvider? _provider; + bool _done = false; + + /// The arrival: the device rises and grows in, then the title, the line under it and the links + /// fade up one after another. A light tap marks the moment the device lands. + late final AnimationController _entrance = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 1400), + ); + late final Animation _device = CurvedAnimation( + parent: _entrance, + curve: const Interval(0.0, 0.55, curve: Curves.easeOutBack), + ); + late final Animation _title = CurvedAnimation( + parent: _entrance, + curve: const Interval(0.35, 0.7, curve: Curves.easeOutCubic), + ); + late final Animation _line = CurvedAnimation( + parent: _entrance, + curve: const Interval(0.45, 0.8, curve: Curves.easeOutCubic), + ); + late final Animation _links = CurvedAnimation( + parent: _entrance, + curve: const Interval(0.7, 1.0, curve: Curves.easeOut), + ); + Timer? _landingTap; + + /// The moment the Omi connects: a ring draws around it, a ✓ lands on the ring and one soft pulse + /// goes out, the title says Connected, then the step moves on by itself. + late final AnimationController _celebration = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 1100), + ); + bool _celebrating = false; + + @override + void initState() { + super.initState(); + _provider = context.read(); + WidgetsBinding.instance.addPostFrameCallback((_) { + if (!mounted) return; + if (OmiMotion.of(context).emphasized == Duration.zero) { + _entrance.value = 1; + _scan(); + } else { + // Let the arrival play before any Bluetooth guidance can cover it. A search started after + // sign-in keeps finding devices in the meantime. + _entrance.forward().whenComplete(() { + if (mounted) _scan(); + }); + _landingTap = Timer(const Duration(milliseconds: 620), () { + if (mounted) OmiHaptics.selection(); + }); + } + }); + } + + /// Fades [child] up from slightly below as [animation] runs. + Widget _rise(Animation animation, Widget child, {double distance = 12}) { + return AnimatedBuilder( + animation: animation, + child: child, + builder: (context, child) { + final t = animation.value.clamp(0.0, 1.0); + return Opacity( + opacity: t, + child: Transform.translate(offset: Offset(0, (1 - t) * distance), child: child), + ); + }, + ); + } + + @override + void dispose() { + _landingTap?.cancel(); + _entrance.dispose(); + _celebration.dispose(); + _provider?.endSearch(); + _provider = null; + super.dispose(); + } + + /// A missing permission goes through the shared Bluetooth guidance prompt, like the pairing page. + Future _scan() async { + void publishGuidance() { + if (mounted) unawaited(BluetoothReadiness.instance.ensureReady(BluetoothUse.discovery)); + } + + await _provider?.scanDevices(onShowDialog: publishGuidance, onShowLocationDialog: publishGuidance); + } + + Future _scanAgain() async { + OmiHaptics.selection(); + _provider?.cancelActiveScan(); + _provider?.enableInstructions = false; + setState(() {}); + await _scan(); + } + + /// "Not ordered yet": the Omi store opens inside the app; closing it carries on with onboarding. + bool _shopOpen = false; + Future _notOrdered() async { + if (_done || _shopOpen) return; + _shopOpen = true; + OmiHaptics.selection(); + await widget.openShop(context, source: 'onboarding_not_ordered'); + _shopOpen = false; + if (mounted) _finish(OnboardingDeviceChoice.notOrdered); + } + + void _finish(OnboardingDeviceChoice choice) { + if (_done) return; + _done = true; + PlatformManager.instance.analytics.onboardingDeviceChoice(choice.wireValue); + widget.onDone(choice); + } + + /// Connected: hold the step for the celebration (long enough to read "Connected"), then move on. + Future _connected() async { + if (_done) return; + _done = true; + PlatformManager.instance.analytics.onboardingDeviceChoice(OnboardingDeviceChoice.connected.wireValue); + final still = OmiMotion.of(context).emphasized == Duration.zero; + setState(() => _celebrating = true); + if (still) { + _celebration.value = 1; + } else { + await _celebration.forward(); + } + await Future.delayed(Duration(milliseconds: still ? 1200 : 700)); + if (mounted) widget.onDone(OnboardingDeviceChoice.connected); + } + + void _showConnectionGuide() { + PlatformManager.instance.analytics.connectionGuideOpened(); + ConnectionGuideSheet.show(context); + } + + bool _feltFound = false; + bool _feltConnected = false; + + /// A light tap when the Omi appears, a firmer one when it is connected: felt, not only seen. + void _feelProgress({required bool found, required bool connected}) { + if (found && !_feltFound) { + _feltFound = true; + WidgetsBinding.instance.addPostFrameCallback((_) => OmiHaptics.light()); + } + if (connected && !_feltConnected) { + _feltConnected = true; + WidgetsBinding.instance.addPostFrameCallback((_) => OmiHaptics.success()); + } + } + + @override + Widget build(BuildContext context) { + final l10n = context.l10n; + final compact = MediaQuery.sizeOf(context).height <= 700; + final stage = compact ? 220.0 : 260.0; + return Consumer( + builder: (context, provider, child) { + final devices = provider.visibleDeviceList; + final found = provider.isConnected || devices.isNotEmpty; + final cantFind = !found && provider.enableInstructions; + final shown = devices.isNotEmpty ? devices.first : null; + _feelProgress(found: found, connected: provider.isConnected); + + final Widget visual = found + ? TweenAnimationBuilder( + // The reveal: the found device grows in from the search. + key: ValueKey('connect_omi_reveal_${shown?.id ?? provider.deviceId}'), + tween: Tween(begin: 0, end: 1), + duration: OmiMotion.of(context).emphasized == Duration.zero + ? Duration.zero + : const Duration(milliseconds: 700), + curve: Curves.easeOutBack, + builder: (context, t, child) => Opacity( + opacity: t.clamp(0.0, 1.0), + child: Transform.scale(scale: 0.6 + 0.4 * t, child: child), + ), + child: DeviceAnimationWidget( + sizeMultiplier: compact ? 0.65 : 0.75, + isConnected: provider.isConnected, + deviceName: provider.isConnected ? provider.deviceName : shown?.name, + deviceType: provider.isConnected ? provider.deviceType : shown?.type, + modelNumber: shown?.modelNumber, + ), + ) + : Stack( + alignment: Alignment.center, + children: [ + if (!cantFind) ScanningRippleWidget(isScanning: true, size: stage, color: OmiColors.textTertiary), + Assets.images.omiWithoutRopeTurnedOff.image(width: stage * 0.42, height: stage * 0.42), + ], + ); + + final String subtitle = provider.isConnected + ? (provider.deviceName.isNotEmpty ? provider.deviceName : l10n.connected) + : found + ? l10n.onboardingConnectTapToConnect + : cantFind + ? l10n.cantFindDeviceHint + : l10n.pairingDescOmi; + + return SafeArea( + child: Padding( + padding: const EdgeInsets.fromLTRB(OmiSpacing.xl, 0, OmiSpacing.xl, kOnboardingButtonBottomGap), + child: Column( + children: [ + Expanded( + child: Center( + child: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + AnimatedBuilder( + animation: _device, + child: SizedBox( + height: stage, + child: _ConnectedHalo( + progress: _celebration, + active: _celebrating, + diameter: stage * 0.86, + child: Center(child: visual), + ), + ), + builder: (context, child) { + final t = _device.value; + return Opacity( + opacity: t.clamp(0.0, 1.0), + child: Transform.translate( + offset: Offset(0, (1 - t) * 24), + child: Transform.scale(scale: 0.7 + 0.3 * t, child: child), + ), + ); + }, + ), + const SizedBox(height: OmiSpacing.xxl), + _rise( + _title, + Semantics( + header: true, + liveRegion: true, + // The title becomes the news once the Omi is connected. + child: AnimatedSwitcher( + duration: OmiMotion.of(context).standard, + transitionBuilder: (child, animation) => FadeTransition( + opacity: animation, + child: SlideTransition( + position: Tween(begin: const Offset(0, 0.25), end: Offset.zero).animate(animation), + child: child, + ), + ), + child: Text( + _celebrating ? l10n.connected : l10n.onboardingConnectOmi, + key: ValueKey(_celebrating), + style: OmiType.title1, + textAlign: TextAlign.center, + ), + ), + ), + ), + const SizedBox(height: OmiSpacing.xs), + // A narrow measure so the instruction wraps evenly instead of leaving one word behind. + _rise( + _line, + ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 280), + child: Text( + subtitle, + key: const Key('connect_omi_status'), + style: OmiType.subhead.copyWith(color: OmiColors.textSecondary, height: 1.35), + textAlign: TextAlign.center, + ), + ), + ), + const SizedBox(height: OmiSpacing.lg), + // The tappable device rows; stays mounted because it owns the connection handling. + FoundDevices( + goNext: _connected, + isFromOnboarding: true, + onRescan: _scanAgain, + showStatus: false, + ), + if (cantFind) ...[ + OmiButton( + key: const Key('connect_omi_scan_again'), + label: l10n.scanAgain, + icon: Icons.refresh, + onPressed: _scanAgain, + ), + OmiButton.tertiary( + key: const Key('connect_omi_how_to_pair'), + label: l10n.howToPair, + onPressed: _showConnectionGuide, + ), + ], + ], + ), + ), + ), + ), + // Once the reader is connecting their own Omi, "Don't have your Omi yet?" no longer applies. + // It fades out but keeps its space, so the device and title above don't jump. + AnimatedOpacity( + opacity: provider.isConnected || provider.isClicked ? 0 : 1, + duration: const Duration(milliseconds: 200), + child: IgnorePointer( + ignoring: provider.isConnected || provider.isClicked, + child: _rise( + _links, + Column(mainAxisSize: MainAxisSize.min, children: [ + Text( + l10n.onboardingNoOmiYet, + style: OmiType.footnote.copyWith(color: OmiColors.textTertiary), + textAlign: TextAlign.center, + ), + // Quiet links, not buttons: they must not compete with the device above. + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Flexible( + child: OmiButton.tertiary( + key: const Key('connect_omi_ordered'), + label: l10n.onboardingOmiOrdered, + size: OmiButtonSize.compact, + onPressed: () => _finish(OnboardingDeviceChoice.ordered), + ), + ), + Container(width: 1, height: 14, color: OmiColors.border), + Flexible( + child: OmiButton.tertiary( + key: const Key('connect_omi_not_ordered'), + label: l10n.onboardingOmiNotOrdered, + size: OmiButtonSize.compact, + onPressed: _notOrdered, + ), + ), + ], + ), + ]), + distance: 8), + ), + ), + ], + ), + ), + ); + }, + ); + } +} + +/// Around the connected Omi: a ring that draws itself, a ✓ that lands where the ring closes, and +/// one pulse outward. Nothing shows until [active]. +class _ConnectedHalo extends StatelessWidget { + const _ConnectedHalo({required this.progress, required this.active, required this.diameter, required this.child}); + + final Animation progress; + final bool active; + final double diameter; + final Widget child; + + static const _badge = 40.0; + + @override + Widget build(BuildContext context) { + if (!active) return child; + return AnimatedBuilder( + animation: progress, + child: child, + builder: (context, child) { + final t = progress.value; + final ring = Curves.easeInOutCubic.transform((t / 0.55).clamp(0.0, 1.0)); + final badge = Curves.easeOutBack.transform(((t - 0.45) / 0.35).clamp(0.0, 1.0)); + final pulse = Curves.easeOut.transform(((t - 0.5) / 0.5).clamp(0.0, 1.0)); + // The ✓ sits on the ring at its lower right, where the drawing ends. + const angle = math.pi / 4; + final r = diameter / 2; + return Stack( + alignment: Alignment.center, + clipBehavior: Clip.none, + children: [ + // One pulse outward as the ✓ lands. + if (pulse > 0 && pulse < 1) + Container( + width: diameter * (1 + 0.22 * pulse), + height: diameter * (1 + 0.22 * pulse), + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all(color: OmiColors.textPrimary.withValues(alpha: 0.3 * (1 - pulse)), width: 2), + ), + ), + SizedBox.square( + dimension: diameter, + child: CustomPaint(painter: _RingPainter(progress: ring, color: OmiColors.textPrimary)), + ), + child!, + Transform.translate( + offset: Offset(r * math.cos(angle), r * math.sin(angle)), + child: Transform.scale( + scale: badge, + child: Container( + key: const Key('connect_omi_connected_badge'), + width: _badge, + height: _badge, + decoration: BoxDecoration( + color: OmiColors.accent, + shape: BoxShape.circle, + border: Border.all(color: OmiColors.surface0, width: 3), + ), + child: Icon(Icons.check_rounded, size: 22, color: OmiColors.onAccent), + ), + ), + ), + ], + ); + }, + ); + } +} + +class _RingPainter extends CustomPainter { + _RingPainter({required this.progress, required this.color}); + + final double progress; + final Color color; + + @override + void paint(Canvas canvas, Size size) { + if (progress <= 0) return; + final rect = Offset.zero & size; + final paint = Paint() + ..color = color + ..style = PaintingStyle.stroke + ..strokeWidth = 2.5 + ..strokeCap = StrokeCap.round; + // Starts and closes under the ✓ (lower right), clockwise. + canvas.drawArc(rect.deflate(1.25), math.pi / 4, 2 * math.pi * progress, false, paint); + } + + @override + bool shouldRepaint(covariant _RingPainter old) => old.progress != progress || old.color != color; +} diff --git a/app/lib/pages/onboarding/device_selection.dart b/app/lib/pages/onboarding/device_selection.dart deleted file mode 100644 index 86fabe9d01a..00000000000 --- a/app/lib/pages/onboarding/device_selection.dart +++ /dev/null @@ -1,214 +0,0 @@ -import 'package:flutter/material.dart'; - -import 'package:video_player/video_player.dart'; - -import 'package:omi/pages/onboarding/wrapper.dart'; -import 'package:omi/ui/omi_tokens.dart'; -import 'package:omi/utils/platform/platform_manager.dart'; -import 'package:omi/utils/l10n_extensions.dart'; -import 'package:omi/utils/logger.dart'; -import 'package:omi/utils/other/temp.dart'; - -class DeviceSelectionPage extends StatefulWidget { - const DeviceSelectionPage({super.key}); - - @override - State createState() => _DeviceSelectionPageState(); -} - -class _DeviceSelectionPageState extends State with SingleTickerProviderStateMixin { - late final AnimationController _controller; - late Animation _arrowAnimation; - late VideoPlayerController _videoController; - bool _isVideoInitialized = false; - double _videoProgress = 0.0; - - @override - void initState() { - super.initState(); - _controller = AnimationController(duration: const Duration(milliseconds: 600), vsync: this)..repeat(reverse: true); - - _arrowAnimation = Tween( - begin: 0, - end: 4, - ).animate(CurvedAnimation(parent: _controller, curve: Curves.easeInOut)); - - _initializeVideo(); - PlatformManager.instance.analytics.onboardingWelcomeShown(); - } - - void _initializeVideo() async { - _videoController = VideoPlayerController.asset('assets/images/onboarding.mp4'); - try { - await _videoController.initialize(); - setState(() { - _isVideoInitialized = true; - }); - _videoController.setLooping(true); - _videoController.play(); - - // Listen to video progress - _videoController.addListener(_updateVideoProgress); - } catch (e) { - Logger.debug('Error initializing video: $e'); - } - } - - void _updateVideoProgress() { - if (_videoController.value.isInitialized) { - final position = _videoController.value.position; - final duration = _videoController.value.duration; - if (duration.inMilliseconds > 0) { - setState(() { - _videoProgress = position.inMilliseconds / duration.inMilliseconds; - }); - } - } - } - - @override - void dispose() { - _controller.dispose(); - _videoController.removeListener(_updateVideoProgress); - _videoController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return Material( - child: Scaffold( - backgroundColor: Colors.white, - body: Stack( - children: [ - // Main content - Column( - children: [ - // Video background area - constrained to available space - Expanded( - child: Container( - width: double.infinity, - margin: const EdgeInsets.only(top: 60, bottom: 20), - child: _isVideoInitialized - ? FittedBox( - fit: BoxFit.cover, - child: SizedBox( - width: _videoController.value.size.width, - height: _videoController.value.size.height, - child: VideoPlayer(_videoController), - ), - ) - : Container(color: Colors.white), - ), - ), - - // Bottom drawer card - wraps content - Container( - width: double.infinity, - padding: const EdgeInsets.fromLTRB(32, 24, 32, 20), - decoration: const BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.only(topLeft: Radius.circular(40), topRight: Radius.circular(40)), - ), - child: SafeArea( - top: false, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - // Title and subtitle - Text( - context.l10n.omiYourAiCompanion, - style: const TextStyle( - color: Colors.black, - fontSize: 34, - fontWeight: FontWeight.bold, - height: 1.2, - fontFamily: 'Manrope', - ), - textAlign: TextAlign.center, - ), - const SizedBox(height: 12), - Text( - context.l10n.captureEveryMoment, - style: TextStyle( - color: Colors.black.withValues(alpha: 0.7), - fontSize: 16, - height: 1.4, - fontFamily: 'Manrope', - ), - textAlign: TextAlign.center, - ), - - const SizedBox(height: 20), - - // Continue button - SizedBox( - width: double.infinity, - height: 56, - child: ElevatedButton( - onPressed: () async { - OmiHaptics.selection(); - PlatformManager.instance.analytics.onboardingGetStartedTapped(); - if (mounted) { - routeToPage(context, const OnboardingWrapper()); - } - }, - style: ElevatedButton.styleFrom( - backgroundColor: Colors.black, - foregroundColor: Colors.white, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(28)), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - context.l10n.getStarted, - style: const TextStyle( - fontSize: 18, - fontWeight: FontWeight.w600, - fontFamily: 'Manrope', - ), - ), - const SizedBox(width: 8), - AnimatedBuilder( - animation: _arrowAnimation, - builder: (context, child) { - return Transform.translate( - offset: Offset(_arrowAnimation.value, 0), - child: const Icon(Icons.arrow_forward, size: 20), - ); - }, - ), - ], - ), - ), - ), - ], - ), - ), - ), - ], - ), - - // Video progress indicator in top right - if (_isVideoInitialized) - Positioned( - top: 60, - right: 20, - child: SizedBox( - width: 18, - height: 18, - child: CircularProgressIndicator( - value: _videoProgress, - strokeWidth: 2, - backgroundColor: Colors.grey.withValues(alpha: 0.3), - valueColor: AlwaysStoppedAnimation(Colors.grey[700]!), - ), - ), - ), - ], - ), - ), - ); - } -} diff --git a/app/lib/pages/onboarding/find_device/found_devices.dart b/app/lib/pages/onboarding/find_device/found_devices.dart index 593c85a2921..785b3bf0242 100644 --- a/app/lib/pages/onboarding/find_device/found_devices.dart +++ b/app/lib/pages/onboarding/find_device/found_devices.dart @@ -23,7 +23,6 @@ import 'package:omi/utils/l10n_extensions.dart'; import 'package:omi/utils/logger.dart'; import 'package:omi/widgets/apple_watch_setup_bottom_sheet.dart'; import 'package:omi/widgets/connection_guide_sheet.dart'; -import 'pairing_failure_card.dart'; @visibleForTesting Future retryOfflineSavedDevice(Future Function() connect) => connect(); @@ -289,8 +288,16 @@ class _FoundDevicesState extends State { final visibleDevices = provider.visibleDeviceList; return MessageListener( showError: (error) { - // The inline card owns connect-failure recovery and its shown event. - if (error != 'DEVICE_CONNECT_FAILED') OmiFeedback.error(context, error); + if (error == 'DEVICE_CONNECT_FAILED') { + OmiFeedback.error( + context, + context.l10n.anErrorOccurredTryAgain, + actionLabel: context.l10n.howToPair, + onAction: () => ConnectionGuideSheet.show(context), + ); + } else { + OmiFeedback.error(context, error); + } }, showInfo: (info) { if (info == "DEVICE_CONNECTED") { @@ -309,7 +316,8 @@ class _FoundDevicesState extends State { mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.center, children: [ - if (!widget.showStatus && !provider.isConnected) + // Without the status line (onboarding), the host shows the state itself. + if (!widget.showStatus) const SizedBox.shrink() else if (!provider.isConnected) Text( @@ -320,20 +328,9 @@ class _FoundDevicesState extends State { ) else Text(context.l10n.pairingSuccessful, style: OmiType.footnote.copyWith(color: OmiColors.textSecondary)), - if (provider.pairingFailureReason != null && !provider.isConnected) - Padding( - padding: const EdgeInsets.all(OmiSpacing.lg), - child: PairingFailureCard( - key: ValueKey(provider.pairingFailureVersion), - reason: provider.pairingFailureReason!, - onDismiss: provider.dismissPairingFailure, - onRetry: widget.onRescan, - onGuide: () => ConnectionGuideSheet.show(context), - ), - ), if (visibleDevices.isNotEmpty) const SizedBox(height: 16), if (!provider.isConnected) ..._devicesList(provider), - if (provider.isConnected) + if (provider.isConnected && widget.showStatus) Text( () { final sameNameCount = provider.visibleDeviceList.where((d) => d.name == provider.deviceName).length; diff --git a/app/lib/pages/onboarding/find_device/page.dart b/app/lib/pages/onboarding/find_device/page.dart index e77dbcfbdc3..b9d7ed5cb9f 100644 --- a/app/lib/pages/onboarding/find_device/page.dart +++ b/app/lib/pages/onboarding/find_device/page.dart @@ -86,8 +86,7 @@ class _FindDevicesPageState extends State { Widget build(BuildContext context) { return Consumer( builder: (context, provider, child) { - final cantFind = - provider.deviceList.isEmpty && provider.enableInstructions && provider.pairingFailureReason == null; + final cantFind = provider.deviceList.isEmpty && provider.enableInstructions; final notNow = widget.includeSkip ? OmiButton.tertiary( key: const Key('find_devices_not_now'), @@ -112,7 +111,7 @@ class _FindDevicesPageState extends State { goNext: widget.goNext, isFromOnboarding: widget.isFromOnboarding, onRescan: _scanAgain, - showStatus: !cantFind && provider.pairingFailureReason == null, + showStatus: !cantFind, ), // Nothing found after a while: troubleshooting first, support last (onboarding-home #13). // The pairing guide has no support route, so Contact Support stays as a small link. @@ -127,17 +126,12 @@ class _FindDevicesPageState extends State { children: [ OmiButton( key: const Key('find_devices_scan_again'), - labelStyle: OmiType.callout.copyWith( - fontWeight: FontWeight.w600, fontFamily: Theme.of(context).textTheme.labelLarge?.fontFamily), label: context.l10n.scanAgain, icon: Icons.refresh, onPressed: _scanAgain, ), const SizedBox(height: OmiSpacing.xs), - OmiButton( - variant: OmiButtonVariant.secondary, - labelStyle: OmiType.callout.copyWith( - fontWeight: FontWeight.w600, fontFamily: Theme.of(context).textTheme.labelLarge?.fontFamily), + OmiButton.secondary( key: const Key('find_devices_how_to_pair'), label: context.l10n.howToPair, onPressed: _showConnectionGuide, @@ -150,8 +144,7 @@ class _FindDevicesPageState extends State { style: TextButton.styleFrom( foregroundColor: OmiColors.textSecondary, minimumSize: const Size(kOmiMinTapTarget, kOmiMinTapTarget), - textStyle: - OmiType.footnote.copyWith(fontFamily: Theme.of(context).textTheme.labelLarge?.fontFamily), + textStyle: OmiType.footnote, ), child: Text(context.l10n.contactSupportAction), ), diff --git a/app/lib/pages/onboarding/find_device/pairing_failure_card.dart b/app/lib/pages/onboarding/find_device/pairing_failure_card.dart deleted file mode 100644 index 9136961f530..00000000000 --- a/app/lib/pages/onboarding/find_device/pairing_failure_card.dart +++ /dev/null @@ -1,108 +0,0 @@ -import 'package:flutter/material.dart'; - -import 'package:omi/l10n/app_localizations.dart'; -import 'package:omi/ui/ui.dart'; -import 'package:omi/utils/analytics/registry/events.g.dart'; -import 'package:omi/utils/analytics/registry/typed_events.dart'; -import 'package:omi/utils/l10n_extensions.dart'; - -String pairingFailureCopy(AppLocalizations l10n, DeviceConnectFailureShownFailureReason reason) => switch (reason) { - DeviceConnectFailureShownFailureReason.bluetoothOff => l10n.pairingBluetoothOff, - DeviceConnectFailureShownFailureReason.bluetoothPermissionDenied => l10n.pairingBluetoothPermission, - DeviceConnectFailureShownFailureReason.nearbyDevicesDenied => l10n.pairingNearbyPermission, - DeviceConnectFailureShownFailureReason.locationPermissionMissing => l10n.pairingLocationPermission, - DeviceConnectFailureShownFailureReason.notFoundOrDropped => l10n.pairingNotFoundOrDropped, - DeviceConnectFailureShownFailureReason.staleBond => l10n.pairingBondRecovery, - DeviceConnectFailureShownFailureReason.unknown => l10n.anErrorOccurredTryAgain, - }; - -/// Inline recovery stays beside discovery and does not expire like a toast. -/// Emit after the first displayed frame, once per presentation/reason. -class PairingFailureCard extends StatefulWidget { - const PairingFailureCard( - {super.key, required this.reason, required this.onDismiss, required this.onGuide, this.onRetry, this.emit}); - final DeviceConnectFailureShownFailureReason reason; - final VoidCallback onDismiss; - final VoidCallback onGuide; - final Future Function()? onRetry; - final void Function(RegisteredEvent)? emit; - - @override - State createState() => _PairingFailureCardState(); -} - -class _PairingFailureCardState extends State { - @override - void initState() { - super.initState(); - _recordShown(); - } - - @override - void didUpdateWidget(PairingFailureCard oldWidget) { - super.didUpdateWidget(oldWidget); - if (oldWidget.reason != widget.reason) _recordShown(); - } - - void _recordShown() { - final reason = widget.reason; - WidgetsBinding.instance.addPostFrameCallback((_) { - if (!mounted || widget.reason != reason) return; - final platform = switch (Theme.of(context).platform) { - TargetPlatform.iOS => DeviceConnectFailureShownPlatform.ios, - TargetPlatform.android => DeviceConnectFailureShownPlatform.android, - _ => DeviceConnectFailureShownPlatform.other, - }; - (widget.emit ?? const TypedEvents().emit)(DeviceConnectFailureShown(failureReason: reason, platform: platform)); - }); - } - - @override - Widget build(BuildContext context) => Material( - color: OmiColors.surface1, - borderRadius: OmiRadius.lgAll, - child: Padding( - padding: const EdgeInsets.all(OmiSpacing.md), - child: Column(crossAxisAlignment: CrossAxisAlignment.stretch, mainAxisSize: MainAxisSize.min, children: [ - Row(crossAxisAlignment: CrossAxisAlignment.start, children: [ - Padding( - padding: const EdgeInsets.only(top: OmiSpacing.sm), - child: ExcludeSemantics(child: Icon(Icons.error_outline, color: OmiColors.danger)), - ), - const SizedBox(width: OmiSpacing.sm), - Expanded( - child: Semantics( - liveRegion: true, - child: Text(pairingFailureCopy(context.l10n, widget.reason), style: OmiType.body), - ), - ), - OmiCloseButton( - key: const Key('pairing_failure_dismiss'), - color: OmiColors.textPrimary, - onPressed: widget.onDismiss, - ), - ]), - const SizedBox(height: OmiSpacing.sm), - if (widget.onRetry != null) ...[ - OmiButton( - key: const Key('pairing_failure_retry'), - labelStyle: OmiType.callout.copyWith( - fontWeight: FontWeight.w600, fontFamily: Theme.of(context).textTheme.labelLarge?.fontFamily), - label: context.l10n.tryAgain, - onPressed: widget.onRetry, - expand: true), - const SizedBox(height: OmiSpacing.sm), - ], - OmiButton( - variant: OmiButtonVariant.secondary, - labelStyle: OmiType.callout.copyWith( - fontWeight: FontWeight.w600, fontFamily: Theme.of(context).textTheme.labelLarge?.fontFamily), - key: const Key('pairing_failure_guide'), - label: context.l10n.pairingHowTo, - onPressed: widget.onGuide, - expand: true, - ), - ]), - ), - ); -} diff --git a/app/lib/pages/onboarding/first_capture_controller.dart b/app/lib/pages/onboarding/first_capture_controller.dart deleted file mode 100644 index 0c931055023..00000000000 --- a/app/lib/pages/onboarding/first_capture_controller.dart +++ /dev/null @@ -1,137 +0,0 @@ -import 'dart:async'; - -import 'package:flutter/foundation.dart'; -import 'package:omi/backend/schema/conversation.dart'; -import 'package:omi/utils/analytics/registry/events.g.dart'; - -enum FirstCapturePhase { ready, starting, recording, processing, result, empty, error, delayed } - -abstract interface class FirstCaptureIO { - String get transcript; - Future start(); - Future finish({bool Function()? isCurrent}); - Future cancel(); -} - -/// Owns only this guided attempt; the IO delegates capture and processing to Omi. -class FirstCaptureController extends ChangeNotifier { - FirstCaptureController(this.io, {required this.emit, DateTime Function()? now}) : now = now ?? DateTime.now; - final FirstCaptureIO io; - final DateTime Function() now; - DateTime? _startedAt; - final void Function(RegisteredEvent) emit; - FirstCapturePhase phase = FirstCapturePhase.ready; - int seconds = 0; - String transcript = ''; - bool transcriptObserved = false; - ServerConversation? conversation; - Timer? _timer; - - bool _disposed = false; - bool _ownsRecording = false; - int _attempt = 0; - - bool get canFinish => phase == FirstCapturePhase.recording; - - Future start() async { - if (_disposed || - phase == FirstCapturePhase.starting || - phase == FirstCapturePhase.recording || - phase == FirstCapturePhase.processing) { - return; - } - final attempt = ++_attempt; - phase = FirstCapturePhase.starting; - seconds = 0; - transcript = ''; - transcriptObserved = false; - conversation = null; - notifyListeners(); - try { - await io.start(); - _ownsRecording = true; - if (_disposed || attempt != _attempt) { - await io.cancel(); - _ownsRecording = false; - return; - } - phase = FirstCapturePhase.recording; - _startedAt = now(); - emit(const OnboardingFirstCaptureRecordingStarted()); - _timer = Timer.periodic(const Duration(milliseconds: 250), (_) { - seconds = now().difference(_startedAt!).inSeconds; - transcript = io.transcript; - transcriptObserved |= transcript.trim().isNotEmpty; - notifyListeners(); - if (seconds >= 60) unawaited(finish()); - }); - notifyListeners(); - } catch (_) { - if (_disposed || attempt != _attempt) return; - phase = FirstCapturePhase.error; - notifyListeners(); - } - } - - Future finish() async { - if (_disposed || !canFinish) return; - final attempt = _attempt; - _ownsRecording = false; - _timer?.cancel(); - - seconds = now().difference(_startedAt!).inSeconds; - transcript = io.transcript; - transcriptObserved |= transcript.trim().isNotEmpty; - phase = FirstCapturePhase.processing; - emit(OnboardingFirstCaptureRecordingCompleted(durationS: seconds, transcriptObserved: transcriptObserved)); - notifyListeners(); - final latency = Stopwatch()..start(); - try { - final result = - await io.finish(isCurrent: () => !_disposed && attempt == _attempt).timeout(const Duration(seconds: 90)); - if (_disposed || attempt != _attempt) return; - transcript = io.transcript; - transcriptObserved |= transcript.trim().isNotEmpty; - if (result == null) { - // A 404 may mean websocket auto-finalization won the race; never call - // an observed transcript "not enough speech" without its saved result. - phase = FirstCapturePhase.delayed; - } else if ((result.status == ConversationStatus.completed && result.transcriptSegments.isEmpty) || - (result.discarded && - (result.status == ConversationStatus.completed || result.status == ConversationStatus.failed))) { - phase = FirstCapturePhase.empty; - emit(const OnboardingFirstCaptureEmptyResult()); - } else if (result.status != ConversationStatus.completed) { - phase = FirstCapturePhase.delayed; - } else { - conversation = result; - phase = FirstCapturePhase.result; - emit(OnboardingFirstCaptureConversationShown(latencyMs: latency.elapsedMilliseconds)); - } - } on TimeoutException { - if (_disposed || attempt != _attempt) return; - phase = FirstCapturePhase.delayed; - } catch (_) { - if (_disposed || attempt != _attempt) return; - phase = FirstCapturePhase.error; - } - notifyListeners(); - } - - Future leave() async { - ++_attempt; - _timer?.cancel(); - - if (_ownsRecording) { - await io.cancel(); - _ownsRecording = false; - } - } - - @override - void dispose() { - _disposed = true; - unawaited(leave()); - super.dispose(); - } -} diff --git a/app/lib/pages/onboarding/first_capture_io.dart b/app/lib/pages/onboarding/first_capture_io.dart deleted file mode 100644 index 8fb22902576..00000000000 --- a/app/lib/pages/onboarding/first_capture_io.dart +++ /dev/null @@ -1,82 +0,0 @@ -import 'dart:async'; - -import 'package:omi/backend/http/api/conversations.dart'; -import 'package:omi/backend/schema/conversation.dart'; -import 'package:omi/providers/capture_provider.dart'; -import 'first_capture_controller.dart'; - -class PhoneFirstCaptureIO implements FirstCaptureIO { - PhoneFirstCaptureIO(this.capture, - {Future Function(String)? loadConversation, Future Function(Duration)? wait}) - : loadConversation = loadConversation ?? getConversationById, - wait = wait ?? Future.delayed; - final CaptureProvider capture; - bool _started = false; - String? _recordingId; - String _lastTranscript = ''; - final Future Function(String) loadConversation; - final Future Function(Duration) wait; - @override - String get transcript { - if (capture.activeRecordingId == _recordingId) { - final live = capture.segments.map((s) => s.text).join(' '); - if (live.isNotEmpty) _lastTranscript = live; - } - return _lastTranscript; - } - - @override - Future start() async { - if (capture.liveCaptureSource == 'phone') throw StateError('phone capture already active'); - _lastTranscript = ''; - await capture.streamRecording(); - _started = true; - _recordingId = capture.activeRecordingId; - if (capture.isPhoneMicBatchRecording) { - await cancel(); - throw StateError('live capture requires connectivity'); - } - } - - @override - Future finish({bool Function()? isCurrent}) async { - _started = false; - transcript; - final response = await capture.finishCaptureWithResult(); - if (isCurrent?.call() == false) return null; - var conversation = response?.conversation; - if (conversation == null) return null; - if (conversation.transcriptSegments.isNotEmpty) { - _lastTranscript = conversation.transcriptSegments.map((s) => s.text).join(' '); - } - // Process Now can return a still-processing row. Poll only its confirmed id, - // using the existing conversation API, never the newest item in the list. - for (var i = 0; i < 30; i++) { - if (isCurrent?.call() == false) return null; - if (conversation!.status == ConversationStatus.completed) { - if (conversation.transcriptSegments.isNotEmpty) { - _lastTranscript = conversation.transcriptSegments.map((s) => s.text).join(' '); - } - return conversation; - } - if (conversation.status == ConversationStatus.failed) { - if (conversation.discarded) return conversation; - throw StateError('processing failed'); - } - await wait(const Duration(seconds: 2)); - if (isCurrent?.call() == false) return null; - conversation = await loadConversation(conversation.id) ?? conversation; - } - if (isCurrent?.call() == false) return null; - throw TimeoutException('conversation still processing'); - } - - @override - Future cancel() async { - if (!_started) return; - _started = false; - if (capture.liveCaptureSource == 'phone' && capture.activeRecordingId == _recordingId) { - await capture.stopStreamRecording(); - } - } -} diff --git a/app/lib/pages/onboarding/first_capture_layout.dart b/app/lib/pages/onboarding/first_capture_layout.dart deleted file mode 100644 index d43d974c67f..00000000000 --- a/app/lib/pages/onboarding/first_capture_layout.dart +++ /dev/null @@ -1,29 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:omi/ui/ui.dart'; - -/// Scrollable, vertically balanced content with actions in the bottom safe area. -class FirstCaptureLayout extends StatelessWidget { - const FirstCaptureLayout({super.key, required this.content, required this.actions}); - final List content; - final List actions; - @override - Widget build(BuildContext context) => SafeArea( - child: Column(children: [ - Expanded( - child: LayoutBuilder( - builder: (context, constraints) => SingleChildScrollView( - padding: const EdgeInsets.all(OmiSpacing.lg), - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: (constraints.maxHeight - OmiSpacing.lg * 2).clamp(0, double.infinity)), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: content)), - ))), - Padding( - padding: const EdgeInsets.fromLTRB(OmiSpacing.lg, OmiSpacing.sm, OmiSpacing.lg, OmiSpacing.md), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, mainAxisSize: MainAxisSize.min, children: actions)), - ])); -} diff --git a/app/lib/pages/onboarding/first_capture_page.dart b/app/lib/pages/onboarding/first_capture_page.dart deleted file mode 100644 index 59b6ce481bd..00000000000 --- a/app/lib/pages/onboarding/first_capture_page.dart +++ /dev/null @@ -1,221 +0,0 @@ -import 'dart:async'; -import 'package:flutter/material.dart'; -import 'package:omi/services/siri_integration.dart'; -import 'package:omi/utils/platform/platform_service.dart'; -import 'package:omi/ui/ui.dart'; -import 'package:omi/utils/l10n_extensions.dart'; -import 'first_capture_controller.dart'; -import 'first_capture_layout.dart'; - -class FirstCapturePage extends StatefulWidget { - const FirstCapturePage( - {super.key, - required this.controller, - required this.onComplete, - required this.onSkip, - this.showSiriHint, - this.appShortcutsAvailabilityProbe}); - final FirstCaptureController controller; - final VoidCallback onComplete; - final VoidCallback onSkip; - final bool? showSiriHint; - final Future Function()? appShortcutsAvailabilityProbe; - @override - State createState() => _FirstCapturePageState(); -} - -class _FirstCapturePageState extends State { - late final VoidCallback _releaseFeedback; - bool _siriAvailable = false; - bool _confirming = false; - FirstCaptureController get controller => widget.controller; - @override - void initState() { - super.initState(); - _releaseFeedback = OmiFeedback.ownInfoFeedback(); - if (widget.showSiriHint ?? PlatformService.isIOSAtLeast(16)) { - unawaited(_loadSiri()); - } - } - - Future _loadSiri() async { - try { - final available = - await (widget.appShortcutsAvailabilityProbe ?? SiriIntegration.instance.appShortcutsAvailable)(); - if (mounted) setState(() => _siriAvailable = available); - } catch (_) {/* Keep the hint hidden if the native bridge is unavailable. */} - } - - Future _finish() async { - if (_confirming) return; - if (controller.seconds < 15) { - _confirming = true; - final bool confirmed; - try { - confirmed = await showOmiConfirm(context, - title: context.l10n.firstCaptureShortTitle, - message: context.l10n.firstCaptureShortWarning, - confirmLabel: context.l10n.firstCaptureFinish, - cancelLabel: context.l10n.keepRecording); - } finally { - _confirming = false; - } - if (!confirmed || !mounted) return; - } - await controller.finish(); - } - - Future _back() async { - if (_confirming) return; - if (controller.phase == FirstCapturePhase.recording || controller.phase == FirstCapturePhase.starting) { - _confirming = true; - final confirmed = await showOmiConfirm(context, - title: context.l10n.firstCaptureCancelTitle, - message: context.l10n.firstCaptureCancelWarning, - confirmLabel: context.l10n.stopRecording, - cancelLabel: context.l10n.keepRecording, - destructive: true); - _confirming = false; - if (!confirmed || !mounted) return; - } - await controller.leave(); - if (mounted) Navigator.of(context).pop(); - } - - @override - void dispose() { - _releaseFeedback(); - unawaited(controller.leave()); - super.dispose(); - } - - @override - Widget build(BuildContext context) => AnimatedBuilder( - animation: controller, - builder: (context, _) { - final phase = controller.phase; - final recording = phase == FirstCapturePhase.recording; - final processing = phase == FirstCapturePhase.processing; - final result = phase == FirstCapturePhase.result; - final empty = phase == FirstCapturePhase.empty; - final busy = phase == FirstCapturePhase.starting || processing; - final labelStyle = OmiType.callout - .copyWith(fontWeight: FontWeight.w600, fontFamily: Theme.of(context).textTheme.labelLarge?.fontFamily); - return PopScope( - canPop: !recording && phase != FirstCapturePhase.starting, - onPopInvokedWithResult: (didPop, _) { - if (!didPop) unawaited(_back()); - }, - child: Scaffold( - backgroundColor: OmiColors.surface0, - appBar: AppBar(leading: OmiBackButton(onPressed: _back), title: Text(context.l10n.firstCapturePhone)), - body: FirstCaptureLayout(content: [ - Icon( - result - ? Icons.auto_awesome - : empty - ? Icons.refresh - : Icons.mic_none, - size: 56, - color: OmiColors.textPrimary), - const SizedBox(height: OmiSpacing.lg), - Text( - empty - ? context.l10n.firstCaptureEmptyTitle - : result || processing || phase == FirstCapturePhase.delayed - ? context.l10n.firstCaptureResult - : context.l10n.firstCapturePrompt, - style: result || empty || processing || phase == FirstCapturePhase.delayed - ? OmiType.title1 - : OmiType.title2, - textAlign: TextAlign.center), - const SizedBox(height: OmiSpacing.xl), - if (recording) ...[ - Text(OmiDuration.offset(controller.seconds), - key: const Key('first_capture_timer'), style: OmiType.largeTitle, textAlign: TextAlign.center), - const SizedBox(height: OmiSpacing.sm), - LinearProgressIndicator(value: (controller.seconds / 30).clamp(0, 1)), - const SizedBox(height: OmiSpacing.md), - Text(controller.seconds < 30 ? context.l10n.firstCaptureKeepGoing : context.l10n.firstCaptureReady, - style: OmiType.body, textAlign: TextAlign.center), - const SizedBox(height: OmiSpacing.lg), - Semantics( - liveRegion: true, - child: Text( - controller.transcript.isEmpty ? context.l10n.firstCaptureListening : controller.transcript, - key: const Key('first_capture_transcript'), - style: OmiType.body)), - ], - if (processing) ...[ - const Center(child: OmiSpinner()), - const SizedBox(height: OmiSpacing.md), - Text(context.l10n.firstCaptureProcessing, textAlign: TextAlign.center, style: OmiType.body), - ], - if (result) - Container( - key: const Key('first_capture_result_card'), - padding: const EdgeInsets.all(OmiSpacing.lg), - decoration: BoxDecoration(color: OmiColors.surface1, borderRadius: OmiRadius.lgAll), - child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(controller.conversation!.structured.title, style: OmiType.title2), - const SizedBox(height: OmiSpacing.md), - Text(controller.conversation!.structured.overview, style: OmiType.body), - ])), - if (empty || phase == FirstCapturePhase.error || phase == FirstCapturePhase.delayed) - Semantics( - liveRegion: true, - child: Text( - empty - ? context.l10n.firstCaptureEmpty - : phase == FirstCapturePhase.delayed - ? context.l10n.firstCaptureDelayed - : context.l10n.firstCaptureUnavailable, - key: const Key('first_capture_feedback'), - style: OmiType.body, - textAlign: TextAlign.center)), - ], actions: [ - if (result && _siriAvailable) - Padding( - padding: const EdgeInsets.only(bottom: OmiSpacing.sm), - child: Tooltip( - message: context.l10n.siriShortcutsSetupHint('Ask Omi', 'Question for Omi'), - child: Text(context.l10n.firstCaptureSiriTip, - key: const Key('onboarding_siri_hint'), - style: OmiType.footnote, - maxLines: 1, - overflow: TextOverflow.ellipsis, - textAlign: TextAlign.center))), - if (!processing) - OmiButton( - key: const Key('first_capture_primary'), - labelStyle: labelStyle, - expand: true, - isLoading: busy, - label: result - ? context.l10n.startUsingOmi - : recording - ? context.l10n.firstCaptureFinish - : phase == FirstCapturePhase.ready - ? context.l10n.firstCaptureStart - : context.l10n.tryAgain, - onPressed: busy - ? null - : result - ? widget.onComplete - : recording - ? () { - // Confirmation is a decision, not a pending capture operation. - unawaited(_finish()); - } - : controller.start), - if (!recording && !busy && !result) - OmiButton( - variant: OmiButtonVariant.tertiary, - labelStyle: labelStyle, - key: const Key('first_capture_skip'), - label: context.l10n.firstCaptureSkip, - onPressed: widget.onSkip), - ])), - ); - }); -} diff --git a/app/lib/pages/onboarding/found_omi/found_omi_widget.dart b/app/lib/pages/onboarding/found_omi/found_omi_widget.dart deleted file mode 100644 index 9069eb77c53..00000000000 --- a/app/lib/pages/onboarding/found_omi/found_omi_widget.dart +++ /dev/null @@ -1,215 +0,0 @@ -import 'package:omi/utils/platform/platform_manager.dart'; -import 'package:flutter/material.dart'; - -import 'package:font_awesome_flutter/font_awesome_flutter.dart'; - -import 'package:omi/services/onboarding_sync_runtime.dart'; -import 'package:omi/backend/preferences.dart'; -import 'package:omi/pages/onboarding/widgets/onboarding_card.dart'; -import 'package:omi/ui/ui.dart'; -import 'package:omi/utils/l10n_extensions.dart'; - -class _SourceOption { - final String label; - final FaIconData icon; - - const _SourceOption(this.label, this.icon); -} - -class FoundOmiWidget extends StatefulWidget { - final Function goNext; - @visibleForTesting - final Future Function(String source)? saveSource; - - const FoundOmiWidget({super.key, required this.goNext, this.saveSource}); - - @override - State createState() => _FoundOmiWidgetState(); -} - -class _FoundOmiWidgetState extends State { - String? _selectedSource; - bool _saving = false; - final TextEditingController _otherController = TextEditingController(); - - List<_SourceOption> _getSources(BuildContext context) { - return [ - _SourceOption(context.l10n.tiktok, FontAwesomeIcons.tiktok), - _SourceOption(context.l10n.youtube, FontAwesomeIcons.youtube), - _SourceOption(context.l10n.instagram, FontAwesomeIcons.instagram), - _SourceOption(context.l10n.xTwitter, FontAwesomeIcons.xTwitter), - _SourceOption(context.l10n.reddit, FontAwesomeIcons.reddit), - _SourceOption(context.l10n.linkedIn, FontAwesomeIcons.linkedin), - _SourceOption(context.l10n.friendWordOfMouth, FontAwesomeIcons.userGroup), - _SourceOption(context.l10n.coworker, FontAwesomeIcons.briefcase), - _SourceOption(context.l10n.event, FontAwesomeIcons.calendarDay), - _SourceOption(context.l10n.appStore, FontAwesomeIcons.appStore), - _SourceOption(context.l10n.googleSearch, FontAwesomeIcons.google), - _SourceOption(context.l10n.otherSource, FontAwesomeIcons.ellipsis), - ]; - } - - bool get _canContinue { - if (_selectedSource == null) return false; - if (_selectedSource == context.l10n.otherSource) { - return _otherController.text.trim().isNotEmpty; - } - return true; - } - - @override - void dispose() { - _otherController.dispose(); - super.dispose(); - } - - Future _submit() async { - if (_saving) return; - setState(() => _saving = true); - FocusManager.instance.primaryFocus?.unfocus(); - final source = _selectedSource == context.l10n.otherSource ? _otherController.text.trim() : _selectedSource!; - final saved = await (widget.saveSource?.call(source) ?? OnboardingSyncRuntime.enqueue(acquisitionSource: source)); - if (!mounted) return; - setState(() => _saving = false); - if (!saved) { - OmiFeedback.error(context, context.l10n.somethingWentWrong); - return; - } - SharedPreferencesUtil().foundOmiSource = source; - PlatformManager.instance.analytics.onboardingUserAcquisitionSource(source); - OmiHaptics.selection(); - widget.goNext(); - } - - /// The survey is optional: Skip moves on without recording a source. - void _skip() { - if (_saving) return; - FocusManager.instance.primaryFocus?.unfocus(); - OmiHaptics.selection(); - widget.goNext(); - } - - @override - Widget build(BuildContext context) { - final sources = _getSources(context); - return OnboardingStep( - card: OnboardingCard( - padding: const EdgeInsets.fromLTRB(OmiSpacing.xxl, OmiSpacing.xl, OmiSpacing.xxl, 0), - content: [ - Semantics( - header: true, - child: Text(context.l10n.whereDidYouHearAboutOmi, style: OmiType.title1, textAlign: TextAlign.center), - ), - const SizedBox(height: OmiSpacing.xl), - LayoutBuilder(builder: (context, constraints) { - final columns = MediaQuery.textScalerOf(context).scale(1) > 1.3 || constraints.maxWidth < 280 ? 1 : 2; - return Wrap(spacing: OmiSpacing.sm, runSpacing: OmiSpacing.sm, children: [ - for (final source in sources) - SizedBox( - width: (constraints.maxWidth - OmiSpacing.sm * (columns - 1)) / columns, - child: _SourceTile( - option: source, - selected: _selectedSource == source.label, - onTap: () { - OmiHaptics.light(); - setState(() { - _selectedSource = _selectedSource == source.label ? null : source.label; - if (_selectedSource != context.l10n.otherSource) _otherController.clear(); - }); - })), - ]); - }), - if (_selectedSource == context.l10n.otherSource) ...[ - const SizedBox(height: OmiSpacing.xxs), - Container( - decoration: BoxDecoration( - color: OmiColors.surface1, - borderRadius: OmiRadius.lgAll, - border: Border.all(color: OmiColors.border), - ), - child: TextField( - controller: _otherController, - style: OmiType.callout, - textAlign: TextAlign.center, - decoration: InputDecoration( - hintText: context.l10n.pleaseSpecify, - hintStyle: OmiType.callout.copyWith(color: OmiColors.textTertiary), - border: InputBorder.none, - contentPadding: const EdgeInsets.symmetric(horizontal: OmiSpacing.xl, vertical: OmiSpacing.md), - ), - onChanged: (_) => setState(() {}), - ), - ), - ], - ], - footer: [ - const SizedBox(height: OmiSpacing.sm), - OmiButton( - key: const Key('found_omi_continue'), - labelStyle: OmiType.callout - .copyWith(fontWeight: FontWeight.w600, fontFamily: Theme.of(context).textTheme.labelLarge?.fontFamily), - label: context.l10n.continueButton, - expand: true, - onPressed: _canContinue && !_saving ? _submit : null, - isLoading: _saving, - ), - OmiButton( - variant: OmiButtonVariant.tertiary, - labelStyle: OmiType.callout - .copyWith(fontWeight: FontWeight.w600, fontFamily: Theme.of(context).textTheme.labelLarge?.fontFamily), - key: const Key('found_omi_skip'), - label: context.l10n.skip, - expand: true, - onPressed: _saving ? null : _skip, - ), - ], - ), - ); - } -} - -class _SourceTile extends StatelessWidget { - const _SourceTile({required this.option, required this.selected, required this.onTap}); - - final _SourceOption option; - final bool selected; - final VoidCallback onTap; - - @override - Widget build(BuildContext context) { - final foreground = selected ? OmiColors.onAccent : OmiColors.textPrimary; - return Semantics( - button: true, - selected: selected, - child: Material( - color: selected ? OmiColors.accent : OmiColors.surface1, - shape: RoundedRectangleBorder( - borderRadius: OmiRadius.pillAll, - side: BorderSide(color: selected ? OmiColors.accent : OmiColors.border), - ), - child: InkWell( - customBorder: const StadiumBorder(), - onTap: onTap, - child: ConstrainedBox( - constraints: const BoxConstraints(minHeight: 48), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: OmiSpacing.sm, vertical: OmiSpacing.sm), - child: Row( - children: [ - ExcludeSemantics(child: FaIcon(option.icon, size: 18, color: foreground)), - const SizedBox(width: OmiSpacing.sm), - Expanded( - child: Text( - option.label, - style: OmiType.subhead.copyWith(color: foreground, fontWeight: FontWeight.w500), - ), - ), - ], - ), - ), - ), - ), - ), - ); - } -} diff --git a/app/lib/pages/onboarding/interactive_device_onboarding/interactive_device_onboarding_wrapper.dart b/app/lib/pages/onboarding/interactive_device_onboarding/interactive_device_onboarding_wrapper.dart index b9e88c274b4..7d04c81ce35 100644 --- a/app/lib/pages/onboarding/interactive_device_onboarding/interactive_device_onboarding_wrapper.dart +++ b/app/lib/pages/onboarding/interactive_device_onboarding/interactive_device_onboarding_wrapper.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; @@ -6,7 +8,11 @@ import 'package:omi/services/onboarding_sync_runtime.dart'; import 'package:omi/utils/l10n_extensions.dart'; import 'package:omi/backend/preferences.dart'; import 'package:omi/providers/capture_provider.dart'; +import 'package:omi/models/subscription.dart'; import 'package:omi/providers/device_onboarding_provider.dart'; +import 'package:omi/providers/usage_provider.dart'; +import 'package:omi/services/superwall_pilot.dart'; +import 'package:omi/utils/platform/platform_manager.dart'; import 'package:omi/pages/onboarding/interactive_device_onboarding/steps/transcription_demo_step.dart'; import 'package:omi/pages/onboarding/interactive_device_onboarding/steps/single_press_step.dart'; import 'package:omi/pages/onboarding/interactive_device_onboarding/steps/voice_reply_step.dart'; @@ -25,7 +31,11 @@ class InteractiveDeviceOnboardingWrapper extends StatefulWidget { // and the tutorial stays reachable from device settings. final bool allowExit; - const InteractiveDeviceOnboardingWrapper({super.key, this.allowExit = false}); + /// True when shown inside first-run onboarding, right after the Omi connects. Onboarding offers + /// the plan at its own end, so closing the tutorial here offers nothing. + final bool duringSetup; + + const InteractiveDeviceOnboardingWrapper({super.key, this.allowExit = false, this.duringSetup = false}); @override State createState() => _InteractiveDeviceOnboardingWrapperState(); @@ -51,7 +61,8 @@ class _InteractiveDeviceOnboardingWrapperState extends State(); + return usage.subscription?.subscription.plan == PlanType.basic && usage.showSubscriptionUI; + } + + /// Device tutorial closed: offer the plan through Superwall (local pilot only). Superwall + /// presents its own screen, so this runs after the tutorial route has closed. + Future _showSetupCompletePaywall() async { + if (SuperwallPilot.instance.offeredDuringOnboarding) return; + final shown = + await SuperwallPilot.instance.showPlacementForCurrentUser(SuperwallPilot.deviceSetupCompletePlacement); + if (shown) PlatformManager.instance.analytics.paywallOpened('Device Setup Complete'); } /// Leave the tutorial from any point. Persists completion so the forced @@ -155,7 +188,7 @@ class _InteractiveDeviceOnboardingWrapperState extends State _skipOnboarding() async { if (!await _saveCompletion() || !mounted) return; SharedPreferencesUtil().deviceOnboardingCompleted = true; - Navigator.of(context).pop(); + _closeAndOfferUpgrade(); } @override diff --git a/app/lib/pages/onboarding/interactive_device_onboarding/steps/all_set_step.dart b/app/lib/pages/onboarding/interactive_device_onboarding/steps/all_set_step.dart index b9a8512f4db..276f7c616b7 100644 --- a/app/lib/pages/onboarding/interactive_device_onboarding/steps/all_set_step.dart +++ b/app/lib/pages/onboarding/interactive_device_onboarding/steps/all_set_step.dart @@ -1,4 +1,6 @@ import 'package:flutter/material.dart'; + +import 'package:omi/pages/onboarding/widgets/onboarding_card.dart'; import 'package:provider/provider.dart'; import 'package:omi/backend/preferences.dart'; @@ -87,9 +89,8 @@ class AllSetStep extends StatelessWidget { label: context.l10n.deviceOnboardingFinish, onPressed: onComplete, expand: true, - labelStyle: OmiType.callout.copyWith(fontWeight: FontWeight.w600, fontFamily: 'Roboto'), ), - const SizedBox(height: OmiSpacing.lg), + const SizedBox(height: kOnboardingButtonBottomGap), ], ), ); diff --git a/app/lib/pages/onboarding/interactive_device_onboarding/steps/voice_reply_step.dart b/app/lib/pages/onboarding/interactive_device_onboarding/steps/voice_reply_step.dart index 5bf79df4e87..1bb655c5bd5 100644 --- a/app/lib/pages/onboarding/interactive_device_onboarding/steps/voice_reply_step.dart +++ b/app/lib/pages/onboarding/interactive_device_onboarding/steps/voice_reply_step.dart @@ -2,6 +2,8 @@ import 'dart:async'; import 'dart:math' as math; import 'package:flutter/material.dart'; + +import 'package:omi/pages/onboarding/widgets/onboarding_card.dart'; import 'package:provider/provider.dart'; import 'package:omi/backend/preferences.dart'; @@ -191,9 +193,8 @@ class _VoiceReplyStepState extends State with SingleTickerProvid label: context.l10n.deviceOnboardingContinue, onPressed: widget.onComplete, expand: true, - labelStyle: OmiType.callout.copyWith(fontWeight: FontWeight.w600, fontFamily: 'Roboto'), ), - const SizedBox(height: OmiSpacing.lg), + const SizedBox(height: kOnboardingButtonBottomGap), ], ), ); diff --git a/app/lib/pages/onboarding/interactive_device_onboarding/widgets/onboarding_intro_screen.dart b/app/lib/pages/onboarding/interactive_device_onboarding/widgets/onboarding_intro_screen.dart index 2c2a9277eb8..74c82e7f649 100644 --- a/app/lib/pages/onboarding/interactive_device_onboarding/widgets/onboarding_intro_screen.dart +++ b/app/lib/pages/onboarding/interactive_device_onboarding/widgets/onboarding_intro_screen.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:omi/gen/assets.gen.dart'; import 'package:omi/pages/onboarding/interactive_device_onboarding/widgets/onboarding_step_scaffold.dart'; +import 'package:omi/pages/onboarding/widgets/onboarding_card.dart'; import 'package:omi/ui/ui.dart'; import 'package:omi/utils/l10n_extensions.dart'; @@ -103,14 +104,9 @@ class _OnboardingIntroScreenState extends State with Sing ], ), const Spacer(flex: 3), + // One way on; the close X above is the way out (no second Skip). OnboardingContinueButton(label: context.l10n.getStarted, onPressed: widget.onStart), - const SizedBox(height: 8), - OmiButton.tertiary( - key: const Key('device_onboarding_skip_button'), - label: context.l10n.skip, - onPressed: widget.onSkip ?? () => Navigator.of(context).maybePop(), - ), - const SizedBox(height: 8), + const SizedBox(height: kOnboardingButtonBottomGap), ], ), ), diff --git a/app/lib/pages/onboarding/interactive_device_onboarding/widgets/onboarding_step_scaffold.dart b/app/lib/pages/onboarding/interactive_device_onboarding/widgets/onboarding_step_scaffold.dart index f3022856255..ff75ce315a3 100644 --- a/app/lib/pages/onboarding/interactive_device_onboarding/widgets/onboarding_step_scaffold.dart +++ b/app/lib/pages/onboarding/interactive_device_onboarding/widgets/onboarding_step_scaffold.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; -import 'package:omi/ui/omi_tokens.dart'; +import 'package:omi/pages/onboarding/widgets/onboarding_card.dart'; +import 'package:omi/ui/ui.dart'; import 'package:omi/providers/device_onboarding_provider.dart'; import 'package:omi/utils/l10n_extensions.dart'; @@ -77,13 +78,15 @@ class OnboardingStepScaffold extends StatelessWidget { ], const SizedBox(height: 40), Expanded(child: content), - if (bottomAction != null) ...[bottomAction!, const SizedBox(height: 24)], + // Continue sits where it does on every other onboarding step. + if (bottomAction != null) ...[bottomAction!, const SizedBox(height: kOnboardingButtonBottomGap)], ], ), ); } } +/// The tutorial's Continue: the same button, at the same height, as the rest of onboarding. class OnboardingContinueButton extends StatelessWidget { final VoidCallback onPressed; final String? label; @@ -92,22 +95,13 @@ class OnboardingContinueButton extends StatelessWidget { @override Widget build(BuildContext context) { - return SizedBox( - width: double.infinity, - height: 56, - child: ElevatedButton( - onPressed: onPressed, - style: ElevatedButton.styleFrom( - backgroundColor: OmiColors.accent, - foregroundColor: OmiColors.onAccent, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(28)), - elevation: 0, - ), - child: Text( - label ?? context.l10n.deviceOnboardingContinue, - style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w600), - ), - ), + return OmiButton( + label: label ?? context.l10n.deviceOnboardingContinue, + expand: true, + onPressed: () { + OmiHaptics.selection(); + onPressed(); + }, ); } } diff --git a/app/lib/pages/onboarding/name/name_widget.dart b/app/lib/pages/onboarding/name/name_widget.dart index cbe694f35a9..c7d5e34a8bf 100644 --- a/app/lib/pages/onboarding/name/name_widget.dart +++ b/app/lib/pages/onboarding/name/name_widget.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:omi/backend/preferences.dart'; import 'package:omi/services/auth_service.dart'; -import 'package:omi/pages/onboarding/widgets/onboarding_card.dart'; +import 'package:omi/pages/onboarding/widgets/onboarding_plain_step.dart'; import 'package:omi/ui/ui.dart'; import 'package:omi/utils/l10n_extensions.dart'; @@ -24,10 +24,10 @@ class _NameWidgetState extends State { nameController = TextEditingController(text: SharedPreferencesUtil().givenName); super.initState(); - // Auto-focus the name input field after the widget is built - // WidgetsBinding.instance.addPostFrameCallback((_) { - // focusNode.requestFocus(); - // }); + // The step opens with the keyboard up, ready to type. + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) focusNode.requestFocus(); + }); } @override @@ -49,49 +49,41 @@ class _NameWidgetState extends State { @override Widget build(BuildContext context) { - return OnboardingStep( - card: OnboardingCard( - content: [ - Semantics( - header: true, - child: Text(context.l10n.whatsYourName, style: OmiType.title1, textAlign: TextAlign.center), + return OnboardingPlainStep( + title: context.l10n.whatsYourName, + centered: true, + body: DecoratedBox( + decoration: BoxDecoration(color: OmiColors.surface1, borderRadius: OmiRadius.lgAll), + child: TextField( + controller: nameController, + focusNode: focusNode, + style: OmiType.title3, + textAlign: TextAlign.center, + textCapitalization: TextCapitalization.words, + textInputAction: TextInputAction.done, + autocorrect: false, + cursorColor: OmiColors.textPrimary, + onSubmitted: (_) => _submit(), + // Keep the keyboard up when a finger lands outside the field, so a tap on Continue lands on + // Continue instead of only closing the keyboard (and moving the button). Tapping empty + // space still closes it through the onboarding host. + onTapOutside: (_) {}, + decoration: InputDecoration( + hintText: context.l10n.enterYourName, + hintStyle: OmiType.title3.copyWith(color: OmiColors.textTertiary, fontWeight: FontWeight.w400), + border: InputBorder.none, + contentPadding: const EdgeInsets.symmetric(horizontal: OmiSpacing.xl, vertical: OmiSpacing.lg), ), - const SizedBox(height: OmiSpacing.xxl), - Container( - decoration: BoxDecoration( - color: OmiColors.surface1, - borderRadius: OmiRadius.lgAll, - border: Border.all(color: OmiColors.border), - ), - child: TextField( - controller: nameController, - focusNode: focusNode, - style: OmiType.body.copyWith(fontWeight: FontWeight.w500), - textAlign: TextAlign.center, - textCapitalization: TextCapitalization.words, - textInputAction: TextInputAction.done, - onSubmitted: (_) => _submit(), - decoration: InputDecoration( - hintText: context.l10n.enterYourName, - hintStyle: OmiType.body.copyWith(color: OmiColors.textTertiary), - border: InputBorder.none, - contentPadding: const EdgeInsets.symmetric(horizontal: OmiSpacing.xl, vertical: OmiSpacing.lg), - ), - onChanged: (value) { - setState(() {}); // Trigger rebuild to update button state - }, - ), - ), - ], - footer: [ - const SizedBox(height: OmiSpacing.xxl), - OmiButton( - key: const Key('onboarding_name_continue'), - label: context.l10n.continueButton, - expand: true, - onPressed: _canContinue ? _submit : null, - ), - ], + onChanged: (value) { + setState(() {}); // Trigger rebuild to update button state + }, + ), + ), + action: OmiButton( + key: const Key('onboarding_name_continue'), + label: context.l10n.continueButton, + expand: true, + onPressed: _canContinue ? _submit : null, ), ); } diff --git a/app/lib/pages/onboarding/onboarding_step_telemetry.dart b/app/lib/pages/onboarding/onboarding_step_telemetry.dart deleted file mode 100644 index 1b064c036d1..00000000000 --- a/app/lib/pages/onboarding/onboarding_step_telemetry.dart +++ /dev/null @@ -1,61 +0,0 @@ -import 'package:omi/utils/analytics/registry/events.g.dart'; - -/// One event per visible step exit. Repeated TabController animation notifications -/// do not duplicate an exit; a return via Back starts a new timed visit. -class MobileOnboardingStepTelemetry { - MobileOnboardingStepTelemetry({required this.emit, required this.platform, int Function()? elapsedMs}) - : elapsedMs = elapsedMs ?? (Stopwatch()..start()).elapsedMillisecondsGetter; - final void Function(RegisteredEvent) emit; - final OnboardingStepCompletedPlatform Function() platform; - final int Function() elapsedMs; - int index = 0; - int _enteredAt = 0; - bool skipped = false; - OnboardingStepCompletedExitReason exitReason = OnboardingStepCompletedExitReason.answered; - bool _exited = false; - static const steps = [ - OnboardingStepCompletedStep.auth, - OnboardingStepCompletedStep.aiConsent, - OnboardingStepCompletedStep.name, - OnboardingStepCompletedStep.primaryLanguage, - OnboardingStepCompletedStep.acquisitionSource, - OnboardingStepCompletedStep.permissions, - OnboardingStepCompletedStep.speechProfile, - OnboardingStepCompletedStep.knowledgeGraph, - OnboardingStepCompletedStep.setup, - OnboardingStepCompletedStep.firstCapture, - ]; - void seed(int restoredIndex) { - index = restoredIndex; - _enteredAt = elapsedMs(); - skipped = false; - exitReason = OnboardingStepCompletedExitReason.answered; - _exited = false; - } - - void enter(int next) { - if (next == index) return; - exit(); - index = next; - _enteredAt = elapsedMs(); - skipped = false; - exitReason = OnboardingStepCompletedExitReason.answered; - _exited = false; - } - - void exit() { - if (_exited) return; - _exited = true; - emit(OnboardingStepCompleted( - step: steps[index], - index: index, - elapsedMs: (elapsedMs() - _enteredAt).clamp(0, 1 << 31), - skipped: skipped, - platform: platform(), - exitReason: skipped ? OnboardingStepCompletedExitReason.skipped : exitReason)); - } -} - -extension on Stopwatch { - int elapsedMillisecondsGetter() => elapsedMilliseconds; -} diff --git a/app/lib/pages/onboarding/permissions/onboarding_permissions_panel.dart b/app/lib/pages/onboarding/permissions/onboarding_permissions_panel.dart index 03019b66714..5e8ac2a7524 100644 --- a/app/lib/pages/onboarding/permissions/onboarding_permissions_panel.dart +++ b/app/lib/pages/onboarding/permissions/onboarding_permissions_panel.dart @@ -8,6 +8,7 @@ import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:provider/provider.dart'; +import 'package:omi/l10n/app_localizations.dart'; import 'package:omi/providers/onboarding_provider.dart'; import 'package:omi/ui/components/omi_permission_row.dart'; import 'package:omi/ui/omi_tokens.dart'; @@ -18,7 +19,10 @@ import 'package:omi/utils/logger.dart'; /// /// "Always" location is deliberately not here: it is asked by the feature that needs it /// (Settings → Permissions), never during first run (docs/ux-contract.md §15). -enum OnboardingPermission { background, location, notifications } +/// +/// [bluetooth] is asked as the app opens; the first-run step shows it again only when it was not +/// allowed. The returning-user interstitial does not list it. +enum OnboardingPermission { bluetooth, background, location, notifications } /// Reads and requests [OnboardingPermission]s. The default reads the platform directly and requests /// through [OnboardingProvider] (which keeps its telemetry and flags); tests pass a fake. @@ -43,6 +47,16 @@ class PlatformOnboardingPermissionsSource implements OnboardingPermissionsSource @override Future status(OnboardingPermission permission) async { switch (permission) { + case OnboardingPermission.bluetooth: + if (Platform.isIOS) return OmiPermissionStatus.fromStatus(await Permission.bluetooth.status); + final scan = OmiPermissionStatus.fromStatus(await Permission.bluetoothScan.status); + final connect = OmiPermissionStatus.fromStatus(await Permission.bluetoothConnect.status); + if (scan == OmiPermissionStatus.granted && connect == OmiPermissionStatus.granted) { + return OmiPermissionStatus.granted; + } + return scan == OmiPermissionStatus.blocked || connect == OmiPermissionStatus.blocked + ? OmiPermissionStatus.blocked + : OmiPermissionStatus.askable; case OnboardingPermission.background: final granted = await FlutterForegroundTask.isIgnoringBatteryOptimizations; return granted ? OmiPermissionStatus.granted : OmiPermissionStatus.askable; @@ -57,6 +71,8 @@ class PlatformOnboardingPermissionsSource implements OnboardingPermissionsSource @override Future request(OnboardingPermission permission) async { switch (permission) { + case OnboardingPermission.bluetooth: + await provider.askForBluetoothPermissions(); case OnboardingPermission.background: await provider.askForBackgroundPermissions(); case OnboardingPermission.location: @@ -67,6 +83,28 @@ class PlatformOnboardingPermissionsSource implements OnboardingPermissionsSource } } +/// The same glyphs as the Settings permission list. +FaIconData onboardingPermissionIcon(OnboardingPermission permission) => switch (permission) { + OnboardingPermission.bluetooth => FontAwesomeIcons.bluetoothB, + OnboardingPermission.background => FontAwesomeIcons.batteryFull, + OnboardingPermission.location => FontAwesomeIcons.locationArrow, + OnboardingPermission.notifications => FontAwesomeIcons.solidBell, + }; + +String onboardingPermissionTitle(AppLocalizations l10n, OnboardingPermission permission) => switch (permission) { + OnboardingPermission.bluetooth => l10n.bluetooth, + OnboardingPermission.background => l10n.backgroundActivity, + OnboardingPermission.location => l10n.locationAccess, + OnboardingPermission.notifications => l10n.notifications, + }; + +String onboardingPermissionReason(AppLocalizations l10n, OnboardingPermission permission) => switch (permission) { + OnboardingPermission.bluetooth => l10n.onboardingBluetoothRequired, + OnboardingPermission.background => l10n.backgroundActivityDesc, + OnboardingPermission.location => l10n.locationAccessDesc, + OnboardingPermission.notifications => l10n.notificationsDesc, + }; + /// [source], or the platform source over the ambient [OnboardingProvider]. OnboardingPermissionsSource resolveOnboardingPermissionsSource( BuildContext context, @@ -164,21 +202,9 @@ class _OnboardingPermissionsPanelState extends State OmiPermissionRow( key: ValueKey('onboarding_permission_${permission.name}'), // The same glyphs as the Settings permission list. - leading: FaIcon(switch (permission) { - OnboardingPermission.background => FontAwesomeIcons.batteryFull, - OnboardingPermission.location => FontAwesomeIcons.locationArrow, - OnboardingPermission.notifications => FontAwesomeIcons.solidBell, - }), - title: switch (permission) { - OnboardingPermission.background => l10n.backgroundActivity, - OnboardingPermission.location => l10n.locationAccess, - OnboardingPermission.notifications => l10n.notifications, - }, - reason: switch (permission) { - OnboardingPermission.background => l10n.backgroundActivityDesc, - OnboardingPermission.location => l10n.locationAccessDesc, - OnboardingPermission.notifications => l10n.notificationsDesc, - }, + leading: FaIcon(onboardingPermissionIcon(permission)), + title: onboardingPermissionTitle(l10n, permission), + reason: onboardingPermissionReason(l10n, permission), status: _statuses[permission] ?? OmiPermissionStatus.askable, onAllow: () => _allow(permission), ), diff --git a/app/lib/pages/onboarding/permissions/permissions_widget.dart b/app/lib/pages/onboarding/permissions/permissions_widget.dart index aa0724e3424..4de30262611 100644 --- a/app/lib/pages/onboarding/permissions/permissions_widget.dart +++ b/app/lib/pages/onboarding/permissions/permissions_widget.dart @@ -1,46 +1,272 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:permission_handler/permission_handler.dart'; + +import 'package:omi/backend/preferences.dart'; import 'package:omi/pages/onboarding/permissions/onboarding_permissions_panel.dart'; -import 'package:omi/pages/onboarding/widgets/onboarding_card.dart'; +import 'package:omi/pages/onboarding/widgets/onboarding_plain_step.dart'; import 'package:omi/ui/ui.dart'; import 'package:omi/utils/l10n_extensions.dart'; +import 'package:omi/utils/logger.dart'; + +/// How one card on the first-run permissions step looks. +enum OnboardingPermissionCardState { + /// Not answered yet: grey, switch off. + unanswered, + + /// Allowed: switch on. + allowed, + + /// Answered with no (or switched off in Settings). Tapping opens Settings. + denied, +} -/// First-run permissions step. Each row can allow its own permission; Continue asks for whatever is -/// still missing, then moves on whatever the reader chose (docs/ux-contract.md §15). -class PermissionsWidget extends StatelessWidget { +/// First-run permissions step: one switch card per permission (docs/ux-contract.md §15). +/// +/// Each card is answered by toggling it, which shows that permission's system prompt. A "no" is an +/// answer: the card turns orange (notifications: red with "Tap to fix") and the reader can still +/// continue. Continue unlocks once every card has been answered. Bluetooth is asked as the app +/// opens, so its card only appears when it was not allowed. +class PermissionsWidget extends StatefulWidget { final VoidCallback goNext; final OnboardingPermissionsSource? source; const PermissionsWidget({super.key, required this.goNext, this.source}); + @override + State createState() => _PermissionsWidgetState(); +} + +class _PermissionsWidgetState extends State with WidgetsBindingObserver { + late final OnboardingPermissionsSource _source = resolveOnboardingPermissionsSource(context, widget.source); + + /// The cards, fixed when the step opens: Bluetooth stays listed after it is allowed here. + List? _permissions; + final Map _statuses = {}; + + /// Cards whose system prompt the reader has already seen (on this step, or Bluetooth at launch). + final Set _asked = {}; + OnboardingPermission? _busy; + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addObserver(this); + if (SharedPreferencesUtil().getBool('onboarding/bluetoothAskedAtLaunch')) { + _asked.add(OnboardingPermission.bluetooth); + } + unawaited(_load()); + } + + @override + void dispose() { + WidgetsBinding.instance.removeObserver(this); + super.dispose(); + } + + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + // Back from Settings: show what the reader switched on there. + if (state == AppLifecycleState.resumed) unawaited(_refresh()); + } + + Future _read(OnboardingPermission permission) async { + try { + return await _source.status(permission); + } catch (_) { + return OmiPermissionStatus.askable; + } + } + + Future _load() async { + final bluetooth = await _read(OnboardingPermission.bluetooth); + if (!mounted) return; + setState(() { + _permissions = [ + if (bluetooth != OmiPermissionStatus.granted) OnboardingPermission.bluetooth, + ..._source.permissions, + ]; + _statuses[OnboardingPermission.bluetooth] = bluetooth; + }); + await _refresh(); + } + + Future _refresh() async { + final permissions = _permissions; + if (permissions == null) return; + for (final permission in permissions) { + final status = await _read(permission); + if (!mounted) return; + setState(() => _statuses[permission] = status); + } + } + + OnboardingPermissionCardState _stateOf(OnboardingPermission permission) { + final status = _statuses[permission] ?? OmiPermissionStatus.askable; + if (status == OmiPermissionStatus.granted) return OnboardingPermissionCardState.allowed; + if (status == OmiPermissionStatus.blocked || status == OmiPermissionStatus.serviceOff) { + return OnboardingPermissionCardState.denied; + } + return _asked.contains(permission) + ? OnboardingPermissionCardState.denied + : OnboardingPermissionCardState.unanswered; + } + + bool get _allAnswered { + final permissions = _permissions; + if (permissions == null) return false; + return permissions.every((p) => _stateOf(p) != OnboardingPermissionCardState.unanswered); + } + + Future _onTap(OnboardingPermission permission) async { + if (_busy != null) return; + final state = _stateOf(permission); + if (state == OnboardingPermissionCardState.allowed) return; + OmiHaptics.selection(); + if (state == OnboardingPermissionCardState.denied) { + // The system will not ask again (or the reader said no): only Settings can turn it on. + await openAppSettings(); + return; + } + setState(() => _busy = permission); + try { + await _source.request(permission); + } catch (e) { + Logger.debug('Onboarding permission request failed for ${permission.name}: $e'); + } finally { + _asked.add(permission); + final status = await _read(permission); + if (mounted) { + // The toggle landing on: a light confirmation. + if (status == OmiPermissionStatus.granted) OmiHaptics.light(); + setState(() { + _statuses[permission] = status; + _busy = null; + }); + } + } + } + @override Widget build(BuildContext context) { - return OnboardingStep( - card: OnboardingCard( - content: [ - Text(context.l10n.grantPermissions, style: OmiType.title1, textAlign: TextAlign.center), - const SizedBox(height: OmiSpacing.xs), - Text( - context.l10n.permissionsChangeAnytime, - style: OmiType.subhead.copyWith(color: OmiColors.textSecondary), - textAlign: TextAlign.center, - ), - const SizedBox(height: OmiSpacing.xl), - OnboardingPermissionsPanel(source: source), + final permissions = _permissions ?? const []; + return OnboardingPlainStep( + title: context.l10n.grantPermissions, + centered: true, + body: Column( + children: [ + for (final permission in permissions) ...[ + _OnboardingPermissionRow( + key: ValueKey('onboarding_permission_${permission.name}'), + permission: permission, + state: _stateOf(permission), + busy: _busy == permission, + onTap: () => _onTap(permission), + ), + const SizedBox(height: OmiSpacing.sm), + ], ], - footer: [ - const SizedBox(height: OmiSpacing.xs), - OmiButton( - key: const Key('onboarding_permissions_continue'), - label: context.l10n.continueButton, - expand: true, - onPressed: () async { - OmiHaptics.selection(); - await requestMissingOnboardingPermissions(resolveOnboardingPermissionsSource(context, source)); - goNext(); - }, + ), + action: OmiButton( + key: const Key('onboarding_permissions_continue'), + label: context.l10n.continueButton, + expand: true, + onPressed: _allAnswered + ? () { + OmiHaptics.selection(); + widget.goNext(); + } + : null, + ), + ); + } +} + +/// One permission as its own card: icon tile, title, and the switch. A line under the title +/// appears only when there is something to do (Bluetooth off, notifications to fix). +class _OnboardingPermissionRow extends StatelessWidget { + const _OnboardingPermissionRow({ + super.key, + required this.permission, + required this.state, + required this.busy, + required this.onTap, + }); + + final OnboardingPermission permission; + final OnboardingPermissionCardState state; + final bool busy; + final VoidCallback onTap; + + @override + Widget build(BuildContext context) { + final l10n = context.l10n; + final denied = state == OnboardingPermissionCardState.denied; + final String? note = !denied + ? null + : switch (permission) { + OnboardingPermission.notifications => l10n.onboardingPermissionTapToFix, + OnboardingPermission.bluetooth => l10n.onboardingBluetoothRequired, + _ => null, + }; + final title = onboardingPermissionTitle(l10n, permission); + + return Semantics( + button: true, + toggled: state == OnboardingPermissionCardState.allowed, + label: note == null ? title : '$title, $note', + excludeSemantics: true, + child: Material( + color: OmiColors.surface1, + borderRadius: OmiRadius.lgAll, + clipBehavior: Clip.antiAlias, + child: InkWell( + onTap: busy ? null : onTap, + child: ConstrainedBox( + constraints: const BoxConstraints(minHeight: 64), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: OmiSpacing.md, vertical: OmiSpacing.sm), + child: Row( + children: [ + Container( + width: 32, + height: 32, + decoration: BoxDecoration(color: OmiColors.surface0, borderRadius: OmiRadius.smAll), + child: Center( + child: FaIcon(onboardingPermissionIcon(permission), size: 15, color: OmiColors.textPrimary), + ), + ), + const SizedBox(width: OmiSpacing.sm), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text(title, style: OmiType.callout.copyWith(fontWeight: FontWeight.w600)), + if (note != null) ...[ + const SizedBox(height: 2), + Text(note, style: OmiType.footnote.copyWith(color: OmiColors.textSecondary)), + ], + ], + ), + ), + const SizedBox(width: OmiSpacing.sm), + OnboardingPermissionToggle( + on: state == OnboardingPermissionCardState.allowed, + denied: denied, + busy: busy, + // Notifications that are off are worth fixing (red); the rest are a caution (orange). + deniedColor: + permission == OnboardingPermission.notifications ? OmiColors.danger : OmiColors.warning, + ), + ], + ), + ), ), - ], + ), ), ); } diff --git a/app/lib/pages/onboarding/primary_language/primary_language_widget.dart b/app/lib/pages/onboarding/primary_language/primary_language_widget.dart index 391ffe2c2f4..02b36af84b2 100644 --- a/app/lib/pages/onboarding/primary_language/primary_language_widget.dart +++ b/app/lib/pages/onboarding/primary_language/primary_language_widget.dart @@ -8,7 +8,7 @@ import 'package:provider/provider.dart'; import 'package:omi/backend/preferences.dart'; import 'package:omi/providers/home_provider.dart'; import 'package:omi/providers/user_provider.dart'; -import 'package:omi/pages/onboarding/widgets/onboarding_card.dart'; +import 'package:omi/pages/onboarding/widgets/onboarding_plain_step.dart'; import 'package:omi/ui/ui.dart'; import 'package:omi/utils/l10n_extensions.dart'; import 'package:omi/utils/logger.dart'; @@ -228,64 +228,50 @@ class _PrimaryLanguageWidgetState extends State { @override Widget build(BuildContext context) { final hasSelection = selectedLanguageName != null; - return OnboardingStep( - card: OnboardingCard( - content: [ - Semantics( - header: true, - child: Text(context.l10n.whatsYourPrimaryLanguage, style: OmiType.title1, textAlign: TextAlign.center), - ), - const SizedBox(height: OmiSpacing.xxl), - Semantics( - button: true, - label: context.l10n.selectPrimaryLanguage, - value: selectedLanguageName, - excludeSemantics: true, - child: Material( - color: OmiColors.surface1, - shape: RoundedRectangleBorder( - borderRadius: OmiRadius.lgAll, - side: BorderSide(color: OmiColors.border), - ), - child: InkWell( - customBorder: const RoundedRectangleBorder(borderRadius: OmiRadius.lgAll), - onTap: () { - final homeProvider = Provider.of(context, listen: false); - _showLanguageSelector(context, homeProvider.availableLanguages); - }, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: OmiSpacing.xl, vertical: OmiSpacing.lg), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Flexible( - child: Text( - selectedLanguageName ?? context.l10n.selectYourLanguage, - style: OmiType.body.copyWith( - color: hasSelection ? OmiColors.textPrimary : OmiColors.textTertiary, - fontWeight: FontWeight.w500, - ), - ), + return OnboardingPlainStep( + title: context.l10n.chooseYourLanguage, + centered: true, + body: Semantics( + button: true, + label: context.l10n.selectPrimaryLanguage, + value: selectedLanguageName, + excludeSemantics: true, + child: Material( + color: OmiColors.surface1, + shape: const RoundedRectangleBorder(borderRadius: OmiRadius.lgAll), + child: InkWell( + customBorder: const RoundedRectangleBorder(borderRadius: OmiRadius.lgAll), + onTap: () { + final homeProvider = Provider.of(context, listen: false); + _showLanguageSelector(context, homeProvider.availableLanguages); + }, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: OmiSpacing.xl, vertical: OmiSpacing.lg), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Flexible( + child: Text( + selectedLanguageName ?? context.l10n.selectYourLanguage, + style: OmiType.title3.copyWith( + color: hasSelection ? OmiColors.textPrimary : OmiColors.textTertiary, ), - const SizedBox(width: OmiSpacing.sm), - Icon(Icons.keyboard_arrow_down, color: OmiColors.textTertiary, size: 24), - ], + ), ), - ), + const SizedBox(width: OmiSpacing.sm), + Icon(Icons.keyboard_arrow_down, color: OmiColors.textTertiary, size: 24), + ], ), ), ), - ], - footer: [ - const SizedBox(height: OmiSpacing.xxl), - // Async: the button shows a spinner and ignores taps while the language saves. - OmiButton( - key: const Key('onboarding_language_continue'), - label: context.l10n.continueButton, - expand: true, - onPressed: selectedLanguage == null ? null : _continue, - ), - ], + ), + ), + // Async: the button shows a spinner and ignores taps while the language saves. + action: OmiButton( + key: const Key('onboarding_language_continue'), + label: context.l10n.continueButton, + expand: true, + onPressed: selectedLanguage == null ? null : _continue, ), ); } diff --git a/app/lib/pages/onboarding/widgets/onboarding_card.dart b/app/lib/pages/onboarding/widgets/onboarding_card.dart index a45070f28c1..d3551cd2cc6 100644 --- a/app/lib/pages/onboarding/widgets/onboarding_card.dart +++ b/app/lib/pages/onboarding/widgets/onboarding_card.dart @@ -1,8 +1,17 @@ +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:omi/env/env.dart'; +import 'package:omi/env/environment_profile.dart'; import 'package:omi/ui/omi_tokens.dart'; +import 'package:omi/ui/ui.dart'; -/// The black bottom card every first-run step sits in, pinned to the bottom of the step. +/// Space between every onboarding step's primary button and the bottom safe area: high enough to +/// sit under the thumb, the same on every step so the button never moves between steps. +const double kOnboardingButtonBottomGap = OmiSpacing.xl; + +/// The bottom block of a first-run step (title, text, buttons), pinned to the bottom of the step. +/// No card: it sits straight on the onboarding surface, under the Omi pendant. /// /// [content] scrolls when they do not fit (large text, small phones); [footer] (the step's /// buttons) stays visible under them. The card reserves the bottom system inset once, through its @@ -16,8 +25,10 @@ class OnboardingCard extends StatelessWidget { super.key, required this.content, this.footer = const [], - this.padding = const EdgeInsets.fromLTRB(OmiSpacing.xxl, OmiSpacing.xxl, OmiSpacing.xxl, OmiSpacing.xs), + this.padding = + const EdgeInsets.fromLTRB(OmiSpacing.xxl, OmiSpacing.xxl, OmiSpacing.xxl, kOnboardingButtonBottomGap), this.crossAxisAlignment = CrossAxisAlignment.center, + this.reserveBottomInset = true, }); final List content; @@ -25,17 +36,18 @@ class OnboardingCard extends StatelessWidget { final EdgeInsets padding; final CrossAxisAlignment crossAxisAlignment; + /// False when the footer lays out the home-indicator area itself (e.g. a fine-print line under + /// the button that sits in the space the other steps leave below theirs). + final bool reserveBottomInset; + @override Widget build(BuildContext context) { return Container( width: double.infinity, padding: padding, - decoration: BoxDecoration( - color: OmiColors.surface0, - borderRadius: const BorderRadius.vertical(top: Radius.circular(OmiRadius.xl)), - ), child: SafeArea( top: false, + bottom: reserveBottomInset, child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, @@ -57,7 +69,7 @@ class OnboardingCard extends StatelessWidget { } } -/// A first-run step: the background shows through above, the [card] sits at the bottom and grows +/// A first-run step: the pendant shows above, the [card] block sits at the bottom and grows /// up to the full height before its content scrolls. class OnboardingStep extends StatelessWidget { const OnboardingStep({super.key, required this.card}); @@ -77,3 +89,103 @@ class OnboardingStep extends StatelessWidget { ); } } + +/// How sign-in and the data & privacy step look: the Omi pendant on the light onboarding surface +/// (default), or the launch film full-screen under white type. The film is a design comparison for +/// now: `--dart-define=OMI_ONBOARDING_HERO=video`, honoured in debug and local_dev builds only. +abstract final class OnboardingHero { + static const _flag = String.fromEnvironment('OMI_ONBOARDING_HERO'); + + static bool get video => (kDebugMode || Env.profile == AppEnvironmentProfile.localDev) && _flag == 'video'; + + /// Type and buttons: white over the film, the theme's own over the pendant. + static OmiPalette get palette => video ? OmiPalette.dark : OmiColors.active; +} + +/// Sign-in and data & privacy: the hero (pendant or film) above, [headline] centred in the space +/// between it and the buttons, and [buttons] pinned exactly where every other step's Continue sits +/// (24pt in, 48pt tall, ending [kOnboardingButtonBottomGap] above the safe area) with [legal] in the space below. +class OnboardingHeroStep extends StatelessWidget { + const OnboardingHeroStep({super.key, required this.headline, required this.buttons, required this.legal}); + + final Widget headline; + final List buttons; + final Widget legal; + + @override + Widget build(BuildContext context) { + final media = MediaQuery.of(context); + final belowButton = media.padding.bottom + kOnboardingButtonBottomGap; + // The pendant's body ends around 40% down; the film fills the screen, so its type sits lower. + final heroBottom = media.size.height * (OnboardingHero.video ? 0.46 : 0.42); + return Padding( + padding: const EdgeInsets.symmetric(horizontal: OmiSpacing.xl), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + SizedBox(height: heroBottom), + // The headline sits low, just above the button(s). + Expanded( + child: Align(alignment: Alignment.bottomCenter, child: SingleChildScrollView(child: headline)), + ), + const SizedBox(height: OmiSpacing.xxl), + ...buttons, + SizedBox( + height: belowButton, + child: Padding(padding: const EdgeInsets.only(top: OmiSpacing.xs), child: legal), + ), + ], + ), + ); + } +} + +/// The onboarding action button in the hero steps: the same 48pt pill as the other steps' Continue, +/// coloured from [OnboardingHero.palette] so it inverts over the film. +class OnboardingHeroButton extends StatelessWidget { + const OnboardingHeroButton( + {super.key, required this.label, required this.onPressed, this.icon, this.outlined = false}); + + final String label; + final VoidCallback onPressed; + final Widget? icon; + final bool outlined; + + @override + Widget build(BuildContext context) { + final p = OnboardingHero.palette; + final style = OmiType.headline.copyWith(color: outlined ? p.textPrimary : p.onAccent); + final child = Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (icon != null) ...[ + IconTheme(data: IconThemeData(color: style.color, size: 20), child: icon!), + const SizedBox(width: OmiSpacing.xs), + ], + Flexible(child: Text(label, style: style, overflow: TextOverflow.ellipsis)), + ], + ); + return SizedBox( + height: 48, + child: outlined + ? OutlinedButton( + onPressed: onPressed, + style: OutlinedButton.styleFrom( + side: BorderSide(color: p.textPrimary.withValues(alpha: 0.4)), + shape: const RoundedRectangleBorder(borderRadius: OmiRadius.mdAll), + ), + child: child, + ) + : ElevatedButton( + onPressed: onPressed, + style: ElevatedButton.styleFrom( + backgroundColor: p.accent, + foregroundColor: p.onAccent, + elevation: 0, + shape: const RoundedRectangleBorder(borderRadius: OmiRadius.mdAll), + ), + child: child, + ), + ); + } +} diff --git a/app/lib/pages/onboarding/widgets/onboarding_hero_film.dart b/app/lib/pages/onboarding/widgets/onboarding_hero_film.dart new file mode 100644 index 00000000000..239a01ececc --- /dev/null +++ b/app/lib/pages/onboarding/widgets/onboarding_hero_film.dart @@ -0,0 +1,92 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +import 'package:video_player/video_player.dart'; + +import 'package:omi/gen/assets.gen.dart'; +import 'package:omi/ui/ui.dart'; + +/// The launch film behind sign-in and data & privacy ([OnboardingHero.video]): full-screen, muted, +/// looping, darkened toward the bottom so the white type and buttons read. Under Reduce Motion it +/// holds its first frame. +class OnboardingHeroFilm extends StatefulWidget { + const OnboardingHeroFilm({super.key}); + + @override + State createState() => _OnboardingHeroFilmState(); +} + +class _OnboardingHeroFilmState extends State { + late final VideoPlayerController _film = VideoPlayerController.asset( + Assets.videos.onboardingLaunchLoop, + videoPlayerOptions: VideoPlayerOptions(mixWithOthers: true), + ); + bool _ready = false; + + @override + void initState() { + super.initState(); + _film.initialize().then((_) async { + if (!mounted) return; + await _film.setVolume(0); + await _film.setLooping(true); + if (!mounted) return; + if (!MediaQuery.disableAnimationsOf(context)) await _film.play(); + if (mounted) setState(() => _ready = true); + }).catchError((Object _) {}); + } + + @override + void dispose() { + _film.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final ink = OmiPalette.light.textPrimary; + return AnnotatedRegion( + value: SystemUiOverlayStyle.light, + child: ColoredBox( + color: ink, + child: Stack( + fit: StackFit.expand, + children: [ + AnimatedOpacity( + opacity: _ready ? 1 : 0, + duration: const Duration(milliseconds: 600), + child: _ready + ? FittedBox( + fit: BoxFit.cover, + clipBehavior: Clip.hardEdge, + child: SizedBox( + width: _film.value.size.width, + height: _film.value.size.height, + child: VideoPlayer(_film), + ), + ) + : const SizedBox.shrink(), + ), + // A little shade under the status bar, and a deep one where the type and buttons sit. + DecoratedBox( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + ink.withValues(alpha: 0.35), + ink.withValues(alpha: 0.0), + ink.withValues(alpha: 0.0), + ink.withValues(alpha: 0.75), + ink.withValues(alpha: 0.92), + ], + stops: const [0.0, 0.18, 0.4, 0.68, 1.0], + ), + ), + ), + ], + ), + ), + ); + } +} diff --git a/app/lib/pages/onboarding/widgets/onboarding_plain_step.dart b/app/lib/pages/onboarding/widgets/onboarding_plain_step.dart new file mode 100644 index 00000000000..e50e911bc85 --- /dev/null +++ b/app/lib/pages/onboarding/widgets/onboarding_plain_step.dart @@ -0,0 +1,151 @@ +import 'package:flutter/material.dart'; + +import 'package:omi/ui/ui.dart'; +import 'package:omi/pages/onboarding/widgets/onboarding_card.dart'; + +const _thumbShadow = BoxShadow( + color: Color(0x26000000), // omi-ux-allow: color-literal -- switch thumb shadow + blurRadius: 4, + offset: Offset(0, 2)); +const _bangStyle = TextStyle( + fontSize: 15, // omi-ux-allow: font-size-literal -- "!" sized to the switch thumb + fontWeight: FontWeight.w800, + height: 1); + +/// A first-run step without artwork: the title (and an optional line under it) at the top, the +/// step's [body] below, and the step's [action] pinned to the bottom, above the keyboard when it is +/// up. The host's [MediaQuery] top padding already clears the progress line and back button. +/// +/// With [centered], the title stays at the top and the body sits in the middle of the space between +/// the title and the action, staying centred as the keyboard opens and closes. +class OnboardingPlainStep extends StatelessWidget { + const OnboardingPlainStep({ + super.key, + required this.title, + this.subtitle, + required this.body, + this.action, + this.centered = false, + }); + + final String title; + final String? subtitle; + final Widget body; + final Widget? action; + final bool centered; + + @override + Widget build(BuildContext context) { + final heading = [ + Semantics(header: true, child: Text(title, style: OmiType.title1, textAlign: TextAlign.center)), + if (subtitle != null) ...[ + const SizedBox(height: OmiSpacing.xs), + Text(subtitle!, style: OmiType.subhead.copyWith(color: OmiColors.textSecondary), textAlign: TextAlign.center), + ], + ]; + final Widget content = Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + ...heading, + const SizedBox(height: OmiSpacing.xxl), + Expanded( + child: centered + // The body alone sits in the middle of the space between the title and the action. + ? LayoutBuilder( + builder: (context, constraints) => SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints(minHeight: constraints.maxHeight), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [body], + ), + ), + ), + ) + : SingleChildScrollView(child: body), + ), + ], + ); + return SafeArea( + child: Padding( + padding: const EdgeInsets.fromLTRB(OmiSpacing.xl, OmiSpacing.lg, OmiSpacing.xl, kOnboardingButtonBottomGap), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Expanded(child: content), + if (action != null) ...[const SizedBox(height: OmiSpacing.md), action!], + ], + ), + ), + ); + } +} + +/// The toggle on a first-run permission row. The state's colour lives only inside the switch: +/// grey track when unanswered, the accent when allowed, orange (or [deniedColor]) when the reader +/// said no, with a "!" on the thumb so the state does not rely on colour alone. +class OnboardingPermissionToggle extends StatelessWidget { + const OnboardingPermissionToggle({ + super.key, + required this.on, + required this.denied, + this.busy = false, + this.deniedColor, + }); + + final bool on; + final bool denied; + final bool busy; + final Color? deniedColor; + + static const double _width = 51; + static const double _height = 31; + static const double _thumb = 27; + + @override + Widget build(BuildContext context) { + final motion = OmiMotion.of(context); + final denyColor = deniedColor ?? OmiColors.warning; + final track = on + ? OmiColors.accent + : denied + ? denyColor + : OmiColors.surface3; + // On the accent track the thumb takes the accent's contrast colour (white on black, black on white). + final thumb = + on ? OmiColors.onAccent : const Color(0xFFFFFFFF); // omi-ux-allow: color-literal -- white thumb, as iOS + return AnimatedContainer( + duration: motion.standard, + curve: Curves.easeOut, + width: _width, + height: _height, + padding: const EdgeInsets.all(2), + decoration: BoxDecoration(color: track, borderRadius: OmiRadius.pillAll), + child: AnimatedAlign( + duration: motion.standard, + curve: Curves.easeOut, + alignment: on ? Alignment.centerRight : Alignment.centerLeft, + child: Container( + width: _thumb, + height: _thumb, + decoration: BoxDecoration( + color: thumb, + shape: BoxShape.circle, + boxShadow: const [_thumbShadow], + ), + child: Center( + child: busy + ? const SizedBox(width: 14, height: 14, child: OmiSpinner(size: OmiSpinnerSize.small)) + : denied && !on + ? Text( + '!', + style: _bangStyle.copyWith(color: denyColor), + ) + : null, + ), + ), + ), + ); + } +} diff --git a/app/lib/pages/onboarding/widgets/onboarding_step_layout.dart b/app/lib/pages/onboarding/widgets/onboarding_step_layout.dart index 81ee3c007ad..c6169324d2b 100644 --- a/app/lib/pages/onboarding/widgets/onboarding_step_layout.dart +++ b/app/lib/pages/onboarding/widgets/onboarding_step_layout.dart @@ -24,7 +24,9 @@ class OnboardingStepLayout extends StatelessWidget { children: [ if (progress != null) Padding( - padding: const EdgeInsets.only(top: OmiSpacing.md), + // Same top inset as the back button so the progress line sits level with it; the bottom + // inset keeps the header row its full height, so content below does not move. + padding: const EdgeInsets.only(top: OmiSpacing.xxs, bottom: OmiSpacing.md - OmiSpacing.xxs), child: progress!, ), if (onBack != null) @@ -32,7 +34,7 @@ class OnboardingStepLayout extends StatelessWidget { padding: const EdgeInsets.only(left: OmiSpacing.xs, top: OmiSpacing.xxs), child: Align( alignment: Alignment.topLeft, - child: OmiBackButton.circled(key: const Key('onboarding_back'), onPressed: onBack), + child: OmiBackButton(key: const Key('onboarding_back'), onPressed: onBack, color: OmiColors.textPrimary), ), ), ], diff --git a/app/lib/pages/onboarding/wrapper.dart b/app/lib/pages/onboarding/wrapper.dart index 8dbfa812174..1fecab68c40 100644 --- a/app/lib/pages/onboarding/wrapper.dart +++ b/app/lib/pages/onboarding/wrapper.dart @@ -1,5 +1,10 @@ +import 'dart:async'; + +import 'package:omi/utils/logger.dart'; +import 'package:omi/l10n/app_localizations.dart'; +import 'package:omi/pages/home/widgets/home_tab_switcher.dart'; +import 'package:omi/backend/http/api/action_items.dart'; import 'package:omi/services/onboarding_sync_runtime.dart'; -import 'onboarding_step_telemetry.dart'; import 'package:omi/utils/platform/platform_manager.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -7,13 +12,20 @@ import 'package:flutter/material.dart'; import 'package:firebase_auth/firebase_auth.dart'; import 'package:provider/provider.dart'; -import 'package:omi/backend/http/api/knowledge_graph_api.dart'; import 'package:omi/backend/preferences.dart'; import 'package:omi/gen/assets.gen.dart'; import 'package:omi/pages/home/page.dart'; +import 'package:omi/pages/onboarding/widgets/onboarding_hero_film.dart'; +import 'package:omi/pages/onboarding/widgets/onboarding_card.dart'; import 'package:omi/pages/onboarding/ai_consent_widget.dart'; import 'package:omi/pages/onboarding/auth.dart'; -import 'package:omi/pages/onboarding/found_omi/found_omi_widget.dart'; +import 'package:omi/pages/onboarding/activation/activation_capture.dart'; +import 'package:omi/pages/onboarding/activation/activation_flow.dart'; +import 'package:omi/pages/onboarding/activation/onboarding_plans_sheet.dart'; +import 'package:omi/pages/onboarding/connect_omi/connect_omi_step.dart'; +import 'package:omi/services/superwall_pilot.dart'; +import 'package:omi/providers/device_provider.dart'; +import 'package:omi/backend/http/api/knowledge_graph_api.dart'; import 'package:omi/pages/onboarding/knowledge_graph_step.dart'; import 'package:omi/pages/onboarding/name/name_widget.dart'; import 'package:omi/pages/onboarding/permissions/permissions_checker.dart'; @@ -30,8 +42,10 @@ import 'package:omi/pages/onboarding/guided_voice_controller.dart'; import 'package:omi/pages/onboarding/guided_voice_io.dart'; import 'package:omi/pages/onboarding/widgets/onboarding_step_layout.dart'; import 'package:omi/providers/home_provider.dart'; +import 'package:omi/providers/onboarding_provider.dart'; import 'package:omi/providers/usage_provider.dart'; import 'package:omi/services/auth_service.dart'; +import 'package:omi/services/experiments/onboarding_activation_flow.dart'; import 'package:omi/services/experiments/onboarding_setup_rating_prompt.dart'; import 'package:omi/utils/analytics/intercom.dart'; import 'package:omi/utils/analytics/registry/events.g.dart'; @@ -39,6 +53,9 @@ import 'package:omi/utils/analytics/registry/typed_events.dart'; import 'package:omi/utils/analytics/product_telemetry.dart'; import 'package:omi/utils/l10n_extensions.dart'; import 'package:omi/utils/other/temp.dart'; +import 'package:omi/utils/device.dart'; +import 'package:omi/pages/onboarding/interactive_device_onboarding/interactive_device_onboarding_wrapper.dart'; +import 'package:omi/backend/schema/bt_device/bt_device.dart'; import 'package:omi/app_globals.dart'; import 'package:omi/core/app_shell.dart'; import 'package:omi/ui/ui.dart'; @@ -49,25 +66,6 @@ class OnboardingWrapper extends StatefulWidget { final bool forceAuthPage; - /// The step helpers live on the private state class. Tests call them through the public widget. - @visibleForTesting - static bool isResumableStepForTesting(int index) => _OnboardingWrapperState.isResumableStepForTesting(index); - - @visibleForTesting - static int? previousStepForTesting({ - required int currentIndex, - required bool setupPageEnabled, - required bool knowledgeGraphStepEnabled, - }) => - _OnboardingWrapperState.previousStepForTesting( - currentIndex: currentIndex, - setupPageEnabled: setupPageEnabled, - knowledgeGraphStepEnabled: knowledgeGraphStepEnabled, - ); - - @visibleForTesting - static int resumeStepForTesting(int saved) => _OnboardingWrapperState.resolveResumeStep(saved); - /// Debug builds with `--dart-define=OMI_ONBOARDING_RERUN=1` walk onboarding again on a /// signed-in account; startup routing honours it so the reader lands in the wrapper. static bool get rerunForTesting => _OnboardingWrapperState._rerunForTesting; @@ -81,13 +79,17 @@ class _OnboardingWrapperState extends State with TickerProvid static const int kAuthPage = 0; static const int kAiConsentPage = 1; // Data-and-AI disclosure with explicit consent static const int kNamePage = 2; - static const int kPrimaryLanguagePage = 3; - static const int kFoundOmiPage = 4; - static const int kPermissionsPage = 5; - static const int kSpeechProfilePage = 6; // Guided voice introduction + static const int kPermissionsPage = 3; + static const int kPrimaryLanguagePage = 4; + static const int kConnectOmiPage = 5; // Finds the reader's Omi (search starts after sign-in) + // The ending after Connect Omi, A/B by OnboardingActivationExperiment: the guided voice + // introduction (control) or the activation (treatment: Omi at work, the first conversation, + // congrats). + static const int kSpeechProfilePage = 6; + static const int kActivationPage = kSpeechProfilePage; static const int kKnowledgeGraphPage = 7; // Memory graph preview static const int kSetupPage = 8; // "Setting up your Omi" + rating pre-prompt; flag-gated, else skipped - static const int kCompletePage = 9; // Capture choice; keep persisted index stable. + static const int kCompletePage = 9; // "You're all set" completion screen static const int kPageCount = 10; /// The steps the progress dots count, in order. Auth, consent and the completion screen are not @@ -105,9 +107,9 @@ class _OnboardingWrapperState extends State with TickerProvid static const List kProgressSteps = [ kNamePage, - kPrimaryLanguagePage, - kFoundOmiPage, kPermissionsPage, + kPrimaryLanguagePage, + kConnectOmiPage, kSpeechProfilePage, if (kKnowledgeGraphStepEnabled) kKnowledgeGraphPage, ]; @@ -115,21 +117,11 @@ class _OnboardingWrapperState extends State with TickerProvid TabController? _controller; late AnimationController _backgroundAnimationController; late Animation _backgroundFadeAnimation; - String _currentBackgroundImage = Assets.images.onboardingBg2.path; bool get hasSpeechProfile => SharedPreferencesUtil().hasSpeakerProfile; Future? _knowledgeGraphPrebuildFuture; Future? _setupPageEnabled; - bool? _setupPageEnabledResolved; ProductAttempt? _onboardingAttempt; bool _finishing = false; - late final _stepTelemetry = MobileOnboardingStepTelemetry( - emit: const TypedEvents().emit, - platform: () => switch (Theme.of(context).platform) { - TargetPlatform.iOS => OnboardingStepCompletedPlatform.ios, - TargetPlatform.android => OnboardingStepCompletedPlatform.android, - _ => OnboardingStepCompletedPlatform.other, - }, - ); GuidedVoiceController? _voiceController; GuidedVoiceController _getVoiceController() { @@ -140,6 +132,14 @@ class _OnboardingWrapperState extends State with TickerProvid ); } + /// Which ending this reader gets; null until the flag has been read. + OnboardingEnding? _ending; + Future? _endingFuture; + + /// The step on screen and which way the reader last moved, for the step transition. + int _shownIndex = 0; + bool _forward = true; + @override void initState() { super.initState(); @@ -149,27 +149,22 @@ class _OnboardingWrapperState extends State with TickerProvid surface: ProductSurface.onboarding, ); } - final signedIn = !widget.forceAuthPage && AuthService.instance.isSignedIn(); - final initialIndex = - signedIn ? (!SharedPreferencesUtil().aiConsentGiven ? kAiConsentPage : _resumeStep()) : kAuthPage; - _stepTelemetry.seed(initialIndex); - // Auth, AiConsent, Name, Lang, FoundOmi, Permissions, SpeechProfile, KnowledgeGraph, Setup, Complete - _controller = TabController(length: kPageCount, initialIndex: initialIndex, vsync: this); + // Auth, AiConsent, Name, Permissions, Lang, ConnectOmi, SpeechProfile, KnowledgeGraph, Setup, Complete + _controller = TabController(length: kPageCount, vsync: this); _controller!.addListener(() { if (!mounted) return; - _stepTelemetry.enter(_controller!.index); - setState(() {}); + setState(() { + _forward = _controller!.index >= _shownIndex; + _shownIndex = _controller!.index; + }); _rememberStep(_controller!.index); - // Update background image when page changes - _updateBackgroundImage(_controller!.index); - // Precache next image for smoother transitions - _precacheNextImage(_controller!.index); - if (_controller!.index == kSpeechProfilePage && _knowledgeGraphPrebuildFuture == null) { - _knowledgeGraphPrebuildFuture = _prebuildKnowledgeGraph().catchError((_) {}); - } - // Read the flag two steps ahead so the decision is ready when the reader taps Continue. - if (_controller!.index == kSpeechProfilePage) { - _ensureSetupPageFlag(); + // Read the ending's flag from Connect Omi on, so it is decided when the reader leaves it. + if (_controller!.index >= kConnectOmiPage) unawaited(_resolveEnding()); + // Control, as before the activation: start the memory graph and read the setup page's flag + // while the reader does the voice introduction, so the setup page has them ready. + if (_controller!.index == kSpeechProfilePage && _ending == OnboardingEnding.voiceIntro) { + _knowledgeGraphPrebuildFuture ??= _prebuildKnowledgeGraph().catchError((_) {}); + _setupPageEnabled ??= OnboardingSetupRatingPromptGate.isEnabled(); } }); @@ -182,7 +177,6 @@ class _OnboardingWrapperState extends State with TickerProvid end: 1.0, ).animate(CurvedAnimation(parent: _backgroundAnimationController, curve: Curves.easeInOut)); - _currentBackgroundImage = _getBackgroundImageForIndex(initialIndex) ?? _currentBackgroundImage; // Start initial animations _backgroundAnimationController.forward(); WidgetsBinding.instance.addPostFrameCallback((_) async { @@ -205,11 +199,20 @@ class _OnboardingWrapperState extends State with TickerProvid } else if (SharedPreferencesUtil().onboardingCompleted && !_rerunForTesting) { await _routeWithPermissionsCheck(context); } else { + _startDeviceSearch(); _enterSteps(); } } } - // If not signed in, it stays at the Auth page (index 0) + // If not signed in, it stays at the Auth page (index 0): the app's first screen. It records the + // welcome and asks for Bluetooth shortly after it appears, so the search for the reader's Omi + // can start right after sign-in (the separate "Get Started" welcome screen is gone). + if (!widget.forceAuthPage && !AuthService.instance.isSignedIn()) { + PlatformManager.instance.analytics.onboardingWelcomeShown(); + await Future.delayed(const Duration(milliseconds: 800)); + if (!mounted) return; + await context.read().askForBluetoothAtLaunch(); + } }); } @@ -228,7 +231,6 @@ class _OnboardingWrapperState extends State with TickerProvid } Future _routeWithPermissionsCheck(BuildContext context) async { - _stepTelemetry.exit(); if (!SharedPreferencesUtil().permissionsCompleted) { final granted = await arePermissionsGranted(); if (!granted) { @@ -240,39 +242,33 @@ class _OnboardingWrapperState extends State with TickerProvid SharedPreferencesUtil().permissionsCompleted = true; } if (context.mounted) { - routeToPage(context, const HomePageWrapper(), replace: true); + _goHome(); } } - /// Marks first-run onboarding complete and routes home. Shared by the native completion screen - /// and the web onboarding page, so only the wrapper ever owns completion. - Future _finishOnboarding() async { - if (_finishing) return; - _finishing = true; - final saved = await OnboardingSyncRuntime.enqueue(completed: true); - _finishing = false; - if (!mounted || !context.mounted) return; - if (!saved) { - OmiFeedback.error(context, context.l10n.somethingWentWrong); - return; - } - SharedPreferencesUtil().onboardingCompleted = true; - SharedPreferencesUtil().permissionsCompleted = true; - SharedPreferencesUtil().remove(_resumeKey); - _stepTelemetry.exit(); - _completeOnboardingTelemetry(); - PlatformManager.instance.analytics.onboardingCompleted(); - PaintingBinding.instance.imageCache.clear(); - await _maybeShowOnboardingPaywall(); - if (!mounted) return; - routeToPage(context, const HomePageWrapper(), replace: true); + /// Onboarding is over: Home replaces everything, fading in (a slide would show it moving over + /// the app root, which may already be redrawing as Home underneath: a double Home). + void _goHome() { + Navigator.of(context).pushAndRemoveUntil(omiRootRoute(builder: (_) => const HomePageWrapper()), (_) => false); } - /// The experiment's end-of-onboarding placement: only arms whose payload asks for it, never - /// control, and never when subscription UI is hidden (the router checks that). + /// Starts looking for the reader's Omi in the background so "Connect your Omi" can show it at once. + void _startDeviceSearch() { + if (widget.forceAuthPage) return; + unawaited(context.read().startQuietDeviceScan()); + } + + /// The paywall experiment's end-of-onboarding placement: only arms whose payload asks for it, + /// never when subscription UI is hidden (the router checks that), and never when the activation + /// already offered a plan. Control sees nothing here but is exposed at the same moment. Future _maybeShowOnboardingPaywall() async { + if (SuperwallPilot.instance.offeredDuringOnboarding) return; final config = await PaywallExperiment.resolve(); - if (!mounted || config.isControl || !config.onboardingPaywall) return; + if (!mounted || !config.onboardingPaywall) return; + if (config.isControl) { + PaywallRouter.exposeControl(context, config: config); + return; + } await PaywallRouter.openVariant(context, source: 'onboarding', config: config); } @@ -288,13 +284,206 @@ class _OnboardingWrapperState extends State with TickerProvid if (!mounted) return; if (result == WebOnboardingResult.completed) { const TypedEvents().emit(const OnboardingWebviewCompleted()); - await _finishOnboarding(); + await _completeOnboarding(); return; } } _controller!.animateTo(_resumeStep()); } + /// Leaving "Connect your Omi". A device connected here skips the post-onboarding device + /// tutorial (it stays in device settings). + OnboardingDeviceChoice? _deviceChoice; + + /// The paywall inside the activation, between the celebration and the rating ask. + Future _showPaywall() async { + SuperwallPilot.instance.offeredDuringOnboarding = true; + unawaited(context.read().fetchSubscription()); + if (await SuperwallPilot.instance.showPlacementAndWait(SuperwallPilot.deviceSetupCompletePlacement)) return; + if (!mounted) return; + // The paywall experiment's arm when the reader is in one; control keeps the onboarding sheet. + if (await PaywallRouter.openVariant(context, source: 'onboarding')) return; + if (!mounted) return; + await showOmiSheet(context: context, padding: EdgeInsets.zero, builder: (_) => const OnboardingPlansSheet()); + } + + /// Saves onboarding as done and opens Home. False when the save failed (the step stays, so the + /// reader can tap again). + Future _completeOnboarding() async { + if (_finishing) return false; + _finishing = true; + final saved = await OnboardingSyncRuntime.enqueue(completed: true); + _finishing = false; + if (!mounted || !context.mounted) return false; + if (!saved) { + OmiFeedback.error(context, context.l10n.somethingWentWrong); + return false; + } + SharedPreferencesUtil().onboardingCompleted = true; + SharedPreferencesUtil().permissionsCompleted = true; + unawaited(_addStarterTasks(context.l10n)); + SharedPreferencesUtil().remove(_resumeKey); + OnboardingActivationExperiment.forget(SharedPreferencesUtil().uid); + _completeOnboardingTelemetry(); + PlatformManager.instance.analytics.onboardingCompleted(); + PaintingBinding.instance.imageCache.clear(); + await _maybeShowOnboardingPaywall(); + if (!mounted) return true; + _goHome(); + return true; + } + + /// An empty Tasks tab teaches nothing: when onboarding produced no to-dos, add three starter tasks + /// that walk the person through Omi (once per account; never on a failed read, so nobody gets + /// them on top of real tasks). + static Future _addStarterTasks(AppLocalizations l10n) async { + final key = 'onboarding/starterTasks/${SharedPreferencesUtil().uid}'; + if (SharedPreferencesUtil().getBool(key)) return; + // The activation already showed real to-dos (they may still be saving): no starter tasks. + if (NewTasksDot.showing) { + SharedPreferencesUtil().saveBool(key, true); + return; + } + try { + final existing = await tryGetActionItems(limit: 1); + if (existing == null) return; + SharedPreferencesUtil().saveBool(key, true); + if (existing.actionItems.isNotEmpty) return; + for (final description in [ + l10n.onboardingStarterTaskAsk, + l10n.onboardingStarterTaskRecord, + l10n.onboardingStarterTaskCheck, + ]) { + await createActionItem(description: description); + } + NewTasksDot.show(); + } catch (e) { + Logger.debug('Starter tasks not added: $e'); + } + } + + Future _leaveConnectOmi(OnboardingDeviceChoice choice) async { + _deviceChoice = choice; + PlatformManager.instance.analytics.onboardingStepCompleted('Connect Omi'); + // A reader who just connected an Omi learns its button now, before trying it; the tutorial + // marks itself done, so Home won't show it again. + if (choice == OnboardingDeviceChoice.connected && _needsDeviceTutorial()) { + await routeToPage(context, const InteractiveDeviceOnboardingWrapper(duringSetup: true)); + if (!mounted) return; + } + unawaited(_enterEnding()); + } + + bool _needsDeviceTutorial() { + if (SharedPreferencesUtil().deviceOnboardingCompleted) return false; + final devices = context.read(); + final device = devices.connectedDevice; + if (device == null || device.type != DeviceType.omi) return false; + return DeviceUtils.isOmiCv1(modelNumber: devices.pairedDevice?.modelNumber, deviceName: device.name); + } + + // ---- The ending: voice introduction (control) or activation (treatment) ------------------ + + /// The ending for this account: the one it already started, else the flag's answer (control on + /// any doubt). Read once per onboarding. + Future _resolveEnding() { + return _endingFuture ??= () async { + final uid = SharedPreferencesUtil().uid; + final ending = OnboardingActivationExperiment.remembered(uid) ?? await OnboardingActivationExperiment.resolve(); + if (mounted) setState(() => _ending = ending); + return ending; + }(); + } + + /// Leaves Connect Omi once the ending is decided, and keeps it for this account so a reader who + /// comes back later finishes the same ending. + Future _enterEnding() async { + final ending = await _resolveEnding(); + if (!mounted) return; + OnboardingActivationExperiment.remember(SharedPreferencesUtil().uid, ending); + // The arm the reader actually sees (a slow flag read falls back to control while PostHog may + // still log the flag call as treatment): compare arms on this, not on the flag exposure. + PlatformManager.instance.analytics.track( + 'Onboarding Ending Shown', + properties: {'ending': ending.wireName, ...OnboardingActivationExperiment.exposureProperties()}, + ); + PlatformManager.instance.analytics.setUserProperty('Onboarding Ending', ending.wireName); + _controller!.animateTo(kSpeechProfilePage); + } + + Future _prebuildKnowledgeGraph() async { + try { + final current = await KnowledgeGraphApi.getKnowledgeGraph(); + final nodes = current['nodes'] as List? ?? const []; + final hasGraph = nodes.any((node) => (node['id'] ?? '') != 'user-node'); + if (hasGraph) return; + } catch (_) { + // Continue to rebuild below. + } + + final rebuildResult = await KnowledgeGraphApi.rebuildKnowledgeGraph(); + final status = rebuildResult['status']; + // 'canonical_up_to_date' is the synthetic client status returned on HTTP 409. + if (status == 'canonical_up_to_date') { + return; + } + await KnowledgeGraphApi.waitForGraphStability( + timeout: const Duration(seconds: 25), + interval: const Duration(seconds: 2), + stabilityChecks: 1, + ); + } + + /// Control: after the voice introduction, the knowledge graph when shown, otherwise the setup + /// page or the completion screen, exactly as before the activation existed. + void _leaveSpeechProfile() { + if (kKnowledgeGraphStepEnabled) { + _controller!.animateTo(kKnowledgeGraphPage); + } else { + _continueAfterKnowledgeGraph(); + } + } + + Widget _endingPage() { + if (widget.forceAuthPage) return const SizedBox.shrink(); + switch (_ending) { + case null: + return const SizedBox.shrink(); + case OnboardingEnding.voiceIntro: + // The guided introduction owns its transcription-only session and reviews statements + // before explicitly saving them as memories. + return SpeechProfileWidget( + flowSource: 'first_run', + controller: _getVoiceController(), + goNext: () { + // All Done is not enroll success (#12765). Upload/embedding events fire only from the + // guided I/O upload receipt. + PlatformManager.instance.analytics.speechProfileContinued(); + _leaveSpeechProfile(); + }, + onSkip: () { + PlatformManager.instance.analytics.speechProfileSkipped(); + _leaveSpeechProfile(); + }, + ); + case OnboardingEnding.activation: + return ActivationFlow( + hasDevice: _deviceChoice == OnboardingDeviceChoice.connected || + context.read().isConnected || + DemoActivationCapture.pretendsDevice, + showPaywall: _showPaywall, + onFinished: () { + PlatformManager.instance.analytics.onboardingStepCompleted('Activation'); + return _completeOnboarding(); + }, + ); + } + } + + /// The activation is a moment, not a form: no progress line and no way back. The voice + /// introduction keeps its progress line. + bool get _onActivation => _controller!.index == kActivationPage && _ending != OnboardingEnding.voiceIntro; + void _goNext() { if (_controller!.index < _controller!.length - 1) { _controller!.animateTo(_controller!.index + 1); @@ -312,39 +501,20 @@ class _OnboardingWrapperState extends State with TickerProvid /// Setup page when the flag allows it, otherwise the completion screen. Used by the Knowledge /// Graph step's Continue and, while that step is hidden, straight from the speech profile. - void _ensureSetupPageFlag() { - _setupPageEnabled ??= OnboardingSetupRatingPromptGate.isEnabled().then((enabled) { - _setupPageEnabledResolved = enabled; - if (mounted) setState(() {}); - return enabled; - }); - } - Future _continueAfterKnowledgeGraph() async { - _ensureSetupPageFlag(); - final enabled = await _setupPageEnabled!; - _setupPageEnabledResolved = enabled; + final enabled = await (_setupPageEnabled ?? OnboardingSetupRatingPromptGate.isEnabled()); if (!mounted) return; _controller!.animateTo(stepAfterKnowledgeGraph(setupPageEnabled: enabled)); } - void _leaveSpeechProfile() { - if (kKnowledgeGraphStepEnabled) { - _controller!.animateTo(kKnowledgeGraphPage); - } else { - _continueAfterKnowledgeGraph(); - } - } - // ---- Resume and back ---------------------------------------------------------------------- /// Per-account key so a different account signing in on this phone starts at the Name step. - String get _resumeKey => 'onboarding/resumeStep/${SharedPreferencesUtil().uid}'; - - bool _isResumableStep(int index) => index >= kNamePage && index <= kCompletePage; + /// v2: the redesign renumbered the steps, so a step saved by an older build is not reused. + String get _resumeKey => 'onboarding/resumeStep.v2/${SharedPreferencesUtil().uid}'; void _rememberStep(int index) { - if (widget.forceAuthPage || !_isResumableStep(index)) return; + if (widget.forceAuthPage || !kProgressSteps.contains(index)) return; SharedPreferencesUtil().saveInt(_resumeKey, index); } @@ -352,87 +522,34 @@ class _OnboardingWrapperState extends State with TickerProvid /// or Name. int _resumeStep() { final saved = SharedPreferencesUtil().getInt(_resumeKey, defaultValue: kNamePage); - return resolveResumeStep(saved); - } - - static int resolveResumeStep(int saved) { - if (!isResumableStepForTesting(saved)) return kNamePage; - if (!kKnowledgeGraphStepEnabled && saved == kKnowledgeGraphPage) { - return stepAfterKnowledgeGraph(setupPageEnabled: false); - } - return saved; + return kProgressSteps.contains(saved) ? saved : kNamePage; } /// The step before the current one, or null when there is nothing to go back to (Auth, consent, - /// Name). + /// Name, completion). int? get _previousStep { - final current = _controller!.index; - if (current == kCompletePage) { - return _setupPageEnabledResolved == true - ? kSetupPage - : (kKnowledgeGraphStepEnabled ? kKnowledgeGraphPage : kSpeechProfilePage); - } - if (current == kSetupPage) { - return kKnowledgeGraphStepEnabled ? kKnowledgeGraphPage : kSpeechProfilePage; - } - if (current == kKnowledgeGraphPage) { - return kSpeechProfilePage; - } - final position = kProgressSteps.indexOf(current); + // Connect Omi is the threshold into the experience: no way back into the setup questions + // (language and permissions can be changed later in Settings). + // The ending that follows (activation or voice introduction) is not a form: no back either. + if (_controller!.index == kConnectOmiPage || _controller!.index == kActivationPage) return null; + final position = kProgressSteps.indexOf(_controller!.index); if (position <= 0) return null; return kProgressSteps[position - 1]; } - @visibleForTesting - static bool isResumableStepForTesting(int index) => index >= kNamePage && index <= kCompletePage; - - @visibleForTesting - static int? previousStepForTesting({ - required int currentIndex, - required bool setupPageEnabled, - required bool knowledgeGraphStepEnabled, - }) { - if (currentIndex == kCompletePage) { - return setupPageEnabled ? kSetupPage : (knowledgeGraphStepEnabled ? kKnowledgeGraphPage : kSpeechProfilePage); - } - if (currentIndex == kSetupPage) { - return knowledgeGraphStepEnabled ? kKnowledgeGraphPage : kSpeechProfilePage; - } - if (currentIndex == kKnowledgeGraphPage) { - return kSpeechProfilePage; - } - final steps = [ - kNamePage, - kPrimaryLanguagePage, - kFoundOmiPage, - kPermissionsPage, - kSpeechProfilePage, - if (knowledgeGraphStepEnabled) kKnowledgeGraphPage, - ]; - final position = steps.indexOf(currentIndex); - if (position <= 0) return null; - return steps[position - 1]; - } - - bool _speechStepBusy = false; - /// Back — the on-screen control, Android system back and the iOS swipe all land here. void _goBack() { final previous = _previousStep; if (previous == null) return; - // Never leave while the introduction is saving; its own PopScope blocks too. - if (_controller!.index == kSpeechProfilePage && _speechStepBusy) return; OmiHaptics.selection(); - _stepTelemetry.skipped = false; - _stepTelemetry.exitReason = OnboardingStepCompletedExitReason.back; _controller!.animateTo(previous); } /// "Use a Different Account" on the consent step: sign out and start again from the beginning. Future _useDifferentAccount() async { + final navigator = Navigator.of(context, rootNavigator: true); _voiceController?.dispose(); _voiceController = null; - final navigator = Navigator.of(context, rootNavigator: true); final rootContext = globalNavigatorKey.currentContext; if (rootContext != null && rootContext.mounted) clearAllUserState(rootContext); await SharedPreferencesUtil().clear(); @@ -440,92 +557,22 @@ class _OnboardingWrapperState extends State with TickerProvid navigator.pushAndRemoveUntil(omiPageRoute(builder: (_) => const AppShell()), (_) => false); } - Future _prebuildKnowledgeGraph() async { - try { - final current = await KnowledgeGraphApi.getKnowledgeGraph(); - final nodes = current['nodes'] as List? ?? const []; - final hasGraph = nodes.any((node) => (node['id'] ?? '') != 'user-node'); - if (hasGraph) return; - } catch (_) { - // Continue to rebuild below. - } - - final rebuildResult = await KnowledgeGraphApi.rebuildKnowledgeGraph(); - final status = rebuildResult['status']; - // 'canonical_up_to_date' is the synthetic client status returned on HTTP 409. - if (status == 'canonical_up_to_date') { - return; - } - await KnowledgeGraphApi.waitForGraphStability( - timeout: const Duration(seconds: 25), - interval: const Duration(seconds: 2), - stabilityChecks: 1, - ); - } - - void _updateBackgroundImage(int pageIndex) { - final newImage = _getBackgroundImageForIndex(pageIndex) ?? Assets.images.onboardingBg1.path; - if (_currentBackgroundImage != newImage) { - setState(() { - _currentBackgroundImage = newImage; - }); - _backgroundAnimationController.reset(); - _backgroundAnimationController.forward(); - } - } - - void _precacheNextImage(int currentIndex) { - // Get the next background image path - String? nextImagePath = _getBackgroundImageForIndex(currentIndex + 1); - if (nextImagePath != null && mounted) { - // Precache the next image - precacheImage( - ResizeImage( - AssetImage(nextImagePath), - width: (MediaQuery.of(context).size.width * MediaQuery.of(context).devicePixelRatio).round(), - height: (MediaQuery.of(context).size.height * MediaQuery.of(context).devicePixelRatio).round(), - ), - context, - ); - } - } - - String? _getBackgroundImageForIndex(int pageIndex) { - switch (pageIndex) { - case kAuthPage: - case kAiConsentPage: - return Assets.images.onboardingBg2.path; - case kNamePage: - case kFoundOmiPage: - return Assets.images.onboardingBg1.path; - case kPrimaryLanguagePage: - return Assets.images.onboardingBg4.path; - case kPermissionsPage: - case kSpeechProfilePage: - return Assets.images.onboardingBg3.path; - case kKnowledgeGraphPage: - case kSetupPage: - case kCompletePage: - return Assets.images.onboardingBg6.path; - default: - return null; - } - } - + /// Sign-in and the data & privacy consent: the Omi pendant at the top, on the same light surface + /// as the rest of onboarding (no photo). Widget _background() { final media = MediaQuery.of(context); return FadeTransition( opacity: _backgroundFadeAnimation, - child: Container( - height: media.size.height, - decoration: BoxDecoration( - image: DecorationImage( - image: ResizeImage( - AssetImage(_currentBackgroundImage), - width: (media.size.width * media.devicePixelRatio).round(), - height: (media.size.height * media.devicePixelRatio).round(), - ), + child: Align( + alignment: Alignment.topCenter, + // The cord runs off the top edge, so the pendant hangs into view. + child: ExcludeSemantics( + child: Assets.images.omiPendantGlow.image( + key: const Key('onboarding_pendant_hero'), + width: media.size.width, + height: media.size.height * 0.6, fit: BoxFit.cover, + alignment: Alignment.topCenter, ), ), ), @@ -554,6 +601,7 @@ class _OnboardingWrapperState extends State with TickerProvid } else if (SharedPreferencesUtil().onboardingCompleted) { await _routeWithPermissionsCheck(context); } else { + _startDeviceSearch(); _enterSteps(); } }, @@ -569,6 +617,7 @@ class _OnboardingWrapperState extends State with TickerProvid if (SharedPreferencesUtil().onboardingCompleted) { await _routeWithPermissionsCheck(context); } else { + _startDeviceSearch(); _enterSteps(); } }, @@ -576,7 +625,7 @@ class _OnboardingWrapperState extends State with TickerProvid ), NameWidget( goNext: () { - _goNext(); // Go to Primary Language page + _goNext(); // Go to Permissions page IntercomManager.instance.updateUser( FirebaseAuth.instance.currentUser!.email, FirebaseAuth.instance.currentUser!.displayName, @@ -585,47 +634,20 @@ class _OnboardingWrapperState extends State with TickerProvid PlatformManager.instance.analytics.onboardingStepCompleted('Name'); }, ), - PrimaryLanguageWidget( - goNext: () { - _goNext(); // Go to Found Omi page - PlatformManager.instance.analytics.onboardingStepCompleted('Primary Language'); - }, - ), - FoundOmiWidget( + PermissionsWidget( goNext: () { - _goNext(); // Go to Permissions page - PlatformManager.instance.analytics.onboardingStepCompleted('Acquisition Source'); + _goNext(); // Go to Primary Language page + PlatformManager.instance.analytics.onboardingStepCompleted('Permissions'); }, ), - PermissionsWidget( + PrimaryLanguageWidget( goNext: () { - // Straight to the voice introduction (phone mic; no device step). The review step was - // removed from onboarding to comply with App Store Guideline 5.6.3 (no rating prompts - // during onboarding). - _goNext(); - PlatformManager.instance.analytics.onboardingStepCompleted('Permissions'); + _goNext(); // Go to Connect Omi page + PlatformManager.instance.analytics.onboardingStepCompleted('Primary Language'); }, ), - widget.forceAuthPage - ? const SizedBox.shrink() - // The guided introduction owns its transcription-only session and - // reviews statements before explicitly saving them as memories. - : SpeechProfileWidget( - flowSource: 'first_run', - controller: _getVoiceController(), - onBusyChanged: (busy) => _speechStepBusy = busy, - goNext: () { - // All Done is not enroll success (#12765). Upload/embedding - // events fire only from the guided I/O upload receipt. - PlatformManager.instance.analytics.speechProfileContinued(); - _leaveSpeechProfile(); - }, - onSkip: () { - PlatformManager.instance.analytics.speechProfileSkipped(); - _stepTelemetry.skipped = true; - _leaveSpeechProfile(); - }, - ), + widget.forceAuthPage ? const SizedBox.shrink() : ConnectOmiStep(onDone: _leaveConnectOmi), + _endingPage(), OnboardingKnowledgeGraphStep(onContinue: _leaveKnowledgeGraph), OnboardingSetupPage( pendingWork: _knowledgeGraphPrebuildFuture, @@ -634,21 +656,24 @@ class _OnboardingWrapperState extends State with TickerProvid _controller!.animateTo(kCompletePage); }, ), - OnboardingCompleteScreen( - onSkipped: () => _stepTelemetry.skipped = true, - onComplete: _finishOnboarding, - ), + OnboardingCompleteScreen(onComplete: () => unawaited(_completeOnboarding())), ]; // The speech step draws the Omi device with a mic-level glow instead of a background image, // matching the Settings redo page; the setup and completion screens draw their own. - final showBackground = index != kCompletePage && index != kSetupPage && index != kSpeechProfilePage; + // Name, permissions, language and connect sit on the plain surface, without artwork. + final showBackground = index == kAuthPage || index == kAiConsentPage; final previous = _previousStep; return PopScope( // System back and the iOS swipe step back one step, like the on-screen back button. On the // first step (and Auth / consent) back leaves onboarding as usual. - canPop: previous == null, + canPop: previous == null && + index != kConnectOmiPage && + index != kActivationPage && + // Control's last screens: Android back must not drop the reader out of onboarding. + index != kSetupPage && + index != kCompletePage, onPopInvokedWithResult: (didPop, _) { if (!didPop) _goBack(); }, @@ -658,14 +683,25 @@ class _OnboardingWrapperState extends State with TickerProvid backgroundColor: OmiColors.surface0, body: Stack( children: [ - if (index == kAuthPage || showBackground) _background(), + if (showBackground) OnboardingHero.video ? const OnboardingHeroFilm() : _background(), OnboardingStepLayout( - reserveHeader: index == kSpeechProfilePage, + reserveHeader: index == kConnectOmiPage, onBack: previous == null ? null : _goBack, - progress: kProgressSteps.contains(index) - ? OnboardingProgressDots(current: kProgressSteps.indexOf(index), total: kProgressSteps.length) + // The activation is a moment, not a form: no progress line over it. + progress: kProgressSteps.contains(index) && !_onActivation + ? OnboardingProgressLine(current: kProgressSteps.indexOf(index), total: kProgressSteps.length) : null, - child: pages[index], + child: _StepTransition( + index: index, + forward: _forward, + // Arriving at Connect Omi is a moment, not another form: a plain cross-fade lets the + // step's own entrance carry it. + fadeOnly: index == kConnectOmiPage, + // Sign-in and consent share their photo: the consent card rises over it, so it + // reads as part of signing in rather than a new page. + rise: index == kAiConsentPage || (index == kAuthPage && !_forward), + child: pages[index], + ), ), ], ), @@ -675,6 +711,61 @@ class _OnboardingWrapperState extends State with TickerProvid } } +/// Moves between steps: the new step slides in from the side the reader is heading and fades up, +/// the old one drifts the other way and fades out. Instant under Reduce Motion. +class _StepTransition extends StatelessWidget { + const _StepTransition({ + required this.index, + required this.forward, + required this.child, + this.fadeOnly = false, + this.rise = false, + }); + + final int index; + final bool forward; + final bool fadeOnly; + + /// The incoming step rises from the bottom edge (a sheet over the same background); the + /// outgoing one sinks back down when going the other way. + final bool rise; + final Widget child; + + static const double _shift = 0.08; + + @override + Widget build(BuildContext context) { + final reduce = OmiMotion.of(context).emphasized == Duration.zero; + return AnimatedSwitcher( + duration: reduce ? Duration.zero : const Duration(milliseconds: 420), + reverseDuration: reduce ? Duration.zero : const Duration(milliseconds: 260), + switchInCurve: Curves.easeOutCubic, + switchOutCurve: Curves.easeInCubic, + layoutBuilder: (current, previous) => Stack( + fit: StackFit.expand, + children: [...previous, if (current != null) current], + ), + transitionBuilder: (child, animation) { + final incoming = child.key == ValueKey(index); + // An outgoing child runs its animation backwards, ending at its begin offset. + final from = fadeOnly || rise ? 0.0 : ((incoming == forward) ? _shift : -_shift); + final Offset begin = rise ? Offset(0, incoming == forward ? 0.35 : 0) : Offset(from, 0); + return IgnorePointer( + ignoring: !incoming, + child: FadeTransition( + opacity: animation, + child: SlideTransition( + position: Tween(begin: begin, end: Offset.zero).animate(animation), + child: child, + ), + ), + ); + }, + child: KeyedSubtree(key: ValueKey(index), child: child), + ); + } +} + /// Exposes the counted steps to tests without widening the wrapper's state class. @visibleForTesting abstract final class OnboardingProgressStepsForTest { @@ -687,10 +778,10 @@ abstract final class OnboardingProgressStepsForTest { _OnboardingWrapperState.stepAfterKnowledgeGraph(setupPageEnabled: setupPageEnabled); } -/// The first-run progress: one dot per real step, the current one larger, with a spoken -/// "Step N of M". -class OnboardingProgressDots extends StatelessWidget { - const OnboardingProgressDots({super.key, required this.current, required this.total}); +/// The first-run progress: one thin line that fills as the reader moves through the real steps, +/// with a spoken "Step N of M". +class OnboardingProgressLine extends StatelessWidget { + const OnboardingProgressLine({super.key, required this.current, required this.total}); /// Zero-based position of the current step. final int current; @@ -699,26 +790,40 @@ class OnboardingProgressDots extends StatelessWidget { @override Widget build(BuildContext context) { final motion = OmiMotion.of(context); + final fraction = total <= 0 ? 0.0 : ((current + 1) / total).clamp(0.0, 1.0); return Semantics( label: context.l10n.onboardingStepOf(current + 1, total), excludeSemantics: true, child: SizedBox( height: kOmiMinTapTarget, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: List.generate(total, (i) { - final isCurrent = i == current; - return AnimatedContainer( - duration: motion.quick, - margin: const EdgeInsets.symmetric(horizontal: OmiSpacing.xxs), - width: isCurrent ? 12.0 : 8.0, - height: isCurrent ? 12.0 : 8.0, - decoration: BoxDecoration( - color: i <= current ? OmiColors.textPrimary : OmiColors.textTertiary, - shape: BoxShape.circle, + child: Center( + child: Padding( + // Clear of the back button on the left, balanced on the right. + padding: const EdgeInsets.symmetric(horizontal: kOmiMinTapTarget + OmiSpacing.md), + child: ClipRRect( + borderRadius: OmiRadius.pillAll, + child: SizedBox( + width: double.infinity, + height: 3, + child: Stack( + children: [ + Positioned.fill(child: ColoredBox(color: OmiColors.surface2)), + TweenAnimationBuilder( + tween: Tween(end: fraction), + duration: motion.standard, + curve: Curves.easeOutCubic, + builder: (context, value, _) => FractionallySizedBox( + widthFactor: value, + heightFactor: 1, + alignment: Alignment.centerLeft, + child: ColoredBox(color: OmiColors.textPrimary), + ), + ), + ], + ), ), - ); - }), + ), + ), ), ), ); diff --git a/app/lib/pages/paywall/paywall_router.dart b/app/lib/pages/paywall/paywall_router.dart index b9a553861e5..dbd497f8565 100644 --- a/app/lib/pages/paywall/paywall_router.dart +++ b/app/lib/pages/paywall/paywall_router.dart @@ -5,6 +5,7 @@ import 'package:omi/pages/paywall/outline_paywall_page.dart'; import 'package:omi/pages/paywall/web_paywall_page.dart'; import 'package:omi/providers/usage_provider.dart'; import 'package:omi/services/experiments/paywall_experiment.dart'; +import 'package:omi/services/experiments/paywall_reask.dart'; import 'package:omi/utils/analytics/registry/events.g.dart'; import 'package:omi/utils/analytics/registry/typed_events.dart'; import 'package:omi/utils/other/temp.dart'; @@ -35,6 +36,7 @@ abstract final class PaywallRouter { // The reader is about to see a paywall: the variant page below, or the sheet the opener shows // right after a false return. Either way they count for their arm. PaywallExperiment.recordExposure(resolved, record: record); + PaywallReask.noteShown(); const TypedEvents().emit(PaywallVariantShown(variant: variantWire(resolved.variant), source: sourceWire(source))); if (resolved.isControl) return false; @@ -52,6 +54,22 @@ abstract final class PaywallRouter { return true; } + /// Control's side of a placement only variants have (the end of first-run onboarding): nothing + /// is shown, but an eligible control reader is exposed at the same moment a variant reader would + /// see a paywall, so both arms enter the experiment at the same point. True when exposed. + static bool exposeControl( + BuildContext context, { + required PaywallExperimentConfig config, + bool Function(String key, String variant)? record, + }) { + if (!config.isControl) return false; + final usage = context.read(); + if (!usage.showSubscriptionUI) return false; + if (usage.subscription?.subscription.plan.isPaid == true) return false; + PaywallExperiment.recordExposure(config, record: record); + return true; + } + static PaywallVariantShownVariant variantWire(PaywallVariant variant) => switch (variant) { PaywallVariant.control => PaywallVariantShownVariant.control, PaywallVariant.outline => PaywallVariantShownVariant.outline, diff --git a/app/lib/pages/settings/chat_apps/chat_apps_page.dart b/app/lib/pages/settings/chat_apps/chat_apps_page.dart index a131f90599d..06726464bad 100644 --- a/app/lib/pages/settings/chat_apps/chat_apps_page.dart +++ b/app/lib/pages/settings/chat_apps/chat_apps_page.dart @@ -19,6 +19,7 @@ import 'package:omi/ui/ui.dart'; import 'package:omi/utils/l10n_extensions.dart'; import 'package:omi/utils/other/temp.dart'; import 'package:omi/utils/platform/platform_manager.dart'; +import 'package:omi/services/superwall_pilot.dart'; /// "Chat apps" on Integrations: one row opening [ChatAppsPage]. Hidden unless the surface is on /// for this account or a link already exists (so Disconnect stays reachable). @@ -95,7 +96,13 @@ class _ChatAppsPageState extends State { 'chat_apps', currentPlan: context.read().subscription?.subscription.plan.wireName ?? 'unknown', ); - routeToPage(context, const UsagePage(showUpgradeDialog: true, paywallSource: 'chat_apps')); + routeToPage( + context, + const UsagePage( + showUpgradeDialog: true, + upgradePlacement: SuperwallPilot.chatAppsPlacement, + paywallSource: 'chat_apps', + )); } Future _connect(ChatChannel channel) async { diff --git a/app/lib/pages/settings/settings_drawer.dart b/app/lib/pages/settings/settings_drawer.dart index 086c3cb6fd9..1582f839382 100644 --- a/app/lib/pages/settings/settings_drawer.dart +++ b/app/lib/pages/settings/settings_drawer.dart @@ -5,9 +5,11 @@ import 'package:provider/provider.dart'; import 'package:omi/backend/preferences.dart'; import 'package:omi/pages/settings/settings_destinations.dart'; +import 'package:omi/models/subscription.dart'; import 'package:omi/pages/settings/settings_groups.dart'; -import 'package:omi/providers/usage_provider.dart'; import 'package:omi/pages/settings/widgets/plans/plan_display_name.dart'; +import 'package:omi/pages/settings/widgets/settings_plan_section.dart'; +import 'package:omi/providers/usage_provider.dart'; import 'package:omi/pages/settings/settings_search_index.dart'; import 'package:omi/providers/device_provider.dart'; import 'package:omi/ui/ui.dart'; @@ -106,11 +108,17 @@ class _SettingsDrawerState extends State { ); } - /// Show the subscriber's actual tier on the Plan & Usage row. + /// The plan on the Plan & Usage row: "Free Plan" and the Plus plan name (their section adds an + /// Upgrade button), "Pro" for the other paid plans. String? _planValue(UsageProvider usage) { - final plan = usage.subscription?.subscription.plan; - if (plan == null || !plan.isPaid) return null; - return currentPlanDisplayName(context, usage.subscription); + final subscription = usage.subscription; + final plan = subscription?.subscription.plan; + if (plan == null) return null; + if (usage.showSubscriptionUI && (plan == PlanType.basic || plan == PlanType.plus)) { + return planDisplayName(plan, subscription!.availablePlans, context.l10n); + } + if (!plan.isPaid) return null; + return currentPlanDisplayName(context, subscription); } Widget _buildSettings(BuildContext context) { @@ -137,12 +145,14 @@ class _SettingsDrawerState extends State { // Plan, referrals and feedback stay one tap from the sheet (David, 2026-09-24). OmiSettingsGroup( children: [ - _row( - SettingsDestination.planAndUsage, - key: 'settings_row_planAndUsage', - icon: FontAwesomeIcons.chartLine, - title: l10n.planAndUsage, - value: planValue, + SettingsPlanSection( + row: _row( + SettingsDestination.planAndUsage, + key: 'settings_row_planAndUsage', + icon: FontAwesomeIcons.chartLine, + title: l10n.planAndUsage, + value: planValue, + ), ), _row( SettingsDestination.referral, diff --git a/app/lib/pages/settings/transcription_settings_page.dart b/app/lib/pages/settings/transcription_settings_page.dart index 639701e7ef0..f6305b7f0ba 100644 --- a/app/lib/pages/settings/transcription_settings_page.dart +++ b/app/lib/pages/settings/transcription_settings_page.dart @@ -33,6 +33,7 @@ import 'package:omi/ui/ui.dart'; import 'package:omi/utils/l10n_extensions.dart'; import 'package:omi/utils/logger.dart'; import 'package:omi/utils/other/temp.dart'; +import 'package:omi/services/superwall_pilot.dart'; /// Top-level transcription source the user picks from the single dropdown. enum TranscriptionMode { omi, onDevice, cloudProvider, omiParakeet } @@ -883,7 +884,11 @@ class _TranscriptionSettingsPageState extends State { PlatformManager.instance.analytics.paywallOpened('transcription_settings', currentPlan: plan); routeToPage( context, - const UsagePage(showUpgradeDialog: true, paywallSource: 'transcription_settings'), + const UsagePage( + showUpgradeDialog: true, + upgradePlacement: SuperwallPilot.transcriptionSettingsPlacement, + paywallSource: 'transcription_settings', + ), ); }, child: ConstrainedBox( diff --git a/app/lib/pages/settings/usage_page.dart b/app/lib/pages/settings/usage_page.dart index a3d3785c426..90229197590 100644 --- a/app/lib/pages/settings/usage_page.dart +++ b/app/lib/pages/settings/usage_page.dart @@ -21,14 +21,14 @@ import 'package:omi/models/subscription.dart'; import 'package:omi/models/user_usage.dart'; import 'package:omi/pages/settings/fair_use_page.dart'; import 'package:omi/pages/settings/transcription_settings_page.dart'; -import 'package:omi/pages/paywall/paywall_router.dart'; -import 'package:omi/pages/settings/widgets/plans_sheet.dart'; import 'package:omi/pages/settings/widgets/plans/contextual_plans.dart'; import 'package:omi/pages/settings/widgets/usage/usage_chart.dart'; import 'package:omi/pages/settings/widgets/usage/usage_stat_tile.dart'; import 'package:omi/providers/usage_provider.dart'; import 'package:omi/pages/settings/widgets/plans/plan_display_name.dart'; import 'package:omi/services/wals/sync_rate_limit_reconciliation.dart'; +import 'package:omi/pages/settings/widgets/upgrade_paywall.dart'; +import 'package:omi/services/superwall_pilot.dart'; import 'package:omi/ui/ui.dart'; import 'package:omi/utils/l10n_extensions.dart'; import 'package:omi/utils/other/temp.dart'; @@ -36,9 +36,13 @@ import 'package:omi/utils/other/temp.dart'; class UsagePage extends StatefulWidget { final bool showUpgradeDialog; - /// Surface id used when [showUpgradeDialog] opens the plans sheet. + /// Surface id used when [showUpgradeDialog] opens the offer on arrival. /// The usage-page upgrade button always reports `usage_page`. final String paywallSource; + + /// Superwall placement for the offer [showUpgradeDialog] opens on arrival. The page's own Upgrade + /// button always uses `usage_upgrade`. + final String upgradePlacement; @visibleForTesting final bool debugSkipFetch; @visibleForTesting @@ -53,6 +57,7 @@ class UsagePage extends StatefulWidget { super.key, this.showUpgradeDialog = false, this.paywallSource = 'usage_page', + this.upgradePlacement = SuperwallPilot.usageUpgradePlacement, this.forceNativePaywall = false, this.debugSkipFetch = false, this.debugNow, @@ -68,10 +73,6 @@ class _UsagePageState extends State with TickerProviderStateMixin, Wi final List _screenshotKeys = List.generate(4, (_) => GlobalKey()); final GlobalKey _shareButtonKey = GlobalKey(); UsageMetric _selectedMetric = UsageMetric.words; - late AnimationController _waveController; - late AnimationController _notesController; - late AnimationController _arrowController; - late Animation _arrowAnimation; String selectedPlan = 'yearly'; // 'yearly' or 'monthly' Map? _fairUseStatus; @@ -319,17 +320,6 @@ class _UsagePageState extends State with TickerProviderStateMixin, Wi WidgetsBinding.instance.addObserver(this); _tabController = TabController(length: 4, vsync: this); _tabController.addListener(_handleTabSelection); - _waveController = AnimationController(duration: const Duration(milliseconds: 18000), vsync: this)..repeat(); - _notesController = AnimationController(duration: const Duration(milliseconds: 36000), vsync: this)..repeat(); - - _arrowController = AnimationController(duration: const Duration(milliseconds: 600), vsync: this) - ..repeat(reverse: true); - - _arrowAnimation = Tween( - begin: 0, - end: 4, - ).animate(CurvedAnimation(parent: _arrowController, curve: Curves.easeInOut)); - WidgetsBinding.instance.addPostFrameCallback((_) { if (widget.debugSkipFetch) return; context.read().fetchUsageStats(period: 'today'); @@ -338,7 +328,7 @@ class _UsagePageState extends State with TickerProviderStateMixin, Wi _loadFairUseStatus(); if (widget.showUpgradeDialog && context.read().showSubscriptionUI) { // The opener already emitted Paywall Opened with this source. - _showPlansSheet(source: widget.paywallSource, trackOpen: false); + _showPlansSheet(placement: widget.upgradePlacement, source: widget.paywallSource, trackOpen: false); } }); } @@ -348,9 +338,6 @@ class _UsagePageState extends State with TickerProviderStateMixin, Wi WidgetsBinding.instance.removeObserver(this); _tabController.removeListener(_handleTabSelection); _tabController.dispose(); - _waveController.dispose(); - _notesController.dispose(); - _arrowController.dispose(); super.dispose(); } @@ -548,31 +535,20 @@ class _UsagePageState extends State with TickerProviderStateMixin, Wi ); } - Future _showPlansSheet({required String source, required bool trackOpen}) async { - if (!context.read().showSubscriptionUI) { - return; - } - if (!widget.forceNativePaywall && await PaywallRouter.openVariant(context, source: source, trackOpen: trackOpen)) { - return; - } - if (!mounted) return; - if (trackOpen) { - final plan = context.read().subscription?.subscription.plan.wireName ?? 'unknown'; - PlatformManager.instance.analytics.paywallOpened(source, currentPlan: plan); - } - final title = await plansSheetTitle(context); - if (!mounted || !context.read().showSubscriptionUI) return; - showOmiSheet( - context: context, - title: title, - padding: EdgeInsets.zero, - builder: (context) => PlansSheet( - waveController: _waveController, - notesController: _notesController, - arrowController: _arrowController, - arrowAnimation: _arrowAnimation, - analyticsSource: source, - ), + void _showPlansSheet({ + String placement = SuperwallPilot.usageUpgradePlacement, + required String source, + required bool trackOpen, + }) { + final usage = context.read(); + if (!usage.showSubscriptionUI) return; + openUpgradePaywall( + context, + placement: placement, + source: source, + trackOpen: trackOpen, + freeUser: usage.subscription?.subscription.plan.isPaid == false, + forceNative: widget.forceNativePaywall, ); } diff --git a/app/lib/pages/settings/widgets/settings_plan_section.dart b/app/lib/pages/settings/widgets/settings_plan_section.dart new file mode 100644 index 00000000000..07a97a174ba --- /dev/null +++ b/app/lib/pages/settings/widgets/settings_plan_section.dart @@ -0,0 +1,98 @@ +import 'package:flutter/material.dart'; + +import 'package:intl/intl.dart'; +import 'package:provider/provider.dart'; + +import 'package:omi/models/subscription.dart'; +import 'package:omi/pages/settings/widgets/upgrade_paywall.dart'; +import 'package:omi/providers/usage_provider.dart'; +import 'package:omi/services/superwall_pilot.dart'; +import 'package:omi/ui/ui.dart'; +import 'package:omi/utils/l10n_extensions.dart'; + +/// The Plan & Usage row on the Settings sheet, plus — for Free and Plus — this month's minutes and +/// an Upgrade button, so upgrading is one tap from Settings instead of two. +class SettingsPlanSection extends StatelessWidget { + const SettingsPlanSection({super.key, required this.row}); + + /// The Plan & Usage row; tapping it still opens the Plan & Usage page. + final Widget row; + + @override + Widget build(BuildContext context) { + final usage = context.watch(); + final subscription = usage.subscription; + final plan = subscription?.subscription.plan; + if (subscription == null || !usage.showSubscriptionUI || (plan != PlanType.basic && plan != PlanType.plus)) { + return row; + } + final l10n = context.l10n; + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + mainAxisSize: MainAxisSize.min, + children: [ + row, + Padding( + padding: const EdgeInsets.fromLTRB(OmiSpacing.lg, 0, OmiSpacing.lg, OmiSpacing.lg), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + mainAxisSize: MainAxisSize.min, + children: [ + if (subscription.transcriptionSecondsLimit > 0) ...[ + _MinutesMeter(subscription: subscription), + const SizedBox(height: OmiSpacing.md), + ], + OmiButton( + key: const ValueKey('settings_plan_upgrade'), + label: plan == PlanType.plus ? l10n.upgradeToUnlimited : l10n.upgrade, + expand: true, + onPressed: () => openSettingsUpgrade(context, plan!), + ), + ], + ), + ), + ], + ); + } +} + +/// Free users go through the Superwall `settings_upgrade` placement when the pilot is on; Plus +/// users (and any fallback) get the existing plans sheet. +Future openSettingsUpgrade(BuildContext context, PlanType plan) => openUpgradePaywall( + context, + placement: SuperwallPilot.settingsUpgradePlacement, + source: 'usage_page', + freeUser: plan == PlanType.basic, + ); + +class _MinutesMeter extends StatelessWidget { + const _MinutesMeter({required this.subscription}); + + final UserSubscriptionResponse subscription; + + @override + Widget build(BuildContext context) { + final l10n = context.l10n; + final used = (subscription.transcriptionSecondsUsed / 60).round(); + final limit = (subscription.transcriptionSecondsLimit / 60).round(); + final ratio = (subscription.transcriptionSecondsUsed / subscription.transcriptionSecondsLimit).clamp(0.0, 1.0); + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + l10n.minsUsedThisMonth(NumberFormat.decimalPattern(l10n.localeName).format(used), limit), + style: OmiType.footnote.copyWith(color: OmiColors.textSecondary), + ), + const SizedBox(height: OmiSpacing.xs), + LinearProgressIndicator( + value: ratio, + backgroundColor: OmiColors.surface3, + valueColor: AlwaysStoppedAnimation(ratio >= .8 ? OmiColors.warning : OmiColors.accent), + minHeight: 4, + borderRadius: OmiRadius.pillAll, + ), + ], + ); + } +} diff --git a/app/lib/pages/settings/widgets/standalone_plans_sheet.dart b/app/lib/pages/settings/widgets/standalone_plans_sheet.dart new file mode 100644 index 00000000000..6a53edd7ac3 --- /dev/null +++ b/app/lib/pages/settings/widgets/standalone_plans_sheet.dart @@ -0,0 +1,54 @@ +import 'package:flutter/material.dart'; + +import 'package:omi/pages/settings/widgets/plans_sheet.dart'; +import 'package:omi/ui/ui.dart'; + +/// [PlansSheet] with its own animation controllers, for callers that open the sheet without +/// hosting the Plan & Usage page's animations. +class StandalonePlansSheet extends StatefulWidget { + const StandalonePlansSheet({super.key, this.analyticsSource = 'usage_page'}); + + final String analyticsSource; + + @override + State createState() => _StandalonePlansSheetState(); +} + +class _StandalonePlansSheetState extends State with TickerProviderStateMixin { + late final AnimationController _waveController = + AnimationController(duration: const Duration(milliseconds: 18000), vsync: this)..repeat(); + late final AnimationController _arrowController = + AnimationController(duration: const Duration(milliseconds: 800), vsync: this)..repeat(reverse: true); + late final Animation _arrowAnimation = + Tween(begin: 0, end: 3).animate(CurvedAnimation(parent: _arrowController, curve: Curves.easeInOut)); + + @override + void dispose() { + _waveController.dispose(); + _arrowController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return PlansSheet( + waveController: _waveController, + notesController: _waveController, + arrowController: _arrowController, + arrowAnimation: _arrowAnimation, + analyticsSource: widget.analyticsSource, + ); + } +} + +/// Opens the plans sheet over [context]. +Future showStandalonePlansSheet(BuildContext context, {String analyticsSource = 'usage_page'}) async { + final title = await plansSheetTitle(context); + if (!context.mounted) return; + return showOmiSheet( + context: context, + title: title, + padding: EdgeInsets.zero, + builder: (_) => StandalonePlansSheet(analyticsSource: analyticsSource), + ); +} diff --git a/app/lib/pages/settings/widgets/upgrade_paywall.dart b/app/lib/pages/settings/widgets/upgrade_paywall.dart new file mode 100644 index 00000000000..ddddd05f7b3 --- /dev/null +++ b/app/lib/pages/settings/widgets/upgrade_paywall.dart @@ -0,0 +1,53 @@ +import 'package:flutter/material.dart'; + +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:provider/provider.dart'; + +import 'package:omi/pages/paywall/paywall_router.dart'; +import 'package:omi/pages/settings/widgets/standalone_plans_sheet.dart'; +import 'package:omi/providers/usage_provider.dart'; +import 'package:omi/services/experiments/paywall_experiment.dart'; +import 'package:omi/services/experiments/paywall_reask.dart'; +import 'package:omi/services/superwall_pilot.dart'; +import 'package:omi/utils/platform/platform_manager.dart'; + +/// Shows the upgrade offer for one paywall placement and logs `Paywall Opened` with [source] (a +/// `PaywallOpenedSource` wire name) once. [trackOpen] is false when the opener already logged it. +/// +/// Free users go through the Superwall [placement] when the pilot is on (iOS debug only). Otherwise +/// the paywall experiment ([PaywallRouter]) picks the reader's arm; control, paid readers and any +/// failure get the existing plans sheet over the current screen. [forceNative] skips both and opens +/// the sheet itself: the web variant's fallback, so a dead page never loops back into itself. +Future openUpgradePaywall( + BuildContext context, { + required String placement, + required String source, + bool freeUser = true, + bool trackOpen = true, + bool forceNative = false, + @visibleForTesting PaywallExperimentConfig? experiment, +}) async { + void logOpen() { + if (!trackOpen) return; + trackOpen = false; + final plan = context.read().subscription?.subscription.plan.wireName ?? 'unknown'; + PlatformManager.instance.analytics.paywallOpened(source, currentPlan: plan); + } + + if (!forceNative && freeUser && SuperwallPilot.instance.isEnabled) { + final user = FirebaseAuth.instance.currentUser; + if (user != null && !user.isAnonymous) { + logOpen(); + PaywallReask.noteShown(); + if (await SuperwallPilot.instance.showPlacement(placement, user.uid)) return; + if (!context.mounted) return; + } + } + if (!forceNative) { + // The router logs `Paywall Opened` itself when it shows a variant. + if (await PaywallRouter.openVariant(context, source: source, trackOpen: trackOpen, config: experiment)) return; + if (!context.mounted) return; + } + logOpen(); + await showStandalonePlansSheet(context, analyticsSource: source); +} diff --git a/app/lib/pages/shop/omi_shop.dart b/app/lib/pages/shop/omi_shop.dart new file mode 100644 index 00000000000..05b07591c3f --- /dev/null +++ b/app/lib/pages/shop/omi_shop.dart @@ -0,0 +1,25 @@ +import 'package:flutter/material.dart'; + +import 'package:omi/pages/apps/widgets/omi_web_page.dart'; +import 'package:omi/utils/l10n_extensions.dart'; +import 'package:omi/utils/platform/platform_manager.dart'; + +/// The Omi store, tagged with where the reader came from so orders can be attributed. +Uri omiShopUrl(String source) => Uri.https('www.omi.me', '/', {'_ref': source}); + +/// Opens the Omi store inside the app (a clean in-app web page, not the browser), so buying the +/// device does not take the reader out of Omi. [source] names the entry point, e.g. +/// `onboarding_not_ordered`, and is sent as `_ref` and on the `Omi Shop Opened` event. Completes +/// when the reader closes the store. +Future openOmiShop(BuildContext context, {required String source}) async { + PlatformManager.instance.analytics.getOmiDeviceClicked(); + PlatformManager.instance.analytics.track('Omi Shop Opened', properties: {'source': source}); + final title = context.l10n.getOmiDevice; + // Presented as a sheet that slides up, with Close rather than Back: it is a detour, not a step. + await Navigator.of(context).push( + MaterialPageRoute( + fullscreenDialog: true, + builder: (_) => OmiWebPage(title: title, url: omiShopUrl(source), showClose: true), + ), + ); +} diff --git a/app/lib/providers/action_items_provider.dart b/app/lib/providers/action_items_provider.dart index 52c78d5f48c..18c060eae7b 100644 --- a/app/lib/providers/action_items_provider.dart +++ b/app/lib/providers/action_items_provider.dart @@ -90,6 +90,9 @@ class ActionItemsProvider extends ChangeNotifier { // UI filter: show completed tasks view bool _showCompletedView = false; + // UI layout: group the list by project instead of by due date (offered once projects exist) + bool _groupByProject = false; + // Date range filter DateTime? _startDate; DateTime? _endDate; @@ -136,6 +139,7 @@ class ActionItemsProvider extends ChangeNotifier { bool get hasMore => _hasMore; bool get includeCompleted => _includeCompleted; bool get showCompletedView => _showCompletedView; + bool get groupByProject => _groupByProject; DateTime? get startDate => _startDate; DateTime? get endDate => _endDate; bool get hasActiveFilter => _startDate != null || _endDate != null; @@ -1043,6 +1047,11 @@ class ActionItemsProvider extends ChangeNotifier { notifyListeners(); } + void toggleGroupByProject() { + _groupByProject = !_groupByProject; + notifyListeners(); + } + void setDateRangeFilter(DateTime? startDate, DateTime? endDate) { _startDate = startDate; _endDate = endDate; diff --git a/app/lib/providers/auth_provider.dart b/app/lib/providers/auth_provider.dart index db8dfa0151c..3dba0e2a9a3 100644 --- a/app/lib/providers/auth_provider.dart +++ b/app/lib/providers/auth_provider.dart @@ -16,6 +16,7 @@ import 'package:omi/providers/base_provider.dart'; import 'package:omi/services/account_cutover/account_cutover_runtime.dart'; import 'package:omi/services/auth_service.dart'; import 'package:omi/services/siri_integration.dart'; +import 'package:omi/services/superwall_pilot.dart'; import 'package:omi/services/auth/auth_token_result.dart'; import 'package:omi/services/notifications.dart'; import 'package:omi/utils/auth/clear_user_state.dart'; @@ -74,6 +75,7 @@ class AuthenticationProvider extends BaseProvider { Future.microtask(() { _authStateSubscription = _auth.authStateChanges().distinct((p, n) => p?.uid == n?.uid).listen((User? user) async { PlatformManager.instance.analytics.bindIdentity(user?.uid); + unawaited(SuperwallPilot.instance.syncUser(user != null && !user.isAnonymous ? user.uid : null)); unawaited(ProactivityRuntime.outbox.bindOwner(user != null && !user.isAnonymous ? user.uid : null)); AuthService.instance.handleAuthUserChanged(user?.uid); Logger.debug( diff --git a/app/lib/providers/device_provider.dart b/app/lib/providers/device_provider.dart index 9d9c3fa760f..4d45caf4a00 100644 --- a/app/lib/providers/device_provider.dart +++ b/app/lib/providers/device_provider.dart @@ -103,6 +103,17 @@ class DeviceProvider extends ChangeNotifier implements IDeviceServiceSubsciption DateTime? _lastBatteryNotifyTime; bool _hasLowBatteryAlerted = false; bool _havingNewFirmware = false; + + /// A firmware update was found during first-run onboarding; Home shows the prompt once it opens. + bool _firmwarePromptDeferred = false; + + /// Shows a firmware update prompt that was held back during first-run onboarding. + void showDeferredFirmwareUpdatePrompt(BuildContext context) { + if (!_firmwarePromptDeferred) return; + _firmwarePromptDeferred = false; + if (_havingNewFirmware && context.mounted) showFirmwareUpdateDialog(context); + } + bool get havingNewFirmware => _havingNewFirmware && pairedDevice != null && isConnected && _allowsFirmwareUpdateForPairedDevice; @@ -1291,6 +1302,11 @@ class DeviceProvider extends ChangeNotifier implements IDeviceServiceSubsciption // Use a small delay to ensure the UI is ready Future.delayed(const Duration(milliseconds: 500), () { if (!_isCurrent(generation) || !_isCurrentFirmwareCheckSession(checkSession)) return; + // A first-run reader is mid-onboarding: hold the prompt until Home is up. + if (!SharedPreferencesUtil().onboardingCompleted) { + _firmwarePromptDeferred = true; + return; + } final context = globalNavigatorKey.currentContext; if (context != null && context.mounted) { showFirmwareUpdateDialog(context); diff --git a/app/lib/providers/message_provider.dart b/app/lib/providers/message_provider.dart index a152710450a..69c97b282f7 100644 --- a/app/lib/providers/message_provider.dart +++ b/app/lib/providers/message_provider.dart @@ -603,6 +603,7 @@ class MessageProvider extends ChangeNotifier with ChatHistoryState { List> audioBytes, { Function? onFirstChunkRecived, Future Function()? onNoSpeech, + Future Function()? onFailed, BleAudioCodec? codec, bool playResponseAudio = false, }) async { @@ -769,6 +770,7 @@ class MessageProvider extends ChangeNotifier with ChatHistoryState { setShowTypingIndicator(false); _logChatFailure(ChatStreamFailureClass.quota, stage: 'voice_stream'); completeChat(ProductOutcome.failure, failure: ProductFailure.quota); + if (onFailed != null) await onFailed(); return; } final failure = _chunkFailureClass(chunk); @@ -779,6 +781,7 @@ class MessageProvider extends ChangeNotifier with ChatHistoryState { } completeChat(ProductOutcome.failure, failure: _productFailureFor(failure)); notifyListeners(); + if (onFailed != null) await onFailed(); return; } } @@ -791,6 +794,7 @@ class MessageProvider extends ChangeNotifier with ChatHistoryState { } completeChat(ProductOutcome.failure, failure: _productFailureFor(failure.kind)); notifyListeners(); + if (onFailed != null) await onFailed(); } finally { _voiceSendInFlight = false; setShowTypingIndicator(false); @@ -801,6 +805,8 @@ class MessageProvider extends ChangeNotifier with ChatHistoryState { await OmiVoicePlaybackService.instance.interrupt(source: VoiceReplyPlaybackInterruptSource.streamError); } completeChat(ProductOutcome.failure, failure: ProductFailure.incomplete); + // The stream ended without an answer: whoever waits on it (the device tutorial) must hear so. + if (onFailed != null) await onFailed(); } } diff --git a/app/lib/providers/onboarding_provider.dart b/app/lib/providers/onboarding_provider.dart index eaa3bbf6887..f988a66ec0b 100644 --- a/app/lib/providers/onboarding_provider.dart +++ b/app/lib/providers/onboarding_provider.dart @@ -31,14 +31,6 @@ import 'package:omi/utils/analytics/product_telemetry.dart'; class OnboardingProvider extends BaseProvider with MessageNotifierMixin implements IDeviceServiceSubsciption { DeviceProvider? deviceProvider; - DeviceConnectFailureShownFailureReason? pairingFailureReason; - int pairingFailureVersion = 0; - - void dismissPairingFailure() { - pairingFailureReason = null; - notifyListeners(); - } - bool isClicked = false; bool isConnected = false; int batteryPercentage = -1; @@ -186,6 +178,52 @@ class OnboardingProvider extends BaseProvider with MessageNotifierMixin implemen notifyListeners(); } + /// Whether Omi may scan for and connect to Bluetooth devices, read without prompting. + Future readBluetoothPermission() async { + final granted = Platform.isIOS + ? await Permission.bluetooth.isGranted + : await Permission.bluetoothScan.isGranted && await Permission.bluetoothConnect.isGranted; + if (granted != hasBluetoothPermission) updateBluetoothPermission(granted); + return granted; + } + + /// The first-run Bluetooth ask, shown once over the welcome screen so that, after sign-in, the + /// reader's Omi can already be found by the time they reach "Connect your Omi". It only shows the + /// system prompt: no Bluetooth-off guidance before the reader has started. + Future askForBluetoothAtLaunch() async { + const askedKey = 'onboarding/bluetoothAskedAtLaunch'; + if (SharedPreferencesUtil().getBool(askedKey)) return; + SharedPreferencesUtil().saveBool(askedKey, true); + try { + if (Platform.isIOS) { + updateBluetoothPermission((await Permission.bluetooth.request()).isGranted); + } else { + final scan = await Permission.bluetoothScan.request(); + final connect = await Permission.bluetoothConnect.request(); + updateBluetoothPermission(scan.isGranted && connect.isGranted); + } + } catch (e) { + Logger.debug('Bluetooth ask at launch failed: $e'); + } + } + + /// Starts looking for nearby devices in the background, without prompts, when Bluetooth is + /// already allowed. "Connect your Omi" then shows the device the moment it opens. + Future startQuietDeviceScan() async { + if (_isDisposed || isConnected) return; + if (!await readBluetoothPermission()) return; + // Only when the radio is already on: an off adapter would raise the shared "Enable Bluetooth" + // guidance over an unrelated step. "Connect your Omi" handles that case itself. + try { + if (BluetoothReadiness.parse(await BleHostApi().getBluetoothState()) != BluetoothAdapterState.on) return; + } catch (_) { + return; + } + if (_isDisposed) return; + ServiceManager.instance().device.subscribe(this, this); + await deviceProvider?.initiateConnection('OnboardingEarlyScan'); + } + Future askForNotificationPermissions() async { final attempt = ProductTelemetry.instance.start( ProductJourney.permission, @@ -312,7 +350,6 @@ class OnboardingProvider extends BaseProvider with MessageNotifierMixin implemen Future handleTap({required BtDevice device, required bool isFromOnboarding, VoidCallback? goNext}) async { try { if (isClicked) return; - pairingFailureReason = null; isClicked = true; connectingToDeviceId = device.id; @@ -378,9 +415,6 @@ class OnboardingProvider extends BaseProvider with MessageNotifierMixin implemen isClicked = false; // Allow clicks again after finishing the operation connectingToDeviceId = null; // Reset the connecting device deviceProvider?.setIsConnected(false); - pairingFailureVersion++; - pairingFailureReason = - connectFailureReason(e, staleBond: ServiceManager.instance().device.staleBondRecoveryRequired); PlatformManager.instance.analytics.deviceConnectFailed(classifyDeviceConnectError(e)); notifyError('DEVICE_CONNECT_FAILED'); notifyListeners(); @@ -460,8 +494,7 @@ class OnboardingProvider extends BaseProvider with MessageNotifierMixin implemen if (!await BluetoothReadiness.instance.ensureReady(BluetoothUse.discovery)) { if (_isDisposed || epoch != _scanEpoch) return; - _emitPairingBlocked(_pairingBlockForAdapter(BluetoothReadiness.instance.state), - adapterState: BluetoothReadiness.instance.state); + _emitPairingBlocked(_pairingBlockForAdapter(BluetoothReadiness.instance.state)); return; } if (_isDisposed || epoch != _scanEpoch) return; @@ -478,6 +511,14 @@ class OnboardingProvider extends BaseProvider with MessageNotifierMixin implemen await deviceProvider?.initiateConnection("Onboarding"); } + /// Ends a search screen's session: stops scanning and clears "nothing found", so the next search + /// screen (onboarding's Connect Omi or the in-app pairing page, which share this provider) starts + /// fresh instead of opening on a stale "No Omi Found". + void endSearch() { + cancelActiveScan(); + enableInstructions = false; + } + void _recordOnboardingPermission({ required OnboardingPermissionResultPermission permission, required bool granted, @@ -533,14 +574,11 @@ class OnboardingProvider extends BaseProvider with MessageNotifierMixin implemen }; } - void _emitPairingBlocked(DevicePairingBlockedReason? reason, {BluetoothAdapterState? adapterState}) { + void _emitPairingBlocked(DevicePairingBlockedReason? reason) { if (reason == null) return; final key = '$_scanEpoch|${reason.wireName}'; if (_lastPairingBlockKey == key) return; _lastPairingBlockKey = key; - pairingFailureVersion++; - pairingFailureReason = blockedFailureReason(reason, adapterState: adapterState); - notifyListeners(); PlatformManager.instance.analytics.devicePairingBlocked( reason: reason, androidSdk: _osAndroidSdk < 0 ? 0 : _osAndroidSdk, @@ -550,7 +588,6 @@ class OnboardingProvider extends BaseProvider with MessageNotifierMixin implemen void cancelActiveScan() { _reportNativeScanBlocks = false; - pairingFailureReason = null; _scanEpoch++; _didNotMakeItTimer?.cancel(); _didNotMakeItTimer = null; @@ -595,10 +632,7 @@ class OnboardingProvider extends BaseProvider with MessageNotifierMixin implemen // Convert the values of the map back to a list List orderedDevices = foundDevicesMap.values.toList(); deviceList = orderedDevices; - if (visibleDeviceList.isNotEmpty) { - if (pairingFailureReason == DeviceConnectFailureShownFailureReason.notFoundOrDropped) { - pairingFailureReason = null; - } + if (orderedDevices.isNotEmpty || savedDeviceList.isNotEmpty) { notifyListeners(); _didNotMakeItTimer?.cancel(); } diff --git a/app/lib/services/capture/capture_controller.dart b/app/lib/services/capture/capture_controller.dart index 290ebaacc36..7554100bec4 100644 --- a/app/lib/services/capture/capture_controller.dart +++ b/app/lib/services/capture/capture_controller.dart @@ -222,6 +222,13 @@ class CaptureController extends ChangeNotifier /// The conversation-capture recorder. Resolves an injected override first so /// deterministic scenarios can drive the real pipeline through the production /// decision path, then falls back to the shared service manager. + /// Receives a copy of the phone microphone's audio (16 kHz mono PCM16) as it arrives, for a caller + /// that needs the sound itself: onboarding enrolls the reader's voice from its first dictation. + void Function(List pcm)? phoneMicAudioTap; + + /// Receives each encoded audio packet from the pendant as it arrives (for a live level meter). + void Function(List packet)? deviceAudioTap; + IMicRecorderService get _phoneMic => _phoneMicRecorderOverride ?? ServiceManager.instance().phoneMic; DateTime _now() => _nowOverride?.call() ?? DateTime.now(); @@ -480,6 +487,7 @@ class CaptureController extends ChangeNotifier await _phoneMic.start( onByteReceived: (bytes) { if (!_admitsCapture(revision)) return; + phoneMicAudioTap?.call(bytes); _recordingTelemetry.observeAudio(bytes.length); if (bytes.isNotEmpty) _livenessWatchdog.observeFrame(_now()); final frames = _activeSource?.processBytes(bytes) ?? []; @@ -981,24 +989,6 @@ class CaptureController extends ChangeNotifier /// Completes when the last Process Now request has an answer. Future? _processInFlight; - Future? _lastProcessRequest; - bool _firstCaptureProcess = false; - - /// Finish through the coordinator and return that exact Process Now response. - /// Used by onboarding to avoid attributing an unrelated list refresh to this capture. - Future finishCaptureWithResult() async { - _lastProcessRequest = null; - _firstCaptureProcess = true; - try { - await finishCapture(); - final request = _lastProcessRequest; - if (request == null) throw StateError('capture did not request live processing'); - return await request; - } finally { - _firstCaptureProcess = false; - } - } - // Force-processing requests share one optimistic row; only the newest may remove it. int _processingPlaceholderOwner = 0; @@ -1813,6 +1803,14 @@ class CaptureController extends ChangeNotifier deviceOnboardingProvider!.onVoiceQuestionFailed(); } }, + // Any other failure (transcription, network, an answer that never came) must not leave the + // tutorial on "processing your question": it offers to try again. + onFailed: () async { + await _playVoiceQuestionFailureHaptic(deviceId); + if (deviceOnboardingProvider?.isOnboardingActive == true) { + deviceOnboardingProvider!.onVoiceQuestionFailed(); + } + }, ); } @@ -2061,6 +2059,7 @@ class CaptureController extends ChangeNotifier if (_captureControllerDisposed || !_admitsCapture(revision) || _recordingDevice?.id != deviceId) return; final snapshot = List.from(value); if (snapshot.isEmpty || snapshot.length < 3) return; + deviceAudioTap?.call(snapshot); // Track bytes received from BLE _metrics.addBleBytes(snapshot.length); @@ -2796,6 +2795,7 @@ class CaptureController extends ChangeNotifier await _phoneMic.start( onByteReceived: (bytes) { if (!_admitsCapture(revision)) return; + phoneMicAudioTap?.call(bytes); _recordingTelemetry.observeAudio(bytes.length); if (bytes.isNotEmpty) _livenessWatchdog.observeFrame(_now()); // Process through AudioSource for frame splitting and sync key generation @@ -3705,7 +3705,6 @@ class CaptureController extends ChangeNotifier _clearSessionLocation(); _resetStateVariables(); - final confirmedProcessing = _firstCaptureProcess; final Future request; if (_processInProgressOutcomeOverride != null) { request = _processInProgressOutcomeOverride!(); @@ -3719,9 +3718,6 @@ class CaptureController extends ChangeNotifier } else { request = processInProgressConversationOutcome(); } - if (confirmedProcessing) { - _lastProcessRequest = request.then((outcome) => outcome.response); - } _processInFlight = request.then((_) {}, onError: (_) {}); request.then((outcome) async { final conversationId = await OptimisticProcessingPlaceholder.applyProcessResult( @@ -3739,9 +3735,6 @@ class CaptureController extends ChangeNotifier _autoSyncSessionWals(); } }).catchError((Object error, StackTrace stack) { - if (confirmedProcessing && placeholderOwner == _processingPlaceholderOwner) { - externalActions.removeProcessingConversation(OptimisticProcessingPlaceholder.id); - } Logger.debug('Process Now result handling failed: ${error.runtimeType}'); return _reportDetachedCaptureError('capture_process_now', error, stack); }); @@ -4002,6 +3995,9 @@ class CaptureController extends ChangeNotifier reason: reason, ); if (!showCopy) return; + // First-run onboarding explains its own short or empty dictation (and the pendant's audio from + // before it); a "talk for 30 seconds" banner over it would only confuse. + if (!_preferences.onboardingCompleted) return; final ctx = globalNavigatorKey.currentContext; if (ctx != null) { AppSnackbar.showSnackbar( diff --git a/app/lib/services/capture/capture_external_actions.dart b/app/lib/services/capture/capture_external_actions.dart index ac9b6cd2754..2d3806036c0 100644 --- a/app/lib/services/capture/capture_external_actions.dart +++ b/app/lib/services/capture/capture_external_actions.dart @@ -14,6 +14,7 @@ abstract interface class CaptureExternalActions { List> data, { required VoidCallback onFirstChunkRecived, required Future Function() onNoSpeech, + Future Function()? onFailed, required BleAudioCodec codec, required bool playResponseAudio, }); @@ -66,6 +67,7 @@ class NoopCaptureExternalActions implements CaptureExternalActions { List> data, { required VoidCallback onFirstChunkRecived, required Future Function() onNoSpeech, + Future Function()? onFailed, required BleAudioCodec codec, required bool playResponseAudio, }) async {} diff --git a/app/lib/services/devices/pairing_failure.dart b/app/lib/services/devices/pairing_failure.dart index e6b003ddafb..ffa806bc495 100644 --- a/app/lib/services/devices/pairing_failure.dart +++ b/app/lib/services/devices/pairing_failure.dart @@ -1,5 +1,3 @@ -import 'dart:async'; -import 'package:omi/services/devices/bluetooth_readiness.dart'; import 'package:omi/utils/analytics/registry/events.g.dart'; /// Closed reasons `OmiBleManager.startScan` reports before it scans. @@ -62,41 +60,3 @@ DeviceConnectFailedErrorClass classifyDeviceConnectError(Object error) { if (text.contains('not found') || text.contains('notfound')) return DeviceConnectFailedErrorClass.notFound; return DeviceConnectFailedErrorClass.unknown; } - -DeviceConnectFailureShownFailureReason blockedFailureReason(DevicePairingBlockedReason reason, - {BluetoothAdapterState? adapterState}) { - // Historical blocked telemetry groups unsupported/resetting/unknown with off. - // Only the actual off state warrants turn-on guidance in the new UI. - if (reason == DevicePairingBlockedReason.bluetoothOff && - adapterState != null && - adapterState != BluetoothAdapterState.off) { - return DeviceConnectFailureShownFailureReason.unknown; - } - return switch (reason) { - DevicePairingBlockedReason.bluetoothOff => DeviceConnectFailureShownFailureReason.bluetoothOff, - DevicePairingBlockedReason.bluetoothPermissionDenied => - DeviceConnectFailureShownFailureReason.bluetoothPermissionDenied, - DevicePairingBlockedReason.nearbyDevicesDenied => DeviceConnectFailureShownFailureReason.nearbyDevicesDenied, - DevicePairingBlockedReason.locationPermissionMissing => - DeviceConnectFailureShownFailureReason.locationPermissionMissing, - _ => DeviceConnectFailureShownFailureReason.unknown, - }; -} - -/// The service's null/incomplete connection result cannot distinguish a -/// timeout, missing device or dropped link. Keep one reachability bucket and -/// use the existing stale-bond latch for the distinct Bluetooth-settings action. -DeviceConnectFailureShownFailureReason connectFailureReason(Object error, {bool staleBond = false}) { - if (error is BluetoothAdapterUnavailableException) { - return switch (error.state) { - BluetoothAdapterState.off => DeviceConnectFailureShownFailureReason.bluetoothOff, - BluetoothAdapterState.unauthorized => DeviceConnectFailureShownFailureReason.bluetoothPermissionDenied, - _ => DeviceConnectFailureShownFailureReason.unknown, - }; - } - if (staleBond) return DeviceConnectFailureShownFailureReason.staleBond; - if (error is TimeoutException || (error is StateError && error.message == 'Device initialization did not complete')) { - return DeviceConnectFailureShownFailureReason.notFoundOrDropped; - } - return DeviceConnectFailureShownFailureReason.unknown; -} diff --git a/app/lib/services/experiments/onboarding_activation_flow.dart b/app/lib/services/experiments/onboarding_activation_flow.dart new file mode 100644 index 00000000000..1c458c63757 --- /dev/null +++ b/app/lib/services/experiments/onboarding_activation_flow.dart @@ -0,0 +1,140 @@ +import 'dart:io'; + +import 'package:flutter/foundation.dart'; + +import 'package:posthog_flutter/posthog_flutter.dart'; + +import 'package:omi/backend/preferences.dart'; +import 'package:omi/env/env.dart'; +import 'package:omi/env/environment_profile.dart'; +import 'package:omi/utils/analytics/analytics_manager.dart'; + +/// How first-run onboarding ends, after "Connect your Omi". +enum OnboardingEnding { + /// Control: the guided voice introduction, then the flag-gated "Setting up your Omi" page and + /// the "You're all set" screen, exactly as before the activation flow existed. + voiceIntro('control'), + + /// Treatment: the activation flow (Omi at work, a first real conversation, its summary and + /// to-dos, the paywall and the rating ask). + activation('activation'); + + const OnboardingEnding(this.wireName); + + /// The value persisted per account and accepted by the debug override. + final String wireName; + + static OnboardingEnding? fromWire(String value) { + for (final ending in values) { + if (ending.wireName == value) return ending; + } + return null; + } +} + +/// A/B switch between the old onboarding ending (control) and the activation flow (treatment). +/// +/// Evaluated on the client from the PostHog flag. Works with a plain boolean flag (on = treatment) +/// and with a PostHog experiment's multivariate flag (`test`, `treatment` or `activation` = +/// treatment; `control` and any other variant = control). The variant is read explicitly because +/// `isFeatureEnabled` is also true for the `control` variant. +/// +/// Fail-closed: no analytics identity, a slow read, an unknown variant or any error all mean +/// control, so onboarding ends exactly as it did before. +/// +/// The arm is remembered per account once the reader reaches the ending, so a reader who quits +/// mid-way and comes back finishes the same ending even if the flag read fails the second time. +abstract final class OnboardingActivationExperiment { + /// PostHog flag key (project 302298). Registered in `config/feature-flags.yaml`. + static const enabledFlag = 'onboarding-activation-flow'; + + /// Pre-registration: `app/lib/services/experiments/onboarding_activation_flow.md`. + static const prereg = 'app/lib/services/experiments/onboarding_activation_flow.md'; + + /// Variants of a multivariate flag that mean treatment. + static const treatmentVariants = {'test', 'treatment', 'activation'}; + + /// Local override: `--dart-define=OMI_ONBOARDING_ENDING=activation` or `=control`, honoured in debug + /// builds and in release builds of the local_dev profile (phone tests against a local server). A + /// dart-define cannot be flipped on a built artifact, and store builds are neither debug nor + /// local_dev, so this never reaches a user. + static const _debugOverride = String.fromEnvironment('OMI_ONBOARDING_ENDING'); + + static OnboardingEnding? get debugOverride => + kDebugMode || Env.profile == AppEnvironmentProfile.localDev ? OnboardingEnding.fromWire(_debugOverride) : null; + + /// Debug builds (people trying the branch) show the activation unless told otherwise; store + /// builds follow the flag. Widget tests keep the flag behaviour. + static OnboardingEnding? get debugDefault => + kDebugMode && !Platform.environment.containsKey('FLUTTER_TEST') ? OnboardingEnding.activation : null; + + static const Duration _variantTimeout = Duration(seconds: 2); + + /// The PostHog variant behind the last [resolve] (`test`, `control`, …), or null when the arm did + /// not come from the flag (override, debug default, no identity, timeout, error). The app turns + /// off `$feature_flag_called`, so this rides on the exposure event as `$feature/` for the + /// PostHog experiment to attribute it; fallback readers carry none and stay out of the analysis. + static String? lastFlagVariant; + + /// Exposure properties for the PostHog experiment: `$feature/` when the arm came from it. + static Map exposureProperties() => + lastFlagVariant == null ? const {} : {'\$feature/$enabledFlag': lastFlagVariant!}; + + /// The arm for a flag value: `true` or a treatment variant is treatment, everything else + /// (false, null, `control`, an unknown variant, a payload-only value) is control. + static OnboardingEnding endingForFlagValue(Object? value) { + if (value == true) return OnboardingEnding.activation; + if (value is String && treatmentVariants.contains(value.trim().toLowerCase())) { + return OnboardingEnding.activation; + } + return OnboardingEnding.voiceIntro; + } + + /// Which ending to show. [readEnabled] defaults to the analytics manager's identity-checked + /// flag read; [readValue] to the PostHog variant read. Both are injectable for tests. + static Future resolve({ + Future Function(String key)? readEnabled, + Future Function(String key)? readValue, + }) async { + lastFlagVariant = null; + final override = debugOverride ?? debugDefault; + if (override != null) return override; + try { + // The manager's read is the gate: it is false without a settled identity, with tracking + // off, on unsupported platforms and after a timeout. + final enabled = await (readEnabled ?? AnalyticsManager().isFeatureEnabled)(enabledFlag); + if (!enabled) return OnboardingEnding.voiceIntro; + final value = await (readValue ?? _readPosthogValue)(enabledFlag).timeout(_variantTimeout); + if (value is String && value.trim().isNotEmpty) { + lastFlagVariant = value.trim(); + } else if (value == true) { + lastFlagVariant = 'true'; + } + return endingForFlagValue(value); + } catch (_) { + return OnboardingEnding.voiceIntro; + } + } + + static Future _readPosthogValue(String key) => Posthog().getFeatureFlag(key); + + // ---- Per-account memory ------------------------------------------------------------------- + + static String _key(String uid) => 'onboarding/ending/$uid'; + + /// The ending this account already started, if any. + static OnboardingEnding? remembered(String uid) { + if (uid.isEmpty) return null; + return OnboardingEnding.fromWire(SharedPreferencesUtil().getString(_key(uid))); + } + + static void remember(String uid, OnboardingEnding ending) { + if (uid.isEmpty) return; + SharedPreferencesUtil().saveString(_key(uid), ending.wireName); + } + + static void forget(String uid) { + if (uid.isEmpty) return; + SharedPreferencesUtil().remove(_key(uid)); + } +} diff --git a/app/lib/services/experiments/onboarding_activation_flow.md b/app/lib/services/experiments/onboarding_activation_flow.md new file mode 100644 index 00000000000..0980d227b9c --- /dev/null +++ b/app/lib/services/experiments/onboarding_activation_flow.md @@ -0,0 +1,34 @@ +# Onboarding ending: activation flow vs voice introduction + +**Status:** pre-registered, not yet started. **Flag:** `onboarding-activation-flow` (PostHog, +mobile, client-evaluated). **Gate:** `onboarding_activation_flow.dart`. + +## Hypothesis + +New mobile users who end onboarding with the activation flow (Omi at work, a first real +conversation, its summary and to-dos, then the paywall) subscribe within 30 days more often than +users who end with the guided voice introduction. + +## Unit, assignment, split + +- **Unit:** the PostHog distinct id after sign-in (the Firebase uid). Assignment is PostHog's. +- **Arms:** control = voice introduction, then the flag-gated "Setting up your Omi" page and + "You're all set". Treatment = activation flow. Every step before (sign-in, consent, name, + permissions, language, Connect Omi) is the same in both arms. +- **Flag shape:** a boolean flag (on = treatment) or a multivariate experiment flag with + `control` and `test`. `treatment` and `activation` are also read as treatment; any other value, + no identity, a timeout or an error is control. +- **Exposure:** the flag is read when the reader reaches Connect Omi, and the arm is fixed for the + account when they leave it, so a reader who resumes later finishes the same ending. + +## Metrics + +- **Primary:** subscription started within 30 days of `Onboarding Completed`. +- **Guardrails:** onboarding completion rate from Connect Omi; voice-profile coverage at day 7 + (the activation enrolls only from phone dictation); paywall views during onboarding. +- **Secondary:** opened 3+ conversations within 3 days of first connecting an Omi. + +## Decision rule + +Ship treatment if the primary metric improves and onboarding completion does not drop by more +than 2 points. Changing the primary metric after enrollment starts needs a new flag key. diff --git a/app/lib/services/experiments/paywall_experiment.dart b/app/lib/services/experiments/paywall_experiment.dart index 87a7e94e8fb..480ef7ebeb9 100644 --- a/app/lib/services/experiments/paywall_experiment.dart +++ b/app/lib/services/experiments/paywall_experiment.dart @@ -42,8 +42,9 @@ class PaywallExperimentConfig { /// Only for [PaywallVariant.web]: the https page to render. final Uri? webUrl; - /// Whether this arm also shows its paywall once at the end of first-run onboarding (a placement - /// the control arm does not have). + /// Whether the end-of-first-run-onboarding placement is on for this arm. A variant shows its + /// paywall there; control shows nothing but is exposed at the same moment, so both arms enter the + /// experiment at the same point. Set it to the same value on every arm. final bool onboardingPaywall; bool get isControl => variant == PaywallVariant.control; @@ -71,7 +72,7 @@ class PaywallExperimentConfig { /// /// Payload per variant (JSON object): /// `{"url": "https://…", "onboarding_paywall": true}` — `url` is required for `web` and must be -/// https; `onboarding_paywall` is optional and defaults to false. +/// https; `onboarding_paywall` is optional, defaults to false, and is read for control too. abstract final class PaywallExperiment { /// PostHog flag key. Registered in `config/feature-flags.yaml`. static const enabledFlag = 'paywall-variant'; @@ -92,7 +93,7 @@ abstract final class PaywallExperiment { /// Parses one flag read into a config. Anything unexpected collapses to control. static PaywallExperimentConfig parse({String? variant, Object? payload}) { final arm = PaywallVariant.fromWire(variant); - if (arm == null || arm == PaywallVariant.control) return PaywallExperimentConfig.control; + if (arm == null) return PaywallExperimentConfig.control; Object? value = payload; if (value is String) { @@ -106,6 +107,10 @@ abstract final class PaywallExperiment { final onboardingPaywall = map['onboarding_paywall'] == true; switch (arm) { + case PaywallVariant.control: + return onboardingPaywall + ? const PaywallExperimentConfig(variant: PaywallVariant.control, onboardingPaywall: true) + : PaywallExperimentConfig.control; case PaywallVariant.outline: return PaywallExperimentConfig(variant: arm, onboardingPaywall: onboardingPaywall); case PaywallVariant.web: @@ -113,8 +118,6 @@ abstract final class PaywallExperiment { final url = raw is String ? Uri.tryParse(raw.trim()) : null; if (url == null || url.scheme != 'https' || url.host.isEmpty) return PaywallExperimentConfig.control; return PaywallExperimentConfig(variant: arm, webUrl: url, onboardingPaywall: onboardingPaywall); - case PaywallVariant.control: - return PaywallExperimentConfig.control; } } @@ -142,9 +145,7 @@ abstract final class PaywallExperiment { try { final variant = await (readVariant ?? AnalyticsManager().getFeatureFlagVariant)(enabledFlag); if (variant == null) return PaywallExperimentConfig.control; // unresolved: try again next time - final payload = variant == PaywallVariant.control.wireName - ? null - : await (readPayload ?? AnalyticsManager().getFeatureFlagPayload)(enabledFlag); + final payload = await (readPayload ?? AnalyticsManager().getFeatureFlagPayload)(enabledFlag); if (whoAmI() != who) return PaywallExperimentConfig.control; // account switched mid-read final config = parse(variant: variant, payload: payload); _cached = config; diff --git a/app/lib/services/experiments/paywall_reask.dart b/app/lib/services/experiments/paywall_reask.dart new file mode 100644 index 00000000000..595d10d40ce --- /dev/null +++ b/app/lib/services/experiments/paywall_reask.dart @@ -0,0 +1,95 @@ +import 'package:flutter/foundation.dart'; + +import 'package:omi/backend/preferences.dart'; +import 'package:omi/utils/analytics/analytics_manager.dart'; + +/// Re-offers the plans to free users when they open the app (A/B, flag `paywall-reask`). +/// +/// Readers who don't pay keep opening Omi but rarely meet a paywall, because every other paywall +/// waits for a limit. The re-ask comes back on app open, spaced out so it never nags: 2, 3, 5 +/// and then every 7 days, counted from the last paywall the reader saw anywhere (so a limit or a +/// Settings upgrade restarts the clock) and, for someone who never saw one, from their first Home +/// visit (never on onboarding day). +/// +/// Each due moment is an exposure for both arms (control is the denominator); control advances the +/// same schedule without showing anything, so both arms are counted at the same moments. +/// Fail-closed: no identity, an unread flag or any error shows nothing and exposes no one. +/// Pre-registration: `app/lib/services/experiments/paywall_reask.md`. +abstract final class PaywallReask { + /// PostHog flag key (project 302298). Registered in `config/feature-flags.yaml`. + static const enabledFlag = 'paywall-reask'; + + /// [PromptQueue] id for the queued re-ask. + static const promptId = 'paywall-reask'; + + /// Days between re-asks; the last gap repeats. + static const gapsInDays = [2, 3, 5, 7]; + + /// Variants of the flag that show the re-ask; `control` and anything else show nothing. + static const treatmentVariants = {'test', 'treatment', 'reask'}; + + /// Debug override: `--dart-define=OMI_PAYWALL_REASK=test` (or `=control`). With + /// `OMI_PAYWALL_REASK_NOW=true` the re-ask is due on every launch, for trying it on a simulator. + static const _debugVariant = String.fromEnvironment('OMI_PAYWALL_REASK'); + static const _debugAlwaysDue = bool.fromEnvironment('OMI_PAYWALL_REASK_NOW'); + + static String _lastShownKey(String uid) => 'paywall/lastShownAt/$uid'; + static String _countKey(String uid) => 'paywall/reaskCount/$uid'; + + /// The gap before the next re-ask after [reasksSoFar] of them. + static int gapAfter(int reasksSoFar) => gapsInDays[reasksSoFar.clamp(0, gapsInDays.length - 1)]; + + /// Whole local calendar days from [from] to [to]. + static int daysBetween(DateTime from, DateTime to) => + (DateTime(to.year, to.month, to.day).difference(DateTime(from.year, from.month, from.day)).inHours / 24).round(); + + /// True when a re-ask is due: at least the scheduled gap since the last paywall. + static bool isDue({required DateTime now, required DateTime lastShown, required int reasksSoFar}) => + daysBetween(lastShown, now) >= gapAfter(reasksSoFar); + + /// A paywall was shown (any source): restarts the clock. + static void noteShown({DateTime? now}) { + final uid = SharedPreferencesUtil().uid; + if (uid.isEmpty) return; + SharedPreferencesUtil().saveInt(_lastShownKey(uid), (now ?? DateTime.now()).millisecondsSinceEpoch); + } + + /// Called on Home open for a free reader. Returns true when the treatment arm should show the + /// re-ask now; records the exposure and advances the schedule for both arms. [readVariant] and + /// [recordExposure] default to the analytics manager and are injectable for tests. + static Future checkDue({ + DateTime? now, + Future Function(String key)? readVariant, + bool Function(String key, String variant)? recordExposure, + }) async { + final prefs = SharedPreferencesUtil(); + final uid = prefs.uid; + if (uid.isEmpty) return false; + final at = now ?? DateTime.now(); + final lastMs = prefs.getInt(_lastShownKey(uid), defaultValue: 0); + if (lastMs == 0) { + // First Home visit without any paywall yet: start the clock, never ask today. + noteShown(now: at); + if (!(kDebugMode && _debugAlwaysDue)) return false; + } + final count = prefs.getInt(_countKey(uid), defaultValue: 0); + final last = DateTime.fromMillisecondsSinceEpoch(lastMs == 0 ? at.millisecondsSinceEpoch : lastMs); + if (!(kDebugMode && _debugAlwaysDue) && !isDue(now: at, lastShown: last, reasksSoFar: count)) return false; + + final String? variant; + if (kDebugMode && _debugVariant.isNotEmpty) { + variant = _debugVariant; + } else { + try { + variant = await (readVariant ?? AnalyticsManager().getFeatureFlagVariant)(enabledFlag); + } catch (_) { + return false; + } + } + if (variant == null || variant.trim().isEmpty) return false; // not in the experiment + (recordExposure ?? AnalyticsManager().recordFeatureFlagExposure)(enabledFlag, variant.trim()); + prefs.saveInt(_countKey(uid), count + 1); + noteShown(now: at); + return treatmentVariants.contains(variant.trim().toLowerCase()); + } +} diff --git a/app/lib/services/experiments/paywall_reask.md b/app/lib/services/experiments/paywall_reask.md new file mode 100644 index 00000000000..55afe4980b6 --- /dev/null +++ b/app/lib/services/experiments/paywall_reask.md @@ -0,0 +1,36 @@ +# Pre-registration: paywall re-ask on app open (`paywall-reask`) + +**Question.** Free readers keep opening Omi but rarely meet a paywall, because every paywall +today waits for a limit, a locked item or a Settings tap. Does offering the plans again on app +open, spaced out so it doesn't nag, raise free→paid conversion without driving readers away? + +**Arms** (PostHog flag `paywall-reask`, experiment with variants `control` / `test`, 50/50, +user-based): +- `control` — nothing new. +- `test` — on app open (launch or back from the background), the paywall the reader would get + anywhere else (`openUpgradePaywall`, so `paywall-variant` still picks its design), queued in + `PromptQueue` (never over a recording, a call or another prompt). + +**Schedule** (`PaywallReask`). Due when 2, 3, 5 and then every 7 local calendar days have +passed since the last paywall the reader saw from any source; a reader who never saw one starts +the clock at their first Home visit, so never on onboarding day. At most one per day. Free plan +only (`basic`, subscription UI shown); dropped if the reader subscribed before it shows. + +**Unit.** A free reader with a due moment. Exposure = `$feature_flag_called` with +`$feature_flag = paywall-reask`, once per reader and arm, at the first due moment; control +advances the same schedule without showing anything, so both arms are counted alike. + +**Primary metric.** `Upgrade Succeeded` per exposed reader within 14 days of exposure. + +**Secondary / guardrails.** `Paywall Opened` (source `app_open_reask`) → `Upgrade Succeeded`; +`Upgrade Modal Dismissed` per reader; days with an app open in the 14 days after exposure (must +not drop more than 5% relative against control); `Product Error` per reader. + +**Decision rule.** Ship to 100% if the primary metric improves at PostHog's default confidence +and the active-days guardrail holds; stop early if active days drop more than 10% relative. +Run until each arm has at least 1,000 exposed readers or 21 days, whichever is later. Tuning the +gaps is a follow-up experiment, not a change mid-run. + +**Kill switch.** Set the split to control 100% (or deactivate the flag); no release needed. + +**Owner.** yinka. Created 2026-10-14. Review by 2026-11-13. diff --git a/app/lib/services/superwall_pilot.dart b/app/lib/services/superwall_pilot.dart new file mode 100644 index 00000000000..e1f667ce3c2 --- /dev/null +++ b/app/lib/services/superwall_pilot.dart @@ -0,0 +1,150 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:flutter/foundation.dart'; + +import 'package:firebase_auth/firebase_auth.dart'; + +import 'package:omi/utils/logger.dart'; +import 'package:superwallkit_flutter/superwallkit_flutter.dart'; + +/// Local development pilot. Production billing still uses Omi's existing flow. +/// +/// Public SDK keys are supplied at build time, never committed to the repo. +class SuperwallPilot { + SuperwallPilot._(); + + static final SuperwallPilot instance = SuperwallPilot._(); + + /// Off unless a debug build passes `--dart-define=SUPERWALL_TEST_MODE=true`; otherwise every + /// upgrade entry point goes through the paywall experiment (`PaywallRouter`). + static const _testEnabled = bool.fromEnvironment('SUPERWALL_TEST_MODE'); + + /// The pilot's own Superwall test project (public SDK key, meant to ship inside apps). Production + /// needs Omi's Superwall project and its App Store Connect in-app purchase products instead. + static const _iosKey = String.fromEnvironment('SUPERWALL_API_KEY_IOS', defaultValue: 'pk_EA03aegLVEAIuDfWT5zTB'); + + bool _configured = false; + String? _identifiedUid; + Future _identityQueue = Future.value(); + + bool get isEnabled => _configured; + + /// The end of onboarding just offered a plan; the device tutorial that follows on Home skips its + /// own offer so nobody sees two paywalls in a row. + bool offeredDuringOnboarding = false; + + void configure() { + if (_configured || !kDebugMode || !_testEnabled || !Platform.isIOS || _iosKey.isEmpty) { + return; + } + + try { + Superwall.configure(_iosKey); + _configured = true; + } catch (error) { + Logger.debug('Superwall pilot configuration failed: $error'); + } + } + + /// Serialize account switches so an earlier identify cannot finish last. + Future syncUser(String? uid) { + if (!_configured) return Future.value(); + _identityQueue = _identityQueue.then((_) async { + if (_identifiedUid == uid) return; + try { + if (_identifiedUid != null) { + await Superwall.shared.reset(); + _identifiedUid = null; + } + if (uid != null) { + await Superwall.shared.identify(uid); + _identifiedUid = uid; + } + } catch (error) { + Logger.debug('Superwall pilot identity update failed: $error'); + } + }); + return _identityQueue; + } + + /// Upgrade on the Plan & Usage page, and that page's automatic offer when opened from Transcription + /// Settings → View usage. + static const usageUpgradePlacement = 'usage_upgrade'; + static const transcriptionSettingsPlacement = 'transcription_settings_usage'; + + /// Free chat limit reached (the sheet opens automatically over chat). + static const chatQuotaPlacement = 'chat_quota'; + + /// Chat Apps Pro gate (Telegram / iMessage rows and the Pro card). + static const chatAppsPlacement = 'chat_apps'; + + /// Plus transcription meter crossing its threshold (queued until recording stops). + static const plusMeterPlacement = 'plus_meter'; + + /// Placement for the Upgrade button on the Settings plan card. + static const settingsUpgradePlacement = 'settings_upgrade'; + + /// Placement for tapping a conversation or memory locked after the free minutes run out. + static const lockedContentPlacement = 'locked_content'; + + /// Placement shown to anyone not subscribed when the device tutorial closes (finished, skipped or + /// replayed). Superwall only: no plans-sheet fallback, so nothing changes when the pilot is off. + static const deviceSetupCompletePlacement = 'device_setup_complete'; + + /// The paywall re-ask on app open for free users (`PaywallReask`, flag `paywall-reask`). + static const appOpenReaskPlacement = 'app_open_reask'; + + /// Shows [placement] for the current user and completes once the paywall closes. Returns false + /// when nothing was shown (pilot off, no paywall for this user, or an error), so the caller can + /// fall back to the plans sheet. + Future showPlacementAndWait(String placement) async { + if (!_configured) return false; + final user = FirebaseAuth.instance.currentUser; + if (user == null || user.isAnonymous) return false; + await syncUser(user.uid); + if (_identifiedUid != user.uid) return false; + final closed = Completer(); + final handler = PaywallPresentationHandler() + ..onDismiss((_, __) { + if (!closed.isCompleted) closed.complete(true); + }) + ..onSkip((_) { + if (!closed.isCompleted) closed.complete(false); + }) + ..onError((_) { + if (!closed.isCompleted) closed.complete(false); + }); + try { + await Superwall.shared.registerPlacement(placement, handler: handler); + } catch (error) { + Logger.debug('Superwall pilot placement failed: $error'); + return false; + } + // A paywall whose close is never reported must not hold onboarding: treat it as closed. + return closed.future.timeout(const Duration(minutes: 5), onTimeout: () => true); + } + + /// [showPlacement] for the signed-in, non-anonymous Firebase user. + Future showPlacementForCurrentUser(String placement) async { + if (!_configured) return false; + final user = FirebaseAuth.instance.currentUser; + if (user == null || user.isAnonymous) return false; + return showPlacement(placement, user.uid); + } + + /// Registers [placement] for [uid]. Returns false when the pilot is off or registration failed, + /// so the caller can fall back to the existing plans sheet. + Future showPlacement(String placement, String uid) async { + if (!_configured) return false; + await syncUser(uid); + if (_identifiedUid != uid) return false; + try { + await Superwall.shared.registerPlacement(placement); + return true; + } catch (error) { + Logger.debug('Superwall pilot placement failed: $error'); + return false; + } + } +} diff --git a/app/lib/ui/components/omi_page_states.dart b/app/lib/ui/components/omi_page_states.dart index 296fe6c01c2..5a1b13e4af2 100644 --- a/app/lib/ui/components/omi_page_states.dart +++ b/app/lib/ui/components/omi_page_states.dart @@ -39,8 +39,9 @@ class OmiLoadingState extends StatelessWidget { /// when the action is how the page gets its first row ("New Memory"). /// /// The glyph is either a Material [icon] or any [glyph] widget — for example -/// `FaIcon(FontAwesomeIcons.key)` when the screen's other controls use FontAwesome. Exactly one of -/// the two is given; either way it is drawn 40pt in [OmiColors.textTertiary]. +/// `FaIcon(FontAwesomeIcons.key)` when the screen's other controls use FontAwesome. At most one of +/// the two is given; either way it is drawn 40pt in [OmiColors.textTertiary]. With neither, the +/// state is text only (Home and Tasks before anything is in them). class OmiEmptyState extends StatelessWidget { const OmiEmptyState({ super.key, @@ -51,7 +52,7 @@ class OmiEmptyState extends StatelessWidget { this.message, this.messageLayoutReference, this.action, - }) : assert((icon == null) != (glyph == null), 'Give OmiEmptyState exactly one of icon or glyph'); + }) : assert(icon == null || glyph == null, 'Give OmiEmptyState at most one of icon or glyph'); final IconData? icon; @@ -73,7 +74,7 @@ class OmiEmptyState extends StatelessWidget { @override Widget build(BuildContext context) { return _StateLayout( - glyph: glyph ?? Icon(icon), + glyph: glyph ?? (icon == null ? null : Icon(icon)), title: title, titleLayoutReference: titleLayoutReference, message: message, @@ -117,14 +118,9 @@ class OmiErrorState extends StatelessWidget { class _StateLayout extends StatelessWidget { const _StateLayout( - {required this.glyph, - this.title, - this.titleLayoutReference, - this.message, - this.messageLayoutReference, - this.action}); - - final Widget glyph; + {this.glyph, this.title, this.titleLayoutReference, this.message, this.messageLayoutReference, this.action}); + + final Widget? glyph; final String? title; final String? titleLayoutReference; final String? message; @@ -140,14 +136,15 @@ class _StateLayout extends StatelessWidget { child: Column( mainAxisSize: MainAxisSize.min, children: [ - ExcludeSemantics( - child: IconTheme.merge( - data: IconThemeData(size: 40, color: OmiColors.textTertiary), - child: glyph, + if (glyph != null) + ExcludeSemantics( + child: IconTheme.merge( + data: IconThemeData(size: 40, color: OmiColors.textTertiary), + child: glyph!, + ), ), - ), if (title != null) ...[ - const SizedBox(height: OmiSpacing.md), + if (glyph != null) const SizedBox(height: OmiSpacing.md), Semantics( header: true, child: titleLayoutReference == null diff --git a/app/lib/ui/components/omi_refresh_control.dart b/app/lib/ui/components/omi_refresh_control.dart new file mode 100644 index 00000000000..95a064c4930 --- /dev/null +++ b/app/lib/ui/components/omi_refresh_control.dart @@ -0,0 +1,124 @@ +import 'dart:io'; +import 'dart:math' as math; + +import 'package:flutter/cupertino.dart'; + +import 'package:omi/ui/components/omi_spinner.dart'; +import 'package:omi/ui/omi_tokens.dart'; + +/// Pull-to-refresh for a [CustomScrollView] on iOS: a thin ring draws itself as the list is pulled, +/// ticks once when letting go would refresh, then becomes the [OmiSpinner] while it loads. +/// +/// Put it first in the slivers and keep the stock `RefreshIndicator` for Android with +/// [OmiRefreshControl.materialPredicate], which turns the Material one off where this one shows: +/// +/// ```dart +/// RefreshIndicator( +/// notificationPredicate: OmiRefreshControl.materialPredicate, +/// onRefresh: refresh, +/// child: CustomScrollView(slivers: [if (OmiRefreshControl.supported) OmiRefreshControl(onRefresh: refresh), ...]), +/// ) +/// ``` +class OmiRefreshControl extends StatelessWidget { + const OmiRefreshControl({super.key, required this.onRefresh}); + + final Future Function() onRefresh; + + /// iOS only: the control needs the bouncing overscroll iOS lists have. + static bool get supported => !_forceMaterial && Platform.isIOS; + + /// Tests pin the Material path. + static bool _forceMaterial = false; + @visibleForTesting + static set debugForceMaterial(bool value) => _forceMaterial = value; + + /// For the Material `RefreshIndicator` that wraps the same list: off wherever this control shows. + static bool materialPredicate(ScrollNotification notification) => !supported && notification.depth == 0; + + static const double _trigger = 88; + static const double _extent = 56; + + @override + Widget build(BuildContext context) { + return CupertinoSliverRefreshControl( + refreshTriggerPullDistance: _trigger, + refreshIndicatorExtent: _extent, + onRefresh: onRefresh, + builder: (context, mode, pulled, trigger, extent) => + _PullIndicator(mode: mode, progress: (pulled / trigger).clamp(0.0, 1.0)), + ); + } +} + +class _PullIndicator extends StatefulWidget { + const _PullIndicator({required this.mode, required this.progress}); + + final RefreshIndicatorMode mode; + final double progress; + + @override + State<_PullIndicator> createState() => _PullIndicatorState(); +} + +class _PullIndicatorState extends State<_PullIndicator> { + @override + void didUpdateWidget(covariant _PullIndicator old) { + super.didUpdateWidget(old); + // One tick at the moment letting go would refresh. + if (widget.mode == RefreshIndicatorMode.armed && old.mode == RefreshIndicatorMode.drag) OmiHaptics.selection(); + } + + @override + Widget build(BuildContext context) { + final loading = widget.mode == RefreshIndicatorMode.armed || widget.mode == RefreshIndicatorMode.refresh; + final done = widget.mode == RefreshIndicatorMode.done || widget.mode == RefreshIndicatorMode.inactive; + final p = widget.progress; + return Center( + child: AnimatedOpacity( + opacity: done ? 0 : (p * 1.8).clamp(0.0, 1.0), + duration: const Duration(milliseconds: 160), + child: AnimatedSwitcher( + duration: const Duration(milliseconds: 180), + child: loading + ? const OmiSpinner(key: ValueKey('spin'), size: OmiSpinnerSize.small) + : SizedBox( + key: const ValueKey('ring'), + width: 22, + height: 22, + child: CustomPaint(painter: _PullRing(progress: p, color: OmiColors.textPrimary)), + ), + ), + ), + ); + } +} + +/// A ring that draws clockwise from the top as the list is pulled, and a dot that grows at its +/// centre: full ring + dot = let go to refresh. +class _PullRing extends CustomPainter { + _PullRing({required this.progress, required this.color}); + + final double progress; + final Color color; + + @override + void paint(Canvas canvas, Size size) { + final c = size.center(Offset.zero); + final r = size.shortestSide / 2 - 1; + final track = Paint() + ..color = color.withValues(alpha: 0.12) + ..style = PaintingStyle.stroke + ..strokeWidth = 2; + canvas.drawCircle(c, r, track); + final arc = Paint() + ..color = color + ..style = PaintingStyle.stroke + ..strokeCap = StrokeCap.round + ..strokeWidth = 2; + canvas.drawArc(Rect.fromCircle(center: c, radius: r), -math.pi / 2, 2 * math.pi * progress, false, arc); + canvas.drawCircle(c, 3.5 * Curves.easeOutBack.transform(progress), Paint()..color = color); + } + + @override + bool shouldRepaint(covariant _PullRing old) => old.progress != progress || old.color != color; +} diff --git a/app/lib/ui/feedback/omi_feedback.dart b/app/lib/ui/feedback/omi_feedback.dart index e657e2b2bb8..905188c051a 100644 --- a/app/lib/ui/feedback/omi_feedback.dart +++ b/app/lib/ui/feedback/omi_feedback.dart @@ -47,29 +47,14 @@ abstract final class OmiFeedback { /// bar, and the chat bar on Home). The shell sets this in `initState` and clears it in `dispose`; /// it is ignored whenever another route is on top. static double Function(BuildContext context)? bottomClearance; - static int _infoOwners = 0; - - /// A guided surface renders explanatory notices itself. The release is - /// idempotent so disposing that surface always restores normal info feedback. - static VoidCallback ownInfoFeedback() { - _infoOwners++; - var released = false; - return () { - if (released) return; - released = true; - _infoOwners--; - }; - } /// Confirms something the reader just did. 1.5 s. static void confirm(BuildContext context, String message) => _show(context, message, kind: OmiFeedbackKind.confirm, duration: OmiFeedbackTiming.confirm); /// Tells the reader something they did not directly cause. 4 s. - static void info(BuildContext context, String message) { - if (_infoOwners > 0) return; - _show(context, message, kind: OmiFeedbackKind.info, duration: OmiFeedbackTiming.info); - } + static void info(BuildContext context, String message) => + _show(context, message, kind: OmiFeedbackKind.info, duration: OmiFeedbackTiming.info); /// Reports a failure. Stays 8 s with a close button, or until [onAction] ("Try Again"). static void error(BuildContext context, String message, {String? actionLabel, VoidCallback? onAction}) => _show( diff --git a/app/lib/ui/omi_routes.dart b/app/lib/ui/omi_routes.dart index a5a33bbb788..afd6305f37b 100644 --- a/app/lib/ui/omi_routes.dart +++ b/app/lib/ui/omi_routes.dart @@ -40,3 +40,18 @@ Route omiPageRoute({ ); } } + +/// Replaces the whole navigator stack with [builder]'s page by fading it in: for hand-offs that +/// leave nothing to go back to (onboarding done → Home). A slide there shows the incoming page +/// moving over whatever the old root is redrawing underneath (often the same page), which reads +/// as a double. Use with `pushAndRemoveUntil(..., (_) => false)`; never for an ordinary push. +Route omiRootRoute({required WidgetBuilder builder, RouteSettings? settings}) { + return PageRouteBuilder( + settings: settings, + transitionDuration: const Duration(milliseconds: 260), + reverseTransitionDuration: const Duration(milliseconds: 200), + pageBuilder: (context, animation, secondaryAnimation) => builder(context), + transitionsBuilder: (context, animation, secondaryAnimation, child) => + FadeTransition(opacity: CurvedAnimation(parent: animation, curve: Curves.easeOut), child: child), + ); +} diff --git a/app/lib/ui/ui.dart b/app/lib/ui/ui.dart index c4d10b330c8..cc738767ee2 100644 --- a/app/lib/ui/ui.dart +++ b/app/lib/ui/ui.dart @@ -40,6 +40,7 @@ export 'components/omi_nav_buttons.dart'; export 'components/omi_page_states.dart'; export 'components/omi_partial_notice.dart'; export 'components/omi_permission_row.dart'; +export 'components/omi_refresh_control.dart'; export 'components/omi_row_menu.dart'; export 'components/omi_search_field.dart'; export 'components/omi_settings.dart'; diff --git a/app/lib/utils/analytics/analytics_manager.dart b/app/lib/utils/analytics/analytics_manager.dart index 446565cb3ad..69d143cf73b 100644 --- a/app/lib/utils/analytics/analytics_manager.dart +++ b/app/lib/utils/analytics/analytics_manager.dart @@ -852,6 +852,13 @@ class AnalyticsManager { void onboardingUserAcquisitionSource(String source) => track('User Acquisition Source', properties: {'source': source}); + /// How the reader left the Connect Omi step: `connected`, `ordered` or `not_ordered`. Also kept on + /// the person, so buyers who say they ordered can be matched to Shopify orders later. + void onboardingDeviceChoice(String choice) { + track('Onboarding Device Choice', properties: {'choice': choice}); + setUserProperty('Onboarding Device Choice', choice); + } + // Interactive device onboarding void deviceOnboardingStarted({String source = 'auto'}) => track('Device Onboarding Started', properties: {'source': source}); diff --git a/app/lib/utils/analytics/registry/events.g.dart b/app/lib/utils/analytics/registry/events.g.dart index 6ed292df719..0e0caf48181 100644 --- a/app/lib/utils/analytics/registry/events.g.dart +++ b/app/lib/utils/analytics/registry/events.g.dart @@ -2389,7 +2389,8 @@ enum PaywallOpenedSource { screenContextGate("screen_context_gate"), mcpDailyCap("mcp_daily_cap"), budget80("budget_80"), - onboarding("onboarding"); + onboarding("onboarding"), + appOpenReask("app_open_reask"); const PaywallOpenedSource(this.wireName); final String wireName; } @@ -2824,7 +2825,8 @@ enum PaywallVariantShownSource { screenContextGate("screen_context_gate"), mcpDailyCap("mcp_daily_cap"), budget80("budget_80"), - onboarding("onboarding"); + onboarding("onboarding"), + appOpenReask("app_open_reask"); const PaywallVariantShownSource(this.wireName); final String wireName; } diff --git a/app/lib/widgets/freemium_switch_dialog.dart b/app/lib/widgets/freemium_switch_dialog.dart index 9da0808b932..f8d077be983 100644 --- a/app/lib/widgets/freemium_switch_dialog.dart +++ b/app/lib/widgets/freemium_switch_dialog.dart @@ -1,13 +1,12 @@ import 'package:flutter/material.dart'; -import 'package:omi/utils/platform/platform_manager.dart'; import 'package:provider/provider.dart'; -import 'package:omi/pages/paywall/paywall_router.dart'; -import 'package:omi/pages/settings/widgets/plans_sheet.dart'; +import 'package:omi/pages/settings/widgets/upgrade_paywall.dart'; import 'package:omi/providers/capture_provider.dart'; import 'package:omi/providers/usage_provider.dart'; import 'package:omi/services/capture/freemium_threshold_tracker.dart'; import 'package:omi/services/freemium_transcription_service.dart'; +import 'package:omi/services/superwall_pilot.dart'; import 'package:omi/ui/ui.dart'; /// Prompt id of the plans sheet offered when the Plus meter runs low. @@ -52,20 +51,12 @@ class FreemiumSwitchHandler { PromptPriority.low, // Still relevant when it is finally shown (the plan may have changed meanwhile). canShowNow: () => _eligible(context, captureProvider), - show: (promptContext) async { - if (await PaywallRouter.openVariant(promptContext, source: 'freemium_switch')) return; - if (!promptContext.mounted) return; - final plan = promptContext.read().subscription?.subscription.plan.wireName ?? 'unknown'; - PlatformManager.instance.analytics.paywallOpened('freemium_switch', currentPlan: plan); - final title = await plansSheetTitle(promptContext); - if (!promptContext.mounted) return; - return showOmiSheet( - context: promptContext, - title: title, - padding: EdgeInsets.zero, - builder: (_) => const _PlansSheetWrapper(), - ); - }, + show: (promptContext) => openUpgradePaywall( + promptContext, + placement: SuperwallPilot.plusMeterPlacement, + source: 'freemium_switch', + freeUser: false, + ), ); } @@ -91,49 +82,3 @@ class FreemiumSwitchHandler { _freemiumService.resetDialogShownFlag(); } } - -/// Wrapper widget to create animation controllers for PlansSheet -class _PlansSheetWrapper extends StatefulWidget { - const _PlansSheetWrapper(); - - @override - State<_PlansSheetWrapper> createState() => _PlansSheetWrapperState(); -} - -class _PlansSheetWrapperState extends State<_PlansSheetWrapper> with TickerProviderStateMixin { - late AnimationController _waveController; - late AnimationController _arrowController; - late Animation _arrowAnimation; - - @override - void initState() { - super.initState(); - _waveController = AnimationController(duration: const Duration(milliseconds: 18000), vsync: this)..repeat(); - - _arrowController = AnimationController(duration: const Duration(milliseconds: 800), vsync: this) - ..repeat(reverse: true); - - _arrowAnimation = Tween( - begin: 0, - end: 3, - ).animate(CurvedAnimation(parent: _arrowController, curve: Curves.easeInOut)); - } - - @override - void dispose() { - _waveController.dispose(); - _arrowController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return PlansSheet( - waveController: _waveController, - notesController: _waveController, - arrowController: _arrowController, - arrowAnimation: _arrowAnimation, - analyticsSource: 'freemium_switch', - ); - } -} diff --git a/app/lib/widgets/home_bottom_bar.dart b/app/lib/widgets/home_bottom_bar.dart index 8fffe843e35..f439d043baf 100644 --- a/app/lib/widgets/home_bottom_bar.dart +++ b/app/lib/widgets/home_bottom_bar.dart @@ -18,8 +18,8 @@ const double kHomeBottomFadeHeight = 20; /// Gap between the chat bar and the system inset (or the screen edge when there is none). const double kHomeChatBarBottomGap = 8; -/// Height of Home's floating chat bar. -const double kHomeChatBarHeight = 62; +/// Height of Home's floating bottom controls (Ask Omi and the record pill). +const double kHomeChatBarHeight = 56; /// Space every Home-shell page leaves below its last row so it can scroll clear of the screen edge. const double kHomeScrollEndPadding = 24; diff --git a/app/lib/widgets/scanning_ripple.dart b/app/lib/widgets/scanning_ripple.dart index 3e10282e88a..b0a471d82f4 100644 --- a/app/lib/widgets/scanning_ripple.dart +++ b/app/lib/widgets/scanning_ripple.dart @@ -4,7 +4,10 @@ class ScanningRippleWidget extends StatefulWidget { final bool isScanning; final double size; - const ScanningRippleWidget({super.key, required this.isScanning, this.size = 300}); + /// Ring colour; white by default (the dark pairing page). Light screens pass the text colour. + final Color color; + + const ScanningRippleWidget({super.key, required this.isScanning, this.size = 300, this.color = Colors.white}); @override State createState() => _ScanningRippleWidgetState(); @@ -44,7 +47,7 @@ class _ScanningRippleWidgetState extends State with Single builder: (context, child) { return CustomPaint( size: Size(widget.size, widget.size), - painter: _RipplePainter(progress: _controller.value, color: Colors.white), + painter: _RipplePainter(progress: _controller.value, color: widget.color), ); }, ), diff --git a/app/pubspec.lock b/app/pubspec.lock index 4d956e95fcb..edceff7eaff 100644 --- a/app/pubspec.lock +++ b/app/pubspec.lock @@ -1160,10 +1160,10 @@ packages: dependency: "direct main" description: name: intl - sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5" + sha256: "1ca20c894b1717686a2319b8548763d812bc0aabdac580420a44c5178c57a867" url: "https://pub.dev" source: hosted - version: "0.20.2" + version: "0.20.3" intl_country_data: dependency: "direct main" description: @@ -1321,10 +1321,10 @@ packages: dependency: transitive description: name: matcher - sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861 + sha256: "31bd099b47c10cd1aeb55146a2d46ce0277630ecef3f7dae54ad7873f36696cd" url: "https://pub.dev" source: hosted - version: "0.12.19" + version: "0.12.20" material_color_utilities: dependency: transitive description: @@ -1345,10 +1345,10 @@ packages: dependency: transitive description: name: meta - sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349" + sha256: "307249ce4ff29d58a18e97f6345f539382eb9c9c29ecda628900f31de0443dd9" url: "https://pub.dev" source: hosted - version: "1.18.0" + version: "1.19.0" mime: dependency: transitive description: @@ -2005,6 +2005,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.1" + superwallkit_flutter: + dependency: "direct main" + description: + name: superwallkit_flutter + sha256: "22c9be16b6306eca879ef32409db3852d271c09a2e999d1346323ffbe1111ece" + url: "https://pub.dev" + source: hosted + version: "2.6.0" sync_http: dependency: transitive description: @@ -2057,10 +2065,10 @@ packages: dependency: transitive description: name: test_api - sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e" + sha256: "2a122cbe059f8b610d3a5415f42e255b6c17b1f21eee1d960f31080237fb4f11" url: "https://pub.dev" source: hosted - version: "0.7.11" + version: "0.7.12" time: dependency: transitive description: @@ -2209,10 +2217,10 @@ packages: dependency: "direct main" description: name: vector_math - sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b + sha256: "92b9910f66ed1057fd4da7b040ae7c74cafacf885bdc81be496928d5049b032d" url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.4.3" version: dependency: "direct main" description: diff --git a/app/pubspec.yaml b/app/pubspec.yaml index 8a97cf1bbf7..28c7ab7dc6f 100644 --- a/app/pubspec.yaml +++ b/app/pubspec.yaml @@ -137,6 +137,7 @@ dependencies: marionette_flutter: ^0.3.0 quick_actions: ^1.0.0 in_app_review: ^2.0.12 + superwallkit_flutter: ^2.6.0 dependency_overrides: js: ^0.7.1 @@ -211,6 +212,8 @@ flutter: - assets/models/silero_vad.onnx - assets/images/stars.png - assets/images/herologo.png + - assets/images/omi_pendant_glow.png + - assets/videos/onboarding_launch_loop.mp4 - assets/fonts/ - assets/images/ - assets/test/tts_progressive_probe.mp3 diff --git a/app/test/providers/message_provider_voice_test.dart b/app/test/providers/message_provider_voice_test.dart index 736371a3817..4295f0e220a 100644 --- a/app/test/providers/message_provider_voice_test.dart +++ b/app/test/providers/message_provider_voice_test.dart @@ -127,4 +127,38 @@ void main() { await stalled.close(); provider.dispose(); }); + + // The device tutorial waits on this callback: a failed transcription, a dropped connection or a + // stream that ends with no answer must each say so, or the tutorial stays on "processing". + for (final (name, streamer) in <(String, Stream Function())>[ + ( + 'a transcription error', + () => Stream.value(ServerMessageChunk('id', 'Transcription failed', MessageChunkType.error)) + ), + ('a dropped connection', () => Stream.error(const SocketException('gone'))), + ('a stream with no answer', () => const Stream.empty()), + ]) { + test('$name reports failure, not silence', () async { + var failed = 0; + var noSpeech = 0; + final provider = MessageProvider( + voiceAudioFileSaver: (_, __, ___) async => File('/tmp/unused-voice-question.opus'), + voiceReplyStreamer: (_, {language}) => streamer(), + ); + + await provider.sendVoiceMessageStreamToServer( + [ + [1] + ], + codec: BleAudioCodec.opus, + onNoSpeech: () async => noSpeech++, + onFailed: () async => failed++, + ); + + expect(failed, 1); + expect(noSpeech, 0); + expect(provider.showTypingIndicator, isFalse); + provider.dispose(); + }); + } } diff --git a/app/test/providers/omi_button_actions_controller_test.dart b/app/test/providers/omi_button_actions_controller_test.dart index 6db7725e0a9..86dcb8eb953 100644 --- a/app/test/providers/omi_button_actions_controller_test.dart +++ b/app/test/providers/omi_button_actions_controller_test.dart @@ -72,6 +72,7 @@ class _RecordingCaptureExternalActions extends NoopCaptureExternalActions { List> data, { required void Function() onFirstChunkRecived, required Future Function() onNoSpeech, + Future Function()? onFailed, required BleAudioCodec codec, required bool playResponseAudio, }) async { diff --git a/app/test/providers/onboarding_provider_pairing_presentation_test.dart b/app/test/providers/onboarding_provider_pairing_presentation_test.dart deleted file mode 100644 index 7bb503bef81..00000000000 --- a/app/test/providers/onboarding_provider_pairing_presentation_test.dart +++ /dev/null @@ -1,183 +0,0 @@ -import 'dart:io'; -import 'dart:ui' as ui; - -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/rendering.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:omi/backend/preferences.dart'; -import 'package:omi/backend/schema/bt_device/bt_device.dart'; -import 'package:omi/gen/pigeon_communicator.g.dart'; -import 'package:omi/l10n/app_localizations_en.dart'; -import 'package:omi/pages/onboarding/find_device/pairing_failure_card.dart'; -import 'package:omi/providers/onboarding_provider.dart'; -import 'package:omi/services/devices/pairing_failure.dart'; -import 'package:omi/services/services.dart'; -import 'package:omi/utils/analytics/analytics_adapter.dart'; -import 'package:omi/utils/analytics/analytics_manager.dart'; -import 'package:omi/utils/analytics/registry/events.g.dart'; -import 'package:package_info_plus/package_info_plus.dart'; -import 'package:shared_preferences/shared_preferences.dart'; - -import '../../integration_test/visual_audit/fakes.dart' show AuditDeviceProvider; -import '../widgets/onboarding_polish_test.dart' as polish; - -typedef Reason = DeviceConnectFailureShownFailureReason; - -class DiscoveryDeviceProvider extends AuditDeviceProvider { - @override - void stopDiscoveryScanning() {} -} - -class RecordingAdapter implements AnalyticsAdapter { - final names = []; - @override - bool isInitialized = false; - @override - Future init() async => isInitialized = true; - @override - void track({required String eventName, Map? properties}) => names.add(eventName); - @override - dynamic noSuchMethod(Invocation invocation) => null; -} - -void main() { - final binding = TestWidgetsFlutterBinding.ensureInitialized(); - const bluetoothChannel = BasicMessageChannel( - 'dev.flutter.pigeon.omi_pigeon.BleHostApi.getBluetoothState', - BleHostApi.pigeonChannelCodec, - ); - const connectivity = MethodChannel('dev.fluttercommunity.plus/connectivity'); - const connectivityEvents = MethodChannel('dev.fluttercommunity.plus/connectivity_status'); - late RecordingAdapter adapter; - late OnboardingProvider provider; - - setUpAll(() async { - // No internet probes: the production service singleton sees an offline - // platform, while the device-provider seam below handles discovery inertly. - binding.defaultBinaryMessenger.setMockMethodCallHandler(connectivity, (_) async => ['none']); - binding.defaultBinaryMessenger.setMockMethodCallHandler(connectivityEvents, (_) async => null); - await ServiceManager.init(); - }); - tearDownAll(() { - binding.defaultBinaryMessenger.setMockMethodCallHandler(connectivity, null); - binding.defaultBinaryMessenger.setMockMethodCallHandler(connectivityEvents, null); - }); - setUp(() async { - SharedPreferences.setMockInitialValues({}); - await SharedPreferencesUtil.init(); - AnalyticsManager.resetForTesting(); - PackageInfo.setMockInitialValues( - appName: 'Omi Test', packageName: 'com.omi.test', version: '1', buildNumber: '1', buildSignature: ''); - adapter = RecordingAdapter(); - AnalyticsManager.configure(adapter); - await AnalyticsManager.init(); - binding.defaultBinaryMessenger.setMockDecodedMessageHandler(bluetoothChannel, (_) async => ['on']); - provider = OnboardingProvider() - ..hasBluetoothPermission = true - ..deviceProvider = DiscoveryDeviceProvider(); - }); - tearDown(() { - provider.dispose(); - AnalyticsManager.resetForTesting(); - binding.defaultBinaryMessenger.setMockDecodedMessageHandler(bluetoothChannel, null); - }); - - Future pumpConnect(WidgetTester tester, {TargetPlatform platform = TargetPlatform.iOS, Key? boundary}) async { - tester.view.physicalSize = const Size(1179, 2556); - tester.view.devicePixelRatio = 3; - addTearDown(tester.view.resetPhysicalSize); - addTearDown(tester.view.resetDevicePixelRatio); - final child = polish.host(polish.connect(provider), platform: platform); - await tester.pumpWidget(boundary == null ? child : RepaintBoundary(key: boundary, child: child)); - // FindDevicesPage's post-frame callback invokes the real scanDevices. - await tester.pump(); - await tester.pump(); - } - - testWidgets('real empty-scan timer retains No Omi Found and never shows or emits a connection failure', - (tester) async { - await pumpConnect(tester); - expect(provider.enableInstructions, isFalse); - await tester.pump(const Duration(seconds: deviceScanEmptyAfterSeconds)); - await tester.pump(); - expect(provider.enableInstructions, isTrue); - expect(provider.pairingFailureReason, isNull); - final l10n = AppLocalizationsEn(); - expect(find.text(l10n.findDeviceNoneTitle), findsOneWidget); - expect(find.text(l10n.findDeviceNoneMessage), findsOneWidget); - expect(find.byKey(const Key('find_devices_contact_support')), findsOneWidget); - expect(find.byKey(const Key('find_devices_scan_again')), findsOneWidget); - expect(find.byType(PairingFailureCard), findsNothing); - await AnalyticsManager.flushPending(force: true); - expect(adapter.names, contains('Device Scan Empty')); - expect(adapter.names, isNot(contains('Device Connect Failure Shown'))); - provider.onDevices([BtDevice(id: 'omi-appeared', name: 'Omi Appeared', type: DeviceType.omi, rssi: -50)]); - await tester.pump(); - expect(find.text(l10n.findDeviceNoneTitle), findsNothing); - expect(find.text('Omi Appeared'), findsOneWidget); - await tester.pumpWidget(const SizedBox()); - }); - - testWidgets('onDevices removes reachability card when a discovered device becomes visible', (tester) async { - await pumpConnect(tester); - provider.pairingFailureReason = Reason.notFoundOrDropped; - provider.notifyListeners(); - await tester.pump(); - await AnalyticsManager.flushPending(force: true); - expect(find.byType(PairingFailureCard), findsOneWidget); - expect(adapter.names.where((name) => name == 'Device Connect Failure Shown'), hasLength(1)); - provider.onDevices([BtDevice(id: 'omi-late', name: 'Late Omi', type: DeviceType.omi, rssi: -50)]); - await tester.pump(); - expect(provider.pairingFailureReason, isNull); - expect(find.byType(PairingFailureCard), findsNothing); - expect(find.text('Late Omi'), findsOneWidget); - await tester.pump(const Duration(seconds: deviceScanEmptyAfterSeconds)); - await AnalyticsManager.flushPending(force: true); - expect(adapter.names.where((name) => name == 'Device Connect Failure Shown'), hasLength(1)); - await tester.pumpWidget(const SizedBox()); - }); - - test('saved visible devices clear reachability, but stale-bond recovery is retained', () async { - await SharedPreferencesUtil() - .btDeviceSet(BtDevice(id: 'saved-omi', name: 'Saved Omi', type: DeviceType.omi, rssi: -50)); - provider.pairingFailureReason = Reason.notFoundOrDropped; - provider.onDevices([]); - expect(provider.visibleDeviceList, isNotEmpty); - expect(provider.pairingFailureReason, isNull); - provider.pairingFailureReason = Reason.staleBond; - provider.onDevices([]); - expect(provider.pairingFailureReason, Reason.staleBond); - }); - - testWidgets('render real empty-scan result on iOS and Android with real fonts', (tester) async { - if (Platform.environment['ONB_POLISH_RENDER'] != '1') return; - await tester.runAsync(polish.loadFonts); - const boundary = Key('provider-empty-render'); - for (final platform in [TargetPlatform.iOS, TargetPlatform.android]) { - debugDefaultTargetPlatformOverride = platform; - addTearDown(() => debugDefaultTargetPlatformOverride = null); - await pumpConnect(tester, platform: platform, boundary: boundary); - await tester.pump(const Duration(seconds: deviceScanEmptyAfterSeconds)); - await tester.pump(); - final scroll = tester.state(find.byType(Scrollable).first); - scroll.position.jumpTo(scroll.position.maxScrollExtent); - await tester.pump(const Duration(milliseconds: 500)); - expect(find.text(AppLocalizationsEn().findDeviceNoneTitle).hitTestable(), findsOneWidget); - expect(find.byKey(const Key('find_devices_contact_support')).hitTestable(), findsOneWidget); - expect(find.byType(PairingFailureCard), findsNothing); - expect(tester.takeException(), isNull); - await tester.runAsync(() async { - final render = tester.element(find.byKey(boundary)).renderObject! as RenderRepaintBoundary; - final png = await (await render.toImage(pixelRatio: 3)).toByteData(format: ui.ImageByteFormat.png); - final tag = platform == TargetPlatform.iOS ? '022-mobile-ios' : '023-mobile-android'; - File('../.agent-artifacts/ui-evidence/onb-polish/$tag-connect-empty-scan.png') - .writeAsBytesSync(png!.buffer.asUint8List()); - }); - await tester.pumpWidget(const SizedBox()); - provider.enableInstructions = false; - } - debugDefaultTargetPlatformOverride = null; - }); -} diff --git a/app/test/services/capture/first_capture_finish_result_test.dart b/app/test/services/capture/first_capture_finish_result_test.dart deleted file mode 100644 index c836746c0f1..00000000000 --- a/app/test/services/capture/first_capture_finish_result_test.dart +++ /dev/null @@ -1,41 +0,0 @@ -import 'dart:io'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:omi/gen/phone_mic_pigeon.g.dart'; -import '../../support/capture/capture_replay_world.dart'; - -void main() { - test('regular Process Now failure does not create an unobserved onboarding result future', () async { - final directory = await Directory.systemTemp.createTemp('regular_capture_process_'); - final world = await CaptureReplayWorld.boot(tempDir: directory); - try { - await world.startLiveCapture(); - world.emitNativeState(PhoneMicCaptureState.running); - await world.settle(); - world.processOutcome = () async => throw StateError('processing failed'); - await world.controller.forceProcessingCurrentConversation(); - await world.settle(); - expect(world.processCalls, 1); - } finally { - await world.dispose(); - directory.deleteSync(recursive: true); - } - }); - - test('onboarding Finish uses the existing coordinator Process Now and returns its empty result', () async { - final directory = await Directory.systemTemp.createTemp('first_capture_finish_'); - final world = await CaptureReplayWorld.boot(tempDir: directory); - try { - await world.startLiveCapture(); - world.emitNativeState(PhoneMicCaptureState.running); - await world.settle(); - expect(world.controller.liveCaptureSource, 'phone'); - final result = await world.controller.finishCaptureWithResult(); - expect(world.processCalls, 1); - expect(result, isNull); - expect(world.controller.liveCaptureSource, isNull); - } finally { - await world.dispose(); - directory.deleteSync(recursive: true); - } - }); -} diff --git a/app/test/services/capture/first_capture_io_cancellation_test.dart b/app/test/services/capture/first_capture_io_cancellation_test.dart deleted file mode 100644 index db2b13990bd..00000000000 --- a/app/test/services/capture/first_capture_io_cancellation_test.dart +++ /dev/null @@ -1,80 +0,0 @@ -import 'dart:async'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:omi/providers/capture_provider.dart'; -import 'package:omi/backend/schema/conversation.dart'; -import 'package:omi/backend/schema/structured.dart'; -import 'package:omi/backend/schema/transcript_segment.dart'; -import 'package:omi/pages/onboarding/first_capture_io.dart'; - -class _Capture implements CaptureProvider { - String? source; - int stops = 0; - final pending = Completer(); - @override - String? get liveCaptureSource => source; - @override - String? get activeRecordingId => 'owned'; - @override - bool get isPhoneMicBatchRecording => false; - @override - List get segments => []; - @override - Future streamRecording({bool resumeCapture = true}) async { - source = 'phone'; - } - - @override - Future finishCaptureWithResult() => pending.future; - @override - Future stopStreamRecording({String reason = 'user_stopped', bool resumeHandedOffPendant = true}) async { - stops++; - return true; - } - - @override - dynamic noSuchMethod(Invocation invocation) => throw UnsupportedError(invocation.memberName.toString()); -} - -void main() { - test('terminal discarded failure reaches the guided outcome classifier', () async { - final capture = _Capture(); - final io = PhoneFirstCaptureIO(capture); - await io.start(); - final discarded = ServerConversation( - id: 'discarded', - createdAt: DateTime(2026), - structured: Structured('', ''), - status: ConversationStatus.failed, - discarded: true); - capture.pending.complete(CreateConversationResponse(messages: [], conversation: discarded)); - expect(await io.finish(), same(discarded)); - }); - - test('leaving while Finish awaits never cancels; superseded attempt performs no more polling', () async { - final capture = _Capture(); - final sleep = Completer(); - var current = true, loads = 0; - final io = PhoneFirstCaptureIO(capture, - wait: (_) => sleep.future, - loadConversation: (_) async { - loads++; - return null; - }); - await io.start(); - final finishing = io.finish(isCurrent: () => current); - await io.cancel(); - expect(capture.stops, 0); - capture.pending.complete(CreateConversationResponse( - messages: [], - conversation: ServerConversation( - id: 'confirmed', - createdAt: DateTime(2026), - structured: Structured('', ''), - status: ConversationStatus.processing))); - await Future.delayed(Duration.zero); - current = false; - sleep.complete(); - expect(await finishing, isNull); - expect(loads, 0); - }); -} diff --git a/app/test/unit/onboarding_activation_experiment_test.dart b/app/test/unit/onboarding_activation_experiment_test.dart new file mode 100644 index 00000000000..46c87bc6664 --- /dev/null +++ b/app/test/unit/onboarding_activation_experiment_test.dart @@ -0,0 +1,141 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +import 'package:omi/backend/preferences.dart'; +import 'package:omi/services/experiments/onboarding_activation_flow.dart'; + +// The onboarding ending is A/B by a PostHog flag and fail-closed: off, slow, missing, unknown or +// erroring flag reads all end onboarding the old way (voice introduction = control). + +void main() { + test('flag key is the registered PostHog key', () { + expect(OnboardingActivationExperiment.enabledFlag, 'onboarding-activation-flow'); + }); + + test('no debug override in an ordinary test build', () { + expect(OnboardingActivationExperiment.debugOverride, isNull); + }); + + group('flag value -> ending', () { + test('boolean on is treatment, off or missing is control', () { + expect(OnboardingActivationExperiment.endingForFlagValue(true), OnboardingEnding.activation); + expect(OnboardingActivationExperiment.endingForFlagValue(false), OnboardingEnding.voiceIntro); + expect(OnboardingActivationExperiment.endingForFlagValue(null), OnboardingEnding.voiceIntro); + }); + + test('experiment variants: test/treatment/activation are treatment, control is control', () { + for (final variant in ['test', 'treatment', 'activation', ' Test ']) { + expect(OnboardingActivationExperiment.endingForFlagValue(variant), OnboardingEnding.activation, + reason: variant); + } + expect(OnboardingActivationExperiment.endingForFlagValue('control'), OnboardingEnding.voiceIntro); + }); + + test('unknown variants and odd values are control', () { + for (final value in [ + '', + 'variant-b', + 'true', + 1, + const {'a': 1} + ]) { + expect(OnboardingActivationExperiment.endingForFlagValue(value), OnboardingEnding.voiceIntro, reason: '$value'); + } + }); + }); + + group('resolve', () { + test('flag off -> control, and the variant is not read', () async { + final keys = []; + var valueRead = false; + final ending = await OnboardingActivationExperiment.resolve( + readEnabled: (key) async { + keys.add(key); + return false; + }, + readValue: (_) async { + valueRead = true; + return 'test'; + }, + ); + expect(ending, OnboardingEnding.voiceIntro); + expect(keys, ['onboarding-activation-flow']); + expect(valueRead, isFalse); + }); + + test('boolean flag on -> activation', () async { + expect( + await OnboardingActivationExperiment.resolve(readEnabled: (_) async => true, readValue: (_) async => true), + OnboardingEnding.activation, + ); + }); + + test('experiment "control" variant is enabled but still control', () async { + expect( + await OnboardingActivationExperiment.resolve(readEnabled: (_) async => true, readValue: (_) async => 'control'), + OnboardingEnding.voiceIntro, + ); + }); + + test('experiment "test" variant -> activation', () async { + expect( + await OnboardingActivationExperiment.resolve(readEnabled: (_) async => true, readValue: (_) async => 'test'), + OnboardingEnding.activation, + ); + }); + + test('flag read error -> control (fail closed)', () async { + expect( + await OnboardingActivationExperiment.resolve(readEnabled: (_) => Future.error(StateError('no sdk'))), + OnboardingEnding.voiceIntro, + ); + expect( + await OnboardingActivationExperiment.resolve( + readEnabled: (_) async => true, readValue: (_) => Future.error(StateError('no sdk'))), + OnboardingEnding.voiceIntro, + ); + }); + }); + + group('experiment exposure', () { + const flag = '\$feature/onboarding-activation-flow'; + + test('carries the PostHog variant the arm came from', () async { + await OnboardingActivationExperiment.resolve(readEnabled: (_) async => true, readValue: (_) async => 'test'); + expect(OnboardingActivationExperiment.exposureProperties(), {flag: 'test'}); + await OnboardingActivationExperiment.resolve(readEnabled: (_) async => true, readValue: (_) async => 'control'); + expect(OnboardingActivationExperiment.exposureProperties(), {flag: 'control'}); + }); + + test('readers who fell back to control carry no variant, so they stay out of the analysis', () async { + await OnboardingActivationExperiment.resolve(readEnabled: (_) async => true, readValue: (_) async => 'test'); + await OnboardingActivationExperiment.resolve(readEnabled: (_) async => false); + expect(OnboardingActivationExperiment.exposureProperties(), isEmpty); + await OnboardingActivationExperiment.resolve(readEnabled: (_) => Future.error(StateError('no sdk'))); + expect(OnboardingActivationExperiment.exposureProperties(), isEmpty); + }); + }); + + group('per-account memory', () { + setUp(() async { + SharedPreferences.setMockInitialValues({}); + await SharedPreferencesUtil.init(); + }); + + test('remembers the started ending per account and forgets it on completion', () { + expect(OnboardingActivationExperiment.remembered('uid-a'), isNull); + OnboardingActivationExperiment.remember('uid-a', OnboardingEnding.activation); + OnboardingActivationExperiment.remember('uid-b', OnboardingEnding.voiceIntro); + expect(OnboardingActivationExperiment.remembered('uid-a'), OnboardingEnding.activation); + expect(OnboardingActivationExperiment.remembered('uid-b'), OnboardingEnding.voiceIntro); + OnboardingActivationExperiment.forget('uid-a'); + expect(OnboardingActivationExperiment.remembered('uid-a'), isNull); + expect(OnboardingActivationExperiment.remembered('uid-b'), OnboardingEnding.voiceIntro); + }); + + test('no account -> nothing remembered', () { + OnboardingActivationExperiment.remember('', OnboardingEnding.activation); + expect(OnboardingActivationExperiment.remembered(''), isNull); + }); + }); +} diff --git a/app/test/unit/paywall_experiment_test.dart b/app/test/unit/paywall_experiment_test.dart index c5d9a67839a..281175dc7b5 100644 --- a/app/test/unit/paywall_experiment_test.dart +++ b/app/test/unit/paywall_experiment_test.dart @@ -15,6 +15,14 @@ void main() { }); group('parse', () { + test('control carries the onboarding placement from its payload', () { + final config = PaywallExperiment.parse(variant: 'control', payload: '{"onboarding_paywall": true}'); + expect(config.isControl, isTrue); + expect(config.onboardingPaywall, isTrue); + expect(PaywallExperiment.parse(variant: 'control', payload: {'url': 'https://x.example'}), + PaywallExperimentConfig.control); + }); + test('control, null and unknown variants are control', () { expect(PaywallExperiment.parse(variant: 'control'), PaywallExperimentConfig.control); expect(PaywallExperiment.parse(variant: null), PaywallExperimentConfig.control); @@ -69,18 +77,21 @@ void main() { expect(payloadReads, ['paywall-variant']); }); - test('control skips the payload read', () async { - var payloadReads = 0; - final config = await PaywallExperiment.resolve( + test('control reads its payload for the onboarding placement', () async { + final plain = await PaywallExperiment.resolve( identity: () => 'u', readVariant: (_) async => 'control', - readPayload: (_) async { - payloadReads++; - return null; - }, + readPayload: (_) async => null, ); - expect(config, PaywallExperimentConfig.control); - expect(payloadReads, 0); + expect(plain, PaywallExperimentConfig.control); + PaywallExperiment.resetForTesting(); + final placed = await PaywallExperiment.resolve( + identity: () => 'u', + readVariant: (_) async => 'control', + readPayload: (_) async => {'onboarding_paywall': true}, + ); + expect(placed.isControl, isTrue); + expect(placed.onboardingPaywall, isTrue); }); test('an unresolved read is control but not cached', () async { diff --git a/app/test/unit/paywall_reask_test.dart b/app/test/unit/paywall_reask_test.dart new file mode 100644 index 00000000000..c1b844ee9d6 --- /dev/null +++ b/app/test/unit/paywall_reask_test.dart @@ -0,0 +1,91 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +import 'package:omi/backend/preferences.dart'; +import 'package:omi/services/experiments/paywall_reask.dart'; + +// The re-ask comes back 2, 3, 5 and then every 7 days after the last paywall, never on the +// first Home visit, and counts both arms at the same moments. + +void main() { + final day0 = DateTime(2026, 10, 14, 9); + DateTime day(int n, [int hour = 9]) => DateTime(day0.year, day0.month, day0.day + n, hour); + + late List exposures; + late String? variant; + + Future check(DateTime now) => PaywallReask.checkDue( + now: now, + readVariant: (_) async => variant, + recordExposure: (key, value) { + exposures.add('$key=$value'); + return true; + }, + ); + + setUp(() async { + SharedPreferences.setMockInitialValues({'uid': 'reader-1'}); + await SharedPreferencesUtil.init(); + exposures = []; + variant = 'test'; + }); + + test('the gaps widen and then hold at a week', () { + expect([for (var i = 0; i < 7; i++) PaywallReask.gapAfter(i)], [2, 3, 5, 7, 7, 7, 7]); + }); + + test('calendar days, not hours: a late open and an early open two days later count as 2', () { + expect(PaywallReask.daysBetween(DateTime(2026, 10, 14, 23), DateTime(2026, 10, 16, 1)), 2); + expect(PaywallReask.daysBetween(DateTime(2026, 10, 14, 1), DateTime(2026, 10, 14, 23)), 0); + }); + + test('never on the first Home visit; due two days later; then the schedule widens', () async { + expect(await check(day0), isFalse, reason: 'first visit starts the clock'); + expect(exposures, isEmpty); + expect(await check(day(1)), isFalse); + expect(await check(day(2)), isTrue, reason: 'gap 2 after the first visit'); + expect(await check(day(2, 18)), isFalse, reason: 'at most once a day'); + expect(await check(day(4)), isFalse); + expect(await check(day(5)), isTrue, reason: 'second gap is 3'); + expect(await check(day(9)), isFalse); + expect(await check(day(10)), isTrue, reason: 'third gap is 5'); + expect(await check(day(16)), isFalse); + expect(await check(day(17)), isTrue, reason: 'then weekly'); + expect(await check(day(24)), isTrue, reason: 'still weekly'); + expect(exposures, everyElement('paywall-reask=test')); + expect(exposures, hasLength(5)); + }); + + test('any other paywall restarts the clock', () async { + await check(day0); + PaywallReask.noteShown(now: day(1)); // e.g. a locked conversation + expect(await check(day(2)), isFalse); + expect(await check(day(3)), isTrue); + }); + + test('control is exposed on the same schedule but shows nothing', () async { + variant = 'control'; + await check(day0); + expect(await check(day(2)), isFalse); + expect(exposures, ['paywall-reask=control']); + expect(await check(day(4)), isFalse); + expect(await check(day(5)), isFalse); + expect(exposures, hasLength(2), reason: 'control advances the same schedule'); + }); + + test('an unread flag shows nothing, exposes no one and keeps the reader due', () async { + variant = null; + await check(day0); + expect(await check(day(2)), isFalse); + expect(exposures, isEmpty); + variant = 'test'; + expect(await check(day(3)), isTrue, reason: 'still due once the flag is read'); + }); + + test('no signed-in reader: nothing happens', () async { + SharedPreferences.setMockInitialValues({}); + await SharedPreferencesUtil.init(); + expect(await check(day(30)), isFalse); + expect(exposures, isEmpty); + }); +} diff --git a/app/test/widgets/action_items_row_gestures_test.dart b/app/test/widgets/action_items_row_gestures_test.dart index 08b164f5a22..d5384895a84 100644 --- a/app/test/widgets/action_items_row_gestures_test.dart +++ b/app/test/widgets/action_items_row_gestures_test.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:omi/backend/schema/schema.dart'; +import 'package:omi/pages/action_items/widgets/task_row_parts.dart'; import 'package:omi/env/env.dart'; import 'package:omi/providers/action_items_provider.dart'; import 'package:omi/utils/platform/platform_manager.dart'; @@ -68,11 +69,28 @@ void main() { await tester.drag(find.text('Book the venue'), const Offset(400, 0)); await tester.pumpAndSettle(); + // The row shows done, folds away, then the task is completed. + await tester.pump(const Duration(seconds: 1)); + await tester.pumpAndSettle(); expect(completions, [true]); expect(provider.actionItems.firstWhere((i) => i.id == 'child').indentLevel, 1); }); + testWidgets('ticking a task shows it done at once, folds it away, then completes it', (tester) async { + final (_, _, completions) = await pumpPage(tester); + + await tester.tap(find.byType(TaskCompletionMark).last); + await tester.pump(const Duration(milliseconds: 100)); + final title = tester.widget(find.text('Book the venue')); + expect(title.style?.decoration, TextDecoration.lineThrough, reason: 'looks done right away'); + expect(completions, isEmpty, reason: 'the save waits for the fold'); + + await tester.pump(const Duration(seconds: 1)); + await tester.pumpAndSettle(); + expect(completions, [true]); + }); + testWidgets('swiping left on an indented task deletes it with Undo, no dialog', (tester) async { final (provider, deletes, _) = await pumpPage(tester); diff --git a/app/test/widgets/bluetooth_guidance_listener_test.dart b/app/test/widgets/bluetooth_guidance_listener_test.dart index ce9ed0e854b..e93e167ca90 100644 --- a/app/test/widgets/bluetooth_guidance_listener_test.dart +++ b/app/test/widgets/bluetooth_guidance_listener_test.dart @@ -30,8 +30,7 @@ void main() { await tester.pumpAndSettle(); expect(find.text('Enable Bluetooth'), findsOneWidget); - expect(find.text('Omi needs Bluetooth to connect to your wearable. Please enable Bluetooth and try again.'), - findsOneWidget); + expect(find.text('Omi connects to your phone over Bluetooth. Turn it on to continue.'), findsOneWidget); await tester.tap(find.text('OK')); await tester.pumpAndSettle(); diff --git a/app/test/widgets/capture_home_ui_test.dart b/app/test/widgets/capture_home_ui_test.dart index b04a71a2fde..29f389ea43f 100644 --- a/app/test/widgets/capture_home_ui_test.dart +++ b/app/test/widgets/capture_home_ui_test.dart @@ -131,6 +131,9 @@ class _Capture extends ChangeNotifier implements CaptureProvider { Future pauseCapture() async => pauses++; @override Future resumeCapture() async => resumes++; + int finishes = 0; + @override + Future finishCapture() async => finishes++; @override Future streamRecording({bool resumeCapture = true}) async { phoneStarts++; @@ -527,10 +530,43 @@ void main() { await tester.pumpWidget(const SizedBox.shrink()); }); + group('live pill replaces the Home card while recording is healthy', () { + testWidgets('the phone pill shows the newest words; Home shows no card', (tester) async { + await pump(tester, const HomeRecordButton(), + device: _Device(connected: false, paired: false), capture: _Capture(_Live.phone)); + await tester.pump(const Duration(milliseconds: 300)); + expect(find.byKey(const ValueKey('home_pill_last_words')), findsOneWidget); + expect(find.text('Keep the pendant flow as it is.'), findsOneWidget); + await tester.pumpWidget(const SizedBox()); + + await pump(tester, const ConversationCaptureWidget(showsCall: true, deviceStatusInBottomBar: true), + device: _Device(connected: false, paired: false), capture: _Capture(_Live.phone)); + expect(find.byType(LiveCaptureCard), findsNothing); + await tester.pumpWidget(const SizedBox()); + }); + + testWidgets('a paused or failing phone recording keeps the card', (tester) async { + for (final capture in [_Capture(_Live.phone, readerPaused: true), _Capture(_Live.phone, failure: true)]) { + await pump(tester, const ConversationCaptureWidget(showsCall: true, deviceStatusInBottomBar: true), + device: _Device(connected: false, paired: false), capture: capture); + expect(find.byType(LiveCaptureCard), findsOneWidget); + await tester.pumpWidget(const SizedBox()); + } + }); + + testWidgets('the pendant pill shows the newest words too', (tester) async { + await pump(tester, const HomeRecordButton(), capture: _Capture(_Live.pendant), device: _Device(paired: true)); + await tester.pump(const Duration(milliseconds: 300)); + expect(find.byKey(const ValueKey('home_pill_last_words')), findsOneWidget); + await tester.pumpWidget(const SizedBox()); + }); + }); + group('record-with-this-phone button', () { - testWidgets('idle with calls hidden: a white dot and no ways-to-record badge', (tester) async { - await pump(tester, const HomeRecordButton(), capture: _Capture(_Live.idle)); - expect(find.bySemanticsLabel(en.startRecording), findsOneWidget); + testWidgets('idle with calls hidden: + New, and no ways-to-record option', (tester) async { + await pump(tester, const HomeRecordButton(), + device: _Device(connected: false, paired: false), capture: _Capture(_Live.idle)); + expect(find.bySemanticsLabel(en.newRecording), findsOneWidget); expect(find.bySemanticsLabel(en.moreWaysToRecord), findsNothing); expect(find.byIcon(Icons.keyboard_arrow_down_rounded), findsNothing); await tester.longPress(find.byType(HomeRecordButton)); @@ -538,9 +574,10 @@ void main() { expect(find.text(en.recordWith), findsNothing, reason: 'the only extra way to record was a phone call'); }, skip: PhoneCallsFeature.visible); - testWidgets('idle: a white dot and a badge that opens the ways to record', (tester) async { - await pump(tester, const HomeRecordButton(), capture: _Capture(_Live.idle)); - expect(find.bySemanticsLabel(en.startRecording), findsOneWidget); + testWidgets('idle: + New, whose long-press opens the ways to record', (tester) async { + await pump(tester, const HomeRecordButton(), + device: _Device(connected: false, paired: false), capture: _Capture(_Live.idle)); + expect(find.bySemanticsLabel(en.newRecording), findsOneWidget); await tester.tap(find.bySemanticsLabel(en.moreWaysToRecord)); await tester.pumpAndSettle(); expect(find.text(en.recordWith), findsOneWidget); @@ -548,24 +585,26 @@ void main() { expect(find.text(en.phoneCall), findsOneWidget); }, skip: !PhoneCallsFeature.visible); - testWidgets('while the pendant records, a tap explains instead of taking over', (tester) async { + testWidgets('while the pendant records, holding its pill offers the phone instead of taking over', (tester) async { final capture = _Capture(_Live.pendant); - await pump(tester, const HomeRecordButton(), capture: capture); - await tester.tap(find.bySemanticsLabel(en.startRecording)); - await tester.pumpAndSettle(); + await pump(tester, const HomeRecordButton(), capture: capture, device: _Device(paired: true)); + await tester.longPress(find.byKey(const ValueKey('home_record_pill_device'))); + await tester.pump(); + await tester.pump(const Duration(seconds: 1)); // the live dot breathes, so never settles expect(find.text(en.pendantIsListeningTitle), findsOneWidget); expect(capture.phoneStarts, 0, reason: 'never a silent takeover'); await tester.tap(find.text(en.keepUsingPendant)); - await tester.pumpAndSettle(); + await tester.pump(); + await tester.pump(const Duration(seconds: 1)); // the live dot breathes, so never settles expect(find.text(en.pendantIsListeningTitle), findsNothing); expect(capture.phoneStarts, 0); }); testWidgets('a Transcribe Later pendant rejects phone takeover with visible feedback', (tester) async { final capture = _Capture(_Live.pendantBatch); - await pump(tester, const HomeRecordButton(), capture: capture); - await tester.tap(find.bySemanticsLabel(en.startRecording)); + await pump(tester, const HomeRecordButton(), device: _Device(connected: false, paired: false), capture: capture); + await tester.tap(find.bySemanticsLabel(en.newRecording)); await tester.pump(); expect(find.text(en.phoneRecordingBlockedByPendantBatch), findsOneWidget); expect(capture.phoneStarts, 0); @@ -574,8 +613,8 @@ void main() { testWidgets('a start that fails says so and never opens the capturing page', (tester) async { final capture = _Capture(_Live.idle)..phoneStartFailure = StateError('refused'); - await pump(tester, const HomeRecordButton(), capture: capture); - await tester.tap(find.bySemanticsLabel(en.startRecording)); + await pump(tester, const HomeRecordButton(), device: _Device(connected: false, paired: false), capture: capture); + await tester.tap(find.bySemanticsLabel(en.newRecording)); await tester.pump(); expect(capture.phoneStarts, 1); expect(find.text(en.somethingWentWrong), findsOneWidget); @@ -584,8 +623,8 @@ void main() { testWidgets('a start that resolves without phone ownership navigates nowhere', (tester) async { final capture = _Capture(_Live.idle); - await pump(tester, const HomeRecordButton(), capture: capture); - await tester.tap(find.bySemanticsLabel(en.startRecording)); + await pump(tester, const HomeRecordButton(), device: _Device(connected: false, paired: false), capture: capture); + await tester.tap(find.bySemanticsLabel(en.newRecording)); await tester.pump(); expect(capture.phoneStarts, 1); expect(find.byType(ConversationCapturingPage), findsNothing); @@ -594,28 +633,54 @@ void main() { testWidgets('during an Omi call the button never starts a recording', (tester) async { final capture = _Capture(_Live.idle); - await pump(tester, const HomeRecordButton(), capture: capture, call: _Call(PhoneCallState.active)); - await tester.tap(find.bySemanticsLabel(en.startRecording)); + await pump(tester, const HomeRecordButton(), + device: _Device(connected: false, paired: false), capture: capture, call: _Call(PhoneCallState.active)); + await tester.tap(find.bySemanticsLabel(en.newRecording)); expect(capture.phoneStarts, 0, reason: 'the tap opens the call page instead'); await tester.pumpWidget(const SizedBox()); // the call page itself is not under test here }); - testWidgets('the badge is fully tappable and does not cover the circle\'s centre', (tester) async { - await pump(tester, const HomeRecordButton(), capture: _Capture(_Live.idle)); - final badge = tester.getRect(find - .ancestor(of: find.byIcon(Icons.keyboard_arrow_down_rounded), matching: find.byType(GestureDetector)) - .first); - final button = tester.getRect(find.byType(HomeRecordButton)); - expect(badge.width, greaterThanOrEqualTo(30)); - expect(button.inflate(0.1).contains(badge.topLeft) && button.inflate(0.1).contains(badge.bottomRight), isTrue, - reason: 'a Stack only hit-tests inside its own box'); - expect(badge.contains(button.center), isFalse); - }, skip: !PhoneCallsFeature.visible); + testWidgets('idle, the pill reads + New Recording', (tester) async { + await pump(tester, const HomeRecordButton(), + device: _Device(connected: false, paired: false), capture: _Capture(_Live.idle)); + expect(find.text(en.homeNewRecordingButton), findsOneWidget); + expect(find.byIcon(Icons.add_rounded), findsOneWidget); + }); - testWidgets('while the phone records, the button is its stop', (tester) async { - await pump(tester, const HomeRecordButton(), capture: _Capture(_Live.phone)); - expect(find.bySemanticsLabel(en.stopRecording), findsOneWidget); - expect(find.bySemanticsLabel(en.moreWaysToRecord), findsNothing); + testWidgets('while the phone records, the pill shows it listening and holds its Stop', (tester) async { + await pump(tester, const HomeRecordButton(), + device: _Device(connected: false, paired: false), capture: _Capture(_Live.phone)); + await tester.pump(const Duration(milliseconds: 300)); + expect(find.text(en.listening), findsOneWidget); + expect(find.byKey(const ValueKey('home_record_pill_stop_button')), findsOneWidget); + final pill = tester.getSemantics(find.byKey(const ValueKey('home_record_pill_stop'))); + expect(pill.getSemanticsData().customSemanticsActionIds, isNotEmpty, reason: 'Stop reachable by VoiceOver'); + expect(find.text(en.homeNewRecordingButton), findsNothing); + await tester.pumpWidget(const SizedBox()); + }); + + testWidgets('Stop on a phone recording that is reconnecting finishes it, never restarts it', (tester) async { + final capture = _Capture(_Live.phone, interrupted: true); + await pump(tester, const HomeRecordButton(), device: _Device(connected: false, paired: false), capture: capture); + await tester.tap(find.byKey(const ValueKey('home_record_pill_stop_button'))); + await tester.pump(); + expect(capture.finishes, 1); + expect(capture.phoneStarts, 0); + await tester.pumpWidget(const SizedBox()); + }); + + testWidgets('a connected pendant that is not capturing leaves + New in place', (tester) async { + await pump(tester, const HomeRecordButton(), + device: _Device(paired: true), capture: _Capture(_Live.idleDeviceConnected)); + expect(find.text(en.homeNewRecordingButton), findsOneWidget); + }); + + testWidgets('with a pendant connected and capturing, the pill shows it listening', (tester) async { + await pump(tester, const HomeRecordButton(), capture: _Capture(_Live.pendant), device: _Device(paired: true)); + await tester.pump(const Duration(milliseconds: 300)); + expect(find.text(en.homeNewRecordingButton), findsNothing); + expect(find.textContaining(en.listening), findsOneWidget); + await tester.pumpWidget(const SizedBox()); }); }); diff --git a/app/test/widgets/chat_welcome_test.dart b/app/test/widgets/chat_welcome_test.dart new file mode 100644 index 00000000000..8aef30cdbb4 --- /dev/null +++ b/app/test/widgets/chat_welcome_test.dart @@ -0,0 +1,48 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +import 'package:omi/backend/preferences.dart'; +import 'package:omi/l10n/app_localizations.dart'; +import 'package:omi/pages/chat/widgets/chat_starters.dart'; + +void main() { + setUp(() async { + SharedPreferences.setMockInitialValues({}); + await SharedPreferencesUtil.init(); + }); + + Widget host({required bool animate}) => MaterialApp( + localizationsDelegates: AppLocalizations.localizationsDelegates, + supportedLocales: const [Locale('en')], + home: Scaffold( + body: ChatGreeting( + isConnected: true, name: 'Yinka', hour: 19, todayCount: 0, welcome: true, animateWelcome: animate), + ), + ); + + testWidgets('the first time, Omi types its welcome in: dots, then the message', (tester) async { + expect(ChatWelcome.pending, isTrue); + expect(ChatWelcome.shown, isFalse); + await tester.pumpWidget(host(animate: true)); + expect(find.byKey(const Key('chat_welcome_typing')), findsOneWidget); + expect(find.text('What do you want to know?'), findsNothing); + + await tester.pump(const Duration(milliseconds: 1400)); + expect(find.byKey(const Key('chat_welcome_typing')), findsNothing); + expect(find.byKey(const Key('chat_welcome_message')), findsOneWidget); + + await tester.pump(const Duration(seconds: 3)); + expect(ChatWelcome.shown, isTrue, reason: 'typed in once'); + expect(ChatWelcome.pending, isTrue, reason: 'still shown until the first message is sent'); + }); + + testWidgets('later visits show it settled; sending a message retires it', (tester) async { + await tester.pumpWidget(host(animate: false)); + expect(find.byKey(const Key('chat_welcome_typing')), findsNothing); + expect(find.textContaining("Hi, I'm Omi."), findsOneWidget); + + ChatWelcome.markDone(); + expect(ChatWelcome.pending, isFalse); + }); +} diff --git a/app/test/widgets/conversation_locked_preview_test.dart b/app/test/widgets/conversation_locked_preview_test.dart index c1d1a462abb..7fc1b18e693 100644 --- a/app/test/widgets/conversation_locked_preview_test.dart +++ b/app/test/widgets/conversation_locked_preview_test.dart @@ -8,7 +8,6 @@ import 'package:omi/backend/schema/conversation.dart'; import 'package:omi/backend/schema/structured.dart'; import 'package:omi/l10n/app_localizations.dart'; import 'package:omi/pages/conversations/widgets/conversation_list_item.dart'; -import 'package:omi/pages/settings/usage_page.dart'; import 'package:omi/providers/conversation_provider.dart'; import 'package:omi/providers/usage_provider.dart'; import 'package:omi/ui/ui.dart'; @@ -139,16 +138,14 @@ void main() { expect(find.text(_upgrade), findsNothing); }); - testWidgets('the upgrade action keeps the existing paywall destination', (tester) async { + testWidgets('the upgrade action opens the plans sheet over the list, not the usage page', (tester) async { final routes = _Routes(); await pumpRow(tester, observer: routes); await tester.tap(find.byKey(const Key('locked_preview_action'))); expect(routes.pushed, hasLength(2)); - final route = routes.pushed.last as MaterialPageRoute; - final destination = route.builder(tester.element(find.byType(ConversationListItem))); - expect(destination, isA()); - expect((destination as UsagePage).showUpgradeDialog, isTrue); - // Check navigation without building the network-backed usage screen. + // The locked_content paywall (or its plans-sheet fallback) opens in place; no page push. + expect(routes.pushed.last, isA()); + // Check navigation without building the network-backed plans sheet. await tester.pumpWidget(const SizedBox.shrink()); }); @@ -191,10 +188,7 @@ void main() { } await tester.tap(find.byKey(const Key('locked_preview_action'))); expect(routes.pushed, hasLength(2)); - expect( - (routes.pushed.last as MaterialPageRoute).builder(tester.element(find.byType(LockedConversationRun))), - isA(), - ); + expect(routes.pushed.last, isA()); await tester.pumpWidget(const SizedBox.shrink()); }); diff --git a/app/test/widgets/device_onboarding_step_contrast_test.dart b/app/test/widgets/device_onboarding_step_contrast_test.dart index 35a70ad37ee..5b4d99404bc 100644 --- a/app/test/widgets/device_onboarding_step_contrast_test.dart +++ b/app/test/widgets/device_onboarding_step_contrast_test.dart @@ -38,7 +38,7 @@ void main() { final subtitle = tester.widget(find.text('Step subtitle')); expect(subtitle.style!.color, palette.textSecondary); - final button = tester.widget(find.byType(ElevatedButton)); + final button = tester.widget(find.descendant(of: find.byType(OnboardingContinueButton), matching: find.byType(TextButton))); final background = button.style!.backgroundColor!.resolve({}); final foreground = button.style!.foregroundColor!.resolve({}); expect(background, palette.accent); diff --git a/app/test/widgets/empty_recording_message_render_test.dart b/app/test/widgets/empty_recording_message_render_test.dart index d3d52c9da77..5deb3eb53a1 100644 --- a/app/test/widgets/empty_recording_message_render_test.dart +++ b/app/test/widgets/empty_recording_message_render_test.dart @@ -1,5 +1,6 @@ import 'dart:convert'; import 'dart:io'; +import 'dart:typed_data'; import 'dart:ui' as ui; import 'package:flutter/foundation.dart'; diff --git a/app/test/widgets/first_capture_test.dart b/app/test/widgets/first_capture_test.dart deleted file mode 100644 index e3e13f9a287..00000000000 --- a/app/test/widgets/first_capture_test.dart +++ /dev/null @@ -1,738 +0,0 @@ -import 'dart:async'; -import 'dart:convert'; -import 'dart:io'; -import 'dart:ui' as ui; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/rendering.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:omi/backend/schema/conversation.dart'; -import 'package:omi/backend/schema/bt_device/bt_device.dart'; -import 'package:omi/backend/schema/structured.dart'; -import 'package:omi/backend/schema/transcript_segment.dart'; -import 'package:omi/l10n/app_localizations.dart'; -import 'package:omi/pages/onboarding/complete_screen.dart'; -import 'package:omi/pages/onboarding/first_capture_controller.dart'; -import 'package:omi/pages/onboarding/first_capture_page.dart'; -import 'package:omi/pages/onboarding/wrapper.dart'; -import 'package:omi/pages/onboarding/widgets/onboarding_step_layout.dart'; -import 'package:omi/pages/onboarding/onboarding_step_telemetry.dart'; -import 'package:omi/pages/capture/connect.dart'; -import 'package:omi/widgets/connection_guide_sheet.dart'; -import 'package:omi/providers/onboarding_provider.dart'; -import 'package:omi/providers/device_provider.dart'; -import 'package:omi/providers/home_provider.dart'; -import 'package:omi/backend/preferences.dart'; -import 'package:provider/provider.dart'; -import 'package:shared_preferences/shared_preferences.dart'; -import '../../integration_test/visual_audit/fakes.dart' show AuditDeviceProvider; -import 'package:omi/ui/ui.dart'; -import 'package:omi/utils/analytics/registry/events.g.dart'; - -Widget host(Widget child, {TargetPlatform platform = TargetPlatform.iOS}) => MaterialApp( - debugShowCheckedModeBanner: false, - theme: buildOmiTheme(brightness: Brightness.light).copyWith( - platform: platform, - snackBarTheme: buildOmiTheme(brightness: Brightness.light).snackBarTheme.copyWith( - contentTextStyle: buildOmiTheme(brightness: Brightness.light) - .snackBarTheme - .contentTextStyle! - .copyWith(fontFamily: platform == TargetPlatform.iOS ? 'SF Pro Display' : 'Roboto')), - appBarTheme: buildOmiTheme(brightness: Brightness.light).appBarTheme.copyWith( - titleTextStyle: - OmiType.headline.copyWith(fontFamily: platform == TargetPlatform.iOS ? 'SF Pro Display' : 'Roboto')), - textTheme: buildOmiTheme(brightness: Brightness.light) - .textTheme - .apply(fontFamily: platform == TargetPlatform.iOS ? 'SF Pro Display' : 'Roboto')), - localizationsDelegates: AppLocalizations.localizationsDelegates, - supportedLocales: AppLocalizations.supportedLocales, - home: Scaffold(body: child), - ); - -class FakeCaptureIO implements FirstCaptureIO { - @override - String transcript = ''; - int starts = 0, finishes = 0, cancels = 0; - ServerConversation? result; - Completer? processing; - bool failStart = false; - @override - Future start() async { - starts++; - if (failStart) throw StateError('permission'); - } - - @override - Future finish({bool Function()? isCurrent}) async { - finishes++; - if (processing != null) return await processing!.future; - return result; - } - - @override - Future cancel() async { - cancels++; - } -} - -ServerConversation exampleConversation() => ServerConversation( - id: 'first', - createdAt: DateTime(2026, 10, 13), - structured: Structured('Planning today’s work', - 'You’re working on the launch today. Your next step is to finish the onboarding flow and share it with your team.'), - transcriptSegments: [ - TranscriptSegment( - id: 'segment', - text: 'Today I am working on the launch and preparing the onboarding flow.', - speaker: 'SPEAKER_0', - isUser: true, - personId: null, - start: 0, - end: 30, - translations: []) - ], - ); - -void main() { - setUp(() async { - SharedPreferences.setMockInitialValues({}); - await SharedPreferencesUtil.init(); - }); - testWidgets('phone routing, minimum duration, processing, result and capture telemetry', (tester) async { - var now = DateTime(2026); - final io = FakeCaptureIO()..processing = Completer(); - final events = []; - final props = {}; - final flow = FirstCaptureController(io, emit: events.add, now: () => now); - var completed = false; - await tester.pumpWidget(host(OnboardingCompleteScreen( - onComplete: () => completed = true, - emit: events.add, - setProperty: (k, v) => props[k] = v, - phoneControllerFactory: () => flow))); - expect(events.single.wireName, 'Onboarding First Capture Shown'); - await tester.tap(find.byKey(const Key('first_capture_phone'))); - await tester.pumpAndSettle(); - expect(props, {'capture_choice': 'phone', 'has_omi_device': false}); - await tester.tap(find.byKey(const Key('first_capture_primary'))); - await tester.pump(); - now = now.add(const Duration(seconds: 5)); - await tester.pump(const Duration(seconds: 5)); - expect(flow.canFinish, isTrue); - await tester.tap(find.byKey(const Key('first_capture_primary'))); - await tester.pump(const Duration(milliseconds: 300)); - expect(find.text('Finish This Recording?'), findsOneWidget); - expect(find.descendant(of: find.byKey(const Key('first_capture_primary')), matching: find.byType(OmiSpinner)), - findsNothing); - expect(find.descendant(of: find.byKey(const Key('first_capture_primary')), matching: find.text('Finish Recording')), - findsOneWidget); - await tester.tap(find.text('Keep Recording')); - await tester.pumpAndSettle(); - expect(io.finishes, 0); - io.transcript = 'Today I am working on the launch.'; - now = now.add(const Duration(seconds: 25)); - await tester.pump(const Duration(seconds: 25)); - await tester.tap(find.byKey(const Key('first_capture_primary'))); - await tester.pump(); - expect(flow.phase, FirstCapturePhase.processing); - expect(completed, isFalse); - io.processing!.complete(exampleConversation()); - await tester.pumpAndSettle(); - expect(find.byKey(const Key('first_capture_result_card')), findsOneWidget); - final event = events.whereType().single; - expect(event.properties, {'duration_s': 30, 'transcript_observed': true}); - expect(events.whereType().length, 1); - await tester.tap(find.byKey(const Key('first_capture_primary'))); - expect(completed, isTrue); - await tester.pumpAndSettle(); - expect(find.byType(FirstCapturePage), findsNothing); - await tester.pumpWidget(const SizedBox()); - }); - - testWidgets('empty response offers retry and explicit skip without completing early', (tester) async { - var now = DateTime(2026); - final io = FakeCaptureIO()..result = (exampleConversation()..discarded = true); - final events = []; - final flow = FirstCaptureController(io, emit: events.add, now: () => now); - var skipped = false; - await tester.pumpWidget(host(FirstCapturePage(controller: flow, onComplete: () {}, onSkip: () => skipped = true))); - await flow.start(); - now = now.add(const Duration(seconds: 30)); - await tester.pump(const Duration(seconds: 30)); - await flow.finish(); - await tester.pump(); - expect(find.text("I didn't catch enough speech, try again with a few sentences"), findsOneWidget); - expect(events.whereType().length, 1); - expect(skipped, isFalse); - await tester.tap(find.byKey(const Key('first_capture_primary'))); - await tester.pump(); - expect(io.starts, 2); - await flow.leave(); - flow.phase = FirstCapturePhase.empty; - await tester.pumpWidget(host(FirstCapturePage(controller: flow, onComplete: () {}, onSkip: () => skipped = true))); - await tester.tap(find.byKey(const Key('first_capture_skip'))); - expect(skipped, isTrue); - flow.dispose(); - }); - - testWidgets('short Finish keeps its label during confirmation and processes only after confirm', (tester) async { - var now = DateTime(2026); - final io = FakeCaptureIO()..processing = Completer(); - final flow = FirstCaptureController(io, emit: (_) {}, now: () => now); - await tester.pumpWidget(host(FirstCapturePage(controller: flow, onComplete: () {}, onSkip: () {}))); - await flow.start(); - now = now.add(const Duration(seconds: 5)); - await tester.pump(const Duration(seconds: 5)); - await tester.tap(find.byKey(const Key('first_capture_primary'))); - await tester.pump(); - await tester.pump(const Duration(milliseconds: 350)); - expect(find.byType(OmiAlertDialog), findsOneWidget); - expect(find.descendant(of: find.byKey(const Key('first_capture_primary')), matching: find.byType(OmiSpinner)), - findsNothing); - expect(io.finishes, 0); - await tester.tap(find.descendant(of: find.byType(OmiAlertDialog), matching: find.text('Finish Recording'))); - await tester.pump(); - await tester.pump(const Duration(milliseconds: 350)); - expect(io.finishes, 1); - expect(flow.phase, FirstCapturePhase.processing); - io.processing!.complete(exampleConversation()); - await tester.pumpAndSettle(); - await tester.pumpWidget(const SizedBox()); - flow.dispose(); - }); - - testWidgets('pair cancel returns to choices; successful pair completes; skip emits properties', (tester) async { - var paired = false, completed = 0; - final events = []; - final props = {}; - await tester.pumpWidget(host(OnboardingCompleteScreen( - onComplete: () => completed++, - emit: events.add, - setProperty: (k, v) => props[k] = v, - pairDevice: (_) async => paired))); - await tester.tap(find.byKey(const Key('first_capture_omi'))); - await tester.pump(); - expect(completed, 0); - expect(props, {'capture_choice': 'omi', 'has_omi_device': false}); - paired = true; - await tester.tap(find.byKey(const Key('first_capture_omi'))); - await tester.pump(); - expect(completed, 1); - expect(props['has_omi_device'], isTrue); - await tester.tap(find.byKey(const Key('onboarding_complete_start'))); - expect(props['capture_choice'], 'skipped'); - expect(events.whereType().length, 1); - }); - - testWidgets('already saved Omi retains has_omi_device without a new pair', (tester) async { - await SharedPreferencesUtil().btDeviceSet(BtDevice(id: 'saved-omi', name: 'Omi', type: DeviceType.omi, rssi: -40)); - final props = {}; - await tester.pumpWidget(host( - OnboardingCompleteScreen(onComplete: () {}, emit: (_) {}, setProperty: (key, value) => props[key] = value))); - await tester.tap(find.byKey(const Key('onboarding_complete_start'))); - expect(props['has_omi_device'], isTrue); - }); - - testWidgets('pair failure stays on Connect with How to pair; explicit Back returns to choices', (tester) async { - final pairing = _RenderPairingProvider(); - var completed = 0; - await tester.pumpWidget(MultiProvider(providers: [ - ChangeNotifierProvider.value(value: pairing), - ChangeNotifierProvider(create: (_) => AuditDeviceProvider()), - ChangeNotifierProvider(create: (_) => _RenderHomeProvider()), - ], child: host(OnboardingCompleteScreen(onComplete: () => completed++, emit: (_) {}, setProperty: (_, value) {})))); - await tester.tap(find.byKey(const Key('first_capture_omi'))); - await tester.pump(); - await tester.pump(const Duration(milliseconds: 500)); - pairing.pairingFailureReason = DeviceConnectFailureShownFailureReason.unknown; - pairing.notifyError('DEVICE_CONNECT_FAILED'); - await tester.pump(); - await tester.pump(const Duration(milliseconds: 500)); - expect(find.byType(ConnectDevicePage), findsOneWidget); - expect(find.text('An error occurred. Please try again.'), findsOneWidget); - expect(find.text('How to pair'), findsOneWidget); - expect(completed, 0); - await tester.tap(find.text('How to pair')); - await tester.pump(); - await tester.pump(const Duration(milliseconds: 500)); - expect(find.byType(ConnectionGuideSheet), findsOneWidget); - Navigator.of(tester.element(find.byType(ConnectionGuideSheet))).pop(); - await tester.pump(); - await tester.pump(const Duration(milliseconds: 500)); - expect(find.byType(ConnectionGuideSheet), findsNothing); - await tester.tap(find.byType(OmiBackButton)); - await tester.pumpAndSettle(); - expect(find.byType(ConnectDevicePage), findsNothing); - expect(find.byKey(const Key('first_capture_phone')), findsOneWidget); - expect(completed, 0); - await tester.pumpWidget(const SizedBox()); - }); - - testWidgets('Watch routes to instructions only on iOS; Back returns to choices', (tester) async { - final events = []; - await tester.pumpWidget(host(OnboardingCompleteScreen( - onComplete: () {}, - emit: events.add, - setProperty: (_, value) {}, - watchStateProbe: () async => (paired: true, installed: false)))); - await tester.tap(find.byKey(const Key('first_capture_watch'))); - await tester.pumpAndSettle(); - expect(find.byType(FirstCaptureWatchInstructions), findsOneWidget); - expect(events.whereType().single.properties, {'choice': 'apple_watch'}); - await tester.tap(find.byType(OmiBackButton)); - await tester.pumpAndSettle(); - expect(find.byKey(const Key('first_capture_phone')), findsOneWidget); - await tester.pumpWidget(host(OnboardingCompleteScreen(onComplete: () {}, emit: (_) {}, setProperty: (_, value) {}), - platform: TargetPlatform.android)); - await tester.pumpAndSettle(); - expect(find.byKey(const Key('first_capture_watch')), findsNothing); - }); - - testWidgets('automatic finish at 60 seconds; leaving cancels only owned attempt', (tester) async { - var now = DateTime(2026); - final io = FakeCaptureIO(); - final flow = FirstCaptureController(io, emit: (_) {}, now: () => now); - await flow.start(); - now = now.add(const Duration(seconds: 60)); - await tester.pump(const Duration(seconds: 60)); - expect(io.finishes, 1); - expect(flow.phase, FirstCapturePhase.delayed); - await flow.start(); - await flow.leave(); - expect(io.cancels, 1); - flow.dispose(); - }); - - testWidgets('Back from phone capture cancels the attempt and returns to the persisted choice screen', (tester) async { - final io = FakeCaptureIO(); - final flow = FirstCaptureController(io, emit: (_) {}); - await tester.pumpWidget(host(OnboardingCompleteScreen( - onComplete: () {}, emit: (_) {}, setProperty: (_, value) {}, phoneControllerFactory: () => flow))); - await tester.tap(find.byKey(const Key('first_capture_phone'))); - await tester.pumpAndSettle(); - await tester.tap(find.byKey(const Key('first_capture_primary'))); - await tester.pump(); - await tester.tap(find.byType(OmiBackButton)); - await tester.pumpAndSettle(); - expect(find.text('Leave This Recording?'), findsOneWidget); - await tester.tap(find.text('Keep Recording')); - await tester.pumpAndSettle(); - expect(io.cancels, 0); - await tester.tap(find.byType(OmiBackButton)); - await tester.pumpAndSettle(); - await tester.tap(find.text('Stop Recording')); - await tester.pumpAndSettle(); - expect(io.cancels, 1); - expect(find.byKey(const Key('first_capture_phone')), findsOneWidget); - }); - - testWidgets('observed speech plus missing result is delayed; start failures allow retry', (tester) async { - var now = DateTime(2026); - final io = FakeCaptureIO()..failStart = true; - final events = []; - final flow = FirstCaptureController(io, emit: events.add, now: () => now); - await flow.start(); - expect(flow.phase, FirstCapturePhase.error); - expect(events, isEmpty); - io.failStart = false; - await flow.start(); - io.transcript = 'My goal today is to finish the launch.'; - now = now.add(const Duration(seconds: 30)); - await tester.pump(const Duration(seconds: 30)); - await flow.finish(); - expect(flow.phase, FirstCapturePhase.delayed); - expect(events.whereType(), isEmpty); - flow.dispose(); - }); - - testWidgets( - 'null Finish is delayed even with no transcript; late speech is observed; Back during processing never cancels', - (tester) async { - final io = FakeCaptureIO()..processing = Completer(); - final events = []; - final flow = FirstCaptureController(io, emit: events.add); - await tester.pumpWidget(host(FirstCapturePage(controller: flow, onComplete: () {}, onSkip: () {}))); - await flow.start(); - final pending = flow.finish(); - await tester.pump(); - await flow.leave(); - expect(io.cancels, 0); - io.processing!.complete(null); - await pending; - flow.dispose(); - final lateIO = FakeCaptureIO()..processing = Completer(); - final late = FirstCaptureController(lateIO, emit: events.add); - await tester.pumpWidget(host(FirstCapturePage(controller: late, onComplete: () {}, onSkip: () {}))); - await late.start(); - final finishing = late.finish(); - lateIO.transcript = 'Speech arrived during processing'; - lateIO.processing!.complete(null); - await finishing; - await tester.pump(); - expect(late.phase, FirstCapturePhase.delayed); - expect(late.transcriptObserved, isTrue); - expect( - find.text('Your recording is taking longer to process. You can check your conversations on Home or try again.'), - findsOneWidget); - expect(events.whereType(), isEmpty); - late.dispose(); - }); - - testWidgets('only a terminal discarded or completed empty conversation gets insufficient-speech feedback', - (tester) async { - for (final discarded in [true, false]) { - final server = exampleConversation(); - final terminal = ServerConversation( - id: server.id, - createdAt: server.createdAt, - structured: server.structured, - discarded: discarded, - transcriptSegments: discarded ? server.transcriptSegments : []); - final events = []; - final flow = FirstCaptureController(FakeCaptureIO()..result = terminal, emit: events.add); - await flow.start(); - await flow.finish(); - expect(flow.phase, FirstCapturePhase.empty); - expect(events.whereType().length, 1); - flow.dispose(); - } - final processing = exampleConversation()..status = ConversationStatus.processing; - final flow = FirstCaptureController(FakeCaptureIO()..result = processing, emit: (_) {}); - await flow.start(); - await flow.finish(); - expect(flow.phase, FirstCapturePhase.delayed); - flow.dispose(); - }); - - testWidgets('null Finish without speech renders delayed copy and never emits Empty Result', (tester) async { - final events = []; - final flow = FirstCaptureController(FakeCaptureIO(), emit: events.add); - await tester.pumpWidget(host(FirstCapturePage(controller: flow, onComplete: () {}, onSkip: () {}))); - await flow.start(); - await flow.finish(); - await tester.pump(); - expect(flow.phase, FirstCapturePhase.delayed); - expect( - find.text('Your recording is taking longer to process. You can check your conversations on Home or try again.'), - findsOneWidget); - expect(events.whereType(), isEmpty); - flow.dispose(); - }); - - testWidgets('Siri tip stays hidden below its version gate or without App Shortcuts availability', (tester) async { - for (final gate in [true, false]) { - final flow = FirstCaptureController(FakeCaptureIO(), emit: (_) {}); - flow.phase = FirstCapturePhase.result; - flow.conversation = exampleConversation(); - await tester.pumpWidget(host(FirstCapturePage( - controller: flow, - onComplete: () {}, - onSkip: () {}, - showSiriHint: gate, - appShortcutsAvailabilityProbe: () async => false))); - await tester.pump(); - expect(find.byKey(const Key('onboarding_siri_hint')), findsNothing); - await tester.pumpWidget(const SizedBox()); - flow.dispose(); - } - }); - - testWidgets('result and empty actions stay at the bottom, including large text and Siri tip', (tester) async { - tester.view.physicalSize = const Size(393, 852); - tester.view.devicePixelRatio = 1; - addTearDown(tester.view.resetPhysicalSize); - addTearDown(tester.view.resetDevicePixelRatio); - final flow = FirstCaptureController(FakeCaptureIO(), emit: (_) {}); - flow.phase = FirstCapturePhase.result; - flow.conversation = exampleConversation(); - await tester.pumpWidget(host(FirstCapturePage( - controller: flow, - onComplete: () {}, - onSkip: () {}, - showSiriHint: true, - appShortcutsAvailabilityProbe: () async => true))); - await tester.pump(); - expect(tester.getRect(find.byKey(const Key('first_capture_primary'))).bottom, greaterThan(800)); - flow.phase = FirstCapturePhase.empty; - await tester.pumpWidget(host(MediaQuery( - data: const MediaQueryData(textScaler: TextScaler.linear(2)), - child: FirstCapturePage(controller: flow, onComplete: () {}, onSkip: () {})))); - await tester.pump(); - expect(find.text("Let's try that again"), findsOneWidget); - expect(find.byKey(const Key('first_capture_skip')).hitTestable(), findsOneWidget); - expect(tester.takeException(), isNull); - flow.dispose(); - }); - - test('restored tracker emits no auth exit and records the actual Back reason', () { - final events = []; - final tracker = - MobileOnboardingStepTelemetry(emit: events.add, platform: () => OnboardingStepCompletedPlatform.ios); - tracker.seed(9); - tracker.enter(9); - expect(events, isEmpty); - tracker.exitReason = OnboardingStepCompletedExitReason.back; - tracker.enter(6); - expect(events.single.properties['step'], 'first_capture'); - expect(events.single.properties['exit_reason'], 'back'); - }); - - testWidgets('guided capture owns info notices and restores them after disposal; Siri tip is gated', (tester) async { - final flow = FirstCaptureController(FakeCaptureIO(), emit: (_) {}); - flow.phase = FirstCapturePhase.result; - flow.conversation = exampleConversation(); - await tester.pumpWidget(host(FirstCapturePage( - controller: flow, - onComplete: () {}, - onSkip: () {}, - showSiriHint: true, - appShortcutsAvailabilityProbe: () async => true))); - await tester.pump(); - expect(find.byKey(const Key('onboarding_siri_hint')), findsOneWidget); - OmiFeedback.info(tester.element(find.byType(FirstCapturePage)), 'Generic empty recording notice'); - await tester.pump(); - expect(find.byType(SnackBar), findsNothing); - await tester.pumpWidget(host(const SizedBox())); - await tester.pump(); - OmiFeedback.info(tester.element(find.byType(Scaffold)), 'Generic empty recording notice'); - await tester.pump(); - expect(find.byType(SnackBar), findsOneWidget); - flow.dispose(); - }); - - test('persisted completion resumes without moving indices or breaking prior saves', () async { - SharedPreferences.setMockInitialValues({}); - await SharedPreferencesUtil.init(); - const key = 'onboarding/resumeStep/first-capture-test'; - await SharedPreferencesUtil().saveInt(key, 9); - expect(OnboardingWrapper.resumeStepForTesting(SharedPreferencesUtil().getInt(key)), 9); - expect(OnboardingWrapper.resumeStepForTesting(8), 8); - expect(OnboardingWrapper.resumeStepForTesting(6), 6); - expect(OnboardingWrapper.resumeStepForTesting(7), 9); - expect(OnboardingWrapper.resumeStepForTesting(10), 2); - expect(OnboardingWrapper.resumeStepForTesting(-1), 2); - }); - - test('step exits emit once, time every visit, and preserve explicit skips and Back', () { - var ms = 0; - final events = []; - final tracker = MobileOnboardingStepTelemetry( - emit: events.add, platform: () => OnboardingStepCompletedPlatform.ios, elapsedMs: () => ms); - ms = 25; - tracker.enter(1); - tracker.enter(1); - expect(events.length, 1); - tracker.enter(9); - ms = 100; - tracker.skipped = true; - tracker.exit(); - tracker.exit(); - expect(events.last.properties, { - 'step': 'first_capture', - 'index': 9, - 'elapsed_ms': 75, - 'skipped': true, - 'platform': 'ios', - 'exit_reason': 'skipped' - }); - tracker.enter(6); - tracker.enter(9); - ms = 120; - tracker.exit(); - expect(events.last.properties['skipped'], isFalse); - expect(events.last.properties['elapsed_ms'], 20); - }); - - test('completion index remains resumable with both back destinations', () { - expect(OnboardingProgressStepsForTest.completePage, 9); - expect(OnboardingWrapper.isResumableStepForTesting(9), isTrue); - expect( - OnboardingWrapper.previousStepForTesting( - currentIndex: 9, setupPageEnabled: false, knowledgeGraphStepEnabled: false), - 6); - expect( - OnboardingWrapper.previousStepForTesting( - currentIndex: 9, setupPageEnabled: true, knowledgeGraphStepEnabled: false), - 8); - for (final step in OnboardingStepCompletedStep.values) { - final event = OnboardingStepCompleted( - step: step, - index: step.index, - elapsedMs: 23, - skipped: true, - platform: OnboardingStepCompletedPlatform.android, - exitReason: OnboardingStepCompletedExitReason.skipped); - expect(event.properties.keys.toSet(), {'step', 'index', 'elapsed_ms', 'skipped', 'platform', 'exit_reason'}); - expect(event.properties['skipped'], isTrue); - } - }); - - testWidgets('render first-capture screens with real fonts on iOS and Android', (tester) async { - if (Platform.environment['FIRST_CAPTURE_RENDER'] != '1') return; - await tester.runAsync(() async { - final manifest = jsonDecode(await rootBundle.loadString('FontManifest.json')) as List; - for (final family in manifest) { - final loader = FontLoader(family['family'] as String); - for (final font in family['fonts'] as List) { - loader.addFont(rootBundle.load(font['asset'] as String)); - } - await loader.load(); - } - }); - await tester.runAsync(() async { - for (final family in ['.SF UI Text', '.SF UI Display', '.SF Pro Text', 'CupertinoSystemText']) { - final loader = FontLoader(family); - loader.addFont(rootBundle.load('assets/fonts/SFPRODISPLAYREGULAR.OTF')); - await loader.load(); - } - }); - await tester.runAsync(() async { - var dir = File(Platform.resolvedExecutable).parent; - Directory? fonts; - for (var i = 0; i < 8; i++) { - final candidate = Directory('${dir.path}/material_fonts'); - if (File('${candidate.path}/Roboto-Regular.ttf').existsSync()) { - fonts = candidate; - break; - } - dir = dir.parent; - } - expect(fonts, isNotNull); - final loader = FontLoader('Roboto'); - for (final weight in ['Regular', 'Medium', 'Bold']) { - loader.addFont(Future.value(ByteData.sublistView(File('${fonts!.path}/Roboto-$weight.ttf').readAsBytesSync()))); - } - await loader.load(); - }); - SharedPreferences.setMockInitialValues({}); - await SharedPreferencesUtil.init(); - tester.view.physicalSize = const Size(1179, 2556); - tester.view.devicePixelRatio = 3; - addTearDown(tester.view.resetPhysicalSize); - addTearDown(tester.view.resetDevicePixelRatio); - const boundary = Key('capture-render'); - for (final platform in [TargetPlatform.iOS, TargetPlatform.android]) { - debugDefaultTargetPlatformOverride = platform; - addTearDown(() => debugDefaultTargetPlatformOverride = null); - Future render(String name, Widget child, {Future Function()? interact}) async { - await tester.pumpWidget(const SizedBox()); - await tester.pump(); - await tester.pumpWidget(RepaintBoundary(key: boundary, child: host(child, platform: platform))); - await tester.runAsync(() => Future.delayed(const Duration(milliseconds: 100))); - await tester.pump(const Duration(milliseconds: 500)); - if (interact != null) { - await interact(); - await tester.pump(); - await tester.pump(const Duration(milliseconds: 350)); - if (name == 'omi-failure') { - // MessageListener publishes feedback after its notification frame. - await tester.pump(const Duration(milliseconds: 350)); - expect(find.byType(ConnectDevicePage), findsOneWidget); - expect(find.text('How to pair'), findsOneWidget); - } else { - expect(find.byType(OmiAlertDialog), findsOneWidget, reason: name); - expect( - find.descendant(of: find.byKey(const Key('first_capture_primary')), matching: find.byType(OmiSpinner)), - findsNothing); - } - } - expect(tester.takeException(), isNull, reason: name); - await tester.runAsync(() async { - final image = tester.element(find.byKey(boundary)).renderObject! as RenderRepaintBoundary; - final png = await (await image.toImage(pixelRatio: 3)).toByteData(format: ui.ImageByteFormat.png); - const states = [ - 'choice', - 'recording-5s', - 'recording-30s', - 'processing', - 'result', - 'empty', - 'omi-handoff', - 'watch-instructions' - ]; - const extra = {'delayed': 16, 'short-warning': 18, 'back-confirmation': 20, 'omi-failure': 22}; - final number = extra.containsKey(name) - ? extra[name]! + (platform == TargetPlatform.android ? 1 : 0) - : states.indexOf(name) + 1 + (platform == TargetPlatform.android ? 8 : 0); - final ordinal = number.toString().padLeft(3, '0'); - final tag = platform == TargetPlatform.iOS ? 'mobile-ios' : 'mobile-android'; - final file = File('../.agent-artifacts/ui-evidence/onb-m-firstcapture/$ordinal-$tag-first-capture-$name.png'); - file.parent.createSync(recursive: true); - file.writeAsBytesSync(png!.buffer.asUint8List()); - }); - } - - await render( - 'choice', - OnboardingStepLayout( - reserveHeader: false, - onBack: () {}, - child: OnboardingCompleteScreen(onComplete: () {}, emit: (_) {}, setProperty: (_, value) {}))); - final flow = FirstCaptureController(FakeCaptureIO(), emit: (_) {}); - Widget page() => FirstCapturePage( - controller: flow, - onComplete: () {}, - onSkip: () {}, - showSiriHint: platform == TargetPlatform.iOS, - appShortcutsAvailabilityProbe: () async => true); - flow.phase = FirstCapturePhase.recording; - flow.seconds = 5; - await render('recording-5s', page()); - flow.seconds = 30; - flow.transcript = 'Today I’m working on the launch. I want to finish onboarding and share it with my team.'; - await render('recording-30s', page()); - flow.phase = FirstCapturePhase.processing; - await render('processing', page()); - flow.phase = FirstCapturePhase.result; - flow.conversation = exampleConversation(); - await render('result', page()); - flow.phase = FirstCapturePhase.empty; - await render('empty', page()); - if (platform == TargetPlatform.iOS) { - await render( - 'watch-instructions', FirstCaptureWatchInstructions(paired: true, installed: true, onComplete: () {})); - } - await render( - 'omi-handoff', - MultiProvider(providers: [ - ChangeNotifierProvider.value(value: _RenderPairingProvider()), - ChangeNotifierProvider(create: (_) => AuditDeviceProvider()), - ChangeNotifierProvider(create: (_) => _RenderHomeProvider()), - ], child: ConnectDevicePage(onConnected: () {}))); - flow.phase = FirstCapturePhase.delayed; - await render('delayed', page()); - flow.phase = FirstCapturePhase.recording; - flow.seconds = 5; - flow.transcript = ''; - await render('short-warning', page(), interact: () => tester.tap(find.byKey(const Key('first_capture_primary')))); - await render('back-confirmation', page(), interact: () => tester.tap(find.byType(OmiBackButton))); - flow.dispose(); - final pairing = _RenderPairingProvider(); - await render( - 'omi-failure', - MultiProvider(providers: [ - ChangeNotifierProvider.value(value: pairing), - ChangeNotifierProvider(create: (_) => AuditDeviceProvider()), - ChangeNotifierProvider(create: (_) => _RenderHomeProvider()), - ], child: ConnectDevicePage(onConnected: () {})), interact: () async { - pairing.pairingFailureReason = DeviceConnectFailureShownFailureReason.unknown; - pairing.notifyError('DEVICE_CONNECT_FAILED'); - }); - await tester.pumpWidget(const SizedBox()); - } - debugDefaultTargetPlatformOverride = null; - }); -} - -class _RenderPairingProvider extends OnboardingProvider { - @override - Future scanDevices({required VoidCallback onShowDialog, VoidCallback? onShowLocationDialog}) async {} - @override - void cancelActiveScan() {} -} - -class _RenderHomeProvider extends HomeProvider { - @override - Future setupHasSpeakerProfile() async {} -} diff --git a/app/test/widgets/grow_route_test.dart b/app/test/widgets/grow_route_test.dart new file mode 100644 index 00000000000..4d7bfde2b72 --- /dev/null +++ b/app/test/widgets/grow_route_test.dart @@ -0,0 +1,86 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'package:omi/pages/home/widgets/grow_route.dart'; + +// The live transcript grows out of Home's record pill: it starts as the pill's shape, widens to +// the screen, runs back into the pill on close, and still leaves with the iOS edge swipe. +void main() { + final pillKey = GlobalKey(); + const pillRect = Rect.fromLTWH(60, 700, 300, 56); + + Future pump(WidgetTester tester) async { + tester.view.physicalSize = const Size(400, 800); + tester.view.devicePixelRatio = 1; + addTearDown(tester.view.reset); + await tester.pumpWidget(MaterialApp( + home: Builder( + builder: (context) => Scaffold( + body: Stack(children: [ + Positioned.fromRect( + rect: pillRect, + child: GestureDetector( + key: pillKey, + onTap: () => pushGrowingFrom( + context, + from: pillKey, + page: const Scaffold(body: Center(child: Text('transcript'))), + ), + child: const ColoredBox(color: Colors.white), + ), + ), + ]), + ), + ), + )); + } + + Rect clip(WidgetTester tester) { + final render = tester.renderObject( + find + .ancestor( + of: find.text('transcript', skipOffstage: false), matching: find.byType(ClipRRect, skipOffstage: false)) + .first, + ); + return render.clipper!.getClip(render.size).outerRect; + } + + testWidgets('opens from the pill, widens to the screen, and closes back into it', (tester) async { + await pump(tester); + await tester.tap(find.byKey(pillKey)); + await tester.pump(); + // The first frame is the offstage measuring pass; the transition starts on the next one. + await tester.pump(); + expect(clip(tester), pillRect, reason: 'the first frame is the pill itself'); + + await tester.pump(const Duration(milliseconds: 230)); + final mid = clip(tester); + expect(mid.width, greaterThan(pillRect.width)); + expect(mid.height, greaterThan(pillRect.height)); + expect(mid.height, lessThan(800)); + + await tester.pumpAndSettle(); + expect(find.text('transcript'), findsOneWidget); + + Navigator.of(tester.element(find.text('transcript'))).pop(); + await tester.pump(); + await tester.pump(const Duration(milliseconds: 200)); + expect(clip(tester).height, lessThan(800), reason: 'it shrinks back toward the pill'); + await tester.pumpAndSettle(); + expect(find.text('transcript'), findsNothing); + }); + + testWidgets('the iOS edge swipe still closes it', (tester) async { + await pump(tester); + await tester.tap(find.byKey(pillKey)); + await tester.pumpAndSettle(); + + final gesture = await tester.startGesture(const Offset(5, 400)); + await gesture.moveBy(const Offset(300, 0)); + await tester.pump(); + await gesture.up(); + await tester.pumpAndSettle(); + expect(find.text('transcript'), findsNothing); + }, variant: TargetPlatformVariant.only(TargetPlatform.iOS)); +} diff --git a/app/test/widgets/home_empty_conversations_test.dart b/app/test/widgets/home_empty_conversations_test.dart index d77f83a4975..8188e85ded0 100644 --- a/app/test/widgets/home_empty_conversations_test.dart +++ b/app/test/widgets/home_empty_conversations_test.dart @@ -44,7 +44,7 @@ void main() { final icon = find.byIcon(Icons.forum_rounded); expect(title, findsOneWidget); expect(hint, findsNothing); - expect(icon, findsOneWidget); + expect(icon, findsNothing); expect(find.text(l10n.tasksEmptyStateMessage), findsNothing); expect(find.bySemanticsLabel(l10n.tasksEmptyStateMessage), findsNothing); expect(find.bySemanticsLabel(l10n.noConversationsHeroMessage), findsNothing); @@ -57,7 +57,7 @@ void main() { expect(find.byKey(const ValueKey('omi.conversations.empty')), findsOneWidget); expect(title, findsOneWidget); expect(hint, findsNothing); - expect(icon, findsOneWidget); + expect(icon, findsNothing); expect(tester.getRect(title), initialTitleRect); expect(tester.widget(title).style!.color, OmiColors.textPrimary); expect(IconTheme.of(tester.element(icon)).color, OmiColors.textTertiary); @@ -73,7 +73,7 @@ void main() { await tester.tap(find.text(l10n.retry)); await tester.pump(); expect(provider.apiViewState.phase, ApiViewPhase.empty); - expect(icon, findsOneWidget); + expect(icon, findsNothing); expect(hint, findsNothing); expect(tester.getRect(title), initialTitleRect); diff --git a/app/test/widgets/memory_graph_fit_to_bounds_test.dart b/app/test/widgets/memory_graph_fit_to_bounds_test.dart index ae0f85ff2ee..85d4d8691a9 100644 --- a/app/test/widgets/memory_graph_fit_to_bounds_test.dart +++ b/app/test/widgets/memory_graph_fit_to_bounds_test.dart @@ -11,20 +11,20 @@ import 'package:omi/pages/onboarding/knowledge_graph_step.dart'; import 'package:omi/utils/platform/platform_manager.dart'; Future> _sampleFiveNodeGraph() async => { - 'nodes': [ - {'id': '1', 'label': 'Sam', 'node_type': 'user'}, - {'id': '2', 'label': 'Coffee', 'node_type': 'thing'}, - {'id': '3', 'label': 'Work', 'node_type': 'thing'}, - {'id': '4', 'label': 'Alice', 'node_type': 'person'}, - {'id': '5', 'label': 'Gym', 'node_type': 'place'}, - ], - 'edges': [ - {'source_id': '1', 'target_id': '2', 'label': 'drinks'}, - {'source_id': '1', 'target_id': '3', 'label': 'works_at'}, - {'source_id': '1', 'target_id': '4', 'label': 'knows'}, - {'source_id': '1', 'target_id': '5', 'label': 'goes_to'}, - ] - }; + 'nodes': [ + {'id': '1', 'label': 'Sam', 'node_type': 'user'}, + {'id': '2', 'label': 'Coffee', 'node_type': 'thing'}, + {'id': '3', 'label': 'Work', 'node_type': 'thing'}, + {'id': '4', 'label': 'Alice', 'node_type': 'person'}, + {'id': '5', 'label': 'Gym', 'node_type': 'place'}, + ], + 'edges': [ + {'source_id': '1', 'target_id': '2', 'label': 'drinks'}, + {'source_id': '1', 'target_id': '3', 'label': 'works_at'}, + {'source_id': '1', 'target_id': '4', 'label': 'knows'}, + {'source_id': '1', 'target_id': '5', 'label': 'goes_to'}, + ] +}; Widget _app(Widget home) { return MaterialApp( diff --git a/app/test/widgets/offline_sync_storage_contrast_test.dart b/app/test/widgets/offline_sync_storage_contrast_test.dart index b8ace53e2d5..5a4779f3353 100644 --- a/app/test/widgets/offline_sync_storage_contrast_test.dart +++ b/app/test/widgets/offline_sync_storage_contrast_test.dart @@ -125,7 +125,7 @@ void main() { final title = find.text('No conversations yet'); expect(title.hitTestable(), findsOneWidget); expect(contrast(tester.widget(title).style!.color!, OmiColors.surface0), greaterThanOrEqualTo(4.5)); - expect(find.byIcon(Icons.forum_rounded), findsOneWidget); + expect(find.byIcon(Icons.forum_rounded), findsNothing); }); } testWidgets('nonzero counts retain category and clear-all callbacks', (tester) async { diff --git a/app/test/widgets/onboarding_activation_flow_test.dart b/app/test/widgets/onboarding_activation_flow_test.dart new file mode 100644 index 00000000000..2ae139e6dcc --- /dev/null +++ b/app/test/widgets/onboarding_activation_flow_test.dart @@ -0,0 +1,330 @@ +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +import 'package:omi/backend/preferences.dart'; +import 'package:omi/l10n/app_localizations.dart'; +import 'package:omi/pages/home/widgets/home_tab_switcher.dart'; +import 'package:omi/pages/onboarding/activation/activation_capture.dart'; +import 'package:omi/pages/onboarding/activation/activation_flow.dart'; + +class _FakeCapture implements ActivationCapture { + _FakeCapture(this.result, {this.startsAs = ActivationStart.listening, this.device = false}); + + final ActivationResult result; + final ActivationStart startsAs; + final bool device; + final ValueNotifier live = ValueNotifier(''); + bool cancelled = false; + + @override + bool get usesDevice => device; + + @override + ValueListenable get liveText => live; + + final ValueNotifier levelNotifier = ValueNotifier(0); + + @override + ValueListenable get level => levelNotifier; + + @override + Future start() async => startsAs; + + @override + Future finish({Duration timeout = const Duration(seconds: 30)}) async => result; + + @override + Future cancel() async => cancelled = true; +} + +/// Omi never finishes its notes (a slow network or a slow server). +class _SlowCapture extends _FakeCapture { + _SlowCapture() : super(const ActivationResult(transcript: '')); + + @override + Future finish({Duration timeout = const Duration(seconds: 30)}) => + Completer().future; +} + +void main() { + setUp(() async { + SharedPreferences.setMockInitialValues({}); + await SharedPreferencesUtil.init(); + }); + + const notes = ActivationResult( + transcript: 'Remind me to send the deck', + emoji: '🥗', + title: 'Lunch with Sam', + overview: 'Sam is moving to Lisbon in June.', + tasks: ['Send the deck to the team by Friday', 'Grab milk on the way home'], + ); + + Future<({_FakeCapture capture, List events})> pump( + WidgetTester tester, { + bool hasDevice = false, + ActivationResult result = notes, + Completer? paywall, + _FakeCapture Function()? capture, + Future Function()? requestMicrophone, + Future Function()? finish, + }) async { + final captures = <_FakeCapture>[]; + _FakeCapture next() { + final made = (captures.isEmpty ? capture?.call() : null) ?? _FakeCapture(result); + captures.add(made); + return made; + } + + final first = next(); + var used = false; + final events = []; + await tester.pumpWidget( + MaterialApp( + localizationsDelegates: AppLocalizations.localizationsDelegates, + supportedLocales: const [Locale('en')], + home: Scaffold( + body: ActivationFlow( + hasDevice: hasDevice, + capture: (_) { + if (!used) { + used = true; + return first; + } + return next(); + }, + microphoneGranted: () async => true, + requestMicrophone: requestMicrophone, + requestReview: () async => events.add('review'), + showPaywall: () { + events.add('paywall'); + return paywall?.future ?? Future.value(); + }, + onFinished: () async { + events.add('finished'); + return finish == null ? true : await finish(); + }, + ), + ), + ), + ); + return (capture: first, events: events); + } + + Future tapKey(WidgetTester tester, String key) async { + await tester.tap(find.byKey(Key(key))); + await tester.pump(const Duration(seconds: 5)); + } + + testWidgets('the full path: intro, dictation, notes, tasks, hooray, rating, paywall, Home', (tester) async { + final paywall = Completer(); + final run = await pump(tester, paywall: paywall); + + expect(find.text("Let's see Omi at work."), findsNothing, reason: 'words appear one by one'); + await tester.pump(const Duration(seconds: 4)); // Moves on by itself, straight to the dictation. + + // Dictation: the check stays off until Omi has heard something. + expect(find.text('Tell Omi about your day'), findsOneWidget); + await tester.tap(find.byKey(const Key('activation_dictation_done'))); + await tester.pump(const Duration(seconds: 1)); + expect(find.text('Tell Omi about your day'), findsOneWidget, reason: 'nothing heard yet'); + run.capture.live.value = 'Remind me to send the deck'; + await tester.pump(); + await tapKey(tester, 'activation_dictation_done'); + + expect(find.text('Your day, summarized'), findsOneWidget); + expect(find.text('Lunch with Sam'), findsOneWidget); + expect(find.text('Just now'), findsOneWidget); + await tapKey(tester, 'activation_continue_summary'); + + expect(find.text('Omi caught your to-dos'), findsOneWidget); + expect(find.text('Grab milk on the way home'), findsOneWidget); + expect(find.text('Saved to your tasks'), findsOneWidget); + expect(NewTasksDot.showing, isTrue, reason: 'Home marks Tasks until it is opened'); + await tapKey(tester, 'activation_continue_tasks'); + + // Hooray, then the rating ask, then the paywall. + expect(find.byKey(const Key('activation_continue_congrats')), findsOneWidget); + expect(run.events, isEmpty); + await tapKey(tester, 'activation_continue_congrats'); + await tester.pump(const Duration(seconds: 2)); // The pop-up waits for the stars. + expect(find.text('Enjoying Omi so far?'), findsOneWidget); + expect(run.events, ['review'], reason: 'the App Store pop-up opens by itself'); + // The stars sit right under the title, above where the pop-up opens. + final titleBottom = tester.getBottomLeft(find.text('Enjoying Omi so far?')).dy; + final starsTop = tester.getTopLeft(find.byIcon(Icons.star_rounded).first).dy; + expect(starsTop - titleBottom, lessThan(40)); + + // The pop-up makes the app inactive; its closing opens the paywall, and closing that goes Home. + tester.binding.handleAppLifecycleStateChanged(AppLifecycleState.inactive); + tester.binding.handleAppLifecycleStateChanged(AppLifecycleState.resumed); + await tester.pump(); + expect(run.events, ['review', 'paywall']); + paywall.complete(); + await tester.pump(); + expect(run.events, ['review', 'paywall', 'finished']); + }); + + testWidgets('rating: once the App Store pop-up is up, it moves on to the paywall by itself', (tester) async { + final run = await pump(tester); + await tester.pump(const Duration(seconds: 4)); + run.capture.live.value = 'Remind me to send the deck'; + await tester.pump(); + await tapKey(tester, 'activation_dictation_done'); + await tapKey(tester, 'activation_continue_summary'); + await tapKey(tester, 'activation_continue_tasks'); + await tapKey(tester, 'activation_continue_congrats'); + + // A tap before the pop-up has been asked for stays on the rating screen. + await tester.tapAt(tester.getCenter(find.text('Enjoying Omi so far?'))); + await tester.pump(); + expect(run.events, isEmpty); + + await tester.pump(const Duration(seconds: 2)); + expect(run.events, ['review'], reason: 'the pop-up is asked for, not yet on screen'); + + // iOS gives no cue when its pop-up closes: the paywall opens underneath it instead. + await tester.pump(const Duration(seconds: 1)); + expect(run.events, ['review', 'paywall', 'finished']); + }); + + testWidgets('Omi owners go straight to the intro (how to wear it is the button tutorial before it)', (tester) async { + await pump(tester, hasDevice: true); + await tester.pump(const Duration(seconds: 1)); + expect(find.text('Wear your Omi'), findsNothing); + await tester.pump(const Duration(seconds: 5)); + }); + + testWidgets('when the Omi is not streaming, the microphone is asked for before the dictation', (tester) async { + var asked = false; + await pump( + tester, + hasDevice: true, + capture: () => _FakeCapture(notes, startsAs: ActivationStart.needsMicrophone), + requestMicrophone: () async => asked = true, + ); + await tester.pump(const Duration(seconds: 4)); + await tester.pump(const Duration(seconds: 1)); + expect(find.text("Your Omi isn't sending audio yet. Use your phone for now."), findsOneWidget); + await tapKey(tester, 'activation_continue_microphone'); + expect(asked, isTrue); + expect(find.text('Tell Omi about your day'), findsOneWidget, reason: 'listening again, through the phone'); + await tester.pumpWidget(const SizedBox()); + await tester.pump(const Duration(seconds: 13)); + }); + + testWidgets('after a quiet stretch the dictation says it cannot hear the Omi', (tester) async { + await pump(tester, hasDevice: true, capture: () => _FakeCapture(notes, device: true)); + await tester.pump(const Duration(seconds: 4)); + await tester.pump(const Duration(seconds: 13)); + expect(find.text("Can't hear your Omi. Make sure it's on and close by."), findsOneWidget); + await tester.pumpWidget(const SizedBox()); + await tester.pump(const Duration(seconds: 1)); + }); + + testWidgets('skipping the dictation leaves a welcome card for Home and goes to the hooray', (tester) async { + final run = await pump(tester); + await tester.pump(const Duration(seconds: 4)); + await tapKey(tester, 'activation_dictation_skip'); + + expect(run.capture.cancelled, isTrue); + expect(SharedPreferencesUtil().getBool(ActivationFlow.welcomeCardKey), isTrue); + expect(find.byKey(const Key('activation_continue_congrats')), findsOneWidget); + await tester.pump(const Duration(seconds: 3)); // Lets the celebration finish. + }); + + testWidgets('when Omi could not make notes, the reader still sees their own words', (tester) async { + final run = await pump(tester, result: const ActivationResult(transcript: 'Just testing this out')); + await tester.pump(const Duration(seconds: 4)); + run.capture.live.value = 'Just testing this out'; + await tester.pump(); + await tapKey(tester, 'activation_dictation_done'); + + expect(find.text("Here's what you said"), findsOneWidget); + await tapKey(tester, 'activation_continue_summary'); + expect(find.byKey(const Key('activation_continue_congrats')), findsOneWidget, reason: 'no tasks step'); + await tester.pump(const Duration(seconds: 3)); // Lets the celebration finish. + }); + + testWidgets('slow notes: after the deadline the reader moves on with their words, told notes are coming', + (tester) async { + final run = await pump(tester, capture: _SlowCapture.new); + await tester.pump(const Duration(seconds: 4)); + run.capture.live.value = 'Lunch with Sam today'; + await tester.pump(); + await tester.tap(find.byKey(const Key('activation_dictation_done'))); + await tester.pump(const Duration(seconds: 5)); + expect(find.text('Summarizing…'), findsOneWidget, reason: 'still within the deadline'); + + await tester.pump(const Duration(seconds: 8)); // Past the 12 s deadline. + await tester.pump(const Duration(seconds: 5)); + expect(find.text("Here's what you said"), findsOneWidget); + expect(find.text('Lunch with Sam today'), findsOneWidget); + expect(find.textContaining('still writing'), findsNothing, reason: 'no apology line; the notes reach Home'); + expect(find.text("Omi remembers so you don't have to"), findsOneWidget); + await tapKey(tester, 'activation_continue_summary'); + expect(find.byKey(const Key('activation_continue_congrats')), findsOneWidget); + await tester.pump(const Duration(seconds: 3)); + }); + + testWidgets('when listening cannot start, Continue moves on instead of a dead end', (tester) async { + await pump(tester, capture: () => _FakeCapture(notes, startsAs: ActivationStart.failed)); + await tester.pump(const Duration(seconds: 4)); + await tester.pump(); + expect(find.byKey(const Key('activation_dictation_status')), findsOneWidget); + await tapKey(tester, 'activation_continue_dictation'); + expect(find.byKey(const Key('activation_continue_congrats')), findsOneWidget); + await tester.pump(const Duration(seconds: 3)); + }); + + testWidgets('when nothing is heard for a while, Continue appears; late words bring the check back', (tester) async { + final run = await pump(tester); + await tester.pump(const Duration(seconds: 4)); + expect(find.byKey(const Key('activation_continue_dictation')), findsNothing); + await tester.pump(const Duration(seconds: 26)); + expect(find.byKey(const Key('activation_continue_dictation')), findsOneWidget); + + run.capture.live.value = 'Finally some words'; + await tester.pump(); + expect(find.byKey(const Key('activation_continue_dictation')), findsNothing); + expect(find.byKey(const Key('activation_dictation_done')), findsOneWidget); + }); + + testWidgets('a failed final save keeps Continue working; the paywall is not shown twice', (tester) async { + var attempts = 0; + final run = await pump(tester, finish: () async => ++attempts > 1); + await tester.pump(const Duration(seconds: 4)); + await tester.tap(find.text('Skip')); + await tester.pump(const Duration(seconds: 5)); + await tapKey(tester, 'activation_continue_congrats'); + await tester.pump(const Duration(seconds: 2)); + await tester.tap(find.byKey(const Key('activation_continue_rating')), warnIfMissed: false); + await tester.pump(); + expect(run.events, ['review'], reason: 'Continue waits for the App Store prompt'); + await tester.pump(const Duration(seconds: 2)); + expect(run.events, ['review', 'paywall', 'finished'], reason: 'moves on by itself; the first save fails'); + + await tapKey(tester, 'activation_continue_rating'); + expect(run.events, ['review', 'paywall', 'finished', 'finished'], reason: 'retried without a second paywall'); + expect(attempts, 2); + }); + + testWidgets('Omi visibly hears the person: the voice ring and meter follow the level', (tester) async { + final run = await pump(tester); + await tester.pump(const Duration(seconds: 4)); + await tester.pump(); + final ring = find.byKey(const Key('activation_voice_ring')); + expect(ring, findsOneWidget); + expect(find.byKey(const Key('activation_level_meter')), findsOneWidget); + final quiet = tester.getSize(ring).width; + + run.capture.levelNotifier.value = 1; + await tester.pump(); + await tester.pump(const Duration(milliseconds: 200)); + expect(tester.getSize(ring).width, greaterThan(quiet + 40)); + }); +} diff --git a/app/test/widgets/onboarding_bottom_inset_test.dart b/app/test/widgets/onboarding_bottom_inset_test.dart index 1524f271f17..198d4161777 100644 --- a/app/test/widgets/onboarding_bottom_inset_test.dart +++ b/app/test/widgets/onboarding_bottom_inset_test.dart @@ -5,6 +5,7 @@ import 'package:shared_preferences/shared_preferences.dart'; import 'package:omi/backend/preferences.dart'; import 'package:omi/l10n/app_localizations.dart'; import 'package:omi/pages/onboarding/name/name_widget.dart'; +import 'package:omi/pages/onboarding/widgets/onboarding_card.dart'; void main() { setUp(() async { @@ -12,8 +13,8 @@ void main() { await SharedPreferencesUtil.init(); }); - Future cardHeight(WidgetTester tester, double bottomInset) async { - await tester.pumpWidget( + Future pump(WidgetTester tester, double bottomInset, Widget step) { + return tester.pumpWidget( MaterialApp( localizationsDelegates: AppLocalizations.localizationsDelegates, supportedLocales: AppLocalizations.supportedLocales, @@ -23,25 +24,43 @@ void main() { padding: EdgeInsets.only(bottom: bottomInset), viewPadding: EdgeInsets.only(bottom: bottomInset), ), - child: Scaffold(body: NameWidget(key: ValueKey(bottomInset), goNext: () {})), + child: Scaffold(body: KeyedSubtree(key: ValueKey(bottomInset), child: step)), ), ), ), ); - // The bottom card is the Container that wraps the SafeArea. - final card = find.ancestor(of: find.byType(SafeArea), matching: find.byType(Container)).first; - return tester.getSize(card).height; } - testWidgets('an onboarding card reserves the bottom system inset once, not twice', (tester) async { - // The onboarding cards added MediaQuery.padding.bottom to their own padding - // and then wrapped their content in a SafeArea that added it again, leaving - // 2 x 34pt + 8pt = 76pt of dead space under the button on an iPhone. - const homeIndicatorInset = 34.0; + // The onboarding cards once added MediaQuery.padding.bottom to their own padding and then wrapped + // their content in a SafeArea that added it again, leaving 2 x 34pt + 8pt of dead space under the + // button on an iPhone. + const homeIndicatorInset = 34.0; - final flat = await cardHeight(tester, 0); - final inset = await cardHeight(tester, homeIndicatorInset); + testWidgets('an onboarding card reserves the bottom system inset once, not twice', (tester) async { + Future cardHeight(double inset) async { + await pump( + tester, + inset, + const OnboardingStep(card: OnboardingCard(content: [Text('step')], footer: [SizedBox(height: 48)])), + ); + // The bottom card is the Container that wraps the SafeArea. + final card = find.ancestor(of: find.byType(SafeArea), matching: find.byType(Container)).first; + return tester.getSize(card).height; + } + final flat = await cardHeight(0); + final inset = await cardHeight(homeIndicatorInset); expect(inset - flat, moreOrLessEquals(homeIndicatorInset, epsilon: 0.5)); }); + + testWidgets('a plain step lifts its button by the bottom inset once, not twice', (tester) async { + Future buttonBottom(double inset) async { + await pump(tester, inset, NameWidget(goNext: () {})); + return tester.getRect(find.byKey(const Key('onboarding_name_continue'))).bottom; + } + + final flat = await buttonBottom(0); + final inset = await buttonBottom(homeIndicatorInset); + expect(flat - inset, moreOrLessEquals(homeIndicatorInset, epsilon: 0.5)); + }); } diff --git a/app/test/widgets/onboarding_complete_large_text_test.dart b/app/test/widgets/onboarding_complete_large_text_test.dart index 0fcebff3def..aa556c13167 100644 --- a/app/test/widgets/onboarding_complete_large_text_test.dart +++ b/app/test/widgets/onboarding_complete_large_text_test.dart @@ -1,30 +1,125 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:shared_preferences/shared_preferences.dart'; -import 'package:omi/backend/preferences.dart'; + +import 'package:omi/l10n/app_localizations.dart'; import 'package:omi/pages/onboarding/complete_screen.dart'; -import 'first_capture_test.dart' show host; + +// Regression for #19946 review feedback: the Siri setup hint lengthened the completion +// screen's fixed Column, so at large text scales (ux-contract §16: test at 200%) the +// "Start Using Omi" button overflowed off-screen. The content is now scrollable, and +// the CTA must stay reachable. showSiriHint forces the iOS 16 App Shortcuts gate so +// the hint-rendering path is testable off-device. + +const Size _compactPhone = Size(320, 568); + +Widget _host(Widget child, {required double textScale}) { + return MaterialApp( + localizationsDelegates: const [ + AppLocalizations.delegate, + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + supportedLocales: AppLocalizations.supportedLocales, + home: MediaQuery( + data: MediaQueryData(textScaler: TextScaler.linear(textScale)), + child: Scaffold(body: child), + ), + ); +} + +Future _pumpComplete(WidgetTester tester, {required bool showSiriHint}) async { + tester.view.physicalSize = _compactPhone; + tester.view.devicePixelRatio = 1.0; + addTearDown(tester.view.resetPhysicalSize); + addTearDown(tester.view.resetDevicePixelRatio); + + await tester.pumpWidget(_host( + OnboardingCompleteScreen( + onComplete: () {}, + showSiriHint: showSiriHint, + appShortcutsAvailabilityProbe: () async => true, + ), + textScale: 1.0)); + // Settle the delayed entrance animation (200 ms delay + 800 ms run). + await tester.pump(const Duration(seconds: 1)); +} void main() { - setUp(() async { - SharedPreferences.setMockInitialValues({}); - await SharedPreferencesUtil.init(); - }); - testWidgets('capture choices and skip remain reachable at 2x text on a compact phone', (tester) async { - tester.view.physicalSize = const Size(320, 568); - tester.view.devicePixelRatio = 1; + testWidgets('completion screen keeps the start CTA reachable at 2x text with the Siri hint shown', (tester) async { + tester.view.physicalSize = _compactPhone; + tester.view.devicePixelRatio = 1.0; addTearDown(tester.view.resetPhysicalSize); addTearDown(tester.view.resetDevicePixelRatio); + var completed = false; - await tester.pumpWidget(host(MediaQuery( - data: const MediaQueryData(textScaler: TextScaler.linear(2)), - child: OnboardingCompleteScreen( - onComplete: () => completed = true, showWatch: true, emit: (_) {}, setProperty: (_, value) {}), - ))); + await tester.pumpWidget( + _host( + OnboardingCompleteScreen( + onComplete: () => completed = true, + showSiriHint: true, + appShortcutsAvailabilityProbe: () async => true, + ), + textScale: 2.0, + ), + ); + await tester.pump(const Duration(seconds: 1)); + + expect(tester.takeException(), isNull); + expect(find.byKey(const Key('onboarding_siri_hint')), findsOneWidget); + final cta = find.byKey(const Key('onboarding_complete_start')); await tester.scrollUntilVisible(cta, 200, scrollable: find.byType(Scrollable).first); - expect(tester.takeException(), isNull); - await tester.tap(cta); + expect(cta.hitTestable(), findsOneWidget); + + await tester.tap(cta.hitTestable()); + await tester.pump(); expect(completed, isTrue); }); + + testWidgets('completion screen without the Siri hint shows no hint key', (tester) async { + await _pumpComplete(tester, showSiriHint: false); + + expect(tester.takeException(), isNull); + expect(find.byKey(const Key('onboarding_siri_hint')), findsNothing); + expect(find.byKey(const Key('onboarding_complete_start')), findsOneWidget); + }); + testWidgets('completion screen waits for App Shortcuts availability before showing the hint', (tester) async { + final availability = Completer(); + await tester.pumpWidget( + _host( + OnboardingCompleteScreen( + onComplete: () {}, + showSiriHint: true, + appShortcutsAvailabilityProbe: () => availability.future, + ), + textScale: 1.0, + ), + ); + + expect(find.byKey(const Key('onboarding_siri_hint')), findsNothing); + await tester.pump(const Duration(seconds: 1)); + availability.complete(true); + await tester.pump(); + expect(find.byKey(const Key('onboarding_siri_hint')), findsOneWidget); + }); + + testWidgets('completion screen hides the hint when App Shortcuts are unavailable', (tester) async { + await tester.pumpWidget( + _host( + OnboardingCompleteScreen( + onComplete: () {}, + showSiriHint: true, + appShortcutsAvailabilityProbe: () async => false, + ), + textScale: 1.0, + ), + ); + await tester.pump(const Duration(seconds: 1)); + + expect(find.byKey(const Key('onboarding_siri_hint')), findsNothing); + }); } diff --git a/app/test/widgets/onboarding_connect_omi_test.dart b/app/test/widgets/onboarding_connect_omi_test.dart new file mode 100644 index 00000000000..cba90376078 --- /dev/null +++ b/app/test/widgets/onboarding_connect_omi_test.dart @@ -0,0 +1,187 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:connectivity_plus_platform_interface/connectivity_plus_platform_interface.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:provider/provider.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +import 'package:omi/backend/preferences.dart'; +import 'package:omi/backend/schema/bt_device/bt_device.dart'; +import 'package:omi/l10n/app_localizations.dart'; +import 'package:omi/pages/onboarding/connect_omi/connect_omi_step.dart'; +import 'package:omi/providers/device_provider.dart'; +import 'package:omi/providers/onboarding_provider.dart'; +import 'package:omi/services/services.dart'; + +/// The shared pairing provider with the radio calls replaced: records what the step asks of it. +class _FakeOnboarding extends OnboardingProvider { + int scans = 0; + int cancels = 0; + + @override + Future scanDevices({required VoidCallback onShowDialog, VoidCallback? onShowLocationDialog}) async { + scans++; + } + + @override + void cancelActiveScan() => cancels++; + + /// Connecting succeeds at once, as `handleTap` does when the radio answers. + @override + Future handleTap({required BtDevice device, required bool isFromOnboarding, VoidCallback? goNext}) async { + isClicked = true; + deviceId = device.id; + deviceName = device.name; + deviceType = device.type; + isConnected = true; + notifyListeners(); + goNext?.call(); + } +} + +class _FakeDevice extends DeviceProvider { + _FakeDevice() : super(chargingConnectionLoader: (_) async => null); + + @override + Future initiateConnection(String caller, {bool boundDeviceOnly = false}) async {} +} + +class _NoConnectivity extends ConnectivityPlatform { + @override + Future> checkConnectivity() async => [ConnectivityResult.none]; + + @override + Stream> get onConnectivityChanged => const Stream.empty(); +} + +void main() { + setUpAll(() async { + SharedPreferences.setMockInitialValues({}); + await SharedPreferencesUtil.init(); + ConnectivityPlatform.instance = _NoConnectivity(); + try { + await ServiceManager.init(); + } catch (_) { + // Already initialised by another test in this isolate. + } + }); + + // The providers are app-wide singletons in production: hand them in by value so the test owns + // their lifetime, as the app does. + Future pump(WidgetTester tester, _FakeOnboarding onboarding, + {required void Function(OnboardingDeviceChoice) onDone, + Future Function(BuildContext, {required String source})? openShop}) { + return tester.pumpWidget( + MultiProvider( + providers: [ + ChangeNotifierProvider.value(value: _FakeDevice()), + ChangeNotifierProvider.value(value: onboarding), + ], + child: MaterialApp( + localizationsDelegates: AppLocalizations.localizationsDelegates, + supportedLocales: const [Locale('en')], + home: Scaffold( + body: + openShop == null ? ConnectOmiStep(onDone: onDone) : ConnectOmiStep(onDone: onDone, openShop: openShop), + ), + ), + ), + ); + } + + testWidgets('says "Connect Omi" and searches only after the arrival has played', (tester) async { + final onboarding = _FakeOnboarding(); + await pump(tester, onboarding, onDone: (_) {}); + + expect(find.text('Connect Omi'), findsOneWidget); + await tester.pump(const Duration(milliseconds: 500)); + expect(onboarding.scans, 0, reason: 'no Bluetooth guidance may cover the arrival'); + await tester.pump(const Duration(seconds: 2)); // The search rings never settle. + expect(onboarding.scans, 1); + }); + + testWidgets('"I\'ve ordered mine" leaves once with that answer', (tester) async { + final choices = []; + await pump(tester, _FakeOnboarding(), onDone: choices.add); + await tester.pump(const Duration(seconds: 2)); // The search rings never settle. + + await tester.tap(find.byKey(const Key('connect_omi_ordered'))); + await tester.tap(find.byKey(const Key('connect_omi_ordered')), warnIfMissed: false); + await tester.pump(); + + expect(choices, [OnboardingDeviceChoice.ordered]); + }); + + testWidgets('"Not ordered yet" opens the Omi store, and onboarding carries on once it is closed', (tester) async { + final choices = []; + final sources = []; + final closed = Completer(); + await pump( + tester, + _FakeOnboarding(), + onDone: choices.add, + openShop: (_, {required source}) { + sources.add(source); + return closed.future; + }, + ); + await tester.pump(const Duration(seconds: 2)); + + await tester.tap(find.byKey(const Key('connect_omi_not_ordered'))); + await tester.tap(find.byKey(const Key('connect_omi_not_ordered')), warnIfMissed: false); + await tester.pump(); + expect(sources, ['onboarding_not_ordered'], reason: 'one store, tagged with where it came from'); + expect(choices, isEmpty, reason: 'still in the store'); + + closed.complete(); + await tester.pump(); + expect(choices, [OnboardingDeviceChoice.notOrdered]); + }); + + testWidgets('a nearby device is shown as a row to connect', (tester) async { + final device = BtDevice(name: 'Omi', id: 'AA:BB', type: DeviceType.omi, rssi: -50); + final onboarding = _FakeOnboarding() + ..deviceList = [device] + ..foundDevicesMap = {device.id: device}; + await pump(tester, onboarding, onDone: (_) {}); + await tester.pump(const Duration(seconds: 2)); // The search rings never settle. + + expect(find.text('Omi'), findsOneWidget); + expect(find.text('Tap your Omi to connect.'), findsOneWidget); + }); + + testWidgets('leaving stops the search and clears "nothing found" for the in-app pairing page', (tester) async { + final onboarding = _FakeOnboarding(); + await pump(tester, onboarding, onDone: (_) {}); + await tester.pump(const Duration(seconds: 2)); // The search rings never settle. + onboarding.enableInstructions = true; // The search came up empty here. + + await tester.pumpWidget(const SizedBox()); + + expect(onboarding.cancels, greaterThan(0)); + expect(onboarding.enableInstructions, isFalse); + }); + + testWidgets('connecting says Connected with a ✓, then moves on once the moment has played', (tester) async { + final device = BtDevice(name: 'Omi', id: 'AA:BB', type: DeviceType.omi, rssi: -50); + final onboarding = _FakeOnboarding() + ..deviceList = [device] + ..foundDevicesMap = {device.id: device}; + final choices = []; + await pump(tester, onboarding, onDone: choices.add); + await tester.pump(const Duration(seconds: 2)); // The search rings never settle. + + await tester.tap(find.text('Omi')); + await tester.pump(); + await tester.pump(const Duration(milliseconds: 900)); + + expect(find.text('Connected'), findsOneWidget); + expect(find.byKey(const Key('connect_omi_connected_badge')), findsOneWidget); + expect(choices, isEmpty, reason: 'the reader sees Connected before the tutorial opens'); + + await tester.pump(const Duration(milliseconds: 300)); // The ring and ✓ finish. + await tester.pump(const Duration(seconds: 1)); // Then a beat to read it. + expect(choices, [OnboardingDeviceChoice.connected]); + }); +} diff --git a/app/test/widgets/onboarding_header_spacing_test.dart b/app/test/widgets/onboarding_header_spacing_test.dart index 28bdbf7d68e..959f635d6cc 100644 --- a/app/test/widgets/onboarding_header_spacing_test.dart +++ b/app/test/widgets/onboarding_header_spacing_test.dart @@ -36,7 +36,7 @@ void main() { body: OnboardingStepLayout( reserveHeader: true, onBack: () => backs++, - progress: const OnboardingProgressDots(current: 4, total: 6), + progress: const OnboardingProgressLine(current: 4, total: 6), child: SpeechProfileWidget(controller: flow, goNext: () {}, onSkip: () {}), ), ), @@ -46,11 +46,12 @@ void main() { final back = find.byKey(const Key('onboarding_back')); final title = find.text('Let Omi get to know you'); final scroll = find.byKey(const Key('introduction_scroll')); - final headerBottom = tester.getRect(find.byType(OnboardingProgressDots)).bottom; + final headerBottom = tester.getRect(find.byType(OnboardingProgressLine)).bottom; expect(tester.getRect(title).top, greaterThanOrEqualTo(headerBottom)); expect(tester.getRect(title).overlaps(tester.getRect(back)), isFalse); - // SafeArea is consumed by the host once; the page starts directly below its header. - expect(tester.getRect(title).top - headerBottom, moreOrLessEquals(20)); + // SafeArea is consumed by the host once; the page starts directly below its header. The + // progress line sits level with the back button, so the header row extends below it. + expect(tester.getRect(title).top - headerBottom, moreOrLessEquals(20 + OmiSpacing.md - OmiSpacing.xxs)); await tester.tap(back); expect(backs, 1); await tester.drag(scroll, const Offset(0, -350)); diff --git a/app/test/widgets/onboarding_intro_skip_test.dart b/app/test/widgets/onboarding_intro_skip_test.dart index c43eebffbeb..f0c9b9750c1 100644 --- a/app/test/widgets/onboarding_intro_skip_test.dart +++ b/app/test/widgets/onboarding_intro_skip_test.dart @@ -20,29 +20,21 @@ Widget _app(Widget child) { void main() { // Regression: the forced first-run tour soft-locked users when a step hung - // (e.g. the mic-test "processing your question"). The intro must always - // offer a skip, and a way out must always be present. The tour floats over the app, so the way - // out is a trailing close X (docs/ux-contract.md §1), not a back chevron that means "quit". - testWidgets('intro screen shows Skip below Get Started and fires onSkip', (tester) async { + // (e.g. the mic-test "processing your question"). A way out must always be present. The tour + // floats over the app, so the way out is a trailing close X (docs/ux-contract.md §1), not a back + // chevron that means "quit" — and not a second Skip under Get Started as well. + testWidgets('intro screen leaves by the close X only, with no Skip under Get Started', (tester) async { var started = false; - var skipped = false; - await tester.pumpWidget(_app(OnboardingIntroScreen( - onStart: () => started = true, - onSkip: () => skipped = true, - ))); + await tester.pumpWidget(_app(OnboardingIntroScreen(onStart: () => started = true, onSkip: () {}))); await tester.pump(); - final skipFinder = find.byKey(const Key('device_onboarding_skip_button')); - expect(skipFinder, findsOneWidget); - - // The close X is always visible (no allowExit gating anymore), and no back chevron is drawn. expect(find.byKey(const Key('device_onboarding_close_button')), findsOneWidget); + expect(find.byKey(const Key('device_onboarding_skip_button')), findsNothing); + expect(find.text('Skip'), findsNothing); expect(find.byIcon(Icons.arrow_back_ios_new), findsNothing); - expect(find.text('Skip'), findsOneWidget); - await tester.tap(skipFinder); - expect(skipped, isTrue); - expect(started, isFalse); + await tester.tap(find.text('Get Started')); + expect(started, isTrue); }); testWidgets('close X fires onSkip and is labelled', (tester) async { diff --git a/app/test/widgets/onboarding_permissions_continue_test.dart b/app/test/widgets/onboarding_permissions_continue_test.dart index f24d22b0766..24531b85800 100644 --- a/app/test/widgets/onboarding_permissions_continue_test.dart +++ b/app/test/widgets/onboarding_permissions_continue_test.dart @@ -4,109 +4,142 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:omi/l10n/app_localizations.dart'; import 'package:omi/pages/onboarding/permissions/onboarding_permissions_panel.dart'; import 'package:omi/pages/onboarding/permissions/permissions_widget.dart'; +import 'package:omi/ui/components/omi_button.dart'; import 'package:omi/ui/components/omi_permission_row.dart'; +/// [statuses] holds every permission's state, Bluetooth included; [permissions] lists only the +/// step's own cards, like the platform source (Bluetooth is added by the step when not allowed). class _FakeSource implements OnboardingPermissionsSource { - _FakeSource(this.statuses, {this.failing = const {}, this.unreadable = const {}}); + _FakeSource(this.statuses, {this.answers = const {}, this.failing = const {}}); final Map statuses; + + /// What the system prompt answers; granted when not listed. + final Map answers; final Set failing; - final Set unreadable; final List requested = []; @override - List get permissions => statuses.keys.toList(); + List get permissions => + statuses.keys.where((p) => p != OnboardingPermission.bluetooth).toList(); @override - Future status(OnboardingPermission permission) async { - if (unreadable.contains(permission)) throw Exception('status unavailable'); - return statuses[permission]!; - } + Future status(OnboardingPermission permission) async => + statuses[permission] ?? OmiPermissionStatus.granted; @override Future request(OnboardingPermission permission) async { requested.add(permission); if (failing.contains(permission)) throw Exception('platform error'); - statuses[permission] = OmiPermissionStatus.granted; + statuses[permission] = answers[permission] ?? OmiPermissionStatus.granted; } } -Future _tapContinue(WidgetTester tester, _FakeSource source) async { - var nextCalls = 0; +Future> _pump(WidgetTester tester, _FakeSource source) async { + final next = [0]; await tester.pumpWidget( MaterialApp( localizationsDelegates: AppLocalizations.localizationsDelegates, supportedLocales: const [Locale('en')], - home: Scaffold(body: PermissionsWidget(source: source, goNext: () => nextCalls++)), + home: Scaffold(body: PermissionsWidget(source: source, goNext: () => next[0]++)), ), ); await tester.pumpAndSettle(); - await tester.ensureVisible(find.byKey(const Key('onboarding_permissions_continue'))); - await tester.tap(find.byKey(const Key('onboarding_permissions_continue'))); + return next; +} + +bool _continueEnabled(WidgetTester tester) => + tester.widget(find.byKey(const Key('onboarding_permissions_continue'))).onPressed != null; + +Future _toggle(WidgetTester tester, OnboardingPermission permission) async { + final card = find.byKey(ValueKey('onboarding_permission_${permission.name}')); + await tester.ensureVisible(card); + await tester.tap(card); await tester.pumpAndSettle(); - return nextCalls; } void main() { - testWidgets('Continue asks for every permission still missing, in row order, then moves on', (tester) async { + testWidgets('Continue stays locked until every card is answered; each toggle asks only its own permission', + (tester) async { final source = _FakeSource({ - OnboardingPermission.background: OmiPermissionStatus.askable, OnboardingPermission.location: OmiPermissionStatus.askable, OnboardingPermission.notifications: OmiPermissionStatus.askable, }); + final next = await _pump(tester, source); + + expect(source.requested, isEmpty, reason: 'nothing prompts by itself'); + expect(_continueEnabled(tester), isFalse); + + await _toggle(tester, OnboardingPermission.location); + expect(source.requested, [OnboardingPermission.location]); + expect(_continueEnabled(tester), isFalse); + + await _toggle(tester, OnboardingPermission.notifications); + expect(source.requested, [OnboardingPermission.location, OnboardingPermission.notifications]); + expect(_continueEnabled(tester), isTrue); - expect(await _tapContinue(tester, source), 1); - expect(source.requested, [ - OnboardingPermission.background, - OnboardingPermission.location, - OnboardingPermission.notifications, - ]); + await tester.tap(find.byKey(const Key('onboarding_permissions_continue'))); + await tester.pumpAndSettle(); + expect(next[0], 1); }); - testWidgets('Continue does not ask again for a permission already allowed', (tester) async { - final source = _FakeSource({ - OnboardingPermission.background: OmiPermissionStatus.granted, - OnboardingPermission.location: OmiPermissionStatus.askable, - OnboardingPermission.notifications: OmiPermissionStatus.granted, - }); + testWidgets('a "no" is an answer: the card shows it and the reader can still continue', (tester) async { + final source = _FakeSource( + { + OnboardingPermission.location: OmiPermissionStatus.askable, + OnboardingPermission.notifications: OmiPermissionStatus.askable, + }, + answers: { + OnboardingPermission.location: OmiPermissionStatus.askable, + OnboardingPermission.notifications: OmiPermissionStatus.blocked, + }, + ); + await _pump(tester, source); - expect(await _tapContinue(tester, source), 1); - expect(source.requested, [OnboardingPermission.location]); + await _toggle(tester, OnboardingPermission.location); + await _toggle(tester, OnboardingPermission.notifications); + + expect(find.text('Tap to fix'), findsOneWidget, reason: 'denied notifications say how to fix them'); + expect(_continueEnabled(tester), isTrue); }); - testWidgets('Continue leaves blocked and service-off permissions to Settings', (tester) async { + testWidgets('allowed and blocked permissions already count as answered', (tester) async { final source = _FakeSource({ - OnboardingPermission.location: OmiPermissionStatus.serviceOff, + OnboardingPermission.location: OmiPermissionStatus.granted, OnboardingPermission.notifications: OmiPermissionStatus.blocked, }); + await _pump(tester, source); - expect(await _tapContinue(tester, source), 1); + expect(_continueEnabled(tester), isTrue); expect(source.requested, isEmpty); }); - testWidgets('a failed ask still asks the rest and moves on', (tester) async { + testWidgets('a failed ask still counts as answered', (tester) async { final source = _FakeSource( - { - OnboardingPermission.location: OmiPermissionStatus.askable, - OnboardingPermission.notifications: OmiPermissionStatus.askable, - }, + {OnboardingPermission.location: OmiPermissionStatus.askable}, failing: {OnboardingPermission.location}, ); + await _pump(tester, source); - expect(await _tapContinue(tester, source), 1); - expect(source.requested, [OnboardingPermission.location, OnboardingPermission.notifications]); + await _toggle(tester, OnboardingPermission.location); + expect(_continueEnabled(tester), isTrue); }); - testWidgets('Continue still asks when a permission status cannot be read', (tester) async { - final source = _FakeSource( - { - OnboardingPermission.location: OmiPermissionStatus.askable, - OnboardingPermission.notifications: OmiPermissionStatus.askable, - }, - unreadable: {OnboardingPermission.notifications}, - ); + testWidgets('Bluetooth gets a card only when it is not allowed', (tester) async { + final allowed = _FakeSource({ + OnboardingPermission.bluetooth: OmiPermissionStatus.granted, + OnboardingPermission.location: OmiPermissionStatus.granted, + }); + await _pump(tester, allowed); + expect(find.byKey(const ValueKey('onboarding_permission_bluetooth')), findsNothing); + await tester.pumpWidget(const SizedBox()); - expect(await _tapContinue(tester, source), 1); - expect(source.requested, [OnboardingPermission.location, OnboardingPermission.notifications]); + final denied = _FakeSource({ + OnboardingPermission.bluetooth: OmiPermissionStatus.blocked, + OnboardingPermission.location: OmiPermissionStatus.granted, + }); + await _pump(tester, denied); + expect(find.byKey(const ValueKey('onboarding_permission_bluetooth')), findsOneWidget); + expect(find.text('Bluetooth permission is required to connect to your device.'), findsOneWidget); }); } diff --git a/app/test/widgets/onboarding_polish_test.dart b/app/test/widgets/onboarding_polish_test.dart deleted file mode 100644 index 96c6e11b93b..00000000000 --- a/app/test/widgets/onboarding_polish_test.dart +++ /dev/null @@ -1,364 +0,0 @@ -import 'dart:async'; -import 'dart:convert'; -import 'dart:io'; -import 'dart:ui' as ui; - -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/rendering.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:provider/provider.dart'; -import 'package:flutter_provider_utilities/flutter_provider_utilities.dart'; -import 'package:omi/backend/schema/bt_device/bt_device.dart'; -import 'package:omi/providers/base_provider.dart'; -import 'package:shared_preferences/shared_preferences.dart'; - -import 'package:omi/backend/preferences.dart'; -import 'package:omi/gen/assets.gen.dart'; -import 'package:omi/l10n/app_localizations.dart'; -import 'package:omi/l10n/app_localizations_en.dart'; -import 'package:omi/pages/capture/connect.dart'; -import 'package:omi/pages/onboarding/complete_screen.dart'; -import 'package:omi/pages/onboarding/wrapper.dart'; -import 'package:omi/pages/onboarding/find_device/pairing_failure_card.dart'; -import 'package:omi/pages/onboarding/found_omi/found_omi_widget.dart'; -import 'package:omi/pages/onboarding/widgets/onboarding_step_layout.dart'; -import 'package:omi/providers/onboarding_provider.dart'; -import 'package:omi/providers/device_provider.dart'; -import 'package:omi/providers/home_provider.dart'; -import 'package:omi/services/devices/bluetooth_readiness.dart'; -import 'package:omi/services/devices/pairing_failure.dart'; -import 'package:omi/ui/ui.dart'; -import 'package:omi/utils/analytics/registry/events.g.dart'; - -import '../../integration_test/visual_audit/fakes.dart' show AuditDeviceProvider; - -typedef Reason = DeviceConnectFailureShownFailureReason; - -Widget host(Widget child, {double scale = 1, TargetPlatform platform = TargetPlatform.iOS}) { - final theme = buildOmiTheme(brightness: Brightness.light); - final font = platform == TargetPlatform.iOS ? 'SF Pro Display' : 'Roboto'; - return MaterialApp( - debugShowCheckedModeBanner: false, - theme: theme.copyWith( - platform: platform, - textTheme: theme.textTheme.apply(fontFamily: font), - appBarTheme: theme.appBarTheme.copyWith(titleTextStyle: OmiType.headline.copyWith(fontFamily: font))), - localizationsDelegates: AppLocalizations.localizationsDelegates, - supportedLocales: const [Locale('en')], - home: Builder( - builder: (context) => MediaQuery( - data: MediaQuery.of(context).copyWith( - textScaler: TextScaler.linear(scale), - padding: const EdgeInsets.only(top: 59, bottom: 34), - viewPadding: const EdgeInsets.only(top: 59, bottom: 34)), - child: Scaffold(body: child), - )), - ); -} - -Widget survey() => Stack(children: [ - Positioned.fill(child: Image.asset(Assets.images.onboardingBg1.path, fit: BoxFit.cover)), - OnboardingStepLayout( - reserveHeader: false, - onBack: () {}, - progress: const OnboardingProgressDots(current: 2, total: 5), - child: FoundOmiWidget(goNext: () {}, saveSource: (_) async => true)), - ]); - -class PairingProvider extends BaseProvider with MessageNotifierMixin implements OnboardingProvider { - @override - Reason? pairingFailureReason; - @override - int pairingFailureVersion = 0; - @override - bool isConnected = false; - @override - String deviceName = ''; - @override - DeviceType? deviceType; - @override - bool enableInstructions = false; - @override - List deviceList = []; - @override - List get visibleDeviceList => []; - @override - int get nearbyDeviceCount => 0; - @override - void dismissPairingFailure() { - pairingFailureReason = null; - notifyListeners(); - } - - @override - dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); - - @override - Future scanDevices({required VoidCallback onShowDialog, VoidCallback? onShowLocationDialog}) async {} - @override - void cancelActiveScan() {} -} - -class PairingHomeProvider extends HomeProvider { - @override - Future setupHasSpeakerProfile() async {} -} - -Widget connect(OnboardingProvider pairing) => MultiProvider(providers: [ - ChangeNotifierProvider.value(value: pairing), - ChangeNotifierProvider(create: (_) => AuditDeviceProvider()), - ChangeNotifierProvider(create: (_) => PairingHomeProvider()), - ], child: const ConnectDevicePage()); - -Future loadFonts() async { - final manifest = jsonDecode(await rootBundle.loadString('FontManifest.json')) as List; - for (final family in manifest) { - final loader = FontLoader(family['family'] as String); - for (final font in family['fonts'] as List) { - loader.addFont(rootBundle.load(font['asset'] as String)); - } - await loader.load(); - } - for (final name in ['.SF UI Text', '.SF UI Display', '.SF Pro Text', 'CupertinoSystemText']) { - final loader = FontLoader(name)..addFont(rootBundle.load('assets/fonts/SFPRODISPLAYREGULAR.OTF')); - await loader.load(); - } - var dir = File(Platform.resolvedExecutable).parent; - Directory? fonts; - for (var i = 0; i < 8; i++) { - final candidate = Directory('${dir.path}/material_fonts'); - if (File('${candidate.path}/Roboto-Regular.ttf').existsSync()) { - fonts = candidate; - break; - } - dir = dir.parent; - } - expect(fonts, isNotNull); - final loader = FontLoader('Roboto'); - for (final weight in ['Regular', 'Medium', 'Bold']) { - loader.addFont(Future.value(ByteData.sublistView(File('${fonts!.path}/Roboto-$weight.ttf').readAsBytesSync()))); - } - await loader.load(); -} - -void main() { - setUp(() async { - SharedPreferences.setMockInitialValues({}); - await SharedPreferencesUtil.init(); - }); - void viewport(WidgetTester tester) { - tester.view.physicalSize = const Size(1179, 2556); - tester.view.devicePixelRatio = 3; - addTearDown(tester.view.resetPhysicalSize); - addTearDown(tester.view.resetDevicePixelRatio); - } - - test('classification uses exposed states; null connection is one reachability bucket', () { - expect(connectFailureReason(StateError('Device initialization did not complete')), Reason.notFoundOrDropped); - expect(connectFailureReason(TimeoutException('timeout')), Reason.notFoundOrDropped); - expect( - connectFailureReason(StateError('Device initialization did not complete'), staleBond: true), Reason.staleBond); - expect(connectFailureReason(const BluetoothAdapterUnavailableException(BluetoothAdapterState.off)), - Reason.bluetoothOff); - expect(connectFailureReason(const BluetoothAdapterUnavailableException(BluetoothAdapterState.unauthorized)), - Reason.bluetoothPermissionDenied); - for (final state in [BluetoothAdapterState.off, BluetoothAdapterState.unauthorized]) { - expect(connectFailureReason(BluetoothAdapterUnavailableException(state), staleBond: true), - state == BluetoothAdapterState.off ? Reason.bluetoothOff : Reason.bluetoothPermissionDenied); - } - expect(connectFailureReason(Exception('unknown')), Reason.unknown); - expect(connectFailureReason(StateError('unrelated')), Reason.unknown); - expect(blockedFailureReason(DevicePairingBlockedReason.nearbyDevicesDenied), Reason.nearbyDevicesDenied); - expect( - blockedFailureReason(DevicePairingBlockedReason.locationPermissionMissing), Reason.locationPermissionMissing); - expect(blockedFailureReason(DevicePairingBlockedReason.bluetoothOff, adapterState: BluetoothAdapterState.off), - Reason.bluetoothOff); - for (final state in [ - BluetoothAdapterState.unknown, - BluetoothAdapterState.unsupported, - BluetoothAdapterState.resetting - ]) { - expect(blockedFailureReason(DevicePairingBlockedReason.bluetoothOff, adapterState: state), Reason.unknown); - } - expect(blockedFailureReason(DevicePairingBlockedReason.noAdapter), Reason.unknown); - expect(blockedFailureReason(DevicePairingBlockedReason.scannerUnavailable), Reason.unknown); - }); - - testWidgets('every failure reason displays its actionable copy and emits once per presentation', (tester) async { - viewport(tester); - final l10n = AppLocalizationsEn(); - final copies = { - Reason.bluetoothOff: 'Turn on Bluetooth on your phone, then try again.', - Reason.bluetoothPermissionDenied: 'Allow Bluetooth access for Omi in Settings, then try again.', - Reason.nearbyDevicesDenied: 'Allow Nearby devices access for Omi in Settings, then try again.', - Reason.locationPermissionMissing: - 'Allow location access for Omi in Settings so this phone can find your device, then try again.', - Reason.notFoundOrDropped: - "Couldn't reach your Omi. Make sure it's charged, close to your phone, and its light is blinking, then try again.", - Reason.staleBond: 'Forget Omi in your phone’s Bluetooth settings, then try pairing again.', - Reason.unknown: 'An error occurred. Please try again.', - }; - for (final platform in [TargetPlatform.iOS, TargetPlatform.android]) { - final events = []; - for (final reason in Reason.values) { - var dismissed = 0, guides = 0; - Widget page() => Center( - child: PairingFailureCard( - key: ValueKey(reason), - reason: reason, - onDismiss: () => dismissed++, - onGuide: () => guides++, - emit: events.add)); - await tester.pumpWidget(host(page(), platform: platform)); - expect(pairingFailureCopy(l10n, reason), copies[reason]); - expect(find.text(copies[reason]!), findsOneWidget); - final count = events.length; - await tester.pumpWidget(host(page(), platform: platform)); - await tester.pump(); - expect(events.length, count); - expect(events.last.wireName, 'Device Connect Failure Shown'); - expect(events.last.properties, - {'failure_reason': reason.wireName, 'platform': platform == TargetPlatform.iOS ? 'ios' : 'android'}); - await tester.tap(find.byKey(const Key('pairing_failure_guide'))); - await tester.pump(); - expect(guides, 1); - await tester.tap(find.byKey(const Key('pairing_failure_dismiss'))); - expect(dismissed, 1); - expect(tester.takeException(), isNull); - } - await tester.pumpWidget(const SizedBox()); - } - }); - - testWidgets('Watch action stays at the sibling bottom inset and state guidance is retained', (tester) async { - viewport(tester); - final l10n = AppLocalizationsEn(); - for (final state in [(true, true), (false, true), (true, false)]) { - await tester - .pumpWidget(host(FirstCaptureWatchInstructions(paired: state.$1, installed: state.$2, onComplete: () {}))); - expect(tester.getRect(find.byKey(const Key('watch_capture_continue'))).bottom, closeTo(802, 1)); - if (!state.$1) { - expect(find.text(l10n.firstCaptureWatchPair), findsOneWidget); - } else if (!state.$2) { - expect(find.text(l10n.firstCaptureWatchInstall), findsOneWidget); - } else { - final text = tester.widget(find.text(l10n.watchCaptureRecord)); - expect(text.style!.fontWeight, OmiType.body.fontWeight); - expect(find.text(l10n.watchCaptureSync), findsOneWidget); - } - expect(tester.takeException(), isNull); - } - await tester - .pumpWidget(host(FirstCaptureWatchInstructions(paired: true, installed: true, onComplete: () {}), scale: 2)); - expect(find.byKey(const Key('watch_capture_continue')).hitTestable(), findsOneWidget); - expect(tester.takeException(), isNull); - }); - - testWidgets('all twelve survey choices fit above Continue at 393x852; large text scrolls', (tester) async { - viewport(tester); - await tester.runAsync(() => loadFonts()); - final l10n = AppLocalizationsEn(); - final labels = [ - l10n.tiktok, - l10n.youtube, - l10n.instagram, - l10n.xTwitter, - l10n.reddit, - l10n.linkedIn, - l10n.friendWordOfMouth, - l10n.coworker, - l10n.event, - l10n.appStore, - l10n.googleSearch, - l10n.otherSource - ]; - await tester.pumpWidget(host(survey())); - final footerTop = tester.getRect(find.byKey(const Key('found_omi_continue'))).top; - for (final label in labels) { - expect(find.text(label).hitTestable(), findsOneWidget, reason: label); - expect(tester.getRect(find.text(label)).bottom, lessThan(footerTop), reason: label); - } - final scroll = tester.state(find.byType(Scrollable)); - expect(scroll.position.maxScrollExtent, 0); - await tester.pumpWidget(host(survey(), scale: 2)); - expect(tester.state(find.byType(Scrollable)).position.maxScrollExtent, greaterThan(0)); - expect(find.byKey(const Key('found_omi_continue')).hitTestable(), findsOneWidget); - await tester.drag(find.byType(SingleChildScrollView), const Offset(0, -1600)); - await tester.pumpAndSettle(); - expect(find.text(l10n.otherSource).hitTestable(), findsOneWidget); - expect(find.text(l10n.googleSearch).hitTestable(), findsOneWidget); - await tester.tap(find.text(l10n.otherSource)); - await tester.pump(); - await tester.ensureVisible(find.byType(TextField)); - expect(find.byType(TextField).hitTestable(), findsOneWidget); - expect(tester.takeException(), isNull); - }); - - testWidgets('connection card is in the connect flow and dismisses without leaving it', (tester) async { - viewport(tester); - final pairing = PairingProvider()..pairingFailureReason = Reason.staleBond; - await tester.pumpWidget(host(connect(pairing))); - await tester.pump(); - expect(find.byType(PairingFailureCard), findsOneWidget); - await tester.tap(find.byKey(const Key('pairing_failure_dismiss'))); - await tester.pump(); - expect(find.byType(PairingFailureCard), findsNothing); - expect(find.byType(ConnectDevicePage), findsOneWidget); - await tester.pumpWidget(const SizedBox()); - pairing.dispose(); - }); - - testWidgets('render onboarding polish with real fonts on both platforms', (tester) async { - if (Platform.environment['ONB_POLISH_RENDER'] != '1') return; - viewport(tester); - await tester.runAsync(() => loadFonts()); - const boundary = Key('polish-render'); - for (final platform in [TargetPlatform.iOS, TargetPlatform.android]) { - debugDefaultTargetPlatformOverride = platform; - addTearDown(() => debugDefaultTargetPlatformOverride = null); - final tag = platform == TargetPlatform.iOS ? 'ios' : 'android'; - Future render(String name, Widget child, {double scale = 1, bool scrollToEnd = false}) async { - await tester.pumpWidget(const SizedBox()); - await tester.pumpWidget(RepaintBoundary(key: boundary, child: host(child, platform: platform, scale: scale))); - await tester.runAsync(() => Future.delayed(const Duration(milliseconds: 100))); - await tester.pump(const Duration(milliseconds: 400)); - if (scrollToEnd) { - await tester.drag(find.byType(SingleChildScrollView), const Offset(0, -1600)); - await tester.pumpAndSettle(); - } - expect(tester.takeException(), isNull, reason: '$tag-$name'); - await tester.runAsync(() async { - final image = tester.element(find.byKey(boundary)).renderObject! as RenderRepaintBoundary; - final png = await (await image.toImage(pixelRatio: 3)).toByteData(format: ui.ImageByteFormat.png); - final ordinal = name == 'watch' - ? 1 - : name.startsWith('connect-') - ? Reason.values.indexWhere((reason) => name == 'connect-${reason.wireName}') + - (platform == TargetPlatform.iOS ? 2 : 12) - : ['find-us', 'find-us-large', 'find-us-large-end'].indexOf(name) + - (platform == TargetPlatform.iOS ? 9 : 19); - final filename = '${ordinal.toString().padLeft(3, '0')}-mobile-$tag-${name.replaceAll('_', '-')}.png'; - final file = File('../.agent-artifacts/ui-evidence/onb-polish/$filename'); - file.parent.createSync(recursive: true); - file.writeAsBytesSync(png!.buffer.asUint8List()); - }); - } - - if (platform == TargetPlatform.iOS) { - await render('watch', FirstCaptureWatchInstructions(paired: true, installed: true, onComplete: () {})); - } - for (final reason in Reason.values) { - final pairing = PairingProvider()..pairingFailureReason = reason; - await render('connect-${reason.wireName}', connect(pairing)); - await tester.pumpWidget(const SizedBox()); - pairing.dispose(); - } - await render('find-us', survey()); - await render('find-us-large', survey(), scale: 2); - await render('find-us-large-end', survey(), scale: 2, scrollToEnd: true); - } - debugDefaultTargetPlatformOverride = null; - }); -} diff --git a/app/test/widgets/onboarding_progress_dots_test.dart b/app/test/widgets/onboarding_progress_line_test.dart similarity index 81% rename from app/test/widgets/onboarding_progress_dots_test.dart rename to app/test/widgets/onboarding_progress_line_test.dart index 85919d9ff44..eb37c503b54 100644 --- a/app/test/widgets/onboarding_progress_dots_test.dart +++ b/app/test/widgets/onboarding_progress_line_test.dart @@ -6,22 +6,23 @@ import 'package:omi/pages/onboarding/widgets/onboarding_step_layout.dart'; import 'package:omi/pages/onboarding/wrapper.dart'; void main() { - testWidgets('progress dots count only real steps and speak "Step N of M"', (tester) async { + testWidgets('progress line fills by real steps and speaks "Step N of M"', (tester) async { final handle = tester.ensureSemantics(); await tester.pumpWidget( const MaterialApp( localizationsDelegates: AppLocalizations.localizationsDelegates, supportedLocales: AppLocalizations.supportedLocales, - home: Scaffold(body: OnboardingProgressDots(current: 1, total: 6)), + home: Scaffold(body: OnboardingProgressLine(current: 1, total: 6)), ), ); + await tester.pumpAndSettle(); expect(find.bySemanticsLabel('Step 2 of 6'), findsOneWidget); - // One dot per step: no placeholder pages inflate the count. - expect( - find.descendant(of: find.byType(OnboardingProgressDots), matching: find.byType(AnimatedContainer)), - findsNWidgets(6), + // The fill covers the steps reached so far: 2 of 6. + final fill = tester.widget( + find.descendant(of: find.byType(OnboardingProgressLine), matching: find.byType(FractionallySizedBox)), ); + expect(fill.widthFactor, closeTo(2 / 6, 1e-9)); handle.dispose(); }); @@ -44,7 +45,7 @@ void main() { child: Scaffold( body: OnboardingStepLayout( reserveHeader: false, - progress: progress == null ? null : OnboardingProgressDots(current: progress, total: 6), + progress: progress == null ? null : OnboardingProgressLine(current: progress, total: 6), onBack: onBack, child: const SafeArea(child: Text('step title', key: Key('title'))), ), @@ -58,7 +59,7 @@ void main() { expect(await top(), 47, reason: 'no chrome, nothing reserved'); final withDots = await top(progress: 4, onBack: () {}); expect(withDots, 47 + kOnboardingChromeHeight); - expect(withDots, greaterThanOrEqualTo(tester.getBottomLeft(find.byType(OnboardingProgressDots)).dy)); + expect(withDots, greaterThanOrEqualTo(tester.getBottomLeft(find.byType(OnboardingProgressLine)).dy)); expect(withDots, greaterThanOrEqualTo(tester.getBottomLeft(find.byKey(const Key('onboarding_back'))).dy)); expect(await top(onBack: () {}), 47 + kOnboardingChromeHeight, reason: 'the back button alone also reserves'); }); diff --git a/app/test/widgets/onboarding_resume_and_voice_retention_test.dart b/app/test/widgets/onboarding_resume_and_voice_retention_test.dart index 17d3081fbd7..2bd8314e476 100644 --- a/app/test/widgets/onboarding_resume_and_voice_retention_test.dart +++ b/app/test/widgets/onboarding_resume_and_voice_retention_test.dart @@ -1,6 +1,5 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:omi/pages/onboarding/guided_voice_controller.dart'; -import 'package:omi/pages/onboarding/wrapper.dart'; import '../providers/guided_voice_controller_test.dart' show FakeVoiceIO; Future _answerAll(FakeVoiceIO io, GuidedVoiceController flow, @@ -65,80 +64,5 @@ void main() { expect(flow.answers.any((a) => a.isGoal && a.saved), isTrue); expect(flow.answers.where((a) => !a.isGoal && a.saved).length, 3); }); - - test('wrapper marks setup and complete pages as resumable steps', () { - // kAuthPage (0) and kAiConsentPage (1) are pre-onboarding gates and must not be resumable - expect(OnboardingWrapper.isResumableStepForTesting(0), isFalse); - expect(OnboardingWrapper.isResumableStepForTesting(1), isFalse); - - // Steps from kNamePage (2) through kCompletePage (9) must be resumable - expect(OnboardingWrapper.isResumableStepForTesting(2), isTrue); // kNamePage - expect(OnboardingWrapper.isResumableStepForTesting(6), isTrue); // kSpeechProfilePage - expect(OnboardingWrapper.isResumableStepForTesting(7), isTrue); // kKnowledgeGraphPage - expect(OnboardingWrapper.isResumableStepForTesting(8), isTrue); // kSetupPage - expect(OnboardingWrapper.isResumableStepForTesting(9), isTrue); // kCompletePage - }); - - test('wrapper previousStep correctly navigates back from complete, setup, and graph', () { - // From Complete page (9): - // When setup page is enabled: returns Setup page (8) - expect( - OnboardingWrapper.previousStepForTesting( - currentIndex: 9, - setupPageEnabled: true, - knowledgeGraphStepEnabled: false, - ), - 8, - ); - - // When setup page is disabled and graph is enabled: returns Graph page (7) - expect( - OnboardingWrapper.previousStepForTesting( - currentIndex: 9, - setupPageEnabled: false, - knowledgeGraphStepEnabled: true, - ), - 7, - ); - - // When setup and graph are disabled: returns Speech profile page (6) - expect( - OnboardingWrapper.previousStepForTesting( - currentIndex: 9, - setupPageEnabled: false, - knowledgeGraphStepEnabled: false, - ), - 6, - ); - - // From Setup page (8): - // Returns graph (7) if graph enabled, else speech profile (6) - expect( - OnboardingWrapper.previousStepForTesting( - currentIndex: 8, - setupPageEnabled: true, - knowledgeGraphStepEnabled: true, - ), - 7, - ); - expect( - OnboardingWrapper.previousStepForTesting( - currentIndex: 8, - setupPageEnabled: true, - knowledgeGraphStepEnabled: false, - ), - 6, - ); - - // From Knowledge Graph page (7): returns speech profile (6) - expect( - OnboardingWrapper.previousStepForTesting( - currentIndex: 7, - setupPageEnabled: false, - knowledgeGraphStepEnabled: true, - ), - 6, - ); - }); }); } diff --git a/app/test/widgets/onboarding_summary_text_test.dart b/app/test/widgets/onboarding_summary_text_test.dart new file mode 100644 index 00000000000..ff0f5175bd7 --- /dev/null +++ b/app/test/widgets/onboarding_summary_text_test.dart @@ -0,0 +1,49 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'package:omi/pages/onboarding/activation/activation_motion.dart'; + +// Omi's notes arrive as markdown; the onboarding card shows them as clean text and never grows +// past its box, however long the notes are. + +const _realNotes = '''## Lunch Update on Sam + +- One speaker mentioned having lunch with Sam, noting that she plans to move to Lisbon in June. + +## Action Item Reminders + +- A speaker requested reminders to **send the deck** to their team by Friday and to pick up milk on the way.'''; + +void main() { + test('markdown becomes headings, bullets and plain text', () { + final lines = summaryLines(_realNotes); + expect(lines.map((l) => l.text), [ + 'Lunch Update on Sam', + 'One speaker mentioned having lunch with Sam, noting that she plans to move to Lisbon in June.', + 'Action Item Reminders', + 'A speaker requested reminders to send the deck to their team by Friday and to pick up milk on the way.', + ]); + expect(lines.map((l) => l.heading), [true, false, true, false]); + expect(lines.map((l) => l.bullet), [false, true, false, true]); + }); + + test('plain prose stays one line', () { + final lines = summaryLines('Lunch with Sam, who is moving to Lisbon in June.'); + expect(lines, hasLength(1)); + expect(lines.single.heading || lines.single.bullet, isFalse); + }); + + testWidgets('long notes stay inside the box and show no markdown', (tester) async { + tester.view.physicalSize = const Size(402 * 3, 874 * 3); + tester.view.devicePixelRatio = 3; + addTearDown(tester.view.reset); + final long = List.filled(4, _realNotes).join('\n\n'); + await tester.pumpWidget(MaterialApp( + home: Scaffold(body: Center(child: SummaryText(text: long, t: 1, style: const TextStyle(fontSize: 17)))), + )); + expect(tester.takeException(), isNull); + expect(tester.getSize(find.byType(SummaryText)).height, lessThanOrEqualTo(190)); + expect(find.textContaining('##', findRichText: true), findsNothing); + expect(find.textContaining('**', findRichText: true), findsNothing); + }); +} diff --git a/app/test/widgets/onboarding_survey_sync_test.dart b/app/test/widgets/onboarding_survey_sync_test.dart deleted file mode 100644 index b0c83d00611..00000000000 --- a/app/test/widgets/onboarding_survey_sync_test.dart +++ /dev/null @@ -1,97 +0,0 @@ -import 'dart:async'; - -import 'package:flutter/material.dart'; -import 'package:flutter_localizations/flutter_localizations.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:shared_preferences/shared_preferences.dart'; - -import 'package:omi/backend/preferences.dart'; -import 'package:omi/l10n/app_localizations.dart'; -import 'package:omi/pages/onboarding/found_omi/found_omi_widget.dart'; -import 'package:omi/services/auth/auth_token_result.dart'; -import 'package:omi/services/onboarding_sync_service.dart'; - -Widget _host(Widget child) => MaterialApp( - localizationsDelegates: const [ - AppLocalizations.delegate, - GlobalMaterialLocalizations.delegate, - GlobalWidgetsLocalizations.delegate, - GlobalCupertinoLocalizations.delegate, - ], - supportedLocales: AppLocalizations.supportedLocales, - home: Scaffold(body: child), - ); - -void main() { - TestWidgetsFlutterBinding.ensureInitialized(); - setUp(() async { - SharedPreferences.setMockInitialValues({}); - await SharedPreferencesUtil.init(); - }); - - testWidgets('survey waits for persistence and advances despite failed upload', (tester) async { - tester.view.physicalSize = const Size(430, 1100); - tester.view.devicePixelRatio = 1; - addTearDown(tester.view.resetPhysicalSize); - addTearDown(tester.view.resetDevicePixelRatio); - final disk = {}; - final persisted = Completer(); - const session = AuthSessionSnapshot(ownerUid: 'owner', generation: 1); - final service = OnboardingSyncService( - read: (owner) => disk[owner] ?? '', - write: (owner, value) async { - if (!await persisted.future) return false; - disk[owner] = value; - return true; - }, - send: (_, __) async => false, - isCurrent: (_) => true, - )..bindSession(session); - addTearDown(service.dispose); - var advanced = 0; - await tester.pumpWidget(_host(FoundOmiWidget( - goNext: () => advanced++, - saveSource: (source) => service.enqueue(session, acquisitionSource: source), - ))); - await tester.tap(find.text('YouTube')); - await tester.pump(); - await tester.tap(find.byKey(const Key('found_omi_continue'))); - await tester.pump(); - expect(advanced, 0); - expect(disk, isEmpty); - await tester.tap(find.byKey(const Key('found_omi_skip'))); - expect(advanced, 0); - persisted.complete(true); - await tester.pump(); - await tester.pump(); - expect(advanced, 1); - expect(service.pending('owner'), {'acquisition_source': 'YouTube'}); - expect(SharedPreferencesUtil().foundOmiSource, 'YouTube'); - service.dispose(); - await tester.pumpWidget(const SizedBox.shrink()); - }); - - testWidgets('failed local save stays on survey and can be retried', (tester) async { - tester.view.physicalSize = const Size(430, 1100); - tester.view.devicePixelRatio = 1; - addTearDown(tester.view.resetPhysicalSize); - addTearDown(tester.view.resetDevicePixelRatio); - var canSave = false; - var advanced = 0; - await tester.pumpWidget(_host(FoundOmiWidget( - goNext: () => advanced++, - saveSource: (_) async => canSave, - ))); - await tester.tap(find.text('YouTube')); - await tester.pump(); - await tester.tap(find.byKey(const Key('found_omi_continue'))); - await tester.pump(); - expect(advanced, 0); - expect(SharedPreferencesUtil().foundOmiSource, ''); - expect(find.text('Something went wrong! Please try again later.'), findsOneWidget); - canSave = true; - await tester.tap(find.byKey(const Key('found_omi_continue'))); - await tester.pump(); - expect(advanced, 1); - }); -} diff --git a/app/test/widgets/paywall_router_test.dart b/app/test/widgets/paywall_router_test.dart index e6f46c189a9..e57fc4bb025 100644 --- a/app/test/widgets/paywall_router_test.dart +++ b/app/test/widgets/paywall_router_test.dart @@ -92,4 +92,51 @@ void main() { expect(shown, isFalse); expect(exposures, isEmpty); }); + + group('exposeControl (end-of-onboarding placement)', () { + const placed = PaywallExperimentConfig(variant: PaywallVariant.control, onboardingPaywall: true); + + testWidgets('a free control reader is exposed as control', (tester) async { + final context = await _context(tester, UsageProvider()); + final exposures = []; + final exposed = PaywallRouter.exposeControl(context, config: placed, record: (key, variant) { + exposures.add('$key=$variant'); + return true; + }); + expect(exposed, isTrue); + expect(exposures, ['paywall-variant=control']); + }); + + testWidgets('a variant reader is not exposed here (openVariant does that)', (tester) async { + final context = await _context(tester, UsageProvider()); + final exposures = []; + final exposed = PaywallRouter.exposeControl( + context, + config: const PaywallExperimentConfig(variant: PaywallVariant.outline, onboardingPaywall: true), + record: (key, variant) { + exposures.add('$key=$variant'); + return true; + }, + ); + expect(exposed, isFalse); + expect(exposures, isEmpty); + }); + + testWidgets('paid readers and hidden subscription UI are never exposed', (tester) async { + for (final response in [ + _response(plan: PlanType.unlimitedV2), + _response(plan: PlanType.basic, showSubscriptionUi: false), + ]) { + final usage = UsageProvider()..debugSetSubscription(response); + final context = await _context(tester, usage); + final exposures = []; + final exposed = PaywallRouter.exposeControl(context, config: placed, record: (key, variant) { + exposures.add('$key=$variant'); + return true; + }); + expect(exposed, isFalse); + expect(exposures, isEmpty); + } + }); + }); } diff --git a/app/test/widgets/settings/settings_plan_section_test.dart b/app/test/widgets/settings/settings_plan_section_test.dart new file mode 100644 index 00000000000..8f0211116d9 --- /dev/null +++ b/app/test/widgets/settings/settings_plan_section_test.dart @@ -0,0 +1,98 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:provider/provider.dart'; + +import 'package:omi/l10n/app_localizations.dart'; +import 'package:omi/models/subscription.dart'; +import 'package:omi/pages/settings/widgets/settings_plan_section.dart'; +import 'package:omi/providers/usage_provider.dart'; + +/// The Settings plan section adds this month's minutes and an Upgrade button under the Plan & +/// Usage row for Free and Plus, and leaves the row alone for every other plan. +class _Usage extends ChangeNotifier implements UsageProvider { + _Usage(this.subscription, {this.showSubscriptionUI = true}); + + @override + final UserSubscriptionResponse? subscription; + + @override + final bool showSubscriptionUI; + + @override + dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); +} + +UserSubscriptionResponse _response(PlanType plan, {int usedSeconds = 0, int limitSeconds = 0}) => + UserSubscriptionResponse( + subscription: Subscription(plan: plan, status: SubscriptionStatus.active), + transcriptionSecondsUsed: usedSeconds, + transcriptionSecondsLimit: limitSeconds, + wordsTranscribedUsed: 0, + wordsTranscribedLimit: 0, + insightsGainedUsed: 0, + insightsGainedLimit: 0, + ); + +void main() { + final en = lookupAppLocalizations(const Locale('en')); + const row = Text('Plan row', key: ValueKey('plan_row')); + + Future pump(WidgetTester tester, UsageProvider usage) async { + await tester.pumpWidget( + ChangeNotifierProvider.value( + value: usage, + child: const MaterialApp( + localizationsDelegates: AppLocalizations.localizationsDelegates, + supportedLocales: AppLocalizations.supportedLocales, + locale: Locale('en'), + home: Scaffold(body: SettingsPlanSection(row: row)), + ), + ), + ); + } + + testWidgets('Free shows minutes used and Upgrade', (tester) async { + await pump(tester, _Usage(_response(PlanType.basic, usedSeconds: 240 * 60, limitSeconds: 300 * 60))); + + expect(find.byKey(const ValueKey('plan_row')), findsOneWidget); + expect(find.text(en.minsUsedThisMonth('240', 300)), findsOneWidget); + expect(find.byType(LinearProgressIndicator), findsOneWidget); + expect(find.text(en.upgrade), findsOneWidget); + }); + + testWidgets('Plus offers Upgrade to Unlimited', (tester) async { + await pump(tester, _Usage(_response(PlanType.plus, usedSeconds: 60, limitSeconds: 1500 * 60))); + + expect(find.text(en.upgradeToUnlimited), findsOneWidget); + expect(find.text(en.minsUsedThisMonth('1', 1500)), findsOneWidget); + }); + + testWidgets('Free with no transcription limit shows Upgrade without a meter', (tester) async { + await pump(tester, _Usage(_response(PlanType.basic))); + + expect(find.byType(LinearProgressIndicator), findsNothing); + expect(find.byKey(const ValueKey('settings_plan_upgrade')), findsOneWidget); + }); + + for (final plan in [PlanType.unlimitedV2, PlanType.unlimited, PlanType.operator, PlanType.architect]) { + testWidgets('${plan.name} keeps the plain row', (tester) async { + await pump(tester, _Usage(_response(plan))); + + expect(find.byKey(const ValueKey('plan_row')), findsOneWidget); + expect(find.byKey(const ValueKey('settings_plan_upgrade')), findsNothing); + }); + } + + testWidgets('hidden subscription UI keeps the plain row', (tester) async { + await pump(tester, _Usage(_response(PlanType.basic, limitSeconds: 18000), showSubscriptionUI: false)); + + expect(find.byKey(const ValueKey('settings_plan_upgrade')), findsNothing); + }); + + testWidgets('no subscription yet keeps the plain row', (tester) async { + await pump(tester, _Usage(null)); + + expect(find.byKey(const ValueKey('plan_row')), findsOneWidget); + expect(find.byKey(const ValueKey('settings_plan_upgrade')), findsNothing); + }); +} diff --git a/app/test/widgets/tasks_empty_state_test.dart b/app/test/widgets/tasks_empty_state_test.dart index 8ac79bb8994..fe883a448a6 100644 --- a/app/test/widgets/tasks_empty_state_test.dart +++ b/app/test/widgets/tasks_empty_state_test.dart @@ -47,16 +47,14 @@ void main() { expect(find.byKey(const ValueKey('omi.action_items.empty')), findsOneWidget); expect(title, findsOneWidget); expect(hint, findsOneWidget); - expect(icon, findsOneWidget); - expect(l10n.tasksEmptyStateMessage, 'Start a conversation to create a task.'); + expect(icon, findsNothing, reason: 'the empty state is text only'); + expect(l10n.tasksEmptyStateMessage, 'To-dos from your conversations will show up here.'); expect(create, findsNothing); expect(find.byType(OmiButton), findsNothing); expect(find.byType(FloatingActionButton), findsNothing); expect(tester.widget(title).style!.color, OmiColors.textPrimary); expect(tester.widget(hint).style!.color, OmiColors.textSecondary); - expect(IconTheme.of(tester.element(icon)).color, OmiColors.textTertiary); final emptyTitleRect = tester.getRect(title); - final emptyIconRect = tester.getRect(icon); status = 503; await provider.forceRefreshActionItems(); @@ -102,7 +100,7 @@ void main() { final homeTitleRect = tester.getRect(find.text(l10n.noConversationsYet)); expect(emptyTitleRect.top, homeTitleRect.top); expect(emptyTitleRect.center.dx, homeTitleRect.center.dx); - expect(emptyIconRect, tester.getRect(find.byIcon(Icons.forum_rounded))); + expect(find.byIcon(Icons.forum_rounded), findsNothing); tester.platformDispatcher.textScaleFactorTestValue = 2; await tester.pumpAndSettle(); diff --git a/app/test/widgets/tasks_more_menu_test.dart b/app/test/widgets/tasks_more_menu_test.dart new file mode 100644 index 00000000000..68480135d28 --- /dev/null +++ b/app/test/widgets/tasks_more_menu_test.dart @@ -0,0 +1,66 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:provider/provider.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +import 'package:omi/backend/preferences.dart'; +import 'package:omi/backend/schema/action_item.dart'; +import 'package:omi/l10n/app_localizations.dart'; +import 'package:omi/pages/action_items/widgets/tasks_more_menu.dart'; +import 'package:omi/providers/action_items_provider.dart'; +import 'package:omi/ui/ui.dart'; + +// Tasks' page actions live in one bare "⋯" beside the Home / Tasks switcher: the completed view +// and export, with no circle behind the glyph. +void main() { + setUp(() async { + SharedPreferences.setMockInitialValues({}); + await SharedPreferencesUtil.init(); + }); + + Future pump(WidgetTester tester) async { + final provider = ActionItemsProvider( + getActionItems: ( + {limit = 50, + offset = 0, + completed, + conversationId, + startDate, + endDate, + dueStartDate, + dueEndDate}) async => + const ActionItemsResponse(actionItems: [], hasMore: false), + ); + addTearDown(provider.dispose); + await tester.pumpWidget( + ChangeNotifierProvider.value( + value: provider, + child: const MaterialApp( + localizationsDelegates: AppLocalizations.localizationsDelegates, + supportedLocales: AppLocalizations.supportedLocales, + home: Scaffold(body: Center(child: TasksMoreMenu())), + ), + ), + ); + return provider; + } + + testWidgets('the button is a bare glyph with a spoken label', (tester) async { + await pump(tester); + final button = tester.widget(find.byKey(TasksMoreMenu.buttonKey)); + expect(button.filled, isFalse); + expect(find.bySemanticsLabel('More options'), findsOneWidget); + }); + + testWidgets('the menu holds the completed view and export, even with no tasks', (tester) async { + final provider = await pump(tester); + await tester.tap(find.byKey(TasksMoreMenu.buttonKey)); + await tester.pumpAndSettle(); + expect(find.text('Show Completed'), findsOneWidget); + expect(find.text('Export'), findsOneWidget); + + await tester.tap(find.text('Show Completed')); + await tester.pumpAndSettle(); + expect(provider.showCompletedView, isTrue); + }); +} diff --git a/app/test/widgets/ui/omi_permission_row_test.dart b/app/test/widgets/ui/omi_permission_row_test.dart index 198b971a6ec..3ad026668b9 100644 --- a/app/test/widgets/ui/omi_permission_row_test.dart +++ b/app/test/widgets/ui/omi_permission_row_test.dart @@ -4,8 +4,6 @@ import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:omi/l10n/app_localizations.dart'; -import 'package:omi/pages/onboarding/permissions/onboarding_permissions_panel.dart'; -import 'package:omi/pages/onboarding/permissions/permissions_widget.dart'; import 'package:omi/ui/components/omi_permission_row.dart'; Widget _app(Widget child) => MaterialApp( @@ -14,29 +12,6 @@ Widget _app(Widget child) => MaterialApp( home: Scaffold(body: child), ); -class _FakeSource implements OnboardingPermissionsSource { - final Map statuses = { - OnboardingPermission.location: OmiPermissionStatus.askable, - OnboardingPermission.notifications: OmiPermissionStatus.blocked, - }; - final List requested = []; - - @override - List get permissions => const [ - OnboardingPermission.location, - OnboardingPermission.notifications, - ]; - - @override - Future status(OnboardingPermission permission) async => statuses[permission]!; - - @override - Future request(OnboardingPermission permission) async { - requested.add(permission); - statuses[permission] = OmiPermissionStatus.granted; - } -} - void main() { test('maps permission_handler statuses to what the reader can do', () { expect(OmiPermissionStatus.fromStatus(PermissionStatus.denied), OmiPermissionStatus.askable); @@ -115,29 +90,6 @@ void main() { expect(opened, 1); }); - testWidgets('onboarding: Allow prompts that one permission; Continue asks only for what is still missing', - (tester) async { - final source = _FakeSource(); - var continued = 0; - await tester.pumpWidget(_app(PermissionsWidget(goNext: () => continued++, source: source))); - await tester.pumpAndSettle(); - - // One row each; the blocked one offers Settings, not a prompt. - expect(find.text('Allow'), findsOneWidget); - expect(find.text('Open Settings'), findsOneWidget); - - await tester.tap(find.text('Allow')); - await tester.pumpAndSettle(); - expect(source.requested, [OnboardingPermission.location]); - expect(find.text('Allowed'), findsOneWidget); - - await tester.tap(find.byKey(const Key('onboarding_permissions_continue'))); - await tester.pumpAndSettle(); - expect(continued, 1); - expect(source.requested, [OnboardingPermission.location], - reason: 'Continue does not re-ask an allowed permission or prompt for a blocked one'); - }); - testWidgets('the action sits on the title line and the reason uses the width below it', (tester) async { tester.view.physicalSize = const Size(390, 844); tester.view.devicePixelRatio = 1; diff --git a/app/test/widgets/upgrade_paywall_test.dart b/app/test/widgets/upgrade_paywall_test.dart new file mode 100644 index 00000000000..7042ad53380 --- /dev/null +++ b/app/test/widgets/upgrade_paywall_test.dart @@ -0,0 +1,68 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:provider/provider.dart'; + +import 'package:omi/l10n/app_localizations.dart'; +import 'package:omi/pages/paywall/outline_paywall_page.dart'; +import 'package:omi/pages/settings/widgets/standalone_plans_sheet.dart'; +import 'package:omi/pages/settings/widgets/upgrade_paywall.dart'; +import 'package:omi/providers/usage_provider.dart'; +import 'package:omi/services/experiments/paywall_experiment.dart'; +import 'package:omi/services/superwall_pilot.dart'; + +// With the Superwall pilot off (every store build), each upgrade entry point asks the paywall +// experiment first: a variant arm gets its page, control gets the plans sheet, and the web +// variant's fallback (forceNative) always gets the sheet. + +Future _open(WidgetTester tester, {PaywallExperimentConfig? experiment, bool forceNative = false}) async { + await tester.pumpWidget( + ChangeNotifierProvider( + create: (_) => UsageProvider(), + child: MaterialApp( + localizationsDelegates: AppLocalizations.localizationsDelegates, + supportedLocales: AppLocalizations.supportedLocales, + home: Builder( + builder: (context) => TextButton( + onPressed: () => openUpgradePaywall( + context, + placement: SuperwallPilot.usageUpgradePlacement, + source: 'usage_page', + forceNative: forceNative, + experiment: experiment, + ), + child: const Text('upgrade'), + ), + ), + ), + ), + ); + await tester.tap(find.text('upgrade')); + await tester.pump(); + await tester.pump(const Duration(milliseconds: 500)); +} + +void main() { + setUp(PaywallExperiment.resetForTesting); + + testWidgets('control opens the plans sheet', (tester) async { + await _open(tester, experiment: PaywallExperimentConfig.control); + expect(find.byType(StandalonePlansSheet), findsOneWidget); + expect(find.byType(OutlinePaywallPage), findsNothing); + }); + + testWidgets('a variant arm opens its paywall instead of the sheet', (tester) async { + await _open(tester, experiment: const PaywallExperimentConfig(variant: PaywallVariant.outline)); + expect(find.byType(OutlinePaywallPage), findsOneWidget); + expect(find.byType(StandalonePlansSheet), findsNothing); + }); + + testWidgets('forceNative skips the experiment and opens the sheet', (tester) async { + await _open( + tester, + experiment: const PaywallExperimentConfig(variant: PaywallVariant.outline), + forceNative: true, + ); + expect(find.byType(StandalonePlansSheet), findsOneWidget); + expect(find.byType(OutlinePaywallPage), findsNothing); + }); +} diff --git a/app/test/widgets/voice_profile_card_test.dart b/app/test/widgets/voice_profile_card_test.dart new file mode 100644 index 00000000000..b8ce3e94983 --- /dev/null +++ b/app/test/widgets/voice_profile_card_test.dart @@ -0,0 +1,87 @@ +import 'package:flutter/material.dart'; +import 'package:connectivity_plus_platform_interface/connectivity_plus_platform_interface.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:provider/provider.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +import 'package:omi/backend/preferences.dart'; +import 'package:omi/backend/schema/bt_device/bt_device.dart'; +import 'package:omi/l10n/app_localizations.dart'; +import 'package:omi/pages/capture/widgets/widgets.dart'; +import 'package:omi/providers/device_provider.dart'; +import 'package:omi/providers/home_provider.dart'; +import 'package:omi/services/services.dart'; + +class _Device extends DeviceProvider { + _Device() : super(chargingConnectionLoader: (_) async => null) { + pairedDevice = BtDevice(id: 'p', name: 'Omi', type: DeviceType.omi, rssi: -40); + isConnected = true; + } + + @override + Future initiateConnection(String caller, {bool boundDeviceOnly = false}) async {} +} + +class _NoConnectivity extends ConnectivityPlatform { + @override + Future> checkConnectivity() async => [ConnectivityResult.none]; + + @override + Stream> get onConnectivityChanged => const Stream.empty(); +} + +// "Teach Omi your voice" shows only without a voice profile; closing it hides it for good and +// points to Settings > Voice Profile. +void main() { + setUpAll(() async { + ConnectivityPlatform.instance = _NoConnectivity(); + try { + await ServiceManager.init(); + } catch (_) { + // Already initialised by another test in this isolate. + } + }); + + setUp(() async { + SharedPreferences.setMockInitialValues({}); + await SharedPreferencesUtil.init(); + }); + + Future pump(WidgetTester tester, {required bool hasProfile}) { + final home = HomeProvider()..hasSpeakerProfile = hasProfile; + return tester.pumpWidget( + MultiProvider( + providers: [ + ChangeNotifierProvider.value(value: home), + ChangeNotifierProvider.value(value: _Device()), + ], + child: const MaterialApp( + localizationsDelegates: AppLocalizations.localizationsDelegates, + supportedLocales: [Locale('en')], + home: Scaffold(body: SpeechProfileCardWidget()), + ), + ), + ); + } + + testWidgets('hidden once the reader has a voice profile', (tester) async { + await pump(tester, hasProfile: true); + expect(find.text('Teach Omi your voice'), findsNothing); + }); + + testWidgets('closing it hides it for good and points to Settings', (tester) async { + await pump(tester, hasProfile: false); + expect(find.text('Teach Omi your voice'), findsOneWidget); + expect(find.bySemanticsLabel('Dismiss'), findsOneWidget); + + await tester.tap(find.byKey(const ValueKey('voice_profile_card_dismiss'))); + await tester.pump(); + expect(find.text('Teach Omi your voice'), findsNothing); + expect(find.text('You can set up Voice Profile anytime in Settings.'), findsOneWidget); + expect(SharedPreferencesUtil().voiceProfileCardDismissed, isTrue); + + await tester.pumpWidget(const SizedBox()); + await pump(tester, hasProfile: false); + expect(find.text('Teach Omi your voice'), findsNothing, reason: 'stays closed'); + }); +} diff --git a/backend/docs/feature-flag-registry.md b/backend/docs/feature-flag-registry.md index 5661e6f7b31..a926deadeb8 100644 --- a/backend/docs/feature-flag-registry.md +++ b/backend/docs/feature-flag-registry.md @@ -109,6 +109,8 @@ entries are exempt: they are queued for removal, not running. | `MEMORY_OWNER_JEV_SHADOW_PERCENT` | dazheng | [backend/docs/experiments/EXP-004-jev-relevance-owner-ramp.md](../../backend/docs/experiments/EXP-004-jev-relevance-owner-ramp.md) | 2026-10-21 | | `SPEAKER_GROUPING_SHADOW` | dazheng | [backend/docs/listen_pusher_pipeline.mdx](../../backend/docs/listen_pusher_pipeline.mdx) | 2026-11-08 | | `exp-002-desktop-identity-v1` | unowned | [backend/docs/experiments/EXP-002-desktop-identity-memory-v1.md](../../backend/docs/experiments/EXP-002-desktop-identity-memory-v1.md) | 2026-10-26 | +| `onboarding-activation-flow` | yinka | [app/lib/services/experiments/onboarding_activation_flow.md](../../app/lib/services/experiments/onboarding_activation_flow.md) | 2026-11-12 | +| `paywall-reask` | yinka | [app/lib/services/experiments/paywall_reask.md](../../app/lib/services/experiments/paywall_reask.md) | 2026-11-13 | | `paywall-variant` | nik | [product/experiments/paywall-variant.md](../../product/experiments/paywall-variant.md) | 2026-11-12 | ## Overdue for a decision @@ -152,6 +154,8 @@ and an explicit empty literal renders as `''`. | `MEMORY_OWNER_JEV_SHADOW_PERCENT` | Candidate-hash percentage for advisory owner measurement | backend | env | closed | — | 0 (cloud_run/backend-sync, cloud_run/backend-sync-backfill); 100 (backend-listen (chart), cloud_run/backend, gke/backend-listen, gke/pusher, pusher (chart)) | 100 (backend-listen (chart), cloud_run/backend, cloud_run/backend-sync, cloud_run/backend-sync-backfill, gke/backend-listen, gke/pusher, pusher (chart)) | — | pending | 2026-10-21 | dazheng | | `SPEAKER_GROUPING_SHADOW` | off\|owner\|cohort; default off; prod cached-evidence comparison at 10 percent; never changes transcript labels | backend | env | closed | — | — | cohort (backend-listen (chart), pusher (chart)); {value: 'cohort', category: rollout} (cloud_run/backend, cloud_run/backend-integration, cloud_run/backend-sync, cloud_run/backend-sync-backfill, gke/backend-listen, gke/pusher) | — | pending | 2026-11-08 | dazheng | | `exp-002-desktop-identity-v1` | EXP-002 memory_v1 desktop identity arm enrollment | backend, macos | posthog | closed | — | — | — | expected (enable) | pending | 2026-10-26 | unowned | +| `onboarding-activation-flow` | Mobile onboarding ending A/B: on or a test/treatment/activation variant ends onboarding with the activation flow (first conversation, summary, paywall); off, control, unknown or an unread flag keeps the guided voice introduction. Client-evaluated, arm fixed per account on leaving Connect Omi | mobile | posthog | closed | — | — | — | expected (enable) | pending | 2026-11-12 | yinka | +| `paywall-reask` | Mobile paywall re-ask on app open for free readers: test shows the usual paywall (openUpgradePaywall, so paywall-variant picks the design) 2, 3, 5 then every 7 days after the last paywall seen; control shows nothing on the same schedule. Client-evaluated; no identity, unread flag or any error shows nothing | mobile | posthog | closed | — | — | — | expected (exposure) | pending | 2026-11-13 | yinka | | `paywall-variant` | Mobile paywall A/B test: control (current PlansSheet) / outline (PR #21412 soft paywall, native) / web (webview paywall whose page is the variant). Client-evaluated multivariate flag; control collapses to the shipped sheet, paid readers and App Review builds never see a variant | mobile | posthog | closed | — | — | — | expected (exposure) | pending | 2026-11-12 | nik | ### rollout diff --git a/config/feature-flags.yaml b/config/feature-flags.yaml index 717f8885b58..66bce43a311 100644 --- a/config/feature-flags.yaml +++ b/config/feature-flags.yaml @@ -4,10 +4,12 @@ flags: - {"key":"STT_LIVE_DEMAND_UID_ALLOWLIST","aliases":[],"kind":"env","lifecycle":"rollout","surfaces":["backend"],"summary":"Explicit live demand canary UIDs, comma separated; empty by default; requires global enable","fail":"closed","owner":"dazheng","created":"2026-10-14","review_by":"2026-11-13","decision":"pending"} - {"key":"STT_LIVE_DEMAND_MIN_DWELL_SECONDS","aliases":[],"kind":"env","lifecycle":"rollout","surfaces":["backend"],"summary":"Stable hidden interval before cheap re-admission, default fifteen seconds; visible demand returns immediately","fail":"closed","owner":"dazheng","created":"2026-10-14","review_by":"2026-11-13","decision":"pending"} - {"key":"OWNER_IDENTITY_RETRY_COALESCE_ENABLED","aliases":[],"kind":"env","lifecycle":"rollout","surfaces":["backend"],"summary":"Default false; bounded per-UID late identity coalescing, 16 cohorts, 128 candidates and eight eligible passes per cohort; two workers unchanged","fail":"closed","owner":"dazheng","created":"2026-10-14","review_by":"2026-11-13","decision":"pending"} - - {"key":"paywall-variant","prereg":"product/experiments/paywall-variant.md","aliases":[],"kind":"posthog","lifecycle":"experiment","surfaces":["mobile"],"summary":"Mobile paywall A/B test: control (current PlansSheet) / outline (PR #21412 soft paywall, native) / web (webview paywall whose page is the variant). Client-evaluated multivariate flag; control collapses to the shipped sheet, paid readers and App Review builds never see a variant","fail":"closed","owner":"nik","created":"2026-10-13","review_by":"2026-11-12","decision":"pending","posthog":{"row":"expected","role":"exposure"},"notes":"PostHog id 957521, control 100% until the build is in the stores; the split is the experiment. Payload per variant: {\"url\": \"https://…\" (web only, https), \"onboarding_paywall\": true|false}. Debug builds: --dart-define=OMI_PAYWALL_VARIANT=outline|web [OMI_PAYWALL_WEB_URL=…]."} + - {"key":"paywall-variant","prereg":"product/experiments/paywall-variant.md","aliases":[],"kind":"posthog","lifecycle":"experiment","surfaces":["mobile"],"summary":"Mobile paywall A/B test: control (current PlansSheet) / outline (PR #21412 soft paywall, native) / web (webview paywall whose page is the variant). Client-evaluated multivariate flag; control collapses to the shipped sheet, paid readers and App Review builds never see a variant","fail":"closed","owner":"nik","created":"2026-10-13","review_by":"2026-11-12","decision":"pending","posthog":{"row":"expected","role":"exposure"},"notes":"PostHog id 957521, control 100% until the build is in the stores; the split is the experiment. Payload per variant: {\"url\": \"https://…\" (web only, https), \"onboarding_paywall\": true|false}; set onboarding_paywall the same on every arm, control included (control is exposed there without a paywall). Debug builds: --dart-define=OMI_PAYWALL_VARIANT=outline|web [OMI_PAYWALL_WEB_URL=…]."} - {"key":"onboarding-webview","aliases":[],"kind":"posthog","lifecycle":"rollout","surfaces":["mobile"],"summary":"Mobile first-run onboarding rendered from a web page after sign-in and consent (OTA); off = native steps unchanged; the page never owns completion","fail":"closed","owner":"nik","created":"2026-10-13","review_by":"2026-11-12","decision":"pending","posthog":{"row":"expected","role":"enable"},"notes":"PostHog id 957522 at 0%. Payload: {\"url\": \"https://…\"} (https). A page that errors or never reports ready within 10s falls back to native onboarding. Debug builds: --dart-define=OMI_ONBOARDING_WEBVIEW_URL=https://…"} - {"key":"DIARIZATION_V2_ENABLED","aliases":[],"kind":"env","lifecycle":"config_switch","surfaces":["backend"],"summary":"Enable the separate batch-only diarizer app; default false; startup loads models before readiness","fail":"closed","owner":"dazheng","created":"2026-10-14","decision":"keep"} - {"key":"DREAM_AGENT_FULL_SAMPLES","aliases":[],"kind":"env","lifecycle":"config_switch","surfaces":["backend"],"summary":"Concurrent independent full Dream reasoning samples; default 3, bounded to 1 through 8; strict majority among returned samples; reserve one JSON retry per sample","fail":"closed","owner":"dazheng","created":"2026-10-13","decision":"pending"} + - {"key":"onboarding-activation-flow","aliases":[],"kind":"posthog","lifecycle":"experiment","surfaces":["mobile"],"summary":"Mobile onboarding ending A/B: on or a test/treatment/activation variant ends onboarding with the activation flow (first conversation, summary, paywall); off, control, unknown or an unread flag keeps the guided voice introduction. Client-evaluated, arm fixed per account on leaving Connect Omi","fail":"closed","owner":"yinka","created":"2026-10-13","review_by":"2026-11-12","decision":"pending","prereg":"app/lib/services/experiments/onboarding_activation_flow.md","posthog":{"row":"expected","role":"enable"},"notes":"PostHog id 957564, draft experiment 482367 (inactive, 50/50 control/test) until a store build with this code ships. Debug builds can force an arm with --dart-define=OMI_ONBOARDING_ENDING=activation or =control."} + - {"key":"paywall-reask","aliases":[],"kind":"posthog","lifecycle":"experiment","surfaces":["mobile"],"summary":"Mobile paywall re-ask on app open for free readers: test shows the usual paywall (openUpgradePaywall, so paywall-variant picks the design) 2, 3, 5 then every 7 days after the last paywall seen; control shows nothing on the same schedule. Client-evaluated; no identity, unread flag or any error shows nothing","fail":"closed","owner":"yinka","created":"2026-10-14","review_by":"2026-11-13","decision":"pending","prereg":"app/lib/services/experiments/paywall_reask.md","posthog":{"row":"expected","role":"exposure"},"notes":"Not yet created in PostHog. Debug builds: --dart-define=OMI_PAYWALL_REASK=test|control [OMI_PAYWALL_REASK_NOW=true]."} - {"key":"DREAM_AGENT_CONTEXT_MODE","aliases":[],"kind":"env","lifecycle":"rollout","surfaces":["backend"],"summary":"Dream context mode bounded (default) or full; full requires the separate UID allowlist","fail":"closed","owner":"dazheng","created":"2026-10-13","decision":"pending","review_by":"2026-11-12"} - {"key":"DREAM_AGENT_FULL_CONTEXT_UIDS","aliases":[],"kind":"env","lifecycle":"rollout","surfaces":["backend"],"summary":"Comma allowlist for full Dream context; empty admits nobody; ordinary Dream eligibility still applies","fail":"closed","owner":"dazheng","created":"2026-10-13","decision":"pending","review_by":"2026-11-12"} - {"key":"DREAM_AGENT_FULL_CONTEXT_TOKENS","aliases":[],"kind":"env","lifecycle":"config_switch","surfaces":["backend"],"summary":"Full Dream input estimate ceiling; default 400000; framing and schema included; hard model window remains authoritative","fail":"closed","owner":"dazheng","created":"2026-10-13","decision":"pending"} diff --git a/contracts/analytics/events.json b/contracts/analytics/events.json index fbb597e56f7..7f79c145332 100644 --- a/contracts/analytics/events.json +++ b/contracts/analytics/events.json @@ -3648,7 +3648,8 @@ "screen_context_gate", "mcp_daily_cap", "budget_80", - "onboarding" + "onboarding", + "app_open_reask" ] }, "currentPlan": { @@ -4220,7 +4221,8 @@ "screen_context_gate", "mcp_daily_cap", "budget_80", - "onboarding" + "onboarding", + "app_open_reask" ] } }, diff --git a/product/experiments/paywall-variant.md b/product/experiments/paywall-variant.md index f3f3b9c6225..06198055481 100644 --- a/product/experiments/paywall-variant.md +++ b/product/experiments/paywall-variant.md @@ -13,6 +13,10 @@ New designs join as new arms; the split is set in PostHog, never in code. **Unit.** A free reader who is shown a paywall. Exposure = `$feature_flag_called` with `$feature_flag = paywall-variant`, emitted once per arm per app process by `PaywallRouter` at the moment a variant (or control) paywall opens. Paid readers and App Review builds never enter. +At the end-of-onboarding placement control shows nothing but is exposed at the same moment +(`PaywallRouter.exposeControl`), so both arms enter at the same point. `onboarding_paywall` must +therefore have the same value in every arm's payload, control included; a mismatch skews the +comparison. **Primary metric.** `Upgrade Succeeded` per exposed reader (funnel, 7-day window).