diff --git a/common/config/rush/pnpm-lock.yaml b/common/config/rush/pnpm-lock.yaml index e85a145fe79..1a06373ad0b 100644 --- a/common/config/rush/pnpm-lock.yaml +++ b/common/config/rush/pnpm-lock.yaml @@ -34,9 +34,6 @@ importers: '@faker-js/faker': specifier: ^8.4.1 version: 8.4.1 - '@hcengineering/measurements': - specifier: ^0.7.7 - version: 0.7.7 '@hcengineering/measurements-otlp': specifier: ^0.7.7 version: 0.7.7(encoding@0.1.13) @@ -913,6 +910,9 @@ importers: '@rush-temp/pod-telegram-bot': specifier: file:./projects/pod-telegram-bot.tgz version: file:projects/pod-telegram-bot.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(@swc/core@1.13.5)(babel-jest@29.7.0(@babel/core@7.23.9))(encoding@0.1.13)(gcp-metadata@5.3.0(encoding@0.1.13))(snappy@7.2.2)(socks@2.8.3) + '@rush-temp/pod-translate': + specifier: file:./projects/pod-translate.tgz + version: file:projects/pod-translate.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(@swc/core@1.13.5)(babel-jest@29.7.0(@babel/core@7.23.9))(encoding@0.1.13)(zod@3.24.2) '@rush-temp/pod-worker': specifier: file:./projects/pod-worker.tgz version: file:projects/pod-worker.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(@swc/core@1.13.5)(babel-jest@29.7.0(@babel/core@7.23.9))(webpack-cli@5.1.4) @@ -1963,6 +1963,9 @@ importers: form-data: specifier: ^4.0.0 version: 4.0.0 + franc-min: + specifier: ^6.2.0 + version: 6.2.0 gaxios: specifier: ^5.0.1 version: 5.1.3(encoding@0.1.13) @@ -4556,7 +4559,7 @@ packages: version: 0.0.0 '@rush-temp/communication-server@file:projects/communication-server.tgz': - resolution: {integrity: sha512-NqgsYYbjEFDp0w3T3g9pjDaYwAfMfqE5PkZoZ2dY/YTudJcRfnYV4uHuyIDKw6HceDKfLZSd3zScMXjR5m3uNA==, tarball: file:projects/communication-server.tgz} + resolution: {integrity: sha512-o3A6bkNKoPybTtuDaNrpgzglNb4Z/YaKOO9XoMdGkbtnVpVBwehnffKiphB9GKZ0SNzUYXRCXbYbBgPnTIoC5g==, tarball: file:projects/communication-server.tgz} version: 0.0.0 '@rush-temp/communication-shared@file:projects/communication-shared.tgz': @@ -4580,7 +4583,7 @@ packages: version: 0.0.0 '@rush-temp/contact@file:projects/contact.tgz': - resolution: {integrity: sha512-J2aOTd+H6SyUEBDB5+3eOs3OiWnqvYy0cGRs/Cpz6+Oqbunn4Hvj+laQqIps3c2t8bQH3u2IBiI3e0jzaPK/Fw==, tarball: file:projects/contact.tgz} + resolution: {integrity: sha512-R1RbX6FWTeUs0TqyJYDUZUiMV7W4mwZ3lWCWFIJfFSGcWE+5Sk+aww2jyVIYh8eiunUo/uSBwXIbSaYBcoyrmQ==, tarball: file:projects/contact.tgz} version: 0.0.0 '@rush-temp/controlled-documents-assets@file:projects/controlled-documents-assets.tgz': @@ -4940,7 +4943,7 @@ packages: version: 0.0.0 '@rush-temp/model-calendar@file:projects/model-calendar.tgz': - resolution: {integrity: sha512-KIpD8iFaZEadGlgNoSsM7/34DiKaZ8PAH7iFYy6e/sijyGvgwtTvDardwNZ6xQfACmBu8CH6zNzn3XcGrYBx5A==, tarball: file:projects/model-calendar.tgz} + resolution: {integrity: sha512-bo+cfLn56htDm9GYZ4sFPx/YFVk6hnoTq0eBHvUoGi9QQTalHRlREiv+c4gZniVpx2sH/OaUMWAXDIfkcTPXkQ==, tarball: file:projects/model-calendar.tgz} version: 0.0.0 '@rush-temp/model-card@file:projects/model-card.tgz': @@ -4960,7 +4963,7 @@ packages: version: 0.0.0 '@rush-temp/model-contact@file:projects/model-contact.tgz': - resolution: {integrity: sha512-F8VPbPQ/7oKCWNW/sqc7ul2CZJ8pRUTW2Tu7wpUU9pCo6AJOMPwB4mBI+M35pWYpoj7FLiojZmwATERl3JH/0g==, tarball: file:projects/model-contact.tgz} + resolution: {integrity: sha512-2SQj04no5IbxGV5MxaO7k3CmOt1jt9Os0/s3yeRbbgHHFNl6uJ6yaOo9Qfq6eaquBfV9nPzJuzmBH2VsTkKA9w==, tarball: file:projects/model-contact.tgz} version: 0.0.0 '@rush-temp/model-controlled-documents@file:projects/model-controlled-documents.tgz': @@ -5408,7 +5411,7 @@ packages: version: 0.0.0 '@rush-temp/pod-server@file:projects/pod-server.tgz': - resolution: {integrity: sha512-z4m5kjmQbBtpRI+5G9R5oylh5Z6IuWrTpC82zkrShOHVYLnLwKwDciVN5bbfAlISdb59ThXyiOzIA5Dr/gfl8g==, tarball: file:projects/pod-server.tgz} + resolution: {integrity: sha512-3n0/hgmREmz0totaUlNjkuPuS+kGz5ekL8OO3z/HsQyI/2vDx+p3O0DsDga2m1fGt296awP3MP/iqQIWwFPcNw==, tarball: file:projects/pod-server.tgz} version: 0.0.0 '@rush-temp/pod-sign@file:projects/pod-sign.tgz': @@ -5427,6 +5430,10 @@ packages: resolution: {integrity: sha512-JxdvigAw/njFUV7DoAoepnhVtYX3vvIuL4cvHy4ZpK5LYx6PjxP5AOcUD3dTb/WZXDAnZAc6APKpAKgmHA5UXQ==, tarball: file:projects/pod-telegram.tgz} version: 0.0.0 + '@rush-temp/pod-translate@file:projects/pod-translate.tgz': + resolution: {integrity: sha512-n2WumWwe2S5s0LBStlOuDVU/99zH1tTOvejx65KReHGy8c/8GzsfbcPapYs2zkLeKnInC0d99iC02Dl32Bnhjw==, tarball: file:projects/pod-translate.tgz} + version: 0.0.0 + '@rush-temp/pod-worker@file:projects/pod-worker.tgz': resolution: {integrity: sha512-6sP8dB+Kv5PnITQ4XVHj9ZHrGXy/7Xbp3VbhdJhVZqvYusL19i2iRQ+RBmuVKeKH/94YIFoRoHn/Y7LDAYCNgQ==, tarball: file:projects/pod-worker.tgz} version: 0.0.0 @@ -8221,6 +8228,9 @@ packages: code-red@1.0.4: resolution: {integrity: sha512-7qJWqItLA8/VPVlKJlFXU+NBlo/qyfs39aJcuMT/2ere32ZqvF5OSxgdM5xOfJJ7O429gg2HM47y8v9P+9wrNw==} + collapse-white-space@2.1.0: + resolution: {integrity: sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw==} + collect-v8-coverage@1.0.2: resolution: {integrity: sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==} @@ -9829,6 +9839,9 @@ packages: fraction.js@4.3.7: resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==} + franc-min@6.2.0: + resolution: {integrity: sha512-1uDIEUSlUZgvJa2AKYR/dmJC66v/PvGQ9mWfI9nOr/kPpMFyvswK0gPXOwpYJYiYD008PpHLkGfG58SPjQJFxw==} + fresh@0.5.2: resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} engines: {node: '>= 0.6'} @@ -11697,6 +11710,9 @@ packages: mz@2.7.0: resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} + n-gram@2.0.2: + resolution: {integrity: sha512-S24aGsn+HLBxUGVAUFOwGpKs7LBcG4RudKU//eWzt/mQ97/NMKQxDWHyHx63UNWk/OOdihgmzoETn1tf5nQDzQ==} + nanoid@3.3.11: resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} @@ -13716,6 +13732,9 @@ packages: resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} hasBin: true + trigram-utils@2.0.1: + resolution: {integrity: sha512-nfWIXHEaB+HdyslAfMxSqWKDdmqY9I32jS7GnqpdWQnLH89r6A5sdk3fDVYqGAZ0CrT8ovAFSAo6HRiWcWNIGQ==} + triple-beam@1.4.1: resolution: {integrity: sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==} engines: {node: '>= 14.0.0'} @@ -19188,6 +19207,7 @@ snapshots: eslint-plugin-import: 2.29.1(eslint@8.56.0) eslint-plugin-n: 15.7.0(eslint@8.56.0) eslint-plugin-promise: 6.1.1(eslint@8.56.0) + franc-min: 6.2.0 jest: 29.7.0(@types/node@22.15.29)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3)) prettier: 3.2.5 typescript: 5.8.3 @@ -25691,6 +25711,42 @@ snapshots: - ts-node - utf-8-validate + '@rush-temp/pod-translate@file:projects/pod-translate.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(@swc/core@1.13.5)(babel-jest@29.7.0(@babel/core@7.23.9))(encoding@0.1.13)(zod@3.24.2)': + dependencies: + '@hcengineering/measurements': 0.7.7 + '@tsconfig/node16': 1.0.4 + '@types/jest': 29.5.12 + '@types/node': 22.15.29 + '@types/uuid': 8.3.4 + '@types/ws': 8.5.11 + '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.3))(eslint@8.56.0)(typescript@5.8.3) + '@typescript-eslint/parser': 6.21.0(eslint@8.56.0)(typescript@5.8.3) + dotenv: 16.0.3 + esbuild: 0.25.9 + eslint: 8.56.0 + eslint-config-standard-with-typescript: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.3))(eslint@8.56.0)(typescript@5.8.3))(eslint-plugin-import@2.29.1(eslint@8.56.0))(eslint-plugin-n@15.7.0(eslint@8.56.0))(eslint-plugin-promise@6.1.1(eslint@8.56.0))(eslint@8.56.0)(typescript@5.8.3) + eslint-plugin-import: 2.29.1(eslint@8.56.0) + eslint-plugin-n: 15.7.0(eslint@8.56.0) + eslint-plugin-node: 11.1.0(eslint@8.56.0) + eslint-plugin-promise: 6.1.1(eslint@8.56.0) + jest: 29.7.0(@types/node@22.15.29)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3)) + openai: 4.56.0(encoding@0.1.13)(zod@3.24.2) + prettier: 3.2.5 + ts-jest: 29.1.2(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.25.9)(jest@29.7.0(@types/node@22.15.29)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3)))(typescript@5.8.3) + ts-node: 10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3) + typescript: 5.8.3 + transitivePeerDependencies: + - '@babel/core' + - '@jest/types' + - '@swc/core' + - '@swc/wasm' + - babel-jest + - babel-plugin-macros + - encoding + - node-notifier + - supports-color + - zod + '@rush-temp/pod-worker@file:projects/pod-worker.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(@swc/core@1.13.5)(babel-jest@29.7.0(@babel/core@7.23.9))(webpack-cli@5.1.4)': dependencies: '@temporalio/worker': 1.12.3(esbuild@0.25.9)(webpack-cli@5.1.4) @@ -33317,6 +33373,8 @@ snapshots: estree-walker: 3.0.3 periscopic: 3.1.0 + collapse-white-space@2.1.0: {} + collect-v8-coverage@1.0.2: {} color-convert@1.9.3: @@ -35231,6 +35289,10 @@ snapshots: fraction.js@4.3.7: {} + franc-min@6.2.0: + dependencies: + trigram-utils: 2.0.1 + fresh@0.5.2: {} fs-extra@10.1.0: @@ -37483,6 +37545,8 @@ snapshots: object-assign: 4.1.1 thenify-all: 1.6.0 + n-gram@2.0.2: {} + nanoid@3.3.11: {} napi-macros@2.0.0: @@ -39745,6 +39809,11 @@ snapshots: tree-kill@1.2.2: {} + trigram-utils@2.0.1: + dependencies: + collapse-white-space: 2.1.0 + n-gram: 2.0.2 + triple-beam@1.4.1: {} truncate-utf8-bytes@1.0.2: diff --git a/communication b/communication index bbccecb9dbb..047325b57fd 160000 --- a/communication +++ b/communication @@ -1 +1 @@ -Subproject commit bbccecb9dbb90802781d37e1f167021ce3b88c0d +Subproject commit 047325b57fd86be9ffce343ec499e72289c29f47 diff --git a/dev/docker-compose.yaml b/dev/docker-compose.yaml index 330dc472f24..53476d9f3cf 100644 --- a/dev/docker-compose.yaml +++ b/dev/docker-compose.yaml @@ -562,6 +562,22 @@ services: - QUEUE_CONFIG=${QUEUE_CONFIG} - QUEUE_REGION=cockroach restart: unless-stopped + translate: + image: hardcoreeng/translate + extra_hosts: + - 'huly.local:host-gateway' + depends_on: + redpanda: + condition: service_started + account: + condition: service_started + environment: + - SECRET=secret + - ACCOUNTS_URL=http://huly.local:3000 + - HULYLAKE_URL=http://huly.local:8096 + - QUEUE_CONFIG=${QUEUE_CONFIG} + - QUEUE_REGION=cockroach + restart: unless-stopped backup-cockroach: image: hardcoreeng/backup extra_hosts: diff --git a/models/contact/package.json b/models/contact/package.json index c3c5b3ccaaf..96278ca9bdb 100644 --- a/models/contact/package.json +++ b/models/contact/package.json @@ -50,6 +50,7 @@ "@hcengineering/model-view": "^0.6.0", "@hcengineering/model-workbench": "^0.6.1", "@hcengineering/model-card": "^0.6.0", + "@hcengineering/model-preference": "^0.6.0", "@hcengineering/notification": "^0.6.23", "@hcengineering/platform": "^0.6.11", "@hcengineering/setting": "^0.6.17", diff --git a/models/contact/src/index.ts b/models/contact/src/index.ts index 70459acd782..94271f2b9f0 100644 --- a/models/contact/src/index.ts +++ b/models/contact/src/index.ts @@ -33,7 +33,8 @@ import { type PersonSpace, type SocialIdentity, type Status, - type SocialIdentityProvider + type SocialIdentityProvider, + type Translation } from '@hcengineering/contact' import { AccountRole, @@ -90,6 +91,7 @@ import { type AnyComponent } from '@hcengineering/ui/src/types' import { type Action } from '@hcengineering/view' import contact from './plugin' import { PaletteColorIndexes } from '@hcengineering/ui/src/colors' +import preference, { TPreference } from '@hcengineering/model-preference' export { contactId } from '@hcengineering/contact' export { contactOperation } from './migration' @@ -291,6 +293,14 @@ export class TUserRole extends TDoc implements UserRole { role!: Ref } +@Model(contact.class.Translation, preference.class.Preference) +export class TTranslation extends TPreference implements Translation { + declare attachedTo: Ref + enabled!: boolean + translateTo?: string + dontTranslate!: string[] +} + export function createModel (builder: Builder): void { builder.createModel( TAvatarProvider, @@ -306,7 +316,8 @@ export function createModel (builder: Builder): void { TMember, TContactsTab, TPersonSpace, - TUserRole + TUserRole, + TTranslation ) builder.mixin(contact.class.PersonSpace, core.class.Class, core.mixin.TxAccessLevel, { @@ -1405,4 +1416,14 @@ export function createModel (builder: Builder): void { isCustom: true, readonly: true }) + + builder.createDoc(setting.class.SettingsCategory, core.space.Model, { + name: 'translation', + label: contact.string.AutoTranslation, + icon: view.icon.Translate, + component: contact.component.TranslationSettings, + group: 'settings-account', + role: AccountRole.Guest, + order: 1600 + }) } diff --git a/models/contact/src/plugin.ts b/models/contact/src/plugin.ts index 74eaacf87c0..5414d33bd8f 100644 --- a/models/contact/src/plugin.ts +++ b/models/contact/src/plugin.ts @@ -58,7 +58,8 @@ export default mergeIds(contactId, contact, { EmployeeFilter: '' as AnyComponent, EmployeeFilterValuePresenter: '' as AnyComponent, ChannelIcon: '' as AnyComponent, - PersonPreviewPresenter: '' as AnyComponent + PersonPreviewPresenter: '' as AnyComponent, + TranslationSettings: '' as AnyComponent }, string: { SearchEmployee: '' as IntlString, diff --git a/plugins/communication-resources/src/actions.ts b/plugins/communication-resources/src/actions.ts index e69140a349f..55b5288e98e 100644 --- a/plugins/communication-resources/src/actions.ts +++ b/plugins/communication-resources/src/actions.ts @@ -42,7 +42,13 @@ import aiBot from '@hcengineering/ai-bot' import CreateCardFromMessagePopup from './components/CreateCardFromMessagePopup.svelte' import { isCardAllowedForCommunications, showForbidden, toggleReaction, toMarkup } from './utils' -import { isMessageTranslating, messageEditingStore, threadCreateMessageStore, translateMessagesStore } from './stores' +import { + isMessageTranslating, + messageEditingStore, + showOriginalMessagesStore, + threadCreateMessageStore, + translateMessagesStore +} from './stores' export const addReaction: MessageActionFunction = async (message, card: Card, evt, onOpen, onClose) => { if (!isCardAllowedForCommunications(card)) { @@ -149,34 +155,41 @@ export const canReplyInThread: MessageActionVisibilityTester = (message: Message } export const translateMessage: MessageActionFunction = async (message: Message): Promise => { - if (isMessageTranslating(message.id)) return - const result = get(translateMessagesStore).get(message.id) + if (isMessageTranslating(message.cardId, message.id)) return + const result = get(translateMessagesStore).find((it) => it.cardId === message.cardId && it.messageId === message.id) - if (result?.result != null) { - translateMessagesStore.update((store) => { - store.set(message.id, { ...result, shown: true }) - return store - }) + if (result != null) { + showOriginalMessagesStore.update((store) => + store.filter(([cId, mId]) => cId !== message.cardId || mId !== message.id) + ) return } translateMessagesStore.update((store) => { - store.set(message.id, { inProgress: true, shown: false }) + store.push({ inProgress: true, messageId: message.id, cardId: message.cardId }) return store }) const markup = toMarkup(message.content) - const response = await aiTranslate(markup, get(languageStore)) + const lang = get(languageStore) + const response = message?.translates?.[lang] ?? (await aiTranslate(markup, lang))?.text if (response !== undefined) { translateMessagesStore.update((store) => { - store.set(message.id, { inProgress: false, result: response.text, shown: true }) - return store + return store.map((it) => { + if (it.cardId === message.cardId && it.messageId === message.id) { + return { + ...it, + inProgress: false, + result: response + } + } + return it + }) }) } else { translateMessagesStore.update((store) => { - store.delete(message.id) - return store + return store.filter((it) => it.cardId !== message.cardId || it.messageId !== message.id) }) } } @@ -189,10 +202,9 @@ export const canTranslateMessage: MessageActionVisibilityTester = (message: Mess export const showOriginalMessage: MessageActionFunction = async (message: Message): Promise => { const messageId = message.id - translateMessagesStore.update((store) => { - const status = store.get(messageId) - if (status == null) return store - store.set(messageId, { ...status, shown: false }) + + showOriginalMessagesStore.update((store) => { + store.push([message.cardId, messageId]) return store }) } diff --git a/plugins/communication-resources/src/components/MessagesList.svelte b/plugins/communication-resources/src/components/MessagesList.svelte index 259a06b44b6..419b0fca673 100644 --- a/plugins/communication-resources/src/components/MessagesList.svelte +++ b/plugins/communication-resources/src/components/MessagesList.svelte @@ -31,6 +31,7 @@ import { createEventDispatcher, onDestroy, onMount, tick } from 'svelte' import { MessagesNavigationAnchors } from '@hcengineering/communication' import { deviceOptionsStore as deviceInfo, isAppFocusedStore } from '@hcengineering/ui' + import { translationStore } from '@hcengineering/contact-resources' import { createMessagesObserver, getGroupDay, groupMessagesByDay, MessagesGroup } from '../messages' import MessagesGroupPresenter from './message/MessagesGroupPresenter.svelte' @@ -101,6 +102,8 @@ readNotifications(new Date()) } + $: translation = $translationStore + $: reinit(position) $: query.query( @@ -123,7 +126,8 @@ autoExpand: true, threads: true, attachments: true, - reactions: true + reactions: true, + language: translation?.enabled === true ? translation?.translateTo : undefined } ) diff --git a/plugins/communication-resources/src/components/message/MessageBody.svelte b/plugins/communication-resources/src/components/message/MessageBody.svelte index 83098b32aa1..a78484eb3b7 100644 --- a/plugins/communication-resources/src/components/message/MessageBody.svelte +++ b/plugins/communication-resources/src/components/message/MessageBody.svelte @@ -14,9 +14,9 @@ --> {#if compact || hideHeader} @@ -106,12 +141,12 @@ (