snapshot of https://github.com/RocketChat/Rocket.Chat.git (develop, 2026-09-16; blobs > 131072B omitted)
Some checks failed
Create issues from TODOs / todos (push) Failing after 3s
Close inactive issues / close-issues (push) Has been cancelled
Deploy GitHub Pages / deploy-preview (push) Has been cancelled
CI / ⚙️ Variables Setup (push) Has been cancelled
CI / ⚙️ Test Guard (push) Has been cancelled
CI / 🚀 Notify external services - draft (push) Has been cancelled
CI / 📦 Build Packages (push) Has been cancelled
CI / 📦 Meteor Build (coverage) (push) Has been cancelled
CI / 📦 Meteor Build (production) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], fips) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Publish Docker Images (ghcr.io) (push) Has been cancelled
CI / 📦 Track Image Sizes (push) Has been cancelled
CI / 🔎 Code Check - Action Lint (push) Has been cancelled
CI / 🔎 Code Check (push) Has been cancelled
CI / 🔨 Test Storybook (push) Has been cancelled
CI / 🔨 Test Unit (push) Has been cancelled
CI / 🔨 Test API (CE) (push) Has been cancelled
CI / 🔨 Test API Livechat (CE) (push) Has been cancelled
CI / 🔨 Test UI (CE) (push) Has been cancelled
CI / 🔨 Test API (EE) (push) Has been cancelled
CI / 🔨 Test API Livechat (EE) (push) Has been cancelled
CI / 🔨 Test API Apps (deno-runtime - EE) (push) Has been cancelled
CI / 🔨 Test UI (EE) (push) Has been cancelled
CI / 🔨 Test API (FIPS) (push) Has been cancelled
CI / 🔨 Test API Livechat (FIPS) (push) Has been cancelled
CI / 🔨 Test UI (FIPS) (push) Has been cancelled
CI / 🔨 Test Federation Matrix (push) Has been cancelled
CI / 📊 Report Coverage (push) Has been cancelled
CI / ✅ Tests Done (push) Has been cancelled
CI / 🚀 Publish build assets (push) Has been cancelled
CI / 🚀 Publish Docker Images (DockerHub) (push) Has been cancelled
CI / 🚀 Notify external services (push) Has been cancelled
CI / Update Version Durability (push) Has been cancelled
Release / Release (push) Has been cancelled
Auto-close duplicate issues / auto-close-duplicates (push) Failing after 1s
Code scanning - action / CodeQL-Build (push) Failing after 1s
Some checks failed
Create issues from TODOs / todos (push) Failing after 3s
Close inactive issues / close-issues (push) Has been cancelled
Deploy GitHub Pages / deploy-preview (push) Has been cancelled
CI / ⚙️ Variables Setup (push) Has been cancelled
CI / ⚙️ Test Guard (push) Has been cancelled
CI / 🚀 Notify external services - draft (push) Has been cancelled
CI / 📦 Build Packages (push) Has been cancelled
CI / 📦 Meteor Build (coverage) (push) Has been cancelled
CI / 📦 Meteor Build (production) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], fips) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Publish Docker Images (ghcr.io) (push) Has been cancelled
CI / 📦 Track Image Sizes (push) Has been cancelled
CI / 🔎 Code Check - Action Lint (push) Has been cancelled
CI / 🔎 Code Check (push) Has been cancelled
CI / 🔨 Test Storybook (push) Has been cancelled
CI / 🔨 Test Unit (push) Has been cancelled
CI / 🔨 Test API (CE) (push) Has been cancelled
CI / 🔨 Test API Livechat (CE) (push) Has been cancelled
CI / 🔨 Test UI (CE) (push) Has been cancelled
CI / 🔨 Test API (EE) (push) Has been cancelled
CI / 🔨 Test API Livechat (EE) (push) Has been cancelled
CI / 🔨 Test API Apps (deno-runtime - EE) (push) Has been cancelled
CI / 🔨 Test UI (EE) (push) Has been cancelled
CI / 🔨 Test API (FIPS) (push) Has been cancelled
CI / 🔨 Test API Livechat (FIPS) (push) Has been cancelled
CI / 🔨 Test UI (FIPS) (push) Has been cancelled
CI / 🔨 Test Federation Matrix (push) Has been cancelled
CI / 📊 Report Coverage (push) Has been cancelled
CI / ✅ Tests Done (push) Has been cancelled
CI / 🚀 Publish build assets (push) Has been cancelled
CI / 🚀 Publish Docker Images (DockerHub) (push) Has been cancelled
CI / 🚀 Notify external services (push) Has been cancelled
CI / Update Version Durability (push) Has been cancelled
Release / Release (push) Has been cancelled
Auto-close duplicate issues / auto-close-duplicates (push) Failing after 1s
Code scanning - action / CodeQL-Build (push) Failing after 1s
This commit is contained in:
350
apps/meteor/client/lib/autotranslate/autotranslate.spec.ts
Normal file
350
apps/meteor/client/lib/autotranslate/autotranslate.spec.ts
Normal file
@@ -0,0 +1,350 @@
|
||||
import type { IMessage, ISupportedLanguage, ITranslatedMessage, MessageAttachmentDefault } from '@rocket.chat/core-typings';
|
||||
import mem from 'mem';
|
||||
|
||||
import { AutoTranslate, createAutoTranslateMessageStreamHandler } from './autotranslate';
|
||||
import { createFakeMessage, createFakeSubscription, createFakeUser } from '../../../tests/mocks/data';
|
||||
import { Messages, Subscriptions, Users } from '../../stores';
|
||||
import { sdk } from '../SDKClient';
|
||||
import { hasPermission } from '../authorization';
|
||||
import { settings } from '../settings';
|
||||
import { userIdStore } from '../user';
|
||||
|
||||
jest.mock('../SDKClient', () => ({
|
||||
sdk: {
|
||||
call: jest.fn(),
|
||||
rest: { get: jest.fn() },
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('../authorization', () => ({
|
||||
hasPermission: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('../settings', () => ({
|
||||
settings: {
|
||||
peek: jest.fn(),
|
||||
observe: jest.fn(() => jest.fn()),
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('../../cachedStores', () => ({
|
||||
PermissionsCachedStore: {
|
||||
useReady: { subscribe: jest.fn(() => jest.fn()) },
|
||||
},
|
||||
}));
|
||||
|
||||
const USER_ID = 'userId';
|
||||
const USERNAME = 'john.doe';
|
||||
const ROOM_ID = 'roomId';
|
||||
|
||||
const asMock = (fn: unknown) => fn as jest.Mock;
|
||||
|
||||
const flushPromises = async () => {
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
};
|
||||
|
||||
const signInAs = ({ language }: { language?: string } = {}) => {
|
||||
Users.state.store(createFakeUser({ _id: USER_ID, username: USERNAME, language }));
|
||||
userIdStore.setState(USER_ID);
|
||||
};
|
||||
|
||||
describe('AutoTranslate', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
asMock(settings.observe).mockReturnValue(jest.fn());
|
||||
asMock(sdk.call).mockResolvedValue({});
|
||||
asMock(sdk.rest.get).mockResolvedValue({ languages: [] });
|
||||
|
||||
Messages.state.replaceAll([]);
|
||||
Subscriptions.state.replaceAll([]);
|
||||
Users.state.replaceAll([]);
|
||||
userIdStore.setState(undefined);
|
||||
|
||||
mem.clear(AutoTranslate.findSubscriptionByRid);
|
||||
|
||||
AutoTranslate.initialized = false;
|
||||
AutoTranslate.providersMetadata = {};
|
||||
AutoTranslate.messageIdsToWait = {};
|
||||
AutoTranslate.supportedLanguages = [];
|
||||
});
|
||||
|
||||
describe('getLanguage', () => {
|
||||
it('should prefer the language configured on the subscription', () => {
|
||||
signInAs({ language: 'en' });
|
||||
Subscriptions.state.store(createFakeSubscription({ rid: ROOM_ID, autoTranslateLanguage: 'fr' }));
|
||||
|
||||
expect(AutoTranslate.getLanguage(ROOM_ID)).toBe('fr');
|
||||
});
|
||||
|
||||
it('should fall back to the user language when the subscription has none', () => {
|
||||
signInAs({ language: 'de' });
|
||||
Subscriptions.state.store(createFakeSubscription({ rid: ROOM_ID, autoTranslateLanguage: undefined }));
|
||||
|
||||
expect(AutoTranslate.getLanguage(ROOM_ID)).toBe('de');
|
||||
});
|
||||
|
||||
it('should fall back to the user language when the room has no subscription', () => {
|
||||
signInAs({ language: 'de' });
|
||||
|
||||
expect(AutoTranslate.getLanguage(ROOM_ID)).toBe('de');
|
||||
});
|
||||
|
||||
it('should keep a regional code that is supported', () => {
|
||||
signInAs();
|
||||
AutoTranslate.supportedLanguages = [{ language: 'zh-HK', name: 'Chinese (Hong Kong)' }] as ISupportedLanguage[];
|
||||
Subscriptions.state.store(createFakeSubscription({ rid: ROOM_ID, autoTranslateLanguage: 'zh-HK' }));
|
||||
|
||||
expect(AutoTranslate.getLanguage(ROOM_ID)).toBe('zh-HK');
|
||||
});
|
||||
|
||||
it('should fall back to the base language when the regional code is not supported', () => {
|
||||
signInAs();
|
||||
AutoTranslate.supportedLanguages = [{ language: 'pt', name: 'Portuguese' }] as ISupportedLanguage[];
|
||||
Subscriptions.state.store(createFakeSubscription({ rid: ROOM_ID, autoTranslateLanguage: 'pt-BR' }));
|
||||
|
||||
expect(AutoTranslate.getLanguage(ROOM_ID)).toBe('pt');
|
||||
});
|
||||
|
||||
it('should fall back to the base language when the supported list has not loaded yet', () => {
|
||||
signInAs();
|
||||
AutoTranslate.supportedLanguages = undefined;
|
||||
Subscriptions.state.store(createFakeSubscription({ rid: ROOM_ID, autoTranslateLanguage: 'pt-BR' }));
|
||||
|
||||
expect(AutoTranslate.getLanguage(ROOM_ID)).toBe('pt');
|
||||
});
|
||||
});
|
||||
|
||||
describe('translateAttachments', () => {
|
||||
beforeEach(() => {
|
||||
signInAs();
|
||||
});
|
||||
|
||||
it('should return attachments untouched when they are not translatable', () => {
|
||||
const attachments = [{ text: 'hello' }] as MessageAttachmentDefault[];
|
||||
|
||||
expect(AutoTranslate.translateAttachments(attachments, 'fr', false)).toBe(attachments);
|
||||
expect(attachments[0].text).toBe('hello');
|
||||
});
|
||||
|
||||
it('should replace the text with the translation and keep the original', () => {
|
||||
const attachments = [
|
||||
{ author_name: 'someone', text: 'hello', translations: { fr: 'bonjour' } },
|
||||
] as unknown as MessageAttachmentDefault[];
|
||||
|
||||
AutoTranslate.translateAttachments(attachments, 'fr', false);
|
||||
|
||||
expect(attachments[0].text).toBe('bonjour');
|
||||
expect((attachments[0] as any).translations.original).toBe('hello');
|
||||
});
|
||||
|
||||
it('should keep the original text when showing the inverse translation', () => {
|
||||
const attachments = [
|
||||
{ author_name: 'someone', text: 'hello', translations: { fr: 'bonjour' } },
|
||||
] as unknown as MessageAttachmentDefault[];
|
||||
|
||||
AutoTranslate.translateAttachments(attachments, 'fr', true);
|
||||
|
||||
expect(attachments[0].text).toBe('hello');
|
||||
});
|
||||
|
||||
it('should replace the description with the translation for the given language', () => {
|
||||
const attachments = [
|
||||
{ author_name: 'someone', description: 'a picture', translations: { fr: 'une image' } },
|
||||
] as unknown as MessageAttachmentDefault[];
|
||||
|
||||
AutoTranslate.translateAttachments(attachments, 'fr', false);
|
||||
|
||||
expect(attachments[0].description).toBe('une image');
|
||||
});
|
||||
|
||||
it('should keep the original description when showing the inverse translation', () => {
|
||||
const attachments = [
|
||||
{ author_name: 'someone', description: 'a picture', translations: { fr: 'une image' } },
|
||||
] as unknown as MessageAttachmentDefault[];
|
||||
|
||||
AutoTranslate.translateAttachments(attachments, 'fr', true);
|
||||
|
||||
expect(attachments[0].description).toBe('a picture');
|
||||
});
|
||||
|
||||
it('should not translate attachments authored by the current user', () => {
|
||||
const attachments = [
|
||||
{ author_name: USERNAME, text: 'hello', translations: { fr: 'bonjour' } },
|
||||
] as unknown as MessageAttachmentDefault[];
|
||||
|
||||
AutoTranslate.translateAttachments(attachments, 'fr', false);
|
||||
|
||||
expect(attachments[0].text).toBe('hello');
|
||||
});
|
||||
|
||||
it('should leave the text alone when there is no translation for the language', () => {
|
||||
const attachments = [
|
||||
{ author_name: 'someone', text: 'hello', translations: { de: 'hallo' } },
|
||||
] as unknown as MessageAttachmentDefault[];
|
||||
|
||||
AutoTranslate.translateAttachments(attachments, 'fr', false);
|
||||
|
||||
expect(attachments[0].text).toBe('hello');
|
||||
});
|
||||
|
||||
it('should translate nested attachments', () => {
|
||||
const attachments = [
|
||||
{
|
||||
author_name: 'someone',
|
||||
text: 'outer',
|
||||
translations: { fr: 'exterieur' },
|
||||
attachments: [{ author_name: 'someone', text: 'inner', translations: { fr: 'interieur' } }],
|
||||
},
|
||||
] as unknown as MessageAttachmentDefault[];
|
||||
|
||||
AutoTranslate.translateAttachments(attachments, 'fr', false);
|
||||
|
||||
expect((attachments[0] as any).attachments[0].text).toBe('interieur');
|
||||
});
|
||||
});
|
||||
|
||||
describe('init', () => {
|
||||
const enableAutoTranslate = () => {
|
||||
asMock(settings.peek).mockReturnValue(true);
|
||||
asMock(hasPermission).mockReturnValue(true);
|
||||
userIdStore.setState(USER_ID);
|
||||
};
|
||||
|
||||
it('should load provider metadata and supported languages when enabled, signed in and permitted', async () => {
|
||||
enableAutoTranslate();
|
||||
asMock(sdk.call).mockResolvedValue({ google: { name: 'google', displayName: 'Google' } });
|
||||
asMock(sdk.rest.get).mockResolvedValue({ languages: [{ language: 'fr', name: 'French' }] });
|
||||
|
||||
AutoTranslate.init();
|
||||
await flushPromises();
|
||||
|
||||
expect(AutoTranslate.providersMetadata).toEqual({ google: { name: 'google', displayName: 'Google' } });
|
||||
expect(AutoTranslate.supportedLanguages).toEqual([{ language: 'fr', name: 'French' }]);
|
||||
expect(AutoTranslate.initialized).toBe(true);
|
||||
});
|
||||
|
||||
it('should not subscribe again when already initialized', () => {
|
||||
AutoTranslate.initialized = true;
|
||||
|
||||
AutoTranslate.init();
|
||||
|
||||
expect(settings.observe).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not request providers when the setting is disabled', () => {
|
||||
asMock(settings.peek).mockReturnValue(false);
|
||||
asMock(hasPermission).mockReturnValue(true);
|
||||
userIdStore.setState(USER_ID);
|
||||
|
||||
AutoTranslate.init();
|
||||
|
||||
expect(sdk.call).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not request providers when there is no signed-in user', () => {
|
||||
asMock(settings.peek).mockReturnValue(true);
|
||||
asMock(hasPermission).mockReturnValue(true);
|
||||
userIdStore.setState(undefined);
|
||||
|
||||
AutoTranslate.init();
|
||||
|
||||
expect(sdk.call).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not request providers when the user lacks the auto-translate permission', () => {
|
||||
asMock(settings.peek).mockReturnValue(true);
|
||||
asMock(hasPermission).mockReturnValue(false);
|
||||
userIdStore.setState(USER_ID);
|
||||
|
||||
AutoTranslate.init();
|
||||
|
||||
expect(sdk.call).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not throw and should keep defaults when loading providers fails', async () => {
|
||||
enableAutoTranslate();
|
||||
asMock(sdk.call).mockRejectedValue(new Error('autotranslate disabled'));
|
||||
asMock(sdk.rest.get).mockRejectedValue(new Error('autotranslate disabled'));
|
||||
const consoleError = jest.spyOn(console, 'error').mockImplementation(() => undefined);
|
||||
|
||||
AutoTranslate.init();
|
||||
await flushPromises();
|
||||
|
||||
expect(AutoTranslate.providersMetadata).toEqual({});
|
||||
expect(consoleError).toHaveBeenCalledWith('autotranslate disabled');
|
||||
|
||||
consoleError.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
describe('createAutoTranslateMessageStreamHandler', () => {
|
||||
beforeEach(() => {
|
||||
signInAs();
|
||||
asMock(settings.peek).mockReturnValue(false);
|
||||
asMock(hasPermission).mockReturnValue(false);
|
||||
userIdStore.setState(USER_ID);
|
||||
});
|
||||
|
||||
const storeMessage = (overrides: Partial<ITranslatedMessage> = {}) => {
|
||||
const message = {
|
||||
...createFakeMessage<IMessage>({ rid: ROOM_ID, msg: 'hello', u: { _id: 'anotherUserId', username: 'someone' } }),
|
||||
...overrides,
|
||||
} as ITranslatedMessage;
|
||||
Messages.state.store(message);
|
||||
return message;
|
||||
};
|
||||
|
||||
it('should ignore messages authored by the current user', () => {
|
||||
const handler = createAutoTranslateMessageStreamHandler();
|
||||
Subscriptions.state.store(createFakeSubscription({ rid: ROOM_ID, autoTranslate: true }));
|
||||
const message = storeMessage({ u: { _id: USER_ID, username: USERNAME } });
|
||||
|
||||
handler(message);
|
||||
|
||||
expect(Messages.state.get(message._id)?.autoTranslateFetching).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should flag an untranslated message as fetching when the subscription auto-translates', () => {
|
||||
Subscriptions.state.store(createFakeSubscription({ rid: ROOM_ID, autoTranslate: true, autoTranslateLanguage: 'fr' }));
|
||||
const handler = createAutoTranslateMessageStreamHandler();
|
||||
const message = storeMessage();
|
||||
|
||||
handler(message);
|
||||
|
||||
expect(Messages.state.get(message._id)?.autoTranslateFetching).toBe(true);
|
||||
});
|
||||
|
||||
it('should not flag a message that already carries the translation', () => {
|
||||
Subscriptions.state.store(createFakeSubscription({ rid: ROOM_ID, autoTranslate: true, autoTranslateLanguage: 'fr' }));
|
||||
const handler = createAutoTranslateMessageStreamHandler();
|
||||
const message = storeMessage({ translations: { fr: 'bonjour' } });
|
||||
|
||||
handler(message);
|
||||
|
||||
expect(Messages.state.get(message._id)?.autoTranslateFetching).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should clear the fetching flag when auto-translate was turned off while waiting', () => {
|
||||
Subscriptions.state.store(createFakeSubscription({ rid: ROOM_ID, autoTranslate: false }));
|
||||
const handler = createAutoTranslateMessageStreamHandler();
|
||||
const message = storeMessage({ autoTranslateFetching: true });
|
||||
AutoTranslate.messageIdsToWait[message._id] = true;
|
||||
|
||||
handler(message);
|
||||
|
||||
expect(Messages.state.get(message._id)?.autoTranslateFetching).toBeUndefined();
|
||||
expect(AutoTranslate.messageIdsToWait[message._id]).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should clear a stale fetching flag when the room has no auto-translating subscription', () => {
|
||||
const handler = createAutoTranslateMessageStreamHandler();
|
||||
const message = storeMessage({ autoTranslateFetching: true });
|
||||
|
||||
handler(message);
|
||||
|
||||
expect(Messages.state.get(message._id)?.autoTranslateFetching).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
170
apps/meteor/client/lib/autotranslate/autotranslate.ts
Normal file
170
apps/meteor/client/lib/autotranslate/autotranslate.ts
Normal file
@@ -0,0 +1,170 @@
|
||||
import type { IRoom, ISubscription, ISupportedLanguage, ITranslatedMessage, MessageAttachmentDefault } from '@rocket.chat/core-typings';
|
||||
import { isTranslatedMessageAttachment } from '@rocket.chat/core-typings';
|
||||
import mem from 'mem';
|
||||
|
||||
import { PermissionsCachedStore } from '../../cachedStores';
|
||||
import { Messages, Subscriptions, Users } from '../../stores';
|
||||
import { hasTranslationLanguageInAttachments, hasTranslationLanguageInMessage } from '../../views/room/MessageList/lib/autoTranslate';
|
||||
import { sdk } from '../SDKClient';
|
||||
import { hasPermission } from '../authorization';
|
||||
import { settings } from '../settings';
|
||||
import { getUserId, userIdStore } from '../user';
|
||||
|
||||
let userLanguage = 'en';
|
||||
let username = '';
|
||||
|
||||
const refreshUserCache = () => {
|
||||
const uid = userIdStore.getState();
|
||||
const user = uid ? Users.use.getState().get(uid) : undefined;
|
||||
if (!user) return;
|
||||
userLanguage = user.language || 'en';
|
||||
username = user.username || '';
|
||||
};
|
||||
refreshUserCache();
|
||||
userIdStore.subscribe(refreshUserCache);
|
||||
Users.use.subscribe(refreshUserCache);
|
||||
|
||||
export const AutoTranslate = {
|
||||
initialized: false,
|
||||
providersMetadata: {} as { [providerNamer: string]: { name: string; displayName: string } },
|
||||
messageIdsToWait: {} as { [messageId: string]: boolean },
|
||||
supportedLanguages: [] as ISupportedLanguage[] | undefined,
|
||||
|
||||
findSubscriptionByRid: mem((rid) => Subscriptions.state.find((record) => record.rid === rid)),
|
||||
|
||||
getLanguage(rid: IRoom['_id']): string {
|
||||
let subscription: ISubscription | undefined;
|
||||
if (rid) {
|
||||
subscription = this.findSubscriptionByRid(rid);
|
||||
}
|
||||
const language = (subscription?.autoTranslateLanguage || userLanguage || window.defaultUserLanguage?.()) as string;
|
||||
if (language.indexOf('-') !== -1) {
|
||||
if (!(this.supportedLanguages || []).some((supportedLanguage) => supportedLanguage.language === language)) {
|
||||
return language.slice(0, 2);
|
||||
}
|
||||
}
|
||||
return language;
|
||||
},
|
||||
|
||||
translateAttachments(
|
||||
attachments: MessageAttachmentDefault[],
|
||||
language: string,
|
||||
autoTranslateShowInverse: boolean,
|
||||
): MessageAttachmentDefault[] {
|
||||
if (!isTranslatedMessageAttachment(attachments)) {
|
||||
return attachments;
|
||||
}
|
||||
for (const attachment of attachments) {
|
||||
if (attachment.author_name !== username) {
|
||||
if (attachment.text && attachment.translations && attachment.translations[language]) {
|
||||
attachment.translations.original = attachment.text;
|
||||
|
||||
if (autoTranslateShowInverse) {
|
||||
attachment.text = attachment.translations.original;
|
||||
} else {
|
||||
attachment.text = attachment.translations[language];
|
||||
}
|
||||
}
|
||||
|
||||
if (attachment.description && attachment.translations && attachment.translations[language]) {
|
||||
attachment.translations.original = attachment.description;
|
||||
|
||||
if (autoTranslateShowInverse) {
|
||||
attachment.description = attachment.translations.original;
|
||||
} else {
|
||||
attachment.description = attachment.translations[language];
|
||||
}
|
||||
}
|
||||
|
||||
if (attachment.attachments && attachment.attachments.length > 0) {
|
||||
// @ts-expect-error - not sure what to do with this
|
||||
attachment.attachments = this.translateAttachments(attachment.attachments, language);
|
||||
}
|
||||
}
|
||||
}
|
||||
return attachments;
|
||||
},
|
||||
|
||||
init(): void {
|
||||
if (this.initialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
const loadProviders = async () => {
|
||||
try {
|
||||
const [providersMetadata, supportedLanguagesResponse] = await Promise.all([
|
||||
sdk.call('autoTranslate.getProviderUiMetadata'),
|
||||
sdk.rest.get('/v1/autotranslate.getSupportedLanguages', { targetLanguage: 'en' }),
|
||||
]);
|
||||
this.providersMetadata = providersMetadata;
|
||||
this.supportedLanguages = supportedLanguagesResponse.languages;
|
||||
} catch (e: unknown) {
|
||||
// Avoid unwanted error message on UI when autotranslate is disabled while fetching data
|
||||
console.error((e as Error).message);
|
||||
}
|
||||
};
|
||||
|
||||
let loaded = false;
|
||||
const unsubs: Array<() => void> = [];
|
||||
const tryLoad = async () => {
|
||||
if (loaded) return;
|
||||
if (!settings.peek('AutoTranslate_Enabled') || !userIdStore.getState() || !hasPermission('auto-translate')) {
|
||||
return;
|
||||
}
|
||||
loaded = true;
|
||||
unsubs.splice(0).forEach((unsubscribe) => unsubscribe());
|
||||
await loadProviders();
|
||||
};
|
||||
|
||||
unsubs.push(userIdStore.subscribe(() => void tryLoad()));
|
||||
unsubs.push(settings.observe('AutoTranslate_Enabled', () => void tryLoad()));
|
||||
unsubs.push(PermissionsCachedStore.useReady.subscribe(() => void tryLoad()));
|
||||
|
||||
void tryLoad();
|
||||
|
||||
Subscriptions.use.subscribe(() => {
|
||||
mem.clear(this.findSubscriptionByRid);
|
||||
});
|
||||
|
||||
this.initialized = true;
|
||||
},
|
||||
};
|
||||
|
||||
export const createAutoTranslateMessageStreamHandler = (): ((message: ITranslatedMessage) => void) => {
|
||||
AutoTranslate.init();
|
||||
|
||||
return (message: ITranslatedMessage): void => {
|
||||
if (message.u && message.u._id !== getUserId()) {
|
||||
const subscription = AutoTranslate.findSubscriptionByRid(message.rid);
|
||||
const language = AutoTranslate.getLanguage(message.rid);
|
||||
if (
|
||||
subscription &&
|
||||
subscription.autoTranslate === true &&
|
||||
message.msg &&
|
||||
(!message.translations ||
|
||||
(!hasTranslationLanguageInMessage(message, language) && !hasTranslationLanguageInAttachments(message.attachments, language)))
|
||||
) {
|
||||
Messages.state.update(
|
||||
(record) => record._id === message._id,
|
||||
(record) => ({
|
||||
...record,
|
||||
autoTranslateFetching: true,
|
||||
}),
|
||||
);
|
||||
} else if (AutoTranslate.messageIdsToWait[message._id] !== undefined && subscription && subscription.autoTranslate !== true) {
|
||||
Messages.state.update(
|
||||
(record) => record._id === message._id,
|
||||
({ autoTranslateFetching: _, ...record }) => ({
|
||||
...record,
|
||||
}),
|
||||
);
|
||||
delete AutoTranslate.messageIdsToWait[message._id];
|
||||
} else if (message.autoTranslateFetching === true) {
|
||||
Messages.state.update(
|
||||
(record) => record._id === message._id,
|
||||
({ autoTranslateFetching: _, ...record }) => record,
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
1
apps/meteor/client/lib/autotranslate/index.ts
Normal file
1
apps/meteor/client/lib/autotranslate/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { AutoTranslate, createAutoTranslateMessageStreamHandler } from './autotranslate';
|
||||
Reference in New Issue
Block a user