From b1165d717aa370f2e79f360014827cdf1ae7d7af Mon Sep 17 00:00:00 2001 From: Giuliano Caregnato Date: Wed, 19 Oct 2022 17:51:46 +0200 Subject: [PATCH] fix: fixed translation function and contexts --- src/boot/app/app-context-provider.tsx | 6 +- src/boot/app/app-loader-functions.ts | 3 +- src/boot/app/default-views.ts | 20 +-- src/boot/app/load-apps.ts | 18 ++- src/boot/bootstrapper-context.ts | 12 +- src/boot/bootstrapper-provider.tsx | 10 +- src/boot/bootstrapper-router.tsx | 66 ++++---- src/boot/bootstrapper.tsx | 7 +- src/boot/init.ts | 9 +- src/reporting/feedback.tsx | 41 +++-- src/search/search-app-view.tsx | 14 +- src/search/search-bar.tsx | 16 +- src/settings/account-wrapper.tsx | 8 +- src/settings/accounts-settings.tsx | 2 +- .../password-recovery-settings.tsx | 17 +-- .../account-settings/persona-use-section.tsx | 10 +- .../settings-sent-messages.tsx | 19 ++- .../components/date-time-select-view.tsx | 10 +- .../folder-select-modal/folder-item.tsx | 2 +- .../general-settings/appearance-settings.tsx | 8 +- .../components/general-settings/logout.tsx | 6 +- .../module-version-settings.tsx | 20 ++- .../general-settings/out-of-office-view.tsx | 39 +++-- .../general-settings/user-quota.tsx | 6 +- src/settings/components/settings-header.tsx | 16 +- src/settings/components/utils.ts | 6 +- src/settings/general-settings.tsx | 14 +- .../language-and-timezone-settings.tsx | 21 ++- src/settings/search-settings-view.tsx | 10 +- src/settings/settings-sidebar.tsx | 4 +- src/shell/boards/board-container.tsx | 14 +- src/shell/boards/board-tab-list.tsx | 16 +- src/shell/boards/board-tab.tsx | 20 +-- src/shell/creation-button.tsx | 16 +- src/shell/shell-view.tsx | 24 +-- src/store/app/store.ts | 2 +- src/store/i18n/hooks.ts | 14 +- src/store/i18n/store.ts | 144 +++++++++++------- src/store/integrations/composer.tsx | 32 ++-- src/ui-extras/nav-guard.tsx | 8 +- src/utility-bar/bar.tsx | 12 +- tsconfig.json | 3 +- types/exports/index.d.ts | 40 +++-- types/i18n/index.d.ts | 6 + 44 files changed, 403 insertions(+), 388 deletions(-) diff --git a/src/boot/app/app-context-provider.tsx b/src/boot/app/app-context-provider.tsx index c58875ce..72338be0 100644 --- a/src/boot/app/app-context-provider.tsx +++ b/src/boot/app/app-context-provider.tsx @@ -6,11 +6,13 @@ import React, { FC } from 'react'; import { I18nextProvider } from 'react-i18next'; +import { SHELL_APP_ID } from '../../constants'; +import { useI18nStore } from '../../store/i18n'; import AppErrorCatcher from './app-error-catcher'; -import { useI18n } from '../../store/i18n'; const AppContextProvider: FC<{ pkg: string }> = ({ pkg, children }) => { - const i18n = useI18n(pkg)(); + const { instances, defaultI18n } = useI18nStore.getState(); + const i18n = instances[pkg] ?? instances[SHELL_APP_ID] ?? defaultI18n; return ( {children} diff --git a/src/boot/app/app-loader-functions.ts b/src/boot/app/app-loader-functions.ts index d23a4ab2..0c4fc3d4 100644 --- a/src/boot/app/app-loader-functions.ts +++ b/src/boot/app/app-loader-functions.ts @@ -76,7 +76,7 @@ import { getTags, useTags } from '../../store/tags'; import { useNotify, useRefresh } from '../../store/network'; import { changeTagColor, createTag, deleteTag, renameTag } from '../../network/tags'; import { runSearch } from '../../search/run-search'; -import { getI18n, useI18n, getTFunction } from '../../store/i18n'; +import { getI18n, getTFunction } from '../../store/i18n'; import { addBoard, closeBoard, @@ -97,7 +97,6 @@ import { getNotificationManager } from '../../notification/NotificationManager'; // eslint-disable-next-line @typescript-eslint/ban-types export const getAppFunctions = (pkg: CarbonioModule): Record => ({ // I18N - useI18n: useI18n(pkg.name), getI18n: getI18n(pkg.name), t: getTFunction(pkg.name), // FETCH diff --git a/src/boot/app/default-views.ts b/src/boot/app/default-views.ts index 33724b8e..f117be93 100644 --- a/src/boot/app/default-views.ts +++ b/src/boot/app/default-views.ts @@ -6,25 +6,27 @@ /* eslint-disable no-param-reassign */ import produce from 'immer'; -import { TFunction } from 'i18next'; -import { useAppStore } from '../../store/app'; -import { SearchAppView } from '../../search/search-app-view'; -import { SettingsAppView } from '../../settings/settings-app-view'; -import { SettingsSidebar } from '../../settings/settings-sidebar'; +import { TFunction } from 'react-i18next'; import { AppState, PrimaryBarView, SettingsView } from '../../../types'; -import GeneralSettings from '../../settings/general-settings'; -import Feedback from '../../reporting/feedback'; +import { SEARCH_APP_ID, SETTINGS_APP_ID, SHELL_APP_ID } from '../../constants'; import DevBoard from '../../dev/dev-board'; import DevBoardTrigger from '../../dev/dev-board-trigger'; -import { SEARCH_APP_ID, SETTINGS_APP_ID, SHELL_APP_ID } from '../../constants'; +import Feedback from '../../reporting/feedback'; +import { SearchAppView } from '../../search/search-app-view'; import AccountWrapper from '../../settings/account-wrapper'; +import GeneralSettings from '../../settings/general-settings'; import { settingsSubSections } from '../../settings/general-settings-sub-sections'; +import { SettingsAppView } from '../../settings/settings-app-view'; +import { SettingsSidebar } from '../../settings/settings-sidebar'; +import { useAppStore } from '../../store/app'; +import { useI18nStore } from '../../store/i18n'; const settingsRoute = { route: SETTINGS_APP_ID, id: SETTINGS_APP_ID, app: SETTINGS_APP_ID }; + const settingsPrimaryBar = (t: TFunction): PrimaryBarView => ({ id: SETTINGS_APP_ID, app: SETTINGS_APP_ID, @@ -56,7 +58,7 @@ const settingsGeneralView = (t: TFunction): SettingsView => ({ app: SHELL_APP_ID, component: GeneralSettings, icon: 'SettingsModOutline', - label: t('settings.general.general', 'General'), + label: t('settings.general.general', 'General Settings'), position: 1, subSections: settingsSubSections(t) }); diff --git a/src/boot/app/load-apps.ts b/src/boot/app/load-apps.ts index bb7ea455..758bcb01 100644 --- a/src/boot/app/load-apps.ts +++ b/src/boot/app/load-apps.ts @@ -6,13 +6,14 @@ import { filter, map } from 'lodash'; -import { loadApp, unloadApps } from './load-app'; import { CarbonioModule } from '../../../types'; -import { injectSharedLibraries } from './shared-libraries'; -import { getUserSetting } from '../../store/account'; -import { useReporter } from '../../reporting'; import { SHELL_APP_ID } from '../../constants'; -import { addI18n } from '../../store/i18n'; +import { useReporter } from '../../reporting'; +import { useAccountStore } from '../../store/account'; +import { getUserSetting } from '../../store/account/hooks'; +import { useI18nStore } from '../../store/i18n'; +import { loadApp, unloadApps } from './load-app'; +import { injectSharedLibraries } from './shared-libraries'; export function loadApps(apps: Array): void { injectSharedLibraries(); @@ -25,8 +26,13 @@ export function loadApps(apps: Array): void { '%cLOADING APPS', 'color: white; background: #2b73d2;padding: 4px 8px 2px 4px; font-family: sans-serif; border-radius: 12px; width: 100%' ); + const { settings } = useAccountStore.getState(); + const locale = + (settings?.prefs?.zimbraPrefLocale as string) ?? + (settings?.attrs?.zimbraLocale as string) ?? + 'en'; + useI18nStore.getState().actions.addI18n(appsToLoad, locale); useReporter.getState().setClients(appsToLoad); - addI18n(appsToLoad); Promise.allSettled(map(appsToLoad, (app) => loadApp(app))); } diff --git a/src/boot/bootstrapper-context.ts b/src/boot/bootstrapper-context.ts index a86ab7c5..959bb4ed 100644 --- a/src/boot/bootstrapper-context.ts +++ b/src/boot/bootstrapper-context.ts @@ -4,16 +4,6 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { createContext, useContext } from 'react'; +import { createContext } from 'react'; export const BootstrapperContext = createContext({}); - -export function useI18nFactory(): any { - const { i18nFactory } = useContext(BootstrapperContext); - return i18nFactory; -} - -export function useStoreFactory(): any { - const { storeFactory } = useContext(BootstrapperContext); - return storeFactory; -} diff --git a/src/boot/bootstrapper-provider.tsx b/src/boot/bootstrapper-provider.tsx index c813ea54..63847dad 100644 --- a/src/boot/bootstrapper-provider.tsx +++ b/src/boot/bootstrapper-provider.tsx @@ -6,11 +6,11 @@ import React, { FC } from 'react'; import { I18nextProvider } from 'react-i18next'; -// eslint-disable-next-line @typescript-eslint/ban-ts-comment -// @ts-ignore +import { SHELL_APP_ID } from '../constants'; import { useI18nStore } from '../store/i18n'; -const BootstrapperContextProvider: FC = ({ children }) => ( - {children} -); +const BootstrapperContextProvider: FC = ({ children }) => { + const i18n = useI18nStore((s) => s.instances[SHELL_APP_ID]); + return {children}; +}; export default BootstrapperContextProvider; diff --git a/src/boot/bootstrapper-router.tsx b/src/boot/bootstrapper-router.tsx index 82099811..16b74f63 100644 --- a/src/boot/bootstrapper-router.tsx +++ b/src/boot/bootstrapper-router.tsx @@ -4,23 +4,21 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import React, { FC, useContext, useEffect } from 'react'; -import { BrowserRouter, Route, Switch, useHistory, useParams } from 'react-router-dom'; import { - SnackbarManagerContext, - ModalManagerContext, ModalManager, - SnackbarManager + ModalManagerContext, + SnackbarManager, + SnackbarManagerContext } from '@zextras/carbonio-design-system'; -import { useTranslation } from 'react-i18next'; -import AppLoaderMounter from './app/app-loader-mounter'; +import React, { FC, useContext, useEffect } from 'react'; +import { TFunction, useTranslation } from 'react-i18next'; +import { BrowserRouter, Route, Switch, useHistory, useParams } from 'react-router-dom'; import { useBridge } from '../store/context-bridge'; -// eslint-disable-next-line @typescript-eslint/ban-ts-comment -// @ts-ignore -import ShellView from '../shell/shell-view'; +import AppLoaderMounter from './app/app-loader-mounter'; import { BASENAME, IS_STANDALONE } from '../constants'; -import { useAppStore } from '../store/app'; import { NotificationPermissionChecker } from '../notification/NotificationPermissionChecker'; +import ShellView from '../shell/shell-view'; +import { useAppStore } from '../store/app'; import { registerDefaultViews } from './app/default-views'; const ContextBridge: FC = () => { @@ -47,32 +45,34 @@ const StandaloneListener: FC = () => { return null; }; -const DefaultViewsRegister: FC = () => { - const [t] = useTranslation(); +const DefaultViewsRegister: FC<{ t: TFunction }> = ({ t }) => { useEffect(() => { registerDefaultViews(t); }, [t]); return null; }; -const BootstrapperRouter: FC = () => ( - - - - {IS_STANDALONE && ( - - - - - - )} - - - - - - - - -); +const BootstrapperRouter: FC = () => { + const { t } = useTranslation(); + return ( + + + + {IS_STANDALONE && ( + + + + + + )} + + + + + + + + + ); +}; export default BootstrapperRouter; diff --git a/src/boot/bootstrapper.tsx b/src/boot/bootstrapper.tsx index 9b7c8cbe..965df069 100644 --- a/src/boot/bootstrapper.tsx +++ b/src/boot/bootstrapper.tsx @@ -5,12 +5,11 @@ */ import React, { FC, useEffect } from 'react'; -import { SnackbarManager, ModalManager } from '@zextras/carbonio-design-system'; +import { unloadAllApps } from './app/load-apps'; +import BootstrapperContextProvider from './bootstrapper-provider'; +import BootstrapperRouter from './bootstrapper-router'; import { init } from './init'; import { ThemeProvider } from './theme-provider'; -import BootstrapperRouter from './bootstrapper-router'; -import BootstrapperContextProvider from './bootstrapper-provider'; -import { unloadAllApps } from './app/load-apps'; const Bootstrapper: FC = () => { useEffect(() => { diff --git a/src/boot/init.ts b/src/boot/init.ts index 201c02f4..95775d04 100644 --- a/src/boot/init.ts +++ b/src/boot/init.ts @@ -4,19 +4,12 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { useAccountStore } from '../store/account'; +import { getInfo } from '../network/get-info'; import { useAppStore } from '../store/app'; import { loadApps } from './app/load-apps'; -import { getInfo } from '../network/get-info'; -import { setLocale } from '../store/i18n'; export const init = (): void => { getInfo().finally(() => { - setLocale( - (useAccountStore.getState().settings?.prefs?.zimbraPrefLocale as string) ?? - (useAccountStore.getState().settings?.attrs?.zimbraLocale as string) ?? - 'en' - ); loadApps(Object.values(useAppStore.getState().apps)); }); }; diff --git a/src/reporting/feedback.tsx b/src/reporting/feedback.tsx index c9b41359..d874fcb3 100644 --- a/src/reporting/feedback.tsx +++ b/src/reporting/feedback.tsx @@ -4,34 +4,34 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import React, { - useEffect, - useState, - useCallback, - useReducer, - useMemo, - FC, - useContext -} from 'react'; +import { Event, Severity } from '@sentry/browser'; import { - Text, ButtonOld as Button, - Select, Container, - Row, + ContainerProps, Icon, + Row, + Select, SnackbarManagerContext, - ContainerProps, - SelectItem + Text } from '@zextras/carbonio-design-system'; -import { Severity, Event } from '@sentry/browser'; import { filter, find, map } from 'lodash'; +import React, { + FC, + useCallback, + useContext, + useEffect, + useMemo, + useReducer, + useState +} from 'react'; +import { TFunction } from 'react-i18next'; import styled from 'styled-components'; -import { TFunction, useTranslation } from 'react-i18next'; import { useUserAccount } from '../store/account'; -import { feedback } from './functions'; import { useAppList } from '../store/app'; import { closeBoard } from '../store/boards'; +import { getT } from '../store/i18n'; +import { feedback } from './functions'; const TextArea = styled.textarea<{ size?: string }>` width: 100%; @@ -218,8 +218,7 @@ const _LabelFactory: FC<{ ); const Feedback: FC = () => { - const [t] = useTranslation(); - const topics = useMemo(() => getTopics(t), [t]); + const t = getT(); const allApps = useAppList(); const apps = useMemo( () => filter(allApps, (app) => !!app.sentryDsn), @@ -371,9 +370,9 @@ const Feedback: FC = () => { { @@ -177,7 +174,7 @@ const OutOfOfficeView: FC<{ = ({ mobileView }) => { - const [t] = useTranslation(); + const t = getT(); const settings = useUserSettings(); const used = useAccountStore((s) => s.usedQuota); diff --git a/src/settings/components/settings-header.tsx b/src/settings/components/settings-header.tsx index 4682b4f3..702b4c84 100644 --- a/src/settings/components/settings-header.tsx +++ b/src/settings/components/settings-header.tsx @@ -4,19 +4,19 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import React, { FC, useEffect } from 'react'; import { - Padding, - Row, - Text, - Container, + Breadcrumbs, ButtonOld as Button, + Container, Divider, - Breadcrumbs + Padding, + Row, + Text } from '@zextras/carbonio-design-system'; -import { useTranslation } from 'react-i18next'; +import React, { FC, useEffect } from 'react'; import { useHistory, useParams } from 'react-router-dom'; import { SETTINGS_APP_ID } from '../../constants'; +import { getT } from '../../store/i18n'; import { RouteLeavingGuard } from '../../ui-extras/nav-guard'; type SettingsHeaderProps = { @@ -27,7 +27,7 @@ type SettingsHeaderProps = { }; const SettingsHeader: FC = ({ onSave, onCancel, isDirty, title }) => { - const [t] = useTranslation(); + const t = getT(); const history = useHistory(); const useparam = useParams(); const crumbs = [ diff --git a/src/settings/components/utils.ts b/src/settings/components/utils.ts index 43141dd4..4e2bf52e 100644 --- a/src/settings/components/utils.ts +++ b/src/settings/components/utils.ts @@ -5,11 +5,9 @@ */ import moment from 'moment'; -import { TFunction } from 'i18next'; +import { TFunction } from 'react-i18next'; import { AccountSettings } from '../../../types'; -// const [t] = useTranslation(); - export const ItemsSendAutoReplies = (t: TFunction): any => [ { label: t('settings.out_of_office.send_auto_replies', 'Send auto-replies'), @@ -95,7 +93,7 @@ export const getExternalSendersPrefsData = ( export const getOutOfOfficeStatusPrefsData = ( settings: AccountSettings, t: TFunction -): { label: string; value: string } => { +): { label: string; value: string; t: TFunction } => { let item; const itemsOutOfOfficeStatus = ItemsOutOfOfficeStatus(t); if (settings.prefs.zimbraPrefOutOfOfficeFreeBusyStatus === 'BUSY') { diff --git a/src/settings/general-settings.tsx b/src/settings/general-settings.tsx index 913b1ef2..5aad7826 100644 --- a/src/settings/general-settings.tsx +++ b/src/settings/general-settings.tsx @@ -4,25 +4,25 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import React, { useCallback, useState, FC, useMemo } from 'react'; import { Container, useSnackbar } from '@zextras/carbonio-design-system'; -import { useTranslation } from 'react-i18next'; import { includes, isEmpty } from 'lodash'; +import React, { FC, useCallback, useMemo, useState } from 'react'; +import { Mods } from '../../types'; +import { editSettings } from '../network/edit-settings'; import { useUserSettings } from '../store/account'; -import Logout from './components/general-settings/logout'; +import { getT } from '../store/i18n'; import AppearanceSettings from './components/general-settings/appearance-settings'; +import Logout from './components/general-settings/logout'; import ModuleVersionSettings from './components/general-settings/module-version-settings'; import OutOfOfficeSettings from './components/general-settings/out-of-office-view'; import UserQuota from './components/general-settings/user-quota'; -import { editSettings } from '../network/edit-settings'; -import { Mods } from '../../types'; +import SettingsHeader from './components/settings-header'; import LanguageAndTimeZoneSettings from './language-and-timezone-settings'; import SearchSettingsView from './search-settings-view'; -import SettingsHeader from './components/settings-header'; const GeneralSettings: FC = () => { const [mods, setMods] = useState({}); - const [t] = useTranslation(); + const t = getT(); const settings = useUserSettings(); const [open, setOpen] = useState(false); const addMod = useCallback((type: 'props' | 'prefs', key, value) => { diff --git a/src/settings/language-and-timezone-settings.tsx b/src/settings/language-and-timezone-settings.tsx index 53563f68..0388dc22 100644 --- a/src/settings/language-and-timezone-settings.tsx +++ b/src/settings/language-and-timezone-settings.tsx @@ -4,23 +4,22 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import React, { useCallback, FC, useMemo } from 'react'; import { Container, FormSubSection, Modal, - Select, - Text, Padding, - SelectItem + Select, + Text } from '@zextras/carbonio-design-system'; +import React, { FC, useCallback, useMemo } from 'react'; // eslint-disable-next-line import/no-extraneous-dependencies -import momentLocalizer from 'react-widgets-moment'; -import { useTranslation } from 'react-i18next'; import { find } from 'lodash'; +import momentLocalizer from 'react-widgets-moment'; import { AccountSettings } from '../../types'; -import { LocaleDescriptor, localeList, timeZoneList } from './components/utils'; +import { getT } from '../store/i18n'; +import { localeList, timeZoneList } from './components/utils'; import { timezoneAndLanguageSubSection } from './general-settings-sub-sections'; momentLocalizer(); @@ -31,9 +30,9 @@ const LanguageAndTimeZone: FC<{ setOpen: (arg: boolean) => any; addMod: (type: 'prefs' | 'props', key: string, value: { value: any; app: string }) => void; }> = ({ settings, addMod, open, setOpen }) => { - const { t } = useTranslation(); - const locales = useMemo(() => localeList(t), [t]); - const timezones = useMemo(() => timeZoneList(t), [t]); + const t = getT(); + const locales = localeList(t); + const timezones = timeZoneList(t); const updatePrefs = useCallback( (v, p) => { @@ -54,7 +53,7 @@ const LanguageAndTimeZone: FC<{ return timezone ?? find(timezones, { value: 'UTC' }); }, [timezones, settings.prefs.zimbraPrefTimeZoneId]); - const sectionTitle = useMemo(() => timezoneAndLanguageSubSection(t), [t]); + const sectionTitle = timezoneAndLanguageSubSection(t); return ( void; }> = ({ settings, addMod }) => { - const { t } = useTranslation(); + const t = getT(); const [searchInSpamFolder, setSearchInSpamFolder] = useState( settings.prefs.zimbraPrefIncludeSpamInSearch === 'TRUE' ); @@ -54,7 +54,7 @@ const SearchSettingsView: FC<{ setSearchInSharedFolder(!searchInSharedFolder); setMode(!searchInSharedFolder, 'zimbraPrefIncludeSharedItemsInSearch'); }, [searchInSharedFolder, setMode]); - const sectionTitle = useMemo(() => searchPrefsSubSection(t), [t]); + const sectionTitle = searchPrefsSubSection(t); return ( ` position: fixed; @@ -74,7 +74,7 @@ const BackButton = styled(IconButton)``; const Actions = styled(Row)``; export const BoardContainer: FC = () => { - const [t] = useTranslation(); + const t = getT(); const { boards, minimized, expanded, current } = useBoardStore(); if (isEmpty(boards) || !current) return null; return ( diff --git a/src/shell/boards/board-tab-list.tsx b/src/shell/boards/board-tab-list.tsx index 52e49959..a6af0aa2 100644 --- a/src/shell/boards/board-tab-list.tsx +++ b/src/shell/boards/board-tab-list.tsx @@ -4,22 +4,22 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { map, noop, slice } from 'lodash'; -import React, { useRef, useLayoutEffect, FC } from 'react'; import { + Button, Container, - Row, Dropdown, - Button, - useHiddenCount, - Tooltip + Row, + Tooltip, + useHiddenCount } from '@zextras/carbonio-design-system'; -import { useTranslation } from 'react-i18next'; +import { map, noop, slice } from 'lodash'; +import React, { FC, useLayoutEffect, useRef } from 'react'; import { setCurrentBoard, useBoardStore } from '../../store/boards'; +import { getT } from '../../store/i18n'; import { AppBoardTab } from './board-tab'; export const TabsList: FC = () => { - const [t] = useTranslation(); + const t = getT(); const { boards, current, expanded } = useBoardStore(); const tabContainerRef = useRef(null); const [hiddenTabsCount, recalculateHiddenTabs] = useHiddenCount(tabContainerRef, expanded); diff --git a/src/shell/boards/board-tab.tsx b/src/shell/boards/board-tab.tsx index a326832c..ae0d89f1 100644 --- a/src/shell/boards/board-tab.tsx +++ b/src/shell/boards/board-tab.tsx @@ -4,20 +4,20 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import React, { FC, useCallback } from 'react'; -import styled from 'styled-components'; -import { useTranslation } from 'react-i18next'; import { + Container, + Icon, IconButton, - Text, - Row, Padding, - Icon, - Container, - Tooltip, - RowProps + Row, + RowProps, + Text, + Tooltip } from '@zextras/carbonio-design-system'; +import React, { FC, useCallback } from 'react'; +import styled from 'styled-components'; import { closeBoard, setCurrentBoard, useBoardStore } from '../../store/boards'; +import { getT } from '../../store/i18n'; const TabContainer = styled(Row)` cursor: pointer; @@ -43,7 +43,7 @@ export const AppBoardTab: FC<{ id: string; icon: string; title: string }> = ({ title }) => { const current = useBoardStore((s) => s.current); - const [t] = useTranslation(); + const t = getT(); const onClick = useCallback(() => setCurrentBoard(id), [id]); const onRemove = useCallback( (ev) => { diff --git a/src/shell/creation-button.tsx b/src/shell/creation-button.tsx index 3d157a46..9a109986 100644 --- a/src/shell/creation-button.tsx +++ b/src/shell/creation-button.tsx @@ -4,23 +4,23 @@ * SPDX-License-Identifier: AGPL-3.0-only */ +import { Button, Dropdown, MultiButton } from '@zextras/carbonio-design-system'; +import { Location } from 'history'; +import { groupBy, noop, reduce } from 'lodash'; import React, { FC, useCallback, useMemo, useState } from 'react'; -import { reduce, groupBy, noop } from 'lodash'; -import { MultiButton, Button, Dropdown } from '@zextras/carbonio-design-system'; -import { useTranslation } from 'react-i18next'; import { useLocation } from 'react-router-dom'; -import { Location } from 'history'; -import { useActions } from '../store/integrations/hooks'; -import { ACTION_TYPES } from '../constants'; import { Action, AppRoute } from '../../types'; -import { useAppList } from '../store/app'; +import { ACTION_TYPES } from '../constants'; import { useCurrentRoute } from '../history/hooks'; +import { useAppList } from '../store/app'; +import { getT } from '../store/i18n'; +import { useActions } from '../store/integrations/hooks'; export const CreationButtonComponent: FC<{ activeRoute: AppRoute; location: Location }> = ({ activeRoute, location }) => { - const [t] = useTranslation(); + const t = getT(); const actions = useActions({ activeRoute, location }, ACTION_TYPES.NEW); const [open, setOpen] = useState(false); const primaryAction = useMemo( diff --git a/src/shell/shell-view.tsx b/src/shell/shell-view.tsx index 7c0ebd32..0da6b2c0 100644 --- a/src/shell/shell-view.tsx +++ b/src/shell/shell-view.tsx @@ -4,23 +4,23 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import React, { useEffect, useState, useContext, FC, useMemo } from 'react'; -import { Row, Responsive } from '@zextras/carbonio-design-system'; -import styled from 'styled-components'; -import { find } from 'lodash'; +import { Responsive, Row } from '@zextras/carbonio-design-system'; import { PreviewManager } from '@zextras/carbonio-ui-preview'; +import { find } from 'lodash'; +import React, { FC, useContext, useEffect, useMemo, useState } from 'react'; +import styled from 'styled-components'; +import { AppRoute, DRPropValues } from '../../types'; +import { ThemeCallbacksContext } from '../boot/theme-provider'; +import { IS_STANDALONE, SHELL_APP_ID } from '../constants'; +import { useCurrentRoute } from '../history/hooks'; +import { goToLogin } from '../network/go-to-login'; +import { useAccountStore, useUserSettings } from '../store/account'; +import { ShellUtilityBar, ShellUtilityPanel } from '../utility-bar'; import AppViewContainer from './app-view-container'; +import { BoardContainer } from './boards/board-container'; import ShellContextProvider from './shell-context-provider'; import ShellHeader from './shell-header'; import ShellNavigationBar from './shell-navigation-bar'; -import { BoardContainer } from './boards/board-container'; -import { ThemeCallbacksContext } from '../boot/theme-provider'; -import { useAccountStore, useUserSettings } from '../store/account'; -import { ShellUtilityBar, ShellUtilityPanel } from '../utility-bar'; -import { useCurrentRoute } from '../history/hooks'; -import { IS_STANDALONE, SHELL_APP_ID } from '../constants'; -import { goToLogin } from '../network/go-to-login'; -import { AppRoute, DRPropValues } from '../../types'; const Background = styled.div` background: ${({ theme }): string => theme.palette.gray6.regular}; diff --git a/src/store/app/store.ts b/src/store/app/store.ts index 2d667af1..069ea7a2 100644 --- a/src/store/app/store.ts +++ b/src/store/app/store.ts @@ -37,7 +37,7 @@ export const useAppStore = create((set, get) => ({ commit: '', description: '', js_entrypoint: '', - name: 'carbonio-shell-ui', + name: SHELL_APP_ID, priority: -1, version: '', type: 'shell', diff --git a/src/store/i18n/hooks.ts b/src/store/i18n/hooks.ts index b10fbd96..99e60fdd 100644 --- a/src/store/i18n/hooks.ts +++ b/src/store/i18n/hooks.ts @@ -4,17 +4,12 @@ * SPDX-License-Identifier: AGPL-3.0-only */ import { i18n, TFunction } from 'i18next'; +import { SHELL_APP_ID } from '../../constants'; import { useI18nStore } from './store'; -export const useI18n = (app: string) => (): i18n => - // eslint-disable-next-line react-hooks/rules-of-hooks - useI18nStore((s) => s.instances[app] ?? s.defaultI18n); - -export const useLocale = (): string => useI18nStore((s) => s.locale); - export const getI18n = (app: string) => (): i18n => { const { instances, defaultI18n } = useI18nStore.getState(); - return instances[app] ?? defaultI18n; + return instances[app] ?? instances[SHELL_APP_ID] ?? defaultI18n; }; export const getTFunction = (app: string): TFunction => { @@ -22,4 +17,7 @@ export const getTFunction = (app: string): TFunction => { return instances[app]?.t ?? defaultI18n.t; }; -export const getLocale = (): string => useI18nStore.getState().locale; +export const getT = (): TFunction => { + const { instances, defaultI18n } = useI18nStore.getState(); + return instances[SHELL_APP_ID]?.t ?? defaultI18n.t; +}; diff --git a/src/store/i18n/store.ts b/src/store/i18n/store.ts index cd68f801..afd89529 100644 --- a/src/store/i18n/store.ts +++ b/src/store/i18n/store.ts @@ -5,27 +5,105 @@ */ import i18next, { i18n } from 'i18next'; -import { dropRight, forEach, map, reduce } from 'lodash'; -import create from 'zustand'; import Backend from 'i18next-http-backend'; +import produce from 'immer'; +import { dropRight, forEach, reduce } from 'lodash'; +import create from 'zustand'; import { CarbonioModule, I18nState } from '../../../types'; +import { SHELL_APP_ID } from '../../constants'; +import { useAccountStore } from '../account'; + +const addShell = (apps: Array): Array => [ + ...apps, + { + commit: '', + description: '', + js_entrypoint: '', + name: SHELL_APP_ID, + priority: -1, + version: '', + type: 'shell', + attrKey: '', + icon: '', + display: 'Shell' + } +]; + +const { settings } = useAccountStore.getState(); -const defaultI18n = i18next.createInstance(); +const defaultLng = + (settings?.prefs?.zimbraPrefLocale as string) ?? + (settings?.attrs?.zimbraLocale as string) ?? + 'en'; -export const useI18nStore = create(() => ({ +const defaultI18n = i18next.createInstance({ lng: defaultLng }); + +export const useI18nStore = create((set) => ({ instances: {}, defaultI18n, - locale: 'en' + locale: 'en', + setters: { + setLocale: (locale: string): void => { + set( + produce((state: I18nState) => { + state.locale = locale; + forEach(state.instances, (i18nInst) => i18nInst.changeLanguage(locale)); + }) + ); + } + }, + getters: { + getLocale: (state: I18nState): string => state.locale + }, + actions: { + addI18n: (apps: Array, locale: string): void => { + const appsWithShell = addShell(apps); + set( + produce((state: I18nState) => { + state.instances = reduce( + appsWithShell, + (acc, app) => { + const newI18n = i18next.createInstance(); + newI18n + // load translation using http -> see /public/locales (i.e. https://github.com/i18next/react-i18next/tree/master/example/react/public/locales) + // learn more: https://github.com/i18next/i18next-http-backend + .use(Backend) + // init i18next + // for all options read: https://www.i18next.com/overview/configuration-options + .init({ + returnEmptyString: true, + compatibilityJSON: 'v3', + fallbackLng: 'en', + lng: locale, + debug: false, + interpolation: { + escapeValue: false // not needed for react as it escapes by default + }, + missingKeyHandler: (_, __, key) => { + // eslint-disable-next-line no-console + console.warn(`Missing translation with key '${key}'`); + }, + backend: { + loadPath: + app.name === SHELL_APP_ID + ? `${BASE_PATH}/i18n/{{lng}}.json` + : `${dropRight(app.js_entrypoint.split('/')).join('/')}/i18n/{{lng}}.json` + } + }); + // eslint-disable-next-line no-param-reassign + acc[app.name] = newI18n; + return acc; + }, + {} as Record + ); + state.defaultI18n.t = state.instances[SHELL_APP_ID].t; + state.locale = locale; + }) + ); + } + } })); -export const setLocale = (locale: string): void => { - const { instances } = useI18nStore.getState(); - forEach(instances, (i18nInst) => i18nInst.changeLanguage(locale)); - useI18nStore.setState(() => ({ - locale - })); -}; - defaultI18n .use(Backend) // init i18next @@ -33,7 +111,7 @@ defaultI18n .init({ returnEmptyString: true, compatibilityJSON: 'v3', - lng: useI18nStore.getState().locale, + lng: defaultLng, fallbackLng: 'en', debug: false, interpolation: { @@ -47,41 +125,3 @@ defaultI18n loadPath: `${BASE_PATH}/i18n/{{lng}}.json` } }); - -export const addI18n = (apps: CarbonioModule[]): void => { - useI18nStore.setState({ - instances: reduce( - apps, - (acc, app) => { - const newI18n = i18next.createInstance(); - newI18n - // load translation using http -> see /public/locales (i.e. https://github.com/i18next/react-i18next/tree/master/example/react/public/locales) - // learn more: https://github.com/i18next/i18next-http-backend - .use(Backend) - // init i18next - // for all options read: https://www.i18next.com/overview/configuration-options - .init({ - returnEmptyString: true, - compatibilityJSON: 'v3', - lng: useI18nStore.getState().locale, - fallbackLng: 'en', - debug: false, - interpolation: { - escapeValue: false // not needed for react as it escapes by default - }, - missingKeyHandler: (_, __, key) => { - // eslint-disable-next-line no-console - console.warn(`Missing translation with key '${key}'`); - }, - backend: { - loadPath: `${dropRight(app.js_entrypoint.split('/')).join('/')}/i18n/{{lng}}.json` - } - }); - // eslint-disable-next-line no-param-reassign - acc[app.name] = newI18n; - return acc; - }, - {} as Record - ) - }); -}; diff --git a/src/store/integrations/composer.tsx b/src/store/integrations/composer.tsx index 052084ff..593f999d 100644 --- a/src/store/integrations/composer.tsx +++ b/src/store/integrations/composer.tsx @@ -3,8 +3,8 @@ * * SPDX-License-Identifier: AGPL-3.0-only */ -import React, { FC, useCallback, useMemo, useRef } from 'react'; import { Container } from '@zextras/carbonio-design-system'; +import React, { FC, useCallback, useMemo, useRef } from 'react'; import styled from 'styled-components'; // TinyMCE so the global var exists // eslint-disable-next-line no-unused-vars @@ -22,31 +22,31 @@ import 'tinymce/skins/ui/oxide/skin.min.css'; // importing the plugin js. import 'tinymce/plugins/advlist'; +import 'tinymce/plugins/anchor'; import 'tinymce/plugins/autolink'; -import 'tinymce/plugins/lists'; -import 'tinymce/plugins/link'; -import 'tinymce/plugins/image'; +import 'tinymce/plugins/autoresize'; import 'tinymce/plugins/charmap'; import 'tinymce/plugins/code'; -import 'tinymce/plugins/print'; -import 'tinymce/plugins/preview'; -import 'tinymce/plugins/anchor'; -import 'tinymce/plugins/searchreplace'; -import 'tinymce/plugins/visualblocks'; +import 'tinymce/plugins/directionality'; import 'tinymce/plugins/fullscreen'; +import 'tinymce/plugins/help'; +import 'tinymce/plugins/image'; import 'tinymce/plugins/insertdatetime'; +import 'tinymce/plugins/link'; +import 'tinymce/plugins/lists'; import 'tinymce/plugins/media'; -import 'tinymce/plugins/table'; import 'tinymce/plugins/paste'; -import 'tinymce/plugins/help'; -import 'tinymce/plugins/wordcount'; +import 'tinymce/plugins/preview'; +import 'tinymce/plugins/print'; import 'tinymce/plugins/quickbars'; -import 'tinymce/plugins/directionality'; -import 'tinymce/plugins/autoresize'; +import 'tinymce/plugins/searchreplace'; +import 'tinymce/plugins/table'; +import 'tinymce/plugins/visualblocks'; +import 'tinymce/plugins/wordcount'; import { Editor } from '@tinymce/tinymce-react'; -import { useTranslation } from 'react-i18next'; import { useUserSettings } from '../account'; +import { getT } from '../i18n'; type ComposerProps = { /** The callback invoked when an edit is performed into the editor. `([text, html]) => {}` */ @@ -101,7 +101,7 @@ const Composer: FC = ({ inputRef.current.click(); } }, []); - const { t } = useTranslation(); + const t = getT(); const inlineLabel = useMemo(() => t('label.add_inline_image', 'Add inline image'), [t]); return ( (lastLocationInitial); const [confirmedNavigation, setConfirmedNavigation] = useState(false); - const [t] = useTranslation(); + const t = getT(); const onClose = (): void => { setModalVisible(false); setConfirmedNavigation(true); diff --git a/src/utility-bar/bar.tsx b/src/utility-bar/bar.tsx index 8eaf1a6a..f73aa0d7 100644 --- a/src/utility-bar/bar.tsx +++ b/src/utility-bar/bar.tsx @@ -3,7 +3,6 @@ * * SPDX-License-Identifier: AGPL-3.0-only */ -import React, { FC, useCallback, useMemo } from 'react'; import { Container, Dropdown, @@ -12,14 +11,15 @@ import { Tooltip } from '@zextras/carbonio-design-system'; import { map, noop } from 'lodash'; -import { useTranslation } from 'react-i18next'; -import { useUtilityBarStore } from './store'; +import React, { FC, useCallback, useMemo } from 'react'; import { SHELL_APP_ID, UtilityView } from '../../types'; -import { useUtilityViews } from './utils'; -import { logout } from '../network/logout'; import { noOp } from '../network/fetch'; +import { logout } from '../network/logout'; import { useUserAccount } from '../store/account'; import { addBoard } from '../store/boards'; +import { getT } from '../store/i18n'; +import { useUtilityBarStore } from './store'; +import { useUtilityViews } from './utils'; const UtilityBarItem: FC<{ view: UtilityView }> = ({ view }) => { const { mode, setMode, current, setCurrent } = useUtilityBarStore(); @@ -45,7 +45,7 @@ const UtilityBarItem: FC<{ view: UtilityView }> = ({ view }) => { export const ShellUtilityBar: FC = () => { const views = useUtilityViews(); - const [t] = useTranslation(); + const t = getT(); const account = useUserAccount(); const accountItems = useMemo( () => diff --git a/tsconfig.json b/tsconfig.json index 8a6aa0f1..c051db62 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,7 +1,8 @@ { "extends": "@zextras/carbonio-ui-configs/rules/typescript.json", "compilerOptions": { - "skipLibCheck": true + "skipLibCheck": true, + "jsx": "react" }, "include": ["types/**/*.d.ts", "src/**/*.ts", "src/**/*.tsx", "types/workers/index.ts"], "exclude": ["node_modules"] diff --git a/types/exports/index.d.ts b/types/exports/index.d.ts index 4a9103f6..41e45e2a 100644 --- a/types/exports/index.d.ts +++ b/types/exports/index.d.ts @@ -5,10 +5,18 @@ */ /* eslint-disable @typescript-eslint/ban-types */ +import { i18n } from 'i18next'; import { ComponentType, Dispatch, FC, SetStateAction } from 'react'; -import { LinkProps } from 'react-router-dom'; import { TFunction } from 'react-i18next'; -import { i18n } from 'i18next'; +import { LinkProps } from 'react-router-dom'; +import { + Account, + AccountRightName, + AccountRights, + AccountRightTarget, + AccountSettings, + SoapFetch +} from '../account'; import { AppRoute, AppRouteDescriptor, @@ -21,29 +29,14 @@ import { SettingsView, UtilityView } from '../apps'; -import { ActionFactory, AnyFunction, CombinedActionFactory, Action } from '../integrations'; -import { - AccountSettings, - Account, - AccountRights, - AccountRightName, - AccountRightTarget, - SoapFetch -} from '../account'; -import { - Mods, - TagActionResponse, - CreateTagResponse, - SoapNotify, - SoapRefresh, - ErrorSoapResponse -} from '../network'; -import { HistoryParams, ShellModes, AccordionFolder } from '../misc'; -import { Tag, Tags } from '../tags'; -import { Folder, Folders } from '../folder'; -import { QueryChip } from '../search'; import { Board, BoardHooksContext } from '../boards'; +import { Folder, Folders } from '../folder'; +import { Action, ActionFactory, AnyFunction, CombinedActionFactory } from '../integrations'; +import { AccordionFolder, HistoryParams, ShellModes } from '../misc'; +import { CreateTagResponse, Mods, SoapNotify, SoapRefresh, TagActionResponse } from '../network'; import { INotificationManager } from '../notification'; +import { QueryChip } from '../search'; +import { Tag, Tags } from '../tags'; export const getBridgedFunctions: () => { createModal: (...params: any[]) => void; @@ -224,6 +217,7 @@ export const runSearch: (query: Array, module: string) => void; export const useLocalStorage: (key: string, initialValue: T) => [T, Dispatch>]; +// TRANSLATIONS export const getI18n: () => i18n; export const useI18n: () => i18n; export const t: TFunction; diff --git a/types/i18n/index.d.ts b/types/i18n/index.d.ts index b2ef762b..e5a71162 100644 --- a/types/i18n/index.d.ts +++ b/types/i18n/index.d.ts @@ -10,4 +10,10 @@ export type I18nState = { instances: Record; defaultI18n: i18n; locale: string; + setters: { + setLocale: (locale: string) => void; + }; + actions: { + addI18n: (apps: Array, locale: string) => void; + }; };