You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
643 lines
20 KiB
643 lines
20 KiB
import 'package:flutter/material.dart';
|
|
import 'package:get/get.dart';
|
|
import 'dart:async';
|
|
import 'dart:convert';
|
|
import 'package:get_storage/get_storage.dart';
|
|
import '../../../data/services/volcano_translation_service.dart';
|
|
import '../../../data/services/azure_asr_service.dart';
|
|
import '../../../data/services/azure_tts_service.dart';
|
|
import '../../../data/services/language_manager.dart';
|
|
import '../../../core/utils/logger.dart';
|
|
|
|
class TranslationController extends GetxController {
|
|
// 服务
|
|
final AzureAsrService _asrService = Get.find<AzureAsrService>();
|
|
final VolcanoTranslationService _translationService = Get.find<VolcanoTranslationService>();
|
|
final AzureTtsService _ttsService = Get.find<AzureTtsService>();
|
|
final LanguageManager _languageManager = Get.find<LanguageManager>();
|
|
final GetStorage _storage = GetStorage();
|
|
|
|
// 存储相关
|
|
static const String _historyKey = 'translation_history';
|
|
static const int _maxHistoryItems = 100;
|
|
|
|
// 状态变量
|
|
final isRecognizing = false.obs;
|
|
final isTranslating = false.obs;
|
|
final isTtsEnabled = true.obs;
|
|
|
|
// 语言相关
|
|
final sourceLanguage = '中文(简体)'.obs;
|
|
final targetLanguage = '英语'.obs;
|
|
final sourceLanguageCode = 'zh-CN'.obs;
|
|
final targetLanguageCode = 'en-US'.obs;
|
|
var detectedLanguageCode = '';
|
|
|
|
// 翻译历史和当前项
|
|
final translationHistory = <TranslationItem>[].obs;
|
|
final Rx<TranslationItem?> currentItem = Rx<TranslationItem?>(null);
|
|
String? currentSessionId;
|
|
|
|
// UI相关
|
|
final ScrollController scrollController = ScrollController();
|
|
|
|
// 其他
|
|
StreamSubscription? _recognitionSubscription;
|
|
Timer? _translationDebounceTimer;
|
|
Timer? _scrollDebounceTimer; // 滚动防抖定时器
|
|
bool _isNearBottom = true; // 标记是否在底部附近
|
|
|
|
// 获取支持的语言列表
|
|
Map<String, String> get supportedLanguages => _languageManager.getChineseNameToAsrCodeMap();
|
|
|
|
@override
|
|
void onInit() {
|
|
super.onInit();
|
|
_initController();
|
|
}
|
|
|
|
// 初始化控制器
|
|
void _initController() {
|
|
_initLanguageCodes();
|
|
loadTranslationHistory();
|
|
_initServices();
|
|
_setupListeners();
|
|
|
|
// 初始化完成后滚动到底部
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
// 延迟 300ms 再次尝试滚动,确保列表已完全渲染
|
|
Future.delayed(const Duration(milliseconds: 300), () {
|
|
scrollToBottom(animate: false);
|
|
});
|
|
|
|
});
|
|
}
|
|
|
|
// 重新初始化控制器(公共方法)
|
|
void reinitialize() {
|
|
_setupListeners();
|
|
|
|
// 初始化完成后滚动到底部
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
scrollToBottom(animate: false);
|
|
});
|
|
}
|
|
|
|
// 仅设置滚动监听器(不启动翻译)
|
|
void setupScrollListener() {
|
|
// 移除之前的监听器,避免重复添加
|
|
scrollController.removeListener(_onScroll);
|
|
// 重新添加滚动监听器
|
|
scrollController.addListener(_onScroll);
|
|
}
|
|
|
|
// 初始化服务
|
|
Future<void> _initServices() async {
|
|
await _initTranslationService();
|
|
await _initAzureAsrService();
|
|
}
|
|
|
|
// 初始化语言代码
|
|
void _initLanguageCodes() {
|
|
final sourceAsrCode = _languageManager.getAsrCodeByChineseName(sourceLanguage.value);
|
|
if (sourceAsrCode != null) {
|
|
sourceLanguageCode.value = sourceAsrCode;
|
|
}
|
|
|
|
final targetAsrCode = _languageManager.getAsrCodeByChineseName(targetLanguage.value);
|
|
if (targetAsrCode != null) {
|
|
targetLanguageCode.value = targetAsrCode;
|
|
}
|
|
}
|
|
|
|
// 初始化翻译服务
|
|
Future<void> _initTranslationService() async {
|
|
try {
|
|
await _translationService.initialize();
|
|
} catch (e) {
|
|
Logger.error('初始化翻译服务失败: $e');
|
|
}
|
|
}
|
|
|
|
// 初始化语音识别服务
|
|
Future<void> _initAzureAsrService() async {
|
|
try {
|
|
final List<String> asrSupportedLanguages = [sourceLanguageCode.value, targetLanguageCode.value];
|
|
await _ttsService.initialize();
|
|
await _asrService.initialize(
|
|
autoDetectLanguage: true,
|
|
supportedLanguages: asrSupportedLanguages,
|
|
);
|
|
} catch (e) {
|
|
Logger.error('初始化语音识别服务失败: $e');
|
|
}
|
|
}
|
|
|
|
// 设置监听器
|
|
void _setupListeners() {
|
|
// 监听历史记录变化,当有新项添加时滚动到底部
|
|
ever(translationHistory, (_) {
|
|
if (_isNearBottom) {
|
|
scrollToBottom();
|
|
}
|
|
});
|
|
|
|
// 监听当前项变化,当有新项添加时滚动到底部
|
|
ever(currentItem, (item) {
|
|
if (item != null && _isNearBottom) {
|
|
scrollToBottom();
|
|
}
|
|
});
|
|
|
|
// 监听滚动位置,判断是否在底部附近
|
|
scrollController.addListener(_onScroll);
|
|
}
|
|
|
|
// 滚动监听器
|
|
void _onScroll() {
|
|
if (!scrollController.hasClients) return;
|
|
|
|
// 判断是否在底部附近(距离底部20像素内)
|
|
final maxScroll = scrollController.position.maxScrollExtent;
|
|
final currentScroll = scrollController.position.pixels;
|
|
_isNearBottom = (maxScroll - currentScroll) < 20;
|
|
}
|
|
|
|
@override
|
|
void onClose() {
|
|
stopRecognition();
|
|
_recognitionSubscription?.cancel();
|
|
_translationDebounceTimer?.cancel();
|
|
_scrollDebounceTimer?.cancel();
|
|
scrollController.removeListener(_onScroll);
|
|
scrollController.dispose();
|
|
super.onClose();
|
|
}
|
|
|
|
// 更新ASR支持的语言
|
|
Future<void> _updateAsrSupportedLanguages() async {
|
|
try {
|
|
final List<String> asrSupportedLanguages = [sourceLanguageCode.value, targetLanguageCode.value];
|
|
await _asrService.setSupportedLanguages(asrSupportedLanguages);
|
|
} catch (e) {
|
|
Logger.error('更新ASR支持的语言失败: $e');
|
|
}
|
|
}
|
|
|
|
// 切换TTS功能
|
|
void toggleTts() => isTtsEnabled.toggle();
|
|
|
|
// 开始语音识别
|
|
Future<void> startRecognition() async {
|
|
if (isRecognizing.value) return;
|
|
|
|
try {
|
|
await _updateAsrSupportedLanguages();
|
|
currentSessionId = DateTime.now().millisecondsSinceEpoch.toString();
|
|
|
|
// 初始化新的翻译项
|
|
currentItem.value = TranslationItem(
|
|
sourceText: '',
|
|
translatedText: '',
|
|
sourceLanguageCode: sourceLanguageCode.value,
|
|
targetLanguageCode: targetLanguageCode.value,
|
|
timestamp: DateTime.now(),
|
|
sessionId: currentSessionId!,
|
|
isFirstInSession: true,
|
|
);
|
|
|
|
// 开始连续语音识别
|
|
final recognitionStream = await _asrService.startContinuousRecognition();
|
|
isRecognizing.value = true;
|
|
|
|
// 监听识别结果
|
|
_recognitionSubscription?.cancel();
|
|
_recognitionSubscription = recognitionStream.listen(_handleRecognitionEvent);
|
|
} catch (e) {
|
|
isRecognizing.value = false;
|
|
Logger.error('启动语音识别失败: $e');
|
|
}
|
|
}
|
|
|
|
// 处理识别事件
|
|
void _handleRecognitionEvent(RecognitionEvent event) {
|
|
if (event.type == RecognitionEventType.finalResult && event.text.isNotEmpty) {
|
|
detectedLanguageCode = event.detectedLanguage;
|
|
handleFinalResult(event.text);
|
|
} else if (event.type == RecognitionEventType.intermediateResult && event.text.isNotEmpty) {
|
|
if (event.detectedLanguage.isNotEmpty) {
|
|
detectedLanguageCode = event.detectedLanguage;
|
|
}
|
|
handleIntermediateResult(event.text);
|
|
} else if (event.type == RecognitionEventType.error) {
|
|
isRecognizing.value = false;
|
|
Logger.error('语音识别错误: ${event.error}');
|
|
} else if (event.type == RecognitionEventType.canceled) {
|
|
isRecognizing.value = false;
|
|
Logger.error('语音识别取消: ${event.error}');
|
|
}
|
|
}
|
|
|
|
// 停止语音识别
|
|
Future<void> stopRecognition() async {
|
|
if (!isRecognizing.value) return;
|
|
|
|
try {
|
|
await _asrService.stopContinuousRecognition();
|
|
_recognitionSubscription?.cancel();
|
|
_recognitionSubscription = null;
|
|
isRecognizing.value = false;
|
|
|
|
// 如果当前有中间结果,将其作为最终结果处理
|
|
if (currentItem.value != null && currentItem.value!.sourceText.isNotEmpty) {
|
|
// 标记为最终结果
|
|
currentItem.value!.isIntermediate = false;
|
|
|
|
// 如果还没有翻译结果,尝试翻译
|
|
if (currentItem.value!.translatedText.isEmpty) {
|
|
await translateText(currentItem.value!.sourceText);
|
|
}
|
|
|
|
translationHistory.add(currentItem.value!);
|
|
saveTranslationHistory();
|
|
}
|
|
|
|
currentSessionId = null;
|
|
currentItem.value = null;
|
|
} catch (e) {
|
|
Logger.error('停止语音识别失败: $e');
|
|
}
|
|
}
|
|
|
|
// 处理中间识别结果
|
|
void handleIntermediateResult(String text) {
|
|
if (text.isEmpty) return;
|
|
|
|
if (currentItem.value == null) {
|
|
currentSessionId ??= DateTime.now().millisecondsSinceEpoch.toString();
|
|
currentItem.value = TranslationItem(
|
|
sourceText: text,
|
|
translatedText: '',
|
|
sourceLanguageCode: sourceLanguageCode.value,
|
|
targetLanguageCode: targetLanguageCode.value,
|
|
timestamp: DateTime.now(),
|
|
sessionId: currentSessionId!,
|
|
isFirstInSession: translationHistory.isEmpty || translationHistory.last.sessionId != currentSessionId,
|
|
isIntermediate: true, // 保留标记,用于内部逻辑
|
|
);
|
|
} else {
|
|
// 更新当前项
|
|
currentItem.value!.sourceText = text;
|
|
currentItem.value!.isIntermediate = true; // 保留标记,用于内部逻辑
|
|
currentItem.refresh();
|
|
}
|
|
|
|
// 防抖处理翻译请求
|
|
_translationDebounceTimer?.cancel();
|
|
_translationDebounceTimer = Timer(const Duration(milliseconds: 300), () {
|
|
translateCurrentItem(isIntermediate: true);
|
|
});
|
|
}
|
|
|
|
// 翻译当前项
|
|
Future<void> translateCurrentItem({bool isIntermediate = false}) async {
|
|
if (currentItem.value == null || currentItem.value!.sourceText.isEmpty) return;
|
|
await translateText(currentItem.value!.sourceText);
|
|
}
|
|
|
|
// 处理最终识别结果
|
|
Future<void> handleFinalResult(String text) async {
|
|
if (text.isEmpty || currentItem.value == null) return;
|
|
|
|
// 更新当前项,但不立即添加到历史记录
|
|
currentItem.value!.sourceText = text;
|
|
currentItem.value!.isIntermediate = false; // 标记为最终结果
|
|
currentItem.refresh();
|
|
|
|
// 先翻译,保持UI稳定
|
|
final translationResult = await translateText(text);
|
|
|
|
// 给UI一点时间更新
|
|
await Future.delayed(const Duration(milliseconds: 100));
|
|
|
|
// 翻译完成后再添加到历史记录
|
|
if (currentItem.value != null) {
|
|
// 确保添加到历史记录的是最终结果
|
|
translationHistory.add(currentItem.value!);
|
|
saveTranslationHistory();
|
|
|
|
// 播放TTS
|
|
if (translationResult != null && isTtsEnabled.value) {
|
|
await playTranslatedText(translationResult);
|
|
}
|
|
|
|
// 重置当前项,准备下一次输入
|
|
currentItem.value = null;
|
|
}
|
|
}
|
|
|
|
// 翻译文本
|
|
Future<String?> translateText(String sourceText) async {
|
|
if (sourceText.isEmpty) return null;
|
|
|
|
isTranslating.value = true;
|
|
String? translationResult;
|
|
|
|
try {
|
|
// 根据检测到的语言确定源语言和目标语言
|
|
final bool shouldSwap = detectedLanguageCode == sourceLanguageCode.value;
|
|
final detectedSourceLanguageCode = shouldSwap ? sourceLanguageCode.value : targetLanguageCode.value;
|
|
final detectedTargetLanguageCode = shouldSwap ? targetLanguageCode.value : sourceLanguageCode.value;
|
|
|
|
// 调用翻译服务
|
|
translationResult = await _translationService.translateText(
|
|
text: sourceText,
|
|
sourceLanguageCode: detectedSourceLanguageCode,
|
|
targetLanguageCode: detectedTargetLanguageCode,
|
|
);
|
|
|
|
if (translationResult != null) {
|
|
_updateTranslationResult(sourceText, translationResult, detectedSourceLanguageCode, detectedTargetLanguageCode);
|
|
}
|
|
} catch (e) {
|
|
Logger.error('翻译失败: $e');
|
|
} finally {
|
|
isTranslating.value = false;
|
|
}
|
|
|
|
return translationResult;
|
|
}
|
|
|
|
// 更新翻译结果
|
|
void _updateTranslationResult(String sourceText, String result, String sourceCode, String targetCode) {
|
|
if (currentItem.value != null && currentItem.value!.sourceText.contains(sourceText)) {
|
|
// 更新当前项
|
|
currentItem.value!.translatedText = result;
|
|
currentItem.value!.sourceLanguageCode = sourceCode;
|
|
currentItem.value!.targetLanguageCode = targetCode;
|
|
currentItem.refresh();
|
|
} else {
|
|
// 更新历史记录
|
|
final index = translationHistory.indexWhere((item) => item.sourceText == sourceText);
|
|
if (index != -1) {
|
|
translationHistory[index].translatedText = result;
|
|
translationHistory[index].sourceLanguageCode = sourceCode;
|
|
translationHistory[index].targetLanguageCode = targetCode;
|
|
translationHistory.refresh();
|
|
saveTranslationHistory();
|
|
}
|
|
}
|
|
}
|
|
|
|
// 播放翻译文本
|
|
Future<void> playTranslatedText(String text) async {
|
|
if (text.isEmpty) return;
|
|
|
|
try {
|
|
final bool shouldSwap = detectedLanguageCode == targetLanguageCode.value;
|
|
final detectedTargetLanguageCode = shouldSwap ? sourceLanguageCode.value : targetLanguageCode.value;
|
|
final voiceName = _languageManager.getTtsVoiceNameByAsrCode(detectedTargetLanguageCode) ?? 'en-US-AriaNeural';
|
|
|
|
await _ttsService.setVoice(voiceName);
|
|
await _ttsService.speak(text);
|
|
} catch (e) {
|
|
Logger.error('播放翻译失败: $e');
|
|
}
|
|
}
|
|
|
|
// 停止所有操作
|
|
Future<void> stopAll() async {
|
|
try {
|
|
if (isRecognizing.value) {
|
|
await stopRecognition();
|
|
}
|
|
|
|
// 取消所有定时器
|
|
_translationDebounceTimer?.cancel();
|
|
_scrollDebounceTimer?.cancel();
|
|
|
|
await _ttsService.stop();
|
|
} catch (e) {
|
|
Logger.error('停止操作失败: $e');
|
|
}
|
|
}
|
|
|
|
// 播放特定翻译项
|
|
Future<void> playTranslationItem(TranslationItem item) async {
|
|
if (item.translatedText.isEmpty) return;
|
|
|
|
try {
|
|
final voiceName = _languageManager.getTtsVoiceNameByAsrCode(item.targetLanguageCode) ?? 'en-US-AriaNeural';
|
|
await _ttsService.setVoice(voiceName);
|
|
await _ttsService.speak(item.translatedText);
|
|
} catch (e) {
|
|
Logger.error('播放翻译项失败: $e');
|
|
}
|
|
}
|
|
|
|
// 播放文本
|
|
Future<void> playText(String text, {String? languageCode}) async {
|
|
if (text.isEmpty) return;
|
|
|
|
try {
|
|
final voiceName = languageCode != null
|
|
? _languageManager.getTtsVoiceNameByAsrCode(languageCode) ?? 'en-US-AriaNeural'
|
|
: _languageManager.getTtsVoiceNameByAsrCode(targetLanguageCode.value) ?? 'en-US-AriaNeural';
|
|
|
|
await _ttsService.setVoice(voiceName);
|
|
await _ttsService.speak(text);
|
|
} catch (e) {
|
|
Logger.error('播放文本失败: $e');
|
|
}
|
|
}
|
|
|
|
// 简洁高效的滚动到底部方法
|
|
void scrollToBottom({bool animate = true}) {
|
|
if (!scrollController.hasClients) return;
|
|
|
|
// 取消之前的定时器
|
|
_scrollDebounceTimer?.cancel();
|
|
|
|
// 创建新的定时器,防抖处理
|
|
_scrollDebounceTimer = Timer(const Duration(milliseconds: 100), () {
|
|
if (!scrollController.hasClients) return;
|
|
|
|
|
|
if (animate) {
|
|
scrollController.animateTo(
|
|
scrollController.position.maxScrollExtent,
|
|
duration: const Duration(milliseconds: 200),
|
|
curve: Curves.easeOut,
|
|
);
|
|
} else {
|
|
scrollController.jumpTo(scrollController.position.maxScrollExtent);
|
|
}
|
|
});
|
|
}
|
|
|
|
// 底部控制栏高度
|
|
double _bottomBarHeight = 0.0;
|
|
|
|
// 设置底部控制栏高度
|
|
void setBottomBarHeight(double height) {
|
|
_bottomBarHeight = height;
|
|
}
|
|
|
|
// 获取底部控制栏高度
|
|
double get bottomBarHeight => _bottomBarHeight;
|
|
|
|
// 切换源语言和目标语言
|
|
void swapLanguages() {
|
|
// 先停止当前的翻译操作
|
|
stopAll();
|
|
|
|
final tempName = sourceLanguage.value;
|
|
sourceLanguage.value = targetLanguage.value;
|
|
targetLanguage.value = tempName;
|
|
|
|
final tempCode = sourceLanguageCode.value;
|
|
sourceLanguageCode.value = targetLanguageCode.value;
|
|
targetLanguageCode.value = tempCode;
|
|
}
|
|
|
|
// 设置源语言
|
|
void setSourceLanguage(String language) {
|
|
if (sourceLanguage.value != language) {
|
|
// 先停止当前的翻译操作
|
|
stopAll();
|
|
|
|
sourceLanguage.value = language;
|
|
|
|
final asrCode = _languageManager.getAsrCodeByChineseName(language);
|
|
if (asrCode != null) {
|
|
sourceLanguageCode.value = asrCode;
|
|
}
|
|
|
|
_updateAsrSupportedLanguages();
|
|
|
|
if (isRecognizing.value) {
|
|
stopRecognition();
|
|
startRecognition();
|
|
}
|
|
}
|
|
}
|
|
|
|
// 设置目标语言
|
|
void setTargetLanguage(String language) {
|
|
if (targetLanguage.value != language) {
|
|
// 先停止当前的翻译操作
|
|
stopAll();
|
|
|
|
targetLanguage.value = language;
|
|
|
|
final asrCode = _languageManager.getAsrCodeByChineseName(language);
|
|
if (asrCode != null) {
|
|
targetLanguageCode.value = asrCode;
|
|
}
|
|
|
|
_updateAsrSupportedLanguages();
|
|
}
|
|
}
|
|
|
|
// 加载翻译历史
|
|
void loadTranslationHistory() {
|
|
final historyJson = _storage.read(_historyKey);
|
|
if (historyJson != null) {
|
|
try {
|
|
final List<dynamic> historyList = jsonDecode(historyJson);
|
|
translationHistory.value = historyList.map((item) => TranslationItem.fromJson(item)).toList();
|
|
} catch (e) {
|
|
_storage.remove(_historyKey);
|
|
Logger.error('加载翻译历史失败: $e');
|
|
}
|
|
}
|
|
}
|
|
|
|
// 保存翻译历史
|
|
void saveTranslationHistory() {
|
|
if (translationHistory.length > _maxHistoryItems) {
|
|
translationHistory.removeRange(0, translationHistory.length - _maxHistoryItems);
|
|
}
|
|
|
|
try {
|
|
final List<Map<String, dynamic>> historyJson = translationHistory.map((item) => item.toJson()).toList();
|
|
_storage.write(_historyKey, jsonEncode(historyJson));
|
|
} catch (e) {
|
|
Logger.error('保存翻译历史失败: $e');
|
|
}
|
|
}
|
|
|
|
// 清除翻译历史
|
|
void clearTranslationHistory() {
|
|
translationHistory.clear();
|
|
_storage.remove(_historyKey);
|
|
}
|
|
}
|
|
|
|
// 翻译项模型
|
|
class TranslationItem {
|
|
String sourceText;
|
|
String translatedText;
|
|
String sourceLanguageCode;
|
|
String targetLanguageCode;
|
|
final DateTime timestamp;
|
|
String sessionId;
|
|
final bool isFirstInSession;
|
|
bool isIntermediate; // 是否为中间结果
|
|
|
|
TranslationItem({
|
|
required this.sourceText,
|
|
required this.translatedText,
|
|
required this.sourceLanguageCode,
|
|
required this.targetLanguageCode,
|
|
required this.timestamp,
|
|
required this.sessionId,
|
|
this.isFirstInSession = false,
|
|
this.isIntermediate = false,
|
|
});
|
|
|
|
factory TranslationItem.fromJson(Map<String, dynamic> json) {
|
|
DateTime timestamp;
|
|
try {
|
|
timestamp = DateTime.parse(json['timestamp']);
|
|
} catch (e) {
|
|
timestamp = DateTime.now();
|
|
}
|
|
|
|
return TranslationItem(
|
|
sourceText: json['sourceText'] ?? '',
|
|
translatedText: json['translatedText'] ?? '',
|
|
sourceLanguageCode: json['sourceLanguageCode'] ?? 'zh-CN',
|
|
targetLanguageCode: json['targetLanguageCode'] ?? 'en-US',
|
|
timestamp: timestamp,
|
|
sessionId: json['sessionId'] ?? DateTime.now().millisecondsSinceEpoch.toString(),
|
|
isFirstInSession: json['isFirstInSession'] ?? false,
|
|
isIntermediate: false, // 从存储加载的都是最终结果
|
|
);
|
|
}
|
|
|
|
Map<String, dynamic> toJson() {
|
|
return {
|
|
'sourceText': sourceText,
|
|
'translatedText': translatedText,
|
|
'sourceLanguageCode': sourceLanguageCode,
|
|
'targetLanguageCode': targetLanguageCode,
|
|
'timestamp': timestamp.toIso8601String(),
|
|
'sessionId': sessionId,
|
|
'isFirstInSession': isFirstInSession,
|
|
// 不保存isIntermediate,因为存储的都是最终结果
|
|
};
|
|
}
|
|
|
|
// 获取源语言的中文名称
|
|
String get sourceLanguageName {
|
|
final languageManager = Get.find<LanguageManager>();
|
|
return languageManager.getChineseNameByAsrCode(sourceLanguageCode) ?? '未知语言';
|
|
}
|
|
|
|
// 获取目标语言的中文名称
|
|
String get targetLanguageName {
|
|
final languageManager = Get.find<LanguageManager>();
|
|
return languageManager.getChineseNameByAsrCode(targetLanguageCode) ?? '未知语言';
|
|
}
|
|
}
|
|
|