diff --git a/examples/getstarted/admin/admin.config.js b/examples/getstarted/admin/admin.config.js new file mode 100644 index 0000000000..a504354550 --- /dev/null +++ b/examples/getstarted/admin/admin.config.js @@ -0,0 +1,15 @@ +'use strict'; + +module.exports = { + webpack: (config, webpack) => { + // Note: we provide webpack above so you should not `require` it + // Perform customizations to webpack config + // Important: return the modified config + return config; + }, + app: config => { + config.locales = ['fr']; + + return config; + }, +}; diff --git a/examples/getstarted/plugins/myplugin/admin/src/index.js b/examples/getstarted/plugins/myplugin/admin/src/index.js index 0c5ee9f361..1262df50e6 100644 --- a/examples/getstarted/plugins/myplugin/admin/src/index.js +++ b/examples/getstarted/plugins/myplugin/admin/src/index.js @@ -1,3 +1,4 @@ +import { prefixPluginTranslations } from '@strapi/helper-plugin'; import pluginPkg from '../../package.json'; import pluginId from './pluginId'; @@ -15,8 +16,6 @@ export default { isRequired: pluginPkg.strapi.required || false, mainComponent: () => 'My plugin', name, - settings: null, - trads: {}, menu: { pluginsSectionLinks: [ { @@ -34,4 +33,27 @@ export default { }); }, boot() {}, + async registerTrads({ locales }) { + const importedTrads = await Promise.all( + locales.map(locale => { + return import( + /* webpackChunkName: "[pluginId]-[request]" */ `./translations/${locale}.json` + ) + .then(({ default: data }) => { + return { + data: prefixPluginTranslations(data, pluginId), + locale, + }; + }) + .catch(() => { + return { + data: {}, + locale, + }; + }); + }) + ); + + return Promise.resolve(importedTrads); + }, }; diff --git a/examples/getstarted/plugins/myplugin/admin/src/pages/App/index.js b/examples/getstarted/plugins/myplugin/admin/src/pages/App/index.js new file mode 100644 index 0000000000..ebd50fc061 --- /dev/null +++ b/examples/getstarted/plugins/myplugin/admin/src/pages/App/index.js @@ -0,0 +1,9 @@ +import React from 'react'; +import { FormattedMessage } from 'react-intl'; +import getTrad from '../../utils/getTrad'; + +const App = () => { + return ; +}; + +export default App; diff --git a/examples/getstarted/plugins/myplugin/admin/src/translations/en.json b/examples/getstarted/plugins/myplugin/admin/src/translations/en.json new file mode 100644 index 0000000000..5a983e46aa --- /dev/null +++ b/examples/getstarted/plugins/myplugin/admin/src/translations/en.json @@ -0,0 +1,3 @@ +{ + "plugin.name": "My plugin" +} diff --git a/examples/getstarted/plugins/myplugin/admin/src/utils/getTrad.js b/examples/getstarted/plugins/myplugin/admin/src/utils/getTrad.js new file mode 100644 index 0000000000..a2b8632a8d --- /dev/null +++ b/examples/getstarted/plugins/myplugin/admin/src/utils/getTrad.js @@ -0,0 +1,5 @@ +import pluginId from '../pluginId'; + +const getTrad = id => `${pluginId}.${id}`; + +export default getTrad; diff --git a/jest.config.front.js b/jest.config.front.js index d588447bf8..a7b652ba57 100644 --- a/jest.config.front.js +++ b/jest.config.front.js @@ -54,7 +54,10 @@ module.exports = { ], moduleNameMapper, rootDir: process.cwd(), - setupFiles: ['/test/config/front/test-bundler.js'], + setupFiles: [ + '/test/config/front/test-bundler.js', + '/packages/admin-test-utils/lib/mocks/LocalStorageMock.js', + ], testPathIgnorePatterns: [ '/node_modules/', '/examples/getstarted/', diff --git a/packages/admin-test-utils/lib/fixtures/store/index.js b/packages/admin-test-utils/lib/fixtures/store/index.js index 9afd206ac4..79e9df606e 100644 --- a/packages/admin-test-utils/lib/fixtures/store/index.js +++ b/packages/admin-test-utils/lib/fixtures/store/index.js @@ -4,7 +4,6 @@ const { combineReducers, createStore } = require('redux'); const reducers = { - language: jest.fn(() => ({ locale: 'en' })), menu: jest.fn(() => ({ generalSectionLinks: [ { diff --git a/packages/admin-test-utils/lib/mocks/LocalStorageMock.js b/packages/admin-test-utils/lib/mocks/LocalStorageMock.js new file mode 100644 index 0000000000..d516355392 --- /dev/null +++ b/packages/admin-test-utils/lib/mocks/LocalStorageMock.js @@ -0,0 +1,25 @@ +'use strict'; + +class LocalStorageMock { + constructor() { + this.store = {}; + } + + clear() { + this.store = {}; + } + + getItem(key) { + return this.store[key] || null; + } + + setItem(key, value) { + this.store[key] = String(value); + } + + removeItem(key) { + delete this.store[key]; + } +} + +global.localStorage = new LocalStorageMock(); diff --git a/packages/core/admin/admin/src/StrapiApp.js b/packages/core/admin/admin/src/StrapiApp.js index 5347364bcd..defac28c0a 100644 --- a/packages/core/admin/admin/src/StrapiApp.js +++ b/packages/core/admin/admin/src/StrapiApp.js @@ -4,6 +4,7 @@ import { BrowserRouter } from 'react-router-dom'; import { QueryClientProvider, QueryClient } from 'react-query'; import { ThemeProvider } from 'styled-components'; import { LibraryProvider, StrapiAppProvider } from '@strapi/helper-plugin'; +import pick from 'lodash/pick'; import createHook from '@strapi/hooks'; import invariant from 'invariant'; import configureStore from './core/store/configureStore'; @@ -17,9 +18,7 @@ import Fonts from './components/Fonts'; import GlobalStyle from './components/GlobalStyle'; import Notifications from './components/Notifications'; import themes from './themes'; - -// TODO -import translations from './translations'; +import languageNativeNames from './translations/languageNativeNames'; window.strapi = { backendURL: process.env.STRAPI_ADMIN_BACKEND_URL, @@ -33,16 +32,15 @@ const queryClient = new QueryClient({ }, }); -const appLocales = Object.keys(translations); - class StrapiApp { - constructor({ appPlugins, library, middlewares, reducers }) { + constructor({ appPlugins, library, locales, middlewares, reducers }) { + this.appLocales = ['en', ...locales.filter(loc => loc !== 'en')]; this.appPlugins = appPlugins || {}; this.library = library; this.middlewares = middlewares; this.plugins = {}; this.reducers = reducers; - this.translations = translations; + this.translations = {}; this.hooksDict = {}; this.menu = []; this.settings = { @@ -186,20 +184,51 @@ class StrapiApp { return this.plugins[pluginId]; }; - // FIXME - registerPluginTranslations(pluginId, trads) { - const pluginTranslations = appLocales.reduce((acc, currentLanguage) => { - const currentLocale = trads[currentLanguage]; + async loadAdminTrads() { + const arrayOfPromises = this.appLocales.map(locale => { + return import(/* webpackChunkName: "[request]" */ `./translations/${locale}.json`) + .then(({ default: data }) => { + return { data, locale }; + }) + .catch(() => { + return { data: {}, locale }; + }); + }); + const adminLocales = await Promise.all(arrayOfPromises); - if (currentLocale) { - const localeprefixedWithPluginId = Object.keys(currentLocale).reduce((acc2, current) => { - acc2[`${pluginId}.${current}`] = currentLocale[current]; + this.translations = adminLocales.reduce((acc, current) => { + acc[current.locale] = current.data; - return acc2; - }, {}); + return acc; + }, {}); - acc[currentLanguage] = localeprefixedWithPluginId; - } + return Promise.resolve(); + } + + async loadTrads() { + const arrayOfPromises = Object.keys(this.appPlugins) + .map(plugin => { + const registerTrads = this.appPlugins[plugin].registerTrads; + + if (registerTrads) { + return registerTrads({ locales: this.appLocales }); + } + + return null; + }) + .filter(a => a); + + const pluginsTrads = await Promise.all(arrayOfPromises); + const mergedTrads = pluginsTrads.reduce((acc, currentPluginTrads) => { + const pluginTrads = currentPluginTrads.reduce((acc1, current) => { + acc1[current.locale] = current.data; + + return acc1; + }, {}); + + Object.keys(pluginTrads).forEach(locale => { + acc[locale] = { ...acc[locale], ...pluginTrads[locale] }; + }); return acc; }, {}); @@ -207,19 +236,16 @@ class StrapiApp { this.translations = Object.keys(this.translations).reduce((acc, current) => { acc[current] = { ...this.translations[current], - ...(pluginTranslations[current] || {}), + ...(mergedTrads[current] || {}), }; return acc; }, {}); + + return Promise.resolve(); } registerPlugin = pluginConf => { - // FIXME - // Translations should be loaded differently - // This is a temporary fix - this.registerPluginTranslations(pluginConf.id, pluginConf.trads); - const plugin = Plugin(pluginConf); this.plugins[plugin.pluginId] = plugin; @@ -245,6 +271,7 @@ class StrapiApp { render() { const store = this.createStore(); + const localeNames = pick(languageNativeNames, this.appLocales); const { components: { components }, @@ -266,7 +293,7 @@ class StrapiApp { settings={this.settings} > - + @@ -286,5 +313,5 @@ class StrapiApp { } } -export default ({ appPlugins, library, middlewares, reducers }) => - new StrapiApp({ appPlugins, library, middlewares, reducers }); +export default ({ appPlugins, library, locales, middlewares, reducers }) => + new StrapiApp({ appPlugins, library, locales, middlewares, reducers }); diff --git a/packages/core/admin/admin/src/admin.config.js b/packages/core/admin/admin/src/admin.config.js new file mode 100644 index 0000000000..94503f6c2c --- /dev/null +++ b/packages/core/admin/admin/src/admin.config.js @@ -0,0 +1,7 @@ +module.exports = { + app: config => { + config.locales = ['fr']; + + return config; + }, +}; diff --git a/packages/core/admin/admin/src/components/LanguageProvider/actions.js b/packages/core/admin/admin/src/components/LanguageProvider/actions.js deleted file mode 100644 index 101d6a17c2..0000000000 --- a/packages/core/admin/admin/src/components/LanguageProvider/actions.js +++ /dev/null @@ -1,16 +0,0 @@ -/* - * - * LanguageProvider actions - * - */ - -/* eslint-disable */ - -import { CHANGE_LOCALE } from './constants'; - -export function changeLocale(languageLocale) { - return { - type: CHANGE_LOCALE, - locale: languageLocale, - }; -} diff --git a/packages/core/admin/admin/src/components/LanguageProvider/constants.js b/packages/core/admin/admin/src/components/LanguageProvider/constants.js deleted file mode 100644 index 8d2bd0d621..0000000000 --- a/packages/core/admin/admin/src/components/LanguageProvider/constants.js +++ /dev/null @@ -1,8 +0,0 @@ -/* - * - * LanguageProvider constants - * - */ - -/* eslint-disable */ -export const CHANGE_LOCALE = 'app/LanguageToggle/CHANGE_LOCALE'; diff --git a/packages/core/admin/admin/src/components/LanguageProvider/hooks/useChangeLanguage.js b/packages/core/admin/admin/src/components/LanguageProvider/hooks/useChangeLanguage.js deleted file mode 100644 index a4d4e55064..0000000000 --- a/packages/core/admin/admin/src/components/LanguageProvider/hooks/useChangeLanguage.js +++ /dev/null @@ -1,14 +0,0 @@ -import { useDispatch } from 'react-redux'; -import { changeLocale } from '../actions'; - -const useChangeLanguage = () => { - const dispatch = useDispatch(); - - const changeLanguage = nextLocale => { - dispatch(changeLocale(nextLocale)); - }; - - return changeLanguage; -}; - -export default useChangeLanguage; diff --git a/packages/core/admin/admin/src/components/LanguageProvider/index.js b/packages/core/admin/admin/src/components/LanguageProvider/index.js index d3486d6427..ac0b8b591f 100644 --- a/packages/core/admin/admin/src/components/LanguageProvider/index.js +++ b/packages/core/admin/admin/src/components/LanguageProvider/index.js @@ -6,44 +6,45 @@ * IntlProvider component and i18n messages (loaded from `app/translations`) */ -import React from 'react'; +import React, { useEffect, useReducer } from 'react'; import PropTypes from 'prop-types'; -import { connect } from 'react-redux'; -import { createSelector } from 'reselect'; import { IntlProvider } from 'react-intl'; -import { defaultsDeep } from 'lodash'; -import { selectLocale } from './selectors'; +import defaultsDeep from 'lodash/defaultsDeep'; +import LocalesProvider from '../LocalesProvider'; +import localStorageKey from './utils/localStorageKey'; +import init from './init'; +import reducer, { initialState } from './reducer'; -// eslint-disable-next-line react/prefer-stateless-function -export class LanguageProvider extends React.Component { - render() { - const messages = defaultsDeep(this.props.messages[this.props.locale], this.props.messages.en); +const LanguageProvider = ({ children, localeNames, messages }) => { + const [{ locale }, dispatch] = useReducer(reducer, initialState, () => init(localeNames)); - return ( - - {React.Children.only(this.props.children)} - - ); - } -} + useEffect(() => { + // Set user language in local storage. + window.localStorage.setItem(localStorageKey, locale); + }, [locale]); + + const changeLocale = locale => { + dispatch({ + type: 'CHANGE_LOCALE', + locale, + }); + }; + + const appMessages = defaultsDeep(messages[locale], messages.en); + + return ( + + + {children} + + + ); +}; LanguageProvider.propTypes = { children: PropTypes.element.isRequired, - locale: PropTypes.string.isRequired, + localeNames: PropTypes.objectOf(PropTypes.string).isRequired, messages: PropTypes.object.isRequired, }; -const mapStateToProps = createSelector(selectLocale(), locale => ({ locale })); - -function mapDispatchToProps(dispatch) { - return { - dispatch, - }; -} - -export default connect(mapStateToProps, mapDispatchToProps)(LanguageProvider); +export default LanguageProvider; diff --git a/packages/core/admin/admin/src/components/LanguageProvider/init.js b/packages/core/admin/admin/src/components/LanguageProvider/init.js new file mode 100644 index 0000000000..2e63937469 --- /dev/null +++ b/packages/core/admin/admin/src/components/LanguageProvider/init.js @@ -0,0 +1,13 @@ +import localStorageKey from './utils/localStorageKey'; + +const init = localeNames => { + const languageFromLocaleStorage = window.localStorage.getItem(localStorageKey); + const appLanguage = localeNames[languageFromLocaleStorage] ? languageFromLocaleStorage : 'en'; + + return { + locale: appLanguage, + localeNames, + }; +}; + +export default init; diff --git a/packages/core/admin/admin/src/components/LanguageProvider/reducer.js b/packages/core/admin/admin/src/components/LanguageProvider/reducer.js index 4ba19fc18e..6d2caec054 100644 --- a/packages/core/admin/admin/src/components/LanguageProvider/reducer.js +++ b/packages/core/admin/admin/src/components/LanguageProvider/reducer.js @@ -4,47 +4,27 @@ * */ -import { get, includes, split } from 'lodash'; - -// Import supported languages from the translations folder -import trads from '../../translations'; -import { CHANGE_LOCALE } from './constants'; - -const languages = Object.keys(trads); - -// Define a key to store and get user preferences in local storage. -const localStorageKey = 'strapi-admin-language'; - -// Detect user language. -const userLanguage = - window.localStorage.getItem(localStorageKey) || - window.navigator.language || - window.navigator.userLanguage; - -let foundLanguage = includes(languages, userLanguage) && userLanguage; - -if (!foundLanguage) { - // Split user language in a correct format. - const userLanguageShort = get(split(userLanguage, '-'), '0'); - - // Check that the language is included in the admin configuration. - foundLanguage = includes(languages, userLanguageShort) && userLanguageShort; -} - const initialState = { - locale: foundLanguage || 'en', + localeNames: { en: 'English' }, + locale: 'en', }; -function languageProviderReducer(state = initialState, action) { +const languageProviderReducer = (state = initialState, action) => { switch (action.type) { - case CHANGE_LOCALE: - // Set user language in local storage. - window.localStorage.setItem(localStorageKey, action.locale); + case 'CHANGE_LOCALE': { + const { locale } = action; - return { ...state, locale: action.locale }; - default: + if (!state.localeNames[locale]) { + return state; + } + + return { ...state, locale }; + } + default: { return state; + } } -} +}; export default languageProviderReducer; +export { initialState }; diff --git a/packages/core/admin/admin/src/components/LanguageProvider/selectors.js b/packages/core/admin/admin/src/components/LanguageProvider/selectors.js deleted file mode 100644 index b5558caad4..0000000000 --- a/packages/core/admin/admin/src/components/LanguageProvider/selectors.js +++ /dev/null @@ -1,17 +0,0 @@ -import { createSelector } from 'reselect'; - -/** - * Direct selector to the languageToggle state domain - */ -const selectLanguage = () => state => state.language; - -/** - * Select the language locale - */ - -const selectLocale = () => createSelector(selectLanguage(), languageState => languageState.locale); - -const makeSelectLocale = () => createSelector(selectLocale(), locale => ({ locale })); - -export default makeSelectLocale; -export { selectLanguage, selectLocale }; diff --git a/packages/core/admin/admin/src/components/LanguageProvider/tests/index.test.js b/packages/core/admin/admin/src/components/LanguageProvider/tests/index.test.js new file mode 100644 index 0000000000..cda97317b9 --- /dev/null +++ b/packages/core/admin/admin/src/components/LanguageProvider/tests/index.test.js @@ -0,0 +1,62 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { useIntl } from 'react-intl'; +import useLocalesProvider from '../../LocalesProvider/useLocalesProvider'; +import LanguageProvider from '../index'; +import en from '../../../translations/en.json'; +import fr from '../../../translations/fr.json'; + +const messages = { en, fr }; +const localeNames = { en: 'English', fr: 'Français' }; + +describe('LanguageProvider', () => { + afterEach(() => { + localStorage.removeItem('strapi-admin-language'); + }); + + it('should not crash', () => { + const { container } = render( + +
Test
+
+ ); + + expect(container.firstChild).toMatchInlineSnapshot(` +
+ Test +
+ `); + }); + + it('should change the locale and set the strapi-admin-language item in the localStorage', () => { + const Test = () => { + const { locale } = useIntl(); + const { changeLocale } = useLocalesProvider(); + + return ( +
+

{localeNames[locale]}

+ +
+ ); + }; + + render( + + + + ); + + expect(localStorage.getItem('strapi-admin-language')).toEqual('en'); + + expect(screen.getByText('English')).toBeInTheDocument(); + + userEvent.click(screen.getByText('CHANGE')); + + expect(screen.getByText('Français')).toBeInTheDocument(); + expect(localStorage.getItem('strapi-admin-language')).toEqual('fr'); + }); +}); diff --git a/packages/core/admin/admin/src/components/LanguageProvider/tests/init.test.js b/packages/core/admin/admin/src/components/LanguageProvider/tests/init.test.js new file mode 100644 index 0000000000..2e8cb1cbae --- /dev/null +++ b/packages/core/admin/admin/src/components/LanguageProvider/tests/init.test.js @@ -0,0 +1,34 @@ +import init from '../init'; + +const localeNames = { en: 'English', fr: 'Français' }; + +describe('LanguageProvider | init', () => { + afterEach(() => { + localStorage.removeItem('strapi-admin-language'); + }); + + it('should return the language from the localStorage', () => { + localStorage.setItem('strapi-admin-language', 'fr'); + + expect(init(localeNames)).toEqual({ + locale: 'fr', + localeNames, + }); + }); + + it('should return "en" when the strapi-admin-language is not set in the locale storage', () => { + expect(init(localeNames)).toEqual({ + locale: 'en', + localeNames, + }); + }); + + it('should return "en" when the language from the local storage is not included in the localeNames', () => { + localStorage.setItem('strapi-admin-language', 'foo'); + + expect(init(localeNames)).toEqual({ + locale: 'en', + localeNames, + }); + }); +}); diff --git a/packages/core/admin/admin/src/components/LanguageProvider/tests/reducer.test.js b/packages/core/admin/admin/src/components/LanguageProvider/tests/reducer.test.js new file mode 100644 index 0000000000..1dbdccce1d --- /dev/null +++ b/packages/core/admin/admin/src/components/LanguageProvider/tests/reducer.test.js @@ -0,0 +1,41 @@ +import reducer, { initialState } from '../reducer'; + +describe('LanguageProvider | reducer', () => { + let state; + + beforeEach(() => { + state = initialState; + }); + + it('should return the initialState', () => { + const action = { type: undefined }; + + expect(reducer(state, action)).toEqual(initialState); + }); + + it('should change the locale correctly when the locale is defined in the localeNames', () => { + state = { + localeNames: { en: 'English', fr: 'Français' }, + locale: 'en', + }; + + const action = { type: 'CHANGE_LOCALE', locale: 'fr' }; + const expected = { + localeNames: { en: 'English', fr: 'Français' }, + locale: 'fr', + }; + + expect(reducer(state, action)).toEqual(expected); + }); + + it('should not change the locale when the language is not defined in the localeNames', () => { + state = { + localeNames: { en: 'English', fr: 'Français' }, + locale: 'en', + }; + + const action = { type: 'CHANGE_LOCALE', locale: 'foo' }; + + expect(reducer(state, action)).toEqual(state); + }); +}); diff --git a/packages/core/admin/admin/src/components/LanguageProvider/utils/localStorageKey.js b/packages/core/admin/admin/src/components/LanguageProvider/utils/localStorageKey.js new file mode 100644 index 0000000000..034ff886ca --- /dev/null +++ b/packages/core/admin/admin/src/components/LanguageProvider/utils/localStorageKey.js @@ -0,0 +1,3 @@ +const localStorageKey = 'strapi-admin-language'; + +export default localStorageKey; diff --git a/packages/core/admin/admin/src/components/LocaleToggle/index.js b/packages/core/admin/admin/src/components/LocaleToggle/index.js index aff3415698..49c4505ac3 100644 --- a/packages/core/admin/admin/src/components/LocaleToggle/index.js +++ b/packages/core/admin/admin/src/components/LocaleToggle/index.js @@ -4,76 +4,42 @@ * */ -import React from 'react'; -import PropTypes from 'prop-types'; -import { connect } from 'react-redux'; -import { createStructuredSelector } from 'reselect'; -import { bindActionCreators, compose } from 'redux'; -import cn from 'classnames'; +import React, { useState } from 'react'; +import { useIntl } from 'react-intl'; import { ButtonDropdown, DropdownItem, DropdownMenu, DropdownToggle } from 'reactstrap'; -import translationMessages, { languageNativeNames } from '../../translations'; -import makeSelectLocale from '../LanguageProvider/selectors'; -import { changeLocale } from '../LanguageProvider/actions'; +import useLocalesProvider from '../LocalesProvider/useLocalesProvider'; import Wrapper from './Wrapper'; -// TODO -const languages = Object.keys(translationMessages); -export class LocaleToggle extends React.Component { - // eslint-disable-line - state = { isOpen: false }; +const LocaleToggle = () => { + const { changeLocale, localeNames } = useLocalesProvider(); - toggle = () => this.setState(prevState => ({ isOpen: !prevState.isOpen })); + const [isOpen, setIsOpen] = useState(false); + const toggle = () => setIsOpen(prev => !prev); + const { locale } = useIntl(); - render() { - const { - currentLocale: { locale }, - } = this.props; + return ( + + + + {localeNames[locale]} + - return ( - - - - {languageNativeNames[locale]} - - - - {languages.map(language => ( + + {Object.keys(localeNames).map(lang => { + return ( this.props.changeLocale(language)} - className={cn( - 'localeToggleItem', - locale === language ? 'localeToggleItemActive' : '' - )} + key={lang} + onClick={() => changeLocale(lang)} + className={`localeToggleItem ${locale === lang ? 'localeToggleItemActive' : ''}`} > - {languageNativeNames[language]} + {localeNames[lang]} - ))} - - - - ); - } -} - -LocaleToggle.propTypes = { - changeLocale: PropTypes.func.isRequired, - currentLocale: PropTypes.object.isRequired, + ); + })} + + + + ); }; -const mapStateToProps = createStructuredSelector({ - currentLocale: makeSelectLocale(), -}); - -export function mapDispatchToProps(dispatch) { - return bindActionCreators( - { - changeLocale, - }, - dispatch - ); -} - -const withConnect = connect(mapStateToProps, mapDispatchToProps); - -export default compose(withConnect)(LocaleToggle); +export default LocaleToggle; diff --git a/packages/core/admin/admin/src/components/LocaleToggle/tests/index.test.js b/packages/core/admin/admin/src/components/LocaleToggle/tests/index.test.js index 30c380389c..1fe55951ca 100644 --- a/packages/core/admin/admin/src/components/LocaleToggle/tests/index.test.js +++ b/packages/core/admin/admin/src/components/LocaleToggle/tests/index.test.js @@ -1,61 +1,217 @@ import React from 'react'; -import { shallow } from 'enzyme'; -import { DropdownItem } from 'reactstrap'; -import { changeLocale } from '../../LanguageProvider/actions'; -import { LocaleToggle, mapDispatchToProps } from '../index'; +import { render } from '@testing-library/react'; +import LanguageProvider from '../../LanguageProvider'; +import en from '../../../translations/en.json'; +import LocaleToggle from '../index'; + +const messages = { en }; +const localeNames = { en: 'English' }; describe('', () => { - let props; - - beforeEach(() => { - props = { - changeLocale: jest.fn(), - currentLocale: { - locale: 'en', - }, - }; - }); - it('should not crash', () => { - shallow(); - }); + const App = ( + + + + ); - describe(', toggle instance', () => { - it('should update the state when called', () => { - const renderedComponent = shallow(); - const { toggle } = renderedComponent.instance(); + const { container } = render(App); + expect(container.firstChild).toMatchInlineSnapshot(` + .c0 { + -webkit-font-smoothing: antialiased; + } - toggle(); + .c0 > div { + height: 6rem; + line-height: 5.8rem; + z-index: 999; + } - expect(renderedComponent.state('isOpen')).toBe(true); - }); + .c0 > div > button { + width: 100%; + padding: 0 30px; + background: transparent; + border: none; + border-radius: 0; + color: #333740; + font-weight: 500; + text-align: right; + cursor: pointer; + -webkit-transition: background 0.2s ease-out; + transition: background 0.2s ease-out; + } - it('call the toggle handle on click', () => { - const renderedComponent = shallow(); - renderedComponent.setState({ isOpen: true }); - const dropDown = renderedComponent.find(DropdownItem).at(0); - dropDown.simulate('click'); + .c0 > div > button:hover, + .c0 > div > button:focus, + .c0 > div > button:active { + color: #333740; + background-color: #fafafb !important; + } - expect(props.changeLocale).toHaveBeenCalled(); - }); - }); + .c0 > div > button > i, + .c0 > div > button > svg { + margin-left: 10px; + -webkit-transition: -webkit-transform 0.3s ease-out; + -webkit-transition: transform 0.3s ease-out; + transition: transform 0.3s ease-out; + } - describe(', mapDispatchToProps', () => { - describe('changeLocale', () => { - it('should be injected', () => { - const dispatch = jest.fn(); - const result = mapDispatchToProps(dispatch); + .c0 > div > button > i[alt='true'], + .c0 > div > button > svg[alt='true'] { + -webkit-transform: rotateX(180deg); + -ms-transform: rotateX(180deg); + transform: rotateX(180deg); + } - expect(result.changeLocale).toBeDefined(); - }); + .c0 .localeDropdownContent { + -webkit-font-smoothing: antialiased; + } - it('should dispatch the changeLocale action when called', () => { - const dispatch = jest.fn(); - const result = mapDispatchToProps(dispatch); - result.changeLocale(); + .c0 .localeDropdownContent span { + color: #333740; + font-size: 13px; + font-family: Lato; + font-weight: 500; + -webkit-letter-spacing: 0.5; + -moz-letter-spacing: 0.5; + -ms-letter-spacing: 0.5; + letter-spacing: 0.5; + vertical-align: baseline; + } - expect(dispatch).toHaveBeenCalledWith(changeLocale()); - }); - }); + .c0 .localeDropdownMenu { + min-width: 90px !important; + max-height: 162px !important; + overflow: auto !important; + margin: 0 !important; + padding: 0; + line-height: 1.8rem; + border: none !important; + border-top-left-radius: 0 !important; + border-top-right-radius: 0 !important; + box-shadow: 0 1px 4px 0px rgba(40,42,49,0.05); + } + + .c0 .localeDropdownMenu:before { + content: ''; + position: absolute; + top: -3px; + left: -1px; + width: calc(100% + 1px); + height: 3px; + box-shadow: 0 1px 2px 0 rgba(40,42,49,0.16); + } + + .c0 .localeDropdownMenu > button { + height: 40px; + padding: 0px 15px; + line-height: 40px; + color: #f75b1d; + font-size: 13px; + font-weight: 500; + -webkit-letter-spacing: 0.5; + -moz-letter-spacing: 0.5; + -ms-letter-spacing: 0.5; + letter-spacing: 0.5; + } + + .c0 .localeDropdownMenu > button:hover, + .c0 .localeDropdownMenu > button:focus, + .c0 .localeDropdownMenu > button:active { + background-color: #fafafb !important; + border-radius: 0px; + cursor: pointer; + } + + .c0 .localeDropdownMenu > button:first-child { + line-height: 50px; + margin-bottom: 4px; + } + + .c0 .localeDropdownMenu > button:first-child:hover, + .c0 .localeDropdownMenu > button:first-child:active { + color: #333740; + } + + .c0 .localeDropdownMenu > button:not(:first-child) { + height: 36px; + line-height: 36px; + } + + .c0 .localeDropdownMenu > button:not(:first-child) > i, + .c0 .localeDropdownMenu > button:not(:first-child) > svg { + margin-left: 10px; + } + + .c0 .localeDropdownMenuNotLogged { + background: transparent !important; + box-shadow: none !important; + border: 1px solid #e3e9f3 !important; + border-top: 0px !important; + } + + .c0 .localeDropdownMenuNotLogged button { + padding-left: 17px; + } + + .c0 .localeDropdownMenuNotLogged button:hover { + background-color: #f7f8f8 !important; + } + + .c0 .localeDropdownMenuNotLogged:before { + box-shadow: none !important; + } + + .c0 .localeToggleItem img { + max-height: 13.37px; + margin-left: 9px; + } + + .c0 .localeToggleItem:active { + color: black; + } + + .c0 .localeToggleItem:hover { + background-color: #fafafb !important; + } + + .c0 .localeToggleItemActive { + color: #333740 !important; + } + +
+
+ + +
+
+ `); }); }); diff --git a/packages/core/admin/admin/src/components/LocalesProvider/context.js b/packages/core/admin/admin/src/components/LocalesProvider/context.js new file mode 100644 index 0000000000..1621c38aa6 --- /dev/null +++ b/packages/core/admin/admin/src/components/LocalesProvider/context.js @@ -0,0 +1,5 @@ +import { createContext } from 'react'; + +const LocalesProviderContext = createContext(); + +export default LocalesProviderContext; diff --git a/packages/core/admin/admin/src/components/LocalesProvider/index.js b/packages/core/admin/admin/src/components/LocalesProvider/index.js new file mode 100644 index 0000000000..53db089b0c --- /dev/null +++ b/packages/core/admin/admin/src/components/LocalesProvider/index.js @@ -0,0 +1,20 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import LocalesProviderContext from './context'; + +const LocalesProvider = ({ changeLocale, children, localeNames, messages }) => { + return ( + + {children} + + ); +}; + +LocalesProvider.propTypes = { + changeLocale: PropTypes.func.isRequired, + children: PropTypes.element.isRequired, + localeNames: PropTypes.object.isRequired, + messages: PropTypes.object.isRequired, +}; + +export default LocalesProvider; diff --git a/packages/core/admin/admin/src/components/LocalesProvider/tests/index.test.js b/packages/core/admin/admin/src/components/LocalesProvider/tests/index.test.js new file mode 100644 index 0000000000..c3a6853e28 --- /dev/null +++ b/packages/core/admin/admin/src/components/LocalesProvider/tests/index.test.js @@ -0,0 +1,23 @@ +import React from 'react'; +import { render } from '@testing-library/react'; +import LocalesProvider from '../index'; + +describe('LocalesProvider', () => { + it('should not crash', () => { + const { container } = render( + +
Test
+
+ ); + + expect(container.firstChild).toMatchInlineSnapshot(` +
+ Test +
+ `); + }); +}); diff --git a/packages/core/admin/admin/src/components/LocalesProvider/useLocalesProvider.js b/packages/core/admin/admin/src/components/LocalesProvider/useLocalesProvider.js new file mode 100644 index 0000000000..1c0e418fb9 --- /dev/null +++ b/packages/core/admin/admin/src/components/LocalesProvider/useLocalesProvider.js @@ -0,0 +1,10 @@ +import { useContext } from 'react'; +import LocalesProviderContext from './context'; + +const useLocalesProvider = () => { + const { changeLocale, localeNames, messages } = useContext(LocalesProviderContext); + + return { changeLocale, localeNames, messages }; +}; + +export default useLocalesProvider; diff --git a/packages/core/admin/admin/src/components/PageTitle/index.js b/packages/core/admin/admin/src/components/PageTitle/index.js index da75bb920c..0e44b36d6e 100644 --- a/packages/core/admin/admin/src/components/PageTitle/index.js +++ b/packages/core/admin/admin/src/components/PageTitle/index.js @@ -4,12 +4,12 @@ import PropTypes from 'prop-types'; import favicon from '../../favicon.png'; -const PageTitle = ({ title }) => ( - -); +const PageTitle = ({ title }) => { + return ; +}; PageTitle.propTypes = { - title: PropTypes.string.isRequired, + title: PropTypes.oneOfType([PropTypes.string, PropTypes.arrayOf(PropTypes.string)]).isRequired, }; export default memo(PageTitle); diff --git a/packages/core/admin/admin/src/index.js b/packages/core/admin/admin/src/index.js index 1bd858f339..3789770c5a 100644 --- a/packages/core/admin/admin/src/index.js +++ b/packages/core/admin/admin/src/index.js @@ -1,22 +1,37 @@ import ReactDOM from 'react-dom'; import StrapiApp from './StrapiApp'; import { Components, Fields, Middlewares, Reducers } from './core/apis'; +import appCustomisations from './admin.config'; import plugins from './plugins'; import appReducers from './reducers'; +const appConfig = { + locales: [], +}; + +const customConfig = appCustomisations.app(appConfig); + const library = { components: Components(), fields: Fields(), }; const middlewares = Middlewares(); const reducers = Reducers({ appReducers }); -const app = StrapiApp({ appPlugins: plugins, library, middlewares, reducers }); +const app = StrapiApp({ + appPlugins: plugins, + library, + locales: customConfig.locales, + middlewares, + reducers, +}); const MOUNT_NODE = document.getElementById('app'); const run = async () => { + await app.loadAdminTrads(); await app.initialize(); await app.boot(); + await app.loadTrads(); ReactDOM.render(app.render(), MOUNT_NODE); }; diff --git a/packages/core/admin/admin/src/pages/Admin/tests/index.test.js b/packages/core/admin/admin/src/pages/Admin/tests/index.test.js index 72b0aac723..5a56eec57d 100644 --- a/packages/core/admin/admin/src/pages/Admin/tests/index.test.js +++ b/packages/core/admin/admin/src/pages/Admin/tests/index.test.js @@ -13,13 +13,14 @@ import RBACProvider from '../../../components/RBACProvider'; import Admin from '../index'; const messages = { en }; +const localeNames = { en: 'English' }; const store = fixtures.store.store; const makeApp = (history, plugins) => ( - + { const toggleNotification = useNotification(); const { push } = useHistory(); - const changeLocale = useChangeLanguage(); + const { changeLocale } = useLocalesProvider(); const { params: { authType }, } = useRouteMatch('/auth/:authType'); diff --git a/packages/core/admin/admin/src/pages/HomePage/index.js b/packages/core/admin/admin/src/pages/HomePage/index.js index 171c59646e..8d2f1891c6 100644 --- a/packages/core/admin/admin/src/pages/HomePage/index.js +++ b/packages/core/admin/admin/src/pages/HomePage/index.js @@ -108,7 +108,7 @@ const HomePage = ({ history: { push } }) => { return ( <> - {title => } + {title => }
diff --git a/packages/core/admin/admin/src/pages/ProfilePage/index.js b/packages/core/admin/admin/src/pages/ProfilePage/index.js index 817f3a26dc..67059ffe74 100644 --- a/packages/core/admin/admin/src/pages/ProfilePage/index.js +++ b/packages/core/admin/admin/src/pages/ProfilePage/index.js @@ -4,25 +4,22 @@ import { Padded, Text } from '@buffetjs/core'; import { Col } from 'reactstrap'; import { get } from 'lodash'; import { useIntl } from 'react-intl'; -import translationMessages, { languageNativeNames } from '../../translations'; import ContainerFluid from '../../components/ContainerFluid'; +import useLocalesProvider from '../../components/LocalesProvider/useLocalesProvider'; import PageTitle from '../../components/PageTitle'; import SizedInput from '../../components/SizedInput'; import { Header } from '../../components/Settings'; import FormBloc from '../../components/FormBloc'; import { useSettingsForm } from '../../hooks'; -import useChangeLanguage from '../../components/LanguageProvider/hooks/useChangeLanguage'; import ProfilePageLabel from './components'; import { form, schema } from './utils'; -const languages = Object.keys(translationMessages); - const ProfilePage = () => { - const changeLanguage = useChangeLanguage(); + const { changeLocale, localeNames } = useLocalesProvider(); const { formatMessage } = useIntl(); const onSubmitSuccessCb = data => { - changeLanguage(data.preferedLanguage); + changeLocale(data.preferedLanguage); auth.setUserInfo(data); }; @@ -129,8 +126,8 @@ const ProfilePage = () => { selectedValue={get(modifiedData, 'preferedLanguage')} onChange={nextLocaleCode => setField('preferedLanguage', nextLocaleCode)} > - {languages.map(language => { - const langName = languageNativeNames[language]; + {Object.keys(localeNames).map(language => { + const langName = localeNames[language]; return (