Complete translation

This commit is contained in:
LittleSheep 2025-03-16 23:24:36 +08:00
parent d2f4e7a969
commit c9f69fed2c
8 changed files with 136 additions and 37 deletions

View File

@ -843,5 +843,8 @@
"accountContactMethodsDeleteDescription": "Are you sure you want to delete contact method {}? This operation is irreversible.", "accountContactMethodsDeleteDescription": "Are you sure you want to delete contact method {}? This operation is irreversible.",
"postCommentAdd": "Write a comment", "postCommentAdd": "Write a comment",
"translate": "Translate", "translate": "Translate",
"translated": "Translated" "translating": "Translating…",
"translated": "Translated",
"settingsAutoTranslate": "Auto Translate",
"settingsAutoTranslateDescription": "Automatically translate text when viewing posts and messages."
} }

View File

@ -841,5 +841,8 @@
"accountContactMethodsDeleteDescription": "你确定要删除联系方式 {} 吗?这个操作不可撤销。", "accountContactMethodsDeleteDescription": "你确定要删除联系方式 {} 吗?这个操作不可撤销。",
"postCommentAdd": "撰写一条评论", "postCommentAdd": "撰写一条评论",
"translate": "翻译", "translate": "翻译",
"translated": "已翻译" "translating": "正在翻译……",
"translated": "已翻译",
"settingsAutoTranslate": "自动翻译",
"settingsAutoTranslateDescription": "在查看帖子、消息时自动翻译文本。"
} }

View File

@ -841,5 +841,8 @@
"accountContactMethodsDeleteDescription": "你確定要刪除聯繫方式 {} 嗎?這個操作不可撤銷。", "accountContactMethodsDeleteDescription": "你確定要刪除聯繫方式 {} 嗎?這個操作不可撤銷。",
"postCommentAdd": "撰寫一條評論", "postCommentAdd": "撰寫一條評論",
"translate": "翻譯", "translate": "翻譯",
"translated": "已翻譯" "translating": "正在翻譯……",
"translated": "已翻譯",
"settingsAutoTranslate": "自動翻譯",
"settingsAutoTranslateDescription": "在查看帖子、消息時自動翻譯文本。"
} }

View File

@ -841,5 +841,8 @@
"accountContactMethodsDeleteDescription": "你確定要刪除聯繫方式 {} 嗎?這個操作不可撤銷。", "accountContactMethodsDeleteDescription": "你確定要刪除聯繫方式 {} 嗎?這個操作不可撤銷。",
"postCommentAdd": "撰寫一條評論", "postCommentAdd": "撰寫一條評論",
"translate": "翻譯", "translate": "翻譯",
"translated": "已翻譯" "translating": "正在翻譯……",
"translated": "已翻譯",
"settingsAutoTranslate": "自動翻譯",
"settingsAutoTranslateDescription": "在查看帖子、消息時自動翻譯文本。"
} }

View File

