From f20a50d8a63780c7779026d10c23b0bcad7717fd Mon Sep 17 00:00:00 2001 From: Vlad Piersec Date: Tue, 7 Dec 2021 12:04:33 +0200 Subject: [PATCH] feat(i18n): Allow label rewrite via advanced branding --- react/features/app/middlewares.web.js | 1 + react/features/base/i18n/actionTypes.js | 11 ++++++ react/features/base/i18n/functions.js | 22 ++++++++++-- react/features/base/i18n/i18next.js | 16 +++++++++ react/features/base/i18n/logger.js | 5 +++ react/features/base/i18n/middleware.js | 39 ++++++++++++++++++++++ react/features/dynamic-branding/reducer.js | 11 ++++++ 7 files changed, 103 insertions(+), 2 deletions(-) create mode 100644 react/features/base/i18n/actionTypes.js create mode 100644 react/features/base/i18n/logger.js create mode 100644 react/features/base/i18n/middleware.js diff --git a/react/features/app/middlewares.web.js b/react/features/app/middlewares.web.js index acb9d3ea1..08c3d74aa 100644 --- a/react/features/app/middlewares.web.js +++ b/react/features/app/middlewares.web.js @@ -1,6 +1,7 @@ // @flow import '../authentication/middleware'; +import '../base/i18n/middleware'; import '../base/devices/middleware'; import '../dynamic-branding/middleware'; import '../e2ee/middleware'; diff --git a/react/features/base/i18n/actionTypes.js b/react/features/base/i18n/actionTypes.js new file mode 100644 index 000000000..cb0752fb3 --- /dev/null +++ b/react/features/base/i18n/actionTypes.js @@ -0,0 +1,11 @@ +// @flow + +/** + * The type of (redux) action which signals that i18next has been initialized. + */ +export const I18NEXT_INITIALIZED = 'I18NEXT_INITIALIZED'; + +/** + * The type of (redux) action which signals that language has been changed. + */ +export const LANGUAGE_CHANGED = 'LANGUAGE_CHANGED'; diff --git a/react/features/base/i18n/functions.js b/react/features/base/i18n/functions.js index 9af593655..201177b65 100644 --- a/react/features/base/i18n/functions.js +++ b/react/features/base/i18n/functions.js @@ -1,6 +1,24 @@ +// @flow + import React from 'react'; import { withTranslation } from 'react-i18next'; +import i18next from './i18next'; + +/** + * Changes the main translation bundle. + * + * @param {string} language - The language e.g. 'en', 'fr'. + * @param {string} url - The url of the translation bundle. + * @returns {void} + */ +export async function changeLanguageBundle(language: string, url: string) { + const res = await fetch(url); + const bundle = await res.json(); + + i18next.addResourceBundle(language, 'main', bundle, true, true); +} + /** * Wraps a specific React Component in order to enable translations in it. * @@ -8,7 +26,7 @@ import { withTranslation } from 'react-i18next'; * @returns {Component} The React Component which wraps {@link component} and * enables translations in it. */ -export function translate(component) { +export function translate(component: any) { // Use the default list of namespaces. return withTranslation([ 'main', 'languages', 'countries' ])(component); } @@ -23,7 +41,7 @@ export function translate(component) { * @returns {ReactElement} A ReactElement which depicts the translated HTML * text. */ -export function translateToHTML(t, key, options = {}) { +export function translateToHTML(t: Function, key: string, options: Object = {}) { // eslint-disable-next-line react/no-danger return ; } diff --git a/react/features/base/i18n/i18next.js b/react/features/base/i18n/i18next.js index d2e4275e3..fba31494f 100644 --- a/react/features/base/i18n/i18next.js +++ b/react/features/base/i18n/i18next.js @@ -1,5 +1,7 @@ // @flow +declare var APP: Object; + import COUNTRIES_RESOURCES from 'i18n-iso-countries/langs/en.json'; import i18next from 'i18next'; import I18nextXHRBackend from 'i18next-xhr-backend'; @@ -7,6 +9,7 @@ import I18nextXHRBackend from 'i18next-xhr-backend'; import LANGUAGES_RESOURCES from '../../../../lang/languages.json'; import MAIN_RESOURCES from '../../../../lang/main.json'; +import { I18NEXT_INITIALIZED, LANGUAGE_CHANGED } from './actionTypes'; import languageDetector from './languageDetector'; /** @@ -46,6 +49,8 @@ const options = { load: 'languageOnly', ns: [ 'main', 'languages', 'countries' ], react: { + // re-render when a new resource bundle is added + bindI18nStore: 'added', useSuspense: false }, returnEmptyString: false, @@ -87,4 +92,15 @@ i18next.addResourceBundle( // since i18next is not yet initialized at that point. require('./BuiltinLanguages'); +// Label change through dynamic branding is available only for web +if (typeof APP !== 'undefined') { + i18next.on('initialized', () => { + APP.store.dispatch({ type: I18NEXT_INITIALIZED }); + }); + + i18next.on('languageChanged', () => { + APP.store.dispatch({ type: LANGUAGE_CHANGED }); + }); +} + export default i18next; diff --git a/react/features/base/i18n/logger.js b/react/features/base/i18n/logger.js new file mode 100644 index 000000000..a0c6cc69c --- /dev/null +++ b/react/features/base/i18n/logger.js @@ -0,0 +1,5 @@ +// @flow + +import { getLogger } from '../logging/functions'; + +export default getLogger('features/base/i18n'); diff --git a/react/features/base/i18n/middleware.js b/react/features/base/i18n/middleware.js new file mode 100644 index 000000000..38a3dee23 --- /dev/null +++ b/react/features/base/i18n/middleware.js @@ -0,0 +1,39 @@ +// @flow + +import { SET_DYNAMIC_BRANDING_DATA } from '../../dynamic-branding/actionTypes'; +import { MiddlewareRegistry } from '../redux'; + +import { I18NEXT_INITIALIZED, LANGUAGE_CHANGED } from './actionTypes'; +import { changeLanguageBundle } from './functions'; +import i18next from './i18next'; +import logger from './logger'; + +/** + * Implements the entry point of the middleware of the feature base/i18n. + * + * @param {Store} store - The redux store. + * @returns {Function} + */ +MiddlewareRegistry.register(store => next => async action => { + switch (action.type) { + case I18NEXT_INITIALIZED: + case LANGUAGE_CHANGED: + case SET_DYNAMIC_BRANDING_DATA: { + const { language } = i18next; + const { labels } = action.type === SET_DYNAMIC_BRANDING_DATA + ? action.value + : store.getState()['features/dynamic-branding']; + + if (language && labels && labels[language]) { + try { + await changeLanguageBundle(language, labels[language]); + } catch (err) { + logger.log('Error setting dynamic language bundle', err); + } + } + break; + } + } + + return next(action); +}); diff --git a/react/features/dynamic-branding/reducer.js b/react/features/dynamic-branding/reducer.js index 39a67ee53..167430d3c 100644 --- a/react/features/dynamic-branding/reducer.js +++ b/react/features/dynamic-branding/reducer.js @@ -84,6 +84,15 @@ const DEFAULT_STATE = { */ inviteDomain: '', + /** + * An object containing the mapping between the language and url where the translation + * bundle is hosted. + * + * @public + * @type {Object} + */ + labels: null, + /** * The custom url used when the user clicks the logo. * @@ -146,6 +155,7 @@ ReducerRegistry.register(STORE_NAME, (state = DEFAULT_STATE, action) => { defaultBranding, didPageUrl, inviteDomain, + labels, logoClickUrl, logoImageUrl, muiBrandedTheme, @@ -160,6 +170,7 @@ ReducerRegistry.register(STORE_NAME, (state = DEFAULT_STATE, action) => { defaultBranding, didPageUrl, inviteDomain, + labels, logoClickUrl, logoImageUrl, muiBrandedTheme,