feat(ui, localization, samples): message translations - #2870
Conversation
… available translation
Automatically detects when Message.i18n already has a translation for the
current user's language (either from server-side auto-translation or an
earlier on-request translate call) and surfaces it as a message annotation,
with a tappable link to toggle back to the original text.
- DefaultStreamMessageHeader renders the annotation last in the mixed-
annotation priority order, per the design system's Figma spec, and names
the message's original language when known.
- The show-original toggle lives in a new StreamMessageTranslationStore,
scoped per StreamMessageListView, so it survives list virtualization
instead of resetting when a message widget scrolls out of view and back.
- Gated behind a new translationDisplayEnabled config flag (default true)
for apps that want to opt out entirely.
- Adds the "Translated"/"Original"/"Show original"/"Show translation"/
"Translated from {language}" strings, plus a curated language-code to
display-name table, to all 11 bundled locales.
…n message previews Stream's API defaults User.language to '' rather than omitting it, so the previous `language ?? currentUser?.language ?? 'en'` fallback never reached its 'en' default and previews silently rendered untranslated. Falls back to the device locale instead, matching the behavior already used elsewhere for message text and headers.
Adds a Translate action to the message long-press menu, gated behind a new "Message Translation" toggle in the sample app's configuration screen (disabled by default). Calls channel.translateMessage and merges the result into local channel state, so the SDK's own translation annotation and message rendering pick it up automatically. Only shown for received messages — translating your own message isn't useful — and always translates directly to the user's language (falling back to the device locale), with no language picker. Disabled once the message is already translated to that language, or the original text is already in it.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (5)
🚧 Files skipped from review as they are similar to previous changes (3)
📝 WalkthroughWalkthroughThe PR adds automatic translated-message display, localized translation annotations, per-message original-text toggles, a global SDK display setting, and a sample-app translation action. ChangesMessage translation
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant MessageListView
participant StreamMessageTranslationStore
participant DefaultStreamMessageItem
participant StreamMessageHeader
participant StreamMessageText
MessageListView->>StreamMessageTranslationStore: provide per-list translation state
DefaultStreamMessageItem->>StreamMessageTranslationStore: read message original-text state
DefaultStreamMessageItem->>StreamMessageHeader: pass toggle state and callback
DefaultStreamMessageItem->>StreamMessageText: pass original-text state
StreamMessageHeader->>StreamMessageTranslationStore: toggle original text
StreamMessageText-->>DefaultStreamMessageItem: render translated or original text
Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches 💡 1🛠️ Fix failing CI checks 💡
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
StreamMessagePreviewText translated unconditionally, unlike StreamMessageText which already gates translation behind translationDisplayEnabled. This left the channel list, thread list, search results, quoted messages, and the reply-preview header showing translated text even when the flag was disabled SDK-wide.
The comment attributed the key to protecting a translation show-original toggle, which moved into StreamMessageTranslationStore (keyed externally by message id) and no longer needs it. The key is still required though: other per-message StatefulWidgets in this subtree (content, attachments, leading) would otherwise have their State reused for the wrong message when pagination prepends older messages and shifts every already-rendered item's index.
…n toggle The long-press message-actions modal re-renders the message in a fresh StreamMessageItem on top of the scrim, outside the StreamMessageListView subtree that owns the StreamMessageTranslationStore. Without re-providing that scope, the preview always showed the translation regardless of the list's toggle state, and its "Show original"/"Show translation" link had no effect. Captures the ambient store (if any) before opening the dialog and re-provides it around the nested StreamMessageItem, matching how StreamChatConfiguration and StreamChannel are already re-provided for the same modal.
StreamThreadListTile passed currentUser?.language straight through to StreamMessagePreviewText's language parameter, bypassing that widget's own null-or-empty-string fallback to the device locale. Stream's API defaults User.language to '' rather than omitting it, so thread previews silently stopped translating for exactly the case the preview-text fix already handled elsewhere. Also extracts the "null or empty string falls back to the device locale" logic into a single UserX.languageOrDeviceLocale extension. It was previously duplicated as two identical _resolveLanguage private functions (stream_message_header.dart, stream_message_text.dart) plus two more inlined switch expressions (stream_message_preview_text.dart, the sample app's custom_message_actions.dart) — all doing the same thing.
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In
`@packages/stream_chat_localizations/lib/src/stream_chat_localizations_es.dart`:
- Around line 812-813: Update translatedFromLanguageText to use the contracted
Spanish preposition “del” before the interpolated source-language name, so the
resulting label reads “Traducido del Inglés” while preserving the existing
_languageNameEs(languageCode) interpolation.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: ac6ac58c-047a-4d69-9905-6c2f42ccf61f
📒 Files selected for processing (28)
packages/stream_chat_flutter/CHANGELOG.mdpackages/stream_chat_flutter/lib/src/channel/stream_message_preview_text.dartpackages/stream_chat_flutter/lib/src/localization/translations.dartpackages/stream_chat_flutter/lib/src/message_list_view/message_list_view.dartpackages/stream_chat_flutter/lib/src/message_widget/components/stream_message_content.dartpackages/stream_chat_flutter/lib/src/message_widget/components/stream_message_header.dartpackages/stream_chat_flutter/lib/src/message_widget/components/stream_message_text.dartpackages/stream_chat_flutter/lib/src/message_widget/stream_message_item.dartpackages/stream_chat_flutter/lib/src/message_widget/stream_message_translation_store.dartpackages/stream_chat_flutter/lib/src/scroll_view/thread_scroll_view/stream_thread_list_tile.dartpackages/stream_chat_flutter/lib/src/stream_chat_configuration.dartpackages/stream_chat_flutter/lib/src/utils/extensions.dartpackages/stream_chat_flutter/lib/stream_chat_flutter.dartpackages/stream_chat_flutter/test/src/channel/stream_message_preview_text_test.dartpackages/stream_chat_localizations/lib/src/stream_chat_localizations_ca.dartpackages/stream_chat_localizations/lib/src/stream_chat_localizations_de.dartpackages/stream_chat_localizations/lib/src/stream_chat_localizations_en.dartpackages/stream_chat_localizations/lib/src/stream_chat_localizations_es.dartpackages/stream_chat_localizations/lib/src/stream_chat_localizations_fr.dartpackages/stream_chat_localizations/lib/src/stream_chat_localizations_hi.dartpackages/stream_chat_localizations/lib/src/stream_chat_localizations_it.dartpackages/stream_chat_localizations/lib/src/stream_chat_localizations_ja.dartpackages/stream_chat_localizations/lib/src/stream_chat_localizations_ko.dartpackages/stream_chat_localizations/lib/src/stream_chat_localizations_no.dartpackages/stream_chat_localizations/lib/src/stream_chat_localizations_pt.dartsample_app/lib/config/sample_app_config.dartsample_app/lib/config/sample_app_config_screen.dartsample_app/lib/widgets/custom_message_actions.dart
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## master #2870 +/- ##
==========================================
- Coverage 73.18% 72.93% -0.26%
==========================================
Files 429 430 +1
Lines 27724 27866 +142
==========================================
+ Hits 20289 20323 +34
- Misses 7435 7543 +108 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/stream_chat_localizations/example/lib/add_new_lang.dart`:
- Around line 818-831: Add `///` documentation comments immediately above each
public localization member in the generated implementation: `translatedLabel`,
`originalLabel`, `showOriginalLabel`, `showTranslationLabel`, and
`translatedFromLanguageText`. Document the purpose of each label and the
language-code interpolation method without changing their existing values or
signatures.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: d82c927f-18b2-4760-a112-4e60b5f23d35
📒 Files selected for processing (1)
packages/stream_chat_localizations/example/lib/add_new_lang.dart
| @override | ||
| String get translatedLabel => 'Translated'; | ||
|
|
||
| @override | ||
| String get originalLabel => 'Original'; | ||
|
|
||
| @override | ||
| String get showOriginalLabel => 'Show original'; | ||
|
|
||
| @override | ||
| String get showTranslationLabel => 'Show translation'; | ||
|
|
||
| @override | ||
| String translatedFromLanguageText(String languageCode) => 'Translated from ${languageCode.toUpperCase()}'; |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win
Add doc comments for the new public localization APIs.
translatedLabel, originalLabel, showOriginalLabel, showTranslationLabel, and translatedFromLanguageText are public members. Add a /// comment for each declaration.
Proposed documentation
+ /// The annotation label shown for translated message content.
`@override`
String get translatedLabel => 'Translated';
+ /// The annotation label shown for original message content.
`@override`
String get originalLabel => 'Original';
+ /// The label that switches a message to its original text.
`@override`
String get showOriginalLabel => 'Show original';
+ /// The label that switches a message to its translated text.
`@override`
String get showTranslationLabel => 'Show translation';
+ /// Returns the translated-message label for the source language code.
`@override`
String translatedFromLanguageText(String languageCode) =>📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| @override | |
| String get translatedLabel => 'Translated'; | |
| @override | |
| String get originalLabel => 'Original'; | |
| @override | |
| String get showOriginalLabel => 'Show original'; | |
| @override | |
| String get showTranslationLabel => 'Show translation'; | |
| @override | |
| String translatedFromLanguageText(String languageCode) => 'Translated from ${languageCode.toUpperCase()}'; | |
| /// The annotation label shown for translated message content. | |
| `@override` | |
| String get translatedLabel => 'Translated'; | |
| /// The annotation label shown for original message content. | |
| `@override` | |
| String get originalLabel => 'Original'; | |
| /// The label that switches a message to its original text. | |
| `@override` | |
| String get showOriginalLabel => 'Show original'; | |
| /// The label that switches a message to its translated text. | |
| `@override` | |
| String get showTranslationLabel => 'Show translation'; | |
| /// Returns the translated-message label for the source language code. | |
| `@override` | |
| String translatedFromLanguageText(String languageCode) => 'Translated from ${languageCode.toUpperCase()}'; |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/stream_chat_localizations/example/lib/add_new_lang.dart` around
lines 818 - 831, Add `///` documentation comments immediately above each public
localization member in the generated implementation: `translatedLabel`,
`originalLabel`, `showOriginalLabel`, `showTranslationLabel`, and
`translatedFromLanguageText`. Document the purpose of each label and the
language-code interpolation method without changing their existing values or
signatures.
Source: Coding guidelines
There was a problem hiding this comment.
The doc comments for translatedLabel, originalLabel, showOriginalLabel, showTranslationLabel, and translatedFromLanguageText already exist on the abstract interface (stream_chat_flutter/lib/src/localization/translations.dart:730-756). Every concrete implementation in the repo — the base DefaultTranslations, all locale files in stream_chat_localizations/lib/src/, and this example file — consistently omits doc comments on @override members and relies on doc inheritance. Adding comments only in the example file would be inconsistent with the established convention, so I left it as-is.
| /// available translations. | ||
| /// | ||
| /// Defaults to `true`. | ||
| final bool translationDisplayEnabled; |
There was a problem hiding this comment.
Something maybe to consider: On Android, this config is two-part:
public data class TranslationConfig(
val enabled: Boolean = true,
val showOriginalEnabled: Boolean = true,
)TranslationConfig.enabled-> controls whether the message automatically displays its translationTranslationConfig.showOriginalEnabled-> controls whether theShow originalis toggled.
It was implemented like this because the features came at a different time and to avoid breaking changes - but maybe it makes sense if a customer would like to use auto translation, but disable the "Show original" toggle.
Again, just a suggestion, since the whole feature is new, we can keep it simple.
There was a problem hiding this comment.
I wonder why you would ever want to hide the show original button? I can only imagine that we want 2 toggles for showing translations at all and showing the translated message automatically. Or is that what is meant with showOriginalEnabled?
There was a problem hiding this comment.
Yes the showOriginalEnabled controls whether the "Show original"/"Show translated" toggle is shown in the message view.
On Android the "Show original" feature request came much later compared to the "Auto translation". So in order to be backwards compatible, we hid the "Show original" button by default - and customers can opt-in via the config to actually show the toggle (this has now changed with the major v7, and both flags are true by default, but they still exist separately).
Now that we are introducing "both" features as a package - maybe it doesn't make sense to have them configured separately. But still, I think we should consider the possibility of a customer not wanting to show the "Show original" toggle, even when they use auto-translation.
There was a problem hiding this comment.
In that case maybe it's better to name it showTranslationAnnotation as it's also hiding 'show translation'. But we do want to have less configuration options and people now have a slot to manage their annotations directly, so I think it's better to write a doc on how to manage your own annotations.
There was a problem hiding this comment.
Yeah I like that naming better. One thing that I can suggest is still wrapping the translationDisplayEnabled flag into a config object, something along the lines of:
class StreamTranslationsConfiguration {
final bool translationDisplayEnabled;
}So we can easily extend it (potentially with showTranslationAnnotation) without breaking changes, and without polluting the StreamChatConfiguration with multiple translations-related fields.
But we do want to have less configuration options and people now have a slot to manage their annotations directly
I am still a bit worried about having the "Show original/translation" displayed by default - mostly because of the reason I mentioned earlier about showing "double" labels (one from the SDK, and one from the integrator app).
There was a problem hiding this comment.
I personally don't like the name translationDisplayEnabled i think showTranslationAnnotation is better suited here
There was a problem hiding this comment.
But I would still prefer not to have this setting at all, but just explain how to disable using the header builder of the message in the factory.
| /// Returns the message unchanged when [language] is `null` or empty — | ||
| /// Stream's API defaults [User.language] to `''` rather than omitting it, | ||
| /// so both are treated as "no language to translate to". | ||
| Message translate(String? language) => switch (language) { |
There was a problem hiding this comment.
Perhaps we can add/extend the unit test for this?
| final icons = context.streamIcons; | ||
| return [ | ||
| StreamContextMenuAction( | ||
| label: const Text('Translate'), |
There was a problem hiding this comment.
| label: const Text('Translate'), | |
| label: const Text('Translate Message'), |
| /// isn't a useful action — and always translates directly to the current | ||
| /// user's [User.language], with no language picker. Hidden entirely when | ||
| /// that isn't set server-side, rather than guessing a target language. | ||
| abstract final class _TranslateAction { |
There was a problem hiding this comment.
| abstract final class _TranslateAction { | |
| abstract final class _TranslateMessageAction { |
| ]; | ||
| } | ||
|
|
||
| static Future<void> _translate( |
There was a problem hiding this comment.
| static Future<void> _translate( | |
| static Future<void> _translateMessage( |
| channel.state?.updateMessage(response.message); | ||
| } catch (e) { | ||
| if (!context.mounted) return; | ||
| ScaffoldMessenger.of(context).showSnackBar( |
There was a problem hiding this comment.
Let's use our StreamSnackbar instead
| /// the individual message widget, is what makes the toggle survive message | ||
| /// items being disposed and recreated as they scroll out of and back into | ||
| /// the list's render window. | ||
| class StreamMessageTranslationStore extends ChangeNotifier { |
There was a problem hiding this comment.
we can use a value notifier here
Submit a pull request
Linear: Fixes FLU-673
CLA
Description of the pull request
The current SDK already supports translating messages, but the UI package misses crucial features. This PR adds the annotations from the new design system that shows the fact that it's translated and you can switch between original and translated message. The SDK also adds a flag
StreamChatConfigurationData.translationDisplayEnabledso you can also disable auto translated messages.The sample app adds a button in the message actions so you can request a translation, as the sdk never automatically translate messages, although the backend can do that for you. The button to translate is disabled if the message is already translated to your language.
Screenshots / Videos
Summary by CodeRabbit
New Features
Bug Fixes