From a2901d2949068de51979cfb54d22f58bcbdf9b5c Mon Sep 17 00:00:00 2001 From: Ava Shilling <165050625+avashilling@users.noreply.github.com> Date: Fri, 16 Jan 2026 12:19:43 -0500 Subject: [PATCH 01/27] exit confirmation and practice session reloads on leaving --- lib/config/routes.dart | 62 +++++++++++++++++++ lib/l10n/intl_en.arb | 3 +- .../completed_activity_session_view.dart | 9 ++- .../vocab_practice/vocab_practice_page.dart | 19 ++---- .../vocab_practice_session_repo.dart | 31 ---------- 5 files changed, 76 insertions(+), 48 deletions(-) diff --git a/lib/config/routes.dart b/lib/config/routes.dart index a2487a95a..392e1e6ac 100644 --- a/lib/config/routes.dart +++ b/lib/config/routes.dart @@ -9,6 +9,7 @@ import 'package:matrix/matrix_api_lite/generated/model.dart'; import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/config/themes.dart'; +import 'package:fluffychat/l10n/l10n.dart'; import 'package:fluffychat/pages/archive/archive.dart'; import 'package:fluffychat/pages/chat/chat.dart'; import 'package:fluffychat/pages/chat_access_settings/chat_access_settings_controller.dart'; @@ -583,6 +584,67 @@ abstract class AppRoutes { const VocabPractice(), ); }, + onExit: (context, state) async { + // Check if bypass flag was set before navigation + if (VocabPractice.bypassExitConfirmation) { + VocabPractice.bypassExitConfirmation = false; + return true; + } + + final bool confirm = await showDialog( + context: context, + builder: (context) { + return Center( + child: ConstrainedBox( + constraints: const BoxConstraints( + maxWidth: 400, + ), + child: Dialog( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + child: Padding( + padding: const EdgeInsets.all(24.0), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: + CrossAxisAlignment.stretch, + children: [ + Center( + child: Text( + L10n.of(context).exitPractice, + style: const TextStyle( + fontSize: 28, + ), + ), + ), + const SizedBox(height: 16), + ElevatedButton( + onPressed: () { + Navigator.of(context).pop(true); + }, + child: Text(L10n.of(context).yes), + ), + const SizedBox(height: 16), + ElevatedButton( + onPressed: () { + Navigator.of(context) + .pop(false); + }, + child: Text(L10n.of(context).no), + ), + ], + ), + ), + ), + ), + ); + }, + ) ?? + false; + + return confirm; + }, ), GoRoute( path: ':construct', diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 487a6df31..a168acbcd 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -5046,5 +5046,6 @@ "voice": "Voice", "youLeftTheChat": "🚪 You left the chat", "downloadInitiated": "Download initiated", - "webDownloadPermissionMessage": "If your browser blocks downloads, please enable downloads for this site." + "webDownloadPermissionMessage": "If your browser blocks downloads, please enable downloads for this site.", + "exitPractice": "Exit practice? Your progress will be lost." } diff --git a/lib/pangea/vocab_practice/completed_activity_session_view.dart b/lib/pangea/vocab_practice/completed_activity_session_view.dart index 5b9761f1f..b783c8a73 100644 --- a/lib/pangea/vocab_practice/completed_activity_session_view.dart +++ b/lib/pangea/vocab_practice/completed_activity_session_view.dart @@ -1,5 +1,7 @@ import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/l10n/l10n.dart'; import 'package:fluffychat/pangea/analytics_misc/level_up/star_rain_widget.dart'; @@ -162,7 +164,12 @@ class CompletedActivitySessionView extends StatelessWidget { vertical: 8.0, ), ), - onPressed: () => Navigator.of(context).pop(), + onPressed: () { + VocabPractice.bypassExitConfirmation = true; + debugPrint( + "VocabPractice.bypassExitConfirmation set to ${VocabPractice.bypassExitConfirmation}"); + context.go('/rooms/analytics/vocab'); + }, child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ diff --git a/lib/pangea/vocab_practice/vocab_practice_page.dart b/lib/pangea/vocab_practice/vocab_practice_page.dart index 4bfbfc111..79f0b6979 100644 --- a/lib/pangea/vocab_practice/vocab_practice_page.dart +++ b/lib/pangea/vocab_practice/vocab_practice_page.dart @@ -46,6 +46,8 @@ class SessionLoader extends AsyncLoader { } class VocabPractice extends StatefulWidget { + static bool bypassExitConfirmation = false; + const VocabPractice({super.key}); @override @@ -83,11 +85,7 @@ class VocabPracticeState extends State with AnalyticsUpdater { @override void dispose() { _languageStreamSubscription?.cancel(); - if (_isComplete) { - VocabPracticeSessionRepo.clear(); - } else { - _saveSession(); - } + VocabPracticeSessionRepo.clear(); _sessionLoader.dispose(); activityState.dispose(); activityConstructId.dispose(); @@ -185,12 +183,6 @@ class VocabPracticeState extends State with AnalyticsUpdater { } } - Future _saveSession() async { - if (_sessionLoader.isLoaded) { - await VocabPracticeSessionRepo.update(_sessionLoader.value!); - } - } - Future _waitForAnalytics() async { if (!_analyticsService.initCompleter.isCompleted) { MatrixState.pangeaController.initControllers(); @@ -225,7 +217,7 @@ class VocabPracticeState extends State with AnalyticsUpdater { Future reloadSession() async { _resetActivityState(); _resetSessionState(); - await VocabPracticeSessionRepo.clear(); + _sessionLoader.reset(); await _startSession(); } @@ -240,7 +232,6 @@ class VocabPracticeState extends State with AnalyticsUpdater { bonus, forceUpdate: true, ); - await _saveSession(); } bool _continuing = false; @@ -394,7 +385,6 @@ class VocabPracticeState extends State with AnalyticsUpdater { await _analyticsService.updateService .addAnalytics(choiceTargetId(choiceContent), [use]); - await _saveSession(); if (!correct) return; // Display the fact that the choice was correct before loading the next activity @@ -403,7 +393,6 @@ class VocabPracticeState extends State with AnalyticsUpdater { // Then mark this activity as completed, and either load the next or complete the session _sessionLoader.value!.completeActivity(); progressNotifier.value = _sessionLoader.value!.progress; - await _saveSession(); _isComplete ? await _completeSession() : await _continueSession(); } diff --git a/lib/pangea/vocab_practice/vocab_practice_session_repo.dart b/lib/pangea/vocab_practice/vocab_practice_session_repo.dart index bf780dda8..30877f42b 100644 --- a/lib/pangea/vocab_practice/vocab_practice_session_repo.dart +++ b/lib/pangea/vocab_practice/vocab_practice_session_repo.dart @@ -14,11 +14,6 @@ class VocabPracticeSessionRepo { static final GetStorage _storage = GetStorage('vocab_practice_session'); static Future get() async { - final cached = _getCached(); - if (cached != null) { - return cached; - } - final r = Random(); final activityTypes = [ ActivityTypeEnum.lemmaMeaning, @@ -46,15 +41,9 @@ class VocabPracticeSessionRepo { startedAt: DateTime.now(), practiceTargets: targets, ); - await _setCached(session); return session; } - static Future update( - VocabPracticeSessionModel session, - ) => - _setCached(session); - static Future clear() => _storage.erase(); static Future> _fetch() async { @@ -85,24 +74,4 @@ class VocabPracticeSessionRepo { } return targets; } - - static VocabPracticeSessionModel? _getCached() { - final keys = List.from(_storage.getKeys()); - if (keys.isEmpty) return null; - try { - final json = _storage.read(keys.first) as Map; - return VocabPracticeSessionModel.fromJson(json); - } catch (e) { - _storage.remove(keys.first); - return null; - } - } - - static Future _setCached(VocabPracticeSessionModel session) async { - await _storage.erase(); - await _storage.write( - session.startedAt.toIso8601String(), - session.toJson(), - ); - } } From 00f3982a8c3dfc96cd253184d6f02d57f15def34 Mon Sep 17 00:00:00 2001 From: Ava Shilling <165050625+avashilling@users.noreply.github.com> Date: Fri, 16 Jan 2026 13:24:16 -0500 Subject: [PATCH 02/27] use showOkCancelAlertDialog --- lib/config/routes.dart | 62 +++++++----------------------------------- lib/l10n/intl_en.arb | 2 +- 2 files changed, 11 insertions(+), 53 deletions(-) diff --git a/lib/config/routes.dart b/lib/config/routes.dart index 392e1e6ac..3157d3f87 100644 --- a/lib/config/routes.dart +++ b/lib/config/routes.dart @@ -61,6 +61,7 @@ import 'package:fluffychat/pangea/space_analytics/space_analytics.dart'; import 'package:fluffychat/pangea/spaces/space_constants.dart'; import 'package:fluffychat/pangea/subscription/pages/settings_subscription.dart'; import 'package:fluffychat/pangea/vocab_practice/vocab_practice_page.dart'; +import 'package:fluffychat/widgets/adaptive_dialogs/show_ok_cancel_alert_dialog.dart'; import 'package:fluffychat/widgets/config_viewer.dart'; import 'package:fluffychat/widgets/layouts/empty_page.dart'; import 'package:fluffychat/widgets/layouts/two_column_layout.dart'; @@ -591,59 +592,16 @@ abstract class AppRoutes { return true; } - final bool confirm = await showDialog( - context: context, - builder: (context) { - return Center( - child: ConstrainedBox( - constraints: const BoxConstraints( - maxWidth: 400, - ), - child: Dialog( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10), - ), - child: Padding( - padding: const EdgeInsets.all(24.0), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: - CrossAxisAlignment.stretch, - children: [ - Center( - child: Text( - L10n.of(context).exitPractice, - style: const TextStyle( - fontSize: 28, - ), - ), - ), - const SizedBox(height: 16), - ElevatedButton( - onPressed: () { - Navigator.of(context).pop(true); - }, - child: Text(L10n.of(context).yes), - ), - const SizedBox(height: 16), - ElevatedButton( - onPressed: () { - Navigator.of(context) - .pop(false); - }, - child: Text(L10n.of(context).no), - ), - ], - ), - ), - ), - ), - ); - }, - ) ?? - false; + final result = await showOkCancelAlertDialog( + useRootNavigator: false, + context: context, + title: L10n.of(context).areYouSure, + okLabel: L10n.of(context).yes, + cancelLabel: L10n.of(context).cancel, + message: L10n.of(context).exitPractice, + ); - return confirm; + return result == OkCancelResult.ok; }, ), GoRoute( diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index a168acbcd..effc63fc6 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -5047,5 +5047,5 @@ "youLeftTheChat": "🚪 You left the chat", "downloadInitiated": "Download initiated", "webDownloadPermissionMessage": "If your browser blocks downloads, please enable downloads for this site.", - "exitPractice": "Exit practice? Your progress will be lost." + "exitPractice": "Your practice session progress won't be saved." } From d2cb270694b436d354916ef1a3ddeef7e253a4fc Mon Sep 17 00:00:00 2001 From: Ava Shilling <165050625+avashilling@users.noreply.github.com> Date: Fri, 16 Jan 2026 14:12:52 -0500 Subject: [PATCH 03/27] chore: remove unused code --- .../vocab_practice/completed_activity_session_view.dart | 3 --- lib/pangea/vocab_practice/vocab_practice_page.dart | 2 +- lib/pangea/vocab_practice/vocab_practice_session_repo.dart | 6 ------ 3 files changed, 1 insertion(+), 10 deletions(-) diff --git a/lib/pangea/vocab_practice/completed_activity_session_view.dart b/lib/pangea/vocab_practice/completed_activity_session_view.dart index b783c8a73..8761627a9 100644 --- a/lib/pangea/vocab_practice/completed_activity_session_view.dart +++ b/lib/pangea/vocab_practice/completed_activity_session_view.dart @@ -165,9 +165,6 @@ class CompletedActivitySessionView extends StatelessWidget { ), ), onPressed: () { - VocabPractice.bypassExitConfirmation = true; - debugPrint( - "VocabPractice.bypassExitConfirmation set to ${VocabPractice.bypassExitConfirmation}"); context.go('/rooms/analytics/vocab'); }, child: Row( diff --git a/lib/pangea/vocab_practice/vocab_practice_page.dart b/lib/pangea/vocab_practice/vocab_practice_page.dart index 79f0b6979..3aacab6be 100644 --- a/lib/pangea/vocab_practice/vocab_practice_page.dart +++ b/lib/pangea/vocab_practice/vocab_practice_page.dart @@ -85,7 +85,6 @@ class VocabPracticeState extends State with AnalyticsUpdater { @override void dispose() { _languageStreamSubscription?.cancel(); - VocabPracticeSessionRepo.clear(); _sessionLoader.dispose(); activityState.dispose(); activityConstructId.dispose(); @@ -224,6 +223,7 @@ class VocabPracticeState extends State with AnalyticsUpdater { Future _completeSession() async { _sessionLoader.value!.finishSession(); + VocabPractice.bypassExitConfirmation = true; setState(() {}); final bonus = _sessionLoader.value!.state.allBonusUses; diff --git a/lib/pangea/vocab_practice/vocab_practice_session_repo.dart b/lib/pangea/vocab_practice/vocab_practice_session_repo.dart index 30877f42b..8ca194590 100644 --- a/lib/pangea/vocab_practice/vocab_practice_session_repo.dart +++ b/lib/pangea/vocab_practice/vocab_practice_session_repo.dart @@ -1,7 +1,5 @@ import 'dart:math'; -import 'package:get_storage/get_storage.dart'; - import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart'; import 'package:fluffychat/pangea/constructs/construct_identifier.dart'; import 'package:fluffychat/pangea/practice_activities/activity_type_enum.dart'; @@ -11,8 +9,6 @@ import 'package:fluffychat/pangea/vocab_practice/vocab_practice_session_model.da import 'package:fluffychat/widgets/matrix.dart'; class VocabPracticeSessionRepo { - static final GetStorage _storage = GetStorage('vocab_practice_session'); - static Future get() async { final r = Random(); final activityTypes = [ @@ -44,8 +40,6 @@ class VocabPracticeSessionRepo { return session; } - static Future clear() => _storage.erase(); - static Future> _fetch() async { final constructs = await MatrixState .pangeaController.matrixState.analyticsDataService From c67a543401297bad0da80d4340d0dadc341c2fa6 Mon Sep 17 00:00:00 2001 From: Ava Shilling <165050625+avashilling@users.noreply.github.com> Date: Fri, 16 Jan 2026 14:16:59 -0500 Subject: [PATCH 04/27] Set bypassExitConfirmation after analytics --- lib/pangea/vocab_practice/vocab_practice_page.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pangea/vocab_practice/vocab_practice_page.dart b/lib/pangea/vocab_practice/vocab_practice_page.dart index 3aacab6be..67b3322f7 100644 --- a/lib/pangea/vocab_practice/vocab_practice_page.dart +++ b/lib/pangea/vocab_practice/vocab_practice_page.dart @@ -223,7 +223,6 @@ class VocabPracticeState extends State with AnalyticsUpdater { Future _completeSession() async { _sessionLoader.value!.finishSession(); - VocabPractice.bypassExitConfirmation = true; setState(() {}); final bonus = _sessionLoader.value!.state.allBonusUses; @@ -232,6 +231,7 @@ class VocabPracticeState extends State with AnalyticsUpdater { bonus, forceUpdate: true, ); + VocabPractice.bypassExitConfirmation = true; } bool _continuing = false; From ce1787ecc2e97ec7049cae085e4a577b6b6a64ba Mon Sep 17 00:00:00 2001 From: ggurdin Date: Fri, 16 Jan 2026 14:34:57 -0500 Subject: [PATCH 05/27] translations --- lib/l10n/intl_ar.arb | 27 ++++++++++++++++++++++++++- lib/l10n/intl_be.arb | 27 ++++++++++++++++++++++++++- lib/l10n/intl_bn.arb | 27 ++++++++++++++++++++++++++- lib/l10n/intl_bo.arb | 27 ++++++++++++++++++++++++++- lib/l10n/intl_ca.arb | 27 ++++++++++++++++++++++++++- lib/l10n/intl_cs.arb | 27 ++++++++++++++++++++++++++- lib/l10n/intl_da.arb | 27 ++++++++++++++++++++++++++- lib/l10n/intl_de.arb | 27 ++++++++++++++++++++++++++- lib/l10n/intl_el.arb | 27 ++++++++++++++++++++++++++- lib/l10n/intl_eo.arb | 27 ++++++++++++++++++++++++++- lib/l10n/intl_es.arb | 27 ++++++++++++++++++++++++++- lib/l10n/intl_et.arb | 27 ++++++++++++++++++++++++++- lib/l10n/intl_eu.arb | 27 ++++++++++++++++++++++++++- lib/l10n/intl_fa.arb | 27 ++++++++++++++++++++++++++- lib/l10n/intl_fi.arb | 27 ++++++++++++++++++++++++++- lib/l10n/intl_fil.arb | 27 ++++++++++++++++++++++++++- lib/l10n/intl_fr.arb | 27 ++++++++++++++++++++++++++- lib/l10n/intl_ga.arb | 27 ++++++++++++++++++++++++++- lib/l10n/intl_gl.arb | 27 ++++++++++++++++++++++++++- lib/l10n/intl_he.arb | 27 ++++++++++++++++++++++++++- lib/l10n/intl_hi.arb | 27 ++++++++++++++++++++++++++- lib/l10n/intl_hr.arb | 27 ++++++++++++++++++++++++++- lib/l10n/intl_hu.arb | 27 ++++++++++++++++++++++++++- lib/l10n/intl_ia.arb | 27 ++++++++++++++++++++++++++- lib/l10n/intl_id.arb | 27 ++++++++++++++++++++++++++- lib/l10n/intl_ie.arb | 27 ++++++++++++++++++++++++++- lib/l10n/intl_it.arb | 27 ++++++++++++++++++++++++++- lib/l10n/intl_ja.arb | 27 ++++++++++++++++++++++++++- lib/l10n/intl_ka.arb | 27 ++++++++++++++++++++++++++- lib/l10n/intl_ko.arb | 27 ++++++++++++++++++++++++++- lib/l10n/intl_lt.arb | 27 ++++++++++++++++++++++++++- lib/l10n/intl_lv.arb | 27 ++++++++++++++++++++++++++- lib/l10n/intl_nb.arb | 27 ++++++++++++++++++++++++++- lib/l10n/intl_nl.arb | 27 ++++++++++++++++++++++++++- lib/l10n/intl_pl.arb | 27 ++++++++++++++++++++++++++- lib/l10n/intl_pt.arb | 27 ++++++++++++++++++++++++++- lib/l10n/intl_pt_BR.arb | 27 ++++++++++++++++++++++++++- lib/l10n/intl_pt_PT.arb | 27 ++++++++++++++++++++++++++- lib/l10n/intl_ro.arb | 27 ++++++++++++++++++++++++++- lib/l10n/intl_ru.arb | 27 ++++++++++++++++++++++++++- lib/l10n/intl_sk.arb | 27 ++++++++++++++++++++++++++- lib/l10n/intl_sl.arb | 27 ++++++++++++++++++++++++++- lib/l10n/intl_sr.arb | 27 ++++++++++++++++++++++++++- lib/l10n/intl_sv.arb | 27 ++++++++++++++++++++++++++- lib/l10n/intl_ta.arb | 27 ++++++++++++++++++++++++++- lib/l10n/intl_te.arb | 27 ++++++++++++++++++++++++++- lib/l10n/intl_th.arb | 27 ++++++++++++++++++++++++++- lib/l10n/intl_tr.arb | 27 ++++++++++++++++++++++++++- lib/l10n/intl_uk.arb | 27 ++++++++++++++++++++++++++- lib/l10n/intl_vi.arb | 27 ++++++++++++++++++++++++++- lib/l10n/intl_yue.arb | 27 ++++++++++++++++++++++++++- lib/l10n/intl_zh.arb | 27 ++++++++++++++++++++++++++- lib/l10n/intl_zh_Hant.arb | 27 ++++++++++++++++++++++++++- 53 files changed, 1378 insertions(+), 53 deletions(-) diff --git a/lib/l10n/intl_ar.arb b/lib/l10n/intl_ar.arb index 23926481c..3f762775a 100644 --- a/lib/l10n/intl_ar.arb +++ b/lib/l10n/intl_ar.arb @@ -1,6 +1,6 @@ { "@@locale": "ar", - "@@last_modified": "2026-01-13 15:17:18.289687", + "@@last_modified": "2026-01-16 14:33:24.230348", "about": "حول", "@about": { "type": "String", @@ -11057,5 +11057,30 @@ "@webDownloadPermissionMessage": { "type": "String", "placeholders": {} + }, + "exitPractice": "لن يتم حفظ تقدم جلسة التدريب الخاصة بك.", + "practiceGrammar": "تدرب على القواعد", + "notEnoughToPractice": "أرسل المزيد من الرسائل لفتح التدريب", + "constructUseCorGCDesc": "تدريب على فئة القواعد الصحيحة", + "constructUseIncGCDesc": "تدريب على فئة القواعد غير الصحيحة", + "@exitPractice": { + "type": "String", + "placeholders": {} + }, + "@practiceGrammar": { + "type": "String", + "placeholders": {} + }, + "@notEnoughToPractice": { + "type": "String", + "placeholders": {} + }, + "@constructUseCorGCDesc": { + "type": "String", + "placeholders": {} + }, + "@constructUseIncGCDesc": { + "type": "String", + "placeholders": {} } } \ No newline at end of file diff --git a/lib/l10n/intl_be.arb b/lib/l10n/intl_be.arb index 9ac6b4642..985752f67 100644 --- a/lib/l10n/intl_be.arb +++ b/lib/l10n/intl_be.arb @@ -1911,7 +1911,7 @@ "playWithAI": "Пакуль гуляйце з ШІ", "courseStartDesc": "Pangea Bot гатовы да працы ў любы час!\n\n...але навучанне лепш з сябрамі!", "@@locale": "be", - "@@last_modified": "2026-01-13 15:17:06.767898", + "@@last_modified": "2026-01-16 14:33:05.869532", "@alwaysUse24HourFormat": { "type": "String", "placeholders": {} @@ -11939,5 +11939,30 @@ "@webDownloadPermissionMessage": { "type": "String", "placeholders": {} + }, + "exitPractice": "Ваш практычны сеанс не будзе захаваны.", + "practiceGrammar": "Практыкаваць граматыку", + "notEnoughToPractice": "Адпраўце больш паведамленняў, каб разблакаваць практыку", + "constructUseCorGCDesc": "Практыка ў катэгорыі правільнай граматыкі", + "constructUseIncGCDesc": "Практыка ў катэгорыі няправільнай граматыкі", + "@exitPractice": { + "type": "String", + "placeholders": {} + }, + "@practiceGrammar": { + "type": "String", + "placeholders": {} + }, + "@notEnoughToPractice": { + "type": "String", + "placeholders": {} + }, + "@constructUseCorGCDesc": { + "type": "String", + "placeholders": {} + }, + "@constructUseIncGCDesc": { + "type": "String", + "placeholders": {} } } \ No newline at end of file diff --git a/lib/l10n/intl_bn.arb b/lib/l10n/intl_bn.arb index bbb28058c..4cf1d6221 100644 --- a/lib/l10n/intl_bn.arb +++ b/lib/l10n/intl_bn.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2026-01-13 15:17:32.418851", + "@@last_modified": "2026-01-16 14:33:49.084569", "about": "সম্পর্কে", "@about": { "type": "String", @@ -11944,5 +11944,30 @@ "@webDownloadPermissionMessage": { "type": "String", "placeholders": {} + }, + "exitPractice": "আপনার অনুশীলন সেশনের অগ্রগতি সংরক্ষিত হবে না।", + "practiceGrammar": "ব্যাকরণ অনুশীলন করুন", + "notEnoughToPractice": "অনুশীলন আনলক করতে আরও বার্তা পাঠান", + "constructUseCorGCDesc": "সঠিক ব্যাকরণ বিভাগ অনুশীলন", + "constructUseIncGCDesc": "ভুল ব্যাকরণ বিভাগ অনুশীলন", + "@exitPractice": { + "type": "String", + "placeholders": {} + }, + "@practiceGrammar": { + "type": "String", + "placeholders": {} + }, + "@notEnoughToPractice": { + "type": "String", + "placeholders": {} + }, + "@constructUseCorGCDesc": { + "type": "String", + "placeholders": {} + }, + "@constructUseIncGCDesc": { + "type": "String", + "placeholders": {} } } \ No newline at end of file diff --git a/lib/l10n/intl_bo.arb b/lib/l10n/intl_bo.arb index 66717d3da..067627647 100644 --- a/lib/l10n/intl_bo.arb +++ b/lib/l10n/intl_bo.arb @@ -4279,7 +4279,7 @@ "joinPublicTrip": "མི་ཚེས་ལ་ལོག་འབད།", "startOwnTrip": "ངེད་རང་གི་ལོག་ལ་སྦྱོར་བཅོས།", "@@locale": "bo", - "@@last_modified": "2026-01-13 15:17:29.552592", + "@@last_modified": "2026-01-16 14:33:43.824560", "@alwaysUse24HourFormat": { "type": "String", "placeholders": {} @@ -10594,5 +10594,30 @@ "@webDownloadPermissionMessage": { "type": "String", "placeholders": {} + }, + "exitPractice": "Ndae bɔkɔɔ a wopɛ no, wo nsɛm a wopɛ sɛ woyɛ no bɛyɛ a, ɛrenyɛ.", + "practiceGrammar": "Bɔ mmara", + "notEnoughToPractice": "Sɛ wopɛ sɛ woyɛ bɔ mmara a, fa nsɛm pii to mu", + "constructUseCorGCDesc": "Nokware mmara kategorie bɔ mmara", + "constructUseIncGCDesc": "Nnokwa mmara kategorie bɔ mmara", + "@exitPractice": { + "type": "String", + "placeholders": {} + }, + "@practiceGrammar": { + "type": "String", + "placeholders": {} + }, + "@notEnoughToPractice": { + "type": "String", + "placeholders": {} + }, + "@constructUseCorGCDesc": { + "type": "String", + "placeholders": {} + }, + "@constructUseIncGCDesc": { + "type": "String", + "placeholders": {} } } \ No newline at end of file diff --git a/lib/l10n/intl_ca.arb b/lib/l10n/intl_ca.arb index f3b23c1b2..80d465783 100644 --- a/lib/l10n/intl_ca.arb +++ b/lib/l10n/intl_ca.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2026-01-13 15:17:08.493897", + "@@last_modified": "2026-01-16 14:33:08.940071", "about": "Quant a", "@about": { "type": "String", @@ -10864,5 +10864,30 @@ "@webDownloadPermissionMessage": { "type": "String", "placeholders": {} + }, + "exitPractice": "El teu progrés de la sessió de pràctica no es desarà.", + "practiceGrammar": "Practica gramàtica", + "notEnoughToPractice": "Envia més missatges per desbloquejar la pràctica", + "constructUseCorGCDesc": "Pràctica de la categoria de gramàtica correcta", + "constructUseIncGCDesc": "Pràctica de la categoria de gramàtica incorrecta", + "@exitPractice": { + "type": "String", + "placeholders": {} + }, + "@practiceGrammar": { + "type": "String", + "placeholders": {} + }, + "@notEnoughToPractice": { + "type": "String", + "placeholders": {} + }, + "@constructUseCorGCDesc": { + "type": "String", + "placeholders": {} + }, + "@constructUseIncGCDesc": { + "type": "String", + "placeholders": {} } } \ No newline at end of file diff --git a/lib/l10n/intl_cs.arb b/lib/l10n/intl_cs.arb index b50f87a46..300a94f0d 100644 --- a/lib/l10n/intl_cs.arb +++ b/lib/l10n/intl_cs.arb @@ -1,6 +1,6 @@ { "@@locale": "cs", - "@@last_modified": "2026-01-13 15:17:03.339822", + "@@last_modified": "2026-01-16 14:32:59.173160", "about": "O aplikaci", "@about": { "type": "String", @@ -11447,5 +11447,30 @@ "@webDownloadPermissionMessage": { "type": "String", "placeholders": {} + }, + "exitPractice": "Pokrok vaší cvičební relace nebude uložen.", + "practiceGrammar": "Cvičit gramatiku", + "notEnoughToPractice": "Odešlete více zpráv, abyste odemkli cvičení", + "constructUseCorGCDesc": "Cvičení správné gramatické kategorie", + "constructUseIncGCDesc": "Cvičení nesprávné gramatické kategorie", + "@exitPractice": { + "type": "String", + "placeholders": {} + }, + "@practiceGrammar": { + "type": "String", + "placeholders": {} + }, + "@notEnoughToPractice": { + "type": "String", + "placeholders": {} + }, + "@constructUseCorGCDesc": { + "type": "String", + "placeholders": {} + }, + "@constructUseIncGCDesc": { + "type": "String", + "placeholders": {} } } \ No newline at end of file diff --git a/lib/l10n/intl_da.arb b/lib/l10n/intl_da.arb index b7fa5a12b..ae7a9ef5a 100644 --- a/lib/l10n/intl_da.arb +++ b/lib/l10n/intl_da.arb @@ -1930,7 +1930,7 @@ "playWithAI": "Leg med AI for nu", "courseStartDesc": "Pangea Bot er klar til at starte når som helst!\n\n...men læring er bedre med venner!", "@@locale": "da", - "@@last_modified": "2026-01-13 15:16:30.904012", + "@@last_modified": "2026-01-16 14:32:06.899995", "@aboutHomeserver": { "type": "String", "placeholders": { @@ -11901,5 +11901,30 @@ "@webDownloadPermissionMessage": { "type": "String", "placeholders": {} + }, + "exitPractice": "Din praksis session fremskridt vil ikke blive gemt.", + "practiceGrammar": "Øv grammatik", + "notEnoughToPractice": "Send flere beskeder for at låse op for praksis", + "constructUseCorGCDesc": "Korrekt grammatik kategori praksis", + "constructUseIncGCDesc": "Ukorrrekt grammatik kategori praksis", + "@exitPractice": { + "type": "String", + "placeholders": {} + }, + "@practiceGrammar": { + "type": "String", + "placeholders": {} + }, + "@notEnoughToPractice": { + "type": "String", + "placeholders": {} + }, + "@constructUseCorGCDesc": { + "type": "String", + "placeholders": {} + }, + "@constructUseIncGCDesc": { + "type": "String", + "placeholders": {} } } \ No newline at end of file diff --git a/lib/l10n/intl_de.arb b/lib/l10n/intl_de.arb index 3f53137d4..0f581a3a0 100644 --- a/lib/l10n/intl_de.arb +++ b/lib/l10n/intl_de.arb @@ -1,6 +1,6 @@ { "@@locale": "de", - "@@last_modified": "2026-01-13 15:16:52.685007", + "@@last_modified": "2026-01-16 14:32:44.617501", "alwaysUse24HourFormat": "true", "@alwaysUse24HourFormat": { "description": "Set to true to always display time of day in 24 hour format." @@ -10847,5 +10847,30 @@ "@webDownloadPermissionMessage": { "type": "String", "placeholders": {} + }, + "exitPractice": "Ihr Fortschritt in der Übungssitzung wird nicht gespeichert.", + "practiceGrammar": "Grammatik üben", + "notEnoughToPractice": "Senden Sie mehr Nachrichten, um die Übung freizuschalten", + "constructUseCorGCDesc": "Übung der korrekten Grammatikkategorie", + "constructUseIncGCDesc": "Übung der inkorrekten Grammatikkategorie", + "@exitPractice": { + "type": "String", + "placeholders": {} + }, + "@practiceGrammar": { + "type": "String", + "placeholders": {} + }, + "@notEnoughToPractice": { + "type": "String", + "placeholders": {} + }, + "@constructUseCorGCDesc": { + "type": "String", + "placeholders": {} + }, + "@constructUseIncGCDesc": { + "type": "String", + "placeholders": {} } } \ No newline at end of file diff --git a/lib/l10n/intl_el.arb b/lib/l10n/intl_el.arb index f18e9b2e4..dfb2f4a86 100644 --- a/lib/l10n/intl_el.arb +++ b/lib/l10n/intl_el.arb @@ -4456,7 +4456,7 @@ "playWithAI": "Παίξτε με την Τεχνητή Νοημοσύνη προς το παρόν", "courseStartDesc": "Ο Pangea Bot είναι έτοιμος να ξεκινήσει οποιαδήποτε στιγμή!\n\n...αλλά η μάθηση είναι καλύτερη με φίλους!", "@@locale": "el", - "@@last_modified": "2026-01-13 15:17:39.060670", + "@@last_modified": "2026-01-16 14:33:59.313114", "@alwaysUse24HourFormat": { "type": "String", "placeholders": {} @@ -11898,5 +11898,30 @@ "@webDownloadPermissionMessage": { "type": "String", "placeholders": {} + }, + "exitPractice": "Η πρόοδος της συνεδρίας πρακτικής σας δεν θα αποθηκευτεί.", + "practiceGrammar": "Πρακτική γραμματικής", + "notEnoughToPractice": "Στείλτε περισσότερα μηνύματα για να ξεκλειδώσετε την πρακτική", + "constructUseCorGCDesc": "Πρακτική κατηγορίας σωστής γραμματικής", + "constructUseIncGCDesc": "Πρακτική κατηγορίας λανθαστής γραμματικής", + "@exitPractice": { + "type": "String", + "placeholders": {} + }, + "@practiceGrammar": { + "type": "String", + "placeholders": {} + }, + "@notEnoughToPractice": { + "type": "String", + "placeholders": {} + }, + "@constructUseCorGCDesc": { + "type": "String", + "placeholders": {} + }, + "@constructUseIncGCDesc": { + "type": "String", + "placeholders": {} } } \ No newline at end of file diff --git a/lib/l10n/intl_eo.arb b/lib/l10n/intl_eo.arb index 8120df2b0..39b6c61c0 100644 --- a/lib/l10n/intl_eo.arb +++ b/lib/l10n/intl_eo.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2026-01-13 15:17:43.889996", + "@@last_modified": "2026-01-16 14:34:08.644512", "about": "Prio", "@about": { "type": "String", @@ -11929,5 +11929,30 @@ "@webDownloadPermissionMessage": { "type": "String", "placeholders": {} + }, + "exitPractice": "Via praktika sesio progreso ne estos konservita.", + "practiceGrammar": "Praktiku gramatikon", + "notEnoughToPractice": "Sendu pli da mesaĝoj por malŝlosi praktikon", + "constructUseCorGCDesc": "Praktiko de ĝusta gramatika kategorio", + "constructUseIncGCDesc": "Praktiko de malĝusta gramatika kategorio", + "@exitPractice": { + "type": "String", + "placeholders": {} + }, + "@practiceGrammar": { + "type": "String", + "placeholders": {} + }, + "@notEnoughToPractice": { + "type": "String", + "placeholders": {} + }, + "@constructUseCorGCDesc": { + "type": "String", + "placeholders": {} + }, + "@constructUseIncGCDesc": { + "type": "String", + "placeholders": {} } } \ No newline at end of file diff --git a/lib/l10n/intl_es.arb b/lib/l10n/intl_es.arb index c308da02f..4ff6e98aa 100644 --- a/lib/l10n/intl_es.arb +++ b/lib/l10n/intl_es.arb @@ -1,6 +1,6 @@ { "@@locale": "es", - "@@last_modified": "2026-01-13 15:16:25.022581", + "@@last_modified": "2026-01-16 14:31:58.653004", "about": "Acerca de", "@about": { "type": "String", @@ -8074,5 +8074,30 @@ "@webDownloadPermissionMessage": { "type": "String", "placeholders": {} + }, + "exitPractice": "El progreso de tu sesión de práctica no se guardará.", + "practiceGrammar": "Practicar gramática", + "notEnoughToPractice": "Envía más mensajes para desbloquear la práctica", + "constructUseCorGCDesc": "Práctica de categoría de gramática correcta", + "constructUseIncGCDesc": "Práctica de categoría de gramática incorrecta", + "@exitPractice": { + "type": "String", + "placeholders": {} + }, + "@practiceGrammar": { + "type": "String", + "placeholders": {} + }, + "@notEnoughToPractice": { + "type": "String", + "placeholders": {} + }, + "@constructUseCorGCDesc": { + "type": "String", + "placeholders": {} + }, + "@constructUseIncGCDesc": { + "type": "String", + "placeholders": {} } } \ No newline at end of file diff --git a/lib/l10n/intl_et.arb b/lib/l10n/intl_et.arb index fd59acc86..a1d03040b 100644 --- a/lib/l10n/intl_et.arb +++ b/lib/l10n/intl_et.arb @@ -1,6 +1,6 @@ { "@@locale": "et", - "@@last_modified": "2026-01-13 15:16:50.842106", + "@@last_modified": "2026-01-16 14:32:41.585481", "about": "Rakenduse teave", "@about": { "type": "String", @@ -11111,5 +11111,30 @@ "@webDownloadPermissionMessage": { "type": "String", "placeholders": {} + }, + "exitPractice": "Teie harjut seansi edusamme ei salvestata.", + "practiceGrammar": "Harjuta grammatikat", + "notEnoughToPractice": "Saada rohkem sõnumeid, et harjutust avada", + "constructUseCorGCDesc": "Õige grammatika kategooria harjutus", + "constructUseIncGCDesc": "Vale grammatika kategooria harjutus", + "@exitPractice": { + "type": "String", + "placeholders": {} + }, + "@practiceGrammar": { + "type": "String", + "placeholders": {} + }, + "@notEnoughToPractice": { + "type": "String", + "placeholders": {} + }, + "@constructUseCorGCDesc": { + "type": "String", + "placeholders": {} + }, + "@constructUseIncGCDesc": { + "type": "String", + "placeholders": {} } } \ No newline at end of file diff --git a/lib/l10n/intl_eu.arb b/lib/l10n/intl_eu.arb index aa375b520..253e41e61 100644 --- a/lib/l10n/intl_eu.arb +++ b/lib/l10n/intl_eu.arb @@ -1,6 +1,6 @@ { "@@locale": "eu", - "@@last_modified": "2026-01-13 15:16:47.445226", + "@@last_modified": "2026-01-16 14:32:36.050138", "about": "Honi buruz", "@about": { "type": "String", @@ -10840,5 +10840,30 @@ "@webDownloadPermissionMessage": { "type": "String", "placeholders": {} + }, + "exitPractice": "Zure praktika saioaren aurrerapena ez da gorde.", + "practiceGrammar": "Gramatika praktikatu", + "notEnoughToPractice": "Praktika desblokeatzeko gehiago mezu bidali", + "constructUseCorGCDesc": "Gramatika kategoriako praktika zuzena", + "constructUseIncGCDesc": "Gramatika kategoriako praktika okerra", + "@exitPractice": { + "type": "String", + "placeholders": {} + }, + "@practiceGrammar": { + "type": "String", + "placeholders": {} + }, + "@notEnoughToPractice": { + "type": "String", + "placeholders": {} + }, + "@constructUseCorGCDesc": { + "type": "String", + "placeholders": {} + }, + "@constructUseIncGCDesc": { + "type": "String", + "placeholders": {} } } \ No newline at end of file diff --git a/lib/l10n/intl_fa.arb b/lib/l10n/intl_fa.arb index 59a88af59..5a64b9826 100644 --- a/lib/l10n/intl_fa.arb +++ b/lib/l10n/intl_fa.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2026-01-13 15:17:33.639092", + "@@last_modified": "2026-01-16 14:33:51.512389", "repeatPassword": "تکرار رمزعبور", "@repeatPassword": {}, "about": "درباره", @@ -11572,5 +11572,30 @@ "@webDownloadPermissionMessage": { "type": "String", "placeholders": {} + }, + "exitPractice": "پیشرفت جلسه تمرین شما ذخیره نخواهد شد.", + "practiceGrammar": "تمرین گرامر", + "notEnoughToPractice": "پیام‌های بیشتری ارسال کنید تا تمرین را باز کنید", + "constructUseCorGCDesc": "تمرین دسته گرامر صحیح", + "constructUseIncGCDesc": "تمرین دسته گرامر نادرست", + "@exitPractice": { + "type": "String", + "placeholders": {} + }, + "@practiceGrammar": { + "type": "String", + "placeholders": {} + }, + "@notEnoughToPractice": { + "type": "String", + "placeholders": {} + }, + "@constructUseCorGCDesc": { + "type": "String", + "placeholders": {} + }, + "@constructUseIncGCDesc": { + "type": "String", + "placeholders": {} } } \ No newline at end of file diff --git a/lib/l10n/intl_fi.arb b/lib/l10n/intl_fi.arb index 5f07a6af3..0c8f48151 100644 --- a/lib/l10n/intl_fi.arb +++ b/lib/l10n/intl_fi.arb @@ -4009,7 +4009,7 @@ "playWithAI": "Leiki tekoälyn kanssa nyt", "courseStartDesc": "Pangea Bot on valmis milloin tahansa!\n\n...mutta oppiminen on parempaa ystävien kanssa!", "@@locale": "fi", - "@@last_modified": "2026-01-13 15:16:29.232516", + "@@last_modified": "2026-01-16 14:32:03.875087", "@alwaysUse24HourFormat": { "type": "String", "placeholders": {} @@ -11463,5 +11463,30 @@ "@webDownloadPermissionMessage": { "type": "String", "placeholders": {} + }, + "exitPractice": "Harjoitussession edistystäsi ei tallenneta.", + "practiceGrammar": "Harjoittele kielioppia", + "notEnoughToPractice": "Lähetä lisää viestejä avataksesi harjoituksen", + "constructUseCorGCDesc": "Oikean kielioppikategorian harjoittelu", + "constructUseIncGCDesc": "Väärän kielioppikategorian harjoittelu", + "@exitPractice": { + "type": "String", + "placeholders": {} + }, + "@practiceGrammar": { + "type": "String", + "placeholders": {} + }, + "@notEnoughToPractice": { + "type": "String", + "placeholders": {} + }, + "@constructUseCorGCDesc": { + "type": "String", + "placeholders": {} + }, + "@constructUseIncGCDesc": { + "type": "String", + "placeholders": {} } } \ No newline at end of file diff --git a/lib/l10n/intl_fil.arb b/lib/l10n/intl_fil.arb index 6ecdaccbc..0a79ce37a 100644 --- a/lib/l10n/intl_fil.arb +++ b/lib/l10n/intl_fil.arb @@ -2787,7 +2787,7 @@ "selectAll": "Piliin lahat", "deselectAll": "Huwag piliin lahat", "@@locale": "fil", - "@@last_modified": "2026-01-13 15:17:14.804213", + "@@last_modified": "2026-01-16 14:33:18.880694", "@setCustomPermissionLevel": { "type": "String", "placeholders": {} @@ -11816,5 +11816,30 @@ "@webDownloadPermissionMessage": { "type": "String", "placeholders": {} + }, + "exitPractice": "Hindi mase-save ang iyong progreso sa sesyon ng pagsasanay.", + "practiceGrammar": "Magsanay ng gramatika", + "notEnoughToPractice": "Magpadala ng higit pang mga mensahe upang i-unlock ang pagsasanay", + "constructUseCorGCDesc": "Pagsasanay sa tamang kategorya ng gramatika", + "constructUseIncGCDesc": "Pagsasanay sa maling kategorya ng gramatika", + "@exitPractice": { + "type": "String", + "placeholders": {} + }, + "@practiceGrammar": { + "type": "String", + "placeholders": {} + }, + "@notEnoughToPractice": { + "type": "String", + "placeholders": {} + }, + "@constructUseCorGCDesc": { + "type": "String", + "placeholders": {} + }, + "@constructUseIncGCDesc": { + "type": "String", + "placeholders": {} } } \ No newline at end of file diff --git a/lib/l10n/intl_fr.arb b/lib/l10n/intl_fr.arb index 0904254ae..0820d0126 100644 --- a/lib/l10n/intl_fr.arb +++ b/lib/l10n/intl_fr.arb @@ -1,6 +1,6 @@ { "@@locale": "fr", - "@@last_modified": "2026-01-13 15:17:52.389568", + "@@last_modified": "2026-01-16 14:34:20.722657", "about": "À propos", "@about": { "type": "String", @@ -11164,5 +11164,30 @@ "@webDownloadPermissionMessage": { "type": "String", "placeholders": {} + }, + "exitPractice": "Les progrès de votre session de pratique ne seront pas enregistrés.", + "practiceGrammar": "Pratiquer la grammaire", + "notEnoughToPractice": "Envoyez plus de messages pour débloquer la pratique", + "constructUseCorGCDesc": "Pratique de la catégorie de grammaire correcte", + "constructUseIncGCDesc": "Pratique de la catégorie de grammaire incorrecte", + "@exitPractice": { + "type": "String", + "placeholders": {} + }, + "@practiceGrammar": { + "type": "String", + "placeholders": {} + }, + "@notEnoughToPractice": { + "type": "String", + "placeholders": {} + }, + "@constructUseCorGCDesc": { + "type": "String", + "placeholders": {} + }, + "@constructUseIncGCDesc": { + "type": "String", + "placeholders": {} } } \ No newline at end of file diff --git a/lib/l10n/intl_ga.arb b/lib/l10n/intl_ga.arb index 28ff7c628..3b91b85c4 100644 --- a/lib/l10n/intl_ga.arb +++ b/lib/l10n/intl_ga.arb @@ -4517,7 +4517,7 @@ "playWithAI": "Imir le AI faoi láthair", "courseStartDesc": "Tá Bot Pangea réidh chun dul am ar bith!\n\n...ach is fearr foghlaim le cairde!", "@@locale": "ga", - "@@last_modified": "2026-01-13 15:17:50.751286", + "@@last_modified": "2026-01-16 14:34:18.473304", "@customReaction": { "type": "String", "placeholders": {} @@ -10838,5 +10838,30 @@ "@webDownloadPermissionMessage": { "type": "String", "placeholders": {} + }, + "exitPractice": "Ní shábhálfar do dhul chun cinn sa seisiún cleachtaidh.", + "practiceGrammar": "Cleachtaigh gramadach", + "notEnoughToPractice": "Seol níos mó teachtaireachtaí chun cleachtadh a dhíghlasáil", + "constructUseCorGCDesc": "Cleachtadh catagóir gramadaí ceart", + "constructUseIncGCDesc": "Cleachtadh catagóir gramadaí mícheart", + "@exitPractice": { + "type": "String", + "placeholders": {} + }, + "@practiceGrammar": { + "type": "String", + "placeholders": {} + }, + "@notEnoughToPractice": { + "type": "String", + "placeholders": {} + }, + "@constructUseCorGCDesc": { + "type": "String", + "placeholders": {} + }, + "@constructUseIncGCDesc": { + "type": "String", + "placeholders": {} } } \ No newline at end of file diff --git a/lib/l10n/intl_gl.arb b/lib/l10n/intl_gl.arb index 74732510f..e4e4a6407 100644 --- a/lib/l10n/intl_gl.arb +++ b/lib/l10n/intl_gl.arb @@ -1,6 +1,6 @@ { "@@locale": "gl", - "@@last_modified": "2026-01-13 15:16:27.098717", + "@@last_modified": "2026-01-16 14:32:01.213945", "about": "Acerca de", "@about": { "type": "String", @@ -10837,5 +10837,30 @@ "@webDownloadPermissionMessage": { "type": "String", "placeholders": {} + }, + "exitPractice": "O progreso da túa sesión de práctica non se gardará.", + "practiceGrammar": "Practicar gramática", + "notEnoughToPractice": "Envía máis mensaxes para desbloquear a práctica", + "constructUseCorGCDesc": "Práctica da categoría de gramática correcta", + "constructUseIncGCDesc": "Práctica da categoría de gramática incorrecta", + "@exitPractice": { + "type": "String", + "placeholders": {} + }, + "@practiceGrammar": { + "type": "String", + "placeholders": {} + }, + "@notEnoughToPractice": { + "type": "String", + "placeholders": {} + }, + "@constructUseCorGCDesc": { + "type": "String", + "placeholders": {} + }, + "@constructUseIncGCDesc": { + "type": "String", + "placeholders": {} } } \ No newline at end of file diff --git a/lib/l10n/intl_he.arb b/lib/l10n/intl_he.arb index 6b56d4c4c..12dd52832 100644 --- a/lib/l10n/intl_he.arb +++ b/lib/l10n/intl_he.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2026-01-13 15:16:42.956612", + "@@last_modified": "2026-01-16 14:32:26.413376", "about": "אודות", "@about": { "type": "String", @@ -11889,5 +11889,30 @@ "@webDownloadPermissionMessage": { "type": "String", "placeholders": {} + }, + "exitPractice": "ההתקדמות שלך במפגש האימון לא תישמר.", + "practiceGrammar": "אימון דקדוק", + "notEnoughToPractice": "שלח יותר הודעות כדי לפתוח אימון", + "constructUseCorGCDesc": "אימון בקטגוריית דקדוק נכון", + "constructUseIncGCDesc": "אימון בקטגוריית דקדוק לא נכון", + "@exitPractice": { + "type": "String", + "placeholders": {} + }, + "@practiceGrammar": { + "type": "String", + "placeholders": {} + }, + "@notEnoughToPractice": { + "type": "String", + "placeholders": {} + }, + "@constructUseCorGCDesc": { + "type": "String", + "placeholders": {} + }, + "@constructUseIncGCDesc": { + "type": "String", + "placeholders": {} } } \ No newline at end of file diff --git a/lib/l10n/intl_hi.arb b/lib/l10n/intl_hi.arb index 55c123a98..a95ad48fc 100644 --- a/lib/l10n/intl_hi.arb +++ b/lib/l10n/intl_hi.arb @@ -4483,7 +4483,7 @@ "playWithAI": "अभी के लिए एआई के साथ खेलें", "courseStartDesc": "पैंजिया बॉट कभी भी जाने के लिए तैयार है!\n\n...लेकिन दोस्तों के साथ सीखना बेहतर है!", "@@locale": "hi", - "@@last_modified": "2026-01-13 15:17:42.151930", + "@@last_modified": "2026-01-16 14:34:05.778231", "@alwaysUse24HourFormat": { "type": "String", "placeholders": {} @@ -11925,5 +11925,30 @@ "@webDownloadPermissionMessage": { "type": "String", "placeholders": {} + }, + "exitPractice": "आपकी प्रैक्टिस सत्र की प्रगति सहेजी नहीं जाएगी।", + "practiceGrammar": "व्याकरण का अभ्यास करें", + "notEnoughToPractice": "अभ्यास अनलॉक करने के लिए अधिक संदेश भेजें", + "constructUseCorGCDesc": "सही व्याकरण श्रेणी का अभ्यास", + "constructUseIncGCDesc": "गलत व्याकरण श्रेणी का अभ्यास", + "@exitPractice": { + "type": "String", + "placeholders": {} + }, + "@practiceGrammar": { + "type": "String", + "placeholders": {} + }, + "@notEnoughToPractice": { + "type": "String", + "placeholders": {} + }, + "@constructUseCorGCDesc": { + "type": "String", + "placeholders": {} + }, + "@constructUseIncGCDesc": { + "type": "String", + "placeholders": {} } } \ No newline at end of file diff --git a/lib/l10n/intl_hr.arb b/lib/l10n/intl_hr.arb index 214d99aca..e05faf2de 100644 --- a/lib/l10n/intl_hr.arb +++ b/lib/l10n/intl_hr.arb @@ -1,6 +1,6 @@ { "@@locale": "hr", - "@@last_modified": "2026-01-13 15:16:41.523925", + "@@last_modified": "2026-01-16 14:32:23.849622", "about": "Informacije", "@about": { "type": "String", @@ -11212,5 +11212,30 @@ "@webDownloadPermissionMessage": { "type": "String", "placeholders": {} + }, + "exitPractice": "Vaš napredak u vježbi neće biti spremljen.", + "practiceGrammar": "Vježbajte gramatiku", + "notEnoughToPractice": "Pošaljite više poruka da otključate vježbu", + "constructUseCorGCDesc": "Vježba ispravne gramatičke kategorije", + "constructUseIncGCDesc": "Vježba neispravne gramatičke kategorije", + "@exitPractice": { + "type": "String", + "placeholders": {} + }, + "@practiceGrammar": { + "type": "String", + "placeholders": {} + }, + "@notEnoughToPractice": { + "type": "String", + "placeholders": {} + }, + "@constructUseCorGCDesc": { + "type": "String", + "placeholders": {} + }, + "@constructUseIncGCDesc": { + "type": "String", + "placeholders": {} } } \ No newline at end of file diff --git a/lib/l10n/intl_hu.arb b/lib/l10n/intl_hu.arb index b6da35f1c..857440581 100644 --- a/lib/l10n/intl_hu.arb +++ b/lib/l10n/intl_hu.arb @@ -1,6 +1,6 @@ { "@@locale": "hu", - "@@last_modified": "2026-01-13 15:16:32.515672", + "@@last_modified": "2026-01-16 14:32:10.796625", "about": "Névjegy", "@about": { "type": "String", @@ -10841,5 +10841,30 @@ "@webDownloadPermissionMessage": { "type": "String", "placeholders": {} + }, + "exitPractice": "A gyakorlási session előrehaladása nem lesz mentve.", + "practiceGrammar": "Nyelvtan gyakorlása", + "notEnoughToPractice": "Több üzenetet kell küldeni a gyakorlás feloldásához", + "constructUseCorGCDesc": "Helyes nyelvtani kategória gyakorlása", + "constructUseIncGCDesc": "Helytelen nyelvtani kategória gyakorlása", + "@exitPractice": { + "type": "String", + "placeholders": {} + }, + "@practiceGrammar": { + "type": "String", + "placeholders": {} + }, + "@notEnoughToPractice": { + "type": "String", + "placeholders": {} + }, + "@constructUseCorGCDesc": { + "type": "String", + "placeholders": {} + }, + "@constructUseIncGCDesc": { + "type": "String", + "placeholders": {} } } \ No newline at end of file diff --git a/lib/l10n/intl_ia.arb b/lib/l10n/intl_ia.arb index b80b94146..88b1df62c 100644 --- a/lib/l10n/intl_ia.arb +++ b/lib/l10n/intl_ia.arb @@ -1958,7 +1958,7 @@ "playWithAI": "Joca con le IA pro ora", "courseStartDesc": "Pangea Bot es preste a comenzar a qualunque momento!\n\n...ma apprender es melior con amicos!", "@@locale": "ia", - "@@last_modified": "2026-01-13 15:16:44.800788", + "@@last_modified": "2026-01-16 14:32:31.137719", "@alwaysUse24HourFormat": { "type": "String", "placeholders": {} @@ -11918,5 +11918,30 @@ "@webDownloadPermissionMessage": { "type": "String", "placeholders": {} + }, + "exitPractice": "Progresul sesiunii tale de practică nu va fi salvat.", + "practiceGrammar": "Exersează gramatică", + "notEnoughToPractice": "Trimite mai multe mesaje pentru a debloca practica", + "constructUseCorGCDesc": "Practică categoria de gramatică corectă", + "constructUseIncGCDesc": "Practică categoria de gramatică incorectă", + "@exitPractice": { + "type": "String", + "placeholders": {} + }, + "@practiceGrammar": { + "type": "String", + "placeholders": {} + }, + "@notEnoughToPractice": { + "type": "String", + "placeholders": {} + }, + "@constructUseCorGCDesc": { + "type": "String", + "placeholders": {} + }, + "@constructUseIncGCDesc": { + "type": "String", + "placeholders": {} } } \ No newline at end of file diff --git a/lib/l10n/intl_id.arb b/lib/l10n/intl_id.arb index 7b8e90e5e..b1c17332d 100644 --- a/lib/l10n/intl_id.arb +++ b/lib/l10n/intl_id.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2026-01-13 15:16:34.707887", + "@@last_modified": "2026-01-16 14:32:13.610901", "setAsCanonicalAlias": "Atur sebagai alias utama", "@setAsCanonicalAlias": { "type": "String", @@ -10831,5 +10831,30 @@ "@webDownloadPermissionMessage": { "type": "String", "placeholders": {} + }, + "exitPractice": "Kemajuan sesi latihan Anda tidak akan disimpan.", + "practiceGrammar": "Latihan tata bahasa", + "notEnoughToPractice": "Kirim lebih banyak pesan untuk membuka latihan", + "constructUseCorGCDesc": "Latihan kategori tata bahasa yang benar", + "constructUseIncGCDesc": "Latihan kategori tata bahasa yang salah", + "@exitPractice": { + "type": "String", + "placeholders": {} + }, + "@practiceGrammar": { + "type": "String", + "placeholders": {} + }, + "@notEnoughToPractice": { + "type": "String", + "placeholders": {} + }, + "@constructUseCorGCDesc": { + "type": "String", + "placeholders": {} + }, + "@constructUseIncGCDesc": { + "type": "String", + "placeholders": {} } } \ No newline at end of file diff --git a/lib/l10n/intl_ie.arb b/lib/l10n/intl_ie.arb index 50efbc948..e53212b6d 100644 --- a/lib/l10n/intl_ie.arb +++ b/lib/l10n/intl_ie.arb @@ -4372,7 +4372,7 @@ "playWithAI": "Joca con AI pro ora", "courseStartDesc": "Pangea Bot es preste a partir a qualunque momento!\n\n...ma apprender es melior con amicos!", "@@locale": "ie", - "@@last_modified": "2026-01-13 15:16:39.872561", + "@@last_modified": "2026-01-16 14:32:21.289462", "@alwaysUse24HourFormat": { "type": "String", "placeholders": {} @@ -11814,5 +11814,30 @@ "@webDownloadPermissionMessage": { "type": "String", "placeholders": {} + }, + "exitPractice": "Tua progressio sessionis exercitationis non servabitur.", + "practiceGrammar": "Exercitia grammatica", + "notEnoughToPractice": "Mitte plura nuntia ad exercitium aperiendum", + "constructUseCorGCDesc": "Correcta grammaticae categoriae exercitium", + "constructUseIncGCDesc": "Incorrecta grammaticae categoriae exercitium", + "@exitPractice": { + "type": "String", + "placeholders": {} + }, + "@practiceGrammar": { + "type": "String", + "placeholders": {} + }, + "@notEnoughToPractice": { + "type": "String", + "placeholders": {} + }, + "@constructUseCorGCDesc": { + "type": "String", + "placeholders": {} + }, + "@constructUseIncGCDesc": { + "type": "String", + "placeholders": {} } } \ No newline at end of file diff --git a/lib/l10n/intl_it.arb b/lib/l10n/intl_it.arb index 62444e58e..9987104e8 100644 --- a/lib/l10n/intl_it.arb +++ b/lib/l10n/intl_it.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2026-01-13 15:16:57.804466", + "@@last_modified": "2026-01-16 14:32:53.055805", "about": "Informazioni", "@about": { "type": "String", @@ -10843,5 +10843,30 @@ "@webDownloadPermissionMessage": { "type": "String", "placeholders": {} + }, + "exitPractice": "I progressi della tua sessione di pratica non verranno salvati.", + "practiceGrammar": "Pratica la grammatica", + "notEnoughToPractice": "Invia più messaggi per sbloccare la pratica", + "constructUseCorGCDesc": "Pratica della categoria grammaticale corretta", + "constructUseIncGCDesc": "Pratica della categoria grammaticale scorretta", + "@exitPractice": { + "type": "String", + "placeholders": {} + }, + "@practiceGrammar": { + "type": "String", + "placeholders": {} + }, + "@notEnoughToPractice": { + "type": "String", + "placeholders": {} + }, + "@constructUseCorGCDesc": { + "type": "String", + "placeholders": {} + }, + "@constructUseIncGCDesc": { + "type": "String", + "placeholders": {} } } \ No newline at end of file diff --git a/lib/l10n/intl_ja.arb b/lib/l10n/intl_ja.arb index d54339df8..fda648203 100644 --- a/lib/l10n/intl_ja.arb +++ b/lib/l10n/intl_ja.arb @@ -1,6 +1,6 @@ { "@@locale": "ja", - "@@last_modified": "2026-01-13 15:17:40.716657", + "@@last_modified": "2026-01-16 14:34:03.101722", "about": "このアプリについて", "@about": { "type": "String", @@ -11630,5 +11630,30 @@ "@webDownloadPermissionMessage": { "type": "String", "placeholders": {} + }, + "exitPractice": "あなたの練習セッションの進捗は保存されません。", + "practiceGrammar": "文法を練習する", + "notEnoughToPractice": "練習を解除するにはもっとメッセージを送信してください", + "constructUseCorGCDesc": "正しい文法カテゴリの練習", + "constructUseIncGCDesc": "間違った文法カテゴリの練習", + "@exitPractice": { + "type": "String", + "placeholders": {} + }, + "@practiceGrammar": { + "type": "String", + "placeholders": {} + }, + "@notEnoughToPractice": { + "type": "String", + "placeholders": {} + }, + "@constructUseCorGCDesc": { + "type": "String", + "placeholders": {} + }, + "@constructUseIncGCDesc": { + "type": "String", + "placeholders": {} } } \ No newline at end of file diff --git a/lib/l10n/intl_ka.arb b/lib/l10n/intl_ka.arb index 18b573f15..9b1de6575 100644 --- a/lib/l10n/intl_ka.arb +++ b/lib/l10n/intl_ka.arb @@ -2594,7 +2594,7 @@ "playWithAI": "ამ დროისთვის ითამაშეთ AI-თან", "courseStartDesc": "Pangea Bot მზადაა ნებისმიერ დროს გასასვლელად!\n\n...მაგრამ სწავლა უკეთესია მეგობრებთან ერთად!", "@@locale": "ka", - "@@last_modified": "2026-01-13 15:17:47.132620", + "@@last_modified": "2026-01-16 14:34:13.710408", "@alwaysUse24HourFormat": { "type": "String", "placeholders": {} @@ -11870,5 +11870,30 @@ "@webDownloadPermissionMessage": { "type": "String", "placeholders": {} + }, + "exitPractice": "თქვენი პრაქტიკის სესიის პროგრესი არ დაიშლება.", + "practiceGrammar": "პრაქტიკა გრამატიკა", + "notEnoughToPractice": "პრაქტიკის გასახსნელად მეტი შეტყობინება გამოაგზავნეთ", + "constructUseCorGCDesc": "სწორი გრამატიკული კატეგორიის პრაქტიკა", + "constructUseIncGCDesc": "არასწორი გრამატიკული კატეგორიის პრაქტიკა", + "@exitPractice": { + "type": "String", + "placeholders": {} + }, + "@practiceGrammar": { + "type": "String", + "placeholders": {} + }, + "@notEnoughToPractice": { + "type": "String", + "placeholders": {} + }, + "@constructUseCorGCDesc": { + "type": "String", + "placeholders": {} + }, + "@constructUseIncGCDesc": { + "type": "String", + "placeholders": {} } } \ No newline at end of file diff --git a/lib/l10n/intl_ko.arb b/lib/l10n/intl_ko.arb index 6defc833d..f2b772efe 100644 --- a/lib/l10n/intl_ko.arb +++ b/lib/l10n/intl_ko.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2026-01-13 15:16:23.091865", + "@@last_modified": "2026-01-16 14:31:56.042093", "about": "소개", "@about": { "type": "String", @@ -10948,5 +10948,30 @@ "@webDownloadPermissionMessage": { "type": "String", "placeholders": {} + }, + "exitPractice": "연습 세션 진행 상황이 저장되지 않습니다.", + "practiceGrammar": "문법 연습", + "notEnoughToPractice": "연습을 잠금 해제하려면 더 많은 메시지를 보내세요.", + "constructUseCorGCDesc": "올바른 문법 카테고리 연습", + "constructUseIncGCDesc": "잘못된 문법 카테고리 연습", + "@exitPractice": { + "type": "String", + "placeholders": {} + }, + "@practiceGrammar": { + "type": "String", + "placeholders": {} + }, + "@notEnoughToPractice": { + "type": "String", + "placeholders": {} + }, + "@constructUseCorGCDesc": { + "type": "String", + "placeholders": {} + }, + "@constructUseIncGCDesc": { + "type": "String", + "placeholders": {} } } \ No newline at end of file diff --git a/lib/l10n/intl_lt.arb b/lib/l10n/intl_lt.arb index 05ad1699e..c46982084 100644 --- a/lib/l10n/intl_lt.arb +++ b/lib/l10n/intl_lt.arb @@ -3861,7 +3861,7 @@ "playWithAI": "Žaiskite su dirbtiniu intelektu dabar", "courseStartDesc": "Pangea botas pasiruošęs bet kada pradėti!\n\n...bet mokymasis yra geresnis su draugais!", "@@locale": "lt", - "@@last_modified": "2026-01-13 15:17:23.202279", + "@@last_modified": "2026-01-16 14:33:32.087889", "@alwaysUse24HourFormat": { "type": "String", "placeholders": {} @@ -11645,5 +11645,30 @@ "@webDownloadPermissionMessage": { "type": "String", "placeholders": {} + }, + "exitPractice": "Jūsų praktikos sesijos pažanga nebus išsaugota.", + "practiceGrammar": "Praktikuoti gramatiką", + "notEnoughToPractice": "Siųskite daugiau žinučių, kad atrakintumėte praktiką", + "constructUseCorGCDesc": "Teisingos gramatikos kategorijos praktika", + "constructUseIncGCDesc": "Neteisingos gramatikos kategorijos praktika", + "@exitPractice": { + "type": "String", + "placeholders": {} + }, + "@practiceGrammar": { + "type": "String", + "placeholders": {} + }, + "@notEnoughToPractice": { + "type": "String", + "placeholders": {} + }, + "@constructUseCorGCDesc": { + "type": "String", + "placeholders": {} + }, + "@constructUseIncGCDesc": { + "type": "String", + "placeholders": {} } } \ No newline at end of file diff --git a/lib/l10n/intl_lv.arb b/lib/l10n/intl_lv.arb index 9d37e9972..66ea87a12 100644 --- a/lib/l10n/intl_lv.arb +++ b/lib/l10n/intl_lv.arb @@ -4482,7 +4482,7 @@ "playWithAI": "Tagad spēlējiet ar AI", "courseStartDesc": "Pangea bots ir gatavs jebkurā laikā!\n\n...bet mācīties ir labāk ar draugiem!", "@@locale": "lv", - "@@last_modified": "2026-01-13 15:17:16.858338", + "@@last_modified": "2026-01-16 14:33:21.744947", "analyticsInactiveTitle": "Pieprasījumi neaktīviem lietotājiem nevar tikt nosūtīti", "analyticsInactiveDesc": "Neaktīvi lietotāji, kuri nav pieteikušies kopš šīs funkcijas ieviešanas, neredzēs jūsu pieprasījumu.\n\nPieprasījuma poga parādīsies, kad viņi atgriezīsies. Jūs varat atkārtoti nosūtīt pieprasījumu vēlāk, noklikšķinot uz pieprasījuma pogas viņu vārdā, kad tā būs pieejama.", "accessRequestedTitle": "Pieprasījums piekļūt analītikai", @@ -10826,5 +10826,30 @@ "@webDownloadPermissionMessage": { "type": "String", "placeholders": {} + }, + "exitPractice": "Jūsu prakses sesijas progress netiks saglabāts.", + "practiceGrammar": "Praktizēt gramatiku", + "notEnoughToPractice": "Sūtiet vairāk ziņojumu, lai atbloķētu praksi", + "constructUseCorGCDesc": "Pareizas gramatikas kategorijas prakse", + "constructUseIncGCDesc": "Nepareizas gramatikas kategorijas prakse", + "@exitPractice": { + "type": "String", + "placeholders": {} + }, + "@practiceGrammar": { + "type": "String", + "placeholders": {} + }, + "@notEnoughToPractice": { + "type": "String", + "placeholders": {} + }, + "@constructUseCorGCDesc": { + "type": "String", + "placeholders": {} + }, + "@constructUseIncGCDesc": { + "type": "String", + "placeholders": {} } } \ No newline at end of file diff --git a/lib/l10n/intl_nb.arb b/lib/l10n/intl_nb.arb index 1e289f13a..6e7c933f3 100644 --- a/lib/l10n/intl_nb.arb +++ b/lib/l10n/intl_nb.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2026-01-13 15:17:04.771433", + "@@last_modified": "2026-01-16 14:33:02.881359", "about": "Om", "@about": { "type": "String", @@ -11933,5 +11933,30 @@ "@webDownloadPermissionMessage": { "type": "String", "placeholders": {} + }, + "exitPractice": "Fremdriften i økten din vil ikke bli lagret.", + "practiceGrammar": "Øv på grammatikk", + "notEnoughToPractice": "Send flere meldinger for å låse opp øving", + "constructUseCorGCDesc": "Øvelse i korrekt grammatikkategori", + "constructUseIncGCDesc": "Øvelse i ukorrekt grammatikkategori", + "@exitPractice": { + "type": "String", + "placeholders": {} + }, + "@practiceGrammar": { + "type": "String", + "placeholders": {} + }, + "@notEnoughToPractice": { + "type": "String", + "placeholders": {} + }, + "@constructUseCorGCDesc": { + "type": "String", + "placeholders": {} + }, + "@constructUseIncGCDesc": { + "type": "String", + "placeholders": {} } } \ No newline at end of file diff --git a/lib/l10n/intl_nl.arb b/lib/l10n/intl_nl.arb index 6b2d28a48..818b6bc60 100644 --- a/lib/l10n/intl_nl.arb +++ b/lib/l10n/intl_nl.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2026-01-13 15:17:28.318296", + "@@last_modified": "2026-01-16 14:33:40.784777", "about": "Over ons", "@about": { "type": "String", @@ -10840,5 +10840,30 @@ "@webDownloadPermissionMessage": { "type": "String", "placeholders": {} + }, + "exitPractice": "Uw voortgang in de oefensessie wordt niet opgeslagen.", + "practiceGrammar": "Oefen grammatica", + "notEnoughToPractice": "Stuur meer berichten om de oefening te ontgrendelen", + "constructUseCorGCDesc": "Oefening in de juiste grammaticacategorie", + "constructUseIncGCDesc": "Oefening in de onjuiste grammaticacategorie", + "@exitPractice": { + "type": "String", + "placeholders": {} + }, + "@practiceGrammar": { + "type": "String", + "placeholders": {} + }, + "@notEnoughToPractice": { + "type": "String", + "placeholders": {} + }, + "@constructUseCorGCDesc": { + "type": "String", + "placeholders": {} + }, + "@constructUseIncGCDesc": { + "type": "String", + "placeholders": {} } } \ No newline at end of file diff --git a/lib/l10n/intl_pl.arb b/lib/l10n/intl_pl.arb index b3d3320da..a8f3c5d85 100644 --- a/lib/l10n/intl_pl.arb +++ b/lib/l10n/intl_pl.arb @@ -1,6 +1,6 @@ { "@@locale": "pl", - "@@last_modified": "2026-01-13 15:17:35.488276", + "@@last_modified": "2026-01-16 14:33:54.131854", "about": "O aplikacji", "@about": { "type": "String", @@ -10838,5 +10838,30 @@ "@webDownloadPermissionMessage": { "type": "String", "placeholders": {} + }, + "exitPractice": "Postęp Twojej sesji ćwiczeń nie zostanie zapisany.", + "practiceGrammar": "Ćwicz gramatykę", + "notEnoughToPractice": "Wyślij więcej wiadomości, aby odblokować ćwiczenia", + "constructUseCorGCDesc": "Ćwiczenie poprawnej kategorii gramatycznej", + "constructUseIncGCDesc": "Ćwiczenie niepoprawnej kategorii gramatycznej", + "@exitPractice": { + "type": "String", + "placeholders": {} + }, + "@practiceGrammar": { + "type": "String", + "placeholders": {} + }, + "@notEnoughToPractice": { + "type": "String", + "placeholders": {} + }, + "@constructUseCorGCDesc": { + "type": "String", + "placeholders": {} + }, + "@constructUseIncGCDesc": { + "type": "String", + "placeholders": {} } } \ No newline at end of file diff --git a/lib/l10n/intl_pt.arb b/lib/l10n/intl_pt.arb index 2b3b819cf..186e08e77 100644 --- a/lib/l10n/intl_pt.arb +++ b/lib/l10n/intl_pt.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2026-01-13 15:16:49.002664", + "@@last_modified": "2026-01-16 14:32:38.304072", "copiedToClipboard": "Copiada para a área de transferência", "@copiedToClipboard": { "type": "String", @@ -11940,5 +11940,30 @@ "@webDownloadPermissionMessage": { "type": "String", "placeholders": {} + }, + "exitPractice": "O progresso da sua sessão de prática não será salvo.", + "practiceGrammar": "Praticar gramática", + "notEnoughToPractice": "Envie mais mensagens para desbloquear a prática", + "constructUseCorGCDesc": "Prática da categoria de gramática correta", + "constructUseIncGCDesc": "Prática da categoria de gramática incorreta", + "@exitPractice": { + "type": "String", + "placeholders": {} + }, + "@practiceGrammar": { + "type": "String", + "placeholders": {} + }, + "@notEnoughToPractice": { + "type": "String", + "placeholders": {} + }, + "@constructUseCorGCDesc": { + "type": "String", + "placeholders": {} + }, + "@constructUseIncGCDesc": { + "type": "String", + "placeholders": {} } } \ No newline at end of file diff --git a/lib/l10n/intl_pt_BR.arb b/lib/l10n/intl_pt_BR.arb index f92547995..a351a7185 100644 --- a/lib/l10n/intl_pt_BR.arb +++ b/lib/l10n/intl_pt_BR.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2026-01-13 15:16:45.933049", + "@@last_modified": "2026-01-16 14:32:33.894131", "about": "Sobre", "@about": { "type": "String", @@ -11198,5 +11198,30 @@ "@webDownloadPermissionMessage": { "type": "String", "placeholders": {} + }, + "exitPractice": "O progresso da sua sessão de prática não será salvo.", + "practiceGrammar": "Praticar gramática", + "notEnoughToPractice": "Envie mais mensagens para desbloquear a prática", + "constructUseCorGCDesc": "Prática da categoria de gramática correta", + "constructUseIncGCDesc": "Prática da categoria de gramática incorreta", + "@exitPractice": { + "type": "String", + "placeholders": {} + }, + "@practiceGrammar": { + "type": "String", + "placeholders": {} + }, + "@notEnoughToPractice": { + "type": "String", + "placeholders": {} + }, + "@constructUseCorGCDesc": { + "type": "String", + "placeholders": {} + }, + "@constructUseIncGCDesc": { + "type": "String", + "placeholders": {} } } \ No newline at end of file diff --git a/lib/l10n/intl_pt_PT.arb b/lib/l10n/intl_pt_PT.arb index 88ce37778..6439f1fd4 100644 --- a/lib/l10n/intl_pt_PT.arb +++ b/lib/l10n/intl_pt_PT.arb @@ -3331,7 +3331,7 @@ "selectAll": "Selecionar tudo", "deselectAll": "Desmarcar tudo", "@@locale": "pt_PT", - "@@last_modified": "2026-01-13 15:17:11.533911", + "@@last_modified": "2026-01-16 14:33:14.369639", "@alwaysUse24HourFormat": { "type": "String", "placeholders": {} @@ -11869,5 +11869,30 @@ "@webDownloadPermissionMessage": { "type": "String", "placeholders": {} + }, + "exitPractice": "O progresso da sua sessão de prática não será salvo.", + "practiceGrammar": "Praticar gramática", + "notEnoughToPractice": "Envie mais mensagens para desbloquear a prática", + "constructUseCorGCDesc": "Prática da categoria de gramática correta", + "constructUseIncGCDesc": "Prática da categoria de gramática incorreta", + "@exitPractice": { + "type": "String", + "placeholders": {} + }, + "@practiceGrammar": { + "type": "String", + "placeholders": {} + }, + "@notEnoughToPractice": { + "type": "String", + "placeholders": {} + }, + "@constructUseCorGCDesc": { + "type": "String", + "placeholders": {} + }, + "@constructUseIncGCDesc": { + "type": "String", + "placeholders": {} } } \ No newline at end of file diff --git a/lib/l10n/intl_ro.arb b/lib/l10n/intl_ro.arb index 808412218..c39285495 100644 --- a/lib/l10n/intl_ro.arb +++ b/lib/l10n/intl_ro.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2026-01-13 15:16:36.323428", + "@@last_modified": "2026-01-16 14:32:16.042822", "about": "Despre", "@about": { "type": "String", @@ -11575,5 +11575,30 @@ "@webDownloadPermissionMessage": { "type": "String", "placeholders": {} + }, + "exitPractice": "Progresul sesiunii tale de practică nu va fi salvat.", + "practiceGrammar": "Exersează gramatică", + "notEnoughToPractice": "Trimite mai multe mesaje pentru a debloca practica", + "constructUseCorGCDesc": "Practică categoria de gramatică corectă", + "constructUseIncGCDesc": "Practică categoria de gramatică incorectă", + "@exitPractice": { + "type": "String", + "placeholders": {} + }, + "@practiceGrammar": { + "type": "String", + "placeholders": {} + }, + "@notEnoughToPractice": { + "type": "String", + "placeholders": {} + }, + "@constructUseCorGCDesc": { + "type": "String", + "placeholders": {} + }, + "@constructUseIncGCDesc": { + "type": "String", + "placeholders": {} } } \ No newline at end of file diff --git a/lib/l10n/intl_ru.arb b/lib/l10n/intl_ru.arb index 406eb3320..42793afeb 100644 --- a/lib/l10n/intl_ru.arb +++ b/lib/l10n/intl_ru.arb @@ -1,6 +1,6 @@ { "@@locale": "ru", - "@@last_modified": "2026-01-13 15:17:45.324896", + "@@last_modified": "2026-01-16 14:34:10.739852", "about": "О проекте", "@about": { "type": "String", @@ -10945,5 +10945,30 @@ "@webDownloadPermissionMessage": { "type": "String", "placeholders": {} + }, + "exitPractice": "Ваш прогресс в сессии практики не будет сохранен.", + "practiceGrammar": "Практика грамматики", + "notEnoughToPractice": "Отправьте больше сообщений, чтобы разблокировать практику", + "constructUseCorGCDesc": "Практика правильной грамматики", + "constructUseIncGCDesc": "Практика неправильной грамматики", + "@exitPractice": { + "type": "String", + "placeholders": {} + }, + "@practiceGrammar": { + "type": "String", + "placeholders": {} + }, + "@notEnoughToPractice": { + "type": "String", + "placeholders": {} + }, + "@constructUseCorGCDesc": { + "type": "String", + "placeholders": {} + }, + "@constructUseIncGCDesc": { + "type": "String", + "placeholders": {} } } \ No newline at end of file diff --git a/lib/l10n/intl_sk.arb b/lib/l10n/intl_sk.arb index dfefa0620..fa42e9927 100644 --- a/lib/l10n/intl_sk.arb +++ b/lib/l10n/intl_sk.arb @@ -1,6 +1,6 @@ { "@@locale": "sk", - "@@last_modified": "2026-01-13 15:16:38.044008", + "@@last_modified": "2026-01-16 14:32:18.425596", "about": "O aplikácii", "@about": { "type": "String", @@ -11924,5 +11924,30 @@ "@webDownloadPermissionMessage": { "type": "String", "placeholders": {} + }, + "exitPractice": "Pokrok vo vašej cvičebnej relácii nebude uložený.", + "practiceGrammar": "Cvičiť gramatiku", + "notEnoughToPractice": "Odošlite viac správ na odomknutie cvičenia", + "constructUseCorGCDesc": "Cvičenie správnej gramatickej kategórie", + "constructUseIncGCDesc": "Cvičenie nesprávnej gramatickej kategórie", + "@exitPractice": { + "type": "String", + "placeholders": {} + }, + "@practiceGrammar": { + "type": "String", + "placeholders": {} + }, + "@notEnoughToPractice": { + "type": "String", + "placeholders": {} + }, + "@constructUseCorGCDesc": { + "type": "String", + "placeholders": {} + }, + "@constructUseIncGCDesc": { + "type": "String", + "placeholders": {} } } \ No newline at end of file diff --git a/lib/l10n/intl_sl.arb b/lib/l10n/intl_sl.arb index 942ad39ea..782e7cbc3 100644 --- a/lib/l10n/intl_sl.arb +++ b/lib/l10n/intl_sl.arb @@ -2464,7 +2464,7 @@ "playWithAI": "Za zdaj igrajte z AI-jem", "courseStartDesc": "Pangea Bot je pripravljen kadarkoli!\n\n...ampak je bolje učiti se s prijatelji!", "@@locale": "sl", - "@@last_modified": "2026-01-13 15:16:54.017311", + "@@last_modified": "2026-01-16 14:32:46.504404", "@alwaysUse24HourFormat": { "type": "String", "placeholders": {} @@ -11921,5 +11921,30 @@ "@webDownloadPermissionMessage": { "type": "String", "placeholders": {} + }, + "exitPractice": "Napredek vaše seje vadbe ne bo shranjen.", + "practiceGrammar": "Vadite slovnico", + "notEnoughToPractice": "Pošljite več sporočil, da odklenete vadbo", + "constructUseCorGCDesc": "Vadba pravilne slovnice", + "constructUseIncGCDesc": "Vadba nepravilne slovnice", + "@exitPractice": { + "type": "String", + "placeholders": {} + }, + "@practiceGrammar": { + "type": "String", + "placeholders": {} + }, + "@notEnoughToPractice": { + "type": "String", + "placeholders": {} + }, + "@constructUseCorGCDesc": { + "type": "String", + "placeholders": {} + }, + "@constructUseIncGCDesc": { + "type": "String", + "placeholders": {} } } \ No newline at end of file diff --git a/lib/l10n/intl_sr.arb b/lib/l10n/intl_sr.arb index 7b270eecc..481093692 100644 --- a/lib/l10n/intl_sr.arb +++ b/lib/l10n/intl_sr.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2026-01-13 15:17:49.129767", + "@@last_modified": "2026-01-16 14:34:16.018548", "about": "О програму", "@about": { "type": "String", @@ -11942,5 +11942,30 @@ "@webDownloadPermissionMessage": { "type": "String", "placeholders": {} + }, + "exitPractice": "Vaš napredak u vežbanju neće biti sačuvan.", + "practiceGrammar": "Vežbajte gramatiku", + "notEnoughToPractice": "Pošaljite više poruka da otključate vežbanje", + "constructUseCorGCDesc": "Vežbanje ispravne gramatike", + "constructUseIncGCDesc": "Vežbanje nepravilne gramatike", + "@exitPractice": { + "type": "String", + "placeholders": {} + }, + "@practiceGrammar": { + "type": "String", + "placeholders": {} + }, + "@notEnoughToPractice": { + "type": "String", + "placeholders": {} + }, + "@constructUseCorGCDesc": { + "type": "String", + "placeholders": {} + }, + "@constructUseIncGCDesc": { + "type": "String", + "placeholders": {} } } \ No newline at end of file diff --git a/lib/l10n/intl_sv.arb b/lib/l10n/intl_sv.arb index 108f48128..117b86faa 100644 --- a/lib/l10n/intl_sv.arb +++ b/lib/l10n/intl_sv.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2026-01-13 15:17:36.926028", + "@@last_modified": "2026-01-16 14:33:56.060391", "about": "Om", "@about": { "type": "String", @@ -11318,5 +11318,30 @@ "@webDownloadPermissionMessage": { "type": "String", "placeholders": {} + }, + "exitPractice": "Din övningssession kommer inte att sparas.", + "practiceGrammar": "Öva grammatik", + "notEnoughToPractice": "Skicka fler meddelanden för att låsa upp övning", + "constructUseCorGCDesc": "Övning i korrekt grammatikkategori", + "constructUseIncGCDesc": "Övning i inkorrekt grammatikkategori", + "@exitPractice": { + "type": "String", + "placeholders": {} + }, + "@practiceGrammar": { + "type": "String", + "placeholders": {} + }, + "@notEnoughToPractice": { + "type": "String", + "placeholders": {} + }, + "@constructUseCorGCDesc": { + "type": "String", + "placeholders": {} + }, + "@constructUseIncGCDesc": { + "type": "String", + "placeholders": {} } } \ No newline at end of file diff --git a/lib/l10n/intl_ta.arb b/lib/l10n/intl_ta.arb index 3cbcd14e3..029b620fe 100644 --- a/lib/l10n/intl_ta.arb +++ b/lib/l10n/intl_ta.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2026-01-13 15:17:27.095421", + "@@last_modified": "2026-01-16 14:33:37.303345", "acceptedTheInvitation": "👍 {username} அழைப்பை ஏற்றுக்கொண்டது", "@acceptedTheInvitation": { "type": "String", @@ -11064,5 +11064,30 @@ "@webDownloadPermissionMessage": { "type": "String", "placeholders": {} + }, + "exitPractice": "உங்கள் பயிற்சி அமர்வின் முன்னேற்றம் சேமிக்கப்படாது.", + "practiceGrammar": "வியாசத்தை பயிற்சி செய்யவும்", + "notEnoughToPractice": "பயிற்சியை திறக்க மேலும் செய்திகளை அனுப்பவும்", + "constructUseCorGCDesc": "சரியான வியாச வகை பயிற்சி", + "constructUseIncGCDesc": "தவறான வியாச வகை பயிற்சி", + "@exitPractice": { + "type": "String", + "placeholders": {} + }, + "@practiceGrammar": { + "type": "String", + "placeholders": {} + }, + "@notEnoughToPractice": { + "type": "String", + "placeholders": {} + }, + "@constructUseCorGCDesc": { + "type": "String", + "placeholders": {} + }, + "@constructUseIncGCDesc": { + "type": "String", + "placeholders": {} } } \ No newline at end of file diff --git a/lib/l10n/intl_te.arb b/lib/l10n/intl_te.arb index 2d08d5476..89018dac2 100644 --- a/lib/l10n/intl_te.arb +++ b/lib/l10n/intl_te.arb @@ -1920,7 +1920,7 @@ "playWithAI": "ఇప్పుడే AI తో ఆడండి", "courseStartDesc": "పాంజియా బాట్ ఎప్పుడైనా సిద్ధంగా ఉంటుంది!\n\n...కానీ స్నేహితులతో నేర్చుకోవడం మెరుగైనది!", "@@locale": "te", - "@@last_modified": "2026-01-13 15:17:21.774356", + "@@last_modified": "2026-01-16 14:33:29.527681", "@setCustomPermissionLevel": { "type": "String", "placeholders": {} @@ -11929,5 +11929,30 @@ "@webDownloadPermissionMessage": { "type": "String", "placeholders": {} + }, + "exitPractice": "మీ ప్రాక్టీస్ సెషన్ పురోగతి సేవ్ చేయబడదు.", + "practiceGrammar": "వ్యాకరణాన్ని అభ్యాసం చేయండి", + "notEnoughToPractice": "ప్రాక్టీస్‌ను అన్లాక్ చేయడానికి మరింత సందేశాలు పంపండి", + "constructUseCorGCDesc": "సరైన వ్యాకరణ శ్రేణి ప్రాక్టీస్", + "constructUseIncGCDesc": "తప్పు వ్యాకరణ శ్రేణి ప్రాక్టీస్", + "@exitPractice": { + "type": "String", + "placeholders": {} + }, + "@practiceGrammar": { + "type": "String", + "placeholders": {} + }, + "@notEnoughToPractice": { + "type": "String", + "placeholders": {} + }, + "@constructUseCorGCDesc": { + "type": "String", + "placeholders": {} + }, + "@constructUseIncGCDesc": { + "type": "String", + "placeholders": {} } } \ No newline at end of file diff --git a/lib/l10n/intl_th.arb b/lib/l10n/intl_th.arb index 9a7649da4..e6d5c7213 100644 --- a/lib/l10n/intl_th.arb +++ b/lib/l10n/intl_th.arb @@ -4456,7 +4456,7 @@ "playWithAI": "เล่นกับ AI ชั่วคราว", "courseStartDesc": "Pangea Bot พร้อมที่จะเริ่มต้นได้ทุกเมื่อ!\n\n...แต่การเรียนรู้ดีกว่ากับเพื่อน!", "@@locale": "th", - "@@last_modified": "2026-01-13 15:17:10.203231", + "@@last_modified": "2026-01-16 14:33:11.816510", "@alwaysUse24HourFormat": { "type": "String", "placeholders": {} @@ -11898,5 +11898,30 @@ "@webDownloadPermissionMessage": { "type": "String", "placeholders": {} + }, + "exitPractice": "ความก้าวหน้าของการฝึกฝนของคุณจะไม่ถูกบันทึก", + "practiceGrammar": "ฝึกไวยากรณ์", + "notEnoughToPractice": "ส่งข้อความเพิ่มเติมเพื่อปลดล็อกการฝึกฝน", + "constructUseCorGCDesc": "การฝึกไวยากรณ์หมวดหมู่ที่ถูกต้อง", + "constructUseIncGCDesc": "การฝึกไวยากรณ์หมวดหมู่ที่ไม่ถูกต้อง", + "@exitPractice": { + "type": "String", + "placeholders": {} + }, + "@practiceGrammar": { + "type": "String", + "placeholders": {} + }, + "@notEnoughToPractice": { + "type": "String", + "placeholders": {} + }, + "@constructUseCorGCDesc": { + "type": "String", + "placeholders": {} + }, + "@constructUseIncGCDesc": { + "type": "String", + "placeholders": {} } } \ No newline at end of file diff --git a/lib/l10n/intl_tr.arb b/lib/l10n/intl_tr.arb index 5565e862c..b3abdfecc 100644 --- a/lib/l10n/intl_tr.arb +++ b/lib/l10n/intl_tr.arb @@ -1,6 +1,6 @@ { "@@locale": "tr", - "@@last_modified": "2026-01-13 15:17:19.618708", + "@@last_modified": "2026-01-16 14:33:26.649412", "about": "Hakkında", "@about": { "type": "String", @@ -11062,5 +11062,30 @@ "@webDownloadPermissionMessage": { "type": "String", "placeholders": {} + }, + "exitPractice": "Pratik oturumunuzun ilerlemesi kaydedilmeyecek.", + "practiceGrammar": "Dil bilgisi pratiği yap", + "notEnoughToPractice": "Pratik yapmak için daha fazla mesaj gönderin", + "constructUseCorGCDesc": "Doğru dil bilgisi kategorisi pratiği", + "constructUseIncGCDesc": "Yanlış dil bilgisi kategorisi pratiği", + "@exitPractice": { + "type": "String", + "placeholders": {} + }, + "@practiceGrammar": { + "type": "String", + "placeholders": {} + }, + "@notEnoughToPractice": { + "type": "String", + "placeholders": {} + }, + "@constructUseCorGCDesc": { + "type": "String", + "placeholders": {} + }, + "@constructUseIncGCDesc": { + "type": "String", + "placeholders": {} } } \ No newline at end of file diff --git a/lib/l10n/intl_uk.arb b/lib/l10n/intl_uk.arb index aca41114b..d4c6ba2bc 100644 --- a/lib/l10n/intl_uk.arb +++ b/lib/l10n/intl_uk.arb @@ -1,6 +1,6 @@ { "@@locale": "uk", - "@@last_modified": "2026-01-13 15:17:01.594985", + "@@last_modified": "2026-01-16 14:32:56.338443", "about": "Про застосунок", "@about": { "type": "String", @@ -10834,5 +10834,30 @@ "@webDownloadPermissionMessage": { "type": "String", "placeholders": {} + }, + "exitPractice": "Ваш прогрес у сесії практики не буде збережено.", + "practiceGrammar": "Практика граматики", + "notEnoughToPractice": "Надішліть більше повідомлень, щоб розблокувати практику", + "constructUseCorGCDesc": "Практика правильної граматичної категорії", + "constructUseIncGCDesc": "Практика неправильної граматичної категорії", + "@exitPractice": { + "type": "String", + "placeholders": {} + }, + "@practiceGrammar": { + "type": "String", + "placeholders": {} + }, + "@notEnoughToPractice": { + "type": "String", + "placeholders": {} + }, + "@constructUseCorGCDesc": { + "type": "String", + "placeholders": {} + }, + "@constructUseIncGCDesc": { + "type": "String", + "placeholders": {} } } \ No newline at end of file diff --git a/lib/l10n/intl_vi.arb b/lib/l10n/intl_vi.arb index 8fa9fa817..22a5c5696 100644 --- a/lib/l10n/intl_vi.arb +++ b/lib/l10n/intl_vi.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2026-01-13 15:17:24.923117", + "@@last_modified": "2026-01-16 14:33:34.535886", "about": "Giới thiệu", "@about": { "type": "String", @@ -6410,5 +6410,30 @@ "@webDownloadPermissionMessage": { "type": "String", "placeholders": {} + }, + "exitPractice": "Tiến trình phiên thực hành của bạn sẽ không được lưu.", + "practiceGrammar": "Thực hành ngữ pháp", + "notEnoughToPractice": "Gửi thêm tin nhắn để mở khóa thực hành", + "constructUseCorGCDesc": "Thực hành thể loại ngữ pháp đúng", + "constructUseIncGCDesc": "Thực hành thể loại ngữ pháp sai", + "@exitPractice": { + "type": "String", + "placeholders": {} + }, + "@practiceGrammar": { + "type": "String", + "placeholders": {} + }, + "@notEnoughToPractice": { + "type": "String", + "placeholders": {} + }, + "@constructUseCorGCDesc": { + "type": "String", + "placeholders": {} + }, + "@constructUseIncGCDesc": { + "type": "String", + "placeholders": {} } } \ No newline at end of file diff --git a/lib/l10n/intl_yue.arb b/lib/l10n/intl_yue.arb index 2b19cae4d..e51a60484 100644 --- a/lib/l10n/intl_yue.arb +++ b/lib/l10n/intl_yue.arb @@ -1856,7 +1856,7 @@ "selectAll": "全選", "deselectAll": "取消全選", "@@locale": "yue", - "@@last_modified": "2026-01-13 15:16:56.151795", + "@@last_modified": "2026-01-16 14:32:49.483939", "@ignoreUser": { "type": "String", "placeholders": {} @@ -11931,5 +11931,30 @@ "@webDownloadPermissionMessage": { "type": "String", "placeholders": {} + }, + "exitPractice": "您的練習進度將不會被保存。", + "practiceGrammar": "練習語法", + "notEnoughToPractice": "發送更多消息以解鎖練習", + "constructUseCorGCDesc": "正確語法類別練習", + "constructUseIncGCDesc": "不正確語法類別練習", + "@exitPractice": { + "type": "String", + "placeholders": {} + }, + "@practiceGrammar": { + "type": "String", + "placeholders": {} + }, + "@notEnoughToPractice": { + "type": "String", + "placeholders": {} + }, + "@constructUseCorGCDesc": { + "type": "String", + "placeholders": {} + }, + "@constructUseIncGCDesc": { + "type": "String", + "placeholders": {} } } \ No newline at end of file diff --git a/lib/l10n/intl_zh.arb b/lib/l10n/intl_zh.arb index 36861b092..212d37e2b 100644 --- a/lib/l10n/intl_zh.arb +++ b/lib/l10n/intl_zh.arb @@ -1,6 +1,6 @@ { "@@locale": "zh", - "@@last_modified": "2026-01-13 15:17:30.643003", + "@@last_modified": "2026-01-16 14:33:45.909821", "about": "关于", "@about": { "type": "String", @@ -10831,5 +10831,30 @@ "@webDownloadPermissionMessage": { "type": "String", "placeholders": {} + }, + "exitPractice": "您的练习会话进度将不会被保存。", + "practiceGrammar": "练习语法", + "notEnoughToPractice": "发送更多消息以解锁练习", + "constructUseCorGCDesc": "正确语法类别练习", + "constructUseIncGCDesc": "错误语法类别练习", + "@exitPractice": { + "type": "String", + "placeholders": {} + }, + "@practiceGrammar": { + "type": "String", + "placeholders": {} + }, + "@notEnoughToPractice": { + "type": "String", + "placeholders": {} + }, + "@constructUseCorGCDesc": { + "type": "String", + "placeholders": {} + }, + "@constructUseIncGCDesc": { + "type": "String", + "placeholders": {} } } \ No newline at end of file diff --git a/lib/l10n/intl_zh_Hant.arb b/lib/l10n/intl_zh_Hant.arb index 85c844340..01b708716 100644 --- a/lib/l10n/intl_zh_Hant.arb +++ b/lib/l10n/intl_zh_Hant.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2026-01-13 15:17:12.846069", + "@@last_modified": "2026-01-16 14:33:16.316521", "about": "關於", "@about": { "type": "String", @@ -10838,5 +10838,30 @@ "@webDownloadPermissionMessage": { "type": "String", "placeholders": {} + }, + "exitPractice": "您的練習進度將不會被保存。", + "practiceGrammar": "練習文法", + "notEnoughToPractice": "發送更多訊息以解鎖練習", + "constructUseCorGCDesc": "正確文法類別練習", + "constructUseIncGCDesc": "不正確文法類別練習", + "@exitPractice": { + "type": "String", + "placeholders": {} + }, + "@practiceGrammar": { + "type": "String", + "placeholders": {} + }, + "@notEnoughToPractice": { + "type": "String", + "placeholders": {} + }, + "@constructUseCorGCDesc": { + "type": "String", + "placeholders": {} + }, + "@constructUseIncGCDesc": { + "type": "String", + "placeholders": {} } } \ No newline at end of file From 232f5342e23bb116e5b3fc6bc8bff85bfd0e592f Mon Sep 17 00:00:00 2001 From: ggurdin Date: Fri, 16 Jan 2026 15:47:08 -0500 Subject: [PATCH 06/27] fix: add bulk updating of uses list --- lib/pangea/analytics_data/analytics_database.dart | 5 +---- lib/pangea/analytics_misc/construct_use_model.dart | 4 ++-- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/lib/pangea/analytics_data/analytics_database.dart b/lib/pangea/analytics_data/analytics_database.dart index 8a1f089c5..cb215ae94 100644 --- a/lib/pangea/analytics_data/analytics_database.dart +++ b/lib/pangea/analytics_data/analytics_database.dart @@ -395,10 +395,7 @@ class AnalyticsDatabase with DatabaseFileStorage { ); } - for (final u in usesForKey) { - model.addUse(u); - } - + model.addUses(usesForKey); updates[key] = model; } diff --git a/lib/pangea/analytics_misc/construct_use_model.dart b/lib/pangea/analytics_misc/construct_use_model.dart index d1ef0eaeb..e3a3ddda5 100644 --- a/lib/pangea/analytics_misc/construct_use_model.dart +++ b/lib/pangea/analytics_misc/construct_use_model.dart @@ -140,8 +140,8 @@ class ConstructUses { _uses.sort((a, b) => a.timeStamp.compareTo(b.timeStamp)); } - void addUse(OneConstructUse use) { - _uses.add(use); + void addUses(List uses) { + _uses.addAll(uses); _sortUses(); } From 4ac11c7e7e3a45542a064517bbc7c0fdaadb2598 Mon Sep 17 00:00:00 2001 From: ggurdin Date: Fri, 16 Jan 2026 16:11:53 -0500 Subject: [PATCH 07/27] chore: add TTL to phonetic transcription local storage --- .../phonetic_transcription_repo.dart | 48 ++++++++++++++++--- 1 file changed, 42 insertions(+), 6 deletions(-) diff --git a/lib/pangea/phonetic_transcription/phonetic_transcription_repo.dart b/lib/pangea/phonetic_transcription/phonetic_transcription_repo.dart index d42728437..d7df0b778 100644 --- a/lib/pangea/phonetic_transcription/phonetic_transcription_repo.dart +++ b/lib/pangea/phonetic_transcription/phonetic_transcription_repo.dart @@ -12,20 +12,47 @@ import 'package:fluffychat/pangea/common/utils/error_handler.dart'; import 'package:fluffychat/pangea/phonetic_transcription/phonetic_transcription_request.dart'; import 'package:fluffychat/pangea/phonetic_transcription/phonetic_transcription_response.dart'; -class _PhoneticTranscriptionCacheItem { +class _PhoneticTranscriptionMemoryCacheItem { final Future> resultFuture; final DateTime timestamp; - const _PhoneticTranscriptionCacheItem({ + const _PhoneticTranscriptionMemoryCacheItem({ required this.resultFuture, required this.timestamp, }); } +class _PhoneticTranscriptionStorageCacheItem { + final PhoneticTranscriptionResponse response; + final DateTime timestamp; + + const _PhoneticTranscriptionStorageCacheItem({ + required this.response, + required this.timestamp, + }); + + Map toJson() { + return { + 'response': response.toJson(), + 'timestamp': timestamp.toIso8601String(), + }; + } + + static _PhoneticTranscriptionStorageCacheItem fromJson( + Map json, + ) { + return _PhoneticTranscriptionStorageCacheItem( + response: PhoneticTranscriptionResponse.fromJson(json['response']), + timestamp: DateTime.parse(json['timestamp']), + ); + } +} + class PhoneticTranscriptionRepo { // In-memory cache - static final Map _cache = {}; + static final Map _cache = {}; static const Duration _cacheDuration = Duration(minutes: 10); + static const Duration _storageDuration = Duration(days: 7); // Persistent storage static final GetStorage _storage = @@ -53,7 +80,7 @@ class PhoneticTranscriptionRepo { final future = _safeFetch(accessToken, request); // 4. Save to in-memory cache - _cache[request.hashCode.toString()] = _PhoneticTranscriptionCacheItem( + _cache[request.hashCode.toString()] = _PhoneticTranscriptionMemoryCacheItem( resultFuture: future, timestamp: DateTime.now(), ); @@ -71,7 +98,11 @@ class PhoneticTranscriptionRepo { await GetStorage.init('phonetic_transcription_storage'); final key = request.hashCode.toString(); try { - await _storage.write(key, resultFuture.toJson()); + final item = _PhoneticTranscriptionStorageCacheItem( + response: resultFuture, + timestamp: DateTime.now(), + ); + await _storage.write(key, item.toJson()); _cache.remove(key); // Invalidate in-memory cache } catch (e, s) { ErrorHandler.logError( @@ -154,7 +185,12 @@ class PhoneticTranscriptionRepo { final entry = _storage.read(key); if (entry == null) return null; - return PhoneticTranscriptionResponse.fromJson(entry); + final item = _PhoneticTranscriptionStorageCacheItem.fromJson(entry); + if (DateTime.now().difference(item.timestamp) >= _storageDuration) { + _storage.remove(key); + return null; + } + return item.response; } catch (e, s) { ErrorHandler.logError( e: e, From c3f74722248836cfb5296f9410cc67487a0deadc Mon Sep 17 00:00:00 2001 From: Ava Shilling <165050625+avashilling@users.noreply.github.com> Date: Fri, 16 Jan 2026 16:52:32 -0500 Subject: [PATCH 08/27] replace constructXpWidget with progress bar for consistent graphics between vocab and grammar --- .../analytics_details_popup/morph_details_view.dart | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/lib/pangea/analytics_details_popup/morph_details_view.dart b/lib/pangea/analytics_details_popup/morph_details_view.dart index 6b22469a7..ddfe948d4 100644 --- a/lib/pangea/analytics_details_popup/morph_details_view.dart +++ b/lib/pangea/analytics_details_popup/morph_details_view.dart @@ -1,10 +1,10 @@ import 'package:flutter/material.dart'; import 'package:fluffychat/pangea/analytics_details_popup/analytics_details_usage_content.dart'; +import 'package:fluffychat/pangea/analytics_details_popup/construct_xp_progress_bar.dart'; import 'package:fluffychat/pangea/analytics_details_popup/morph_meaning_widget.dart'; import 'package:fluffychat/pangea/constructs/construct_identifier.dart'; import 'package:fluffychat/pangea/constructs/construct_level_enum.dart'; -import 'package:fluffychat/pangea/lemmas/construct_xp_widget.dart'; import 'package:fluffychat/pangea/morphs/morph_feature_display.dart'; import 'package:fluffychat/pangea/morphs/morph_features_enum.dart'; import 'package:fluffychat/pangea/morphs/morph_tag_display.dart'; @@ -54,11 +54,7 @@ class MorphDetailsView extends StatelessWidget { ), const Divider(), if (construct != null) ...[ - ConstructXpWidget( - icon: construct.lemmaCategory.icon(30.0), - level: construct.lemmaCategory, - points: construct.points, - ), + ConstructXPProgressBar(construct: construct.id), Padding( padding: const EdgeInsets.all(20.0), child: AnalyticsDetailsUsageContent( From 3f61607995cea8d2196c5e73a903c7da5cfcf9e4 Mon Sep 17 00:00:00 2001 From: ggurdin Date: Mon, 19 Jan 2026 09:32:03 -0500 Subject: [PATCH 09/27] remove unreferenced widget --- lib/pangea/lemmas/construct_xp_widget.dart | 37 ---------------------- 1 file changed, 37 deletions(-) delete mode 100644 lib/pangea/lemmas/construct_xp_widget.dart diff --git a/lib/pangea/lemmas/construct_xp_widget.dart b/lib/pangea/lemmas/construct_xp_widget.dart deleted file mode 100644 index 093c33806..000000000 --- a/lib/pangea/lemmas/construct_xp_widget.dart +++ /dev/null @@ -1,37 +0,0 @@ -import 'package:flutter/material.dart'; - -import 'package:fluffychat/pangea/constructs/construct_level_enum.dart'; - -class ConstructXpWidget extends StatelessWidget { - final ConstructLevelEnum level; - final int points; - final Widget icon; - - const ConstructXpWidget({ - super.key, - required this.level, - required this.points, - required this.icon, - }); - - @override - Widget build(BuildContext context) { - final Color textColor = Theme.of(context).brightness != Brightness.light - ? level.color(context) - : level.darkColor(context); - - return Row( - spacing: 16.0, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - icon, - Text( - "$points XP", - style: Theme.of(context).textTheme.titleMedium?.copyWith( - color: textColor, - ), - ), - ], - ); - } -} From 257fb465be7ecccbbc4d89a5ef5ca58039f107c8 Mon Sep 17 00:00:00 2001 From: ggurdin Date: Mon, 19 Jan 2026 12:18:27 -0500 Subject: [PATCH 10/27] chore: reload info in word card in vocab details after submitting token feedback --- .../analytics_details_popup.dart | 34 +++++- .../vocab_analytics_details_view.dart | 32 ++---- .../lemmas/lemma_highlight_emoji_row.dart | 71 ++++++------ lib/pangea/lemmas/lemma_meaning_builder.dart | 59 +++++----- lib/pangea/lemmas/lemma_meaning_widget.dart | 77 ------------- .../phonetic_transcription_builder.dart | 66 +++++------ .../phonetic_transcription_widget.dart | 104 +++++++++--------- .../show_token_feedback_dialog.dart | 3 + .../word_card/lemma_meaning_display.dart | 87 +++++++-------- .../toolbar/word_card/word_zoom_widget.dart | 4 + 10 files changed, 236 insertions(+), 301 deletions(-) delete mode 100644 lib/pangea/lemmas/lemma_meaning_widget.dart diff --git a/lib/pangea/analytics_details_popup/analytics_details_popup.dart b/lib/pangea/analytics_details_popup/analytics_details_popup.dart index 4a227e243..7bfa27c6a 100644 --- a/lib/pangea/analytics_details_popup/analytics_details_popup.dart +++ b/lib/pangea/analytics_details_popup/analytics_details_popup.dart @@ -18,9 +18,13 @@ import 'package:fluffychat/pangea/analytics_summary/learning_progress_indicators import 'package:fluffychat/pangea/common/utils/error_handler.dart'; import 'package:fluffychat/pangea/constructs/construct_identifier.dart'; import 'package:fluffychat/pangea/constructs/construct_level_enum.dart'; +import 'package:fluffychat/pangea/events/models/pangea_token_model.dart'; +import 'package:fluffychat/pangea/lemmas/lemma_info_response.dart'; import 'package:fluffychat/pangea/morphs/default_morph_mapping.dart'; import 'package:fluffychat/pangea/morphs/morph_models.dart'; import 'package:fluffychat/pangea/morphs/morph_repo.dart'; +import 'package:fluffychat/pangea/token_info_feedback/show_token_feedback_dialog.dart'; +import 'package:fluffychat/pangea/token_info_feedback/token_info_feedback_request.dart'; import 'package:fluffychat/widgets/matrix.dart'; class ConstructAnalyticsView extends StatefulWidget { @@ -49,6 +53,7 @@ class ConstructAnalyticsViewState extends State { FocusNode searchFocusNode = FocusNode(); ConstructLevelEnum? selectedConstructLevel; StreamSubscription? _constructUpdateSub; + final ValueNotifier reloadNotifier = ValueNotifier(0); @override void initState() { @@ -72,6 +77,7 @@ class ConstructAnalyticsViewState extends State { searchController.dispose(); _constructUpdateSub?.cancel(); searchFocusNode.dispose(); + reloadNotifier.dispose(); super.dispose(); } @@ -162,6 +168,29 @@ class ConstructAnalyticsViewState extends State { }); } + Future onFlagTokenInfo( + PangeaToken token, + LemmaInfoResponse lemmaInfo, + String phonetics, + ) async { + final requestData = TokenInfoFeedbackRequestData( + userId: Matrix.of(context).client.userID!, + detectedLanguage: MatrixState.pangeaController.userController.userL2Code!, + tokens: [token], + selectedToken: 0, + wordCardL1: MatrixState.pangeaController.userController.userL1Code!, + lemmaInfo: lemmaInfo, + phonetics: phonetics, + ); + + await TokenFeedbackUtil.showTokenFeedbackDialog( + context, + requestData: requestData, + langCode: MatrixState.pangeaController.userController.userL2Code!, + onUpdated: () => reloadNotifier.value++, + ); + } + @override Widget build(BuildContext context) { return Scaffold( @@ -182,7 +211,10 @@ class ConstructAnalyticsViewState extends State { : MorphDetailsView(constructId: widget.construct!) : widget.construct == null ? VocabAnalyticsListView(controller: this) - : VocabDetailsView(constructId: widget.construct!), + : VocabDetailsView( + constructId: widget.construct!, + controller: this, + ), ), ], ), diff --git a/lib/pangea/analytics_details_popup/vocab_analytics_details_view.dart b/lib/pangea/analytics_details_popup/vocab_analytics_details_view.dart index 58bafc387..79dda8441 100644 --- a/lib/pangea/analytics_details_popup/vocab_analytics_details_view.dart +++ b/lib/pangea/analytics_details_popup/vocab_analytics_details_view.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import 'package:collection/collection.dart'; import 'package:fluffychat/l10n/l10n.dart'; +import 'package:fluffychat/pangea/analytics_details_popup/analytics_details_popup.dart'; import 'package:fluffychat/pangea/analytics_details_popup/analytics_details_usage_content.dart'; import 'package:fluffychat/pangea/analytics_details_popup/construct_xp_progress_bar.dart'; import 'package:fluffychat/pangea/analytics_details_popup/word_text_with_audio_button.dart'; @@ -12,8 +13,6 @@ import 'package:fluffychat/pangea/events/models/pangea_token_model.dart'; import 'package:fluffychat/pangea/events/models/pangea_token_text_model.dart'; import 'package:fluffychat/pangea/lemmas/lemma.dart'; import 'package:fluffychat/pangea/lemmas/lemma_info_response.dart'; -import 'package:fluffychat/pangea/token_info_feedback/show_token_feedback_dialog.dart'; -import 'package:fluffychat/pangea/token_info_feedback/token_info_feedback_request.dart'; import 'package:fluffychat/pangea/toolbar/word_card/word_zoom_widget.dart'; import 'package:fluffychat/widgets/adaptive_dialogs/show_ok_cancel_alert_dialog.dart'; import 'package:fluffychat/widgets/future_loading_dialog.dart'; @@ -23,10 +22,12 @@ import 'package:fluffychat/widgets/matrix.dart'; /// Displays information about selected lemma, and its usage class VocabDetailsView extends StatelessWidget { final ConstructIdentifier constructId; + final ConstructAnalyticsViewState controller; const VocabDetailsView({ super.key, required this.constructId, + required this.controller, }); Future _blockLemma(BuildContext context) async { @@ -93,27 +94,12 @@ class VocabDetailsView extends StatelessWidget { MatrixState.pangeaController.userController.userL2Code!, construct: constructId, onClose: Navigator.of(context).pop, - onFlagTokenInfo: - (LemmaInfoResponse lemmaInfo, String phonetics) { - final requestData = TokenInfoFeedbackRequestData( - userId: Matrix.of(context).client.userID!, - detectedLanguage: MatrixState - .pangeaController.userController.userL2Code!, - tokens: [token], - selectedToken: 0, - wordCardL1: MatrixState - .pangeaController.userController.userL1Code!, - lemmaInfo: lemmaInfo, - phonetics: phonetics, - ); - - TokenFeedbackUtil.showTokenFeedbackDialog( - context, - requestData: requestData, - langCode: MatrixState - .pangeaController.userController.userL2Code!, - ); - }, + onFlagTokenInfo: ( + LemmaInfoResponse lemmaInfo, + String phonetics, + ) => + controller.onFlagTokenInfo(token, lemmaInfo, phonetics), + reloadNotifier: controller.reloadNotifier, maxWidth: double.infinity, ), ), diff --git a/lib/pangea/lemmas/lemma_highlight_emoji_row.dart b/lib/pangea/lemmas/lemma_highlight_emoji_row.dart index 4dec278b9..70d7ed1c2 100644 --- a/lib/pangea/lemmas/lemma_highlight_emoji_row.dart +++ b/lib/pangea/lemmas/lemma_highlight_emoji_row.dart @@ -6,6 +6,7 @@ import 'package:shimmer/shimmer.dart'; import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/pangea/analytics_data/analytics_updater_mixin.dart'; +import 'package:fluffychat/pangea/common/utils/async_state.dart'; import 'package:fluffychat/pangea/common/widgets/shimmer_background.dart'; import 'package:fluffychat/pangea/constructs/construct_identifier.dart'; import 'package:fluffychat/pangea/lemmas/lemma_meaning_builder.dart'; @@ -49,35 +50,15 @@ class LemmaHighlightEmojiRowState extends State constructId: widget.cId, messageInfo: widget.messageInfo, builder: (context, controller) { - if (controller.error != null) { - return const SizedBox.shrink(); - } - - final emojis = controller.lemmaInfo?.emoji; - return SizedBox( - height: 70.0, - child: Row( - spacing: 4.0, - mainAxisSize: MainAxisSize.min, - children: emojis == null || emojis.isEmpty - ? List.generate( - 3, - (_) => Shimmer.fromColors( - baseColor: Colors.transparent, - highlightColor: - Theme.of(context).colorScheme.primary.withAlpha(70), - child: Container( - height: 55.0, - width: 55.0, - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.primary, - borderRadius: - BorderRadius.circular(AppConfig.borderRadius), - ), - ), - ), - ) - : emojis.map( + return switch (controller.state) { + AsyncError() => const SizedBox.shrink(), + AsyncLoaded(value: final lemmaInfo) => SizedBox( + height: 70.0, + child: Row( + spacing: 4.0, + mainAxisSize: MainAxisSize.min, + children: [ + ...lemmaInfo.emoji.map( (emoji) { final targetId = "${widget.targetId}-$emoji"; return EmojiChoiceItem( @@ -94,9 +75,35 @@ class LemmaHighlightEmojiRowState extends State enabled: widget.enabled, ); }, - ).toList(), - ), - ); + ), + ], + ), + ), + _ => SizedBox( + height: 70.0, + child: Row( + spacing: 4.0, + mainAxisSize: MainAxisSize.min, + children: List.generate( + 3, + (_) => Shimmer.fromColors( + baseColor: Colors.transparent, + highlightColor: + Theme.of(context).colorScheme.primary.withAlpha(70), + child: Container( + height: 55.0, + width: 55.0, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.primary, + borderRadius: + BorderRadius.circular(AppConfig.borderRadius), + ), + ), + ), + ), + ), + ), + }; }, ); } diff --git a/lib/pangea/lemmas/lemma_meaning_builder.dart b/lib/pangea/lemmas/lemma_meaning_builder.dart index b6cfcd0f3..78a598833 100644 --- a/lib/pangea/lemmas/lemma_meaning_builder.dart +++ b/lib/pangea/lemmas/lemma_meaning_builder.dart @@ -8,29 +8,11 @@ import 'package:fluffychat/pangea/lemmas/lemma_info_request.dart'; import 'package:fluffychat/pangea/lemmas/lemma_info_response.dart'; import 'package:fluffychat/widgets/matrix.dart'; -class _LemmaMeaningLoader extends AsyncLoader { - final LemmaInfoRequest request; - _LemmaMeaningLoader(this.request) : super(); - - @override - Future fetch() async { - final result = await LemmaInfoRepo.get( - MatrixState.pangeaController.userController.accessToken, - request, - ); - - if (result.isError) { - throw result.asError!.error; - } - - return result.asValue!.value; - } -} - class LemmaMeaningBuilder extends StatefulWidget { final String langCode; final ConstructIdentifier constructId; final Map messageInfo; + final ValueNotifier? reloadNotifier; final Widget Function( BuildContext context, @@ -43,6 +25,7 @@ class LemmaMeaningBuilder extends StatefulWidget { required this.constructId, required this.builder, required this.messageInfo, + this.reloadNotifier, }); @override @@ -50,12 +33,14 @@ class LemmaMeaningBuilder extends StatefulWidget { } class LemmaMeaningBuilderState extends State { - late _LemmaMeaningLoader _loader; + final ValueNotifier> _loader = + ValueNotifier(const AsyncState.idle()); @override void initState() { super.initState(); - _reload(); + _load(); + widget.reloadNotifier?.addListener(_load); } @override @@ -63,24 +48,22 @@ class LemmaMeaningBuilderState extends State { super.didUpdateWidget(oldWidget); if (oldWidget.constructId != widget.constructId || oldWidget.langCode != widget.langCode) { - _loader.dispose(); - _reload(); + _load(); } } @override void dispose() { + widget.reloadNotifier?.removeListener(_load); _loader.dispose(); super.dispose(); } - bool get isLoading => _loader.isLoading; - bool get isError => _loader.isError; - - Object? get error => - isError ? (_loader.state.value as AsyncError).error : null; - - LemmaInfoResponse? get lemmaInfo => _loader.value; + AsyncState get state => _loader.value; + bool get isError => _loader.value is AsyncError; + bool get isLoaded => _loader.value is AsyncLoaded; + LemmaInfoResponse? get lemmaInfo => + isLoaded ? (_loader.value as AsyncLoaded).value : null; LemmaInfoRequest get _request => LemmaInfoRequest( lemma: widget.constructId.lemma, @@ -91,15 +74,23 @@ class LemmaMeaningBuilderState extends State { messageInfo: widget.messageInfo, ); - void _reload() { - _loader = _LemmaMeaningLoader(_request); - _loader.load(); + Future _load() async { + _loader.value = const AsyncState.loading(); + final result = await LemmaInfoRepo.get( + MatrixState.pangeaController.userController.accessToken, + _request, + ); + + if (!mounted) return; + result.isError + ? _loader.value = AsyncState.error(result.asError!.error) + : _loader.value = AsyncState.loaded(result.asValue!.value); } @override Widget build(BuildContext context) { return ValueListenableBuilder( - valueListenable: _loader.state, + valueListenable: _loader, builder: (context, _, __) => widget.builder( context, this, diff --git a/lib/pangea/lemmas/lemma_meaning_widget.dart b/lib/pangea/lemmas/lemma_meaning_widget.dart deleted file mode 100644 index e2daea447..000000000 --- a/lib/pangea/lemmas/lemma_meaning_widget.dart +++ /dev/null @@ -1,77 +0,0 @@ -import 'package:flutter/material.dart'; - -import 'package:fluffychat/l10n/l10n.dart'; -import 'package:fluffychat/pangea/analytics_misc/text_loading_shimmer.dart'; -import 'package:fluffychat/pangea/common/network/requests.dart'; -import 'package:fluffychat/pangea/common/widgets/error_indicator.dart'; -import 'package:fluffychat/pangea/constructs/construct_identifier.dart'; -import 'package:fluffychat/pangea/lemmas/lemma_meaning_builder.dart'; -import 'package:fluffychat/widgets/matrix.dart'; - -class LemmaMeaningWidget extends StatelessWidget { - final ConstructIdentifier constructId; - final TextStyle? style; - final InlineSpan? leading; - final Map messageInfo; - - const LemmaMeaningWidget({ - super.key, - required this.constructId, - required this.messageInfo, - this.style, - this.leading, - }); - - @override - Widget build(BuildContext context) { - return LemmaMeaningBuilder( - langCode: MatrixState.pangeaController.userController.userL2!.langCode, - constructId: constructId, - messageInfo: messageInfo, - builder: (context, controller) { - if (controller.isLoading) { - return const TextLoadingShimmer(); - } - - if (controller.error != null) { - if (controller.error is UnsubscribedException) { - return ErrorIndicator( - message: L10n.of(context).subscribeToUnlockDefinitions, - style: style, - onTap: () { - MatrixState.pangeaController.subscriptionController - .showPaywall(context); - }, - ); - } - return ErrorIndicator( - message: L10n.of(context).errorFetchingDefinition, - style: style, - ); - } - - return Row( - mainAxisSize: MainAxisSize.min, - children: [ - Flexible( - child: RichText( - textAlign: leading == null ? TextAlign.center : TextAlign.start, - text: TextSpan( - style: style, - children: [ - if (leading != null) leading!, - if (leading != null) - const WidgetSpan(child: SizedBox(width: 6.0)), - TextSpan( - text: controller.lemmaInfo?.meaning, - ), - ], - ), - ), - ), - ], - ); - }, - ); - } -} diff --git a/lib/pangea/phonetic_transcription/phonetic_transcription_builder.dart b/lib/pangea/phonetic_transcription/phonetic_transcription_builder.dart index 366f9b1fe..3627f78a3 100644 --- a/lib/pangea/phonetic_transcription/phonetic_transcription_builder.dart +++ b/lib/pangea/phonetic_transcription/phonetic_transcription_builder.dart @@ -8,29 +8,10 @@ import 'package:fluffychat/pangea/phonetic_transcription/phonetic_transcription_ import 'package:fluffychat/widgets/matrix.dart'; import 'phonetic_transcription_repo.dart'; -class _TranscriptLoader extends AsyncLoader { - final PhoneticTranscriptionRequest request; - _TranscriptLoader(this.request) : super(); - - @override - Future fetch() async { - final resp = await PhoneticTranscriptionRepo.get( - MatrixState.pangeaController.userController.accessToken, - request, - ); - - if (resp.isError) { - throw resp.asError!.error; - } - - return resp.asValue!.value.phoneticTranscriptionResult.phoneticTranscription - .first.phoneticL1Transcription.content; - } -} - class PhoneticTranscriptionBuilder extends StatefulWidget { final LanguageModel textLanguage; final String text; + final ValueNotifier? reloadNotifier; final Widget Function( BuildContext context, @@ -42,6 +23,7 @@ class PhoneticTranscriptionBuilder extends StatefulWidget { required this.textLanguage, required this.text, required this.builder, + this.reloadNotifier, }); @override @@ -51,12 +33,14 @@ class PhoneticTranscriptionBuilder extends StatefulWidget { class PhoneticTranscriptionBuilderState extends State { - late _TranscriptLoader _loader; + final ValueNotifier> _loader = + ValueNotifier(const AsyncState.idle()); @override void initState() { super.initState(); - _reload(); + _load(); + widget.reloadNotifier?.addListener(_load); } @override @@ -64,27 +48,24 @@ class PhoneticTranscriptionBuilderState super.didUpdateWidget(oldWidget); if (oldWidget.text != widget.text || oldWidget.textLanguage != widget.textLanguage) { - _loader.dispose(); - _reload(); + _load(); } } @override void dispose() { + widget.reloadNotifier?.removeListener(_load); _loader.dispose(); super.dispose(); } - bool get isLoading => _loader.isLoading; - bool get isError => _loader.isError; + AsyncState get state => _loader.value; + bool get isError => _loader.value is AsyncError; + bool get isLoaded => _loader.value is AsyncLoaded; + String? get transcription => + isLoaded ? (_loader.value as AsyncLoaded).value : null; - Object? get error => - isError ? (_loader.state.value as AsyncError).error : null; - - String? get transcription => _loader.value; - - PhoneticTranscriptionRequest get _transcriptRequest => - PhoneticTranscriptionRequest( + PhoneticTranscriptionRequest get _request => PhoneticTranscriptionRequest( arc: LanguageArc( l1: MatrixState.pangeaController.userController.userL1!, l2: widget.textLanguage, @@ -92,15 +73,26 @@ class PhoneticTranscriptionBuilderState content: PangeaTokenText.fromString(widget.text), ); - void _reload() { - _loader = _TranscriptLoader(_transcriptRequest); - _loader.load(); + Future _load() async { + _loader.value = const AsyncState.loading(); + final resp = await PhoneticTranscriptionRepo.get( + MatrixState.pangeaController.userController.accessToken, + _request, + ); + + if (!mounted) return; + resp.isError + ? _loader.value = AsyncState.error(resp.asError!.error) + : _loader.value = AsyncState.loaded( + resp.asValue!.value.phoneticTranscriptionResult + .phoneticTranscription.first.phoneticL1Transcription.content, + ); } @override Widget build(BuildContext context) { return ValueListenableBuilder( - valueListenable: _loader.state, + valueListenable: _loader, builder: (context, _, __) => widget.builder( context, this, diff --git a/lib/pangea/phonetic_transcription/phonetic_transcription_widget.dart b/lib/pangea/phonetic_transcription/phonetic_transcription_widget.dart index 7736c13b9..de6a7c494 100644 --- a/lib/pangea/phonetic_transcription/phonetic_transcription_widget.dart +++ b/lib/pangea/phonetic_transcription/phonetic_transcription_widget.dart @@ -1,10 +1,9 @@ -import 'dart:async'; - import 'package:flutter/material.dart'; import 'package:fluffychat/l10n/l10n.dart'; import 'package:fluffychat/pangea/analytics_misc/text_loading_shimmer.dart'; import 'package:fluffychat/pangea/common/network/requests.dart'; +import 'package:fluffychat/pangea/common/utils/async_state.dart'; import 'package:fluffychat/pangea/common/widgets/error_indicator.dart'; import 'package:fluffychat/pangea/languages/language_model.dart'; import 'package:fluffychat/pangea/phonetic_transcription/phonetic_transcription_builder.dart'; @@ -22,6 +21,7 @@ class PhoneticTranscriptionWidget extends StatefulWidget { final int? maxLines; final VoidCallback? onTranscriptionFetched; + final ValueNotifier? reloadNotifier; const PhoneticTranscriptionWidget({ super.key, @@ -32,6 +32,7 @@ class PhoneticTranscriptionWidget extends StatefulWidget { this.iconColor, this.maxLines, this.onTranscriptionFetched, + this.reloadNotifier, }); @override @@ -85,59 +86,58 @@ class _PhoneticTranscriptionWidgetState key: MatrixState.pAnyState.layerLinkAndKey(targetId).key, textLanguage: widget.textLanguage, text: widget.text, + reloadNotifier: widget.reloadNotifier, builder: (context, controller) { - if (controller.isError) { - return controller.error is UnsubscribedException - ? ErrorIndicator( - message: L10n.of(context) - .subscribeToUnlockTranscriptions, - onTap: () { - MatrixState - .pangeaController.subscriptionController - .showPaywall(context); - }, - ) - : ErrorIndicator( - message: - L10n.of(context).failedToFetchTranscription, - ); - } - - if (controller.isLoading || - controller.transcription == null) { - return const TextLoadingShimmer( - width: 125.0, - height: 20.0, - ); - } - - return Row( - spacing: 8.0, - mainAxisSize: MainAxisSize.min, - children: [ - Flexible( - child: Text( - controller.transcription!, - textScaler: TextScaler.noScaling, - style: widget.style ?? - Theme.of(context).textTheme.bodyMedium, - maxLines: widget.maxLines, - overflow: TextOverflow.ellipsis, - ), + return switch (controller.state) { + AsyncError(error: final error) => + error is UnsubscribedException + ? ErrorIndicator( + message: L10n.of(context) + .subscribeToUnlockTranscriptions, + onTap: () { + MatrixState + .pangeaController.subscriptionController + .showPaywall(context); + }, + ) + : ErrorIndicator( + message: + L10n.of(context).failedToFetchTranscription, + ), + AsyncLoaded(value: final transcription) => Row( + spacing: 8.0, + mainAxisSize: MainAxisSize.min, + children: [ + Flexible( + child: Text( + transcription, + textScaler: TextScaler.noScaling, + style: widget.style ?? + Theme.of(context).textTheme.bodyMedium, + maxLines: widget.maxLines, + overflow: TextOverflow.ellipsis, + ), + ), + Tooltip( + message: _isPlaying + ? L10n.of(context).stop + : L10n.of(context).playAudio, + child: Icon( + _isPlaying + ? Icons.pause_outlined + : Icons.volume_up, + size: widget.iconSize ?? 24, + color: widget.iconColor ?? + Theme.of(context).iconTheme.color, + ), + ), + ], ), - Tooltip( - message: _isPlaying - ? L10n.of(context).stop - : L10n.of(context).playAudio, - child: Icon( - _isPlaying ? Icons.pause_outlined : Icons.volume_up, - size: widget.iconSize ?? 24, - color: widget.iconColor ?? - Theme.of(context).iconTheme.color, - ), + _ => const TextLoadingShimmer( + width: 125.0, + height: 20.0, ), - ], - ); + }; }, ), ), diff --git a/lib/pangea/token_info_feedback/show_token_feedback_dialog.dart b/lib/pangea/token_info_feedback/show_token_feedback_dialog.dart index 7d60c9181..9d34b7141 100644 --- a/lib/pangea/token_info_feedback/show_token_feedback_dialog.dart +++ b/lib/pangea/token_info_feedback/show_token_feedback_dialog.dart @@ -12,6 +12,7 @@ class TokenFeedbackUtil { required TokenInfoFeedbackRequestData requestData, required String langCode, PangeaMessageEvent? event, + VoidCallback? onUpdated, }) async { final resp = await showDialog( context: context, @@ -23,6 +24,8 @@ class TokenFeedbackUtil { ); if (resp == null) return; + + onUpdated?.call(); await showDialog( context: context, builder: (context) { diff --git a/lib/pangea/toolbar/word_card/lemma_meaning_display.dart b/lib/pangea/toolbar/word_card/lemma_meaning_display.dart index 1859282b6..70dd51f3e 100644 --- a/lib/pangea/toolbar/word_card/lemma_meaning_display.dart +++ b/lib/pangea/toolbar/word_card/lemma_meaning_display.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:fluffychat/l10n/l10n.dart'; import 'package:fluffychat/pangea/analytics_misc/text_loading_shimmer.dart'; +import 'package:fluffychat/pangea/common/utils/async_state.dart'; import 'package:fluffychat/pangea/common/widgets/error_indicator.dart'; import 'package:fluffychat/pangea/constructs/construct_identifier.dart'; import 'package:fluffychat/pangea/lemmas/lemma_meaning_builder.dart'; @@ -12,6 +13,7 @@ class LemmaMeaningDisplay extends StatelessWidget { final ConstructIdentifier constructId; final String text; final Map messageInfo; + final ValueNotifier? reloadNotifier; const LemmaMeaningDisplay({ super.key, @@ -19,6 +21,7 @@ class LemmaMeaningDisplay extends StatelessWidget { required this.constructId, required this.text, required this.messageInfo, + this.reloadNotifier, }); @override @@ -27,53 +30,47 @@ class LemmaMeaningDisplay extends StatelessWidget { langCode: langCode, constructId: constructId, messageInfo: messageInfo, + reloadNotifier: reloadNotifier, builder: (context, controller) { - if (controller.isError) { - return ErrorIndicator( - message: L10n.of(context).errorFetchingDefinition, - style: const TextStyle(fontSize: 14.0), - ); - } - - if (controller.isLoading || controller.lemmaInfo == null) { - return const TextLoadingShimmer( - width: 125.0, - height: 20.0, - ); - } - - final pos = getGrammarCopy( - category: "POS", - lemma: constructId.category, - context: context, - ) ?? - L10n.of(context).other; - - return RichText( - maxLines: 2, - overflow: TextOverflow.ellipsis, - textAlign: TextAlign.center, - text: TextSpan( - style: DefaultTextStyle.of(context).style.copyWith( - fontSize: 14.0, - ), - children: [ - TextSpan( - text: "${constructId.lemma} ($pos)", + return switch (controller.state) { + AsyncError() => ErrorIndicator( + message: L10n.of(context).errorFetchingDefinition, + style: const TextStyle(fontSize: 14.0), + ), + AsyncLoaded(value: final lemmaInfo) => RichText( + maxLines: 2, + overflow: TextOverflow.ellipsis, + textAlign: TextAlign.center, + text: TextSpan( + style: DefaultTextStyle.of(context).style.copyWith( + fontSize: 14.0, + ), + children: [ + TextSpan( + text: "${constructId.lemma} (${getGrammarCopy( + category: "POS", + lemma: constructId.category, + context: context, + ) ?? L10n.of(context).other})", + ), + const WidgetSpan( + child: SizedBox(width: 8.0), + ), + const TextSpan(text: ":"), + const WidgetSpan( + child: SizedBox(width: 8.0), + ), + TextSpan( + text: lemmaInfo.meaning, + ), + ], ), - const WidgetSpan( - child: SizedBox(width: 8.0), - ), - const TextSpan(text: ":"), - const WidgetSpan( - child: SizedBox(width: 8.0), - ), - TextSpan( - text: controller.lemmaInfo!.meaning, - ), - ], - ), - ); + ), + _ => const TextLoadingShimmer( + width: 125.0, + height: 20.0, + ), + }; }, ); } diff --git a/lib/pangea/toolbar/word_card/word_zoom_widget.dart b/lib/pangea/toolbar/word_card/word_zoom_widget.dart index 0d53ba459..4e9b40ffc 100644 --- a/lib/pangea/toolbar/word_card/word_zoom_widget.dart +++ b/lib/pangea/toolbar/word_card/word_zoom_widget.dart @@ -30,6 +30,7 @@ class WordZoomWidget extends StatelessWidget { final bool enableEmojiSelection; final VoidCallback? onDismissNewWordOverlay; final Function(LemmaInfoResponse, String)? onFlagTokenInfo; + final ValueNotifier? reloadNotifier; final double? maxWidth; const WordZoomWidget({ @@ -42,6 +43,7 @@ class WordZoomWidget extends StatelessWidget { this.enableEmojiSelection = true, this.onDismissNewWordOverlay, this.onFlagTokenInfo, + this.reloadNotifier, this.maxWidth, }); @@ -143,6 +145,7 @@ class WordZoomWidget extends StatelessWidget { style: const TextStyle(fontSize: 14.0), iconSize: 24.0, maxLines: 2, + reloadNotifier: reloadNotifier, ) : WordAudioButton( text: token.content, @@ -161,6 +164,7 @@ class WordZoomWidget extends StatelessWidget { constructId: construct, text: token.content, messageInfo: event?.content ?? {}, + reloadNotifier: reloadNotifier, ), ], ), From 765a83084c478c1601bf779df54e051e5cdee34b Mon Sep 17 00:00:00 2001 From: ggurdin Date: Mon, 19 Jan 2026 12:33:52 -0500 Subject: [PATCH 11/27] chore: only show SSO login redirect dialog while fetching login token --- lib/pangea/login/widgets/p_sso_button.dart | 51 ++++++++------ lib/pangea/login/widgets/p_sso_dialog.dart | 79 +++++++++------------- 2 files changed, 61 insertions(+), 69 deletions(-) diff --git a/lib/pangea/login/widgets/p_sso_button.dart b/lib/pangea/login/widgets/p_sso_button.dart index d89c0e0e1..a02219333 100644 --- a/lib/pangea/login/widgets/p_sso_button.dart +++ b/lib/pangea/login/widgets/p_sso_button.dart @@ -9,12 +9,12 @@ import 'package:matrix/matrix.dart'; import 'package:universal_html/html.dart' as html; import 'package:fluffychat/config/app_config.dart'; -import 'package:fluffychat/pages/homeserver_picker/homeserver_picker.dart'; import 'package:fluffychat/pangea/common/utils/firebase_analytics.dart'; import 'package:fluffychat/pangea/login/sso_provider_enum.dart'; import 'package:fluffychat/pangea/login/widgets/p_sso_dialog.dart'; import 'package:fluffychat/utils/platform_infos.dart'; import 'package:fluffychat/widgets/fluffy_chat_app.dart'; +import 'package:fluffychat/widgets/future_loading_dialog.dart'; import 'package:fluffychat/widgets/matrix.dart'; class PangeaSsoButton extends StatelessWidget { @@ -27,23 +27,25 @@ class PangeaSsoButton extends StatelessWidget { super.key, }); - Future _runSSOLogin(BuildContext context) => showAdaptiveDialog( - context: context, - builder: (context) => SSODialog( - future: () => _ssoAction( - IdentityProvider( - id: provider.id, - name: provider.name, - ), - context, - ), - ), - ); + Future _runSSOLogin(BuildContext context) async { + final token = await showAdaptiveDialog( + context: context, + builder: (context) => SSODialog( + future: () => _getSSOToken(context), + ), + ); - Future _ssoAction( - IdentityProvider provider, - BuildContext context, - ) async { + if (token == null || token.isEmpty) { + return; + } + + await showFutureLoadingDialog( + context: context, + future: () => _ssoAction(token, context), + ); + } + + Future _getSSOToken(BuildContext context) async { final bool isDefaultPlatform = (PlatformInfos.isMobile || PlatformInfos.isWeb || PlatformInfos.isMacOS); @@ -58,7 +60,7 @@ class PangeaSsoButton extends StatelessWidget { : 'http://localhost:3001//login'; final client = await Matrix.of(context).getLoginClient(); final url = client.homeserver!.replace( - path: '/_matrix/client/v3/login/sso/redirect/${provider.id ?? ''}', + path: '/_matrix/client/v3/login/sso/redirect/${provider.id}', queryParameters: {'redirectUrl': redirectUrl}, ); @@ -74,13 +76,20 @@ class PangeaSsoButton extends StatelessWidget { } catch (err) { if (err is PlatformException && err.code == 'CANCELED') { debugPrint("user cancelled SSO login"); - return; + return null; } rethrow; } final token = Uri.parse(result).queryParameters['loginToken']; - if (token?.isEmpty ?? false) return; + if (token?.isEmpty ?? false) return null; + return token; + } + Future _ssoAction( + String token, + BuildContext context, + ) async { + final client = Matrix.of(context).client; final redirect = client.onLoginStateChanged.stream .where((state) => state == LoginState.loggedIn) .first @@ -110,7 +119,7 @@ class PangeaSsoButton extends StatelessWidget { await redirect; } - GoogleAnalytics.login(provider.name!, loginRes.userId); + GoogleAnalytics.login(provider.name, loginRes.userId); } @override diff --git a/lib/pangea/login/widgets/p_sso_dialog.dart b/lib/pangea/login/widgets/p_sso_dialog.dart index fb7184672..2620c3a11 100644 --- a/lib/pangea/login/widgets/p_sso_dialog.dart +++ b/lib/pangea/login/widgets/p_sso_dialog.dart @@ -5,11 +5,10 @@ import 'package:flutter/material.dart'; import 'package:flutter_linkify/flutter_linkify.dart'; import 'package:fluffychat/l10n/l10n.dart'; -import 'package:fluffychat/utils/localized_exception_extension.dart'; import 'package:fluffychat/utils/url_launcher.dart'; class SSODialog extends StatefulWidget { - final Future Function() future; + final Future Function() future; const SSODialog({ super.key, required this.future, @@ -22,7 +21,6 @@ class SSODialog extends StatefulWidget { class SSODialogState extends State { Timer? _hintTimer; bool _showHint = false; - Object? _error; @override void initState() { @@ -43,9 +41,10 @@ class SSODialogState extends State { Future _runFuture() async { try { - await widget.future(); + final token = await widget.future(); + Navigator.of(context).pop(token); } catch (e) { - setState(() => _error = e); + Navigator.of(context).pop(); } } @@ -69,17 +68,11 @@ class SSODialogState extends State { icon: const Icon(Icons.close), ), ), - _error == null - ? Text( - L10n.of(context).ssoDialogTitle, - style: Theme.of(context).textTheme.headlineMedium, - textAlign: TextAlign.center, - ) - : Icon( - Icons.error_outline_outlined, - color: Theme.of(context).colorScheme.error, - size: 48, - ), + Text( + L10n.of(context).ssoDialogTitle, + style: Theme.of(context).textTheme.headlineMedium, + textAlign: TextAlign.center, + ), Container( alignment: Alignment.center, constraints: const BoxConstraints(minHeight: 150), @@ -88,39 +81,29 @@ class SSODialogState extends State { spacing: 16.0, mainAxisSize: MainAxisSize.min, children: [ - if (_error != null) - Text( - _error!.toLocalizedString(context), - style: Theme.of(context).textTheme.titleMedium, - textAlign: TextAlign.center, - ) - else ...[ - SelectableLinkify( - text: L10n.of(context).ssoDialogDesc, - textScaleFactor: - MediaQuery.textScalerOf(context).scale(1), - linkStyle: TextStyle( - color: Theme.of(context).colorScheme.primary, - decorationColor: Theme.of(context).colorScheme.primary, - ), - options: const LinkifyOptions(humanize: false), - onOpen: (url) => - UrlLauncher(context, url.url).launchUrl(), - style: Theme.of(context).textTheme.bodyLarge, - textAlign: TextAlign.center, + SelectableLinkify( + text: L10n.of(context).ssoDialogDesc, + textScaleFactor: MediaQuery.textScalerOf(context).scale(1), + linkStyle: TextStyle( + color: Theme.of(context).colorScheme.primary, + decorationColor: Theme.of(context).colorScheme.primary, ), - _showHint - ? Text( - L10n.of(context).ssoDialogHelpText, - style: Theme.of(context).textTheme.bodyLarge, - textAlign: TextAlign.center, - ) - : const SizedBox( - height: 16.0, - width: 16.0, - child: CircularProgressIndicator.adaptive(), - ), - ], + options: const LinkifyOptions(humanize: false), + onOpen: (url) => UrlLauncher(context, url.url).launchUrl(), + style: Theme.of(context).textTheme.bodyLarge, + textAlign: TextAlign.center, + ), + _showHint + ? Text( + L10n.of(context).ssoDialogHelpText, + style: Theme.of(context).textTheme.bodyLarge, + textAlign: TextAlign.center, + ) + : const SizedBox( + height: 16.0, + width: 16.0, + child: CircularProgressIndicator.adaptive(), + ), ], ), ), From 33b05f6f24dccd736fd7c8c27e50e92e47e488b4 Mon Sep 17 00:00:00 2001 From: ggurdin Date: Mon, 19 Jan 2026 15:34:20 -0500 Subject: [PATCH 12/27] setup for grammar error practice --- lib/l10n/intl_en.arb | 4 +- .../construct_use_type_enum.dart | 18 ++++++++ .../grammar_error_practice_generator.dart | 42 +++++++++++++++++++ .../choreographer/igc/span_data_model.dart | 5 ++- .../activity_type_enum.dart | 19 ++++++++- .../practice_activity_model.dart | 11 +++++ .../practice_generation_repo.dart | 3 ++ 7 files changed, 98 insertions(+), 4 deletions(-) create mode 100644 lib/pangea/analytics_practice/grammar_error_practice_generator.dart diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index be647da95..16fb400ac 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -5051,5 +5051,7 @@ "practiceGrammar": "Practice grammar", "notEnoughToPractice": "Send more messages to unlock practice", "constructUseCorGCDesc": "Correct grammar category practice", - "constructUseIncGCDesc": "Incorrect grammar category practice" + "constructUseIncGCDesc": "Incorrect grammar category practice", + "constructUseCorGEDesc": "Correct grammar error practice", + "constructUseIncGEDesc": "Incorrect grammar error practice" } diff --git a/lib/pangea/analytics_misc/construct_use_type_enum.dart b/lib/pangea/analytics_misc/construct_use_type_enum.dart index 028759fe4..ec5cae6d8 100644 --- a/lib/pangea/analytics_misc/construct_use_type_enum.dart +++ b/lib/pangea/analytics_misc/construct_use_type_enum.dart @@ -86,6 +86,10 @@ enum ConstructUseTypeEnum { // grammar category activity corGC, incGC, + + // grammar error activity + corGE, + incGE, } extension ConstructUseTypeExtension on ConstructUseTypeEnum { @@ -171,6 +175,10 @@ extension ConstructUseTypeExtension on ConstructUseTypeEnum { return L10n.of(context).constructUseCorGCDesc; case ConstructUseTypeEnum.incGC: return L10n.of(context).constructUseIncGCDesc; + case ConstructUseTypeEnum.corGE: + return L10n.of(context).constructUseCorGEDesc; + case ConstructUseTypeEnum.incGE: + return L10n.of(context).constructUseIncGEDesc; } } @@ -213,6 +221,8 @@ extension ConstructUseTypeExtension on ConstructUseTypeEnum { case ConstructUseTypeEnum.ignM: case ConstructUseTypeEnum.corGC: case ConstructUseTypeEnum.incGC: + case ConstructUseTypeEnum.corGE: + case ConstructUseTypeEnum.incGE: return ActivityTypeEnum.morphId.icon; case ConstructUseTypeEnum.em: return ActivityTypeEnum.emoji.icon; @@ -246,6 +256,7 @@ extension ConstructUseTypeExtension on ConstructUseTypeEnum { case ConstructUseTypeEnum.corLM: case ConstructUseTypeEnum.corLA: case ConstructUseTypeEnum.corGC: + case ConstructUseTypeEnum.corGE: return 5; case ConstructUseTypeEnum.pvm: @@ -287,6 +298,7 @@ extension ConstructUseTypeExtension on ConstructUseTypeEnum { case ConstructUseTypeEnum.incLM: case ConstructUseTypeEnum.incLA: case ConstructUseTypeEnum.incGC: + case ConstructUseTypeEnum.incGE: return -1; case ConstructUseTypeEnum.incPA: @@ -340,6 +352,8 @@ extension ConstructUseTypeExtension on ConstructUseTypeEnum { case ConstructUseTypeEnum.bonus: case ConstructUseTypeEnum.corGC: case ConstructUseTypeEnum.incGC: + case ConstructUseTypeEnum.corGE: + case ConstructUseTypeEnum.incGE: return false; } } @@ -385,6 +399,8 @@ extension ConstructUseTypeExtension on ConstructUseTypeEnum { case ConstructUseTypeEnum.incLM: case ConstructUseTypeEnum.corGC: case ConstructUseTypeEnum.incGC: + case ConstructUseTypeEnum.corGE: + case ConstructUseTypeEnum.incGE: return LearningSkillsEnum.reading; case ConstructUseTypeEnum.pvm: return LearningSkillsEnum.speaking; @@ -415,6 +431,7 @@ extension ConstructUseTypeExtension on ConstructUseTypeEnum { case ConstructUseTypeEnum.corLM: case ConstructUseTypeEnum.corLA: case ConstructUseTypeEnum.corGC: + case ConstructUseTypeEnum.corGE: return SpaceAnalyticsSummaryEnum.numChoicesCorrect; case ConstructUseTypeEnum.incIt: @@ -428,6 +445,7 @@ extension ConstructUseTypeExtension on ConstructUseTypeEnum { case ConstructUseTypeEnum.incLM: case ConstructUseTypeEnum.incLA: case ConstructUseTypeEnum.incGC: + case ConstructUseTypeEnum.incGE: return SpaceAnalyticsSummaryEnum.numChoicesIncorrect; case ConstructUseTypeEnum.ignIt: diff --git a/lib/pangea/analytics_practice/grammar_error_practice_generator.dart b/lib/pangea/analytics_practice/grammar_error_practice_generator.dart new file mode 100644 index 000000000..66ec9c5db --- /dev/null +++ b/lib/pangea/analytics_practice/grammar_error_practice_generator.dart @@ -0,0 +1,42 @@ +import 'package:fluffychat/pangea/practice_activities/message_activity_request.dart'; +import 'package:fluffychat/pangea/practice_activities/multiple_choice_activity_model.dart'; +import 'package:fluffychat/pangea/practice_activities/practice_activity_model.dart'; +import 'package:fluffychat/widgets/matrix.dart'; + +class GrammarErrorPracticeGenerator { + static Future get( + MessageActivityRequest req, + ) async { + final igcMatch = target.igcMatch; + assert(igcMatch.bestChoice != null, 'IGC match must have a best choice'); + assert(igcMatch.choices != null, 'IGC match must have choices'); + + final errorSpan = igcMatch.errorSpan; + final correctChoice = igcMatch.bestChoice!.value; + final choices = igcMatch.choices!.map((c) => c.value).toList(); + + final choiceTokens = target.tokens.where( + (token) => choices.any( + (choice) => choice.contains(token.text.content), + ), + ); + + assert( + choiceTokens.isNotEmpty, + 'At least one token should match the error choices', + ); + + choices.add(errorSpan); + choices.shuffle(); + return MessageActivityResponse( + activity: GrammarErrorPracticeActivityModel( + tokens: choiceTokens.toList(), + langCode: req.userL2, + multipleChoiceContent: MultipleChoiceActivity( + choices: choices.toSet(), + answers: {correctChoice}, + ), + ), + ); + } +} diff --git a/lib/pangea/choreographer/igc/span_data_model.dart b/lib/pangea/choreographer/igc/span_data_model.dart index 220e245f2..ac65a1347 100644 --- a/lib/pangea/choreographer/igc/span_data_model.dart +++ b/lib/pangea/choreographer/igc/span_data_model.dart @@ -132,6 +132,9 @@ class SpanData { return choices![index]; } + String get errorSpan => + fullText.characters.skip(offset).take(length).toString(); + bool isNormalizationError() { final correctChoice = choices ?.firstWhereOrNull( @@ -139,8 +142,6 @@ class SpanData { ) ?.value; - final errorSpan = fullText.characters.skip(offset).take(length).toString(); - final l2Code = MatrixState.pangeaController.userController.userL2?.langCodeShort; diff --git a/lib/pangea/practice_activities/activity_type_enum.dart b/lib/pangea/practice_activities/activity_type_enum.dart index 02a9e9c91..b8e493d9d 100644 --- a/lib/pangea/practice_activities/activity_type_enum.dart +++ b/lib/pangea/practice_activities/activity_type_enum.dart @@ -15,7 +15,8 @@ enum ActivityTypeEnum { messageMeaning, lemmaMeaning, lemmaAudio, - grammarCategory; + grammarCategory, + grammarError; bool get includeTTSOnClick { switch (this) { @@ -30,6 +31,7 @@ enum ActivityTypeEnum { case ActivityTypeEnum.lemmaAudio: case ActivityTypeEnum.lemmaMeaning: case ActivityTypeEnum.grammarCategory: + case ActivityTypeEnum.grammarError: return true; } } @@ -68,6 +70,9 @@ enum ActivityTypeEnum { case 'grammar_category': case 'grammarCategory': return ActivityTypeEnum.grammarCategory; + case 'grammar_error': + case 'grammarError': + return ActivityTypeEnum.grammarError; default: throw Exception('Unknown activity type: $split'); } @@ -128,6 +133,11 @@ enum ActivityTypeEnum { ConstructUseTypeEnum.corGC, ConstructUseTypeEnum.incGC, ]; + case ActivityTypeEnum.grammarError: + return [ + ConstructUseTypeEnum.corGE, + ConstructUseTypeEnum.incGE, + ]; } } @@ -153,6 +163,8 @@ enum ActivityTypeEnum { return ConstructUseTypeEnum.corLM; case ActivityTypeEnum.grammarCategory: return ConstructUseTypeEnum.corGC; + case ActivityTypeEnum.grammarError: + return ConstructUseTypeEnum.corGE; } } @@ -178,6 +190,8 @@ enum ActivityTypeEnum { return ConstructUseTypeEnum.incLM; case ActivityTypeEnum.grammarCategory: return ConstructUseTypeEnum.incGC; + case ActivityTypeEnum.grammarError: + return ConstructUseTypeEnum.incGE; } } @@ -198,6 +212,7 @@ enum ActivityTypeEnum { return Icons.format_shapes; case ActivityTypeEnum.messageMeaning: case ActivityTypeEnum.grammarCategory: + case ActivityTypeEnum.grammarError: return Icons.star; // TODO: Add to L10n } } @@ -217,6 +232,7 @@ enum ActivityTypeEnum { case ActivityTypeEnum.lemmaMeaning: case ActivityTypeEnum.lemmaAudio: case ActivityTypeEnum.grammarCategory: + case ActivityTypeEnum.grammarError: return 1; } } @@ -235,6 +251,7 @@ enum ActivityTypeEnum { static List get _grammarPracticeTypes => [ ActivityTypeEnum.grammarCategory, + ActivityTypeEnum.grammarError, ]; static List analyticsPracticeTypes( diff --git a/lib/pangea/practice_activities/practice_activity_model.dart b/lib/pangea/practice_activities/practice_activity_model.dart index e9d37f2ba..36d284f0b 100644 --- a/lib/pangea/practice_activities/practice_activity_model.dart +++ b/lib/pangea/practice_activities/practice_activity_model.dart @@ -48,6 +48,8 @@ sealed class PracticeActivityModel { return ActivityTypeEnum.morphId; case WordListeningPracticeActivityModel(): return ActivityTypeEnum.wordFocusListening; + case GrammarErrorPracticeActivityModel(): + return ActivityTypeEnum.grammarError; } } @@ -350,6 +352,15 @@ class LemmaPracticeActivityModel extends MultipleChoicePracticeActivityModel { }); } +class GrammarErrorPracticeActivityModel + extends MultipleChoicePracticeActivityModel { + GrammarErrorPracticeActivityModel({ + required super.tokens, + required super.langCode, + required super.multipleChoiceContent, + }); +} + class EmojiPracticeActivityModel extends MatchPracticeActivityModel { EmojiPracticeActivityModel({ required super.tokens, diff --git a/lib/pangea/practice_activities/practice_generation_repo.dart b/lib/pangea/practice_activities/practice_generation_repo.dart index 68d3cb821..3ff418594 100644 --- a/lib/pangea/practice_activities/practice_generation_repo.dart +++ b/lib/pangea/practice_activities/practice_generation_repo.dart @@ -9,6 +9,7 @@ import 'package:async/async.dart'; import 'package:get_storage/get_storage.dart'; import 'package:http/http.dart'; +import 'package:fluffychat/pangea/analytics_practice/grammar_error_practice_generator.dart'; import 'package:fluffychat/pangea/analytics_practice/morph_category_activity_generator.dart'; import 'package:fluffychat/pangea/analytics_practice/vocab_audio_activity_generator.dart'; import 'package:fluffychat/pangea/analytics_practice/vocab_meaning_activity_generator.dart'; @@ -128,6 +129,8 @@ class PracticeRepo { return VocabAudioActivityGenerator.get(req); case ActivityTypeEnum.grammarCategory: return MorphCategoryActivityGenerator.get(req); + case ActivityTypeEnum.grammarError: + return GrammarErrorPracticeGenerator.get(req); case ActivityTypeEnum.morphId: return MorphActivityGenerator.get(req); case ActivityTypeEnum.wordMeaning: From 8fb41cdc7a755d37a69104da2ec4f325d305cc7e Mon Sep 17 00:00:00 2001 From: ggurdin Date: Tue, 20 Jan 2026 11:39:48 -0500 Subject: [PATCH 13/27] grammar error practice UI elements --- lib/l10n/intl_en.arb | 3 +- .../analytics_data_service.dart | 3 + .../analytics_data/analytics_database.dart | 5 + .../analytics_practice_page.dart | 49 +++++--- .../analytics_practice_session_model.dart | 32 ++++- .../analytics_practice_session_repo.dart | 119 ++++++++++++++++-- .../analytics_practice_view.dart | 113 +++++++++++++++-- .../grammar_error_practice_generator.dart | 43 ++++--- .../message_activity_request.dart | 57 ++++++++- .../practice_activity_model.dart | 9 ++ .../practice_generation_repo.dart | 4 + .../practice_activities/practice_target.dart | 14 --- 12 files changed, 374 insertions(+), 77 deletions(-) diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 16fb400ac..b1bcf4e02 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -5053,5 +5053,6 @@ "constructUseCorGCDesc": "Correct grammar category practice", "constructUseIncGCDesc": "Incorrect grammar category practice", "constructUseCorGEDesc": "Correct grammar error practice", - "constructUseIncGEDesc": "Incorrect grammar error practice" + "constructUseIncGEDesc": "Incorrect grammar error practice", + "fillInBlank": "Fill in the blank with the correct choice" } diff --git a/lib/pangea/analytics_data/analytics_data_service.dart b/lib/pangea/analytics_data/analytics_data_service.dart index 1c10b1208..0f250f121 100644 --- a/lib/pangea/analytics_data/analytics_data_service.dart +++ b/lib/pangea/analytics_data/analytics_data_service.dart @@ -14,6 +14,7 @@ import 'package:fluffychat/pangea/analytics_data/level_up_analytics_service.dart import 'package:fluffychat/pangea/analytics_misc/client_analytics_extension.dart'; import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart'; import 'package:fluffychat/pangea/analytics_misc/construct_use_model.dart'; +import 'package:fluffychat/pangea/analytics_misc/construct_use_type_enum.dart'; import 'package:fluffychat/pangea/analytics_misc/constructs_event.dart'; import 'package:fluffychat/pangea/analytics_misc/constructs_model.dart'; import 'package:fluffychat/pangea/analytics_settings/analytics_settings_extension.dart'; @@ -233,12 +234,14 @@ class AnalyticsDataService { int? count, String? roomId, DateTime? since, + ConstructUseTypeEnum? type, }) async { await _ensureInitialized(); final uses = await _analyticsClientGetter.database.getUses( count: count, roomId: roomId, since: since, + type: type, ); final blocked = blockedConstructs; diff --git a/lib/pangea/analytics_data/analytics_database.dart b/lib/pangea/analytics_data/analytics_database.dart index cb215ae94..5fe681042 100644 --- a/lib/pangea/analytics_data/analytics_database.dart +++ b/lib/pangea/analytics_data/analytics_database.dart @@ -10,6 +10,7 @@ import 'package:synchronized/synchronized.dart'; import 'package:fluffychat/pangea/analytics_data/derived_analytics_data_model.dart'; import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart'; import 'package:fluffychat/pangea/analytics_misc/construct_use_model.dart'; +import 'package:fluffychat/pangea/analytics_misc/construct_use_type_enum.dart'; import 'package:fluffychat/pangea/analytics_misc/constructs_event.dart'; import 'package:fluffychat/pangea/analytics_misc/constructs_model.dart'; import 'package:fluffychat/pangea/constructs/construct_identifier.dart'; @@ -197,6 +198,7 @@ class AnalyticsDatabase with DatabaseFileStorage { int? count, String? roomId, DateTime? since, + ConstructUseTypeEnum? type, }) async { final stopwatch = Stopwatch()..start(); final results = []; @@ -208,6 +210,9 @@ class AnalyticsDatabase with DatabaseFileStorage { if (roomId != null && use.metadata.roomId != roomId) { return true; // skip but continue } + if (type != null && use.useType != type) { + return true; // skip but continue + } results.add(use); return count == null || results.length < count; diff --git a/lib/pangea/analytics_practice/analytics_practice_page.dart b/lib/pangea/analytics_practice/analytics_practice_page.dart index 22bca0912..67b7e2d26 100644 --- a/lib/pangea/analytics_practice/analytics_practice_page.dart +++ b/lib/pangea/analytics_practice/analytics_practice_page.dart @@ -26,18 +26,28 @@ import 'package:fluffychat/pangea/toolbar/message_practice/practice_record_contr import 'package:fluffychat/widgets/future_loading_dialog.dart'; import 'package:fluffychat/widgets/matrix.dart'; -class PracticeChoice { +class VocabPracticeChoice { final String choiceId; final String choiceText; final String? choiceEmoji; - const PracticeChoice({ + const VocabPracticeChoice({ required this.choiceId, required this.choiceText, this.choiceEmoji, }); } +class _PracticeQueueEntry { + final MessageActivityRequest request; + final Completer completer; + + _PracticeQueueEntry({ + required this.request, + required this.completer, + }); +} + class SessionLoader extends AsyncLoader { final ConstructTypeEnum type; SessionLoader({required this.type}); @@ -67,12 +77,10 @@ class AnalyticsPracticeState extends State final ValueNotifier> activityState = ValueNotifier(const AsyncState.idle()); - final Queue< - MapEntry>> _queue = Queue(); + final Queue<_PracticeQueueEntry> _queue = Queue(); - final ValueNotifier activityTarget = - ValueNotifier(null); + final ValueNotifier activityTarget = + ValueNotifier(null); final ValueNotifier progressNotifier = ValueNotifier(0.0); @@ -116,13 +124,13 @@ class AnalyticsPracticeState extends State AnalyticsDataService get _analyticsService => Matrix.of(context).analyticsDataService; - List filteredChoices( + List filteredChoices( MultipleChoicePracticeActivityModel activity, ) { final content = activity.multipleChoiceContent; final choices = content.choices.toList(); final answer = content.answers.first; - final filtered = []; + final filtered = []; final seenTexts = {}; for (final id in choices) { @@ -137,7 +145,7 @@ class AnalyticsPracticeState extends State (choice) => choice.choiceText == text, ); if (index != -1) { - filtered[index] = PracticeChoice( + filtered[index] = VocabPracticeChoice( choiceId: id, choiceText: text, choiceEmoji: getChoiceEmoji(activity.storageKey, id), @@ -148,7 +156,7 @@ class AnalyticsPracticeState extends State seenTexts.add(text); filtered.add( - PracticeChoice( + VocabPracticeChoice( choiceId: id, choiceText: text, choiceEmoji: getChoiceEmoji(activity.storageKey, id), @@ -202,7 +210,7 @@ class AnalyticsPracticeState extends State if (activityTarget.value == null) return; if (widget.type != ConstructTypeEnum.vocab) return; TtsController.tryToSpeak( - activityTarget.value!.tokens.first.vocabConstructID.lemma, + activityTarget.value!.target.tokens.first.vocabConstructID.lemma, langCode: MatrixState.pangeaController.userController.userL2!.langCode, ); } @@ -275,10 +283,10 @@ class AnalyticsPracticeState extends State activityState.value = const AsyncState.loading(); final nextActivityCompleter = _queue.removeFirst(); - activityTarget.value = nextActivityCompleter.key; + activityTarget.value = nextActivityCompleter.request; _playAudio(); - final activity = await nextActivityCompleter.value.future; + final activity = await nextActivityCompleter.completer.future; activityState.value = AsyncState.loaded(activity); } } catch (e) { @@ -298,7 +306,7 @@ class AnalyticsPracticeState extends State activityState.value = const AsyncState.loading(); final req = requests.first; - activityTarget.value = req.target; + activityTarget.value = req; _playAudio(); final res = await _fetchActivity(req); @@ -314,10 +322,17 @@ class AnalyticsPracticeState extends State _fillActivityQueue(requests.skip(1).toList()); } - Future _fillActivityQueue(List requests) async { + Future _fillActivityQueue( + List requests, + ) async { for (final request in requests) { final completer = Completer(); - _queue.add(MapEntry(request.target, completer)); + _queue.add( + _PracticeQueueEntry( + request: request, + completer: completer, + ), + ); try { final res = await _fetchActivity(request); diff --git a/lib/pangea/analytics_practice/analytics_practice_session_model.dart b/lib/pangea/analytics_practice/analytics_practice_session_model.dart index b438cce39..e0ee08718 100644 --- a/lib/pangea/analytics_practice/analytics_practice_session_model.dart +++ b/lib/pangea/analytics_practice/analytics_practice_session_model.dart @@ -6,9 +6,32 @@ import 'package:fluffychat/pangea/analytics_practice/analytics_practice_constant import 'package:fluffychat/pangea/practice_activities/message_activity_request.dart'; import 'package:fluffychat/pangea/practice_activities/practice_target.dart'; +class AnalyticsActivityTarget { + final PracticeTarget target; + final GrammarErrorRequestInfo? grammarErrorInfo; + + AnalyticsActivityTarget({ + required this.target, + this.grammarErrorInfo, + }); + + Map toJson() => { + 'target': target.toJson(), + 'grammarErrorInfo': grammarErrorInfo?.toJson(), + }; + + factory AnalyticsActivityTarget.fromJson(Map json) => + AnalyticsActivityTarget( + target: PracticeTarget.fromJson(json['target']), + grammarErrorInfo: json['grammarErrorInfo'] != null + ? GrammarErrorRequestInfo.fromJson(json['grammarErrorInfo']) + : null, + ); +} + class AnalyticsPracticeSessionModel { final DateTime startedAt; - final List practiceTargets; + final List practiceTargets; final String userL1; final String userL2; @@ -38,7 +61,8 @@ class AnalyticsPracticeSessionModel { userL1: userL1, userL2: userL2, activityQualityFeedback: null, - target: target, + target: target.target, + grammarErrorInfo: target.grammarErrorInfo, ); }).toList(); } @@ -59,8 +83,8 @@ class AnalyticsPracticeSessionModel { return AnalyticsPracticeSessionModel( startedAt: DateTime.parse(json['startedAt'] as String), practiceTargets: (json['practiceTargets'] as List) - .map((e) => PracticeTarget.fromJson(e)) - .whereType() + .map((e) => AnalyticsActivityTarget.fromJson(e)) + .whereType() .toList(), userL1: json['userL1'] as String, userL2: json['userL2'] as String, diff --git a/lib/pangea/analytics_practice/analytics_practice_session_repo.dart b/lib/pangea/analytics_practice/analytics_practice_session_repo.dart index dcf8a1f35..6124234e7 100644 --- a/lib/pangea/analytics_practice/analytics_practice_session_repo.dart +++ b/lib/pangea/analytics_practice/analytics_practice_session_repo.dart @@ -1,14 +1,17 @@ import 'dart:math'; import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart'; +import 'package:fluffychat/pangea/analytics_misc/construct_use_type_enum.dart'; import 'package:fluffychat/pangea/analytics_practice/analytics_practice_constants.dart'; import 'package:fluffychat/pangea/analytics_practice/analytics_practice_session_model.dart'; import 'package:fluffychat/pangea/constructs/construct_identifier.dart'; +import 'package:fluffychat/pangea/events/event_wrappers/pangea_message_event.dart'; import 'package:fluffychat/pangea/events/models/pangea_token_model.dart'; import 'package:fluffychat/pangea/events/models/pangea_token_text_model.dart'; import 'package:fluffychat/pangea/lemmas/lemma.dart'; import 'package:fluffychat/pangea/morphs/morph_features_enum.dart'; import 'package:fluffychat/pangea/practice_activities/activity_type_enum.dart'; +import 'package:fluffychat/pangea/practice_activities/message_activity_request.dart'; import 'package:fluffychat/pangea/practice_activities/practice_target.dart'; import 'package:fluffychat/widgets/matrix.dart'; @@ -24,28 +27,23 @@ class AnalyticsPracticeSessionRepo { (_) => activityTypes[r.nextInt(activityTypes.length)], ); - final List targets = []; + final List targets = []; if (type == ConstructTypeEnum.vocab) { final constructs = await _fetchVocab(); final targetCount = min(constructs.length, types.length); targets.addAll([ for (var i = 0; i < targetCount; i++) - PracticeTarget( - tokens: [constructs[i].asToken], - activityType: types[i], + AnalyticsActivityTarget( + target: PracticeTarget( + tokens: [constructs[i].asToken], + activityType: types[i], + ), ), ]); } else { - final morphs = await _fetchMorphs(); - targets.addAll([ - for (final entry in morphs.entries) - PracticeTarget( - tokens: [entry.key], - activityType: types[targets.length], - morphFeature: entry.value, - ), - ]); + final errorTargets = await _fetchErrors(); + targets.addAll(errorTargets); } final session = AnalyticsPracticeSessionModel( @@ -144,4 +142,99 @@ class AnalyticsPracticeSessionRepo { return targets; } + + static Future> _fetchErrors() async { + final uses = await MatrixState + .pangeaController.matrixState.analyticsDataService + .getUses(count: 100, type: ConstructUseTypeEnum.ga); + + final client = MatrixState.pangeaController.matrixState.client; + final Map idsToEvents = {}; + + for (final use in uses) { + final eventID = use.metadata.eventId; + if (eventID == null || idsToEvents.containsKey(eventID)) continue; + + final roomID = use.metadata.roomId; + if (roomID == null) { + idsToEvents[eventID] = null; + continue; + } + + final room = client.getRoomById(roomID); + final event = await room?.getEventById(eventID); + if (event == null || event.redacted) { + idsToEvents[eventID] = null; + continue; + } + + final timeline = await room!.getTimeline(); + idsToEvents[eventID] = PangeaMessageEvent( + event: event, + timeline: timeline, + ownMessage: event.senderId == client.userID, + ); + } + + final l2Code = + MatrixState.pangeaController.userController.userL2!.langCodeShort; + + final events = idsToEvents.values.whereType().toList(); + final eventsWithContent = events.where((e) { + final originalSent = e.originalSent; + final choreo = originalSent?.choreo; + final tokens = originalSent?.tokens; + return originalSent?.langCode.split("-").first == l2Code && + choreo != null && + tokens != null && + tokens.isNotEmpty && + choreo.choreoSteps.any( + (step) => + step.acceptedOrIgnoredMatch?.isGrammarMatch == true && + step.acceptedOrIgnoredMatch?.match.bestChoice != null, + ); + }); + + final targets = []; + for (final event in eventsWithContent) { + final originalSent = event.originalSent!; + final choreo = originalSent.choreo!; + final tokens = originalSent.tokens!; + + for (int i = 0; i < choreo.choreoSteps.length; i++) { + final step = choreo.choreoSteps[i]; + final igcMatch = step.acceptedOrIgnoredMatch; + if (igcMatch?.isGrammarMatch != true || + igcMatch?.match.bestChoice == null) { + continue; + } + + final choices = igcMatch!.match.choices!.map((c) => c.value).toList(); + final choiceTokens = tokens.where( + (token) => + token.lemma.saveVocab && + choices.any( + (choice) => choice.contains(token.text.content), + ), + ); + + targets.add( + AnalyticsActivityTarget( + target: PracticeTarget( + tokens: choiceTokens.toList(), + activityType: ActivityTypeEnum.grammarError, + morphFeature: null, + ), + grammarErrorInfo: GrammarErrorRequestInfo( + choreo: choreo, + stepIndex: i, + eventID: event.eventId, + ), + ), + ); + } + } + + return targets; + } } diff --git a/lib/pangea/analytics_practice/analytics_practice_view.dart b/lib/pangea/analytics_practice/analytics_practice_view.dart index a41f0ec3e..9b46524a9 100644 --- a/lib/pangea/analytics_practice/analytics_practice_view.dart +++ b/lib/pangea/analytics_practice/analytics_practice_view.dart @@ -143,8 +143,8 @@ class _AnalyticsActivityView extends StatelessWidget { if (controller.widget.type == ConstructTypeEnum.vocab) PhoneticTranscriptionWidget( - text: - target.tokens.first.vocabConstructID.lemma, + text: target + .target.tokens.first.vocabConstructID.lemma, textLanguage: MatrixState .pangeaController.userController.userL2!, style: const TextStyle(fontSize: 14.0), @@ -157,13 +157,8 @@ class _AnalyticsActivityView extends StatelessWidget { Expanded( flex: 2, child: Center( - child: ValueListenableBuilder( - valueListenable: controller.activityTarget, - builder: (context, target, __) => target != null - ? _ExampleMessageWidget( - controller.getExampleMessage(target), - ) - : const SizedBox(), + child: _AnalyticsPracticeCenterContent( + controller: controller, ), ), ), @@ -179,6 +174,36 @@ class _AnalyticsActivityView extends StatelessWidget { } } +class _AnalyticsPracticeCenterContent extends StatelessWidget { + final AnalyticsPracticeState controller; + + const _AnalyticsPracticeCenterContent({ + required this.controller, + }); + + @override + Widget build(BuildContext context) { + return ValueListenableBuilder( + valueListenable: controller.activityTarget, + builder: (context, target, __) => switch (target?.target.activityType) { + null => const SizedBox(), + ActivityTypeEnum.grammarError => ValueListenableBuilder( + valueListenable: controller.activityState, + builder: (context, state, __) => switch (state) { + AsyncLoaded(value: final activity) => _ErrorBlankWidget( + activity: activity as GrammarErrorPracticeActivityModel, + ), + _ => const SizedBox(), + }, + ), + _ => _ExampleMessageWidget( + controller.getExampleMessage(target!.target), + ), + }, + ); + } +} + class _ExampleMessageWidget extends StatelessWidget { final Future?> future; @@ -220,6 +245,62 @@ class _ExampleMessageWidget extends StatelessWidget { } } +class _ErrorBlankWidget extends StatelessWidget { + final GrammarErrorPracticeActivityModel activity; + + const _ErrorBlankWidget({ + required this.activity, + }); + + @override + Widget build(BuildContext context) { + final text = activity.text; + final errorOffset = activity.errorOffset; + final errorLength = activity.errorLength; + + return Container( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 8, + ), + decoration: BoxDecoration( + color: Color.alphaBlend( + Colors.white.withAlpha(180), + ThemeData.dark().colorScheme.primary, + ), + borderRadius: BorderRadius.circular(16), + ), + child: RichText( + text: TextSpan( + style: TextStyle( + color: Theme.of(context).colorScheme.onPrimaryFixed, + fontSize: AppConfig.fontSizeFactor * AppConfig.messageFontSize, + ), + children: [ + if (errorOffset > 0) + TextSpan(text: text.characters.take(errorOffset).toString()), + WidgetSpan( + child: Container( + height: 4.0, + width: (errorLength * 8).toDouble(), + padding: const EdgeInsets.only(bottom: 2.0), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.primary, + ), + ), + ), + if (errorOffset + errorLength < text.length) + TextSpan( + text: + text.characters.skip(errorOffset + errorLength).toString(), + ), + ], + ), + ), + ); + } +} + class _ActivityChoicesWidget extends StatelessWidget { final AnalyticsPracticeState controller; @@ -366,6 +447,20 @@ class _ChoiceCard extends StatelessWidget { isCorrect: isCorrect, ); + case ActivityTypeEnum.grammarError: + final activity = this.activity as GrammarErrorPracticeActivityModel; + return GameChoiceCard( + key: ValueKey( + '${activity.errorLength}_${activity.errorOffset}_${activity.eventID}_${activityType.name}_grammar_error_$choiceId', + ), + shouldFlip: false, + targetId: targetId, + onPressed: onPressed, + isCorrect: isCorrect, + height: cardHeight, + child: Text(choiceText), + ); + default: return GameChoiceCard( key: ValueKey( diff --git a/lib/pangea/analytics_practice/grammar_error_practice_generator.dart b/lib/pangea/analytics_practice/grammar_error_practice_generator.dart index 66ec9c5db..30880ab50 100644 --- a/lib/pangea/analytics_practice/grammar_error_practice_generator.dart +++ b/lib/pangea/analytics_practice/grammar_error_practice_generator.dart @@ -1,41 +1,50 @@ +import 'package:flutter/material.dart'; + import 'package:fluffychat/pangea/practice_activities/message_activity_request.dart'; import 'package:fluffychat/pangea/practice_activities/multiple_choice_activity_model.dart'; import 'package:fluffychat/pangea/practice_activities/practice_activity_model.dart'; -import 'package:fluffychat/widgets/matrix.dart'; class GrammarErrorPracticeGenerator { static Future get( MessageActivityRequest req, ) async { - final igcMatch = target.igcMatch; - assert(igcMatch.bestChoice != null, 'IGC match must have a best choice'); - assert(igcMatch.choices != null, 'IGC match must have choices'); + assert( + req.grammarErrorInfo != null, + 'Grammar error info must be provided for grammar error practice', + ); - final errorSpan = igcMatch.errorSpan; - final correctChoice = igcMatch.bestChoice!.value; + final choreo = req.grammarErrorInfo!.choreo; + final stepIndex = req.grammarErrorInfo!.stepIndex; + final eventID = req.grammarErrorInfo!.eventID; + + final igcMatch = + choreo.choreoSteps[stepIndex].acceptedOrIgnoredMatch?.match; + assert(igcMatch?.choices != null, 'IGC match must have choices'); + assert(igcMatch?.bestChoice != null, 'IGC match must have a best choice'); + + final correctChoice = igcMatch!.bestChoice!.value; final choices = igcMatch.choices!.map((c) => c.value).toList(); - final choiceTokens = target.tokens.where( - (token) => choices.any( - (choice) => choice.contains(token.text.content), - ), - ); - - assert( - choiceTokens.isNotEmpty, - 'At least one token should match the error choices', - ); + final stepText = choreo.stepText(stepIndex: stepIndex - 1); + final errorSpan = stepText.characters + .skip(igcMatch.offset) + .take(igcMatch.length) + .toString(); choices.add(errorSpan); choices.shuffle(); return MessageActivityResponse( activity: GrammarErrorPracticeActivityModel( - tokens: choiceTokens.toList(), + tokens: req.target.tokens, langCode: req.userL2, multipleChoiceContent: MultipleChoiceActivity( choices: choices.toSet(), answers: {correctChoice}, ), + text: stepText, + errorOffset: igcMatch.offset, + errorLength: igcMatch.length, + eventID: eventID, ), ); } diff --git a/lib/pangea/practice_activities/message_activity_request.dart b/lib/pangea/practice_activities/message_activity_request.dart index fef5c5243..47eebe6db 100644 --- a/lib/pangea/practice_activities/message_activity_request.dart +++ b/lib/pangea/practice_activities/message_activity_request.dart @@ -1,5 +1,11 @@ +import 'package:flutter/material.dart'; + import 'package:sentry_flutter/sentry_flutter.dart'; +import 'package:fluffychat/l10n/l10n.dart'; +import 'package:fluffychat/pangea/choreographer/choreo_record_model.dart'; +import 'package:fluffychat/pangea/morphs/morph_features_enum.dart'; +import 'package:fluffychat/pangea/practice_activities/activity_type_enum.dart'; import 'package:fluffychat/pangea/practice_activities/practice_activity_model.dart'; import 'package:fluffychat/pangea/practice_activities/practice_target.dart'; @@ -35,23 +41,67 @@ class ActivityQualityFeedback { } } +class GrammarErrorRequestInfo { + final ChoreoRecordModel choreo; + final int stepIndex; + final String eventID; + + const GrammarErrorRequestInfo({ + required this.choreo, + required this.stepIndex, + required this.eventID, + }); + + Map toJson() { + return { + 'choreo': choreo.toJson(), + 'step_index': stepIndex, + 'event_id': eventID, + }; + } + + factory GrammarErrorRequestInfo.fromJson(Map json) { + return GrammarErrorRequestInfo( + choreo: ChoreoRecordModel.fromJson(json['choreo']), + stepIndex: json['step_index'] as int, + eventID: json['event_id'] as String, + ); + } +} + class MessageActivityRequest { final String userL1; final String userL2; final PracticeTarget target; final ActivityQualityFeedback? activityQualityFeedback; + final GrammarErrorRequestInfo? grammarErrorInfo; MessageActivityRequest({ required this.userL1, required this.userL2, required this.activityQualityFeedback, required this.target, + this.grammarErrorInfo, }) { if (target.tokens.isEmpty) { throw Exception('Target tokens must not be empty'); } } + String promptText(BuildContext context) { + switch (target.activityType) { + case ActivityTypeEnum.grammarCategory: + return L10n.of(context).whatIsTheMorphTag( + target.morphFeature!.getDisplayCopy(context), + target.tokens.first.text.content, + ); + case ActivityTypeEnum.grammarError: + return L10n.of(context).fillInBlank; + default: + return target.tokens.first.vocabConstructID.lemma; + } + } + Map toJson() { return { 'user_l1': userL1, @@ -60,6 +110,7 @@ class MessageActivityRequest { 'target_tokens': target.tokens.map((e) => e.toJson()).toList(), 'target_type': target.activityType.name, 'target_morph_feature': target.morphFeature, + 'grammar_error_info': grammarErrorInfo?.toJson(), }; } @@ -72,7 +123,8 @@ class MessageActivityRequest { other.userL2 == userL2 && other.target == target && other.activityQualityFeedback?.feedbackText == - activityQualityFeedback?.feedbackText; + activityQualityFeedback?.feedbackText && + other.grammarErrorInfo == grammarErrorInfo; } @override @@ -80,7 +132,8 @@ class MessageActivityRequest { return activityQualityFeedback.hashCode ^ target.hashCode ^ userL1.hashCode ^ - userL2.hashCode; + userL2.hashCode ^ + grammarErrorInfo.hashCode; } } diff --git a/lib/pangea/practice_activities/practice_activity_model.dart b/lib/pangea/practice_activities/practice_activity_model.dart index 36d284f0b..602165b7c 100644 --- a/lib/pangea/practice_activities/practice_activity_model.dart +++ b/lib/pangea/practice_activities/practice_activity_model.dart @@ -354,10 +354,19 @@ class LemmaPracticeActivityModel extends MultipleChoicePracticeActivityModel { class GrammarErrorPracticeActivityModel extends MultipleChoicePracticeActivityModel { + final String text; + final int errorOffset; + final int errorLength; + final String eventID; + GrammarErrorPracticeActivityModel({ required super.tokens, required super.langCode, required super.multipleChoiceContent, + required this.text, + required this.errorOffset, + required this.errorLength, + required this.eventID, }); } diff --git a/lib/pangea/practice_activities/practice_generation_repo.dart b/lib/pangea/practice_activities/practice_generation_repo.dart index 3ff418594..35a961940 100644 --- a/lib/pangea/practice_activities/practice_generation_repo.dart +++ b/lib/pangea/practice_activities/practice_generation_repo.dart @@ -130,6 +130,10 @@ class PracticeRepo { case ActivityTypeEnum.grammarCategory: return MorphCategoryActivityGenerator.get(req); case ActivityTypeEnum.grammarError: + assert( + req.grammarErrorInfo != null, + 'Grammar error info must be provided for grammar error activities', + ); return GrammarErrorPracticeGenerator.get(req); case ActivityTypeEnum.morphId: return MorphActivityGenerator.get(req); diff --git a/lib/pangea/practice_activities/practice_target.dart b/lib/pangea/practice_activities/practice_target.dart index d6a0a1540..87c1ce7e7 100644 --- a/lib/pangea/practice_activities/practice_target.dart +++ b/lib/pangea/practice_activities/practice_target.dart @@ -1,9 +1,7 @@ import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; import 'package:collection/collection.dart'; -import 'package:fluffychat/l10n/l10n.dart'; import 'package:fluffychat/pangea/constructs/construct_identifier.dart'; import 'package:fluffychat/pangea/events/models/pangea_token_model.dart'; import 'package:fluffychat/pangea/morphs/morph_features_enum.dart'; @@ -85,18 +83,6 @@ class PracticeTarget { (morphFeature?.name ?? ""); } - String promptText(BuildContext context) { - switch (activityType) { - case ActivityTypeEnum.grammarCategory: - return L10n.of(context).whatIsTheMorphTag( - morphFeature!.getDisplayCopy(context), - tokens.first.text.content, - ); - default: - return tokens.first.vocabConstructID.lemma; - } - } - ConstructIdentifier targetTokenConstructID(PangeaToken token) { final defaultID = token.vocabConstructID; final ConstructIdentifier? cId = morphFeature == null From 112354f7ae924df46f40ba35225fd0e3713da777 Mon Sep 17 00:00:00 2001 From: ggurdin Date: Tue, 20 Jan 2026 11:43:34 -0500 Subject: [PATCH 14/27] if not enough error practice, fill in with morph match practice --- .../analytics_practice_session_repo.dart | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/lib/pangea/analytics_practice/analytics_practice_session_repo.dart b/lib/pangea/analytics_practice/analytics_practice_session_repo.dart index 6124234e7..378dc4486 100644 --- a/lib/pangea/analytics_practice/analytics_practice_session_repo.dart +++ b/lib/pangea/analytics_practice/analytics_practice_session_repo.dart @@ -44,6 +44,27 @@ class AnalyticsPracticeSessionRepo { } else { final errorTargets = await _fetchErrors(); targets.addAll(errorTargets); + + if (targets.length < AnalyticsPracticeConstants.practiceGroupSize) { + final morphs = await _fetchMorphs(); + final remainingCount = + AnalyticsPracticeConstants.practiceGroupSize - targets.length; + final morphEntries = morphs.entries.take(remainingCount); + + for (final entry in morphEntries) { + targets.add( + AnalyticsActivityTarget( + target: PracticeTarget( + tokens: [entry.key], + activityType: types[targets.length], + morphFeature: entry.value, + ), + ), + ); + } + + targets.shuffle(); + } } final session = AnalyticsPracticeSessionModel( From 4559e95285a63fcac3f785685997036437dfdfba Mon Sep 17 00:00:00 2001 From: ggurdin Date: Tue, 20 Jan 2026 11:54:44 -0500 Subject: [PATCH 15/27] chore: improve visibility of token hover underlines --- lib/pages/chat/events/html_message.dart | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/pages/chat/events/html_message.dart b/lib/pages/chat/events/html_message.dart index dda308453..579045142 100644 --- a/lib/pages/chat/events/html_message.dart +++ b/lib/pages/chat/events/html_message.dart @@ -417,7 +417,9 @@ class HtmlMessage extends StatelessWidget { final renderer = TokenRenderingUtil(); - final underlineColor = Theme.of(context).colorScheme.primary.withAlpha(200); + final underlineColor = pangeaMessageEvent!.ownMessage + ? ThemeData.dark().colorScheme.primaryContainer.withAlpha(200) + : Theme.of(context).colorScheme.primary.withAlpha(200); final newTokens = pangeaMessageEvent != null && !pangeaMessageEvent!.ownMessage From 91701b18342eb3a5f20a4801ff7ff0b88657271b Mon Sep 17 00:00:00 2001 From: ggurdin Date: Tue, 20 Jan 2026 12:01:09 -0500 Subject: [PATCH 16/27] chore: use cached derived data in level bar when available --- .../analytics_data/analytics_data_service.dart | 2 ++ .../learning_progress_indicators.dart | 14 ++++++++------ 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/lib/pangea/analytics_data/analytics_data_service.dart b/lib/pangea/analytics_data/analytics_data_service.dart index 0f250f121..31032b887 100644 --- a/lib/pangea/analytics_data/analytics_data_service.dart +++ b/lib/pangea/analytics_data/analytics_data_service.dart @@ -214,6 +214,8 @@ class AnalyticsDataService { await _syncController?.waitForSync(analyticsRoomID); } + DerivedAnalyticsDataModel? get cachedDerivedData => _cachedDerivedStats; + Future get derivedData async { await _ensureInitialized(); diff --git a/lib/pangea/analytics_summary/learning_progress_indicators.dart b/lib/pangea/analytics_summary/learning_progress_indicators.dart index d89851c6c..8c7079d2c 100644 --- a/lib/pangea/analytics_summary/learning_progress_indicators.dart +++ b/lib/pangea/analytics_summary/learning_progress_indicators.dart @@ -206,21 +206,23 @@ class LearningProgressIndicators extends StatelessWidget { child: FutureBuilder( future: analyticsService.derivedData, builder: (context, snapshot) { + final cached = + analyticsService.cachedDerivedData; + final data = snapshot.data ?? cached; return Row( spacing: 8.0, children: [ Expanded( child: LearningProgressBar( height: 24.0, - loading: !snapshot.hasData, - progress: snapshot - .data?.levelProgress ?? - 0.0, + loading: data == null, + progress: + data?.levelProgress ?? 0.0, ), ), - if (snapshot.hasData) + if (data != null) Text( - "⭐ ${snapshot.data!.level}", + "⭐ ${data.level}", style: Theme.of(context) .textTheme .titleLarge From 445ed0338d9cdd60f2db4f227af266018abc1320 Mon Sep 17 00:00:00 2001 From: Ava Shilling <165050625+avashilling@users.noreply.github.com> Date: Tue, 20 Jan 2026 12:04:30 -0500 Subject: [PATCH 17/27] chore: move regeneration button down to toolbar --- lib/pages/chat/events/message.dart | 14 ----- .../widgets/request_regeneration_button.dart | 59 ------------------- .../toolbar/layout/overlay_message.dart | 6 -- .../select_mode_buttons.dart | 19 +++++- .../select_mode_controller.dart | 1 + 5 files changed, 18 insertions(+), 81 deletions(-) delete mode 100644 lib/pangea/chat/widgets/request_regeneration_button.dart diff --git a/lib/pages/chat/events/message.dart b/lib/pages/chat/events/message.dart index d3ac2306b..fe4f9bf1f 100644 --- a/lib/pages/chat/events/message.dart +++ b/lib/pages/chat/events/message.dart @@ -16,7 +16,6 @@ import 'package:fluffychat/pangea/activity_sessions/activity_summary_widget.dart import 'package:fluffychat/pangea/bot/utils/bot_name.dart'; import 'package:fluffychat/pangea/bot/widgets/bot_settings_language_icon.dart'; import 'package:fluffychat/pangea/chat/extensions/custom_room_display_extension.dart'; -import 'package:fluffychat/pangea/chat/widgets/request_regeneration_button.dart'; import 'package:fluffychat/pangea/common/widgets/pressable_button.dart'; import 'package:fluffychat/pangea/common/widgets/shimmer_background.dart'; import 'package:fluffychat/pangea/events/constants/pangea_event_types.dart'; @@ -822,20 +821,7 @@ class Message extends StatelessWidget { ), ], ), - ) - // #Pangea - else if (canRefresh) - RequestRegenerationButton( - textColor: - textColor, - onPressed: () => - controller - .requestRegeneration( - event - .eventId, - ), ), - // Pangea# ], ), ), diff --git a/lib/pangea/chat/widgets/request_regeneration_button.dart b/lib/pangea/chat/widgets/request_regeneration_button.dart deleted file mode 100644 index eaa4cc56b..000000000 --- a/lib/pangea/chat/widgets/request_regeneration_button.dart +++ /dev/null @@ -1,59 +0,0 @@ -import 'package:flutter/material.dart'; - -import 'package:fluffychat/l10n/l10n.dart'; - -class RequestRegenerationButton extends StatelessWidget { - final Color textColor; - final VoidCallback onPressed; - - const RequestRegenerationButton({ - super.key, - required this.textColor, - required this.onPressed, - }); - - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.only( - bottom: 8.0, - left: 16.0, - right: 16.0, - ), - child: TextButton( - style: TextButton.styleFrom( - padding: EdgeInsets.zero, - minimumSize: const Size( - 0, - 0, - ), - ), - onPressed: onPressed, - child: Row( - mainAxisSize: MainAxisSize.min, - spacing: 4.0, - children: [ - Icon( - Icons.refresh, - color: textColor.withAlpha( - 164, - ), - size: 14, - ), - Text( - L10n.of( - context, - ).requestRegeneration, - style: TextStyle( - color: textColor.withAlpha( - 164, - ), - fontSize: 11, - ), - ), - ], - ), - ), - ); - } -} diff --git a/lib/pangea/toolbar/layout/overlay_message.dart b/lib/pangea/toolbar/layout/overlay_message.dart index ad575ca0e..2e0b134f3 100644 --- a/lib/pangea/toolbar/layout/overlay_message.dart +++ b/lib/pangea/toolbar/layout/overlay_message.dart @@ -10,7 +10,6 @@ import 'package:fluffychat/l10n/l10n.dart'; import 'package:fluffychat/pages/chat/chat.dart'; import 'package:fluffychat/pages/chat/events/message_content.dart'; import 'package:fluffychat/pages/chat/events/reply_content.dart'; -import 'package:fluffychat/pangea/chat/widgets/request_regeneration_button.dart'; import 'package:fluffychat/pangea/common/utils/async_state.dart'; import 'package:fluffychat/pangea/common/widgets/error_indicator.dart'; import 'package:fluffychat/pangea/events/extensions/pangea_event_extension.dart'; @@ -258,11 +257,6 @@ class OverlayMessage extends StatelessWidget { ), ], ), - ) - else if (canRefresh) - RequestRegenerationButton( - textColor: textColor, - onPressed: () => controller.requestRegeneration(event.eventId), ), ], ), diff --git a/lib/pangea/toolbar/reading_assistance/select_mode_buttons.dart b/lib/pangea/toolbar/reading_assistance/select_mode_buttons.dart index 116fd451d..6549403a9 100644 --- a/lib/pangea/toolbar/reading_assistance/select_mode_buttons.dart +++ b/lib/pangea/toolbar/reading_assistance/select_mode_buttons.dart @@ -29,7 +29,8 @@ enum SelectMode { translate(Icons.translate), practice(Symbols.fitness_center), emoji(Icons.add_reaction_outlined), - speechTranslation(Icons.translate); + speechTranslation(Icons.translate), + requestRegenerate(Icons.replay); final IconData icon; const SelectMode(this.icon); @@ -46,6 +47,8 @@ enum SelectMode { return l10n.practice; case SelectMode.emoji: return l10n.emojiView; + case SelectMode.requestRegenerate: + return l10n.requestRegeneration; } } } @@ -214,6 +217,12 @@ class SelectModeButtonsState extends State { if (updatedMode == SelectMode.speechTranslation) { await controller.fetchSpeechTranslation(); } + + if (updatedMode == SelectMode.requestRegenerate) { + widget.controller.requestRegeneration( + messageEvent.eventId, + ); + } } Future modeDisabled() async { @@ -348,7 +357,13 @@ class SelectModeButtonsState extends State { Widget build(BuildContext context) { final theme = Theme.of(context); final modes = controller.readingAssistanceModes; - final allModes = controller.allModes; + final allModes = controller.allModes + .where( + (mode) => + mode != SelectMode.requestRegenerate || + messageEvent.eventId == widget.controller.refreshEventID, + ) + .toList(); return Material( type: MaterialType.transparency, child: SizedBox( diff --git a/lib/pangea/toolbar/reading_assistance/select_mode_controller.dart b/lib/pangea/toolbar/reading_assistance/select_mode_controller.dart index eee6bdcbc..e5de6dfaa 100644 --- a/lib/pangea/toolbar/reading_assistance/select_mode_controller.dart +++ b/lib/pangea/toolbar/reading_assistance/select_mode_controller.dart @@ -113,6 +113,7 @@ class SelectModeController with LemmaEmojiSetter { SelectMode.translate, SelectMode.practice, SelectMode.emoji, + SelectMode.requestRegenerate, ]; static List get _audioModes => [ From eafa8f60f0f8a6aff260d6e4527666aa5c9b1807 Mon Sep 17 00:00:00 2001 From: ggurdin Date: Tue, 20 Jan 2026 12:33:25 -0500 Subject: [PATCH 18/27] chore: replace message toolbar language mismatch popup with button in disabled snackbar --- lib/l10n/intl_ar.arb | 27 ++++++++- lib/l10n/intl_be.arb | 27 ++++++++- lib/l10n/intl_bn.arb | 27 ++++++++- lib/l10n/intl_bo.arb | 27 ++++++++- lib/l10n/intl_ca.arb | 27 ++++++++- lib/l10n/intl_cs.arb | 27 ++++++++- lib/l10n/intl_da.arb | 27 ++++++++- lib/l10n/intl_de.arb | 27 ++++++++- lib/l10n/intl_el.arb | 27 ++++++++- lib/l10n/intl_en.arb | 4 +- lib/l10n/intl_eo.arb | 27 ++++++++- lib/l10n/intl_es.arb | 27 ++++++++- lib/l10n/intl_et.arb | 27 ++++++++- lib/l10n/intl_eu.arb | 27 ++++++++- lib/l10n/intl_fa.arb | 27 ++++++++- lib/l10n/intl_fi.arb | 27 ++++++++- lib/l10n/intl_fil.arb | 27 ++++++++- lib/l10n/intl_fr.arb | 27 ++++++++- lib/l10n/intl_ga.arb | 27 ++++++++- lib/l10n/intl_gl.arb | 27 ++++++++- lib/l10n/intl_he.arb | 27 ++++++++- lib/l10n/intl_hi.arb | 27 ++++++++- lib/l10n/intl_hr.arb | 27 ++++++++- lib/l10n/intl_hu.arb | 27 ++++++++- lib/l10n/intl_ia.arb | 27 ++++++++- lib/l10n/intl_id.arb | 27 ++++++++- lib/l10n/intl_ie.arb | 27 ++++++++- lib/l10n/intl_it.arb | 27 ++++++++- lib/l10n/intl_ja.arb | 27 ++++++++- lib/l10n/intl_ka.arb | 27 ++++++++- lib/l10n/intl_ko.arb | 27 ++++++++- lib/l10n/intl_lt.arb | 27 ++++++++- lib/l10n/intl_lv.arb | 27 ++++++++- lib/l10n/intl_nb.arb | 27 ++++++++- lib/l10n/intl_nl.arb | 27 ++++++++- lib/l10n/intl_pl.arb | 27 ++++++++- lib/l10n/intl_pt.arb | 27 ++++++++- lib/l10n/intl_pt_BR.arb | 27 ++++++++- lib/l10n/intl_pt_PT.arb | 27 ++++++++- lib/l10n/intl_ro.arb | 27 ++++++++- lib/l10n/intl_ru.arb | 27 ++++++++- lib/l10n/intl_sk.arb | 27 ++++++++- lib/l10n/intl_sl.arb | 27 ++++++++- lib/l10n/intl_sr.arb | 27 ++++++++- lib/l10n/intl_sv.arb | 27 ++++++++- lib/l10n/intl_ta.arb | 27 ++++++++- lib/l10n/intl_te.arb | 27 ++++++++- lib/l10n/intl_th.arb | 27 ++++++++- lib/l10n/intl_tr.arb | 27 ++++++++- lib/l10n/intl_uk.arb | 27 ++++++++- lib/l10n/intl_vi.arb | 27 ++++++++- lib/l10n/intl_yue.arb | 27 ++++++++- lib/l10n/intl_zh.arb | 27 ++++++++- lib/l10n/intl_zh_Hant.arb | 27 ++++++++- lib/pages/chat/chat.dart | 56 ++++++++++--------- .../language_mismatch_repo.dart | 4 -- .../select_mode_buttons.dart | 34 +++++++++-- 57 files changed, 1441 insertions(+), 88 deletions(-) diff --git a/lib/l10n/intl_ar.arb b/lib/l10n/intl_ar.arb index 3f762775a..70fa0fc97 100644 --- a/lib/l10n/intl_ar.arb +++ b/lib/l10n/intl_ar.arb @@ -1,6 +1,6 @@ { "@@locale": "ar", - "@@last_modified": "2026-01-16 14:33:24.230348", + "@@last_modified": "2026-01-20 12:31:24.671375", "about": "حول", "@about": { "type": "String", @@ -11082,5 +11082,30 @@ "@constructUseIncGCDesc": { "type": "String", "placeholders": {} + }, + "constructUseCorGEDesc": "ممارسة تصحيح أخطاء القواعد", + "constructUseIncGEDesc": "ممارسة أخطاء القواعد غير الصحيحة", + "fillInBlank": "املأ الفراغ بالخيار الصحيح", + "learn": "تعلم", + "languageUpdated": "تم تحديث اللغة المستهدفة!", + "@constructUseCorGEDesc": { + "type": "String", + "placeholders": {} + }, + "@constructUseIncGEDesc": { + "type": "String", + "placeholders": {} + }, + "@fillInBlank": { + "type": "String", + "placeholders": {} + }, + "@learn": { + "type": "String", + "placeholders": {} + }, + "@languageUpdated": { + "type": "String", + "placeholders": {} } } \ No newline at end of file diff --git a/lib/l10n/intl_be.arb b/lib/l10n/intl_be.arb index 985752f67..d1ff0babf 100644 --- a/lib/l10n/intl_be.arb +++ b/lib/l10n/intl_be.arb @@ -1911,7 +1911,7 @@ "playWithAI": "Пакуль гуляйце з ШІ", "courseStartDesc": "Pangea Bot гатовы да працы ў любы час!\n\n...але навучанне лепш з сябрамі!", "@@locale": "be", - "@@last_modified": "2026-01-16 14:33:05.869532", + "@@last_modified": "2026-01-20 12:31:06.570011", "@alwaysUse24HourFormat": { "type": "String", "placeholders": {} @@ -11964,5 +11964,30 @@ "@constructUseIncGCDesc": { "type": "String", "placeholders": {} + }, + "constructUseCorGEDesc": "Практыка правільнага выкарыстання граматычных памылак", + "constructUseIncGEDesc": "Практыка няправільнага выкарыстання граматычных памылак", + "fillInBlank": "Запоўніце прабел правільным выбарам", + "learn": "Навучыцца", + "languageUpdated": "Мэтавая мова абноўлена!", + "@constructUseCorGEDesc": { + "type": "String", + "placeholders": {} + }, + "@constructUseIncGEDesc": { + "type": "String", + "placeholders": {} + }, + "@fillInBlank": { + "type": "String", + "placeholders": {} + }, + "@learn": { + "type": "String", + "placeholders": {} + }, + "@languageUpdated": { + "type": "String", + "placeholders": {} } } \ No newline at end of file diff --git a/lib/l10n/intl_bn.arb b/lib/l10n/intl_bn.arb index 4cf1d6221..28c180422 100644 --- a/lib/l10n/intl_bn.arb +++ b/lib/l10n/intl_bn.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2026-01-16 14:33:49.084569", + "@@last_modified": "2026-01-20 12:31:49.464406", "about": "সম্পর্কে", "@about": { "type": "String", @@ -11969,5 +11969,30 @@ "@constructUseIncGCDesc": { "type": "String", "placeholders": {} + }, + "constructUseCorGEDesc": "সঠিক ব্যাকরণ ত্রুটি অনুশীলন", + "constructUseIncGEDesc": "ভুল ব্যাকরণ ত্রুটি অনুশীলন", + "fillInBlank": "সঠিক পছন্দ দিয়ে ফাঁকা স্থান পূরণ করুন", + "learn": "শিখুন", + "languageUpdated": "লক্ষ্য ভাষা আপডেট করা হয়েছে!", + "@constructUseCorGEDesc": { + "type": "String", + "placeholders": {} + }, + "@constructUseIncGEDesc": { + "type": "String", + "placeholders": {} + }, + "@fillInBlank": { + "type": "String", + "placeholders": {} + }, + "@learn": { + "type": "String", + "placeholders": {} + }, + "@languageUpdated": { + "type": "String", + "placeholders": {} } } \ No newline at end of file diff --git a/lib/l10n/intl_bo.arb b/lib/l10n/intl_bo.arb index 067627647..800c24594 100644 --- a/lib/l10n/intl_bo.arb +++ b/lib/l10n/intl_bo.arb @@ -4279,7 +4279,7 @@ "joinPublicTrip": "མི་ཚེས་ལ་ལོག་འབད།", "startOwnTrip": "ངེད་རང་གི་ལོག་ལ་སྦྱོར་བཅོས།", "@@locale": "bo", - "@@last_modified": "2026-01-16 14:33:43.824560", + "@@last_modified": "2026-01-20 12:31:44.969872", "@alwaysUse24HourFormat": { "type": "String", "placeholders": {} @@ -10619,5 +10619,30 @@ "@constructUseIncGCDesc": { "type": "String", "placeholders": {} + }, + "constructUseCorGEDesc": "Praktik kesalahan tata bahasa yang benar", + "constructUseIncGEDesc": "Praktik kesalahan tata bahasa yang salah", + "fillInBlank": "Isi kekosongan dengan pilihan yang benar", + "learn": "Belajar", + "languageUpdated": "Bahasa target diperbarui!", + "@constructUseCorGEDesc": { + "type": "String", + "placeholders": {} + }, + "@constructUseIncGEDesc": { + "type": "String", + "placeholders": {} + }, + "@fillInBlank": { + "type": "String", + "placeholders": {} + }, + "@learn": { + "type": "String", + "placeholders": {} + }, + "@languageUpdated": { + "type": "String", + "placeholders": {} } } \ No newline at end of file diff --git a/lib/l10n/intl_ca.arb b/lib/l10n/intl_ca.arb index 80d465783..5eb437241 100644 --- a/lib/l10n/intl_ca.arb +++ b/lib/l10n/intl_ca.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2026-01-16 14:33:08.940071", + "@@last_modified": "2026-01-20 12:31:09.126351", "about": "Quant a", "@about": { "type": "String", @@ -10889,5 +10889,30 @@ "@constructUseIncGCDesc": { "type": "String", "placeholders": {} + }, + "constructUseCorGEDesc": "Pràctica d'errors gramaticals correctes", + "constructUseIncGEDesc": "Pràctica d'errors gramaticals incorrectes", + "fillInBlank": "Omple el buit amb l'elecció correcta", + "learn": "Aprendre", + "languageUpdated": "Idioma objectiu actualitzat!", + "@constructUseCorGEDesc": { + "type": "String", + "placeholders": {} + }, + "@constructUseIncGEDesc": { + "type": "String", + "placeholders": {} + }, + "@fillInBlank": { + "type": "String", + "placeholders": {} + }, + "@learn": { + "type": "String", + "placeholders": {} + }, + "@languageUpdated": { + "type": "String", + "placeholders": {} } } \ No newline at end of file diff --git a/lib/l10n/intl_cs.arb b/lib/l10n/intl_cs.arb index 300a94f0d..321734d12 100644 --- a/lib/l10n/intl_cs.arb +++ b/lib/l10n/intl_cs.arb @@ -1,6 +1,6 @@ { "@@locale": "cs", - "@@last_modified": "2026-01-16 14:32:59.173160", + "@@last_modified": "2026-01-20 12:31:00.323945", "about": "O aplikaci", "@about": { "type": "String", @@ -11472,5 +11472,30 @@ "@constructUseIncGCDesc": { "type": "String", "placeholders": {} + }, + "constructUseCorGEDesc": "Cvičení správné gramatiky", + "constructUseIncGEDesc": "Cvičení nesprávné gramatiky", + "fillInBlank": "Doplňte prázdné místo správnou volbou", + "learn": "Učit se", + "languageUpdated": "Cílový jazyk byl aktualizován!", + "@constructUseCorGEDesc": { + "type": "String", + "placeholders": {} + }, + "@constructUseIncGEDesc": { + "type": "String", + "placeholders": {} + }, + "@fillInBlank": { + "type": "String", + "placeholders": {} + }, + "@learn": { + "type": "String", + "placeholders": {} + }, + "@languageUpdated": { + "type": "String", + "placeholders": {} } } \ No newline at end of file diff --git a/lib/l10n/intl_da.arb b/lib/l10n/intl_da.arb index ae7a9ef5a..372a6e7c8 100644 --- a/lib/l10n/intl_da.arb +++ b/lib/l10n/intl_da.arb @@ -1930,7 +1930,7 @@ "playWithAI": "Leg med AI for nu", "courseStartDesc": "Pangea Bot er klar til at starte når som helst!\n\n...men læring er bedre med venner!", "@@locale": "da", - "@@last_modified": "2026-01-16 14:32:06.899995", + "@@last_modified": "2026-01-20 12:30:10.761209", "@aboutHomeserver": { "type": "String", "placeholders": { @@ -11926,5 +11926,30 @@ "@constructUseIncGCDesc": { "type": "String", "placeholders": {} + }, + "constructUseCorGEDesc": "Korrekt grammatikfejl praksis", + "constructUseIncGEDesc": "Ukorrrekt grammatikfejl praksis", + "fillInBlank": "Udfyld det tomme felt med det korrekte valg", + "learn": "Lær", + "languageUpdated": "Mål sprog opdateret!", + "@constructUseCorGEDesc": { + "type": "String", + "placeholders": {} + }, + "@constructUseIncGEDesc": { + "type": "String", + "placeholders": {} + }, + "@fillInBlank": { + "type": "String", + "placeholders": {} + }, + "@learn": { + "type": "String", + "placeholders": {} + }, + "@languageUpdated": { + "type": "String", + "placeholders": {} } } \ No newline at end of file diff --git a/lib/l10n/intl_de.arb b/lib/l10n/intl_de.arb index 0f581a3a0..634b76211 100644 --- a/lib/l10n/intl_de.arb +++ b/lib/l10n/intl_de.arb @@ -1,6 +1,6 @@ { "@@locale": "de", - "@@last_modified": "2026-01-16 14:32:44.617501", + "@@last_modified": "2026-01-20 12:30:47.434524", "alwaysUse24HourFormat": "true", "@alwaysUse24HourFormat": { "description": "Set to true to always display time of day in 24 hour format." @@ -10872,5 +10872,30 @@ "@constructUseIncGCDesc": { "type": "String", "placeholders": {} + }, + "constructUseCorGEDesc": "Korrekte Grammatikfehlerübung", + "constructUseIncGEDesc": "Falsche Grammatikfehlerübung", + "fillInBlank": "Füllen Sie die Lücke mit der richtigen Wahl aus", + "learn": "Lernen", + "languageUpdated": "Zielsprache aktualisiert!", + "@constructUseCorGEDesc": { + "type": "String", + "placeholders": {} + }, + "@constructUseIncGEDesc": { + "type": "String", + "placeholders": {} + }, + "@fillInBlank": { + "type": "String", + "placeholders": {} + }, + "@learn": { + "type": "String", + "placeholders": {} + }, + "@languageUpdated": { + "type": "String", + "placeholders": {} } } \ No newline at end of file diff --git a/lib/l10n/intl_el.arb b/lib/l10n/intl_el.arb index dfb2f4a86..176fb6f07 100644 --- a/lib/l10n/intl_el.arb +++ b/lib/l10n/intl_el.arb @@ -4456,7 +4456,7 @@ "playWithAI": "Παίξτε με την Τεχνητή Νοημοσύνη προς το παρόν", "courseStartDesc": "Ο Pangea Bot είναι έτοιμος να ξεκινήσει οποιαδήποτε στιγμή!\n\n...αλλά η μάθηση είναι καλύτερη με φίλους!", "@@locale": "el", - "@@last_modified": "2026-01-16 14:33:59.313114", + "@@last_modified": "2026-01-20 12:31:59.503296", "@alwaysUse24HourFormat": { "type": "String", "placeholders": {} @@ -11923,5 +11923,30 @@ "@constructUseIncGCDesc": { "type": "String", "placeholders": {} + }, + "constructUseCorGEDesc": "Πρακτική διόρθωσης γραμματικών λαθών", + "constructUseIncGEDesc": "Πρακτική λανθασμένων γραμματικών λαθών", + "fillInBlank": "Συμπληρώστε το κενό με τη σωστή επιλογή", + "learn": "Μάθετε", + "languageUpdated": "Η γλώσσα στόχος ενημερώθηκε!", + "@constructUseCorGEDesc": { + "type": "String", + "placeholders": {} + }, + "@constructUseIncGEDesc": { + "type": "String", + "placeholders": {} + }, + "@fillInBlank": { + "type": "String", + "placeholders": {} + }, + "@learn": { + "type": "String", + "placeholders": {} + }, + "@languageUpdated": { + "type": "String", + "placeholders": {} } } \ No newline at end of file diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index b1bcf4e02..abc4a3d63 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -5054,5 +5054,7 @@ "constructUseIncGCDesc": "Incorrect grammar category practice", "constructUseCorGEDesc": "Correct grammar error practice", "constructUseIncGEDesc": "Incorrect grammar error practice", - "fillInBlank": "Fill in the blank with the correct choice" + "fillInBlank": "Fill in the blank with the correct choice", + "learn": "Learn", + "languageUpdated": "Target language updated!" } diff --git a/lib/l10n/intl_eo.arb b/lib/l10n/intl_eo.arb index 39b6c61c0..5914ed46b 100644 --- a/lib/l10n/intl_eo.arb +++ b/lib/l10n/intl_eo.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2026-01-16 14:34:08.644512", + "@@last_modified": "2026-01-20 12:32:07.490560", "about": "Prio", "@about": { "type": "String", @@ -11954,5 +11954,30 @@ "@constructUseIncGCDesc": { "type": "String", "placeholders": {} + }, + "constructUseCorGEDesc": "Praktiko pri ĝusta gramatika eraro", + "constructUseIncGEDesc": "Praktiko pri malĝusta gramatika eraro", + "fillInBlank": "Plenigu la malplenan lokon per la ĝusta elekto", + "learn": "Lerni", + "languageUpdated": "Celo lingvo ĝisdatigita!", + "@constructUseCorGEDesc": { + "type": "String", + "placeholders": {} + }, + "@constructUseIncGEDesc": { + "type": "String", + "placeholders": {} + }, + "@fillInBlank": { + "type": "String", + "placeholders": {} + }, + "@learn": { + "type": "String", + "placeholders": {} + }, + "@languageUpdated": { + "type": "String", + "placeholders": {} } } \ No newline at end of file diff --git a/lib/l10n/intl_es.arb b/lib/l10n/intl_es.arb index 4ff6e98aa..144e71870 100644 --- a/lib/l10n/intl_es.arb +++ b/lib/l10n/intl_es.arb @@ -1,6 +1,6 @@ { "@@locale": "es", - "@@last_modified": "2026-01-16 14:31:58.653004", + "@@last_modified": "2026-01-20 12:29:59.898184", "about": "Acerca de", "@about": { "type": "String", @@ -8099,5 +8099,30 @@ "@constructUseIncGCDesc": { "type": "String", "placeholders": {} + }, + "constructUseCorGEDesc": "Práctica de errores gramaticales correctos", + "constructUseIncGEDesc": "Práctica de errores gramaticales incorrectos", + "fillInBlank": "Completa el espacio en blanco con la opción correcta", + "learn": "Aprender", + "languageUpdated": "¡Idioma objetivo actualizado!", + "@constructUseCorGEDesc": { + "type": "String", + "placeholders": {} + }, + "@constructUseIncGEDesc": { + "type": "String", + "placeholders": {} + }, + "@fillInBlank": { + "type": "String", + "placeholders": {} + }, + "@learn": { + "type": "String", + "placeholders": {} + }, + "@languageUpdated": { + "type": "String", + "placeholders": {} } } \ No newline at end of file diff --git a/lib/l10n/intl_et.arb b/lib/l10n/intl_et.arb index a1d03040b..5d4e3f6c2 100644 --- a/lib/l10n/intl_et.arb +++ b/lib/l10n/intl_et.arb @@ -1,6 +1,6 @@ { "@@locale": "et", - "@@last_modified": "2026-01-16 14:32:41.585481", + "@@last_modified": "2026-01-20 12:30:45.162738", "about": "Rakenduse teave", "@about": { "type": "String", @@ -11136,5 +11136,30 @@ "@constructUseIncGCDesc": { "type": "String", "placeholders": {} + }, + "constructUseCorGEDesc": "Õige grammatika vea harjutamine", + "constructUseIncGEDesc": "Vale grammatika vea harjutamine", + "fillInBlank": "Täida tühik õige valikuga", + "learn": "Õpi", + "languageUpdated": "Sihtkeel on uuendatud!", + "@constructUseCorGEDesc": { + "type": "String", + "placeholders": {} + }, + "@constructUseIncGEDesc": { + "type": "String", + "placeholders": {} + }, + "@fillInBlank": { + "type": "String", + "placeholders": {} + }, + "@learn": { + "type": "String", + "placeholders": {} + }, + "@languageUpdated": { + "type": "String", + "placeholders": {} } } \ No newline at end of file diff --git a/lib/l10n/intl_eu.arb b/lib/l10n/intl_eu.arb index 253e41e61..225d963e7 100644 --- a/lib/l10n/intl_eu.arb +++ b/lib/l10n/intl_eu.arb @@ -1,6 +1,6 @@ { "@@locale": "eu", - "@@last_modified": "2026-01-16 14:32:36.050138", + "@@last_modified": "2026-01-20 12:30:40.144354", "about": "Honi buruz", "@about": { "type": "String", @@ -10865,5 +10865,30 @@ "@constructUseIncGCDesc": { "type": "String", "placeholders": {} + }, + "constructUseCorGEDesc": "Gramatika akats zuzenketa praktika", + "constructUseIncGEDesc": "Gramatika akats okerra praktika", + "fillInBlank": "Betekoa bete aukerarik egokienarekin", + "learn": "Ikasi", + "languageUpdated": "Helmuga hizkuntza eguneratua!", + "@constructUseCorGEDesc": { + "type": "String", + "placeholders": {} + }, + "@constructUseIncGEDesc": { + "type": "String", + "placeholders": {} + }, + "@fillInBlank": { + "type": "String", + "placeholders": {} + }, + "@learn": { + "type": "String", + "placeholders": {} + }, + "@languageUpdated": { + "type": "String", + "placeholders": {} } } \ No newline at end of file diff --git a/lib/l10n/intl_fa.arb b/lib/l10n/intl_fa.arb index 5a64b9826..977dd42b8 100644 --- a/lib/l10n/intl_fa.arb +++ b/lib/l10n/intl_fa.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2026-01-16 14:33:51.512389", + "@@last_modified": "2026-01-20 12:31:52.231221", "repeatPassword": "تکرار رمزعبور", "@repeatPassword": {}, "about": "درباره", @@ -11597,5 +11597,30 @@ "@constructUseIncGCDesc": { "type": "String", "placeholders": {} + }, + "constructUseCorGEDesc": "تمرین خطای گرامری صحیح", + "constructUseIncGEDesc": "تمرین خطای گرامری نادرست", + "fillInBlank": "جای خالی را با گزینه صحیح پر کنید", + "learn": "یاد بگیرید", + "languageUpdated": "زبان هدف به‌روزرسانی شد!", + "@constructUseCorGEDesc": { + "type": "String", + "placeholders": {} + }, + "@constructUseIncGEDesc": { + "type": "String", + "placeholders": {} + }, + "@fillInBlank": { + "type": "String", + "placeholders": {} + }, + "@learn": { + "type": "String", + "placeholders": {} + }, + "@languageUpdated": { + "type": "String", + "placeholders": {} } } \ No newline at end of file diff --git a/lib/l10n/intl_fi.arb b/lib/l10n/intl_fi.arb index 0c8f48151..6184adfec 100644 --- a/lib/l10n/intl_fi.arb +++ b/lib/l10n/intl_fi.arb @@ -4009,7 +4009,7 @@ "playWithAI": "Leiki tekoälyn kanssa nyt", "courseStartDesc": "Pangea Bot on valmis milloin tahansa!\n\n...mutta oppiminen on parempaa ystävien kanssa!", "@@locale": "fi", - "@@last_modified": "2026-01-16 14:32:03.875087", + "@@last_modified": "2026-01-20 12:30:08.099637", "@alwaysUse24HourFormat": { "type": "String", "placeholders": {} @@ -11488,5 +11488,30 @@ "@constructUseIncGCDesc": { "type": "String", "placeholders": {} + }, + "constructUseCorGEDesc": "Oikean kielioppivirheen harjoittelu", + "constructUseIncGEDesc": "Väärän kielioppivirheen harjoittelu", + "fillInBlank": "Täytä tyhjä kohta oikealla valinnalla", + "learn": "Oppia", + "languageUpdated": "Kohdekieli päivitetty!", + "@constructUseCorGEDesc": { + "type": "String", + "placeholders": {} + }, + "@constructUseIncGEDesc": { + "type": "String", + "placeholders": {} + }, + "@fillInBlank": { + "type": "String", + "placeholders": {} + }, + "@learn": { + "type": "String", + "placeholders": {} + }, + "@languageUpdated": { + "type": "String", + "placeholders": {} } } \ No newline at end of file diff --git a/lib/l10n/intl_fil.arb b/lib/l10n/intl_fil.arb index 0a79ce37a..986aa4015 100644 --- a/lib/l10n/intl_fil.arb +++ b/lib/l10n/intl_fil.arb @@ -2787,7 +2787,7 @@ "selectAll": "Piliin lahat", "deselectAll": "Huwag piliin lahat", "@@locale": "fil", - "@@last_modified": "2026-01-16 14:33:18.880694", + "@@last_modified": "2026-01-20 12:31:19.884620", "@setCustomPermissionLevel": { "type": "String", "placeholders": {} @@ -11841,5 +11841,30 @@ "@constructUseIncGCDesc": { "type": "String", "placeholders": {} + }, + "constructUseCorGEDesc": "Pagsasanay sa tamang pagkakamali sa gramatika", + "constructUseIncGEDesc": "Pagsasanay sa maling pagkakamali sa gramatika", + "fillInBlank": "Punan ang blangko ng tamang pagpipilian", + "learn": "Matuto", + "languageUpdated": "Na-update ang target na wika!", + "@constructUseCorGEDesc": { + "type": "String", + "placeholders": {} + }, + "@constructUseIncGEDesc": { + "type": "String", + "placeholders": {} + }, + "@fillInBlank": { + "type": "String", + "placeholders": {} + }, + "@learn": { + "type": "String", + "placeholders": {} + }, + "@languageUpdated": { + "type": "String", + "placeholders": {} } } \ No newline at end of file diff --git a/lib/l10n/intl_fr.arb b/lib/l10n/intl_fr.arb index 0820d0126..c279297f0 100644 --- a/lib/l10n/intl_fr.arb +++ b/lib/l10n/intl_fr.arb @@ -1,6 +1,6 @@ { "@@locale": "fr", - "@@last_modified": "2026-01-16 14:34:20.722657", + "@@last_modified": "2026-01-20 12:32:20.398562", "about": "À propos", "@about": { "type": "String", @@ -11189,5 +11189,30 @@ "@constructUseIncGCDesc": { "type": "String", "placeholders": {} + }, + "constructUseCorGEDesc": "Pratique de correction des erreurs grammaticales", + "constructUseIncGEDesc": "Pratique des erreurs grammaticales incorrectes", + "fillInBlank": "Remplissez le blanc avec le choix correct", + "learn": "Apprendre", + "languageUpdated": "Langue cible mise à jour !", + "@constructUseCorGEDesc": { + "type": "String", + "placeholders": {} + }, + "@constructUseIncGEDesc": { + "type": "String", + "placeholders": {} + }, + "@fillInBlank": { + "type": "String", + "placeholders": {} + }, + "@learn": { + "type": "String", + "placeholders": {} + }, + "@languageUpdated": { + "type": "String", + "placeholders": {} } } \ No newline at end of file diff --git a/lib/l10n/intl_ga.arb b/lib/l10n/intl_ga.arb index 3b91b85c4..c923a3bbd 100644 --- a/lib/l10n/intl_ga.arb +++ b/lib/l10n/intl_ga.arb @@ -4517,7 +4517,7 @@ "playWithAI": "Imir le AI faoi láthair", "courseStartDesc": "Tá Bot Pangea réidh chun dul am ar bith!\n\n...ach is fearr foghlaim le cairde!", "@@locale": "ga", - "@@last_modified": "2026-01-16 14:34:18.473304", + "@@last_modified": "2026-01-20 12:32:18.033824", "@customReaction": { "type": "String", "placeholders": {} @@ -10863,5 +10863,30 @@ "@constructUseIncGCDesc": { "type": "String", "placeholders": {} + }, + "constructUseCorGEDesc": "Cleachtadh ar earráidí gramadaí ceart", + "constructUseIncGEDesc": "Cleachtadh ar earráidí gramadaí míchruinn", + "fillInBlank": "Líon isteach an folt le rogha cheart", + "learn": "Foghlaim", + "languageUpdated": "Teanga sprioc nuashonraithe!", + "@constructUseCorGEDesc": { + "type": "String", + "placeholders": {} + }, + "@constructUseIncGEDesc": { + "type": "String", + "placeholders": {} + }, + "@fillInBlank": { + "type": "String", + "placeholders": {} + }, + "@learn": { + "type": "String", + "placeholders": {} + }, + "@languageUpdated": { + "type": "String", + "placeholders": {} } } \ No newline at end of file diff --git a/lib/l10n/intl_gl.arb b/lib/l10n/intl_gl.arb index e4e4a6407..3ebc2943d 100644 --- a/lib/l10n/intl_gl.arb +++ b/lib/l10n/intl_gl.arb @@ -1,6 +1,6 @@ { "@@locale": "gl", - "@@last_modified": "2026-01-16 14:32:01.213945", + "@@last_modified": "2026-01-20 12:30:05.234280", "about": "Acerca de", "@about": { "type": "String", @@ -10862,5 +10862,30 @@ "@constructUseIncGCDesc": { "type": "String", "placeholders": {} + }, + "constructUseCorGEDesc": "Práctica de erro gramatical correcto", + "constructUseIncGEDesc": "Práctica de erro gramatical incorrecto", + "fillInBlank": "Completa o espazo en branco coa opción correcta", + "learn": "Aprender", + "languageUpdated": "Idioma de destino actualizado!", + "@constructUseCorGEDesc": { + "type": "String", + "placeholders": {} + }, + "@constructUseIncGEDesc": { + "type": "String", + "placeholders": {} + }, + "@fillInBlank": { + "type": "String", + "placeholders": {} + }, + "@learn": { + "type": "String", + "placeholders": {} + }, + "@languageUpdated": { + "type": "String", + "placeholders": {} } } \ No newline at end of file diff --git a/lib/l10n/intl_he.arb b/lib/l10n/intl_he.arb index 12dd52832..7ad96af48 100644 --- a/lib/l10n/intl_he.arb +++ b/lib/l10n/intl_he.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2026-01-16 14:32:26.413376", + "@@last_modified": "2026-01-20 12:30:31.884050", "about": "אודות", "@about": { "type": "String", @@ -11914,5 +11914,30 @@ "@constructUseIncGCDesc": { "type": "String", "placeholders": {} + }, + "constructUseCorGEDesc": "תרגול תיקון שגיאות דקדוק", + "constructUseIncGEDesc": "תרגול שגיאות דקדוק לא נכונות", + "fillInBlank": "מלא את החסר עם הבחירה הנכונה", + "learn": "ללמוד", + "languageUpdated": "שפת היעד עודכנה!", + "@constructUseCorGEDesc": { + "type": "String", + "placeholders": {} + }, + "@constructUseIncGEDesc": { + "type": "String", + "placeholders": {} + }, + "@fillInBlank": { + "type": "String", + "placeholders": {} + }, + "@learn": { + "type": "String", + "placeholders": {} + }, + "@languageUpdated": { + "type": "String", + "placeholders": {} } } \ No newline at end of file diff --git a/lib/l10n/intl_hi.arb b/lib/l10n/intl_hi.arb index a95ad48fc..a465a07b2 100644 --- a/lib/l10n/intl_hi.arb +++ b/lib/l10n/intl_hi.arb @@ -4483,7 +4483,7 @@ "playWithAI": "अभी के लिए एआई के साथ खेलें", "courseStartDesc": "पैंजिया बॉट कभी भी जाने के लिए तैयार है!\n\n...लेकिन दोस्तों के साथ सीखना बेहतर है!", "@@locale": "hi", - "@@last_modified": "2026-01-16 14:34:05.778231", + "@@last_modified": "2026-01-20 12:32:05.240015", "@alwaysUse24HourFormat": { "type": "String", "placeholders": {} @@ -11950,5 +11950,30 @@ "@constructUseIncGCDesc": { "type": "String", "placeholders": {} + }, + "constructUseCorGEDesc": "व्याकरण त्रुटि सुधार अभ्यास", + "constructUseIncGEDesc": "व्याकरण त्रुटि गलत अभ्यास", + "fillInBlank": "सही विकल्प के साथ रिक्त स्थान भरें", + "learn": "सीखें", + "languageUpdated": "लक्षित भाषा अपडेट की गई!", + "@constructUseCorGEDesc": { + "type": "String", + "placeholders": {} + }, + "@constructUseIncGEDesc": { + "type": "String", + "placeholders": {} + }, + "@fillInBlank": { + "type": "String", + "placeholders": {} + }, + "@learn": { + "type": "String", + "placeholders": {} + }, + "@languageUpdated": { + "type": "String", + "placeholders": {} } } \ No newline at end of file diff --git a/lib/l10n/intl_hr.arb b/lib/l10n/intl_hr.arb index e05faf2de..64f6cccf8 100644 --- a/lib/l10n/intl_hr.arb +++ b/lib/l10n/intl_hr.arb @@ -1,6 +1,6 @@ { "@@locale": "hr", - "@@last_modified": "2026-01-16 14:32:23.849622", + "@@last_modified": "2026-01-20 12:30:29.823787", "about": "Informacije", "@about": { "type": "String", @@ -11237,5 +11237,30 @@ "@constructUseIncGCDesc": { "type": "String", "placeholders": {} + }, + "constructUseCorGEDesc": "Prakticiranje ispravne gramatičke greške", + "constructUseIncGEDesc": "Prakticiranje pogrešne gramatičke greške", + "fillInBlank": "Ispunite prazno mjesto s ispravnim izborom", + "learn": "Učite", + "languageUpdated": "Ciljani jezik ažuriran!", + "@constructUseCorGEDesc": { + "type": "String", + "placeholders": {} + }, + "@constructUseIncGEDesc": { + "type": "String", + "placeholders": {} + }, + "@fillInBlank": { + "type": "String", + "placeholders": {} + }, + "@learn": { + "type": "String", + "placeholders": {} + }, + "@languageUpdated": { + "type": "String", + "placeholders": {} } } \ No newline at end of file diff --git a/lib/l10n/intl_hu.arb b/lib/l10n/intl_hu.arb index 857440581..8d543fa7e 100644 --- a/lib/l10n/intl_hu.arb +++ b/lib/l10n/intl_hu.arb @@ -1,6 +1,6 @@ { "@@locale": "hu", - "@@last_modified": "2026-01-16 14:32:10.796625", + "@@last_modified": "2026-01-20 12:30:13.762355", "about": "Névjegy", "@about": { "type": "String", @@ -10866,5 +10866,30 @@ "@constructUseIncGCDesc": { "type": "String", "placeholders": {} + }, + "constructUseCorGEDesc": "Helyes nyelvtani hiba gyakorlás", + "constructUseIncGEDesc": "Helytelen nyelvtani hiba gyakorlás", + "fillInBlank": "Töltsd ki a hiányzó részt a helyes választással", + "learn": "Tanulj", + "languageUpdated": "Cél nyelv frissítve!", + "@constructUseCorGEDesc": { + "type": "String", + "placeholders": {} + }, + "@constructUseIncGEDesc": { + "type": "String", + "placeholders": {} + }, + "@fillInBlank": { + "type": "String", + "placeholders": {} + }, + "@learn": { + "type": "String", + "placeholders": {} + }, + "@languageUpdated": { + "type": "String", + "placeholders": {} } } \ No newline at end of file diff --git a/lib/l10n/intl_ia.arb b/lib/l10n/intl_ia.arb index 88b1df62c..4fda86d9e 100644 --- a/lib/l10n/intl_ia.arb +++ b/lib/l10n/intl_ia.arb @@ -1958,7 +1958,7 @@ "playWithAI": "Joca con le IA pro ora", "courseStartDesc": "Pangea Bot es preste a comenzar a qualunque momento!\n\n...ma apprender es melior con amicos!", "@@locale": "ia", - "@@last_modified": "2026-01-16 14:32:31.137719", + "@@last_modified": "2026-01-20 12:30:35.012898", "@alwaysUse24HourFormat": { "type": "String", "placeholders": {} @@ -11943,5 +11943,30 @@ "@constructUseIncGCDesc": { "type": "String", "placeholders": {} + }, + "constructUseCorGEDesc": "Praktiko de ĝusta gramatika eraro", + "constructUseIncGEDesc": "Praktiko de malĝusta gramatika eraro", + "fillInBlank": "Plenigu la malplenan lokon kun la ĝusta elekto", + "learn": "Lerni", + "languageUpdated": "Celo lingvo ĝisdatigita!", + "@constructUseCorGEDesc": { + "type": "String", + "placeholders": {} + }, + "@constructUseIncGEDesc": { + "type": "String", + "placeholders": {} + }, + "@fillInBlank": { + "type": "String", + "placeholders": {} + }, + "@learn": { + "type": "String", + "placeholders": {} + }, + "@languageUpdated": { + "type": "String", + "placeholders": {} } } \ No newline at end of file diff --git a/lib/l10n/intl_id.arb b/lib/l10n/intl_id.arb index b1c17332d..50917db2e 100644 --- a/lib/l10n/intl_id.arb +++ b/lib/l10n/intl_id.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2026-01-16 14:32:13.610901", + "@@last_modified": "2026-01-20 12:30:15.788809", "setAsCanonicalAlias": "Atur sebagai alias utama", "@setAsCanonicalAlias": { "type": "String", @@ -10856,5 +10856,30 @@ "@constructUseIncGCDesc": { "type": "String", "placeholders": {} + }, + "constructUseCorGEDesc": "Latihan kesalahan tata bahasa yang benar", + "constructUseIncGEDesc": "Latihan kesalahan tata bahasa yang salah", + "fillInBlank": "Isi kekosongan dengan pilihan yang benar", + "learn": "Belajar", + "languageUpdated": "Bahasa target diperbarui!", + "@constructUseCorGEDesc": { + "type": "String", + "placeholders": {} + }, + "@constructUseIncGEDesc": { + "type": "String", + "placeholders": {} + }, + "@fillInBlank": { + "type": "String", + "placeholders": {} + }, + "@learn": { + "type": "String", + "placeholders": {} + }, + "@languageUpdated": { + "type": "String", + "placeholders": {} } } \ No newline at end of file diff --git a/lib/l10n/intl_ie.arb b/lib/l10n/intl_ie.arb index e53212b6d..17b5245cf 100644 --- a/lib/l10n/intl_ie.arb +++ b/lib/l10n/intl_ie.arb @@ -4372,7 +4372,7 @@ "playWithAI": "Joca con AI pro ora", "courseStartDesc": "Pangea Bot es preste a partir a qualunque momento!\n\n...ma apprender es melior con amicos!", "@@locale": "ie", - "@@last_modified": "2026-01-16 14:32:21.289462", + "@@last_modified": "2026-01-20 12:30:26.700297", "@alwaysUse24HourFormat": { "type": "String", "placeholders": {} @@ -11839,5 +11839,30 @@ "@constructUseIncGCDesc": { "type": "String", "placeholders": {} + }, + "constructUseCorGEDesc": "Praktika korrekta gramatikfehler", + "constructUseIncGEDesc": "Praktika inkorrekt gramatikfehler", + "fillInBlank": "Fyll i tomrummet med det korrekta valget", + "learn": "Lær", + "languageUpdated": "Mål sprog opdateret!", + "@constructUseCorGEDesc": { + "type": "String", + "placeholders": {} + }, + "@constructUseIncGEDesc": { + "type": "String", + "placeholders": {} + }, + "@fillInBlank": { + "type": "String", + "placeholders": {} + }, + "@learn": { + "type": "String", + "placeholders": {} + }, + "@languageUpdated": { + "type": "String", + "placeholders": {} } } \ No newline at end of file diff --git a/lib/l10n/intl_it.arb b/lib/l10n/intl_it.arb index 9987104e8..5c8d17791 100644 --- a/lib/l10n/intl_it.arb +++ b/lib/l10n/intl_it.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2026-01-16 14:32:53.055805", + "@@last_modified": "2026-01-20 12:30:55.791406", "about": "Informazioni", "@about": { "type": "String", @@ -10868,5 +10868,30 @@ "@constructUseIncGCDesc": { "type": "String", "placeholders": {} + }, + "constructUseCorGEDesc": "Pratica degli errori grammaticali corretti", + "constructUseIncGEDesc": "Pratica degli errori grammaticali scorretti", + "fillInBlank": "Compila lo spazio vuoto con la scelta corretta", + "learn": "Impara", + "languageUpdated": "Lingua target aggiornata!", + "@constructUseCorGEDesc": { + "type": "String", + "placeholders": {} + }, + "@constructUseIncGEDesc": { + "type": "String", + "placeholders": {} + }, + "@fillInBlank": { + "type": "String", + "placeholders": {} + }, + "@learn": { + "type": "String", + "placeholders": {} + }, + "@languageUpdated": { + "type": "String", + "placeholders": {} } } \ No newline at end of file diff --git a/lib/l10n/intl_ja.arb b/lib/l10n/intl_ja.arb index fda648203..30d520729 100644 --- a/lib/l10n/intl_ja.arb +++ b/lib/l10n/intl_ja.arb @@ -1,6 +1,6 @@ { "@@locale": "ja", - "@@last_modified": "2026-01-16 14:34:03.101722", + "@@last_modified": "2026-01-20 12:32:02.883097", "about": "このアプリについて", "@about": { "type": "String", @@ -11655,5 +11655,30 @@ "@constructUseIncGCDesc": { "type": "String", "placeholders": {} + }, + "constructUseCorGEDesc": "文法エラーの正しい練習", + "constructUseIncGEDesc": "文法エラーの不正確な練習", + "fillInBlank": "正しい選択肢で空欄を埋めてください", + "learn": "学ぶ", + "languageUpdated": "ターゲット言語が更新されました!", + "@constructUseCorGEDesc": { + "type": "String", + "placeholders": {} + }, + "@constructUseIncGEDesc": { + "type": "String", + "placeholders": {} + }, + "@fillInBlank": { + "type": "String", + "placeholders": {} + }, + "@learn": { + "type": "String", + "placeholders": {} + }, + "@languageUpdated": { + "type": "String", + "placeholders": {} } } \ No newline at end of file diff --git a/lib/l10n/intl_ka.arb b/lib/l10n/intl_ka.arb index 9b1de6575..8026eff56 100644 --- a/lib/l10n/intl_ka.arb +++ b/lib/l10n/intl_ka.arb @@ -2594,7 +2594,7 @@ "playWithAI": "ამ დროისთვის ითამაშეთ AI-თან", "courseStartDesc": "Pangea Bot მზადაა ნებისმიერ დროს გასასვლელად!\n\n...მაგრამ სწავლა უკეთესია მეგობრებთან ერთად!", "@@locale": "ka", - "@@last_modified": "2026-01-16 14:34:13.710408", + "@@last_modified": "2026-01-20 12:32:12.611848", "@alwaysUse24HourFormat": { "type": "String", "placeholders": {} @@ -11895,5 +11895,30 @@ "@constructUseIncGCDesc": { "type": "String", "placeholders": {} + }, + "constructUseCorGEDesc": "მართებული გრამატიკული შეცდომების პრაქტიკა", + "constructUseIncGEDesc": "არასწორი გრამატიკული შეცდომების პრაქტიკა", + "fillInBlank": "შეავსეთ ცარიელი ადგილი სწორი არჩევანით", + "learn": "სწავლა", + "languageUpdated": "მიზნობრივი ენა განახლებულია!", + "@constructUseCorGEDesc": { + "type": "String", + "placeholders": {} + }, + "@constructUseIncGEDesc": { + "type": "String", + "placeholders": {} + }, + "@fillInBlank": { + "type": "String", + "placeholders": {} + }, + "@learn": { + "type": "String", + "placeholders": {} + }, + "@languageUpdated": { + "type": "String", + "placeholders": {} } } \ No newline at end of file diff --git a/lib/l10n/intl_ko.arb b/lib/l10n/intl_ko.arb index f2b772efe..f711d1304 100644 --- a/lib/l10n/intl_ko.arb +++ b/lib/l10n/intl_ko.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2026-01-16 14:31:56.042093", + "@@last_modified": "2026-01-20 12:29:56.840054", "about": "소개", "@about": { "type": "String", @@ -10973,5 +10973,30 @@ "@constructUseIncGCDesc": { "type": "String", "placeholders": {} + }, + "constructUseCorGEDesc": "문법 오류 수정 연습", + "constructUseIncGEDesc": "문법 오류 비정상 연습", + "fillInBlank": "올바른 선택으로 빈칸을 채우세요", + "learn": "배우다", + "languageUpdated": "목표 언어가 업데이트되었습니다!", + "@constructUseCorGEDesc": { + "type": "String", + "placeholders": {} + }, + "@constructUseIncGEDesc": { + "type": "String", + "placeholders": {} + }, + "@fillInBlank": { + "type": "String", + "placeholders": {} + }, + "@learn": { + "type": "String", + "placeholders": {} + }, + "@languageUpdated": { + "type": "String", + "placeholders": {} } } \ No newline at end of file diff --git a/lib/l10n/intl_lt.arb b/lib/l10n/intl_lt.arb index c46982084..888944382 100644 --- a/lib/l10n/intl_lt.arb +++ b/lib/l10n/intl_lt.arb @@ -3861,7 +3861,7 @@ "playWithAI": "Žaiskite su dirbtiniu intelektu dabar", "courseStartDesc": "Pangea botas pasiruošęs bet kada pradėti!\n\n...bet mokymasis yra geresnis su draugais!", "@@locale": "lt", - "@@last_modified": "2026-01-16 14:33:32.087889", + "@@last_modified": "2026-01-20 12:31:36.164653", "@alwaysUse24HourFormat": { "type": "String", "placeholders": {} @@ -11670,5 +11670,30 @@ "@constructUseIncGCDesc": { "type": "String", "placeholders": {} + }, + "constructUseCorGEDesc": "Teisingos gramatikos klaidų praktika", + "constructUseIncGEDesc": "Neteisingos gramatikos klaidų praktika", + "fillInBlank": "Užpildykite tuščią vietą teisingu pasirinkimu", + "learn": "Mokytis", + "languageUpdated": "Tikslo kalba atnaujinta!", + "@constructUseCorGEDesc": { + "type": "String", + "placeholders": {} + }, + "@constructUseIncGEDesc": { + "type": "String", + "placeholders": {} + }, + "@fillInBlank": { + "type": "String", + "placeholders": {} + }, + "@learn": { + "type": "String", + "placeholders": {} + }, + "@languageUpdated": { + "type": "String", + "placeholders": {} } } \ No newline at end of file diff --git a/lib/l10n/intl_lv.arb b/lib/l10n/intl_lv.arb index 66ea87a12..79a2fd834 100644 --- a/lib/l10n/intl_lv.arb +++ b/lib/l10n/intl_lv.arb @@ -4482,7 +4482,7 @@ "playWithAI": "Tagad spēlējiet ar AI", "courseStartDesc": "Pangea bots ir gatavs jebkurā laikā!\n\n...bet mācīties ir labāk ar draugiem!", "@@locale": "lv", - "@@last_modified": "2026-01-16 14:33:21.744947", + "@@last_modified": "2026-01-20 12:31:22.453166", "analyticsInactiveTitle": "Pieprasījumi neaktīviem lietotājiem nevar tikt nosūtīti", "analyticsInactiveDesc": "Neaktīvi lietotāji, kuri nav pieteikušies kopš šīs funkcijas ieviešanas, neredzēs jūsu pieprasījumu.\n\nPieprasījuma poga parādīsies, kad viņi atgriezīsies. Jūs varat atkārtoti nosūtīt pieprasījumu vēlāk, noklikšķinot uz pieprasījuma pogas viņu vārdā, kad tā būs pieejama.", "accessRequestedTitle": "Pieprasījums piekļūt analītikai", @@ -10851,5 +10851,30 @@ "@constructUseIncGCDesc": { "type": "String", "placeholders": {} + }, + "constructUseCorGEDesc": "Pareiza gramatikas kļūdu prakse", + "constructUseIncGEDesc": "Nepareiza gramatikas kļūdu prakse", + "fillInBlank": "Aizpildiet tukšo vietu ar pareizo izvēli", + "learn": "Mācīties", + "languageUpdated": "Mērķa valoda atjaunota!", + "@constructUseCorGEDesc": { + "type": "String", + "placeholders": {} + }, + "@constructUseIncGEDesc": { + "type": "String", + "placeholders": {} + }, + "@fillInBlank": { + "type": "String", + "placeholders": {} + }, + "@learn": { + "type": "String", + "placeholders": {} + }, + "@languageUpdated": { + "type": "String", + "placeholders": {} } } \ No newline at end of file diff --git a/lib/l10n/intl_nb.arb b/lib/l10n/intl_nb.arb index 6e7c933f3..8d8592bdb 100644 --- a/lib/l10n/intl_nb.arb +++ b/lib/l10n/intl_nb.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2026-01-16 14:33:02.881359", + "@@last_modified": "2026-01-20 12:31:02.821968", "about": "Om", "@about": { "type": "String", @@ -11958,5 +11958,30 @@ "@constructUseIncGCDesc": { "type": "String", "placeholders": {} + }, + "constructUseCorGEDesc": "Korrekt grammatikkfeil praksis", + "constructUseIncGEDesc": "Feil grammatikkfeil praksis", + "fillInBlank": "Fyll inn blanketten med riktig valg", + "learn": "Lær", + "languageUpdated": "Mål språk oppdatert!", + "@constructUseCorGEDesc": { + "type": "String", + "placeholders": {} + }, + "@constructUseIncGEDesc": { + "type": "String", + "placeholders": {} + }, + "@fillInBlank": { + "type": "String", + "placeholders": {} + }, + "@learn": { + "type": "String", + "placeholders": {} + }, + "@languageUpdated": { + "type": "String", + "placeholders": {} } } \ No newline at end of file diff --git a/lib/l10n/intl_nl.arb b/lib/l10n/intl_nl.arb index 818b6bc60..755841dc9 100644 --- a/lib/l10n/intl_nl.arb +++ b/lib/l10n/intl_nl.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2026-01-16 14:33:40.784777", + "@@last_modified": "2026-01-20 12:31:42.507524", "about": "Over ons", "@about": { "type": "String", @@ -10865,5 +10865,30 @@ "@constructUseIncGCDesc": { "type": "String", "placeholders": {} + }, + "constructUseCorGEDesc": "Oefening voor correcte grammatica", + "constructUseIncGEDesc": "Oefening voor onjuiste grammatica", + "fillInBlank": "Vul de lege ruimte in met de juiste keuze", + "learn": "Leren", + "languageUpdated": "Doeltaal bijgewerkt!", + "@constructUseCorGEDesc": { + "type": "String", + "placeholders": {} + }, + "@constructUseIncGEDesc": { + "type": "String", + "placeholders": {} + }, + "@fillInBlank": { + "type": "String", + "placeholders": {} + }, + "@learn": { + "type": "String", + "placeholders": {} + }, + "@languageUpdated": { + "type": "String", + "placeholders": {} } } \ No newline at end of file diff --git a/lib/l10n/intl_pl.arb b/lib/l10n/intl_pl.arb index a8f3c5d85..48ebf5396 100644 --- a/lib/l10n/intl_pl.arb +++ b/lib/l10n/intl_pl.arb @@ -1,6 +1,6 @@ { "@@locale": "pl", - "@@last_modified": "2026-01-16 14:33:54.131854", + "@@last_modified": "2026-01-20 12:31:54.796841", "about": "O aplikacji", "@about": { "type": "String", @@ -10863,5 +10863,30 @@ "@constructUseIncGCDesc": { "type": "String", "placeholders": {} + }, + "constructUseCorGEDesc": "Ćwiczenie poprawnych błędów gramatycznych", + "constructUseIncGEDesc": "Ćwiczenie niepoprawnych błędów gramatycznych", + "fillInBlank": "Uzupełnij lukę poprawnym wyborem", + "learn": "Ucz się", + "languageUpdated": "Język docelowy zaktualizowany!", + "@constructUseCorGEDesc": { + "type": "String", + "placeholders": {} + }, + "@constructUseIncGEDesc": { + "type": "String", + "placeholders": {} + }, + "@fillInBlank": { + "type": "String", + "placeholders": {} + }, + "@learn": { + "type": "String", + "placeholders": {} + }, + "@languageUpdated": { + "type": "String", + "placeholders": {} } } \ No newline at end of file diff --git a/lib/l10n/intl_pt.arb b/lib/l10n/intl_pt.arb index 186e08e77..1ac41d131 100644 --- a/lib/l10n/intl_pt.arb +++ b/lib/l10n/intl_pt.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2026-01-16 14:32:38.304072", + "@@last_modified": "2026-01-20 12:30:42.601068", "copiedToClipboard": "Copiada para a área de transferência", "@copiedToClipboard": { "type": "String", @@ -11965,5 +11965,30 @@ "@constructUseIncGCDesc": { "type": "String", "placeholders": {} + }, + "constructUseCorGEDesc": "Prática de erro gramatical correto", + "constructUseIncGEDesc": "Prática de erro gramatical incorreto", + "fillInBlank": "Preencha a lacuna com a escolha correta", + "learn": "Aprender", + "languageUpdated": "Idioma de destino atualizado!", + "@constructUseCorGEDesc": { + "type": "String", + "placeholders": {} + }, + "@constructUseIncGEDesc": { + "type": "String", + "placeholders": {} + }, + "@fillInBlank": { + "type": "String", + "placeholders": {} + }, + "@learn": { + "type": "String", + "placeholders": {} + }, + "@languageUpdated": { + "type": "String", + "placeholders": {} } } \ No newline at end of file diff --git a/lib/l10n/intl_pt_BR.arb b/lib/l10n/intl_pt_BR.arb index a351a7185..da1140756 100644 --- a/lib/l10n/intl_pt_BR.arb +++ b/lib/l10n/intl_pt_BR.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2026-01-16 14:32:33.894131", + "@@last_modified": "2026-01-20 12:30:37.380939", "about": "Sobre", "@about": { "type": "String", @@ -11223,5 +11223,30 @@ "@constructUseIncGCDesc": { "type": "String", "placeholders": {} + }, + "constructUseCorGEDesc": "Prática de erro gramatical correto", + "constructUseIncGEDesc": "Prática de erro gramatical incorreto", + "fillInBlank": "Preencha a lacuna com a escolha correta", + "learn": "Aprender", + "languageUpdated": "Idioma de destino atualizado!", + "@constructUseCorGEDesc": { + "type": "String", + "placeholders": {} + }, + "@constructUseIncGEDesc": { + "type": "String", + "placeholders": {} + }, + "@fillInBlank": { + "type": "String", + "placeholders": {} + }, + "@learn": { + "type": "String", + "placeholders": {} + }, + "@languageUpdated": { + "type": "String", + "placeholders": {} } } \ No newline at end of file diff --git a/lib/l10n/intl_pt_PT.arb b/lib/l10n/intl_pt_PT.arb index 6439f1fd4..c0a85355c 100644 --- a/lib/l10n/intl_pt_PT.arb +++ b/lib/l10n/intl_pt_PT.arb @@ -3331,7 +3331,7 @@ "selectAll": "Selecionar tudo", "deselectAll": "Desmarcar tudo", "@@locale": "pt_PT", - "@@last_modified": "2026-01-16 14:33:14.369639", + "@@last_modified": "2026-01-20 12:31:13.609297", "@alwaysUse24HourFormat": { "type": "String", "placeholders": {} @@ -11894,5 +11894,30 @@ "@constructUseIncGCDesc": { "type": "String", "placeholders": {} + }, + "constructUseCorGEDesc": "Prática de erro gramatical correto", + "constructUseIncGEDesc": "Prática de erro gramatical incorreto", + "fillInBlank": "Preencha a lacuna com a escolha correta", + "learn": "Aprender", + "languageUpdated": "Idioma de destino atualizado!", + "@constructUseCorGEDesc": { + "type": "String", + "placeholders": {} + }, + "@constructUseIncGEDesc": { + "type": "String", + "placeholders": {} + }, + "@fillInBlank": { + "type": "String", + "placeholders": {} + }, + "@learn": { + "type": "String", + "placeholders": {} + }, + "@languageUpdated": { + "type": "String", + "placeholders": {} } } \ No newline at end of file diff --git a/lib/l10n/intl_ro.arb b/lib/l10n/intl_ro.arb index c39285495..5154c946d 100644 --- a/lib/l10n/intl_ro.arb +++ b/lib/l10n/intl_ro.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2026-01-16 14:32:16.042822", + "@@last_modified": "2026-01-20 12:30:21.408747", "about": "Despre", "@about": { "type": "String", @@ -11600,5 +11600,30 @@ "@constructUseIncGCDesc": { "type": "String", "placeholders": {} + }, + "constructUseCorGEDesc": "Practică corectă a erorilor de gramatică", + "constructUseIncGEDesc": "Practică incorectă a erorilor de gramatică", + "fillInBlank": "Completați spațiul gol cu alegerea corectă", + "learn": "Învățați", + "languageUpdated": "Limba țintă a fost actualizată!", + "@constructUseCorGEDesc": { + "type": "String", + "placeholders": {} + }, + "@constructUseIncGEDesc": { + "type": "String", + "placeholders": {} + }, + "@fillInBlank": { + "type": "String", + "placeholders": {} + }, + "@learn": { + "type": "String", + "placeholders": {} + }, + "@languageUpdated": { + "type": "String", + "placeholders": {} } } \ No newline at end of file diff --git a/lib/l10n/intl_ru.arb b/lib/l10n/intl_ru.arb index 42793afeb..ca196e82b 100644 --- a/lib/l10n/intl_ru.arb +++ b/lib/l10n/intl_ru.arb @@ -1,6 +1,6 @@ { "@@locale": "ru", - "@@last_modified": "2026-01-16 14:34:10.739852", + "@@last_modified": "2026-01-20 12:32:09.850624", "about": "О проекте", "@about": { "type": "String", @@ -10970,5 +10970,30 @@ "@constructUseIncGCDesc": { "type": "String", "placeholders": {} + }, + "constructUseCorGEDesc": "Практика исправления грамматических ошибок", + "constructUseIncGEDesc": "Практика неправильных грамматических ошибок", + "fillInBlank": "Заполните пропуск правильным вариантом", + "learn": "Учить", + "languageUpdated": "Целевой язык обновлен!", + "@constructUseCorGEDesc": { + "type": "String", + "placeholders": {} + }, + "@constructUseIncGEDesc": { + "type": "String", + "placeholders": {} + }, + "@fillInBlank": { + "type": "String", + "placeholders": {} + }, + "@learn": { + "type": "String", + "placeholders": {} + }, + "@languageUpdated": { + "type": "String", + "placeholders": {} } } \ No newline at end of file diff --git a/lib/l10n/intl_sk.arb b/lib/l10n/intl_sk.arb index fa42e9927..ceb204959 100644 --- a/lib/l10n/intl_sk.arb +++ b/lib/l10n/intl_sk.arb @@ -1,6 +1,6 @@ { "@@locale": "sk", - "@@last_modified": "2026-01-16 14:32:18.425596", + "@@last_modified": "2026-01-20 12:30:24.498564", "about": "O aplikácii", "@about": { "type": "String", @@ -11949,5 +11949,30 @@ "@constructUseIncGCDesc": { "type": "String", "placeholders": {} + }, + "constructUseCorGEDesc": "Cvičenie na opravu gramatických chýb", + "constructUseIncGEDesc": "Cvičenie na nesprávne gramatické chyby", + "fillInBlank": "Doplňte prázdne miesto správnou voľbou", + "learn": "Učte sa", + "languageUpdated": "Cieľový jazyk bol aktualizovaný!", + "@constructUseCorGEDesc": { + "type": "String", + "placeholders": {} + }, + "@constructUseIncGEDesc": { + "type": "String", + "placeholders": {} + }, + "@fillInBlank": { + "type": "String", + "placeholders": {} + }, + "@learn": { + "type": "String", + "placeholders": {} + }, + "@languageUpdated": { + "type": "String", + "placeholders": {} } } \ No newline at end of file diff --git a/lib/l10n/intl_sl.arb b/lib/l10n/intl_sl.arb index 782e7cbc3..9e4a1ff56 100644 --- a/lib/l10n/intl_sl.arb +++ b/lib/l10n/intl_sl.arb @@ -2464,7 +2464,7 @@ "playWithAI": "Za zdaj igrajte z AI-jem", "courseStartDesc": "Pangea Bot je pripravljen kadarkoli!\n\n...ampak je bolje učiti se s prijatelji!", "@@locale": "sl", - "@@last_modified": "2026-01-16 14:32:46.504404", + "@@last_modified": "2026-01-20 12:30:51.399294", "@alwaysUse24HourFormat": { "type": "String", "placeholders": {} @@ -11946,5 +11946,30 @@ "@constructUseIncGCDesc": { "type": "String", "placeholders": {} + }, + "constructUseCorGEDesc": "Praksa pravilne rabe slovnice", + "constructUseIncGEDesc": "Praksa nepravilne rabe slovnice", + "fillInBlank": "Izpolnite prazno mesto s pravilno izbiro", + "learn": "Učite se", + "languageUpdated": "Ciljni jezik je posodobljen!", + "@constructUseCorGEDesc": { + "type": "String", + "placeholders": {} + }, + "@constructUseIncGEDesc": { + "type": "String", + "placeholders": {} + }, + "@fillInBlank": { + "type": "String", + "placeholders": {} + }, + "@learn": { + "type": "String", + "placeholders": {} + }, + "@languageUpdated": { + "type": "String", + "placeholders": {} } } \ No newline at end of file diff --git a/lib/l10n/intl_sr.arb b/lib/l10n/intl_sr.arb index 481093692..fe98f2112 100644 --- a/lib/l10n/intl_sr.arb +++ b/lib/l10n/intl_sr.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2026-01-16 14:34:16.018548", + "@@last_modified": "2026-01-20 12:32:14.799697", "about": "О програму", "@about": { "type": "String", @@ -11967,5 +11967,30 @@ "@constructUseIncGCDesc": { "type": "String", "placeholders": {} + }, + "constructUseCorGEDesc": "Vežba ispravne gramatike", + "constructUseIncGEDesc": "Vežba nepravilne gramatike", + "fillInBlank": "Popunite prazno mesto sa ispravnim izborom", + "learn": "Učite", + "languageUpdated": "Ciljni jezik je ažuriran!", + "@constructUseCorGEDesc": { + "type": "String", + "placeholders": {} + }, + "@constructUseIncGEDesc": { + "type": "String", + "placeholders": {} + }, + "@fillInBlank": { + "type": "String", + "placeholders": {} + }, + "@learn": { + "type": "String", + "placeholders": {} + }, + "@languageUpdated": { + "type": "String", + "placeholders": {} } } \ No newline at end of file diff --git a/lib/l10n/intl_sv.arb b/lib/l10n/intl_sv.arb index 117b86faa..1d9224f70 100644 --- a/lib/l10n/intl_sv.arb +++ b/lib/l10n/intl_sv.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2026-01-16 14:33:56.060391", + "@@last_modified": "2026-01-20 12:31:57.066428", "about": "Om", "@about": { "type": "String", @@ -11343,5 +11343,30 @@ "@constructUseIncGCDesc": { "type": "String", "placeholders": {} + }, + "constructUseCorGEDesc": "Korrekt grammatikfel övning", + "constructUseIncGEDesc": "Inkorrekt grammatikfel övning", + "fillInBlank": "Fyll i det tomma med rätt val", + "learn": "Lär dig", + "languageUpdated": "Målspråk uppdaterat!", + "@constructUseCorGEDesc": { + "type": "String", + "placeholders": {} + }, + "@constructUseIncGEDesc": { + "type": "String", + "placeholders": {} + }, + "@fillInBlank": { + "type": "String", + "placeholders": {} + }, + "@learn": { + "type": "String", + "placeholders": {} + }, + "@languageUpdated": { + "type": "String", + "placeholders": {} } } \ No newline at end of file diff --git a/lib/l10n/intl_ta.arb b/lib/l10n/intl_ta.arb index 029b620fe..42b73b4d5 100644 --- a/lib/l10n/intl_ta.arb +++ b/lib/l10n/intl_ta.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2026-01-16 14:33:37.303345", + "@@last_modified": "2026-01-20 12:31:40.562260", "acceptedTheInvitation": "👍 {username} அழைப்பை ஏற்றுக்கொண்டது", "@acceptedTheInvitation": { "type": "String", @@ -11089,5 +11089,30 @@ "@constructUseIncGCDesc": { "type": "String", "placeholders": {} + }, + "constructUseCorGEDesc": "சரியான இலக்கண பிழை பயிற்சி", + "constructUseIncGEDesc": "தவறான இலக்கண பிழை பயிற்சி", + "fillInBlank": "சரியான தேர்வுடன் காலியை நிரப்பவும்", + "learn": "கற்றுக்கொள்ளுங்கள்", + "languageUpdated": "இலக்கு மொழி புதுப்பிக்கப்பட்டது!", + "@constructUseCorGEDesc": { + "type": "String", + "placeholders": {} + }, + "@constructUseIncGEDesc": { + "type": "String", + "placeholders": {} + }, + "@fillInBlank": { + "type": "String", + "placeholders": {} + }, + "@learn": { + "type": "String", + "placeholders": {} + }, + "@languageUpdated": { + "type": "String", + "placeholders": {} } } \ No newline at end of file diff --git a/lib/l10n/intl_te.arb b/lib/l10n/intl_te.arb index 89018dac2..3043221dc 100644 --- a/lib/l10n/intl_te.arb +++ b/lib/l10n/intl_te.arb @@ -1920,7 +1920,7 @@ "playWithAI": "ఇప్పుడే AI తో ఆడండి", "courseStartDesc": "పాంజియా బాట్ ఎప్పుడైనా సిద్ధంగా ఉంటుంది!\n\n...కానీ స్నేహితులతో నేర్చుకోవడం మెరుగైనది!", "@@locale": "te", - "@@last_modified": "2026-01-16 14:33:29.527681", + "@@last_modified": "2026-01-20 12:31:32.903548", "@setCustomPermissionLevel": { "type": "String", "placeholders": {} @@ -11954,5 +11954,30 @@ "@constructUseIncGCDesc": { "type": "String", "placeholders": {} + }, + "constructUseCorGEDesc": "సరైన వ్యాకరణ దోషం అభ్యాసం", + "constructUseIncGEDesc": "తప్పు వ్యాకరణ దోషం అభ్యాసం", + "fillInBlank": "సరైన ఎంపికతో ఖాళీని నింపండి", + "learn": "కలవు", + "languageUpdated": "లక్ష్య భాష నవీకరించబడింది!", + "@constructUseCorGEDesc": { + "type": "String", + "placeholders": {} + }, + "@constructUseIncGEDesc": { + "type": "String", + "placeholders": {} + }, + "@fillInBlank": { + "type": "String", + "placeholders": {} + }, + "@learn": { + "type": "String", + "placeholders": {} + }, + "@languageUpdated": { + "type": "String", + "placeholders": {} } } \ No newline at end of file diff --git a/lib/l10n/intl_th.arb b/lib/l10n/intl_th.arb index e6d5c7213..ceb9ff50a 100644 --- a/lib/l10n/intl_th.arb +++ b/lib/l10n/intl_th.arb @@ -4456,7 +4456,7 @@ "playWithAI": "เล่นกับ AI ชั่วคราว", "courseStartDesc": "Pangea Bot พร้อมที่จะเริ่มต้นได้ทุกเมื่อ!\n\n...แต่การเรียนรู้ดีกว่ากับเพื่อน!", "@@locale": "th", - "@@last_modified": "2026-01-16 14:33:11.816510", + "@@last_modified": "2026-01-20 12:31:11.891533", "@alwaysUse24HourFormat": { "type": "String", "placeholders": {} @@ -11923,5 +11923,30 @@ "@constructUseIncGCDesc": { "type": "String", "placeholders": {} + }, + "constructUseCorGEDesc": "การฝึกฝนข้อผิดพลาดทางไวยากรณ์ที่ถูกต้อง", + "constructUseIncGEDesc": "การฝึกฝนข้อผิดพลาดทางไวยากรณ์ที่ไม่ถูกต้อง", + "fillInBlank": "กรอกข้อมูลในช่องว่างด้วยตัวเลือกที่ถูกต้อง", + "learn": "เรียนรู้", + "languageUpdated": "อัปเดตภาษาที่ต้องการแล้ว!", + "@constructUseCorGEDesc": { + "type": "String", + "placeholders": {} + }, + "@constructUseIncGEDesc": { + "type": "String", + "placeholders": {} + }, + "@fillInBlank": { + "type": "String", + "placeholders": {} + }, + "@learn": { + "type": "String", + "placeholders": {} + }, + "@languageUpdated": { + "type": "String", + "placeholders": {} } } \ No newline at end of file diff --git a/lib/l10n/intl_tr.arb b/lib/l10n/intl_tr.arb index b3abdfecc..8d86e681c 100644 --- a/lib/l10n/intl_tr.arb +++ b/lib/l10n/intl_tr.arb @@ -1,6 +1,6 @@ { "@@locale": "tr", - "@@last_modified": "2026-01-16 14:33:26.649412", + "@@last_modified": "2026-01-20 12:31:28.469826", "about": "Hakkında", "@about": { "type": "String", @@ -11087,5 +11087,30 @@ "@constructUseIncGCDesc": { "type": "String", "placeholders": {} + }, + "constructUseCorGEDesc": "Doğru dil bilgisi hatası pratiği", + "constructUseIncGEDesc": "Yanlış dil bilgisi hatası pratiği", + "fillInBlank": "Boşluğu doğru seçimle doldurun", + "learn": "Öğren", + "languageUpdated": "Hedef dil güncellendi!", + "@constructUseCorGEDesc": { + "type": "String", + "placeholders": {} + }, + "@constructUseIncGEDesc": { + "type": "String", + "placeholders": {} + }, + "@fillInBlank": { + "type": "String", + "placeholders": {} + }, + "@learn": { + "type": "String", + "placeholders": {} + }, + "@languageUpdated": { + "type": "String", + "placeholders": {} } } \ No newline at end of file diff --git a/lib/l10n/intl_uk.arb b/lib/l10n/intl_uk.arb index d4c6ba2bc..b8330d92c 100644 --- a/lib/l10n/intl_uk.arb +++ b/lib/l10n/intl_uk.arb @@ -1,6 +1,6 @@ { "@@locale": "uk", - "@@last_modified": "2026-01-16 14:32:56.338443", + "@@last_modified": "2026-01-20 12:30:57.869011", "about": "Про застосунок", "@about": { "type": "String", @@ -10859,5 +10859,30 @@ "@constructUseIncGCDesc": { "type": "String", "placeholders": {} + }, + "constructUseCorGEDesc": "Практика виправлення граматичних помилок", + "constructUseIncGEDesc": "Практика неправильних граматичних помилок", + "fillInBlank": "Заповніть пропуск правильним вибором", + "learn": "Вчити", + "languageUpdated": "Цільова мова оновлена!", + "@constructUseCorGEDesc": { + "type": "String", + "placeholders": {} + }, + "@constructUseIncGEDesc": { + "type": "String", + "placeholders": {} + }, + "@fillInBlank": { + "type": "String", + "placeholders": {} + }, + "@learn": { + "type": "String", + "placeholders": {} + }, + "@languageUpdated": { + "type": "String", + "placeholders": {} } } \ No newline at end of file diff --git a/lib/l10n/intl_vi.arb b/lib/l10n/intl_vi.arb index 22a5c5696..69d334c03 100644 --- a/lib/l10n/intl_vi.arb +++ b/lib/l10n/intl_vi.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2026-01-16 14:33:34.535886", + "@@last_modified": "2026-01-20 12:31:38.101874", "about": "Giới thiệu", "@about": { "type": "String", @@ -6435,5 +6435,30 @@ "@constructUseIncGCDesc": { "type": "String", "placeholders": {} + }, + "constructUseCorGEDesc": "Thực hành lỗi ngữ pháp đúng", + "constructUseIncGEDesc": "Thực hành lỗi ngữ pháp sai", + "fillInBlank": "Điền vào chỗ trống với lựa chọn đúng", + "learn": "Học", + "languageUpdated": "Ngôn ngữ mục tiêu đã được cập nhật!", + "@constructUseCorGEDesc": { + "type": "String", + "placeholders": {} + }, + "@constructUseIncGEDesc": { + "type": "String", + "placeholders": {} + }, + "@fillInBlank": { + "type": "String", + "placeholders": {} + }, + "@learn": { + "type": "String", + "placeholders": {} + }, + "@languageUpdated": { + "type": "String", + "placeholders": {} } } \ No newline at end of file diff --git a/lib/l10n/intl_yue.arb b/lib/l10n/intl_yue.arb index e51a60484..69867a6fc 100644 --- a/lib/l10n/intl_yue.arb +++ b/lib/l10n/intl_yue.arb @@ -1856,7 +1856,7 @@ "selectAll": "全選", "deselectAll": "取消全選", "@@locale": "yue", - "@@last_modified": "2026-01-16 14:32:49.483939", + "@@last_modified": "2026-01-20 12:30:53.854617", "@ignoreUser": { "type": "String", "placeholders": {} @@ -11956,5 +11956,30 @@ "@constructUseIncGCDesc": { "type": "String", "placeholders": {} + }, + "constructUseCorGEDesc": "正確語法錯誤練習", + "constructUseIncGEDesc": "不正確語法錯誤練習", + "fillInBlank": "用正確的選擇填空", + "learn": "學習", + "languageUpdated": "目標語言已更新!", + "@constructUseCorGEDesc": { + "type": "String", + "placeholders": {} + }, + "@constructUseIncGEDesc": { + "type": "String", + "placeholders": {} + }, + "@fillInBlank": { + "type": "String", + "placeholders": {} + }, + "@learn": { + "type": "String", + "placeholders": {} + }, + "@languageUpdated": { + "type": "String", + "placeholders": {} } } \ No newline at end of file diff --git a/lib/l10n/intl_zh.arb b/lib/l10n/intl_zh.arb index 212d37e2b..0953fb23e 100644 --- a/lib/l10n/intl_zh.arb +++ b/lib/l10n/intl_zh.arb @@ -1,6 +1,6 @@ { "@@locale": "zh", - "@@last_modified": "2026-01-16 14:33:45.909821", + "@@last_modified": "2026-01-20 12:31:47.017242", "about": "关于", "@about": { "type": "String", @@ -10856,5 +10856,30 @@ "@constructUseIncGCDesc": { "type": "String", "placeholders": {} + }, + "constructUseCorGEDesc": "正确语法错误练习", + "constructUseIncGEDesc": "不正确语法错误练习", + "fillInBlank": "用正确的选项填空", + "learn": "学习", + "languageUpdated": "目标语言已更新!", + "@constructUseCorGEDesc": { + "type": "String", + "placeholders": {} + }, + "@constructUseIncGEDesc": { + "type": "String", + "placeholders": {} + }, + "@fillInBlank": { + "type": "String", + "placeholders": {} + }, + "@learn": { + "type": "String", + "placeholders": {} + }, + "@languageUpdated": { + "type": "String", + "placeholders": {} } } \ No newline at end of file diff --git a/lib/l10n/intl_zh_Hant.arb b/lib/l10n/intl_zh_Hant.arb index 01b708716..26f055262 100644 --- a/lib/l10n/intl_zh_Hant.arb +++ b/lib/l10n/intl_zh_Hant.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2026-01-16 14:33:16.316521", + "@@last_modified": "2026-01-20 12:31:16.140892", "about": "關於", "@about": { "type": "String", @@ -10863,5 +10863,30 @@ "@constructUseIncGCDesc": { "type": "String", "placeholders": {} + }, + "constructUseCorGEDesc": "正確語法錯誤練習", + "constructUseIncGEDesc": "不正確語法錯誤練習", + "fillInBlank": "用正確的選擇填空", + "learn": "學習", + "languageUpdated": "目標語言已更新!", + "@constructUseCorGEDesc": { + "type": "String", + "placeholders": {} + }, + "@constructUseIncGEDesc": { + "type": "String", + "placeholders": {} + }, + "@fillInBlank": { + "type": "String", + "placeholders": {} + }, + "@learn": { + "type": "String", + "placeholders": {} + }, + "@languageUpdated": { + "type": "String", + "placeholders": {} } } \ No newline at end of file diff --git a/lib/pages/chat/chat.dart b/lib/pages/chat/chat.dart index 18a21f926..1dce476c6 100644 --- a/lib/pages/chat/chat.dart +++ b/lib/pages/chat/chat.dart @@ -32,7 +32,6 @@ import 'package:fluffychat/pangea/activity_sessions/activity_session_chat/activi import 'package:fluffychat/pangea/activity_sessions/activity_session_chat/activity_chat_extension.dart'; import 'package:fluffychat/pangea/analytics_data/analytics_update_dispatcher.dart'; import 'package:fluffychat/pangea/analytics_data/analytics_updater_mixin.dart'; -import 'package:fluffychat/pangea/analytics_misc/client_analytics_extension.dart'; import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart'; import 'package:fluffychat/pangea/analytics_misc/constructs_model.dart'; import 'package:fluffychat/pangea/analytics_misc/level_up/level_up_banner.dart'; @@ -2046,31 +2045,6 @@ class ChatController extends State return; } - final langCode = - pangeaMessageEvent?.originalSent?.langCode.split('-').first; - - if (LanguageMismatchRepo.shouldShowByEvent(event.eventId) && - langCode != null && - pangeaMessageEvent?.originalSent?.content.langCodeMatchesL2 == false && - room.client.allMyAnalyticsRooms.any((r) => r.madeForLang == langCode)) { - LanguageMismatchRepo.setEvent(event.eventId); - OverlayUtil.showLanguageMismatchPopup( - context: context, - targetId: event.eventId, - message: L10n.of(context).messageLanguageMismatchMessage, - targetLanguage: pangeaMessageEvent!.originalSent!.langCode, - onConfirm: () => showToolbar( - event, - pangeaMessageEvent: pangeaMessageEvent, - selectedToken: selectedToken, - mode: mode, - nextEvent: nextEvent, - prevEvent: prevEvent, - ), - ); - return; - } - final overlayEntry = MessageSelectionOverlay( chatController: this, event: event, @@ -2294,6 +2268,36 @@ class ChatController extends State ); } + Future updateLanguageOnMismatch(String target) async { + final messenger = ScaffoldMessenger.of(context); + messenger.hideCurrentSnackBar(); + final resp = await showFutureLoadingDialog( + context: context, + future: () async { + clearSelectedEvents(); + await MatrixState.pangeaController.userController.updateProfile( + (profile) { + profile.userSettings.targetLanguage = target; + return profile; + }, + waitForDataInSync: true, + ); + }, + ); + if (resp.isError) return; + if (mounted) { + messenger.hideCurrentSnackBar(); + messenger.showSnackBar( + SnackBar( + content: Text( + L10n.of(context).languageUpdated, + textAlign: TextAlign.center, + ), + ), + ); + } + } + void _onCloseIT() { if (choreographer.timesDismissedIT.value >= 3) { showDisableLanguageToolsPopup(); diff --git a/lib/pangea/learning_settings/language_mismatch_repo.dart b/lib/pangea/learning_settings/language_mismatch_repo.dart index 0096bff42..430f3a586 100644 --- a/lib/pangea/learning_settings/language_mismatch_repo.dart +++ b/lib/pangea/learning_settings/language_mismatch_repo.dart @@ -5,14 +5,10 @@ class LanguageMismatchRepo { static const Duration displayInterval = Duration(minutes: 30); static String _roomKey(String roomId) => 'language_mismatch_room_$roomId'; - static String _eventKey(String eventId) => 'language_mismatch_event_$eventId'; static bool shouldShowByRoom(String roomId) => _get(_roomKey(roomId)); - static bool shouldShowByEvent(String eventId) => _get(_eventKey(eventId)); static Future setRoom(String roomId) async => _set(_roomKey(roomId)); - static Future setEvent(String eventId) async => - _set(_eventKey(eventId)); static Future _set(String key) async { await _storage.write( diff --git a/lib/pangea/toolbar/reading_assistance/select_mode_buttons.dart b/lib/pangea/toolbar/reading_assistance/select_mode_buttons.dart index 116fd451d..82f6f2e26 100644 --- a/lib/pangea/toolbar/reading_assistance/select_mode_buttons.dart +++ b/lib/pangea/toolbar/reading_assistance/select_mode_buttons.dart @@ -217,10 +217,32 @@ class SelectModeButtonsState extends State { } Future modeDisabled() async { - ScaffoldMessenger.of(context).hideCurrentSnackBar(); - ScaffoldMessenger.of(context).showSnackBar( + final target = controller.messageEvent.originalSent?.langCode; + final messenger = ScaffoldMessenger.of(context); + messenger.hideCurrentSnackBar(); + messenger.showSnackBar( SnackBar( - content: Text(L10n.of(context).modeDisabled), + content: Row( + spacing: 12.0, + children: [ + Flexible( + child: Text( + L10n.of(context).modeDisabled, + textAlign: TextAlign.center, + ), + ), + if (target != null) + TextButton( + style: TextButton.styleFrom( + foregroundColor: + Theme.of(context).colorScheme.primaryContainer, + ), + onPressed: () => + widget.controller.updateLanguageOnMismatch(target), + child: Text(L10n.of(context).learn), + ), + ], + ), ), ); } @@ -405,6 +427,7 @@ class SelectModeButtonsState extends State { loading: controller.isLoading && mode == selectedMode, playing: mode == SelectMode.audio && playing, + color: theme.colorScheme.onPrimaryContainer, ), ), ), @@ -435,11 +458,13 @@ class _SelectModeButtonIcon extends StatelessWidget { final SelectMode mode; final bool loading; final bool playing; + final Color color; const _SelectModeButtonIcon({ required this.mode, this.loading = false, this.playing = false, + required this.color, }); @override @@ -458,10 +483,11 @@ class _SelectModeButtonIcon extends StatelessWidget { return Icon( playing ? Icons.pause_outlined : Icons.volume_up, size: 20, + color: color, ); } - return Icon(mode.icon, size: 20); + return Icon(mode.icon, size: 20, color: color); } } From 2b68f4a1fbee3b663de076b1629da192e1a86197 Mon Sep 17 00:00:00 2001 From: ggurdin Date: Tue, 20 Jan 2026 12:56:10 -0500 Subject: [PATCH 19/27] fix: disable other practice choices immeadiatley after correct choice made --- .../analytics_practice_page.dart | 8 ++++ .../analytics_practice_view.dart | 47 ++++++++++++------- .../choice_cards/game_choice_card.dart | 3 +- .../choice_cards/grammar_choice_card.dart | 3 ++ 4 files changed, 41 insertions(+), 20 deletions(-) diff --git a/lib/pangea/analytics_practice/analytics_practice_page.dart b/lib/pangea/analytics_practice/analytics_practice_page.dart index 67b7e2d26..d80bd7800 100644 --- a/lib/pangea/analytics_practice/analytics_practice_page.dart +++ b/lib/pangea/analytics_practice/analytics_practice_page.dart @@ -83,6 +83,7 @@ class AnalyticsPracticeState extends State ValueNotifier(null); final ValueNotifier progressNotifier = ValueNotifier(0.0); + final ValueNotifier enableChoicesNotifier = ValueNotifier(true); final Map> _choiceTexts = {}; final Map> _choiceEmojis = {}; @@ -106,6 +107,7 @@ class AnalyticsPracticeState extends State activityState.dispose(); activityTarget.dispose(); progressNotifier.dispose(); + enableChoicesNotifier.dispose(); super.dispose(); } @@ -190,6 +192,7 @@ class AnalyticsPracticeState extends State void _resetActivityState() { activityState.value = const AsyncState.loading(); activityTarget.value = null; + enableChoicesNotifier.value = true; } void _resetSessionState() { @@ -272,6 +275,7 @@ class AnalyticsPracticeState extends State Future _continueSession() async { if (_continuing) return; _continuing = true; + enableChoicesNotifier.value = true; try { if (activityState.value @@ -403,6 +407,10 @@ class AnalyticsPracticeState extends State ) async { if (_currentActivity == null) return; final activity = _currentActivity!; + final isCorrect = activity.multipleChoiceContent.isCorrect(choiceContent); + if (isCorrect) { + enableChoicesNotifier.value = false; + } // Update activity record PracticeRecordController.onSelectChoice( diff --git a/lib/pangea/analytics_practice/analytics_practice_view.dart b/lib/pangea/analytics_practice/analytics_practice_view.dart index 9b46524a9..6cbef9eae 100644 --- a/lib/pangea/analytics_practice/analytics_practice_view.dart +++ b/lib/pangea/analytics_practice/analytics_practice_view.dart @@ -346,25 +346,29 @@ class _ActivityChoicesWidget extends StatelessWidget { return Container( constraints: const BoxConstraints(maxHeight: 400.0), - child: Column( - spacing: 4.0, - mainAxisSize: MainAxisSize.min, - children: choices - .map( - (choice) => _ChoiceCard( - activity: value, - targetId: - controller.choiceTargetId(choice.choiceId), - choiceId: choice.choiceId, - onPressed: () => controller.onSelectChoice( - choice.choiceId, + child: ValueListenableBuilder( + valueListenable: controller.enableChoicesNotifier, + builder: (context, enabled, __) => Column( + spacing: 4.0, + mainAxisSize: MainAxisSize.min, + children: choices + .map( + (choice) => _ChoiceCard( + activity: value, + targetId: + controller.choiceTargetId(choice.choiceId), + choiceId: choice.choiceId, + onPressed: () => controller.onSelectChoice( + choice.choiceId, + ), + cardHeight: cardHeight, + choiceText: choice.choiceText, + choiceEmoji: choice.choiceEmoji, + enabled: enabled, ), - cardHeight: cardHeight, - choiceText: choice.choiceText, - choiceEmoji: choice.choiceEmoji, - ), - ) - .toList(), + ) + .toList(), + ), ), ); }, @@ -390,6 +394,7 @@ class _ChoiceCard extends StatelessWidget { final String choiceText; final String? choiceEmoji; + final bool enabled; const _ChoiceCard({ required this.activity, @@ -399,6 +404,7 @@ class _ChoiceCard extends StatelessWidget { required this.cardHeight, required this.choiceText, required this.choiceEmoji, + this.enabled = true, }); @override @@ -420,6 +426,7 @@ class _ChoiceCard extends StatelessWidget { onPressed: onPressed, isCorrect: isCorrect, height: cardHeight, + isEnabled: enabled, ); case ActivityTypeEnum.lemmaAudio: @@ -432,6 +439,7 @@ class _ChoiceCard extends StatelessWidget { onPressed: onPressed, isCorrect: isCorrect, height: cardHeight, + isEnabled: enabled, ); case ActivityTypeEnum.grammarCategory: @@ -445,6 +453,7 @@ class _ChoiceCard extends StatelessWidget { tag: choiceText, onPressed: onPressed, isCorrect: isCorrect, + enabled: enabled, ); case ActivityTypeEnum.grammarError: @@ -458,6 +467,7 @@ class _ChoiceCard extends StatelessWidget { onPressed: onPressed, isCorrect: isCorrect, height: cardHeight, + isEnabled: enabled, child: Text(choiceText), ); @@ -471,6 +481,7 @@ class _ChoiceCard extends StatelessWidget { onPressed: onPressed, isCorrect: isCorrect, height: cardHeight, + isEnabled: enabled, child: Text(choiceText), ); } diff --git a/lib/pangea/analytics_practice/choice_cards/game_choice_card.dart b/lib/pangea/analytics_practice/choice_cards/game_choice_card.dart index ad2e5c461..58c5095a1 100644 --- a/lib/pangea/analytics_practice/choice_cards/game_choice_card.dart +++ b/lib/pangea/analytics_practice/choice_cards/game_choice_card.dart @@ -62,6 +62,7 @@ class _GameChoiceCardState extends State Future _handleTap() async { if (!widget.isEnabled) return; + widget.onPressed(); if (widget.shouldFlip) { if (_controller.isAnimating || _revealed) return; @@ -73,8 +74,6 @@ class _GameChoiceCardState extends State if (_clicked) return; setState(() => _clicked = true); } - - widget.onPressed(); } @override diff --git a/lib/pangea/analytics_practice/choice_cards/grammar_choice_card.dart b/lib/pangea/analytics_practice/choice_cards/grammar_choice_card.dart index c1299b611..1bfa5c831 100644 --- a/lib/pangea/analytics_practice/choice_cards/grammar_choice_card.dart +++ b/lib/pangea/analytics_practice/choice_cards/grammar_choice_card.dart @@ -15,6 +15,7 @@ class GrammarChoiceCard extends StatelessWidget { final VoidCallback onPressed; final bool isCorrect; final double height; + final bool enabled; const GrammarChoiceCard({ required this.choiceId, @@ -24,6 +25,7 @@ class GrammarChoiceCard extends StatelessWidget { required this.onPressed, required this.isCorrect, this.height = 72.0, + this.enabled = true, super.key, }); @@ -42,6 +44,7 @@ class GrammarChoiceCard extends StatelessWidget { onPressed: onPressed, isCorrect: isCorrect, height: height, + isEnabled: enabled, child: Text(copy), ); } From 86b69a67fab28e8546e6193f236f0444b932e0be Mon Sep 17 00:00:00 2001 From: ggurdin Date: Tue, 20 Jan 2026 13:12:25 -0500 Subject: [PATCH 20/27] fix: pass manual IGC status after showing language mismatch popup --- lib/pages/chat/chat.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/pages/chat/chat.dart b/lib/pages/chat/chat.dart index 1dce476c6..918a52709 100644 --- a/lib/pages/chat/chat.dart +++ b/lib/pages/chat/chat.dart @@ -2237,7 +2237,7 @@ class ChatController extends State bool autosend = false, }) async { if (shouldShowLanguageMismatchPopupByActivity) { - return showLanguageMismatchPopup(); + return showLanguageMismatchPopup(manual: manual); } await choreographer.requestWritingAssistance(manual: manual); @@ -2250,7 +2250,7 @@ class ChatController extends State } } - void showLanguageMismatchPopup() { + void showLanguageMismatchPopup({bool manual = false}) { if (!shouldShowLanguageMismatchPopupByActivity) { return; } @@ -2263,7 +2263,7 @@ class ChatController extends State message: L10n.of(context).languageMismatchDesc, targetLanguage: targetLanguage, onConfirm: () => WidgetsBinding.instance.addPostFrameCallback( - (_) => onRequestWritingAssistance(manual: false, autosend: true), + (_) => onRequestWritingAssistance(manual: manual, autosend: true), ), ); } From fecf8de443aa2708e622d797c9f07fb67c627b88 Mon Sep 17 00:00:00 2001 From: ggurdin Date: Tue, 20 Jan 2026 13:32:50 -0500 Subject: [PATCH 21/27] fix: restrict height of dropdowns in user menu popup --- lib/pangea/bot/widgets/bot_chat_settings_dialog.dart | 4 ++-- lib/pangea/chat_settings/widgets/language_level_dropdown.dart | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/pangea/bot/widgets/bot_chat_settings_dialog.dart b/lib/pangea/bot/widgets/bot_chat_settings_dialog.dart index 47aa375e6..a748fdccc 100644 --- a/lib/pangea/bot/widgets/bot_chat_settings_dialog.dart +++ b/lib/pangea/bot/widgets/bot_chat_settings_dialog.dart @@ -1,4 +1,3 @@ -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:dropdown_button2/dropdown_button2.dart'; @@ -155,6 +154,7 @@ class BotChatSettingsDialogState extends State { onChanged: _setLevel, enabled: !widget.room.isActivitySession, width: 300, + maxHeight: 300, ), DropdownButtonFormField2( customButton: _selectedVoice != null @@ -171,7 +171,7 @@ class BotChatSettingsDialogState extends State { ), isExpanded: true, dropdownStyleData: DropdownStyleData( - maxHeight: kIsWeb ? 250 : null, + maxHeight: 250, decoration: BoxDecoration( color: Theme.of(context).colorScheme.surfaceContainerHigh, borderRadius: BorderRadius.circular(14.0), diff --git a/lib/pangea/chat_settings/widgets/language_level_dropdown.dart b/lib/pangea/chat_settings/widgets/language_level_dropdown.dart index 11e9cd0d8..19bd2aa15 100644 --- a/lib/pangea/chat_settings/widgets/language_level_dropdown.dart +++ b/lib/pangea/chat_settings/widgets/language_level_dropdown.dart @@ -14,6 +14,7 @@ class LanguageLevelDropdown extends StatelessWidget { final bool enabled; final Color? backgroundColor; final double? width; + final double? maxHeight; const LanguageLevelDropdown({ super.key, @@ -23,6 +24,7 @@ class LanguageLevelDropdown extends StatelessWidget { this.enabled = true, this.backgroundColor, this.width, + this.maxHeight, }); @override @@ -46,7 +48,7 @@ class LanguageLevelDropdown extends StatelessWidget { ), isExpanded: true, dropdownStyleData: DropdownStyleData( - maxHeight: kIsWeb ? 500 : null, + maxHeight: maxHeight ?? (kIsWeb ? 500 : null), decoration: BoxDecoration( color: backgroundColor ?? Theme.of(context).colorScheme.surfaceContainerHigh, From 0c9767307fb3650b3ff515ee8b9a0233ba0bca01 Mon Sep 17 00:00:00 2001 From: ggurdin Date: Tue, 20 Jan 2026 13:35:57 -0500 Subject: [PATCH 22/27] chore: make sso button order consistent --- lib/pangea/login/pages/signup_view.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pangea/login/pages/signup_view.dart b/lib/pangea/login/pages/signup_view.dart index 8492775ca..fdac63894 100644 --- a/lib/pangea/login/pages/signup_view.dart +++ b/lib/pangea/login/pages/signup_view.dart @@ -46,8 +46,8 @@ class SignupPageView extends StatelessWidget { fontWeight: FontWeight.bold, ), ), - const PangeaSsoButton(provider: SSOProvider.google), const PangeaSsoButton(provider: SSOProvider.apple), + const PangeaSsoButton(provider: SSOProvider.google), ElevatedButton( onPressed: () => context.go( '/home/language/signup/email', From bf0c49035d7afa21acd29bc89f6740236791c1c6 Mon Sep 17 00:00:00 2001 From: ggurdin Date: Tue, 20 Jan 2026 14:08:11 -0500 Subject: [PATCH 23/27] fix: use latest edit to make representations --- .../event_wrappers/pangea_message_event.dart | 43 ++++++++++++------- 1 file changed, 28 insertions(+), 15 deletions(-) diff --git a/lib/pangea/events/event_wrappers/pangea_message_event.dart b/lib/pangea/events/event_wrappers/pangea_message_event.dart index f1322fdf2..22cf8ca1d 100644 --- a/lib/pangea/events/event_wrappers/pangea_message_event.dart +++ b/lib/pangea/events/event_wrappers/pangea_message_event.dart @@ -114,7 +114,7 @@ class PangeaMessageEvent { .map( (e) => RepresentationEvent( event: e, - parentMessageEvent: _event, + parentMessageEvent: _latestEdit, timeline: timeline, ), ) @@ -181,14 +181,14 @@ class PangeaMessageEvent { final original = PangeaRepresentation( langCode: lang ?? LanguageKeys.unknownLanguage, - text: _event.body, + text: _latestEdit.body, originalSent: true, originalWritten: false, ); _representations!.add( RepresentationEvent( - parentMessageEvent: _event, + parentMessageEvent: _latestEdit, content: original, tokens: tokens, choreo: _embeddedChoreo, @@ -202,7 +202,7 @@ class PangeaMessageEvent { e: err, s: s, data: { - "event": _event.toJson(), + "event": _latestEdit.toJson(), }, ); } @@ -211,7 +211,7 @@ class PangeaMessageEvent { try { _representations!.add( RepresentationEvent( - parentMessageEvent: _event, + parentMessageEvent: _latestEdit, content: PangeaRepresentation.fromJson( _latestEdit.content[ModelKey.originalWritten] as Map, @@ -229,7 +229,7 @@ class PangeaMessageEvent { e: err, s: s, data: { - "event": _event.toJson(), + "event": _latestEdit.toJson(), }, ); } @@ -278,7 +278,8 @@ class PangeaMessageEvent { /// Gets the message display text for the current language code. /// If the message display text is not available for the current language code, /// it returns the message body. - String get messageDisplayText => messageDisplayRepresentation?.text ?? body; + String get messageDisplayText => + messageDisplayRepresentation?.text ?? _latestEdit.body; TextDirection get textDirection => LanguageConstants.rtlLanguageCodes.contains(messageDisplayLangCode) @@ -293,12 +294,14 @@ class PangeaMessageEvent { RepresentationEvent? representationByLanguage( String langCode, { bool Function(RepresentationEvent)? filter, - }) => - representations.firstWhereOrNull( - (element) => - element.langCode.split("-")[0] == langCode.split("-")[0] && - (filter?.call(element) ?? true), - ); + }) { + representations.firstWhereOrNull( + (element) => + element.langCode.split("-")[0] == langCode.split("-")[0] && + (filter?.call(element) ?? true), + ); + return null; + } Event? getTextToSpeechLocal(String langCode, String text) { for (final audio in allAudio) { @@ -492,7 +495,7 @@ class PangeaMessageEvent { _representations = null; return room.sendPangeaEvent( content: representation.toJson(), - parentEventId: eventId, + parentEventId: _latestEdit.eventId, type: PangeaEventTypes.representation, ); } @@ -586,6 +589,7 @@ class PangeaMessageEvent { } Future requestRespresentationByL1() async { + debugPrint("LATEST EDIT: ${_latestEdit.toJson()}"); if (_l1Code == null || _l2Code == null) { throw Exception("Missing language codes"); } @@ -597,7 +601,9 @@ class PangeaMessageEvent { RepresentationEvent? rep; if (!includedIT) { // if the message didn't go through translation, get any l1 rep + debugPrint("REPRESENTATIONS: ${representations.length}"); rep = representationByLanguage(_l1Code!); + debugPrint("REP: $rep"); } else { // if the message went through translation, get the non-original // l1 rep since originalWritten could contain some l2 words @@ -614,6 +620,13 @@ class PangeaMessageEvent { ? (originalWritten?.langCode ?? _l1Code!) : (originalSent?.langCode ?? _l2Code!); + debugPrint("Original written content: $originalWrittenContent"); + debugPrint("Message display text: $messageDisplayText"); + debugPrint("Original sent: ${originalSent?.content.toJson()}"); + debugPrint( + "Message display rep: ${representationByLanguage(messageDisplayLangCode)?.content.toJson()}", + ); + final resp = await _requestRepresentation( includedIT ? originalWrittenContent : messageDisplayText, _l1Code!, @@ -661,7 +674,7 @@ class PangeaMessageEvent { ) async { final repEvent = await room.sendPangeaEvent( content: representation.toJson(), - parentEventId: eventId, + parentEventId: _latestEdit.eventId, type: PangeaEventTypes.representation, ); return repEvent?.eventId; From 68ec80a25a6af5327a0f197eb35c9af16987ac80 Mon Sep 17 00:00:00 2001 From: ggurdin Date: Tue, 20 Jan 2026 14:17:09 -0500 Subject: [PATCH 24/27] chore: show tooltip on full phonetic transcription widget --- .../phonetic_transcription_widget.dart | 123 +++++++++--------- 1 file changed, 61 insertions(+), 62 deletions(-) diff --git a/lib/pangea/phonetic_transcription/phonetic_transcription_widget.dart b/lib/pangea/phonetic_transcription/phonetic_transcription_widget.dart index de6a7c494..8f5737125 100644 --- a/lib/pangea/phonetic_transcription/phonetic_transcription_widget.dart +++ b/lib/pangea/phonetic_transcription/phonetic_transcription_widget.dart @@ -69,60 +69,59 @@ class _PhoneticTranscriptionWidgetState final targetId = 'phonetic-transcription-${widget.text}-$hashCode'; return HoverBuilder( builder: (context, hovering) { - return GestureDetector( - onTap: () => _handleAudioTap(targetId), - child: AnimatedContainer( - duration: const Duration(milliseconds: 150), - decoration: BoxDecoration( - color: hovering - ? Colors.grey.withAlpha((0.2 * 255).round()) - : Colors.transparent, - borderRadius: BorderRadius.circular(6), - ), - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), - child: CompositedTransformTarget( - link: MatrixState.pAnyState.layerLinkAndKey(targetId).link, - child: PhoneticTranscriptionBuilder( - key: MatrixState.pAnyState.layerLinkAndKey(targetId).key, - textLanguage: widget.textLanguage, - text: widget.text, - reloadNotifier: widget.reloadNotifier, - builder: (context, controller) { - return switch (controller.state) { - AsyncError(error: final error) => - error is UnsubscribedException - ? ErrorIndicator( - message: L10n.of(context) - .subscribeToUnlockTranscriptions, - onTap: () { - MatrixState - .pangeaController.subscriptionController - .showPaywall(context); - }, - ) - : ErrorIndicator( - message: - L10n.of(context).failedToFetchTranscription, + return Tooltip( + message: + _isPlaying ? L10n.of(context).stop : L10n.of(context).playAudio, + child: GestureDetector( + onTap: () => _handleAudioTap(targetId), + child: AnimatedContainer( + duration: const Duration(milliseconds: 150), + decoration: BoxDecoration( + color: hovering + ? Colors.grey.withAlpha((0.2 * 255).round()) + : Colors.transparent, + borderRadius: BorderRadius.circular(6), + ), + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + child: CompositedTransformTarget( + link: MatrixState.pAnyState.layerLinkAndKey(targetId).link, + child: PhoneticTranscriptionBuilder( + key: MatrixState.pAnyState.layerLinkAndKey(targetId).key, + textLanguage: widget.textLanguage, + text: widget.text, + reloadNotifier: widget.reloadNotifier, + builder: (context, controller) { + return switch (controller.state) { + AsyncError(error: final error) => + error is UnsubscribedException + ? ErrorIndicator( + message: L10n.of(context) + .subscribeToUnlockTranscriptions, + onTap: () { + MatrixState + .pangeaController.subscriptionController + .showPaywall(context); + }, + ) + : ErrorIndicator( + message: + L10n.of(context).failedToFetchTranscription, + ), + AsyncLoaded(value: final transcription) => Row( + spacing: 8.0, + mainAxisSize: MainAxisSize.min, + children: [ + Flexible( + child: Text( + transcription, + textScaler: TextScaler.noScaling, + style: widget.style ?? + Theme.of(context).textTheme.bodyMedium, + maxLines: widget.maxLines, + overflow: TextOverflow.ellipsis, + ), ), - AsyncLoaded(value: final transcription) => Row( - spacing: 8.0, - mainAxisSize: MainAxisSize.min, - children: [ - Flexible( - child: Text( - transcription, - textScaler: TextScaler.noScaling, - style: widget.style ?? - Theme.of(context).textTheme.bodyMedium, - maxLines: widget.maxLines, - overflow: TextOverflow.ellipsis, - ), - ), - Tooltip( - message: _isPlaying - ? L10n.of(context).stop - : L10n.of(context).playAudio, - child: Icon( + Icon( _isPlaying ? Icons.pause_outlined : Icons.volume_up, @@ -130,15 +129,15 @@ class _PhoneticTranscriptionWidgetState color: widget.iconColor ?? Theme.of(context).iconTheme.color, ), - ), - ], - ), - _ => const TextLoadingShimmer( - width: 125.0, - height: 20.0, - ), - }; - }, + ], + ), + _ => const TextLoadingShimmer( + width: 125.0, + height: 20.0, + ), + }; + }, + ), ), ), ), From 3f9f8867b87cb242e1308d07260e343b5ec53cd5 Mon Sep 17 00:00:00 2001 From: Ava Shilling <165050625+avashilling@users.noreply.github.com> Date: Tue, 20 Jan 2026 14:25:16 -0500 Subject: [PATCH 25/27] chore: shrink tooltip text size Also give it maxTimelineWidth in chat to match other widgets placement, and give slightly less padding between icons --- lib/pangea/chat/widgets/chat_input_bar.dart | 32 +++++++++++++------ .../instructions_inline_tooltip.dart | 17 ++++++---- 2 files changed, 32 insertions(+), 17 deletions(-) diff --git a/lib/pangea/chat/widgets/chat_input_bar.dart b/lib/pangea/chat/widgets/chat_input_bar.dart index 78a0cdf4f..599c25abb 100644 --- a/lib/pangea/chat/widgets/chat_input_bar.dart +++ b/lib/pangea/chat/widgets/chat_input_bar.dart @@ -31,18 +31,30 @@ class ChatInputBar extends StatelessWidget { valueListenable: controller.choreographer.itController.open, builder: (context, open, __) { return open - ? InstructionsInlineTooltip( - instructionsEnum: InstructionsEnum.clickBestOption, - animate: false, - padding: EdgeInsets.only( - left: 16.0, - right: 16.0, - top: FluffyThemes.isColumnMode(context) ? 16.0 : 8.0, + ? Container( + constraints: const BoxConstraints( + maxWidth: FluffyThemes.maxTimelineWidth, + ), + alignment: Alignment.center, + child: InstructionsInlineTooltip( + instructionsEnum: InstructionsEnum.clickBestOption, + animate: false, + padding: EdgeInsets.only( + left: 16.0, + right: 16.0, + top: FluffyThemes.isColumnMode(context) ? 16.0 : 8.0, + ), ), ) - : ActivityRoleTooltip( - room: controller.room, - hide: controller.choreographer.itController.open, + : Container( + constraints: const BoxConstraints( + maxWidth: FluffyThemes.maxTimelineWidth, + ), + alignment: Alignment.center, + child: ActivityRoleTooltip( + room: controller.room, + hide: controller.choreographer.itController.open, + ), ); }, ), diff --git a/lib/pangea/instructions/instructions_inline_tooltip.dart b/lib/pangea/instructions/instructions_inline_tooltip.dart index 6059cb90a..5839d5657 100644 --- a/lib/pangea/instructions/instructions_inline_tooltip.dart +++ b/lib/pangea/instructions/instructions_inline_tooltip.dart @@ -134,25 +134,28 @@ class InlineTooltipState extends State crossAxisAlignment: CrossAxisAlignment.center, mainAxisSize: MainAxisSize.min, children: [ - Icon( - Icons.lightbulb, - size: 20, - color: Theme.of(context).colorScheme.onSurface, + Padding( + padding: const EdgeInsets.only(right: 6.0), + child: Icon( + Icons.lightbulb, + size: 20, + color: Theme.of(context).colorScheme.onSurface, + ), ), - const SizedBox(width: 8), Flexible( child: Center( child: Text( widget.message, style: widget.textStyle ?? (FluffyThemes.isColumnMode(context) - ? Theme.of(context).textTheme.titleLarge - : Theme.of(context).textTheme.bodyLarge), + ? Theme.of(context).textTheme.titleSmall + : Theme.of(context).textTheme.bodyMedium), textAlign: TextAlign.center, ), ), ), IconButton( + padding: const EdgeInsets.only(left: 6.0), constraints: const BoxConstraints(), icon: Icon( Icons.close_outlined, From ee882d3ea45c938939cf8141dc458dc3ef8159dc Mon Sep 17 00:00:00 2001 From: ggurdin Date: Tue, 20 Jan 2026 14:31:15 -0500 Subject: [PATCH 26/27] feat: show audio message transcripts in vocab practice --- .../analytics_misc/example_message_util.dart | 28 ++++++++++++++----- .../analytics_practice_page.dart | 7 +---- 2 files changed, 22 insertions(+), 13 deletions(-) diff --git a/lib/pangea/analytics_misc/example_message_util.dart b/lib/pangea/analytics_misc/example_message_util.dart index 43a3e659c..10012f75f 100644 --- a/lib/pangea/analytics_misc/example_message_util.dart +++ b/lib/pangea/analytics_misc/example_message_util.dart @@ -6,6 +6,7 @@ import 'package:matrix/matrix.dart'; import 'package:fluffychat/pangea/analytics_misc/client_analytics_extension.dart'; import 'package:fluffychat/pangea/analytics_misc/construct_use_model.dart'; import 'package:fluffychat/pangea/events/event_wrappers/pangea_message_event.dart'; +import 'package:fluffychat/pangea/events/models/pangea_token_model.dart'; class ExampleMessageUtil { static Future?> getExampleMessage( @@ -49,14 +50,27 @@ class ExampleMessageUtil { String? form, PangeaMessageEvent messageEvent, ) { - final tokens = messageEvent.messageDisplayRepresentation?.tokens; - if (tokens == null || tokens.isEmpty) return null; - final token = tokens.firstWhereOrNull( - (token) => token.text.content == form, - ); - if (token == null) return null; + PangeaToken? token; + String? text; - final text = messageEvent.messageDisplayText; + if (messageEvent.isAudioMessage) { + final stt = messageEvent.getSpeechToTextLocal(); + if (stt == null) return null; + final tokens = stt.transcript.sttTokens.map((t) => t.token).toList(); + token = tokens.firstWhereOrNull( + (token) => token.text.content == form, + ); + text = stt.transcript.text; + } else { + final tokens = messageEvent.messageDisplayRepresentation?.tokens; + if (tokens == null || tokens.isEmpty) return null; + token = tokens.firstWhereOrNull( + (token) => token.text.content == form, + ); + text = messageEvent.messageDisplayText; + } + + if (token == null) return null; final tokenText = token.text.content; int tokenIndex = text.indexOf(tokenText); if (tokenIndex == -1) return null; diff --git a/lib/pangea/analytics_practice/analytics_practice_page.dart b/lib/pangea/analytics_practice/analytics_practice_page.dart index d80bd7800..97ca1d1a7 100644 --- a/lib/pangea/analytics_practice/analytics_practice_page.dart +++ b/lib/pangea/analytics_practice/analytics_practice_page.dart @@ -442,12 +442,7 @@ class AnalyticsPracticeState extends State PracticeTarget target, ) async { final token = target.tokens.first; - final construct = switch (widget.type) { - ConstructTypeEnum.vocab => token.vocabConstructID, - ConstructTypeEnum.morph => token.morphIdByFeature(target.morphFeature!), - }; - - if (construct == null) return null; + final construct = target.targetTokenConstructID(token); String? form; if (widget.type == ConstructTypeEnum.morph) { From 79926a9aded0b46c1fdc14175da5d78b1b70fe8d Mon Sep 17 00:00:00 2001 From: ggurdin Date: Tue, 20 Jan 2026 14:51:44 -0500 Subject: [PATCH 27/27] moved some logic around --- lib/pages/chat/chat.dart | 2 ++ .../select_mode_buttons.dart | 19 ++++++++------- .../select_mode_controller.dart | 24 ++++++++++++++----- 3 files changed, 30 insertions(+), 15 deletions(-) diff --git a/lib/pages/chat/chat.dart b/lib/pages/chat/chat.dart index 18a21f926..f96091ab4 100644 --- a/lib/pages/chat/chat.dart +++ b/lib/pages/chat/chat.dart @@ -2414,6 +2414,8 @@ class ChatController extends State ); if (reason == null) return; + + clearSelectedEvents(); await showFutureLoadingDialog( context: context, future: () => room.sendEvent( diff --git a/lib/pangea/toolbar/reading_assistance/select_mode_buttons.dart b/lib/pangea/toolbar/reading_assistance/select_mode_buttons.dart index 6549403a9..b97537288 100644 --- a/lib/pangea/toolbar/reading_assistance/select_mode_buttons.dart +++ b/lib/pangea/toolbar/reading_assistance/select_mode_buttons.dart @@ -183,6 +183,9 @@ class SelectModeButtonsState extends State { SelectModeController get controller => widget.overlayController.selectModeController; + bool get _canRefresh => + messageEvent.eventId == widget.controller.refreshEventID; + Future updateMode(SelectMode? mode) async { if (mode == null) { matrix?.audioPlayer?.stop(); @@ -219,9 +222,13 @@ class SelectModeButtonsState extends State { } if (updatedMode == SelectMode.requestRegenerate) { - widget.controller.requestRegeneration( + await widget.controller.requestRegeneration( messageEvent.eventId, ); + + if (mounted) { + controller.setSelectMode(null); + } } } @@ -357,13 +364,7 @@ class SelectModeButtonsState extends State { Widget build(BuildContext context) { final theme = Theme.of(context); final modes = controller.readingAssistanceModes; - final allModes = controller.allModes - .where( - (mode) => - mode != SelectMode.requestRegenerate || - messageEvent.eventId == widget.controller.refreshEventID, - ) - .toList(); + final allModes = controller.allModes(enableRefresh: _canRefresh); return Material( type: MaterialType.transparency, child: SizedBox( @@ -373,7 +374,7 @@ class SelectModeButtonsState extends State { children: List.generate(allModes.length + 1, (index) { if (index < allModes.length) { final mode = allModes[index]; - final enabled = modes.contains(mode); + final enabled = modes(enableRefresh: _canRefresh).contains(mode); return Container( width: 45.0, alignment: Alignment.center, diff --git a/lib/pangea/toolbar/reading_assistance/select_mode_controller.dart b/lib/pangea/toolbar/reading_assistance/select_mode_controller.dart index e5de6dfaa..a5f243674 100644 --- a/lib/pangea/toolbar/reading_assistance/select_mode_controller.dart +++ b/lib/pangea/toolbar/reading_assistance/select_mode_controller.dart @@ -113,7 +113,6 @@ class SelectModeController with LemmaEmojiSetter { SelectMode.translate, SelectMode.practice, SelectMode.emoji, - SelectMode.requestRegenerate, ]; static List get _audioModes => [ @@ -131,7 +130,7 @@ class SelectModeController with LemmaEmojiSetter { (PangeaAudioFile, File?)? get audioFile => _audioLoader.value; - List get allModes { + List allModes({bool enableRefresh = false}) { final validTypes = {MessageTypes.Text, MessageTypes.Audio}; if (!messageEvent.event.status.isSent || messageEvent.event.type != EventTypes.Message || @@ -139,12 +138,18 @@ class SelectModeController with LemmaEmojiSetter { return []; } - return messageEvent.event.messageType == MessageTypes.Text + final types = messageEvent.event.messageType == MessageTypes.Text ? _textModes : _audioModes; + + if (enableRefresh) { + return [...types, SelectMode.requestRegenerate]; + } + + return types; } - List get readingAssistanceModes { + List readingAssistanceModes({bool enableRefresh = false}) { final validTypes = {MessageTypes.Text, MessageTypes.Audio}; if (!messageEvent.event.status.isSent || messageEvent.event.type != EventTypes.Message || @@ -152,6 +157,7 @@ class SelectModeController with LemmaEmojiSetter { return []; } + List modes = []; if (messageEvent.event.messageType == MessageTypes.Text) { final lang = messageEvent.messageDisplayLangCode.split("-").first; @@ -161,14 +167,20 @@ class SelectModeController with LemmaEmojiSetter { final matchesL1 = lang == MatrixState.pangeaController.userController.userL1!.langCodeShort; - return matchesL2 + modes = matchesL2 ? _textModes : matchesL1 ? [] : [SelectMode.translate]; + } else { + modes = _audioModes; } - return _audioModes; + if (enableRefresh) { + modes = [...modes, SelectMode.requestRegenerate]; + } + + return modes; } bool get isLoading => currentModeStateNotifier?.value is AsyncLoading;