init
Some checks failed
Close stale issues and PRs / stale (push) Has been cancelled

This commit is contained in:
2025-09-02 14:49:16 +08:00
commit 38ba663466
2885 changed files with 391107 additions and 0 deletions

View File

@@ -0,0 +1,189 @@
import i18next from 'i18next';
/**
* The builtin languages.
*/
const _LANGUAGES = {
// Afrikaans
'af': {
main: require('../../../../lang/main-af')
},
// Arabic
'ar': {
main: require('../../../../lang/main-ar')
},
// Bulgarian
'bg': {
main: require('../../../../lang/main-bg')
},
// Catalan
'ca': {
main: require('../../../../lang/main-ca')
},
// German
'de': {
main: require('../../../../lang/main-de')
},
// Esperanto
'eo': {
main: require('../../../../lang/main-eo')
},
// Spanish
'es': {
main: require('../../../../lang/main-es')
},
// Spanish (Latin America)
'esUS': {
main: require('../../../../lang/main-esUS')
},
// Estonian
'et': {
main: require('../../../../lang/main-et')
},
// Persian
'fa': {
main: require('../../../../lang/main-fa')
},
// Finnish
'fi': {
main: require('../../../../lang/main-fi')
},
// French
'fr': {
main: require('../../../../lang/main-fr')
},
// French (Canadian)
'frCA': {
main: require('../../../../lang/main-frCA')
},
// Croatian
'hr': {
main: require('../../../../lang/main-hr')
},
// Hungarian
'hu': {
main: require('../../../../lang/main-hu')
},
// Italian
'it': {
main: require('../../../../lang/main-it')
},
// Japanese
'ja': {
main: require('../../../../lang/main-ja')
},
// Korean
'ko': {
main: require('../../../../lang/main-ko')
},
// Mongolian
'mn': {
main: require('../../../../lang/main-mn')
},
// Dutch
'nl': {
main: require('../../../../lang/main-nl')
},
// Occitan
'oc': {
main: require('../../../../lang/main-oc')
},
// Polish
'pl': {
main: require('../../../../lang/main-pl')
},
// Portuguese (Brazil)
'ptBR': {
main: require('../../../../lang/main-ptBR')
},
// Romanian
'ro': {
main: require('../../../../lang/main-ro')
},
// Russian
'ru': {
main: require('../../../../lang/main-ru')
},
// Sardinian (Sardinia)
'sc': {
main: require('../../../../lang/main-sc')
},
// Slovak
'sk': {
main: require('../../../../lang/main-sk')
},
// Slovenian
'sl': {
main: require('../../../../lang/main-sl')
},
// Swedish
'sv': {
main: require('../../../../lang/main-sv')
},
// Turkish
'tr': {
main: require('../../../../lang/main-tr')
},
// Ukrainian
'uk': {
main: require('../../../../lang/main-uk')
},
// Vietnamese
'vi': {
main: require('../../../../lang/main-vi')
},
// Chinese (Simplified)
'zhCN': {
main: require('../../../../lang/main-zhCN')
},
// Chinese (Traditional)
'zhTW': {
main: require('../../../../lang/main-zhTW')
}
};
// Register all builtin languages with the i18n library.
for (const name in _LANGUAGES) { // eslint-disable-line guard-for-in
const { main } = _LANGUAGES[name as keyof typeof _LANGUAGES];
i18next.addResourceBundle(
name,
'main',
main,
/* deep */ true,
/* overwrite */ true);
}

View File

@@ -0,0 +1,9 @@
/**
* 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

@@ -0,0 +1,28 @@
declare let config: any;
/**
* Custom language detection, just returns the config property if any.
*/
export default {
/**
* Does not support caching.
*
* @returns {void}
*/
cacheUserLanguage: Function.prototype,
/**
* Looks the language up in the config.
*
* @returns {string} The default language if any.
*/
lookup() {
return config.defaultLanguage;
},
/**
* Name of the language detector.
*/
name: 'configLanguageDetector'
};

View File

