diff --git a/.storybook/LangSwitcher.jsx b/.storybook/LangSwitcher.jsx new file mode 100644 index 0000000..be17dc1 --- /dev/null +++ b/.storybook/LangSwitcher.jsx @@ -0,0 +1,22 @@ +import React, { useState, useMemo } from 'react' +import { TransProvider } from '../src/Atoms/Trans' + +const LangSwitcher = ({ langs = {}, children }) => { + const availableLangs = useMemo(() => Object.keys(langs), []) + const [lang, setLang] = useState(availableLangs[0]) + + return ( + + +
+ {children} +
+
+ ) +} + +export default LangSwitcher diff --git a/.storybook/config.js b/.storybook/config.js index cfed3e5..ee5acf7 100644 --- a/.storybook/config.js +++ b/.storybook/config.js @@ -9,18 +9,18 @@ import { withThemesProvider } from 'storybook-addon-styled-component-theme' import { ThemeProvider } from '@material-ui/core/styles' import { MuiPickersUtilsProvider } from '../src/Atoms/DateTimePicker' -import { TransProvider } from '../src/Atoms/Trans' import results from '../.jest-test-results.json' import { muiRg6Theme } from './themes' import { rg6, dark } from '../src/themes' -import { en } from './locales' +import LangSwitcher from './LangSwitcher' +import { locales } from '../src' addDecorator(withKnobs) addDecorator(withA11y) addDecorator(withThemesProvider([rg6, dark])) addDecorator(withTests({ results })) -addDecorator(storyFn => {storyFn()}) addDecorator(storyFn => {storyFn()}) addDecorator(storyFn => {storyFn()}) +addDecorator(storyFn => {storyFn()}) // automatically import all files ending in *.stories.jsx configure(require.context('../src', true, /\.stories\.jsx$/), module) diff --git a/.storybook/locales/en.js b/.storybook/locales/en.js deleted file mode 100644 index dfa1835..0000000 --- a/.storybook/locales/en.js +++ /dev/null @@ -1,34 +0,0 @@ -export default { - global: { - dateFormat: 'dd/MM/yyyy hh:mm', - no_results: 'No results found', - filter: { - clearCurrent: 'Clear current filters', - }, - action: { - chooseOption: 'Choose an option', - cancel: 'Cancel', - add: 'Add', - }, - pagination: { - perPage: '%count% per page', - }, - action: { - remove: 'Supprimer', - }, - export: { - title: 'Export', - description: 'You will receive an email containing a link that contains the export', - filename: 'File name', - defaultFilename: 'My export', - format: 'File format', - actionExport: 'Export' - }, - editColumns: { - title: 'Edit columns', - description: 'You can hide/show the columns as well as change the display order', - enabledColumns: 'Enabled columns', - disabledColumns: 'Disabled columns', - } - } -} diff --git a/.storybook/locales/index.js b/.storybook/locales/index.js deleted file mode 100644 index e31d752..0000000 --- a/.storybook/locales/index.js +++ /dev/null @@ -1,3 +0,0 @@ -import en from './en' - -export { en } diff --git a/src/Atoms/Trans/Trans.stories.jsx b/src/Atoms/Trans/Trans.stories.jsx index 811868f..733eef6 100644 --- a/src/Atoms/Trans/Trans.stories.jsx +++ b/src/Atoms/Trans/Trans.stories.jsx @@ -1,6 +1,7 @@ import React from 'react' -import Trans, { TransProvider } from './index' +import Trans, { TransProvider, useTranslation } from './index' +import FlexBox from '../../Templates/FlexBox' import markdown from './README.md' @@ -8,20 +9,64 @@ export default { title: 'Atoms/Trans', } -const translations = { - global: { - welcome: 'Hello there !', - }, +const LocalWrapper = ({ translations = {}, ...props }) => { + const t = useTranslation() + const lang = t('lang') + + return } -export const trans = () => ( - - -
- -
+const TransWrapper = ({ transKey, ...props }) =>
{transKey}: {transKey}
+ +const FirstChild = () => ( + + + + + ) +const SecondChild = () => ( + + + + + + + +) + +export const trans = () => <> + + + variables} /> + + + + + + outscope + + + + + trans.story = { parameters: { notes: { markdown }, diff --git a/src/Atoms/Trans/useTranslation.js b/src/Atoms/Trans/useTranslation.js index 189a3c5..c9a8e28 100644 --- a/src/Atoms/Trans/useTranslation.js +++ b/src/Atoms/Trans/useTranslation.js @@ -1,22 +1,70 @@ -import { createContext, useContext } from 'react' +import React, { createContext, useContext, useMemo, Fragment } from 'react' +import { deepMerge } from '../../utils' const Context = createContext() -export const { Provider } = Context +const { Provider: BaseProvider } = Context + +export const Provider = ({ value, ...props }) => { + const translations = useContext(Context) || {} + const merged = useMemo( + () => deepMerge(translations, value), + [translations, value], + ) + + return +} + +const preProcessor = ({ translations }) => (key = '') => ( + key.split('.').reduce( + (acc, k) => (acc[k] || key), + translations, + ) +) + +const processor = ({ translations = {}, ...context }) => { + const params = Object.entries(context) + if (!params.length) { + return translation => [translation] + } + const mapReplace = params.reduce( + (acc, [k, v]) => Object.assign(acc, { [`%${k}%`]: v }), + {}, + ) + const re = new RegExp(Object.keys(mapReplace).join('|'), 'g') + + return function * (translation = '') { + let lastIndex = 0 + for (const match of translation.matchAll(re)) { + yield translation.slice(lastIndex, match.index) + yield mapReplace[match[0]] + lastIndex = match.index + match[0].length + } + yield translation.slice(lastIndex) + } +} + +const postProcessor = () => (processedTranslation = []) => { + let result = '' + for (const chunk of processedTranslation) { + console.info('chunk', chunk) + if (!['number', 'string'].includes(typeof chunk)) { + return React.createElement(Fragment, null, result, chunk, ...processedTranslation) + } + result = result.concat(chunk) + } + return result +} export default () => { const translations = useContext(Context) || {} return (transKey = '', parameters = {}) => { - let translated = transKey - .split('.') - .reduce( - (acc, k) => acc[k] || transKey, - translations, - ) - - Object.entries(parameters).forEach(([key, value]) => { - translated = translated.replace(new RegExp(`%${key}%`, 'g'), value) - }) - - return translated + const context = { translations, ...parameters } + + const preProcessed = preProcessor(context)(transKey) + const processed = processor(context)(preProcessed) + const postProcessed = postProcessor(context)(processed) + + console.info(transKey, postProcessed) + return postProcessed } } diff --git a/src/Molecules/DateRange/index.js b/src/Molecules/DateRange/index.js index a5d3aba..d115397 100644 --- a/src/Molecules/DateRange/index.js +++ b/src/Molecules/DateRange/index.js @@ -1,3 +1,5 @@ import DateRange from './DateRange' +import * as locales from './locales' +export { locales } export default DateRange diff --git a/src/Molecules/DateRange/locales/en.js b/src/Molecules/DateRange/locales/en.js new file mode 100644 index 0000000..a1e6be3 --- /dev/null +++ b/src/Molecules/DateRange/locales/en.js @@ -0,0 +1,6 @@ +const locale = { + startDate: 'Start date', + endDate: 'End date', +} + +export default locale diff --git a/src/Molecules/DateRange/locales/fr.js b/src/Molecules/DateRange/locales/fr.js new file mode 100644 index 0000000..0e45b2a --- /dev/null +++ b/src/Molecules/DateRange/locales/fr.js @@ -0,0 +1,6 @@ +const locale = { + startDate: 'Date de début', + endDate: 'Date de fin', +} + +export default locale diff --git a/src/Molecules/DateRange/locales/index.js b/src/Molecules/DateRange/locales/index.js new file mode 100644 index 0000000..0d882f7 --- /dev/null +++ b/src/Molecules/DateRange/locales/index.js @@ -0,0 +1,4 @@ +import en from './en' +import fr from './fr' + +export { en, fr } diff --git a/src/Molecules/Export/Export.jsx b/src/Molecules/Export/Export.jsx index 75e4003..07843c6 100644 --- a/src/Molecules/Export/Export.jsx +++ b/src/Molecules/Export/Export.jsx @@ -20,12 +20,12 @@ const Actions = ({ onClose, onExport, disabled }) => <> const Export = ({ - descriptionText = , + descriptionText = , onExport = () => {}, value = {}, onChange = () => {}, @@ -36,7 +36,7 @@ const Export = ({ ...props }) => { const t = useTranslation() - const { defaultName = t('global.export.defaultFilename') } = props + const { defaultName = t('molecules.export.defaultFilename') } = props const { format = '', filename = defaultName } = value const onFileNameChange = event => onChange({ filename: event.target.value, format }) @@ -45,13 +45,13 @@ const Export = ({ - + {descriptionText} - + diff --git a/src/Molecules/Export/Export.stories.jsx b/src/Molecules/Export/Export.stories.jsx index 4a933e7..38bd644 100644 --- a/src/Molecules/Export/Export.stories.jsx +++ b/src/Molecules/Export/Export.stories.jsx @@ -1,10 +1,10 @@ import React, { useState } from 'react' import { action } from '@storybook/addon-actions' -import { } from '@storybook/addon-knobs' import Export from './index' import FormControl, { FormLabel, FormControlLabel } from '../../Molecules/FormControl' import Radio from '../../Atoms/Radio' +import { TransProvider, useTranslation } from '../../Atoms/Trans' import RadioGroup from '@material-ui/core/RadioGroup' import markdown from './README.md' @@ -23,57 +23,87 @@ const exportFormats = [ ] const extraOptions = [ - { label: 'Choose red pill', value: 'red' }, - { label: 'Choose blue pill', value: 'blue' }, + { label: 'extraOptions.redPill', value: 'red' }, + { label: 'extraOptions.bluePill', value: 'blue' }, ] -const ExtraOptions = ({ value, onChange }) => - - - Extra option - - { - onChange(event.target.value) - action('extra option changed')(event.target.value) - }} - > - {extraOptions.map(({ label, value }) => - } - label={label} - />, - )} - - +const ExtraOptions = ({ value, onChange }) => { + const t = useTranslation() + return ( + + {t('extraOptions.label')} + { + onChange(event.target.value) + action('extra option changed')(event.target.value) + }} + > + {extraOptions.map(({ label, value }) => + } + label={t(label)} + />, + )} + + + ) +} + +const customLocales = { + en: { + extraOptions: { + label: 'Extra option', + redPill: 'Choose red pill', + bluePill: 'Choose blue pill', + }, + }, + fr: { + extraOptions: { + label: 'Option supplémentaire', + redPill: 'Prendre la pilule rouge', + bluePill: 'prendre la pilule bleue', + }, + }, +} + +const Wrapper = ({ ...props }) => { + const t = useTranslation() + const lang = t('lang') + + return +} export const exportStory = () => { const [value, setValue] = useState({ format: '' }) - return { - setValue({ ...value, ...newValue }) - action('export value changed')(newValue) - }} - onClose={action('export canceled')} - onExport={action('export launched')} - formats={exportFormats} - extraOptions={ - { - setValue({ ...value, extraOption }) + return ( + + { + setValue({ ...value, ...newValue }) + action('export value changed')(newValue) }} + onClose={action('export canceled')} + onExport={action('export launched')} + formats={exportFormats} + extraOptions={ + { + setValue({ ...value, extraOption }) + }} + /> + } /> - } - /> + + ) } exportStory.story = { diff --git a/src/Molecules/Export/Export.test.jsx b/src/Molecules/Export/Export.test.jsx index 54f9c41..7b1bf14 100644 --- a/src/Molecules/Export/Export.test.jsx +++ b/src/Molecules/Export/Export.test.jsx @@ -29,7 +29,7 @@ it('should not call onExport when the filename is empty', () => { , ) - userEvent.click(getByText('global.export.actionExport')) + userEvent.click(getByText('molecules.export.actionExport')) expect(onExport).toHaveBeenCalledTimes(0) }) @@ -40,7 +40,7 @@ it('should not call onExport when the format is empty', () => { , ) - userEvent.click(getByText('global.export.actionExport')) + userEvent.click(getByText('molecules.export.actionExport')) expect(onExport).toHaveBeenCalledTimes(0) }) @@ -51,7 +51,7 @@ it('should call onExport when the filename and format are filled', () => { , ) - userEvent.click(getByText('global.export.actionExport')) + userEvent.click(getByText('molecules.export.actionExport')) expect(onExport).toHaveBeenCalled() }) @@ -67,7 +67,7 @@ it('should call onExport when filename and format are filled in but disabled is />, ) - userEvent.click(getByText('global.export.actionExport')) + userEvent.click(getByText('molecules.export.actionExport')) expect(onExport).toHaveBeenCalledTimes(0) }) @@ -83,8 +83,8 @@ it('should call onChange when filename has changed', async () => { const { getByLabelText } = render() - fireEvent.change(getByLabelText(/global\.export\.filename/), { target: { value: 'test' } }) - userEvent.selectOptions(getByLabelText(/global\.export\.format/), 'json') + fireEvent.change(getByLabelText(/molecules\.export\.filename/), { target: { value: 'test' } }) + userEvent.selectOptions(getByLabelText(/molecules\.export\.format/), 'json') expect(onChange).toHaveBeenNthCalledWith(1, { filename: 'test', format: 'xls' }) expect(onChange).toHaveBeenNthCalledWith(2, { filename: 'test', format: 'json' }) diff --git a/src/Molecules/Export/Formats/Formats.jsx b/src/Molecules/Export/Formats/Formats.jsx index 5f3b2df..32f41e3 100644 --- a/src/Molecules/Export/Formats/Formats.jsx +++ b/src/Molecules/Export/Formats/Formats.jsx @@ -9,7 +9,7 @@ const Formats = ({ formats, value, onChange }) => formats.length > 0 && - + diff --git a/src/Molecules/Export/Formats/locales/en.js b/src/Molecules/Export/Formats/locales/en.js new file mode 100644 index 0000000..e4169a8 --- /dev/null +++ b/src/Molecules/Export/Formats/locales/en.js @@ -0,0 +1,5 @@ +const locale = { + label: 'File format', +} + +export default locale diff --git a/src/Molecules/Export/Formats/locales/fr.js b/src/Molecules/Export/Formats/locales/fr.js new file mode 100644 index 0000000..f46fd31 --- /dev/null +++ b/src/Molecules/Export/Formats/locales/fr.js @@ -0,0 +1,5 @@ +const locale = { + label: 'Format de fichier', +} + +export default locale diff --git a/src/Molecules/Export/Formats/locales/index.js b/src/Molecules/Export/Formats/locales/index.js new file mode 100644 index 0000000..0d882f7 --- /dev/null +++ b/src/Molecules/Export/Formats/locales/index.js @@ -0,0 +1,4 @@ +import en from './en' +import fr from './fr' + +export { en, fr } diff --git a/src/Molecules/Export/locales/en.js b/src/Molecules/Export/locales/en.js new file mode 100644 index 0000000..b401528 --- /dev/null +++ b/src/Molecules/Export/locales/en.js @@ -0,0 +1,12 @@ +import { en as format } from '../Formats/locales' + +const locale = { + format, + title: 'Export', + description: 'You will receive an email containing a link that contains the export', + filename: 'File name', + defaultFilename: 'My export', + actionExport: 'Export', +} + +export default locale diff --git a/src/Molecules/Export/locales/fr.js b/src/Molecules/Export/locales/fr.js new file mode 100644 index 0000000..8c67683 --- /dev/null +++ b/src/Molecules/Export/locales/fr.js @@ -0,0 +1,12 @@ +import { fr as format } from '../Formats/locales' + +const locale = { + format, + title: 'Export', + description: 'Vous allez recevoir un email cotenant un lien vers le fichier exporté', + filename: 'Nom du fichier', + defaultFilename: 'Mon export', + actionExport: 'Exporter', +} + +export default locale diff --git a/src/Molecules/Export/locales/index.js b/src/Molecules/Export/locales/index.js new file mode 100644 index 0000000..0d882f7 --- /dev/null +++ b/src/Molecules/Export/locales/index.js @@ -0,0 +1,4 @@ +import en from './en' +import fr from './fr' + +export { en, fr } diff --git a/src/Molecules/index.js b/src/Molecules/index.js index 3c2f534..914afc8 100644 --- a/src/Molecules/index.js +++ b/src/Molecules/index.js @@ -1,3 +1,6 @@ +import * as locales from './locales' + +export { locales } export { default as Pagination } from './Pagination' export { default as FormControl, diff --git a/src/Molecules/locales/en.js b/src/Molecules/locales/en.js new file mode 100644 index 0000000..26ae5ae --- /dev/null +++ b/src/Molecules/locales/en.js @@ -0,0 +1,9 @@ +import { en as dateRange } from '../DateRange/locales' +import { en as exportLocales } from '../Export/locales' + +const locale = { + dateRange, + export: exportLocales, +} + +export default locale diff --git a/src/Molecules/locales/fr.js b/src/Molecules/locales/fr.js new file mode 100644 index 0000000..bbd318d --- /dev/null +++ b/src/Molecules/locales/fr.js @@ -0,0 +1,9 @@ +import { fr as dateRange } from '../DateRange/locales' +import { fr as exportLocales } from '../Export/locales' + +const locale = { + dateRange, + export: exportLocales, +} + +export default locale diff --git a/src/Molecules/locales/index.js b/src/Molecules/locales/index.js new file mode 100644 index 0000000..0d882f7 --- /dev/null +++ b/src/Molecules/locales/index.js @@ -0,0 +1,4 @@ +import en from './en' +import fr from './fr' + +export { en, fr } diff --git a/src/Organisms/EnhancedList/EnhancedList.jsx b/src/Organisms/EnhancedList/EnhancedList.jsx index 0f67a68..857492f 100644 --- a/src/Organisms/EnhancedList/EnhancedList.jsx +++ b/src/Organisms/EnhancedList/EnhancedList.jsx @@ -115,7 +115,7 @@ const EnhancedList = ({ {!!Export && - + setExportAnchorEl(event.currentTarget)} /> } diff --git a/src/index.js b/src/index.js index 58ff210..728a04a 100644 --- a/src/index.js +++ b/src/index.js @@ -1,3 +1,6 @@ +import * as locales from './locales' +export { locales } + export { Button } from './Atoms' export { Pagination } from './Molecules' export { Input } from './Atoms' diff --git a/src/locales/en.js b/src/locales/en.js new file mode 100644 index 0000000..ed8b4d9 --- /dev/null +++ b/src/locales/en.js @@ -0,0 +1,30 @@ +import { en as molecules } from '../Molecules/locales' + +const locale = { + lang: 'en', + molecules, + global: { + dateFormat: 'dd/MM/yyyy hh:mm', + no_results: 'No results found', + filter: { + clearCurrent: 'Clear current filters', + }, + action: { + chooseOption: 'Choose an option', + cancel: 'Cancel', + add: 'Add', + remove: 'Remove', + }, + pagination: { + perPage: '%count% per page', + }, + editColumns: { + title: 'Edit columns', + description: 'You can hide/show the columns as well as change the display order', + enabledColumns: 'Enabled columns', + disabledColumns: 'Disabled columns', + }, + }, +} + +export default locale diff --git a/src/locales/fr.js b/src/locales/fr.js new file mode 100644 index 0000000..c2e8da6 --- /dev/null +++ b/src/locales/fr.js @@ -0,0 +1,30 @@ +import { fr as molecules } from '../Molecules/locales' + +const locale = { + lang: 'fr', + molecules, + global: { + dateFormat: 'dd/MM/yyyy hh:mm', + no_results: 'Aucun résultat', + filter: { + clearCurrent: 'Clear current filters', + }, + action: { + chooseOption: 'Choisissez une option', + cancel: 'Anuler', + add: 'Ajouter', + remove: 'Supprimer', + }, + pagination: { + perPage: '%count% par page', + }, + editColumns: { + title: 'Edition des colonnes', + description: 'Vous pouvez afficher/cacher les colonnes ou changer l\'ordre des colonnes', + enabledColumns: 'Colonnes affichées', + disabledColumns: 'Colonnes cachées', + }, + }, +} + +export default locale diff --git a/src/locales/index.js b/src/locales/index.js new file mode 100644 index 0000000..0d882f7 --- /dev/null +++ b/src/locales/index.js @@ -0,0 +1,4 @@ +import en from './en' +import fr from './fr' + +export { en, fr } diff --git a/src/utils.js b/src/utils.js index e8e59f6..34cfea5 100644 --- a/src/utils.js +++ b/src/utils.js @@ -6,3 +6,17 @@ export const groupBy = key => arrayOfObjects => arrayOfObjects.reduce( }), {}, ) + +const areObjects = (...things) => things.reduce( + (result, thing) => result && typeof thing === 'object', + true, +) + +export const deepMerge = (source, overrides = {}) => Object.entries(overrides).reduce( + (source, [k, v]) => ( + source[k] === v ? source : // nothing to override + areObjects(source[k], v) ? { ...source, [k]: deepMerge(source[k], v) } : // go deeper + { ...source, [k]: v } // simple override + ), + source, +) diff --git a/src/utils.test.js b/src/utils.test.js index 1a37409..2338802 100644 --- a/src/utils.test.js +++ b/src/utils.test.js @@ -1,5 +1,5 @@ /* eslint-disable max-lines */ -import { pipe, groupBy } from './utils' +import { pipe, groupBy, deepMerge } from './utils' describe('pipe', () => { const multiply = x => y => y * x @@ -100,3 +100,35 @@ describe('groupBy', () => { }) }) }) + +// eslint-disable-next-line max-lines-per-function +describe('deepMerge', () => { + it('groups objects by values of a given key', () => { + const source = { + override: { + key: 'source', + keep: true, + }, + keep: true, + } + const override = { + override: { + key: 'override', + add: true, + }, + add: true, + } + + const result = deepMerge(source, override) + + expect(result).toEqual({ + override: { + key: 'override', + keep: true, + add: true, + }, + keep: true, + add: true, + }) + }) +})