diff --git a/config.js b/config.js index 1d262f6511..a5f65e999b 100644 --- a/config.js +++ b/config.js @@ -375,6 +375,14 @@ var config = { // // Whether the feature should be enabled or not. // enabled: false, + // // Translation languages. + // // Available languages can be found in + // // ./src/react/features/transcribing/translation-languages.json. + // translationLanguages: ['en', 'es', 'fr', 'ro'], + + // // Important languages to show on the top of the language list. + // translationLanguagesHead: ['en'], + // // If true transcriber will use the application language. // // The application language is either explicitly set by participants in their settings or automatically // // detected based on the environment, e.g. if the app is opened in a chrome instance which @@ -1349,6 +1357,8 @@ var config = { */ mouseMoveCallbackInterval: 1000, + hiddenDomain: 'tdomokos.jitsi.net', + /** Use this array to configure which notifications will be shown to the user The items correspond to the title or description key of that notification diff --git a/lang/translation-languages.json b/lang/translation-languages.json new file mode 100644 index 0000000000..f4a4a9f485 --- /dev/null +++ b/lang/translation-languages.json @@ -0,0 +1,111 @@ +{ + "af": "Afrikaans", + "am": "Amharic", + "ar": "Arabic", + "az": "Azerbaijani", + "be": "Belarusian", + "bg": "Bulgarian", + "bn": "Bengali", + "bs": "Bosnian", + "ca": "Catalan", + "ceb": "Cebuano", + "co": "Corsican", + "cs": "Czech", + "cy": "Welsh", + "da": "Danish", + "de": "German", + "el": "Greek", + "en": "English", + "eo": "Esperanto", + "es": "Spanish", + "et": "Estonian", + "eu": "Basque", + "fa": "Persian", + "fi": "Finnish", + "fr": "French", + "fy": "Frisian", + "ga": "Irish", + "gd": "Scots Gaelic", + "gl": "Galician", + "gu": "Gujarati", + "ha": "Hausa", + "haw": "Hawaiian", + "he": "Hebrew", + "hi": "Hindi", + "hmn": "Hmong", + "hr": "Croatian", + "ht": "Haitian Creole", + "hu": "Hungarian", + "hy": "Armenian", + "id": "Indonesian", + "ig": "Igbo", + "is": "Icelandic", + "it": "Italian", + "ja": "Japanese", + "jv": "Javanese", + "ka": "Georgian", + "kk": "Kazakh", + "km": "Khmer", + "kn": "Kannada", + "ko": "Korean", + "ku": "Kurdish", + "ky": "Kyrgyz", + "la": "Latin", + "lb": "Luxembourgish", + "lo": "Lao", + "lt": "Lithuanian", + "lv": "Latvian", + "mg": "Malagasy", + "mi": "Maori", + "mk": "Macedonian", + "ml": "Malayalam", + "mn": "Mongolian", + "mr": "Marathi", + "ms": "Malay", + "mt": "Maltese", + "my": "Myanmar (Burmese)", + "ne": "Nepali", + "nl": "Dutch", + "no": "Norwegian", + "ny": "Nyanja (Chichewa)", + "or": "Odia (Oriya)", + "pa": "Punjabi", + "pl": "Polish", + "ps": "Pashto", + "pt": "Portuguese (Portugal, Brazil)", + "ro": "Romanian", + "ru": "Russian", + "rw": "Kinyarwanda", + "sd": "Sindhi", + "si": "Sinhala (Sinhalese)", + "sk": "Slovak", + "sl": "Slovenian", + "sm": "Samoan", + "sn": "Shona", + "so": "Somali", + "sq": "Albanian", + "sr": "Serbian", + "st": "Sesotho", + "su": "Sundanese", + "sv": "Swedish", + "sw": "Swahili", + "ta": "Tamil", + "te": "Telugu", + "tg": "Tajik", + "th": "Thai", + "tk": "Turkmen", + "tl": "Tagalog (Filipino)", + "tr": "Turkish", + "tt": "Tatar", + "ug": "Uyghur", + "uk": "Ukrainian", + "ur": "Urdu", + "uz": "Uzbek", + "vi": "Vietnamese", + "xh": "Xhosa", + "yi": "Yiddish", + "yo": "Yoruba", + "zh-CN": "Chinese (Simplified)", + "zh-TW": "Chinese (Traditional)", + "zu": "Zulu" +} diff --git a/react/features/base/config/configType.ts b/react/features/base/config/configType.ts index 5904347cfd..e30f508efc 100644 --- a/react/features/base/config/configType.ts +++ b/react/features/base/config/configType.ts @@ -464,6 +464,8 @@ export interface IConfig { disableStartForAll?: boolean; enabled?: boolean; preferredLanguage?: string; + translationLanguages?: Array; + translationLanguagesHead?: Array; useAppLanguage?: boolean; }; useHostPageLocalStorage?: boolean; diff --git a/react/features/base/i18n/i18next.ts b/react/features/base/i18n/i18next.ts index 983c14b718..38623b7583 100644 --- a/react/features/base/i18n/i18next.ts +++ b/react/features/base/i18n/i18next.ts @@ -7,6 +7,7 @@ import _ from 'lodash'; import LANGUAGES_RESOURCES from '../../../../lang/languages.json'; import MAIN_RESOURCES from '../../../../lang/main.json'; +import TRANSLATION_LANGUAGES_RESOURCES from '../../../../lang/translation-languages.json'; import { I18NEXT_INITIALIZED, LANGUAGE_CHANGED } from './actionTypes'; // eslint-disable-next-line lines-around-comment @@ -36,22 +37,21 @@ const COUNTRIES = _.merge({}, COUNTRIES_RESOURCES, COUNTRIES_RESOURCES_OVERRIDES export const LANGUAGES: Array = Object.keys(LANGUAGES_RESOURCES); /** - * The languages for the top section of the translation language list. + * The available/supported translation languages. + * + * @public + * @type {Array} + */ +export const TRANSLATION_LANGUAGES: Array = Object.keys(TRANSLATION_LANGUAGES_RESOURCES); + +/** + * The available/supported translation languages head. (Languages displayed on the top ). * * @public * @type {Array} */ export const TRANSLATION_LANGUAGES_HEAD: Array = [ 'en' ]; -/** - * The languages to explude from the translation language list. - * - * @public - * @type {Array} - */ -export const TRANSLATION_LANGUAGES_EXCLUDE: Array -= [ 'enGB', 'esUS', 'frCA', 'hsb', 'kab', 'ptBR', 'zhCN', 'zhTW' ]; - /** * The default language. * @@ -77,7 +77,7 @@ const options = { escapeValue: false // not needed for react as it escapes by default }, load: 'languageOnly', - ns: [ 'main', 'languages', 'countries' ], + ns: [ 'main', 'languages', 'countries', 'translation-languages' ], react: { // re-render when a new resource bundle is added bindI18nStore: 'added', @@ -109,6 +109,12 @@ i18next.addResourceBundle( LANGUAGES_RESOURCES, /* deep */ true, /* overwrite */ true); +i18next.addResourceBundle( + DEFAULT_LANGUAGE, + 'translation-languages', + TRANSLATION_LANGUAGES_RESOURCES, + /* deep */ true, + /* overwrite */ true); i18next.addResourceBundle( DEFAULT_LANGUAGE, 'main', diff --git a/react/features/base/i18n/index.js b/react/features/base/i18n/index.js index 668a1a35ec..cc4c047e99 100644 --- a/react/features/base/i18n/index.js +++ b/react/features/base/i18n/index.js @@ -4,4 +4,4 @@ export * from './functions'; // TODO Eventually (e.g. when the non-React Web app is rewritten into React), it // should not be necessary to export i18next. export { default as i18next, DEFAULT_LANGUAGE, - LANGUAGES, TRANSLATION_LANGUAGES_HEAD, TRANSLATION_LANGUAGES_EXCLUDE } from './i18next'; + LANGUAGES, TRANSLATION_LANGUAGES, TRANSLATION_LANGUAGES_HEAD, i18n } from './i18next'; diff --git a/react/features/subtitles/actions.web.js b/react/features/subtitles/actions.web.js index d062924284..d9673bf9af 100644 --- a/react/features/subtitles/actions.web.js +++ b/react/features/subtitles/actions.web.js @@ -11,7 +11,7 @@ export * from './actions.any'; * type: UPDATE_TRANSLATION_LANGUAGE * }} */ -export function toggleLangugeSelectorDialog() { +export function toggleLanguageSelectorDialog() { return function(dispatch: (Object) => Object) { dispatch(toggleDialog(LanguageSelectorDialogWeb)); }; diff --git a/react/features/subtitles/components/ClosedCaptionButton.web.js b/react/features/subtitles/components/ClosedCaptionButton.web.js index 2a12e07379..655ade2a93 100644 --- a/react/features/subtitles/components/ClosedCaptionButton.web.js +++ b/react/features/subtitles/components/ClosedCaptionButton.web.js @@ -3,7 +3,7 @@ import { translate } from '../../base/i18n'; import { IconClosedCaption } from '../../base/icons'; import { connect } from '../../base/redux'; -import { toggleLangugeSelectorDialog } from '../actions'; +import { toggleLanguageSelectorDialog } from '../actions'; import { AbstractClosedCaptionButton, @@ -20,7 +20,9 @@ class ClosedCaptionButton tooltip = 'transcribing.ccButtonTooltip'; label = 'toolbar.startSubtitles'; labelProps = { - language: this.props.t(this.props._language) + language: this.props.t(this.props._language), + languages: this.props.t(this.props.languages), + languagesHead: this.props.t(this.props.languagesHead) }; /** @@ -31,7 +33,7 @@ class ClosedCaptionButton _handleClickOpenLanguageSelector() { const { dispatch } = this.props; - dispatch(toggleLangugeSelectorDialog()); + dispatch(toggleLanguageSelectorDialog()); } } diff --git a/react/features/subtitles/components/LanguageListItem.tsx b/react/features/subtitles/components/LanguageListItem.tsx index 3425b128e0..3e6ce208e7 100644 --- a/react/features/subtitles/components/LanguageListItem.tsx +++ b/react/features/subtitles/components/LanguageListItem.tsx @@ -31,7 +31,11 @@ const useStyles = makeStyles()((theme: Theme) => { display: 'flex', color: theme.palette.text01, alignItems: 'center', - fontSize: '14px' + fontSize: '14px', + cursor: 'pointer', + '&:hover': { + backgroundColor: theme.palette.ui04 + } }, iconWrapper: { margin: '4px 10px', diff --git a/react/features/subtitles/components/LanguageSelectorDialog.web.tsx b/react/features/subtitles/components/LanguageSelectorDialog.web.tsx index 793f1bc3f2..6c376aabb7 100644 --- a/react/features/subtitles/components/LanguageSelectorDialog.web.tsx +++ b/react/features/subtitles/components/LanguageSelectorDialog.web.tsx @@ -6,15 +6,17 @@ import { IState } from '../../app/types'; // @ts-ignore import { Dialog } from '../../base/dialog'; // @ts-ignore -import { LANGUAGES, TRANSLATION_LANGUAGES_EXCLUDE, TRANSLATION_LANGUAGES_HEAD } from '../../base/i18n'; +import { TRANSLATION_LANGUAGES, TRANSLATION_LANGUAGES_HEAD } from '../../base/i18n'; import { connect } from '../../base/redux/functions'; // @ts-ignore -import { setRequestingSubtitles, toggleLangugeSelectorDialog, updateTranslationLanguage } from '../actions'; +import { setRequestingSubtitles, toggleLanguageSelectorDialog, updateTranslationLanguage } from '../actions'; import LanguageList from './LanguageList'; interface ILanguageSelectorDialogProps { _language: string; + _translationLanguages: Array; + _translationLanguagesHead: Array; t: Function; } @@ -23,18 +25,21 @@ interface ILanguageSelectorDialogProps { * * @returns {React$Element} */ -const LanguageSelectorDialog = ({ _language }: ILanguageSelectorDialogProps) => { +const LanguageSelectorDialog = ({ _language, _translationLanguages, _translationLanguagesHead }: + ILanguageSelectorDialogProps) => { + const dispatch = useDispatch(); const off = 'transcribing.subtitlesOff'; const [ language, setLanguage ] = useState(off); - const importantLanguages = TRANSLATION_LANGUAGES_HEAD.map((lang: string) => `languages:${lang}`); - const fixedItems = [ off, ...importantLanguages ]; - - const languages = LANGUAGES - .filter((lang: string) => !TRANSLATION_LANGUAGES_EXCLUDE.includes(lang)) - .map((lang: string) => `languages:${lang}`) - .filter((lang: string) => !(lang === language || importantLanguages.includes(lang))); + const languagesHead = _translationLanguagesHead.map((lang: string) => `translation-languages:${lang}`); + // The off and the head languages are always on the top of the list. But once you are selecting + // a language from the translationLanguages, that language is moved under the fixedItems list, + // until a new languages is selected. FixedItems keep their positions. + const fixedItems = [ off, ...languagesHead ]; + const languages = _translationLanguages + .map((lang: string) => `translation-languages:${lang}`) + .filter((lang: string) => !(lang === language || languagesHead.includes(lang))); const listItems = (fixedItems.includes(language) ? [ ...fixedItems, ...languages ] @@ -55,7 +60,7 @@ const LanguageSelectorDialog = ({ _language }: ILanguageSelectorDialogProps) => setLanguage(e); dispatch(updateTranslationLanguage(e)); dispatch(setRequestingSubtitles(e !== off)); - dispatch(toggleLangugeSelectorDialog()); + dispatch(toggleLanguageSelectorDialog()); }, [ _language ]); return ( @@ -81,17 +86,18 @@ const LanguageSelectorDialog = ({ _language }: ILanguageSelectorDialogProps) => * @returns {Props} */ function mapStateToProps(state: IState) { - const { - conference - } = state['features/base/conference']; + const { conference } = state['features/base/conference']; + const { _language } = state['features/subtitles']; + const { transcription } = state['features/base/config']; - const { - _language - } = state['features/subtitles']; + const languages = transcription?.translationLanguages ?? TRANSLATION_LANGUAGES; + const languagesHead = transcription?.translationLanguagesHead ?? TRANSLATION_LANGUAGES_HEAD; return { _conference: conference, - _language + _language, + _translationLanguages: languages, + _translationLanguagesHead: languagesHead }; } diff --git a/react/features/subtitles/middleware.js b/react/features/subtitles/middleware.js index d083fd1ac7..53577f2f8a 100644 --- a/react/features/subtitles/middleware.js +++ b/react/features/subtitles/middleware.js @@ -1,4 +1,5 @@ // @flow +import i18next from 'i18next'; import { MiddlewareRegistry } from '../base/redux'; @@ -115,7 +116,7 @@ function _endpointMessageReceived({ dispatch, getState }, next, action) { newTranscriptMessage)); } else if (json.type === JSON_TYPE_TRANSCRIPTION_RESULT - && !translationLanguage) { + && i18next.language === translationLanguage) { // Displays interim and final results without any translation if // translations are disabled. @@ -186,7 +187,7 @@ function _requestingSubtitlesChange({ getState }) { if (requestingSubtitles) { conference.setLocalParticipantProperty( P_NAME_TRANSLATION_LANGUAGE, - _language.replace('languages:', '')); + _language.replace('translation-languages:', '')); } }