{
+ return (
+
+
+
+
Only available in Enterprise Advanced.',
+ values: {strong: (msg: string) => {msg}, br:
},
+ })}
+ learnMoreURL='https://docs.mattermost.com'
+ featureDiscoveryImage={
+
+ }
+ />
+
+
+
+ );
+};
+
+export default AutoTranslationFeatureDiscovery;
diff --git a/webapp/channels/src/components/admin_console/feature_discovery/features/images/auto_translate_svg.tsx b/webapp/channels/src/components/admin_console/feature_discovery/features/images/auto_translate_svg.tsx
new file mode 100644
index 00000000000..39094e95da0
--- /dev/null
+++ b/webapp/channels/src/components/admin_console/feature_discovery/features/images/auto_translate_svg.tsx
@@ -0,0 +1,185 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import * as React from 'react';
+
+type SvgProps = {
+ width?: number;
+ height?: number;
+}
+
+const AutoTranslationSVG = (props: SvgProps) => (
+
+);
+export default AutoTranslationSVG;
diff --git a/webapp/channels/src/components/admin_console/localization/auto_translation.tsx b/webapp/channels/src/components/admin_console/localization/auto_translation.tsx
new file mode 100644
index 00000000000..6f5e798572e
--- /dev/null
+++ b/webapp/channels/src/components/admin_console/localization/auto_translation.tsx
@@ -0,0 +1,135 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import React, {useCallback, useMemo, useState} from 'react';
+import {defineMessages, FormattedMessage} from 'react-intl';
+
+import type {AutoTranslationSettings} from '@mattermost/types/config';
+
+import DropdownSetting from 'components/admin_console/dropdown_setting';
+import {
+ AdminSection,
+ SectionContent,
+ SectionHeader,
+} from 'components/admin_console/system_properties/controls';
+import Toggle from 'components/toggle';
+
+import AutoTranslationInfo from './auto_translation_info';
+import LibreTranslateSettings from './libreTranslate_settings';
+
+import type {SystemConsoleCustomSettingsComponentProps} from '../schema_admin_settings';
+import './localization.scss';
+import type {SearchableStrings} from '../types';
+
+const messages = defineMessages({
+ enableAutoTranslationTitle: {
+ id: 'admin.site.localization.enableAutoTranslationTitle',
+ defaultMessage: 'Auto-translation',
+ },
+ enableAutoTranslationDescription: {
+ id: 'admin.site.localization.enableAutoTranslationDescription',
+ defaultMessage: 'Configure auto-translation for channels and direct messages',
+ },
+});
+
+export const searchableStrings: SearchableStrings = Object.values(messages);
+
+export default function AutoTranslation(props: SystemConsoleCustomSettingsComponentProps) {
+ const [autoTranslationSettings, setAutoTranslationSettings] = useState
(props.value as AutoTranslationSettings);
+
+ const handleChange = useCallback((id: string, value: any) => {
+ const updatedSettings = {
+ ...autoTranslationSettings,
+ [id]: value,
+ };
+ setAutoTranslationSettings(updatedSettings);
+ props.onChange(props.id, updatedSettings);
+ }, [props, autoTranslationSettings]);
+
+ const handleToggle = useCallback(() => {
+ handleChange('Enable', !autoTranslationSettings.Enable);
+ }, [autoTranslationSettings, handleChange]);
+
+ const providerHelpTextValues = useMemo(() => ({
+ br:
,
+ strong: (msg: React.ReactNode) => {msg},
+ }), []);
+
+ const on = (
+
+ );
+ const off = (
+
+ );
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {autoTranslationSettings.Enable ? on : off}
+
+
+
+
+
+ {autoTranslationSettings.Enable &&
+
+
+ }
+ values={[
+ {value: 'libretranslate', text: 'LibreTranslate'},
+ ]}
+ helpText={
+
+ }
+ value={autoTranslationSettings.Provider || 'libretranslate'}
+ disabled={props.disabled || props.setByEnv}
+ setByEnv={props.setByEnv}
+ onChange={handleChange}
+ />
+ {autoTranslationSettings.Provider === 'libretranslate' &&
+
+ }
+
+
+ }
+
+ );
+}
diff --git a/webapp/channels/src/components/admin_console/localization/auto_translation_info.scss b/webapp/channels/src/components/admin_console/localization/auto_translation_info.scss
new file mode 100644
index 00000000000..8a3c26b66bf
--- /dev/null
+++ b/webapp/channels/src/components/admin_console/localization/auto_translation_info.scss
@@ -0,0 +1,3 @@
+.autoTranslationInfo .sectionNoticeTitle {
+ margin-bottom: 0;
+}
diff --git a/webapp/channels/src/components/admin_console/localization/auto_translation_info.tsx b/webapp/channels/src/components/admin_console/localization/auto_translation_info.tsx
new file mode 100644
index 00000000000..f7c498e9fc6
--- /dev/null
+++ b/webapp/channels/src/components/admin_console/localization/auto_translation_info.tsx
@@ -0,0 +1,32 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import React from 'react';
+import {FormattedMessage, useIntl} from 'react-intl';
+
+import './auto_translation_info.scss';
+
+import SectionNotice from 'components/section_notice';
+
+const AutoTranslationInfo = () => {
+ const intl = useIntl();
+ return (
+
+
+ }
+ text={intl.formatMessage({
+ id: 'admin.site.localization.autoTranslationInfoSecondary',
+ defaultMessage: 'When multiple languages are detected, users will be prompted to enable auto-translation. [Learn more](https://docs.mattermost.com/).',
+ })}
+ type='info'
+ />
+
+ );
+};
+
+export default React.memo(AutoTranslationInfo);
diff --git a/webapp/channels/src/components/admin_console/localization/libreTranslate_settings.tsx b/webapp/channels/src/components/admin_console/localization/libreTranslate_settings.tsx
new file mode 100644
index 00000000000..b2e1bb1503c
--- /dev/null
+++ b/webapp/channels/src/components/admin_console/localization/libreTranslate_settings.tsx
@@ -0,0 +1,89 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import React, {useCallback, useState} from 'react';
+import {defineMessage, FormattedMessage} from 'react-intl';
+
+import type {AutoTranslationSettings} from '@mattermost/types/config';
+
+import TextSetting from 'components/admin_console/text_setting';
+import ExternalLink from 'components/external_link';
+
+import './localization.scss';
+
+import {type SystemConsoleCustomSettingsComponentProps} from '../schema_admin_settings';
+
+type LibreTranslateSettings = {
+ URL: string;
+ APIKey: string;
+}
+
+export default function LibreTranslateSettings(props: SystemConsoleCustomSettingsComponentProps) {
+ const values = props.value as AutoTranslationSettings;
+ const [libreTranslateSettings, setLibreTranslateSettings] = useState(values.LibreTranslate);
+
+ const handleChange = useCallback((id: string, value: any) => {
+ const updatedSettings = {
+ ...libreTranslateSettings,
+ [id]: value,
+ };
+ setLibreTranslateSettings(updatedSettings);
+ props.onChange('LibreTranslate', updatedSettings);
+ }, [props, libreTranslateSettings]);
+
+ return (
+ <>
+
+ }
+ placeholder={defineMessage({
+ id: 'admin.site.localization.autoTranslationProviderLibreTranslateURLExample',
+ defaultMessage: 'e.g.: "https://libretranslate.yourdomain.com"',
+ })}
+ type='url'
+ value={libreTranslateSettings.URL}
+ setByEnv={false}
+ onChange={handleChange}
+ disabled={props.disabled}
+ />
+
+ }
+ placeholder={defineMessage({
+ id: 'admin.site.localization.autoTranslationProviderLibreTranslateAPIKeyExample',
+ defaultMessage: 'Enter LibreTranslate API Key',
+ })}
+ helpText={
+ (
+
+ {msg}
+
+ ),
+ }}
+ />}
+ type='password'
+ value={libreTranslateSettings.APIKey}
+ setByEnv={false}
+ onChange={handleChange}
+ disabled={props.disabled}
+ />
+ >
+ );
+}
diff --git a/webapp/channels/src/components/admin_console/localization/localization.scss b/webapp/channels/src/components/admin_console/localization/localization.scss
new file mode 100644
index 00000000000..ed1d61bc414
--- /dev/null
+++ b/webapp/channels/src/components/admin_console/localization/localization.scss
@@ -0,0 +1,31 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+.autotranslation-section-header {
+ display: flex;
+ width: 100%;
+ min-width: 0; // prevents overflow in flex layouts
+ align-items: flex-start;
+ justify-content: space-between;
+ gap: 24px;
+}
+
+.autotranslation-section-toggle {
+ display: flex;
+ align-items: center;
+ align-self: center;
+}
+
+.localization-section-title {
+ margin: unset;
+ font-size: 16px;
+}
+
+.localization-section-description {
+ margin-bottom: 0;
+ color: var(--center-channel-color-72);
+ font-size: 14px;
+ font-style: normal;
+ font-weight: 400;
+}
+
diff --git a/webapp/channels/src/components/admin_console/localization/localization.tsx b/webapp/channels/src/components/admin_console/localization/localization.tsx
new file mode 100644
index 00000000000..9b96d47fea1
--- /dev/null
+++ b/webapp/channels/src/components/admin_console/localization/localization.tsx
@@ -0,0 +1,192 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import React, {useCallback, useMemo, useState} from 'react';
+import {defineMessages, FormattedMessage} from 'react-intl';
+import styled from 'styled-components';
+
+import type {LocalizationSettings} from '@mattermost/types/config';
+
+import BooleanSetting from 'components/admin_console/boolean_setting';
+import DropdownSetting from 'components/admin_console/dropdown_setting';
+import MultiSelectSetting from 'components/admin_console/multiselect_settings';
+import {
+ SectionContent,
+ SectionHeader,
+} from 'components/admin_console/system_properties/controls';
+import ExternalLink from 'components/external_link';
+
+import * as I18n from 'i18n/i18n.jsx';
+
+import type {SystemConsoleCustomSettingsComponentProps} from '../schema_admin_settings';
+import type {SearchableStrings} from '../types';
+import './localization.scss';
+
+const locales = I18n.getAllLanguages();
+
+const AdminSection = styled.section.attrs({className: 'AdminPanel'})`
+ && {
+ overflow: visible;
+ margin-top: 0;
+ }
+`;
+
+const messages = defineMessages({
+ langTitle: {
+ id: 'admin.site.localization.languages.title',
+ defaultMessage: 'Languages',
+ },
+ langDescription: {
+ id: 'admin.site.localization.languages.description',
+ defaultMessage: 'Choose which languages should be the defaults',
+ },
+ serverLocaleTitle: {
+ id: 'admin.general.localization.serverLocaleTitle',
+ defaultMessage: 'Default Server Language:',
+ },
+ serverLocaleDescription: {
+ id: 'admin.general.localization.serverLocaleDescription',
+ defaultMessage: 'Default language for system messages.',
+ },
+ clientLocaleTitle: {
+ id: 'admin.general.localization.clientLocaleTitle',
+ defaultMessage: 'Default Client Language:',
+ },
+ clientLocaleDescription: {
+ id: 'admin.general.localization.clientLocaleDescription',
+ defaultMessage: "Default language for newly created users and pages where the user hasn't logged in.",
+ },
+ availableLocalesTitle: {
+ id: 'admin.general.localization.availableLocalesTitle',
+ defaultMessage: 'Available Languages:',
+ },
+ availableLocalesDescription: {
+ id: 'admin.general.localization.availableLocalesDescription',
+ defaultMessage: "Set which languages are available for users in Settings > Display > Language (leave this field blank to have all supported languages available). If you're manually adding new languages, the Default Client Language must be added before saving this setting.\n \nWould like to help with translations? Join the Mattermost Translation Server to contribute.",
+ },
+ availableLocalesNoResults: {
+ id: 'admin.general.localization.availableLocalesNoResults',
+ defaultMessage: 'No results found',
+ },
+ enableExperimentalLocalesTitle: {
+ id: 'admin.general.localization.enableExperimentalLocalesTitle',
+ defaultMessage: 'Enable Experimental Locales:',
+ },
+ enableExperimentalLocalesDescription: {
+ id: 'admin.general.localization.enableExperimentalLocalesDescription',
+ defaultMessage: 'When true, it allows users to select experimental (e.g., in progress) languages.',
+ },
+});
+
+export const searchableStrings: SearchableStrings = Object.values(messages);
+
+export default function Localization(props: SystemConsoleCustomSettingsComponentProps) {
+ const [localizationSettings, setLocalizationSettings] = useState(props.value as LocalizationSettings);
+
+ const handleChange = useCallback((id: string, value: any) => {
+ const updatedSettings = {
+ ...localizationSettings,
+ [id]: value,
+ };
+ setLocalizationSettings(updatedSettings);
+ props.onChange(props.id, updatedSettings);
+ }, [props, localizationSettings]);
+
+ const availableLanguages = useMemo(() => {
+ const values: Array<{value: string; text: string; order: number}> = [];
+ for (const l of Object.values(locales)) {
+ values.push({value: l.value, text: l.name, order: l.order});
+ }
+ values.sort((a, b) => a.order - b.order);
+ return values;
+ }, []);
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ }
+ values={availableLanguages}
+ helpText={
+
+ }
+ value={localizationSettings.DefaultServerLocale || availableLanguages[0].value}
+ disabled={props.disabled}
+ setByEnv={props.setByEnv}
+ onChange={handleChange}
+ />
+
+ }
+ values={availableLanguages}
+ helpText={
+
+ }
+ value={localizationSettings.DefaultClientLocale || availableLanguages[0].value}
+ disabled={props.disabled}
+ setByEnv={props.setByEnv}
+ onChange={handleChange}
+ />
+
+ }
+ values={availableLanguages}
+ helpText={
+ (
+
+ {msg}
+
+ ),
+ strong: (msg: React.ReactNode) => {msg},
+ }}
+ />
+ }
+ selected={(localizationSettings.AvailableLocales.split(',')) || []}
+ disabled={props.disabled}
+ setByEnv={props.setByEnv}
+ onChange={(changedId, value) => handleChange(changedId, value.join(','))}
+ noOptionsMessage={
+
+ }
+ />
+
+ }
+ helpText={
+
+ }
+ value={localizationSettings.EnableExperimentalLocales}
+ disabled={props.disabled}
+ setByEnv={props.setByEnv}
+ onChange={handleChange}
+ />
+
+
+ );
+}
diff --git a/webapp/channels/src/components/widgets/tag/sku_tag.test.tsx b/webapp/channels/src/components/widgets/tag/sku_tag.test.tsx
new file mode 100644
index 00000000000..9155ba589ca
--- /dev/null
+++ b/webapp/channels/src/components/widgets/tag/sku_tag.test.tsx
@@ -0,0 +1,21 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import React from 'react';
+
+import {render, screen} from 'tests/react_testing_utils';
+import {LicenseSkus} from 'utils/constants';
+
+import SkuTag from './sku_tag';
+
+describe('components/widgets/tag/SkuTag', () => {
+ test('should match the ENTRY SKU', () => {
+ render();
+ expect(screen.getByText('ENTRY')).toBeInTheDocument();
+ });
+
+ test('should match the ENTERPRISE ADVANCED SKU', () => {
+ render();
+ expect(screen.getByText('ENTERPRISE ADVANCED')).toBeInTheDocument();
+ });
+});
diff --git a/webapp/channels/src/components/widgets/tag/sku_tag.tsx b/webapp/channels/src/components/widgets/tag/sku_tag.tsx
new file mode 100644
index 00000000000..5e1c8feaf87
--- /dev/null
+++ b/webapp/channels/src/components/widgets/tag/sku_tag.tsx
@@ -0,0 +1,50 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import classNames from 'classnames';
+import React, {useMemo} from 'react';
+
+import {LicenseSkus} from 'utils/constants';
+
+import Tag from './tag';
+import type {TagSize} from './tag';
+
+type Props = {
+ className?: string;
+ size?: TagSize;
+ sku: LicenseSkus;
+};
+
+const SkuTag = ({className = '', size = 'xs', sku}: Props) => {
+ const namedSku = useMemo(() => {
+ switch (sku) {
+ case LicenseSkus.Starter:
+ return 'STARTER';
+ case LicenseSkus.Professional:
+ return 'PROFESSIONAL';
+ case LicenseSkus.Enterprise:
+ return 'ENTERPRISE';
+ case LicenseSkus.E10:
+ return 'ENTERPRISE E10';
+ case LicenseSkus.E20:
+ return 'ENTERPRISE E20';
+ case LicenseSkus.EnterpriseAdvanced:
+ return 'ENTERPRISE ADVANCED';
+ case LicenseSkus.Entry:
+ return 'ENTRY';
+ default:
+ return 'UNKNOWN';
+ }
+ }, [sku]);
+
+ return (
+
+ );
+};
+
+export default SkuTag;
diff --git a/webapp/channels/src/i18n/en.json b/webapp/channels/src/i18n/en.json
index 34b04721559..64308419a3d 100644
--- a/webapp/channels/src/i18n/en.json
+++ b/webapp/channels/src/i18n/en.json
@@ -389,6 +389,8 @@
"admin.authentication.openid": "OpenID Connect",
"admin.authentication.saml": "SAML 2.0",
"admin.authentication.signup": "Signup",
+ "admin.auto_translation_feature_discovery.copy": "Effortlessly collaborate across languages with auto-translation. Messages in shared channels are instantly translated based on each user’s language preference—no extra steps required.{br}Only available in Enterprise Advanced.",
+ "admin.auto_translation_feature_discovery.title": "Remove language barriers with auto-translation",
"admin.banner.heading": "Note:",
"admin.billing.company_info_display.companyDetails": "Company Details",
"admin.billing.company_info_display.detailsProvided": "Your company name and address",
@@ -2751,6 +2753,21 @@
"admin.site.emoji": "Emoji",
"admin.site.fileSharingDownloads": "File Sharing and Downloads",
"admin.site.localization": "Localization",
+ "admin.site.localization.auto_translation.off": "Off",
+ "admin.site.localization.auto_translation.on": "On",
+ "admin.site.localization.autoTranslationInfo": "Auto-translation must also be enabled in each channel where it's needed.",
+ "admin.site.localization.autoTranslationInfoSecondary": "When multiple languages are detected, users will be prompted to enable auto-translation. [Learn more](https://docs.mattermost.com/).",
+ "admin.site.localization.autoTranslationProviderDescription": "NOTE: If using external translation services (e.g., cloud based),{br}message data may be processed outside of your environment.",
+ "admin.site.localization.autoTranslationProviderLibreTranslateAPIKeyDescription": "If your LibreTranslate server requires an API key, enter it here. Otherwise, leave this field blank. View LibreTranslate docs for API Key management.",
+ "admin.site.localization.autoTranslationProviderLibreTranslateAPIKeyExample": "Enter LibreTranslate API Key",
+ "admin.site.localization.autoTranslationProviderLibreTranslateAPIKeyTitle": "LibreTranslate API Key:",
+ "admin.site.localization.autoTranslationProviderLibreTranslateURLExample": "e.g.: \"https://libretranslate.yourdomain.com\"",
+ "admin.site.localization.autoTranslationProviderLibreTranslateURLTitle": "LibreTranslate API Endpoint:",
+ "admin.site.localization.autoTranslationProviderTitle": "Translation Service:",
+ "admin.site.localization.enableAutoTranslationDescription": "Configure auto-translation for channels and direct messages",
+ "admin.site.localization.enableAutoTranslationTitle": "Auto-translation",
+ "admin.site.localization.languages.description": "Choose which languages should be the defaults",
+ "admin.site.localization.languages.title": "Languages",
"admin.site.move_thread": "Move thread",
"admin.site.notices": "Notices",
"admin.site.posts": "Posts",
diff --git a/webapp/platform/types/src/config.ts b/webapp/platform/types/src/config.ts
index 7f27320f515..0e7e5aa8162 100644
--- a/webapp/platform/types/src/config.ts
+++ b/webapp/platform/types/src/config.ts
@@ -739,6 +739,20 @@ export type LocalizationSettings = {
EnableExperimentalLocales: boolean;
};
+export type AutoTranslationSettings = {
+ Enable: boolean;
+ Provider: '' | 'libretranslate';
+ LibreTranslate: {
+ URL: string;
+ APIKey: string;
+ };
+ TimeoutMs: {
+ NewPost: number;
+ Fetch: number;
+ Notification: number;
+ };
+};
+
export type SamlSettings = {
Enable: boolean;
EnableSyncWithLdap: boolean;
@@ -1052,6 +1066,7 @@ export type AdminConfig = {
ConnectedWorkspacesSettings: ConnectedWorkspacesSettings;
AccessControlSettings: AccessControlSettings;
ContentFlaggingSettings: ContentFlaggingSettings;
+ AutoTranslationSettings: AutoTranslationSettings;
};
export type ReplicaLagSetting = {