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,
+ })
+ })
+})