@ -20,6 +20,7 @@ const kAppExpandChatLink = 'app_expand_chat_link';
const kAppRealmCompactView = 'app_realm_compact_view'; const kAppRealmCompactView = 'app_realm_compact_view';
const kAppCustomFonts = 'app_custom_fonts'; const kAppCustomFonts = 'app_custom_fonts';
const kAppMixedFeed = 'app_mixed_feed'; const kAppMixedFeed = 'app_mixed_feed';
const kAppAutoTranslate = 'app_auto_translate';
const Map<String, FilterQuality> kImageQualityLevel = { const Map<String, FilterQuality> kImageQualityLevel = {
'settingsImageQualityLowest': FilterQuality.none, 'settingsImageQualityLowest': FilterQuality.none,
@ -86,6 +87,15 @@ class ConfigProvider extends ChangeNotifier {
return prefs.getBool(kAppMixedFeed) ?? true; 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) { set mixedFeed(bool value) {
prefs.setBool(kAppMixedFeed, value); prefs.setBool(kAppMixedFeed, value);
notifyListeners(); notifyListeners();

View File

@ -387,6 +387,18 @@ class _SettingsScreenState extends State<SettingsScreen> {
.fontSize(17) .fontSize(17)
.tr() .tr()
.padding(horizontal: 20, bottom: 4), .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( CheckboxListTile(
secondary: const Icon(Symbols.vibration), secondary: const Icon(Symbols.vibration),
contentPadding: const EdgeInsets.only(left: 24, right: 17), contentPadding: const EdgeInsets.only(left: 24, right: 17),

View File

@ -246,9 +246,14 @@ class _ChatMessageText extends StatefulWidget {
class _ChatMessageTextState extends State<_ChatMessageText> { class _ChatMessageTextState extends State<_ChatMessageText> {
late String _displayText = widget.data.body['text'] ?? ''; late String _displayText = widget.data.body['text'] ?? '';
bool _isTranslated = false; bool _isTranslated = false;
bool _isTranslating = false;
Future<void> _translateText() async { Future<void> _translateText() async {
if (widget.data.body['text'] == null || widget.data.body['text']!.isEmpty) {
return;
}
final ta = context.read<SnTranslator>(); final ta = context.read<SnTranslator>();
setState(() => _isTranslating = true);
try { try {
final to = EasyLocalization.of(context)!.locale.languageCode; final to = EasyLocalization.of(context)!.locale.languageCode;
_displayText = await ta.translate( _displayText = await ta.translate(
@ -259,6 +264,19 @@ class _ChatMessageTextState extends State<_ChatMessageText> {
if (mounted) setState(() {}); if (mounted) setState(() {});
} catch (err) { } catch (err) {
if (mounted) context.showErrorDialog(err); if (mounted) context.showErrorDialog(err);
} finally {
setState(() => _isTranslating = false);
}
}
@override
void initState() {
super.initState();
final cfg = context.read<ConfigProvider>();
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) if (widget.data.updatedAt != widget.data.createdAt)
Text('messageEditedHint'.tr()).fontSize(13).opacity(0.75), 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) if (_isTranslated)
InkWell( InkWell(
child: Text('translated').tr().opacity(0.75), child: Text('translated').tr().opacity(0.75),

View File

@ -8,6 +8,7 @@ import 'package:file_saver/file_saver.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_animate/flutter_animate.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:google_fonts/google_fonts.dart'; import 'package:google_fonts/google_fonts.dart';
import 'package:material_symbols_icons/symbols.dart'; import 'package:material_symbols_icons/symbols.dart';
@ -152,6 +153,49 @@ class _PostItemState extends State<PostItem> {
late String _displayTitle = widget.data.body['title'] ?? ''; late String _displayTitle = widget.data.body['title'] ?? '';
late String _displayDescription = widget.data.body['description'] ?? ''; late String _displayDescription = widget.data.body['description'] ?? '';
bool _isTranslated = false; bool _isTranslated = false;
bool _isTranslating = false;
@override
void initState() {
super.initState();
final cfg = context.read<ConfigProvider>();
if (cfg.autoTranslate) {
Future.delayed(const Duration(milliseconds: 100), () {
_translateText();
});
}
}
Future<void> _translateText() async {
final ta = context.read<SnTranslator>();
setState(() => _isTranslating = true);
try {
final to = EasyLocalization.of(context)!.locale.languageCode;
final futures = List<Future<void>>.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) { void _onChanged(SnPost data) {
if (widget.onChanged != null) widget.onChanged!(data); if (widget.onChanged != null) widget.onChanged!(data);
@ -280,14 +324,8 @@ class _PostItemState extends State<PostItem> {
onDeleted: () { onDeleted: () {
widget.onDeleted?.call(); widget.onDeleted?.call();
}, },
onTranslate: (text) { onTranslate: () {
setState(() { _translateText();
_displayText = text['content']?.trim() ?? '';
_displayTitle = text['title']?.trim() ?? '';
_displayDescription =
text['description']?.trim() ?? '';
_isTranslated = true;
});
}, },
), ),
], ],
@ -384,6 +422,23 @@ class _PostItemState extends State<PostItem> {
.plural(widget.data.totalViews), .plural(widget.data.totalViews),
], ],
).opacity(0.75), ).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) if (_isTranslated)
InkWell( InkWell(
child: Row( child: Row(
@ -407,7 +462,11 @@ class _PostItemState extends State<PostItem> {
), ),
], ],
).padding( ).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 onDeleted;
final Function() onShare, onShareImage; final Function() onShare, onShareImage;
final Function()? onSelectAnswer; final Function()? onSelectAnswer;
final Function(Map<String, dynamic>)? onTranslate; final Function()? onTranslate;
const _PostActionPopup({ const _PostActionPopup({
required this.data, required this.data,
required this.isAuthor, required this.isAuthor,
@ -1041,28 +1100,6 @@ class _PostActionPopup extends StatelessWidget {
} }
} }
Future<void> _translatePost(BuildContext context) async {
final ta = context.read<SnTranslator>();
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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return SizedBox( return SizedBox(
@ -1084,7 +1121,7 @@ class _PostActionPopup extends StatelessWidget {
], ],
), ),
onTap: () { onTap: () {
_translatePost(context); onTranslate?.call();
}, },
), ),
if (onTranslate != null) PopupMenuDivider(), if (onTranslate != null) PopupMenuDivider(),