From c9f69fed2ca68d0ea608f9df646897e0635040a0 Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Sun, 16 Mar 2025 23:24:36 +0800 Subject: [PATCH] :sparkles: Complete translation --- assets/translations/en-US.json | 5 +- assets/translations/zh-CN.json | 5 +- assets/translations/zh-HK.json | 5 +- assets/translations/zh-TW.json | 5 +- lib/providers/config.dart | 10 +++ lib/screens/settings.dart | 12 ++++ lib/widgets/chat/chat_message.dart | 28 ++++++++ lib/widgets/post/post_item.dart | 103 ++++++++++++++++++++--------- 8 files changed, 136 insertions(+), 37 deletions(-) diff --git a/assets/translations/en-US.json b/assets/translations/en-US.json index 960c537..3fc0d2c 100644 --- a/assets/translations/en-US.json +++ b/assets/translations/en-US.json @@ -843,5 +843,8 @@ "accountContactMethodsDeleteDescription": "Are you sure you want to delete contact method {}? This operation is irreversible.", "postCommentAdd": "Write a comment", "translate": "Translate", - "translated": "Translated" + "translating": "Translating…", + "translated": "Translated", + "settingsAutoTranslate": "Auto Translate", + "settingsAutoTranslateDescription": "Automatically translate text when viewing posts and messages." } diff --git a/assets/translations/zh-CN.json b/assets/translations/zh-CN.json index 87859a5..1b24440 100644 --- a/assets/translations/zh-CN.json +++ b/assets/translations/zh-CN.json @@ -841,5 +841,8 @@ "accountContactMethodsDeleteDescription": "你确定要删除联系方式 {} 吗?这个操作不可撤销。", "postCommentAdd": "撰写一条评论", "translate": "翻译", - "translated": "已翻译" + "translating": "正在翻译……", + "translated": "已翻译", + "settingsAutoTranslate": "自动翻译", + "settingsAutoTranslateDescription": "在查看帖子、消息时自动翻译文本。" } diff --git a/assets/translations/zh-HK.json b/assets/translations/zh-HK.json index 28dbdba..5c7fbe6 100644 --- a/assets/translations/zh-HK.json +++ b/assets/translations/zh-HK.json @@ -841,5 +841,8 @@ "accountContactMethodsDeleteDescription": "你確定要刪除聯繫方式 {} 嗎?這個操作不可撤銷。", "postCommentAdd": "撰寫一條評論", "translate": "翻譯", - "translated": "已翻譯" + "translating": "正在翻譯……", + "translated": "已翻譯", + "settingsAutoTranslate": "自動翻譯", + "settingsAutoTranslateDescription": "在查看帖子、消息時自動翻譯文本。" } diff --git a/assets/translations/zh-TW.json b/assets/translations/zh-TW.json index 115b828..43a6003 100644 --- a/assets/translations/zh-TW.json +++ b/assets/translations/zh-TW.json @@ -841,5 +841,8 @@ "accountContactMethodsDeleteDescription": "你確定要刪除聯繫方式 {} 嗎?這個操作不可撤銷。", "postCommentAdd": "撰寫一條評論", "translate": "翻譯", - "translated": "已翻譯" + "translating": "正在翻譯……", + "translated": "已翻譯", + "settingsAutoTranslate": "自動翻譯", + "settingsAutoTranslateDescription": "在查看帖子、消息時自動翻譯文本。" } diff --git a/lib/providers/config.dart b/lib/providers/config.dart index 7d4a502..374dcfb 100644 --- a/lib/providers/config.dart +++ b/lib/providers/config.dart @@ -20,6 +20,7 @@ const kAppExpandChatLink = 'app_expand_chat_link'; const kAppRealmCompactView = 'app_realm_compact_view'; const kAppCustomFonts = 'app_custom_fonts'; const kAppMixedFeed = 'app_mixed_feed'; +const kAppAutoTranslate = 'app_auto_translate'; const Map kImageQualityLevel = { 'settingsImageQualityLowest': FilterQuality.none, @@ -86,6 +87,15 @@ class ConfigProvider extends ChangeNotifier { return prefs.getBool(kAppMixedFeed) ?? true; } + bool get autoTranslate { + return prefs.getBool(kAppAutoTranslate) ?? false; + } + + set autoTranslate(bool value) { + prefs.setBool(kAppAutoTranslate, value); + notifyListeners(); + } + set mixedFeed(bool value) { prefs.setBool(kAppMixedFeed, value); notifyListeners(); diff --git a/lib/screens/settings.dart b/lib/screens/settings.dart index 1468de7..ac2533c 100644 --- a/lib/screens/settings.dart +++ b/lib/screens/settings.dart @@ -387,6 +387,18 @@ class _SettingsScreenState extends State { .fontSize(17) .tr() .padding(horizontal: 20, bottom: 4), + CheckboxListTile( + secondary: const Icon(Symbols.translate), + contentPadding: const EdgeInsets.only(left: 24, right: 17), + title: Text('settingsAutoTranslate').tr(), + subtitle: Text('settingsAutoTranslateDescription').tr(), + value: _prefs.getBool(kAppAutoTranslate) ?? false, + onChanged: (value) { + setState(() { + _prefs.setBool(kAppAutoTranslate, value ?? false); + }); + }, + ), CheckboxListTile( secondary: const Icon(Symbols.vibration), contentPadding: const EdgeInsets.only(left: 24, right: 17), diff --git a/lib/widgets/chat/chat_message.dart b/lib/widgets/chat/chat_message.dart index e10575c..5585fe9 100644 --- a/lib/widgets/chat/chat_message.dart +++ b/lib/widgets/chat/chat_message.dart @@ -246,9 +246,14 @@ class _ChatMessageText extends StatefulWidget { class _ChatMessageTextState extends State<_ChatMessageText> { late String _displayText = widget.data.body['text'] ?? ''; bool _isTranslated = false; + bool _isTranslating = false; Future _translateText() async { + if (widget.data.body['text'] == null || widget.data.body['text']!.isEmpty) { + return; + } final ta = context.read(); + setState(() => _isTranslating = true); try { final to = EasyLocalization.of(context)!.locale.languageCode; _displayText = await ta.translate( @@ -259,6 +264,19 @@ class _ChatMessageTextState extends State<_ChatMessageText> { if (mounted) setState(() {}); } catch (err) { if (mounted) context.showErrorDialog(err); + } finally { + setState(() => _isTranslating = false); + } + } + + @override + void initState() { + super.initState(); + final cfg = context.read(); + if (cfg.autoTranslate) { + Future.delayed(const Duration(milliseconds: 100), () { + _translateText(); + }); } } @@ -345,6 +363,16 @@ class _ChatMessageTextState extends State<_ChatMessageText> { ), if (widget.data.updatedAt != widget.data.createdAt) Text('messageEditedHint'.tr()).fontSize(13).opacity(0.75), + if (_isTranslating) + AnimateWidgetExtensions(Text('translating').tr()) + .animate(onPlay: (e) => e.repeat()) + .fadeIn(duration: 500.ms, curve: Curves.easeOut) + .then() + .fadeOut( + duration: 500.ms, + delay: 1000.ms, + curve: Curves.easeIn, + ), if (_isTranslated) InkWell( child: Text('translated').tr().opacity(0.75), diff --git a/lib/widgets/post/post_item.dart b/lib/widgets/post/post_item.dart index 91aadfe..97192dc 100644 --- a/lib/widgets/post/post_item.dart +++ b/lib/widgets/post/post_item.dart @@ -8,6 +8,7 @@ import 'package:file_saver/file_saver.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_animate/flutter_animate.dart'; import 'package:go_router/go_router.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:material_symbols_icons/symbols.dart'; @@ -152,6 +153,49 @@ class _PostItemState extends State { late String _displayTitle = widget.data.body['title'] ?? ''; late String _displayDescription = widget.data.body['description'] ?? ''; bool _isTranslated = false; + bool _isTranslating = false; + + @override + void initState() { + super.initState(); + final cfg = context.read(); + if (cfg.autoTranslate) { + Future.delayed(const Duration(milliseconds: 100), () { + _translateText(); + }); + } + } + + Future _translateText() async { + final ta = context.read(); + setState(() => _isTranslating = true); + try { + final to = EasyLocalization.of(context)!.locale.languageCode; + final futures = List>.empty(growable: true); + if (_displayTitle.isNotEmpty) { + futures.add(ta.translate(_displayTitle, to: to).then((value) { + _displayTitle = value; + })); + } + if (_displayDescription.isNotEmpty) { + futures.add(ta.translate(_displayDescription, to: to).then((value) { + _displayDescription = value; + })); + } + if (_displayText.isNotEmpty) { + futures.add(ta.translate(_displayText, to: to).then((value) { + _displayText = value; + })); + } + await Future.wait(futures); + _isTranslated = true; + } catch (err) { + if (!mounted) return; + context.showErrorDialog(err); + } finally { + setState(() => _isTranslating = false); + } + } void _onChanged(SnPost data) { if (widget.onChanged != null) widget.onChanged!(data); @@ -280,14 +324,8 @@ class _PostItemState extends State { onDeleted: () { widget.onDeleted?.call(); }, - onTranslate: (text) { - setState(() { - _displayText = text['content']?.trim() ?? ''; - _displayTitle = text['title']?.trim() ?? ''; - _displayDescription = - text['description']?.trim() ?? ''; - _isTranslated = true; - }); + onTranslate: () { + _translateText(); }, ), ], @@ -384,6 +422,23 @@ class _PostItemState extends State { .plural(widget.data.totalViews), ], ).opacity(0.75), + if (_isTranslating) + AnimateWidgetExtensions(Row( + children: [ + Icon(Symbols.translate, size: 20), + const Gap(4), + Text('translating').tr(), + ], + )) + .animate(onPlay: (e) => e.repeat()) + .fadeIn( + duration: 500.ms, curve: Curves.easeOut) + .then() + .fadeOut( + duration: 500.ms, + delay: 1000.ms, + curve: Curves.easeIn, + ), if (_isTranslated) InkWell( child: Row( @@ -407,7 +462,11 @@ class _PostItemState extends State { ), ], ).padding( - bottom: widget.showViews || _isTranslated ? 8 : 0, + bottom: widget.showViews || + _isTranslated || + _isTranslating + ? 8 + : 0, ), ], ), @@ -989,7 +1048,7 @@ class _PostActionPopup extends StatelessWidget { final Function onDeleted; final Function() onShare, onShareImage; final Function()? onSelectAnswer; - final Function(Map)? onTranslate; + final Function()? onTranslate; const _PostActionPopup({ required this.data, required this.isAuthor, @@ -1041,28 +1100,6 @@ class _PostActionPopup extends StatelessWidget { } } - Future _translatePost(BuildContext context) async { - final ta = context.read(); - try { - final to = EasyLocalization.of(context)!.locale.languageCode; - final body = { - 'title': (data.body['title']?.isNotEmpty ?? false) - ? await ta.translate(data.body['title'], to: to) - : null, - 'description': (data.body['description']?.isNotEmpty ?? false) - ? await ta.translate(data.body['description'], to: to) - : null, - 'content': (data.body['content']?.isNotEmpty ?? false) - ? await ta.translate(data.body['content'], to: to) - : null, - }; - onTranslate?.call(body); - } catch (err) { - if (!context.mounted) return; - context.showErrorDialog(err); - } - } - @override Widget build(BuildContext context) { return SizedBox( @@ -1084,7 +1121,7 @@ class _PostActionPopup extends StatelessWidget { ], ), onTap: () { - _translatePost(context); + onTranslate?.call(); }, ), if (onTranslate != null) PopupMenuDivider(),