translation.ts 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204
  1. import TUIChatEngine, {
  2. IMessageModel,
  3. TUIChatService,
  4. TUIStore,
  5. TUITranslateService,
  6. TUIUserService,
  7. } from '@tencentcloud/chat-uikit-engine';
  8. import { IChatResponese, IUserProfile } from '../../../interface';
  9. /**
  10. * three type for origin text to be translated
  11. * 1. image: small face text
  12. * 2. text: plain text
  13. * 3. mention: mention '@'
  14. */
  15. interface ITextFace {
  16. type: 'face';
  17. value: string;
  18. }
  19. interface ITextPlain {
  20. type: 'text';
  21. value: string;
  22. }
  23. interface ITextAt {
  24. type: 'mention';
  25. value: string;
  26. }
  27. export type TranslationTextType = ITextFace | ITextPlain | ITextAt;
  28. class Translator {
  29. public isUseCache = true;
  30. private translationCache = new Map<string, TranslationTextType[]>();
  31. private static instance: Translator | undefined = undefined;
  32. private constructor() {}
  33. static getInstance() {
  34. if (!Translator.instance) {
  35. Translator.instance = new Translator();
  36. }
  37. return Translator.instance;
  38. }
  39. async get(message: IMessageModel): Promise<TranslationTextType[]> {
  40. // step1: check in cache if translation exist
  41. if (this.isUseCache) {
  42. const cache = this.translationCache.get(message.ID);
  43. if (cache !== undefined) {
  44. return cache;
  45. }
  46. }
  47. // step2: get message model with prototype methods
  48. const currentMessage: IMessageModel = TUIStore.getMessageModel(message.ID);
  49. if (!currentMessage) {
  50. return [];
  51. }
  52. const { text } = currentMessage.getMessageContent() || {};
  53. const textList: TranslationTextType[] = [];
  54. const splittingList = await this.getNickList(currentMessage);
  55. // step3: Categorize origin messages to 'plain text', 'face', 'mention'
  56. for (let i = 0; i < text.length; ++i) {
  57. const item = text[i];
  58. if (item.name === 'img') {
  59. textList.push({ type: 'face', value: item.src });
  60. continue;
  61. }
  62. const { transSplitingList, atNickList } = this.getSplitResult(item.text, splittingList);
  63. for (let j = 0; j < transSplitingList.length; ++j) {
  64. textList.push({ type: 'text', value: transSplitingList[j] });
  65. if (j < atNickList.length) {
  66. textList.push({ type: 'mention', value: atNickList[j] });
  67. }
  68. }
  69. }
  70. // step4: filter plain text to be translated
  71. const needTranslateTextIndex: number[] = [];
  72. const needTranslateText = textList.filter((item, index) => {
  73. if (item.type === 'text' && item.value.trim() !== '') {
  74. needTranslateTextIndex.push(index);
  75. return true;
  76. }
  77. return false;
  78. }).map(item => item.value);
  79. if (needTranslateText.length === 0) {
  80. this.translationCache.set(currentMessage.ID, textList);
  81. return textList;
  82. }
  83. // step5: get final translation result
  84. const translationResult = await this.getTranslationStandard(needTranslateText) as string[];
  85. translationResult.forEach((item, index) => {
  86. textList[needTranslateTextIndex[index]].value = item;
  87. });
  88. // step6: cache translation result
  89. this.translationCache.set(currentMessage.ID, textList);
  90. return textList;
  91. }
  92. /**
  93. * Clears the translation cache.
  94. */
  95. clear() {
  96. this.translationCache.clear();
  97. }
  98. disableCache() {
  99. this.isUseCache = false;
  100. }
  101. enableCache() {
  102. this.isUseCache = true;
  103. }
  104. private getTranslationStandard(originTextList: string[]): Promise<string[]> {
  105. return new Promise((resolve, reject) => {
  106. TUIChatService.translateText({
  107. sourceTextList: originTextList,
  108. sourceLanguage: 'auto',
  109. })
  110. .then((response: IChatResponese<{ translatedTextList: string[] }>) => {
  111. const {
  112. data: { translatedTextList },
  113. } = response;
  114. resolve(translatedTextList);
  115. })
  116. .catch((error) => {
  117. reject(error);
  118. });
  119. });
  120. }
  121. /**
  122. * the nick list is used to split the text by @ + {nick or userID}
  123. * @param message
  124. * @returns e.g. ['@james', '@john']
  125. */
  126. private async getNickList(message: IMessageModel): Promise<string[]> {
  127. const splittingList: string[] = [];
  128. const { atUserList = [] } = message;
  129. const atAllID: string = TUIChatEngine.TYPES.MSG_AT_ALL;
  130. if (atUserList.includes(atAllID)) {
  131. splittingList.push(`@${TUITranslateService.t('TUIChat.所有人')}`);
  132. }
  133. if (atUserList.length > 0) {
  134. const { data: userProfileList } = await TUIUserService.getUserProfile({ userIDList: atUserList }) as IChatResponese<IUserProfile[]>;
  135. userProfileList.forEach((user) => {
  136. const atNick = `@${user.nick || user.userID}`;
  137. splittingList.push(atNick);
  138. });
  139. }
  140. return [...new Set(splittingList)];
  141. }
  142. /**
  143. * Splits the given text into substrings based on the provided splitString array.
  144. *
  145. * @param {string} text - The text to be split.
  146. * @param {string[]} splitString - The array of strings to split the text by.
  147. * @return {{ transSplitingList: string[]; atNickList: string[] }} - An object containing two arrays:
  148. * - transSplitingList: An array of substrings extracted from the text.
  149. * - atNickList: An array of split strings that were found in the text.
  150. */
  151. private getSplitResult(text: string, splitString: string[]): { transSplitingList: string[]; atNickList: string[] } {
  152. let searchStartPos = 0;
  153. const transSplitingList: string[] = [];
  154. const atNickList: string[] = [];
  155. while (searchStartPos < text.length) {
  156. const nextAtCharPos = text.indexOf('@', searchStartPos);
  157. if (nextAtCharPos === -1) {
  158. transSplitingList.push(text.substring(searchStartPos));
  159. break;
  160. }
  161. let found = false;
  162. for (let i = 0; i < splitString.length; ++i) {
  163. const pos = text.indexOf(splitString[i], nextAtCharPos);
  164. if (pos !== -1 && pos === nextAtCharPos) {
  165. transSplitingList.push(text.substring(searchStartPos, pos));
  166. atNickList.push(splitString[i]);
  167. searchStartPos = pos + splitString[i].length;
  168. found = true;
  169. break;
  170. }
  171. }
  172. if (!found) {
  173. transSplitingList.push(text.substring(searchStartPos));
  174. break;
  175. }
  176. }
  177. return {
  178. transSplitingList,
  179. atNickList,
  180. };
  181. }
  182. }
  183. export const translator = Translator.getInstance();