feat(i18n): Allow label rewrite via advanced branding

This commit is contained in:
Vlad Piersec 2021-12-07 12:04:33 +02:00 committed by vp8x8
parent df3545d287
commit f20a50d8a6
7 changed files with 103 additions and 2 deletions

View File

@ -1,6 +1,7 @@
// @flow
import '../authentication/middleware';
import '../base/i18n/middleware';
import '../base/devices/middleware';
import '../dynamic-branding/middleware';
import '../e2ee/middleware';

View File

@ -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';

View File

@ -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 <span dangerouslySetInnerHTML = {{ __html: t(key, options) }} />;
}

View File

@ -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;

View File

@ -0,0 +1,5 @@
// @flow
import { getLogger } from '../logging/functions';
export default getLogger('features/base/i18n');

View File

@ -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);
});

View File

@ -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,