From 38e908c33c9147ddfe5034e0e4129758a1944469 Mon Sep 17 00:00:00 2001 From: wcjord <32568597+wcjord@users.noreply.github.com> Date: Mon, 2 Mar 2026 13:01:03 -0500 Subject: [PATCH] docs: add User Feedback section to toolbar reading assistance instructions (#5839) * docs: add User Feedback section to toolbar reading assistance instructions Describes the shared feedback pattern for word cards (existing) and translations (planned). References #5838. * feat: allow users to flag translations * make flag button smaller --------- Co-authored-by: ggurdin --- ...toolbar-reading-assistance.instructions.md | 23 +++++- lib/l10n/intl_en.arb | 3 +- .../analytics_practice_data_service.dart | 4 +- .../grammar_error_target_generator.dart | 3 +- .../choreographer/choreo_record_model.dart | 2 +- .../event_wrappers/pangea_message_event.dart | 82 +++++++++++++------ .../toolbar/layout/overlay_message.dart | 56 +++++++++++-- .../select_mode_controller.dart | 51 ++++++++---- .../full_text_translation_repo.dart | 18 ++-- .../full_text_translation_request_model.dart | 14 +++- .../full_text_translation_response_model.dart | 14 +++- 11 files changed, 201 insertions(+), 69 deletions(-) diff --git a/.github/instructions/toolbar-reading-assistance.instructions.md b/.github/instructions/toolbar-reading-assistance.instructions.md index 0a0140343..ca37677e2 100644 --- a/.github/instructions/toolbar-reading-assistance.instructions.md +++ b/.github/instructions/toolbar-reading-assistance.instructions.md @@ -77,7 +77,7 @@ The word card is the detailed view for a single token. It appears above the mess - **Meaning** — either user-set or auto-generated L1 translation - **Phonetic transcription** — IPA or simplified pronunciation guide - **Emoji** — the user's personal emoji association (if set), or a picker to set one -- **Feedback button** — lets the user report bad tokenization or incorrect meanings +- **Feedback button** — lets the user flag incorrect token data (POS, meaning, phonetics, language). See §User Feedback below. The word card is intentionally compact — it should be glanceable, not a full dictionary entry. The goal is quick recognition, not exhaustive reference. @@ -119,6 +119,27 @@ The toolbar must work within chat layout constraints: - The overlay must survive screen rotation and keyboard appearance without losing state - On width changes (e.g., split-screen), the overlay dismisses rather than attempting to reposition (avoids jarring layout jumps) +## User Feedback + +AI-generated content in the toolbar — word card info and translations — can be wrong. Users need a lightweight way to say "this is incorrect" without leaving the toolbar flow. The pattern is the same everywhere: a small **flag icon** beside the content opens a dialog where the user describes the problem in free text. The server re-generates the content with the feedback in context and returns an improved result. + +### Design Principles + +- **Low friction**: One tap to flag, one text field, done. The user shouldn't need to know *what* is wrong technically — just describe it in their own words. +- **Immediate improvement**: After flagging, the UI replaces the old content with the regenerated version so the user sees the fix right away. +- **Same interaction everywhere**: Word card flagging and translation flagging look and feel identical to the user. Same icon, same dialog, same flow. +- **Auditable**: Every flag is recorded on the server with the user's identity, building a quality signal that improves future results for all users. + +### Word Card Feedback (exists) + +The word card already has a flag button. When tapped, the user can report issues with tokenization, meaning, phonetics, or language detection. The server figures out which fields need correction and returns updates. See [token-info-feedback-v2.instructions.md](token-info-feedback-v2.instructions.md). + +### Translation Feedback (planned) + +The full-text translation shown in Translate mode currently has no flag button. Add one — same icon, same dialog, same UX as word card feedback. When the user flags a bad translation, the server regenerates it with a stronger model and the user's feedback as context. + +This is especially important for mixed-language and polysemous inputs where the default model gets it wrong (see [#1311](https://github.com/pangeachat/2-step-choreographer/issues/1311), [#1477](https://github.com/pangeachat/2-step-choreographer/issues/1477)). No new server endpoint is needed — the existing translation endpoint already supports feedback. See [direct-translate.instructions.md](../../2-step-choreographer/.github/instructions/direct-translate.instructions.md). + ## Key Contracts - **Overlay, not navigation.** The toolbar never pushes a route. It's a composited overlay that lives on top of the chat. Dismissal returns to the exact same chat state. diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 47eb03a32..a1111c381 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -5358,5 +5358,6 @@ "cannotJoinBannedRoom": "Banned. Unable to join.", "sessionFull": "Too late! This activity is full.", "returnToCourse": "Return to course", - "returnHome": "Return home" + "returnHome": "Return home", + "translationFeedback": "Translation Feedback" } diff --git a/lib/pangea/analytics_practice/analytics_practice_data_service.dart b/lib/pangea/analytics_practice/analytics_practice_data_service.dart index 0ee73e6e7..f91b91ed1 100644 --- a/lib/pangea/analytics_practice/analytics_practice_data_service.dart +++ b/lib/pangea/analytics_practice/analytics_practice_data_service.dart @@ -180,8 +180,8 @@ class AnalyticsPracticeDataService { } // Prefetch the translation - final translation = await pangeaEvent.requestRespresentationByL1(); - _setAudioInfo(eventId, audioFile, translation); + final translation = await pangeaEvent.requestTranslationByL1(); + _setAudioInfo(eventId, audioFile, translation.bestTranslation); } Future _prefetchLemmaInfo( diff --git a/lib/pangea/analytics_practice/grammar_error_target_generator.dart b/lib/pangea/analytics_practice/grammar_error_target_generator.dart index 83ae65418..c49de272a 100644 --- a/lib/pangea/analytics_practice/grammar_error_target_generator.dart +++ b/lib/pangea/analytics_practice/grammar_error_target_generator.dart @@ -134,7 +134,8 @@ class GrammarErrorTargetGenerator { } try { - translation ??= await event.requestRespresentationByL1(); + final resp = await event.requestTranslationByL1(); + translation ??= resp.bestTranslation; } catch (e, s) { ErrorHandler.logError( e: e, diff --git a/lib/pangea/choreographer/choreo_record_model.dart b/lib/pangea/choreographer/choreo_record_model.dart index 84d7ccdd4..9f20a1db7 100644 --- a/lib/pangea/choreographer/choreo_record_model.dart +++ b/lib/pangea/choreographer/choreo_record_model.dart @@ -159,7 +159,7 @@ class ChoreoRecordModel { }); bool endedWithIT(String sent) { - return includedIT && stepText() == sent; + return includedIT && !includedIGC && stepText() == sent; } /// Get the text at [stepIndex] diff --git a/lib/pangea/events/event_wrappers/pangea_message_event.dart b/lib/pangea/events/event_wrappers/pangea_message_event.dart index 7d93b9c06..e3e7f9eca 100644 --- a/lib/pangea/events/event_wrappers/pangea_message_event.dart +++ b/lib/pangea/events/event_wrappers/pangea_message_event.dart @@ -11,6 +11,7 @@ import 'package:sentry_flutter/sentry_flutter.dart'; import 'package:fluffychat/pangea/choreographer/choreo_record_model.dart'; import 'package:fluffychat/pangea/common/constants/model_keys.dart'; +import 'package:fluffychat/pangea/common/models/llm_feedback_model.dart'; import 'package:fluffychat/pangea/events/event_wrappers/pangea_representation_event.dart'; import 'package:fluffychat/pangea/events/extensions/pangea_event_extension.dart'; import 'package:fluffychat/pangea/events/models/representation_content_model.dart'; @@ -31,6 +32,7 @@ import 'package:fluffychat/pangea/text_to_speech/text_to_speech_response_model.d import 'package:fluffychat/pangea/toolbar/message_practice/message_audio_card.dart'; import 'package:fluffychat/pangea/translation/full_text_translation_repo.dart'; import 'package:fluffychat/pangea/translation/full_text_translation_request_model.dart'; +import 'package:fluffychat/pangea/translation/full_text_translation_response_model.dart'; import 'package:fluffychat/widgets/future_loading_dialog.dart'; import '../../../widgets/matrix.dart'; import '../../common/utils/error_handler.dart'; @@ -505,7 +507,7 @@ class PangeaMessageEvent { } final translation = SttTranslationModel( - translation: res.result!, + translation: res.result!.bestTranslation, langCode: l1Code, ); @@ -544,44 +546,70 @@ class PangeaMessageEvent { return _sendRepresentationEvent(res.result!); } - Future requestRespresentationByL1() async { + Future requestTranslationByL1({ + List>? feedback, + }) async { if (_l1Code == null || _l2Code == null) { throw Exception("Missing language codes"); } - final includedIT = - (originalSent?.choreo?.endedWithIT(originalSent!.text) ?? false) && - !(originalSent?.choreo?.includedIGC ?? true); - - RepresentationEvent? rep; - if (!includedIT) { - // if the message didn't go through translation, get any l1 rep - rep = _representationByLanguage(_l1Code!); - } else { - // if the message went through translation, get the non-original - // l1 rep since originalWritten could contain some l2 words - // (https://github.com/pangeachat/client/issues/3591) - rep = _representationByLanguage( - _l1Code!, - filter: (rep) => !rep.content.originalWritten, - ); + if (feedback == null) { + final includedIT = + originalSent?.choreo?.endedWithIT(originalSent!.text) == true; + RepresentationEvent? rep; + if (!includedIT) { + // if the message didn't go through translation, get any l1 rep + rep = _representationByLanguage(_l1Code!); + } else { + // if the message went through translation, get the non-original + // l1 rep since originalWritten could contain some l2 words + // (https://github.com/pangeachat/client/issues/3591) + rep = _representationByLanguage( + _l1Code!, + filter: (rep) => !rep.content.originalWritten, + ); + } + if (rep != null) { + return FullTextTranslationResponseModel( + translation: rep.text, + translations: [rep.text], + source: messageDisplayLangCode, + ); + } } - if (rep != null) return rep.content.text; + final includedIT = + originalSent?.choreo?.endedWithIT(originalSent!.text) == true; final String srcLang = includedIT ? (originalWritten?.langCode ?? _l1Code!) : (originalSent?.langCode ?? _l2Code!); - final resp = await _requestRepresentation( - includedIT ? originalWrittenContent : messageDisplayText, - _l1Code!, - srcLang, + final text = includedIT ? originalWrittenContent : messageDisplayText; + final resp = await FullTextTranslationRepo.get( + MatrixState.pangeaController.userController.accessToken, + FullTextTranslationRequestModel( + text: text, + srcLang: srcLang, + tgtLang: _l1Code!, + userL2: + MatrixState.pangeaController.userController.userL2Code ?? + LanguageKeys.unknownLanguage, + userL1: _l1Code!, + feedback: feedback, + ), ); if (resp.isError) throw resp.error!; - _sendRepresentationEvent(resp.result!); - return resp.result!.text; + _sendRepresentationEvent( + PangeaRepresentation( + langCode: _l1Code!, + text: resp.result!.bestTranslation, + originalSent: false, + originalWritten: false, + ), + ); + return resp.result!; } Future> _requestRepresentation( @@ -589,6 +617,7 @@ class PangeaMessageEvent { String targetLang, String sourceLang, { bool originalSent = false, + List>? feedback, }) async { _representations = null; @@ -600,6 +629,7 @@ class PangeaMessageEvent { tgtLang: targetLang, userL2: _l2Code ?? LanguageKeys.unknownLanguage, userL1: _l1Code ?? LanguageKeys.unknownLanguage, + feedback: feedback, ), ); @@ -608,7 +638,7 @@ class PangeaMessageEvent { : Result.value( PangeaRepresentation( langCode: targetLang, - text: res.result!, + text: res.result!.bestTranslation, originalSent: originalSent, originalWritten: false, ), diff --git a/lib/pangea/toolbar/layout/overlay_message.dart b/lib/pangea/toolbar/layout/overlay_message.dart index f50a096ee..3c4093e94 100644 --- a/lib/pangea/toolbar/layout/overlay_message.dart +++ b/lib/pangea/toolbar/layout/overlay_message.dart @@ -12,6 +12,7 @@ import 'package:fluffychat/pages/chat/events/message_content.dart'; import 'package:fluffychat/pages/chat/events/reply_content.dart'; import 'package:fluffychat/pangea/common/utils/async_state.dart'; import 'package:fluffychat/pangea/common/widgets/error_indicator.dart'; +import 'package:fluffychat/pangea/common/widgets/feedback_dialog.dart'; import 'package:fluffychat/pangea/events/extensions/pangea_event_extension.dart'; import 'package:fluffychat/pangea/events/models/pangea_token_model.dart'; import 'package:fluffychat/pangea/toolbar/layout/reading_assistance_mode_enum.dart'; @@ -300,6 +301,7 @@ class OverlayMessage extends StatelessWidget { controller: selectModeController, style: style, maxWidth: maxWidth, + minWidth: messageWidth ?? 0, ), ], ), @@ -313,13 +315,31 @@ class _MessageSelectModeContent extends StatelessWidget { final SelectModeController controller; final TextStyle style; final double maxWidth; + final double minWidth; const _MessageSelectModeContent({ required this.controller, required this.style, required this.maxWidth, + required this.minWidth, }); + Future onFlagTranslation(BuildContext context) async { + final resp = await showDialog( + context: context, + builder: (context) => FeedbackDialog( + title: L10n.of(context).translationFeedback, + onSubmit: (feedback) => Navigator.of(context).pop(feedback), + ), + ); + + if (resp == null || resp.isEmpty) { + return; + } + + await controller.fetchTranslation(feedback: resp); + } + @override Widget build(BuildContext context) { return ListenableBuilder( @@ -356,8 +376,13 @@ class _MessageSelectModeContent extends StatelessWidget { ? controller.translationState.value : controller.speechTranslationState.value; - return Padding( + return Container( padding: const EdgeInsets.all(12.0), + constraints: BoxConstraints( + minHeight: 40.0, + maxWidth: maxWidth, + minWidth: minWidth, + ), child: switch (state) { AsyncLoading() => Row( mainAxisSize: MainAxisSize.min, @@ -371,15 +396,28 @@ class _MessageSelectModeContent extends StatelessWidget { message: L10n.of(context).translationError, style: style.copyWith(fontStyle: FontStyle.italic), ), - AsyncLoaded(value: final value) => Container( - constraints: BoxConstraints(maxWidth: maxWidth), - child: SingleChildScrollView( - child: Text( - value, - textScaler: TextScaler.noScaling, - style: style.copyWith(fontStyle: FontStyle.italic), + AsyncLoaded(value: final value) => Row( + spacing: 8.0, + mainAxisSize: .min, + mainAxisAlignment: .spaceBetween, + children: [ + Flexible( + child: Text( + value, + textScaler: TextScaler.noScaling, + style: style.copyWith(fontStyle: FontStyle.italic), + ), ), - ), + if (mode == SelectMode.translate) + InkWell( + onTap: () => onFlagTranslation(context), + child: Icon( + Icons.flag_outlined, + color: style.color, + size: 16.0, + ), + ), + ], ), _ => const SizedBox(), }, diff --git a/lib/pangea/toolbar/reading_assistance/select_mode_controller.dart b/lib/pangea/toolbar/reading_assistance/select_mode_controller.dart index b748e4bc0..9f2eb40ec 100644 --- a/lib/pangea/toolbar/reading_assistance/select_mode_controller.dart +++ b/lib/pangea/toolbar/reading_assistance/select_mode_controller.dart @@ -7,12 +7,14 @@ import 'package:matrix/matrix.dart'; import 'package:path_provider/path_provider.dart'; import 'package:fluffychat/pangea/analytics_misc/lemma_emoji_setter_mixin.dart'; +import 'package:fluffychat/pangea/common/models/llm_feedback_model.dart'; import 'package:fluffychat/pangea/common/utils/async_state.dart'; import 'package:fluffychat/pangea/events/event_wrappers/pangea_message_event.dart'; import 'package:fluffychat/pangea/events/models/pangea_token_text_model.dart'; import 'package:fluffychat/pangea/speech_to_text/speech_to_text_response_model.dart'; import 'package:fluffychat/pangea/toolbar/message_practice/message_audio_card.dart'; import 'package:fluffychat/pangea/toolbar/reading_assistance/select_mode_buttons.dart'; +import 'package:fluffychat/pangea/translation/full_text_translation_response_model.dart'; import 'package:fluffychat/widgets/matrix.dart'; class _TranscriptionLoader extends AsyncLoader { @@ -38,14 +40,6 @@ class _STTTranslationLoader extends AsyncLoader { ); } -class _TranslationLoader extends AsyncLoader { - final PangeaMessageEvent messageEvent; - _TranslationLoader(this.messageEvent) : super(); - - @override - Future fetch() => messageEvent.requestRespresentationByL1(); -} - class _AudioLoader extends AsyncLoader<(PangeaAudioFile, File?)> { final PangeaMessageEvent messageEvent; _AudioLoader(this.messageEvent) : super(); @@ -71,21 +65,26 @@ class _AudioLoader extends AsyncLoader<(PangeaAudioFile, File?)> { } } +typedef _TranslationLoader = ValueNotifier>; + class SelectModeController with LemmaEmojiSetter { final PangeaMessageEvent messageEvent; final _TranscriptionLoader _transcriptLoader; final _TranslationLoader _translationLoader; + final _AudioLoader _audioLoader; final _STTTranslationLoader _sttTranslationLoader; SelectModeController(this.messageEvent) : _transcriptLoader = _TranscriptionLoader(messageEvent), - _translationLoader = _TranslationLoader(messageEvent), + _translationLoader = _TranslationLoader(AsyncIdle()), _audioLoader = _AudioLoader(messageEvent), _sttTranslationLoader = _STTTranslationLoader(messageEvent); ValueNotifier selectedMode = ValueNotifier(null); + FullTextTranslationResponseModel? _lastTranslationResponse; + // Sometimes the same token is clicked twice. Setting it to the same value // won't trigger the notifier, so use the bool for force it to trigger. ValueNotifier<(PangeaTokenText?, bool)> playTokenNotifier = @@ -109,8 +108,7 @@ class SelectModeController with LemmaEmojiSetter { static List get _audioModes => [SelectMode.speechTranslation]; - ValueNotifier> get translationState => - _translationLoader.state; + ValueNotifier> get translationState => _translationLoader; ValueNotifier> get transcriptionState => _transcriptLoader.state; @@ -179,7 +177,7 @@ class SelectModeController with LemmaEmojiSetter { bool get isShowingExtraContent => (selectedMode.value == SelectMode.translate && - _translationLoader.isLoaded) || + _translationLoader.value is AsyncLoaded) || (selectedMode.value == SelectMode.speechTranslation && _sttTranslationLoader.isLoaded) || _transcriptLoader.isLoaded || @@ -191,7 +189,7 @@ class SelectModeController with LemmaEmojiSetter { ValueNotifier? modeStateNotifier(SelectMode? mode) => switch (mode) { SelectMode.audio => _audioLoader.state, - SelectMode.translate => _translationLoader.state, + SelectMode.translate => _translationLoader, SelectMode.speechTranslation => _sttTranslationLoader.state, _ => null, }; @@ -205,7 +203,32 @@ class SelectModeController with LemmaEmojiSetter { playTokenNotifier.value = (token, !playTokenNotifier.value.$2); Future fetchAudio() => _audioLoader.load(); - Future fetchTranslation() => _translationLoader.load(); Future fetchTranscription() => _transcriptLoader.load(); Future fetchSpeechTranslation() => _sttTranslationLoader.load(); + + Future fetchTranslation({String? feedback}) async { + try { + _translationLoader.value = AsyncLoading(); + + List>? feedbackModel; + if (feedback != null && _lastTranslationResponse != null) { + feedbackModel = [ + LLMFeedbackModel( + feedback: feedback, + content: _lastTranslationResponse!, + contentToJson: (c) => c.toJson(), + ), + ]; + } + + final resp = await messageEvent.requestTranslationByL1( + feedback: feedbackModel, + ); + + _lastTranslationResponse = resp; + _translationLoader.value = AsyncLoaded(resp.bestTranslation); + } catch (e) { + _translationLoader.value = AsyncError(e.toString()); + } + } } diff --git a/lib/pangea/translation/full_text_translation_repo.dart b/lib/pangea/translation/full_text_translation_repo.dart index 4fbb5dd63..6ff80465d 100644 --- a/lib/pangea/translation/full_text_translation_repo.dart +++ b/lib/pangea/translation/full_text_translation_repo.dart @@ -14,7 +14,7 @@ import '../common/network/requests.dart'; import '../common/network/urls.dart'; class _TranslateCacheItem { - final Future response; + final Future response; final DateTime timestamp; const _TranslateCacheItem({required this.response, required this.timestamp}); @@ -24,7 +24,7 @@ class FullTextTranslationRepo { static final Map _cache = {}; static const Duration _cacheDuration = Duration(minutes: 10); - static Future> get( + static Future> get( String accessToken, FullTextTranslationRequestModel request, ) { @@ -38,7 +38,7 @@ class FullTextTranslationRepo { return _getResult(request, future); } - static Future _fetch( + static Future _fetch( String accessToken, FullTextTranslationRequestModel request, ) async { @@ -60,12 +60,12 @@ class FullTextTranslationRepo { return FullTextTranslationResponseModel.fromJson( jsonDecode(utf8.decode(res.bodyBytes)), - ).bestTranslation; + ); } - static Future> _getResult( + static Future> _getResult( FullTextTranslationRequestModel request, - Future future, + Future future, ) async { try { final res = await future; @@ -77,7 +77,9 @@ class FullTextTranslationRepo { } } - static Future? _getCached(FullTextTranslationRequestModel request) { + static Future? _getCached( + FullTextTranslationRequestModel request, + ) { final cacheKeys = [..._cache.keys]; for (final key in cacheKeys) { if (DateTime.now().difference(_cache[key]!.timestamp) >= _cacheDuration) { @@ -90,7 +92,7 @@ class FullTextTranslationRepo { static void _setCached( FullTextTranslationRequestModel request, - Future response, + Future response, ) => _cache[request.hashCode.toString()] = _TranslateCacheItem( response: response, timestamp: DateTime.now(), diff --git a/lib/pangea/translation/full_text_translation_request_model.dart b/lib/pangea/translation/full_text_translation_request_model.dart index c14fbc9a8..c91b6664e 100644 --- a/lib/pangea/translation/full_text_translation_request_model.dart +++ b/lib/pangea/translation/full_text_translation_request_model.dart @@ -1,4 +1,8 @@ +import 'package:collection/collection.dart'; + import 'package:fluffychat/pangea/common/constants/model_keys.dart'; +import 'package:fluffychat/pangea/common/models/llm_feedback_model.dart'; +import 'package:fluffychat/pangea/translation/full_text_translation_response_model.dart'; class FullTextTranslationRequestModel { final String text; @@ -9,6 +13,7 @@ class FullTextTranslationRequestModel { final bool? deepL; final int? offset; final int? length; + final List>? feedback; const FullTextTranslationRequestModel({ required this.text, @@ -19,6 +24,7 @@ class FullTextTranslationRequestModel { this.deepL = false, this.offset, this.length, + this.feedback, }); Map toJson() => { @@ -30,6 +36,8 @@ class FullTextTranslationRequestModel { ModelKey.deepL: deepL, ModelKey.offset: offset, ModelKey.length: length, + if (feedback != null) + ModelKey.feedback: feedback!.map((f) => f.toJson()).toList(), }; // override equals and hashcode @@ -45,7 +53,8 @@ class FullTextTranslationRequestModel { other.userL1 == userL1 && other.deepL == deepL && other.offset == offset && - other.length == length; + other.length == length && + ListEquality().equals(other.feedback, feedback); } @override @@ -57,5 +66,6 @@ class FullTextTranslationRequestModel { userL1.hashCode ^ deepL.hashCode ^ offset.hashCode ^ - length.hashCode; + length.hashCode ^ + ListEquality().hash(feedback); } diff --git a/lib/pangea/translation/full_text_translation_response_model.dart b/lib/pangea/translation/full_text_translation_response_model.dart index a46f650cc..90ed61982 100644 --- a/lib/pangea/translation/full_text_translation_response_model.dart +++ b/lib/pangea/translation/full_text_translation_response_model.dart @@ -2,25 +2,31 @@ import 'package:fluffychat/pangea/common/constants/model_keys.dart'; class FullTextTranslationResponseModel { final List translations; + final String translation; final String source; - final String? deepL; const FullTextTranslationResponseModel({ + required this.translation, required this.translations, required this.source, - required this.deepL, }); factory FullTextTranslationResponseModel.fromJson(Map json) { return FullTextTranslationResponseModel( + translation: json['translation'] as String, translations: (json["translations"] as Iterable) .map((e) => e) .toList() .cast(), source: json[ModelKey.srcLang], - deepL: json['deepl_res'], ); } - String get bestTranslation => deepL ?? translations.first; + Map toJson() => { + 'translation': translation, + 'translations': translations, + ModelKey.srcLang: source, + }; + + String get bestTranslation => translation; }