@@ -0,0 +1,67 @@
declare let navigator: any;
/**
* Custom language detection, just returns the config property if any.
*/
export default {
/**
* Does not support caching.
*
* @returns {void}
*/
cacheUserLanguage: Function.prototype,
/**
* Looks the language up in the config.
*
* @returns {string} The default language if any.
*/
lookup() {
let found = [];
if (typeof navigator !== 'undefined') {
if (navigator.languages) {
// chrome only; not an array, so can't use .push.apply instead of iterating
for (let i = 0; i < navigator.languages.length; i++) {
found.push(navigator.languages[i]);
}
}
if (navigator.userLanguage) {
found.push(navigator.userLanguage);
}
if (navigator.language) {
found.push(navigator.language);
}
}
found = found.map<string>(normalizeLanguage);
return found.length > 0 ? found : undefined;
},
/**
* Name of the language detector.
*/
name: 'customNavigatorDetector'
};
/**
* Normalize language format.
*
* (en-US => enUS)
* (en-gb => enGB)
* (es-es => es).
*
* @param {string} language - Language.
* @returns {string} The normalized language.
*/
function normalizeLanguage(language: string) {
const [ lang, variant ] = language.replace('_', '-').split('-');
if (!variant || lang.toUpperCase() === variant.toUpperCase()) {
return lang;
}
return lang + variant.toUpperCase();
}

View File

@@ -0,0 +1,150 @@
import dayjs from 'dayjs';
import durationPlugin from 'dayjs/plugin/duration';
import localizedFormatPlugin from 'dayjs/plugin/localizedFormat';
import relativeTimePlugin from 'dayjs/plugin/relativeTime';
import i18next from './i18next';
dayjs.extend(durationPlugin);
dayjs.extend(relativeTimePlugin);
dayjs.extend(localizedFormatPlugin);
// Day.js uses static language bundle loading, so in order to support dynamic
// language selection in the app we need to load all bundles that we support in
// the app.
import 'dayjs/locale/af';
import 'dayjs/locale/ar';
import 'dayjs/locale/be';
import 'dayjs/locale/bg';
import 'dayjs/locale/ca';
import 'dayjs/locale/cs';
import 'dayjs/locale/da';
import 'dayjs/locale/de';
import 'dayjs/locale/el';
import 'dayjs/locale/eo';
import 'dayjs/locale/es';
import 'dayjs/locale/es-us';
import 'dayjs/locale/et';
import 'dayjs/locale/eu';
import 'dayjs/locale/fa';
import 'dayjs/locale/fi';
import 'dayjs/locale/fr';
import 'dayjs/locale/fr-ca';
import 'dayjs/locale/gl';
import 'dayjs/locale/he';
import 'dayjs/locale/hi';
import 'dayjs/locale/hr';
import 'dayjs/locale/hu';
import 'dayjs/locale/hy-am';
import 'dayjs/locale/id';
import 'dayjs/locale/is';
import 'dayjs/locale/it';
import 'dayjs/locale/ja';
import 'dayjs/locale/ko';
import 'dayjs/locale/lt';
import 'dayjs/locale/lv';
import 'dayjs/locale/ml';
import 'dayjs/locale/mn';
import 'dayjs/locale/mr';
import 'dayjs/locale/nb';
import 'dayjs/locale/nl';
import 'dayjs/locale/oc-lnc';
import 'dayjs/locale/pl';
import 'dayjs/locale/pt';
import 'dayjs/locale/pt-br';
import 'dayjs/locale/ro';
import 'dayjs/locale/ru';
import 'dayjs/locale/sk';
import 'dayjs/locale/sl';
import 'dayjs/locale/sq';
import 'dayjs/locale/sr';
import 'dayjs/locale/sv';
import 'dayjs/locale/te';
import 'dayjs/locale/tr';
import 'dayjs/locale/uk';
import 'dayjs/locale/vi';
import 'dayjs/locale/zh-cn';
import 'dayjs/locale/zh-tw';
const LOCALE_MAPPING: Record<string, string> = {
// i18next -> dayjs
'hy': 'hy-am',
'oc': 'oc-lnc',
'zhCN': 'zh-cn',
'zhTW': 'zh-tw',
'ptBR': 'pt-br',
'esUS': 'es-us',
'frCA': 'fr-ca'
};
/**
* Returns a localized date formatter initialized with a specific {@code Date}
* or timestamp ({@code number}).
*
* @private
* @param {Date | number} dateOrTimeStamp - The date or unix timestamp (ms)
* to format.
* @returns {Object}
*/
export function getLocalizedDateFormatter(dateOrTimeStamp: Date | number) {
return dayjs(dateOrTimeStamp).locale(_getSupportedLocale());
}
/**
* Returns a localized duration formatter initialized with a
* specific duration ({@code number}).
*
* @private
* @param {number} duration - The duration (ms)
* to format.
* @returns {Object}
*/
export function getLocalizedDurationFormatter(duration: number) {
// If the conference is under an hour long we want to display it without
// showing the hour and we want to include the hour if the conference is
// more than an hour long
const d = dayjs.duration(duration);
if (d.hours() !== 0) {
return d.format('H:mm:ss');
}
return d.format('mm:ss');
}
/**
* A lenient locale matcher to match language and dialect if possible.
*
* @private
* @returns {string}
*/
function _getSupportedLocale() {
const availableLocales = Object.keys(dayjs.Ls);
const i18nLocale = i18next.language;
let supportedLocale;
if (LOCALE_MAPPING[i18nLocale]) {
return LOCALE_MAPPING[i18nLocale];
}
if (availableLocales.includes(i18nLocale)) {
return i18nLocale;
}
if (i18nLocale) {
const localeRegexp = new RegExp('^([a-z]{2,2})(-)*([a-z]{2,2})*$');
const localeResult = localeRegexp.exec(i18nLocale.toLowerCase());
if (localeResult) {
const currentLocaleRegexp
= new RegExp(
`^${localeResult[1]}(-)*${`(${localeResult[3]})*` || ''}`);
supportedLocale
= availableLocales.find(lang => currentLocaleRegexp.exec(lang));
}
}
return supportedLocale || 'en';
}

