diff --git a/frontend/src/locale/de.json b/frontend/src/locale/de.json index 11d6fdc3..d7900af1 100644 --- a/frontend/src/locale/de.json +++ b/frontend/src/locale/de.json @@ -1815,7 +1815,8 @@ }, "connectData": { "title": "Deine Daten verbinden", - "description": "Google Drive, SharePoint, Confluence und mehr synchronisieren" + "description": "{{services}} synchronisieren", + "more": "mehr" } }, "createAPIKey": { diff --git a/frontend/src/locale/en.json b/frontend/src/locale/en.json index e6992055..4c03953c 100644 --- a/frontend/src/locale/en.json +++ b/frontend/src/locale/en.json @@ -1821,7 +1821,8 @@ }, "connectData": { "title": "Connect your data", - "description": "Sync Google Drive, SharePoint, Confluence and more" + "description": "Sync {{services}}", + "more": "more" } }, "createAPIKey": { diff --git a/frontend/src/locale/es.json b/frontend/src/locale/es.json index 2c0321c4..f2065bf4 100644 --- a/frontend/src/locale/es.json +++ b/frontend/src/locale/es.json @@ -1815,7 +1815,8 @@ }, "connectData": { "title": "Conecta tus datos", - "description": "Sincroniza Google Drive, SharePoint, Confluence y más" + "description": "Sincroniza {{services}}", + "more": "más" } }, "createAPIKey": { diff --git a/frontend/src/locale/jp.json b/frontend/src/locale/jp.json index b224a4b1..e6ccba46 100644 --- a/frontend/src/locale/jp.json +++ b/frontend/src/locale/jp.json @@ -1804,7 +1804,8 @@ }, "connectData": { "title": "データを接続", - "description": "Google Drive、SharePoint、Confluence などを同期" + "description": "{{services}} を同期", + "more": "その他" } }, "createAPIKey": { diff --git a/frontend/src/locale/ru.json b/frontend/src/locale/ru.json index 79d12254..4007f104 100644 --- a/frontend/src/locale/ru.json +++ b/frontend/src/locale/ru.json @@ -1899,7 +1899,8 @@ }, "connectData": { "title": "Подключите свои данные", - "description": "Синхронизируйте Google Drive, SharePoint, Confluence и другие сервисы" + "description": "Синхронизируйте {{services}}", + "more": "другие сервисы" } }, "createAPIKey": { diff --git a/frontend/src/locale/zh-TW.json b/frontend/src/locale/zh-TW.json index 0392782e..4dcf1061 100644 --- a/frontend/src/locale/zh-TW.json +++ b/frontend/src/locale/zh-TW.json @@ -1804,7 +1804,8 @@ }, "connectData": { "title": "連接你的資料", - "description": "同步 Google Drive、SharePoint、Confluence 等" + "description": "同步 {{services}}", + "more": "更多服務" } }, "createAPIKey": { diff --git a/frontend/src/locale/zh.json b/frontend/src/locale/zh.json index 78557c3e..020d5dce 100644 --- a/frontend/src/locale/zh.json +++ b/frontend/src/locale/zh.json @@ -1804,7 +1804,8 @@ }, "connectData": { "title": "连接你的数据", - "description": "同步 Google Drive、SharePoint、Confluence 等" + "description": "同步 {{services}}", + "more": "更多服务" } }, "createAPIKey": { diff --git a/frontend/src/upload/Upload.test.tsx b/frontend/src/upload/Upload.test.tsx index 3a5ac6cf..5af0f062 100644 --- a/frontend/src/upload/Upload.test.tsx +++ b/frontend/src/upload/Upload.test.tsx @@ -2,7 +2,11 @@ import { act } from 'react'; import { createRoot, type Root } from 'react-dom/client'; vi.mock('react-i18next', () => ({ - useTranslation: () => ({ t: (key: string) => key }), + useTranslation: () => ({ + t: (key: string, opts?: { services?: string }) => + opts?.services ? `${key}(${opts.services})` : key, + i18n: { language: 'en' }, + }), })); const connectorsState = vi.hoisted(() => ({ @@ -136,6 +140,7 @@ describe('Upload source-type tiles', () => { const DRIVE = { key: 'google_drive', + name: 'Google Drive', icon: 'drive', sync_ingestor: 'google_drive', capabilities: ['sync'], @@ -168,11 +173,30 @@ describe('Upload source-type tiles', () => { false, ); expect(labels.at(-1)).toContain('modals.uploadDoc.connectData.title'); - expect(labels.at(-1)).toContain('modals.uploadDoc.connectData.description'); + // Named after what this instance can sync. + expect(labels.at(-1)).toContain( + 'modals.uploadDoc.connectData.description(Google Drive)', + ); expect(document.body.querySelector('h3')).toBeNull(); connectorsState.catalog = []; }); + it('names three syncing services, then the rest as more', async () => { + connectorsState.catalog = [ + DRIVE, + { ...DRIVE, key: 'share_point', name: 'SharePoint' }, + { ...DRIVE, key: 'confluence', name: 'Confluence' }, + { ...DRIVE, key: 's3', name: 'Amazon S3' }, + { ...DRIVE, key: 'off', name: 'Needs setup', available: false }, + { key: 'telegram', name: 'Telegram', capabilities: ['write'] }, + ]; + await render(); + expect(connectTile()!.textContent).toContain( + 'modals.uploadDoc.connectData.description(Google Drive, SharePoint, Confluence, and modals.uploadDoc.connectData.more)', + ); + connectorsState.catalog = []; + }); + it('opens the Connectors page on the syncing services', async () => { connectorsState.catalog = [DRIVE]; navigate.mockClear(); @@ -190,6 +214,7 @@ describe('Upload source-type tiles', () => { it('offers no Connect tile when no service can sync', async () => { connectorsState.catalog = [ { key: 'telegram', capabilities: ['write'], available: true }, + { ...DRIVE, available: false }, ]; await render(); expect(connectTile()).toBeUndefined(); diff --git a/frontend/src/upload/Upload.tsx b/frontend/src/upload/Upload.tsx index e431c95d..16b2042e 100644 --- a/frontend/src/upload/Upload.tsx +++ b/frontend/src/upload/Upload.tsx @@ -29,6 +29,8 @@ import { Separator } from '../components/ui/separator'; import { OptionCard } from '../components/ui/option-card'; import ConnectorSetupNotice from '../connectors/ConnectorSetupNotice'; import useConnectorLauncher from '../connectors/useConnectorLauncher'; +import { connectorName } from '../connectors/i18n'; +import { intlLocale } from '../utils/dateTimeUtils'; import { loadConnectors, selectConnections, @@ -474,7 +476,7 @@ function Upload({ })); const [nameTouched, setNameTouched] = useState(false); - const { t } = useTranslation(); + const { t, i18n } = useTranslation(); const dispatch = useDispatch(); const store = useStore(); @@ -1152,10 +1154,27 @@ function Upload({ }; // Services that sync into Knowledge are connected on the Connectors page, - // so the picker offers one way there instead of a tile per service. - const offersConnect = - connectorsEnabled && - connectorCatalog.some((c) => c.capabilities?.includes('sync')); + // so the picker offers one way there instead of a tile per service, named + // after the first few this instance can sync. + const syncServices = connectorsEnabled + ? connectorCatalog + .filter((c) => c.available && c.capabilities?.includes('sync')) + .map((c) => connectorName(t, c)) + : []; + const SERVICES_NAMED = 3; + const connectDescription = t('modals.uploadDoc.connectData.description', { + services: new Intl.ListFormat(intlLocale(i18n.language), { + type: 'conjunction', + }).format( + syncServices.length > SERVICES_NAMED + ? [ + ...syncServices.slice(0, SERVICES_NAMED), + t('modals.uploadDoc.connectData.more'), + ] + : syncServices, + ), + interpolation: { escapeValue: false }, + }); const renderIngestorSelection = () => { const options = UPLOAD_AND_WEB_INGESTORS.map((type) => @@ -1173,11 +1192,12 @@ function Upload({ onClick={() => handleIngestorTypeChange(option.value)} /> ))} - {offersConnect && ( + {syncServices.length > 0 && ( } title={t('modals.uploadDoc.connectData.title')} - description={t('modals.uploadDoc.connectData.description')} + description={connectDescription} onClick={() => { handleClose(); navigate('/settings/connectors?capability=sync');