Skip to content

Commit f574825

Browse files
committed
feat: added youdao translator
1 parent 4477068 commit f574825

File tree

16 files changed

+602
-11
lines changed

16 files changed

+602
-11
lines changed

assets/images/youdao_icon.svg

Lines changed: 1 addition & 0 deletions
Loading

assets/images/youdao_icon.svg.vec

4.01 KB
Binary file not shown.

lib/constants/key_name.dart

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,15 @@ abstract class KeyNameConstants {
99

1010
// Normal translators
1111
static const String google = 'google';
12+
13+
// Baidu Translator
1214
static const String baiduAppID = 'baiduAppID';
1315
static const String baiduSecretKey = 'baiduSecretKey';
16+
17+
// YouDao Translator
18+
static const String youDaoAppID = 'youDaoAppID';
19+
static const String youDaoSecretKey = 'youDaoSecretKey';
20+
1421
static const String deepL = 'deepL';
1522

1623
// LLMs

lib/constants/link.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ abstract class LinkConstants {
1717
static const String baiduTranslateTranslationUrl =
1818
'$baiduTranslateBaseUrl/trans/vip/translate';
1919

20+
/// Base URL for the YouDao Translate API
21+
static const String youDaoTranslateTextUrl = 'https://openapi.youdao.com/api';
22+
2023
static const String deepSeekBaseUrl = 'https://api.deepseek.com/v1';
2124

2225
static const String deepSeekChatCompletionUrl =

lib/gen/assets.gen.dart

Lines changed: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

lib/presentation/home/bloc/home_bloc.dart

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,14 @@ class HomeBloc extends Bloc<HomeEvent, HomeState> {
6060
return;
6161
}
6262

63+
// TODO: Tell the user that the translation won't work if there is no
64+
// translator enabled
65+
if (settingsBloc.state.translatorSettings == null ||
66+
settingsBloc.state.translatorSettings!.enabledTranslators.isEmpty) {
67+
emit(state.error(const NoTranslatorEnabledException()));
68+
return;
69+
}
70+
6371
await emit.forEach(
6472
homeRepository.translateText(
6573
sourceText,

lib/presentation/home/models/base_translation_details.dart

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import 'package:equatable/equatable.dart';
22
import 'package:json_annotation/json_annotation.dart';
33
import 'package:qack/presentation/home/models/models.dart';
4+
import 'package:qack/presentation/home/models/youdao_translation.dart';
45
import 'package:qack/presentation/settings/models/models.dart';
56

67
/// Contains all the enabled translators and their translated output.
@@ -74,6 +75,31 @@ abstract class BaseTranslationError extends Equatable implements Exception {
7475
List<Object?> get props => [errorMessage];
7576
}
7677

78+
/// {@template no_translator_enabled_exception}
79+
/// Exception thrown when no translator is enabled.
80+
/// This is a [BaseTranslationError] and should be thrown when no translator
81+
/// is enabled.
82+
/// {@endtemplate}
83+
class NoTranslatorEnabledException implements Exception, BaseTranslationError {
84+
/// {@macro no_translator_enabled_exception}
85+
const NoTranslatorEnabledException();
86+
87+
static const _errorMessage =
88+
'No translator enabled. Please enable a translator.';
89+
90+
@override
91+
String toString() => _errorMessage;
92+
93+
@override
94+
String get errorMessage => _errorMessage;
95+
96+
@override
97+
List<Object?> get props => [];
98+
99+
@override
100+
bool? get stringify => false;
101+
}
102+
77103
/// {@template translated_text}
78104
/// Contains the translated input and output text.
79105
/// {@endtemplate}
@@ -101,6 +127,8 @@ extension BaseTranslationX on BaseTranslationDetails {
101127
return Translator.baidu;
102128
} else if (this is DeepseekChatCompletion) {
103129
return Translator.deepSeek;
130+
} else if (this is YouDaoTranslation) {
131+
return Translator.youDao;
104132
}
105133
return Translator.google;
106134
}
@@ -111,8 +139,10 @@ extension BaseTranslationX on BaseTranslationDetails {
111139
return 'assets/images/baidu_icon.svg.vec';
112140
} else if (this is DeepseekChatCompletion) {
113141
return 'assets/images/deepseek_icon.svg.vec';
142+
} else if (this is YouDaoTranslation) {
143+
return 'assets/images/youdao_icon.svg.vec';
114144
}
115-
throw Exception('All cases in BaseTranslationDetaisl svgName extension '
145+
throw Exception('All cases in BaseTranslationDetails svgName extension '
116146
'are not matched.');
117147
}
118148
}
@@ -124,6 +154,8 @@ extension BaseTranslationStringExtension on String {
124154
return 'Baidu';
125155
} else if (this == 'Deepseek') {
126156
return 'Deepseek';
157+
} else if (this == 'YouDao') {
158+
return 'YouDao';
127159
}
128160
return 'Unknown';
129161
}
Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
// ignore_for_file: use_super_parameters
2+
3+
import 'dart:convert';
4+
5+
import 'package:crypto/crypto.dart';
6+
import 'package:equatable/equatable.dart';
7+
import 'package:freezed_annotation/freezed_annotation.dart';
8+
import 'package:json_annotation/json_annotation.dart';
9+
import 'package:qack/presentation/home/models/base_translation_details.dart';
10+
11+
part 'youdao_translation.g.dart';
12+
13+
@JsonSerializable()
14+
final class YouDaoTranslation extends BaseTranslationDetails {
15+
YouDaoTranslation({
16+
required this.errorCode,
17+
required this.query,
18+
required this.translation,
19+
required this.langPair,
20+
required this.dict,
21+
required this.webdict,
22+
required this.tSpeakUrl,
23+
required this.speakUrl,
24+
TranslationStatus status = TranslationStatus.success,
25+
Exception? exception,
26+
}) : super(
27+
srcLanguage: langPair.split('2').first,
28+
targetLanguage: langPair.split('2').last,
29+
translatedText: TranslatedText(
30+
inputText: query,
31+
outputText: translation.fold(
32+
'',
33+
(previousValue, element) => '$previousValue \n $element\n',
34+
),
35+
),
36+
status: status,
37+
exception: exception,
38+
);
39+
40+
YouDaoTranslation.loading(String query)
41+
: this(
42+
errorCode: '',
43+
query: query,
44+
translation: [],
45+
langPair: '',
46+
dict: null,
47+
webdict: null,
48+
tSpeakUrl: null,
49+
speakUrl: null,
50+
exception: null,
51+
status: TranslationStatus.loading,
52+
);
53+
54+
YouDaoTranslation.error(
55+
Exception e, {
56+
required String errorCode,
57+
required String query,
58+
required String langPair,
59+
}) : this(
60+
errorCode: errorCode,
61+
query: query,
62+
translation: [],
63+
langPair: langPair,
64+
dict: null,
65+
webdict: null,
66+
tSpeakUrl: null,
67+
speakUrl: null,
68+
exception: e,
69+
);
70+
71+
factory YouDaoTranslation.fromJson(Map<String, dynamic> json) =>
72+
_$YouDaoTranslationFromJson(json);
73+
74+
final String errorCode;
75+
76+
final String query;
77+
final List<String> translation;
78+
final Map<String, String>? dict; // Dictionary deeplink
79+
final Map<String, String>? webdict; // Web deeplink
80+
final String? tSpeakUrl; // Translated text speech URL
81+
final String? speakUrl; // Source text speech URL
82+
83+
@JsonKey(name: 'l')
84+
final String langPair; // Language pair, e.g., "en2zh" for English to Chinese
85+
86+
/// This should not be implemented since the toJson method is not used in the
87+
/// YouDaoTranslation class.
88+
// Map<String, dynamic> toJson() => _$YouDaoTranslationToJson(this);
89+
}
90+
91+
@JsonSerializable()
92+
final class FailedYouDaoTranslation extends BaseTranslationError
93+
with EquatableMixin {
94+
const FailedYouDaoTranslation({
95+
required this.errorCode,
96+
required this.langPair,
97+
}) : super(
98+
errorMessage: 'YouDao translation failed with error code: $errorCode',
99+
);
100+
101+
factory FailedYouDaoTranslation.fromJson(Map<String, dynamic> json) =>
102+
_$FailedYouDaoTranslationFromJson(json);
103+
104+
@JsonKey(name: 'errorCode', includeToJson: false)
105+
final String errorCode;
106+
107+
// Translation language pair, e.g., "en2zh" for English to Chinese
108+
@JsonKey(name: 'l', includeToJson: false)
109+
final String langPair;
110+
111+
@override
112+
List<Object?> get props => [errorCode, langPair];
113+
114+
@override
115+
String toString() => 'YouDaoTranslation err code: $errorCode';
116+
}
117+
118+
/// This should not be implemented since the toJson method is not used in the
119+
/// YouDaoTranslation class.
120+
@override
121+
List<Map<String, String>> toJson(TranslatedText object) {
122+
throw UnimplementedError();
123+
}
124+
125+
/// {@template youdao_translation_request}
126+
/// Request model for YouDao translation API.
127+
/// {@endtemplate}
128+
@JsonSerializable()
129+
final class YouDaoTranslationRequest {
130+
/// {@macro youdao_translation_request}
131+
const YouDaoTranslationRequest({
132+
required this.inputText,
133+
required this.srcLanguage,
134+
required this.targetLanguage,
135+
required this.appID,
136+
required this.salt,
137+
required this.secondsSinceEpoch,
138+
this.signType = 'v3',
139+
});
140+
141+
@JsonKey(name: 'q')
142+
final String inputText;
143+
144+
@JsonKey(name: 'from')
145+
final String srcLanguage;
146+
@JsonKey(name: 'to')
147+
final String targetLanguage;
148+
149+
@JsonKey(name: 'appKey')
150+
final String appID;
151+
152+
@JsonKey(name: 'salt')
153+
final String salt;
154+
155+
@JsonKey(name: 'signType')
156+
final String signType;
157+
158+
@JsonKey(name: 'curtime')
159+
final int secondsSinceEpoch;
160+
161+
/// Creates a sign for the YouDao translation request.
162+
static String createSignJson({
163+
required String appID,
164+
required String inputText,
165+
required String salt,
166+
required int secondsSinceEpoch,
167+
required String secretKey,
168+
}) {
169+
// According to the YouDao API docs, if the inputText is longer than 20
170+
// characters, the input should be constructed as inputText
171+
// first 10 characters + inputText length + inputText last 10 characters.
172+
173+
late final String editedInputText;
174+
175+
if (inputText.length > 20) {
176+
editedInputText = '${inputText.substring(0, 10)}${inputText.length}'
177+
'${inputText.substring(inputText.length - 10)}';
178+
} else {
179+
editedInputText = inputText;
180+
}
181+
182+
print(
183+
'YouDao sign: $appID$editedInputText$salt$secondsSinceEpoch$secretKey',
184+
);
185+
186+
final sign = '$appID$editedInputText$salt$secondsSinceEpoch$secretKey';
187+
188+
final hashedSign = utf8.encode(sign);
189+
190+
return sha256.convert(hashedSign).toString();
191+
}
192+
193+
Map<String, dynamic> toJson() => _$YouDaoTranslationRequestToJson(this);
194+
}

0 commit comments

Comments
 (0)