View File

@@ -0,0 +1,45 @@
import React from 'react';
import { WithTranslation, 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.
*
* @param {Component} component - The React Component to wrap.
* @returns {Component} The React Component which wraps {@link component} and
* enables translations in it.
*/
export function translate<P extends WithTranslation>(component: React.ComponentType<P>) {
// Use the default list of namespaces.
return withTranslation([ 'main', 'languages', 'countries' ])(component);
}
/**
* Translates a specific key to text containing HTML via a specific translate
* function.
*
* @param {Function} t - The translate function.
* @param {string} key - The key to translate.
* @param {Array<*>} options - The options, if any, to pass to {@link t}.
* @returns {ReactElement} A ReactElement which depicts the translated HTML
* text.
*/
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

@@ -0,0 +1,147 @@
import COUNTRIES_RESOURCES from 'i18n-iso-countries/langs/en.json';
import i18next from 'i18next';
import I18nextXHRBackend, { HttpBackendOptions } from 'i18next-http-backend';
import { merge } from 'lodash-es';
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';
import languageDetector from './languageDetector';
/**
* Override certain country names.
*/
const COUNTRIES_RESOURCES_OVERRIDES = {
countries: {
TW: 'Taiwan'
}
};
/**
* Merged country names.
*/
const COUNTRIES = merge({}, COUNTRIES_RESOURCES, COUNTRIES_RESOURCES_OVERRIDES);
/**
* The available/supported languages.
*
* @public
* @type {Array<string>}
*/
export const LANGUAGES: Array<string> = Object.keys(LANGUAGES_RESOURCES);
/**
* The available/supported translation languages.
*
* @public
* @type {Array<string>}
*/
export const TRANSLATION_LANGUAGES: Array<string> = Object.keys(TRANSLATION_LANGUAGES_RESOURCES);
/**
* The default language.
*
* English is the default language.
*
* @public
* @type {string} The default language.
*/
export const DEFAULT_LANGUAGE = 'en';
/**
* The available/supported translation languages head. (Languages displayed on the top ).
*
* @public
* @type {Array<string>}
*/
export const TRANSLATION_LANGUAGES_HEAD: Array<string> = [ DEFAULT_LANGUAGE ];
/**
* The options to initialize i18next with.
*
* @type {i18next.InitOptions}
*/
const options: i18next.InitOptions = {
backend: <HttpBackendOptions>{
loadPath: (lng: string[], ns: string[]) => {
switch (ns[0]) {
case 'countries':
case 'main':
return 'lang/{{ns}}-{{lng}}.json';
default:
return 'lang/{{ns}}.json';
}
}
},
defaultNS: 'main',
fallbackLng: DEFAULT_LANGUAGE,
interpolation: {
escapeValue: false // not needed for react as it escapes by default
},
load: 'languageOnly',
ns: [ 'main', 'languages', 'countries', 'translation-languages' ],
react: {
// re-render when a new resource bundle is added
// @ts-expect-error. Fixed in i18next 19.6.1.
bindI18nStore: 'added',
useSuspense: false
},
returnEmptyString: false,
returnNull: false,
// XXX i18next modifies the array lngWhitelist so make sure to clone
// LANGUAGES.
whitelist: LANGUAGES.slice()
};
i18next
.use(navigator.product === 'ReactNative' ? {} : I18nextXHRBackend)
.use(languageDetector)
.init(options);
// Add default language which is preloaded from the source code.
i18next.addResourceBundle(
DEFAULT_LANGUAGE,
'countries',
COUNTRIES,
/* deep */ true,
/* overwrite */ true);
i18next.addResourceBundle(
DEFAULT_LANGUAGE,
'languages',
LANGUAGES_RESOURCES,
/* deep */ true,
/* overwrite */ true);
i18next.addResourceBundle(
DEFAULT_LANGUAGE,
'translation-languages',
TRANSLATION_LANGUAGES_RESOURCES,
/* deep */ true,
/* overwrite */ true);
i18next.addResourceBundle(
DEFAULT_LANGUAGE,
'main',
MAIN_RESOURCES,
/* deep */ true,
/* overwrite */ true);
// Add builtin languages.
// XXX: Note we are using require here, because we want the side-effects of the
// import, but imports can only be placed at the top, and it would be too early,
// 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,43 @@
import { NativeModules } from 'react-native';
import LANGUAGES_RESOURCES from '../../../../lang/languages.json';
const LANGUAGES = Object.keys(LANGUAGES_RESOURCES);
/**
* The singleton language detector for React Native which uses the system-wide
* locale.
*/
export default {
/**
* Does not support caching.
*
* @returns {void}
*/
cacheUserLanguage: Function.prototype,
detect() {
const { LocaleDetector } = NativeModules;
const parts = LocaleDetector.locale.replace(/_/, '-').split('-');
const [ lang, regionOrScript, region ] = parts;
let locale;
if (parts.length >= 3) {
locale = `${lang}${region}`;
} else if (parts.length === 2) {
locale = `${lang}${regionOrScript}`;
} else {
locale = lang;
}
if (LANGUAGES.includes(locale)) {
return locale;
}
return lang;
},
init: Function.prototype,
type: 'languageDetector'
};

View File

@@ -0,0 +1,45 @@
import BrowserLanguageDetector from 'i18next-browser-languagedetector';
import configLanguageDetector from './configLanguageDetector';
import customNavigatorDetector from './customNavigatorDetector';
/**
* The ordered list (by name) of language detectors to be utilized as backends
* by the singleton language detector for Web.
*
* @type {Array<string>}
*/
const order = [
'querystring',
'localStorage'
];
// Allow i18next to detect the system language reported by the Web browser
// itself.
interfaceConfig.LANG_DETECTION && order.push(customNavigatorDetector.name);
// Default use configured language
order.push(configLanguageDetector.name);
/**
* The singleton language detector for Web.
*/
const languageDetector
= new BrowserLanguageDetector(
/* services */ null,
/* options */ {
caches: [ 'localStorage' ],
lookupLocalStorage: 'language',
lookupQuerystring: 'lang',
order
});
// Add the language detector which looks the language up in the config. Its
// order has already been established above.
// @ts-ignore
languageDetector.addDetector(customNavigatorDetector);
// @ts-ignore
languageDetector.addDetector(configLanguageDetector);
export default languageDetector;

View File

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

View File

@@ -0,0 +1,49 @@
import { SET_DYNAMIC_BRANDING_DATA } from '../../dynamic-branding/actionTypes';
import { getConferenceState } from '../conference/functions';
import MiddlewareRegistry from '../redux/MiddlewareRegistry';
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 => 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?.[language]) {
changeLanguageBundle(language, labels[language])
.catch(err => {
logger.log('Error setting dynamic language bundle', err);
});
}
// Update transcription language, if applicable.
if (action.type === SET_DYNAMIC_BRANDING_DATA) {
const { defaultTranscriptionLanguage } = action.value;
if (typeof defaultTranscriptionLanguage !== 'undefined') {
const { conference } = getConferenceState(store.getState());
conference?.setTranscriptionLanguage(defaultTranscriptionLanguage);
}
}
break;
}
}
return next(action);
});