diff --git a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/getting-started.mdx b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/getting-started.mdx
index efe9c42ae53..89d46921970 100644
--- a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/getting-started.mdx
+++ b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/getting-started.mdx
@@ -390,7 +390,7 @@ const validator = debounceAsync(async function myValidator(value) {
render( )
```
-### Localization (locale) and translation
+### Localization
You can set the locale for your form by using the `locale` property on the [Form.Handler](/uilib/extensions/forms/Form/Handler/) component. This will ensure that the correct language is used for all the fields in your form.
@@ -406,17 +406,39 @@ function MyForm() {
}
```
-Alternatively, you can use the global Eufemia [Provider](/uilib/usage/customisation/localization/) to set the locale for your application (forms). The locale will be inherited by all the components in your form.
+#### Customize translations
-#### Localization with translation
+You can customize the internal translations in a flat structure:
-In addition, you can customize the translations:
+```tsx
+{
+ 'nb-NO': { 'PhoneNumber.label': 'Egendefinert' },
+ 'en-GB': { 'PhoneNumber.label': 'Custom' },
+}
+```
+
+or an object based structure:
```tsx
-const myTranslations = {
+{
'nb-NO': { PhoneNumber: { label: 'Egendefinert' } },
'en-GB': { PhoneNumber: { label: 'Custom' } },
}
+```
+
+#### How to customize translations in a form
+
+You can customize the translations in a form by using the `translations` property on the [Form.Handler](/uilib/extensions/forms/Form/Handler/).
+
+Alternatively, you can use the global Eufemia [Provider](/uilib/usage/customisation/localization/) (example further down).
+
+```tsx
+import { Form, Field } from '@dnb/eufemia/extensions/forms'
+
+const myTranslations = {
+ 'nb-NO': { 'PhoneNumber.label': 'Egendefinert' },
+ 'en-GB': { 'PhoneNumber.label': 'Custom' },
+}
render(
@@ -425,10 +447,91 @@ render(
)
```
-Here is an [example](/uilib/extensions/forms/Form/Handler/demos/#locale-and-translations) of how to use the translations in a form.
+You can consume them with the `useTranslation` hook:
+
+```tsx
+import { Form, Field } from '@dnb/eufemia/extensions/forms'
+import { useTranslation } from '@dnb/eufemia/shared'
+
+const myTranslations = {
+ 'nb-NO': { 'custom.string': 'Min egendefinerte streng' },
+ 'en-GB': { 'custom.string': 'My custom string' },
+}
+
+const MyComponent = () => {
+ const t = useTranslation()
+ return t.custom.string
+}
+
+render(
+
+
+ ,
+)
+```
+
+Or customize them with the ` ` component:
+
+```tsx
+import { Form, Field } from '@dnb/eufemia/extensions/forms'
+import { Translation, TranslationProps } from '@dnb/eufemia/shared'
+
+const myTranslations = {
+ 'nb-NO': { 'custom.string': 'Min egendefinerte streng' },
+ 'en-GB': { 'custom.string': 'My custom string' },
+}
+
+// For TypeScript support
+type Tr = TranslationProps
+const Tr = (props: Tr) =>
+
+render(
+
+
+
+
+
+
+ t.custom.string} />
+
+ ,
+)
+```
+
+Here is a [demo](/uilib/extensions/forms/Form/Handler/demos/#locale-and-translations) of how to use the translations in a form.
When creating [your own field](/uilib/extensions/forms/create-component/#localization-and-translations), you can use the `Form.useTranslation` hook to localize your field.
+#### Use the shared Provider to customize translations
+
+```tsx
+import { Form, Field } from '@dnb/eufemia/extensions/forms'
+import { Provider, Translation } from '@dnb/eufemia/shared'
+
+const myTranslations = {
+ 'nb-NO': {
+ 'PhoneNumber.label': 'Egendefinert',
+ 'custom.string': 'Min egendefinerte streng',
+ },
+ 'en-GB': {
+ 'PhoneNumber.label': 'Custom',
+ 'custom.string': 'My custom string',
+ },
+}
+
+render(
+
+
+
+
+
+
+
+
+ ,
+)
+```
+
### Layout
When building your application forms, preferably use the following layout components. They seamlessly places all the fields and components of Eufemia Forms correctly into place.
diff --git a/packages/dnb-eufemia/src/extensions/forms/Form/FieldProps/FieldProps.tsx b/packages/dnb-eufemia/src/extensions/forms/Form/FieldProps/FieldProps.tsx
index 178a8f28341..20bd6a6f7e2 100644
--- a/packages/dnb-eufemia/src/extensions/forms/Form/FieldProps/FieldProps.tsx
+++ b/packages/dnb-eufemia/src/extensions/forms/Form/FieldProps/FieldProps.tsx
@@ -8,7 +8,7 @@ import {
} from '../../../../shared/component-helper'
import FieldPropsContext from './FieldPropsContext'
import SharedProvider from '../../../../shared/Provider'
-import { ContextProps } from '../../../../shared/Context'
+import SharedContext, { ContextProps } from '../../../../shared/Context'
import type { FieldProps, Path, UseFieldProps } from '../../types'
export type FieldPropsProps = FieldProps & {
@@ -49,21 +49,16 @@ function FieldPropsProvider(props: FieldPropsProps) {
...restProps
} = props
+ const sharedProviderParams: ContextProps = {}
const nestedContext = useContext(FieldPropsContext)
+ const sharedContext = useContext(SharedContext)
const dataContextRef = useRef()
- dataContextRef.current = useContext(DataContext)
-
- const sharedProviderProps: ContextProps = {}
+ dataContextRef.current = useContext(DataContext)
/**
* Always use data context as the last source for localization
*/
const locale = dataContextRef.current?.props?.locale ?? restProps?.locale
- const translations = extendDeep(
- {},
- restProps?.translations,
- dataContextRef.current?.props?.translations
- ) as ContextProps
const nestedFieldProps = useMemo(() => {
return Object.assign(
@@ -73,27 +68,29 @@ function FieldPropsProvider(props: FieldPropsProps) {
}, [nestedContext?.inheritedProps, restProps])
if (typeof nestedFieldProps.disabled === 'boolean') {
- sharedProviderProps.formElement = {
+ sharedProviderParams.formElement = {
disabled: nestedFieldProps.disabled,
}
}
if (formElement) {
- sharedProviderProps.formElement = formElement
+ sharedProviderParams.formElement = formElement
}
if (FormStatus) {
- sharedProviderProps.FormStatus = FormStatus
+ sharedProviderParams.FormStatus = FormStatus
}
if (locale) {
- sharedProviderProps.locale = locale
- }
- const formTranslations = useMemo(() => {
- if (translations) {
- return transformTranslations(translations)
- }
- }, [translations])
- if (formTranslations) {
- sharedProviderProps.translations = formTranslations
+ sharedProviderParams.locale = locale
}
+ sharedProviderParams.translations = useMemo(() => {
+ const translations = extendDeep(
+ {},
+ sharedContext.translations,
+ restProps?.translations,
+ dataContextRef.current?.props?.translations
+ ) as ContextProps
+
+ return translations
+ }, [restProps?.translations, sharedContext.translations])
const extend = useCallback(
(fieldProps: T) => {
@@ -135,46 +132,10 @@ function FieldPropsProvider(props: FieldPropsProps) {
inheritedContext: nestedFieldProps,
}}
>
- {children}
+ {children}
)
}
-/**
- * Transform translations into { 'nb-NO': { Forms: { ... } } }
- */
-function transformTranslations(
- translations: ContextProps['translations']
-) {
- const result = {}
- const trObj = translations as Record<
- ContextProps['locale'],
- Record>
- >
-
- for (const locale in trObj) {
- const newObj: Record<
- 'Forms',
- Record>
- > = {
- Forms: {},
- }
-
- for (const key in trObj[locale]) {
- const newKeyObj: Record = {}
-
- for (const subKey in trObj[locale][key]) {
- newKeyObj[subKey] = trObj[locale][key][subKey]
- }
-
- newObj.Forms[key] = newKeyObj
- }
-
- result[locale] = newObj
- }
-
- return result
-}
-
FieldPropsProvider._supportsSpacingProps = 'children'
export default FieldPropsProvider
diff --git a/packages/dnb-eufemia/src/extensions/forms/Form/Handler/__tests__/Handler.test.tsx b/packages/dnb-eufemia/src/extensions/forms/Form/Handler/__tests__/Handler.test.tsx
index 21eb03a971c..6cb2ca80a03 100644
--- a/packages/dnb-eufemia/src/extensions/forms/Form/Handler/__tests__/Handler.test.tsx
+++ b/packages/dnb-eufemia/src/extensions/forms/Form/Handler/__tests__/Handler.test.tsx
@@ -958,7 +958,7 @@ describe('Form.Handler', () => {
)
})
- it('should support nested translations prop', () => {
+ it('should support translations prop', () => {
const translations = {
'nb-NO': { PhoneNumber: { label: 'Egendefinert' } },
'en-GB': { PhoneNumber: { label: 'Custom' } },
diff --git a/packages/dnb-eufemia/src/extensions/forms/constants/__tests__/FormsTranslations.test.tsx b/packages/dnb-eufemia/src/extensions/forms/constants/__tests__/FormsTranslations.test.tsx
new file mode 100644
index 00000000000..bd4c84b2a92
--- /dev/null
+++ b/packages/dnb-eufemia/src/extensions/forms/constants/__tests__/FormsTranslations.test.tsx
@@ -0,0 +1,564 @@
+import React from 'react'
+import { render } from '@testing-library/react'
+import { Field, Form } from '../..'
+import {
+ Provider,
+ Translation,
+ useTranslation,
+ TranslationProps,
+} from '../../../../../shared'
+import nbNO from '../locales/nb-NO'
+import enGB from '../locales/en-GB'
+
+const nb = nbNO['nb-NO']
+const gb = enGB['en-GB']
+
+type Tr = TranslationProps
+
+describe('Form.Handler', () => {
+ describe('with object translations', () => {
+ it('should handle custom and internal strings', () => {
+ const translations = {
+ 'nb-NO': {
+ my: { string: 'Min streng 1' },
+ PhoneNumber: { label: 'Egendefinert 1' },
+ },
+ 'en-GB': {
+ my: { string: 'My string 1' },
+ PhoneNumber: { label: 'Custom 1' },
+ },
+ }
+
+ const Tr = (p: Tr) =>
+
+ const MockComponent = (props) => {
+ return (
+
+
+
+ t.my.string} />
+
+
+ )
+ }
+
+ const { rerender } = render( )
+
+ const [countryCode, phoneNumber, output1] = Array.from(
+ document.querySelectorAll('label, output')
+ )
+
+ expect(countryCode).toHaveTextContent(
+ gb.PhoneNumber.countryCodeLabel
+ )
+ expect(phoneNumber).toHaveTextContent(
+ translations['en-GB'].PhoneNumber.label
+ )
+ expect(output1).toHaveTextContent(translations['en-GB'].my.string)
+
+ rerender( )
+
+ expect(countryCode).toHaveTextContent(
+ nb.PhoneNumber.countryCodeLabel
+ )
+ expect(phoneNumber).toHaveTextContent(
+ translations['nb-NO'].PhoneNumber.label
+ )
+ expect(output1).toHaveTextContent(translations['nb-NO'].my.string)
+ })
+ })
+
+ describe('with flat translations', () => {
+ it('should handle custom and internal strings', () => {
+ const translations = {
+ 'nb-NO': {
+ 'my.string': 'Min streng 2',
+ 'PhoneNumber.label': 'Egendefinert 2',
+ },
+ 'en-GB': {
+ 'my.string': 'My string 2',
+ 'PhoneNumber.label': 'Custom 2',
+ },
+ }
+
+ const Tr = (p: Tr) =>
+
+ const GlobalHook = () => {
+ const t = useTranslation()
+ return t.my.string
+ }
+ const FormsHook = () => {
+ const t = Form.useTranslation()
+ return t.PhoneNumber.label
+ }
+
+ const MockComponent = (props) => {
+ return (
+
+
+
+ t.my.string} />
+
+
+
+
+
+
+
+
+ )
+ }
+
+ const { rerender } = render( )
+
+ const [countryCode, phoneNumber, output1, output2, output3] =
+ Array.from(document.querySelectorAll('label, output'))
+
+ expect(countryCode).toHaveTextContent(
+ gb.PhoneNumber.countryCodeLabel
+ )
+ expect(phoneNumber).toHaveTextContent(
+ translations['en-GB']['PhoneNumber.label']
+ )
+ expect(output1).toHaveTextContent(translations['en-GB']['my.string'])
+ expect(output2).toHaveTextContent(translations['en-GB']['my.string'])
+ expect(output3).toHaveTextContent(
+ translations['en-GB']['PhoneNumber.label']
+ )
+
+ rerender( )
+
+ expect(countryCode).toHaveTextContent(
+ nb.PhoneNumber.countryCodeLabel
+ )
+ expect(phoneNumber).toHaveTextContent(
+ translations['nb-NO']['PhoneNumber.label']
+ )
+ expect(output1).toHaveTextContent(translations['nb-NO']['my.string'])
+ expect(output2).toHaveTextContent(translations['nb-NO']['my.string'])
+ expect(output3).toHaveTextContent(
+ translations['nb-NO']['PhoneNumber.label']
+ )
+ })
+ })
+})
+
+describe('Form.Section', () => {
+ describe('with object translations', () => {
+ it('should handle custom and internal strings', () => {
+ const translations = {
+ 'nb-NO': {
+ my: { string: 'Min streng 1' },
+ PhoneNumber: { label: 'Egendefinert 1' },
+ },
+ 'en-GB': {
+ my: { string: 'My string 1' },
+ PhoneNumber: { label: 'Custom 1' },
+ },
+ }
+
+ const Tr = (p: Tr) =>
+
+ const MockComponent = (props) => {
+ return (
+
+
+
+
+ t.my.string} />
+
+
+
+ )
+ }
+
+ const { rerender } = render( )
+
+ const [countryCode, phoneNumber, output1] = Array.from(
+ document.querySelectorAll('label, output')
+ )
+
+ expect(countryCode).toHaveTextContent(
+ gb.PhoneNumber.countryCodeLabel
+ )
+ expect(phoneNumber).toHaveTextContent(
+ translations['en-GB'].PhoneNumber.label
+ )
+ expect(output1).toHaveTextContent(translations['en-GB'].my.string)
+
+ rerender( )
+
+ expect(countryCode).toHaveTextContent(
+ nb.PhoneNumber.countryCodeLabel
+ )
+ expect(phoneNumber).toHaveTextContent(
+ translations['nb-NO'].PhoneNumber.label
+ )
+ expect(output1).toHaveTextContent(translations['nb-NO'].my.string)
+ })
+
+ it('should let Form.Handler overwrite translations', () => {
+ const sectionTranslations = {
+ 'nb-NO': {
+ my: { string: 'Min streng 1' },
+ PhoneNumber: { label: 'Egendefinert 1' },
+ },
+ 'en-GB': {
+ my: { string: 'My string 1' },
+ PhoneNumber: { label: 'Custom 1' },
+ },
+ }
+ const handlerTranslations = {
+ 'nb-NO': {}, // Skip overwriting of sectionTranslations when switching to nb-NO
+ 'en-GB': {
+ my: { string: 'My string 2' },
+ PhoneNumber: { label: 'Custom 2' },
+ },
+ }
+
+ const Tr = (p: Tr) => (
+
+ )
+
+ const MockComponent = (props) => {
+ return (
+
+
+
+
+ t.my.string} />
+
+
+
+ )
+ }
+
+ const { rerender } = render( )
+
+ const [countryCode, phoneNumber, output1] = Array.from(
+ document.querySelectorAll('label, output')
+ )
+
+ expect(countryCode).toHaveTextContent(
+ gb.PhoneNumber.countryCodeLabel
+ )
+ expect(phoneNumber).toHaveTextContent(
+ handlerTranslations['en-GB'].PhoneNumber.label
+ )
+ expect(output1).toHaveTextContent(
+ handlerTranslations['en-GB'].my.string
+ )
+
+ rerender( )
+
+ expect(countryCode).toHaveTextContent(
+ nb.PhoneNumber.countryCodeLabel
+ )
+ expect(phoneNumber).toHaveTextContent(
+ sectionTranslations['nb-NO'].PhoneNumber.label
+ )
+ expect(output1).toHaveTextContent(
+ sectionTranslations['nb-NO'].my.string
+ )
+ })
+ })
+
+ describe('with flat translations', () => {
+ it('should handle custom and internal strings', () => {
+ const translations = {
+ 'nb-NO': {
+ 'my.string': 'Min streng 2',
+ 'PhoneNumber.label': 'Egendefinert 2',
+ },
+ 'en-GB': {
+ 'my.string': 'My string 2',
+ 'PhoneNumber.label': 'Custom 2',
+ },
+ }
+
+ const Tr = (p: Tr) =>
+
+ const GlobalHook = () => {
+ const t = useTranslation()
+ return t.my.string
+ }
+ const FormsHook = () => {
+ const t = Form.useTranslation()
+ return t.PhoneNumber.label
+ }
+
+ const MockComponent = (props) => {
+ return (
+
+
+
+
+ t.my.string} />
+
+
+
+
+
+
+
+
+
+ )
+ }
+
+ const { rerender } = render( )
+
+ const [countryCode, phoneNumber, output1, output2, output3] =
+ Array.from(document.querySelectorAll('label, output'))
+
+ expect(countryCode).toHaveTextContent(
+ gb.PhoneNumber.countryCodeLabel
+ )
+ expect(phoneNumber).toHaveTextContent(
+ translations['en-GB']['PhoneNumber.label']
+ )
+ expect(output1).toHaveTextContent(translations['en-GB']['my.string'])
+ expect(output2).toHaveTextContent(translations['en-GB']['my.string'])
+ expect(output3).toHaveTextContent(
+ translations['en-GB']['PhoneNumber.label']
+ )
+
+ rerender( )
+
+ expect(countryCode).toHaveTextContent(
+ nb.PhoneNumber.countryCodeLabel
+ )
+ expect(phoneNumber).toHaveTextContent(
+ translations['nb-NO']['PhoneNumber.label']
+ )
+ expect(output1).toHaveTextContent(translations['nb-NO']['my.string'])
+ expect(output2).toHaveTextContent(translations['nb-NO']['my.string'])
+ expect(output3).toHaveTextContent(
+ translations['nb-NO']['PhoneNumber.label']
+ )
+ })
+
+ it('should let Form.Handler overwrite translations', () => {
+ const sectionTranslations = {
+ 'nb-NO': {
+ 'my.string': 'Min streng 2',
+ 'PhoneNumber.label': 'Egendefinert 2',
+ },
+ 'en-GB': {
+ 'my.string': 'My string 2',
+ 'PhoneNumber.label': 'Custom 2',
+ },
+ }
+ const handlerTranslations = {
+ 'nb-NO': {}, // Skip overwriting of sectionTranslations when switching to nb-NO
+ 'en-GB': {
+ 'my.string': 'My string 2',
+ 'PhoneNumber.label': 'Custom 2',
+ },
+ }
+
+ const Tr = (p: Tr) => (
+
+ )
+
+ const GlobalHook = () => {
+ const t = useTranslation()
+ return t.my.string
+ }
+ const FormsHook = () => {
+ const t = Form.useTranslation()
+ return t.PhoneNumber.label
+ }
+
+ const MockComponent = (props) => {
+ return (
+
+
+
+
+ t.my.string} />
+
+
+
+
+
+
+
+
+
+ )
+ }
+
+ const { rerender } = render( )
+
+ const [countryCode, phoneNumber, output1, output2, output3] =
+ Array.from(document.querySelectorAll('label, output'))
+
+ expect(countryCode).toHaveTextContent(
+ gb.PhoneNumber.countryCodeLabel
+ )
+ expect(phoneNumber).toHaveTextContent(
+ handlerTranslations['en-GB']['PhoneNumber.label']
+ )
+ expect(output1).toHaveTextContent(
+ handlerTranslations['en-GB']['my.string']
+ )
+ expect(output2).toHaveTextContent(
+ handlerTranslations['en-GB']['my.string']
+ )
+ expect(output3).toHaveTextContent(
+ handlerTranslations['en-GB']['PhoneNumber.label']
+ )
+
+ rerender( )
+
+ expect(countryCode).toHaveTextContent(
+ nb.PhoneNumber.countryCodeLabel
+ )
+ expect(phoneNumber).toHaveTextContent(
+ sectionTranslations['nb-NO']['PhoneNumber.label']
+ )
+ expect(output1).toHaveTextContent(
+ sectionTranslations['nb-NO']['my.string']
+ )
+ expect(output2).toHaveTextContent(
+ sectionTranslations['nb-NO']['my.string']
+ )
+ expect(output3).toHaveTextContent(
+ sectionTranslations['nb-NO']['PhoneNumber.label']
+ )
+ })
+ })
+})
+
+describe('Shared Provider', () => {
+ describe('with object translations', () => {
+ it('should handle custom and internal strings', () => {
+ const translations = {
+ 'nb-NO': {
+ my: { string: 'Min streng 3' },
+ PhoneNumber: { label: 'Egendefinert 3' },
+ },
+ 'en-GB': {
+ my: { string: 'My string 3' },
+ PhoneNumber: { label: 'Custom 3' },
+ },
+ }
+
+ const Tr = (p: Tr) =>
+
+ const MockComponent = (props) => {
+ return (
+
+
+
+
+ t.my.string} />
+
+
+
+ )
+ }
+
+ const { rerender } = render( )
+
+ const [countryCode, phoneNumber, output1] = Array.from(
+ document.querySelectorAll('label, output')
+ )
+
+ expect(countryCode).toHaveTextContent(
+ gb.PhoneNumber.countryCodeLabel
+ )
+ expect(phoneNumber).toHaveTextContent(
+ translations['en-GB'].PhoneNumber.label
+ )
+ expect(output1).toHaveTextContent(translations['en-GB'].my.string)
+
+ rerender( )
+
+ expect(countryCode).toHaveTextContent(
+ nb.PhoneNumber.countryCodeLabel
+ )
+ expect(phoneNumber).toHaveTextContent(
+ translations['nb-NO'].PhoneNumber.label
+ )
+ expect(output1).toHaveTextContent(translations['nb-NO'].my.string)
+ })
+ })
+
+ describe('with flat translations', () => {
+ it('should handle custom and internal strings', () => {
+ const translations = {
+ 'nb-NO': {
+ 'my.string': 'Min streng 4',
+ 'PhoneNumber.label': 'Egendefinert 4',
+ },
+ 'en-GB': {
+ 'my.string': 'My string 4',
+ 'PhoneNumber.label': 'Custom 4',
+ },
+ }
+
+ const Tr = (p: Tr) =>
+
+ const GlobalHook = () => {
+ const t = useTranslation()
+ return t.my.string
+ }
+ const FormsHook = () => {
+ const t = Form.useTranslation()
+ return t.PhoneNumber.label
+ }
+
+ const MockComponent = (props) => {
+ return (
+
+
+
+
+ t.my.string} />
+
+
+
+
+
+
+
+
+
+ )
+ }
+
+ const { rerender } = render( )
+
+ const [countryCode, phoneNumber, output1, output2, output3] =
+ Array.from(document.querySelectorAll('label, output'))
+
+ expect(countryCode).toHaveTextContent(
+ gb.PhoneNumber.countryCodeLabel
+ )
+ expect(phoneNumber).toHaveTextContent(
+ translations['en-GB']['PhoneNumber.label']
+ )
+ expect(output1).toHaveTextContent(translations['en-GB']['my.string'])
+ expect(output2).toHaveTextContent(translations['en-GB']['my.string'])
+ expect(output3).toHaveTextContent(
+ translations['en-GB']['PhoneNumber.label']
+ )
+
+ rerender( )
+
+ expect(countryCode).toHaveTextContent(
+ nb.PhoneNumber.countryCodeLabel
+ )
+ expect(phoneNumber).toHaveTextContent(
+ translations['nb-NO']['PhoneNumber.label']
+ )
+ expect(output1).toHaveTextContent(translations['nb-NO']['my.string'])
+ expect(output2).toHaveTextContent(translations['nb-NO']['my.string'])
+ expect(output3).toHaveTextContent(
+ translations['nb-NO']['PhoneNumber.label']
+ )
+ })
+ })
+})
diff --git a/packages/dnb-eufemia/src/extensions/forms/hooks/__tests__/useTranslation.test.tsx b/packages/dnb-eufemia/src/extensions/forms/hooks/__tests__/useTranslation.test.tsx
index 68d166c36ab..116c895ed89 100644
--- a/packages/dnb-eufemia/src/extensions/forms/hooks/__tests__/useTranslation.test.tsx
+++ b/packages/dnb-eufemia/src/extensions/forms/hooks/__tests__/useTranslation.test.tsx
@@ -10,8 +10,10 @@ import Provider from '../../../../shared/Provider'
import { LOCALE as defaultLocale } from '../../../../shared/defaults'
// Translations
-import nbNO from '../../constants/locales/nb-NO'
-import enGB from '../../constants/locales/en-GB'
+import forms_nbNO from '../../constants/locales/nb-NO'
+import forms_enGB from '../../constants/locales/en-GB'
+import global_nbNO from '../../../../shared/locales/nb-NO'
+import global_enGB from '../../../../shared/locales/en-GB'
describe('Form.useTranslation', () => {
it('should default to nb-NO if no locale is specified in context', () => {
@@ -20,9 +22,13 @@ describe('Form.useTranslation', () => {
})
expect(result.current).toEqual(
- Object.assign(nbNO[defaultLocale], {
- formatMessage: expect.any(Function),
- })
+ Object.assign(
+ forms_nbNO[defaultLocale],
+ global_nbNO[defaultLocale],
+ {
+ formatMessage: expect.any(Function),
+ }
+ )
)
})
@@ -34,7 +40,9 @@ describe('Form.useTranslation', () => {
})
expect(resultGB.current).toEqual(
- Object.assign(enGB['en-GB'], { formatMessage: expect.any(Function) })
+ Object.assign(forms_enGB['en-GB'], global_enGB['en-GB'], {
+ formatMessage: expect.any(Function),
+ })
)
const { result: resultNO } = renderHook(() => useTranslation(), {
@@ -44,7 +52,9 @@ describe('Form.useTranslation', () => {
})
expect(resultNO.current).toEqual(
- Object.assign(nbNO['nb-NO'], { formatMessage: expect.any(Function) })
+ Object.assign(forms_nbNO['nb-NO'], {
+ formatMessage: expect.any(Function),
+ })
)
})
diff --git a/packages/dnb-eufemia/src/extensions/forms/hooks/useTranslation.tsx b/packages/dnb-eufemia/src/extensions/forms/hooks/useTranslation.tsx
index 31480f62b28..5637f3fc62a 100644
--- a/packages/dnb-eufemia/src/extensions/forms/hooks/useTranslation.tsx
+++ b/packages/dnb-eufemia/src/extensions/forms/hooks/useTranslation.tsx
@@ -1,6 +1,7 @@
import { useMemo, useContext } from 'react'
import SharedContext from '../../../shared/Context'
-import { combineTranslations } from '../../../shared/useTranslation'
+import { combineWithExternalTranslations } from '../../../shared/useTranslation'
+import { extendDeep } from '../../../shared/component-helper'
import { DeepPartial } from '../../../shared/types'
import { LOCALE } from '../../../shared/defaults'
import formsLocales from '../constants/locales'
@@ -25,22 +26,20 @@ export default function useTranslation(
| CustomLocales
| Record
) {
- const { locale, translation } = useContext(SharedContext)
+ const { locale, translation: globalTranslation } =
+ useContext(SharedContext)
return useMemo(() => {
- const tr = formsLocales[locale] || formsLocales[LOCALE]
+ const translation = extendDeep(
+ {},
+ formsLocales[locale] || formsLocales[LOCALE],
+ globalTranslation
+ )
- const Forms = translation?.Forms
- if (Forms) {
- for (const key in Forms) {
- tr[key] = Object.assign(tr?.[key] || {}, Forms[key])
- }
- }
-
- return combineTranslations({
- translation: tr,
+ return combineWithExternalTranslations({
+ translation,
messages,
locale,
}) as T
- }, [locale, messages, translation])
+ }, [globalTranslation, locale, messages])
}
diff --git a/packages/dnb-eufemia/src/shared/Context.tsx b/packages/dnb-eufemia/src/shared/Context.tsx
index e324e673c31..48fb37aa60a 100644
--- a/packages/dnb-eufemia/src/shared/Context.tsx
+++ b/packages/dnb-eufemia/src/shared/Context.tsx
@@ -3,10 +3,11 @@
*
*/
-import React from 'react'
+import { createContext } from 'react'
import { LOCALE, CURRENCY, CURRENCY_DISPLAY } from './defaults'
import defaultLocales from './locales'
import { extendDeep } from './component-helper'
+import pointer from 'json-pointer'
// All TypeScript based Eufemia elements
import type { ScrollViewProps } from '../fragments/scroll-view/ScrollView'
@@ -200,9 +201,7 @@ export type TranslationLocale = keyof TranslationDefaultLocales
export type TranslationKeys =
keyof TranslationDefaultLocales[TranslationLocale]
export type TranslationValues =
- TranslationDefaultLocales[TranslationLocale] & {
- Forms?: Record
- }
+ TranslationDefaultLocales[TranslationLocale]
export type TranslationCustomLocales = Record<
never,
string | Record
@@ -221,24 +220,35 @@ export type TranslationFlat = Record<
TranslationKeys | ComponentTranslation
>
+export type TranslationFlatToObject = T extends Record
+ ? {
+ // eslint-disable-next-line no-unused-vars
+ [K in keyof T as K extends `${infer First}.${infer Rest}`
+ ? First
+ : // eslint-disable-next-line no-unused-vars
+ K]: K extends `${infer First}.${infer Rest}`
+ ? TranslationFlatToObject>
+ : T[K]
+ }
+ : T
+
export function prepareContext(
props: ContextProps = {}
): Props & ContextProps {
- const translations: Translations =
- props.translations || props.locales
- ? extendDeep(
- { ...defaultLocales },
- props.translations || props.locales
- )
- : defaultLocales
-
- if (props.__context__) {
- Object.assign(props, props.__context__)
+ if (props?.__context__) {
+ props = Object.assign({}, props, props.__context__)
delete props.__context__
}
- const key = handleLocaleFallbacks(props.locale || LOCALE, translations)
- const translation = translations[key] || defaultLocales[LOCALE] || {} // here we could use Object.freeze
+ const translations: Translations =
+ props.translations || props.locales
+ ? extendDeep({}, defaultLocales, props.translations || props.locales)
+ : extendDeep({}, defaultLocales)
+
+ const localeWithFallback = handleLocaleFallbacks(
+ props.locale || LOCALE,
+ props.translations || props.locales
+ )
/**
* The code above adds support for strings, defined like:
@@ -246,56 +256,57 @@ export function prepareContext(
* "Modal.close_title": "Lukk",
* }
*/
- if (translations[key]) {
- translations[key] = destruct(
- translations[key] as TranslationFlat,
- translation
+ for (const locale in translations) {
+ translations[locale] = destructFlatTranslation(
+ translations[locale] as TranslationFlat
)
}
+ const translation =
+ translations[localeWithFallback] || defaultLocales[LOCALE] || {}
+
const context = {
- // We may use that in future
- updateTranslation: (locale, translation) => {
- context.translation = (context.translations || context.locales)[
- locale
- ] = translation
+ ...props,
+ updateTranslation: (locale, newTranslations) => {
+ context.translation = newTranslations[locale]
+ context.translations = newTranslations
+
+ if (context.locales) {
+ context.locales = context.translations
+ }
},
- getTranslation: (props) => {
- if (props) {
- const lang = props.lang || props.locale
+ getTranslation: (localProps) => {
+ if (localProps) {
+ const locale = localProps.lang || localProps.locale
if (
- lang &&
- (context.translations || context.locales)[lang] &&
- lang !== key
+ locale &&
+ (context.translations || context.locales)[locale] &&
+ locale !== localeWithFallback
) {
- return (context.translations || context.locales)[lang]
+ const tr = context.translations || context.locales
+ return tr[locale]
}
}
- return context.translation
+ return context.translation || defaultLocales[LOCALE]
},
- translations,
- locale: null,
- breakpoints: null,
- skeleton: null,
-
- ...props,
-
/**
* Make sure we set this after props, since we update this one!
*/
+ locales: translations, // @deprecated – can be removed in v11
+ translations,
translation,
} as Props & ContextProps
- return context
+ return { ...context }
}
function handleLocaleFallbacks(
locale: InternalLocale | AnyLocale,
- translations: Translations
+ translations: Translations = {}
) {
if (!translations[locale]) {
- if (locale === 'en' || locale.split('-')[0] === 'en') {
+ if (locale === 'en' || String(locale).split('-')[0] === 'en') {
return 'en-GB'
}
}
@@ -303,7 +314,7 @@ function handleLocaleFallbacks(
}
// If no provider is given, we use the default context from here
-const Context = React.createContext(
+const Context = createContext(
prepareContext({
locale: LOCALE,
currency: CURRENCY,
@@ -313,28 +324,20 @@ const Context = React.createContext(
export default Context
-function destruct(
- source: TranslationFlat,
- validKeys: Record
-): TranslationFlat {
+export function destructFlatTranslation(source: TranslationFlat) {
+ let hasFlatTr = false
+ const destructed = {}
+
for (const k in source) {
if (String(k).includes('.')) {
- const list = k.split('.')
-
- if (validKeys[list[0]]) {
- const val = source[k]
- const last = list.length - 1
-
- list.forEach((k, i) => {
- source[k] = i === last ? val : source[k]
-
- // eslint-disable-next-line @typescript-eslint/ban-ts-comment
- // @ts-ignore
- source = source[k] || {}
- })
- }
+ pointer.set(destructed, '/' + k.replace(/\./g, '/'), source[k])
+ hasFlatTr = true
}
}
+ if (hasFlatTr) {
+ return extendDeep({}, source, destructed)
+ }
+
return source
}
diff --git a/packages/dnb-eufemia/src/shared/Provider.tsx b/packages/dnb-eufemia/src/shared/Provider.tsx
index 705dac438ab..fa2230af71a 100644
--- a/packages/dnb-eufemia/src/shared/Provider.tsx
+++ b/packages/dnb-eufemia/src/shared/Provider.tsx
@@ -3,9 +3,12 @@
*
*/
-import React from 'react'
-import Context, { prepareContext } from './Context'
-import type { ContextProps, InternalLocale } from './Context'
+import React, { useCallback, useContext, useMemo, useState } from 'react'
+import Context, {
+ prepareContext,
+ ContextProps,
+ InternalLocale,
+} from './Context'
import { prepareFormElementContext } from './helpers/filterValidProps'
export type ProviderProps = {
@@ -23,44 +26,70 @@ export type ProviderProps = {
export default function Provider(
localProps: ProviderProps & Props
) {
- const { children, ...props } = localProps
+ const { children, props } = useMemo(() => {
+ const { children, ...props } = localProps
+ return { children, props }
+ }, [localProps])
- const context = React.useContext(Context)
- const [localContext, setLocalContext] = React.useState(null)
+ const nestedContext = useContext(Context)
+ const [localContext, setLocalContext] = useState(null)
- let value = mergeContext(context, { ...localContext, ...props })
-
- if (context) {
- value = prepareContext(value)
- context.updateTranslation(value.locale, value.translation)
- }
-
- value.update = updateAll
- value.setLocale = setAllLocale
- value.updateCurrent = updateCurrent
- value.setCurrentLocale = setCurrentLocale
-
- return {children}
-
- function updateCurrent(props: ContextProps) {
+ const updateCurrent = useCallback((props: ContextProps) => {
setLocalContext({ __context__: props })
- }
+ }, [])
- function setCurrentLocale(locale: InternalLocale) {
+ const setCurrentLocale = useCallback((locale: InternalLocale) => {
setLocalContext({ __context__: { locale } })
- }
+ }, [])
+
+ const update = useCallback(
+ (props: ContextProps) => {
+ nestedContext.update?.(props)
+ setLocalContext({ __context__: props })
+ },
+ [nestedContext]
+ )
+
+ const setLocale = useCallback(
+ (locale: InternalLocale) => {
+ update({ locale })
+ },
+ [update]
+ )
+
+ const value = useMemo(() => {
+ const preparedContext = {
+ // Make copy to avoid extending the root context
+ ...prepareContext(
+ mergeContextWithProps(nestedContext, {
+ ...localContext,
+ ...props,
+ })
+ ),
+ }
- function setAllLocale(locale: InternalLocale) {
- updateAll({ locale })
- }
+ preparedContext.update = update
+ preparedContext.setLocale = setLocale
+ preparedContext.updateCurrent = updateCurrent
+ preparedContext.setCurrentLocale = setCurrentLocale
- function updateAll(props: ContextProps) {
- if (typeof context.update === 'function') {
- context.update(props)
- }
+ nestedContext.updateTranslation(
+ preparedContext.locale,
+ preparedContext.translations
+ )
- setLocalContext({ __context__: props })
- }
+ return preparedContext
+ }, [
+ nestedContext,
+ localContext,
+ props,
+ setLocale,
+ setCurrentLocale,
+ update,
+ updateCurrent,
+ ])
+
+ return {children}
}
type MergeContext = {
@@ -70,24 +99,24 @@ type MergeContextProps = {
value: ProviderProps
} & MergeContext
-function mergeContext(
+function mergeContextWithProps(
context: ContextT & ContextProps,
- props: PropsT & MergeContextProps
+ providerProps: PropsT & MergeContextProps
) {
// When value is given as so:
- const { value, ...rest } = props
+ const { value, ...rest } = providerProps
// Make sure we create a copy, because we add some custom methods to it
- const merge = { ...value, ...rest }
+ const props = { ...value, ...rest }
// Merge our new values with an existing context
- const mergedContext = { ...context, ...merge }
+ const mergedContext = { ...context, ...props }
// Because we don't want to deep merge, we merge formElement additionally
- if (context?.formElement && merge.formElement) {
+ if (context?.formElement && props.formElement) {
mergedContext.formElement = {
...context.formElement,
- ...merge.formElement,
+ ...props.formElement,
}
mergedContext.formElement = prepareFormElementContext(
mergedContext.formElement
@@ -95,10 +124,10 @@ function mergeContext(
}
// Deprecated – can be removed in v11
- if (context?.FormRow && merge.FormRow) {
+ if (context?.FormRow && props.FormRow) {
mergedContext.FormRow = {
...context.FormRow,
- ...merge.FormRow,
+ ...props.FormRow,
}
mergedContext.FormRow = prepareFormElementContext(
mergedContext.FormRow
diff --git a/packages/dnb-eufemia/src/shared/Translation.tsx b/packages/dnb-eufemia/src/shared/Translation.tsx
index 6331cee79c9..2fb52563eb2 100644
--- a/packages/dnb-eufemia/src/shared/Translation.tsx
+++ b/packages/dnb-eufemia/src/shared/Translation.tsx
@@ -1,14 +1,17 @@
-import React, { useContext } from 'react'
+import { useContext } from 'react'
import {
TranslationArguments,
TranslationId,
TranslationIdAsFunction,
formatMessage,
} from './useTranslation'
-import Context, { TranslationCustomLocales } from './Context'
+import SharedContext, {
+ TranslationCustomLocales,
+ TranslationFlatToObject,
+} from './Context'
export type TranslationProps = {
- id?: TranslationId | TranslationIdAsFunction
+ id?: TranslationId | TranslationIdAsFunction>
children?: TranslationId
} & TranslationArguments
@@ -17,10 +20,12 @@ export default function Translation({
children,
...params
}: TranslationProps) {
- const { translation } = useContext(Context)
- return formatMessage(id || children, params, translation)
-}
+ const { translation } = useContext(SharedContext)
+ const result = formatMessage(id || children, params, translation)
+
+ if (typeof result !== 'string') {
+ return String(id)
+ }
-export function getTranslation(id: TranslationId, params) {
- return
+ return result
}
diff --git a/packages/dnb-eufemia/src/shared/__tests__/Provider.test.tsx b/packages/dnb-eufemia/src/shared/__tests__/Provider.test.tsx
index 027d428ce3e..96c08a2fb66 100644
--- a/packages/dnb-eufemia/src/shared/__tests__/Provider.test.tsx
+++ b/packages/dnb-eufemia/src/shared/__tests__/Provider.test.tsx
@@ -16,467 +16,469 @@ import Provider, { ProviderProps } from '../Provider'
import { fireEvent, render } from '@testing-library/react'
describe('Provider', () => {
- const title_nb = 'Tekst'
- const title_gb = 'Text'
-
- const nbNO: TranslationFlat = {
- 'HelpButton.title': title_nb,
- }
- const enGB: TranslationFlat = {
- 'HelpButton.title': title_gb,
- }
-
- const defaultTranslations: Translations = {
- 'nb-NO': nbNO,
- 'en-GB': enGB,
- }
-
- const LocalProvider = (props: ProviderProps) => {
- return
- }
-
- const ChangeLocale = () => {
- const { setLocale, locale } = React.useContext(Context)
-
- expect(typeof setLocale).toBe('function')
-
- return (
- {
- setLocale(value)
- }}
- >
-
- nb-NO
-
-
- en-GB
-
-
- en-US
-
-
- )
- }
-
- const MagicProvider = ({
- children = null,
- ...props
- }: Partial) => {
- return (
-
-
- {(context) => {
- const title = context.translation.HelpButton.title
- return (
- <>
- {title}
-
- {children}
- >
- )
+ describe('translations', () => {
+ const title_nb = 'Tekst'
+ const title_gb = 'Text'
+
+ const nbNO: TranslationFlat = {
+ 'HelpButton.title': title_nb,
+ }
+ const enGB: TranslationFlat = {
+ 'HelpButton.title': title_gb,
+ }
+
+ const defaultTranslations: Translations = {
+ 'nb-NO': nbNO,
+ 'en-GB': enGB,
+ }
+
+ const LocalProvider = (props: ProviderProps) => {
+ return
+ }
+
+ const ChangeLocale = () => {
+ const { setLocale, locale } = React.useContext(Context)
+
+ expect(typeof setLocale).toBe('function')
+
+ return (
+ {
+ setLocale(value)
}}
-
-
- )
- }
-
- it('locales should translate component strings', () => {
- const { rerender } = render(
-
- content
-
- )
-
- expect(
- document
- .querySelector('button.dnb-help-button')
- .getAttribute('aria-label')
- ).toBe(title_nb)
- expect(
- document
- .querySelector('button.dnb-help-button')
- .getAttribute('aria-roledescription')
- ).toBe('Hjelp-knapp')
-
- rerender(
-
- content
-
- )
-
- expect(
- document
- .querySelector('button.dnb-help-button')
- .getAttribute('aria-label')
- ).toBe(title_gb)
- expect(
- document
- .querySelector('button.dnb-help-button')
- .getAttribute('aria-roledescription')
- ).toBe('Help button')
- })
+ >
+
+ nb-NO
+
+
+ en-GB
+
+
+ en-US
+
+
+ )
+ }
+
+ const MagicProvider = ({
+ children = null,
+ ...props
+ }: Partial) => {
+ return (
+
+
+ {(context) => {
+ const title = context.translation.HelpButton.title
+ return (
+ <>
+ {title}
+
+ {children}
+ >
+ )
+ }}
+
+
+ )
+ }
- it('locales should react on prop change', () => {
- const { rerender } = render( )
+ it('should translate component strings', () => {
+ const { rerender } = render(
+
+ content
+
+ )
- expect(document.querySelector('p').textContent).toBe(title_nb)
+ expect(
+ document
+ .querySelector('button.dnb-help-button')
+ .getAttribute('aria-label')
+ ).toBe(title_nb)
+ expect(
+ document
+ .querySelector('button.dnb-help-button')
+ .getAttribute('aria-roledescription')
+ ).toBe('Hjelp-knapp')
+
+ rerender(
+
+ content
+
+ )
- rerender( )
+ expect(
+ document
+ .querySelector('button.dnb-help-button')
+ .getAttribute('aria-label')
+ ).toBe(title_gb)
+ expect(
+ document
+ .querySelector('button.dnb-help-button')
+ .getAttribute('aria-roledescription')
+ ).toBe('Help button')
+ })
- expect(document.querySelector('p').textContent).toBe(title_gb)
- })
+ it('should react on prop change', () => {
+ const { rerender } = render( )
- it('locales should react on locale change', () => {
- render( )
+ expect(document.querySelector('p').textContent).toBe(title_nb)
- expect(document.querySelector('p').textContent).toBe(title_nb)
+ rerender( )
- fireEvent.click(document.querySelector('.en-GB button'))
+ expect(document.querySelector('p').textContent).toBe(title_gb)
+ })
- expect(document.querySelector('p').textContent).toBe(title_gb)
+ it('should react on locale change', () => {
+ render( )
- fireEvent.click(document.querySelector('.en-US button'))
+ expect(document.querySelector('p').textContent).toBe(title_nb)
- expect(document.querySelector('p').textContent).toBe(title_gb)
+ fireEvent.click(document.querySelector('.en-GB button'))
- fireEvent.click(document.querySelector('.nb-NO button'))
+ expect(document.querySelector('p').textContent).toBe(title_gb)
- expect(document.querySelector('p').textContent).toBe(title_nb)
- })
+ fireEvent.click(document.querySelector('.en-US button'))
- it('locales should support nested providers', () => {
- render(
-
-
-
- )
-
- const getRootElement = () => document.querySelectorAll('p')[0]
- const getNestedElement = () => document.querySelectorAll('p')[1]
- const switchRootTo = (locale: string) => {
- fireEvent.click(document.querySelectorAll(`.${locale} button`)[0])
- }
- const switchNestedTo = (locale: string) => {
- fireEvent.click(document.querySelectorAll(`.${locale} button`)[1])
- }
+ expect(document.querySelector('p').textContent).toBe(title_gb)
- expect(getRootElement().textContent).toBe(title_nb)
- expect(getNestedElement().textContent).toBe(title_gb)
+ fireEvent.click(document.querySelector('.nb-NO button'))
- switchNestedTo('nb-NO')
+ expect(document.querySelector('p').textContent).toBe(title_nb)
+ })
- expect(getNestedElement().textContent).toBe(title_nb)
- expect(
- document
- .querySelectorAll('.nb-NO button')[1]
- .getAttribute('aria-pressed')
- ).toBe('true')
+ it('should support nested providers handling locales', () => {
+ render(
+
+
+
+ )
- // should not have changed
- expect(getRootElement().textContent).toBe(title_nb)
+ const getRootElement = () => document.querySelectorAll('p')[0]
+ const getNestedElement = () => document.querySelectorAll('p')[1]
+ const switchRootTo = (locale: string) => {
+ fireEvent.click(document.querySelectorAll(`.${locale} button`)[0])
+ }
+ const switchNestedTo = (locale: string) => {
+ fireEvent.click(document.querySelectorAll(`.${locale} button`)[1])
+ }
- switchRootTo('en-GB')
+ expect(getRootElement().textContent).toBe(title_nb)
+ expect(getNestedElement().textContent).toBe(title_gb)
- expect(
- document
- .querySelectorAll('.en-GB button')[0]
- .getAttribute('aria-pressed')
- ).toBe('true')
- expect(getRootElement().textContent).toBe(title_gb)
+ switchNestedTo('nb-NO')
- switchRootTo('en-US')
+ expect(getNestedElement().textContent).toBe(title_nb)
+ expect(
+ document
+ .querySelectorAll('.nb-NO button')[1]
+ .getAttribute('aria-pressed')
+ ).toBe('true')
- expect(
- document
- .querySelectorAll('.en-US button')[0]
- .getAttribute('aria-pressed')
- ).toBe('true')
- expect(getRootElement().textContent).toBe(title_gb)
+ // should not have changed
+ expect(getRootElement().textContent).toBe(title_nb)
- // should not have changed
- expect(getNestedElement().textContent).toBe(title_nb)
- })
+ switchRootTo('en-GB')
- it('locale should be inherited in nested providers', () => {
- const locale = 'nb-NO'
- let receivedLocale = null
+ expect(
+ document
+ .querySelectorAll('.en-GB button')[0]
+ .getAttribute('aria-pressed')
+ ).toBe('true')
+ expect(getRootElement().textContent).toBe(title_gb)
- const Consumer = () => {
- receivedLocale = React.useContext(Context).locale
- return null
- }
+ switchRootTo('en-US')
- render(
-
-
-
-
-
- )
+ expect(
+ document
+ .querySelectorAll('.en-US button')[0]
+ .getAttribute('aria-pressed')
+ ).toBe('true')
+ expect(getRootElement().textContent).toBe(title_gb)
- expect(receivedLocale).toBe(locale)
- expect(document.querySelectorAll('p')[0].textContent).toBe(title_nb)
- })
+ // should not have changed
+ expect(getNestedElement().textContent).toBe(title_nb)
+ })
- it('locale should be changed in root context', () => {
- const Consumer = ({ id }) => {
- const context = React.useContext(Context)
- const { locale, setLocale } = context
+ it('should inherit locale in nested providers', () => {
+ const locale = 'nb-NO'
+ let receivedLocale = null
- const handleOnChange = () => {
- setLocale(locale === 'nb-NO' ? 'en-GB' : 'nb-NO')
+ const Consumer = () => {
+ receivedLocale = React.useContext(Context).locale
+ return null
}
- return (
- <>
- {locale}
-
- change
-
- >
+ render(
+
+
+
+
+
)
- }
- const RootConsumer = () => {
- return
- }
+ expect(receivedLocale).toBe(locale)
+ expect(document.querySelectorAll('p')[0].textContent).toBe(title_nb)
+ })
+
+ it('should change locale in root context', () => {
+ const Consumer = ({ id }) => {
+ const context = React.useContext(Context)
+ const { locale, setLocale } = context
+
+ const handleOnChange = () => {
+ setLocale(locale === 'nb-NO' ? 'en-GB' : 'nb-NO')
+ }
+
+ return (
+ <>
+ {locale}
+
+ change
+
+ >
+ )
+ }
- const NestedConsumer = () => {
- return
- }
+ const RootConsumer = () => {
+ return
+ }
+
+ const NestedConsumer = () => {
+ return
+ }
- render(
-
-
-
-
+ render(
+
+
+
+
+
-
- )
+ )
- const getRootLocale = () =>
- document.getElementById('root-locale').textContent
- const getNestedLocale = () =>
- document.getElementById('nested-locale').textContent
+ const getRootLocale = () =>
+ document.getElementById('root-locale').textContent
+ const getNestedLocale = () =>
+ document.getElementById('nested-locale').textContent
- expect(getRootLocale()).toBe('nb-NO')
- expect(getNestedLocale()).toBe('nb-NO')
+ expect(getRootLocale()).toBe('nb-NO')
+ expect(getNestedLocale()).toBe('nb-NO')
- fireEvent.click(document.getElementById('root-button'))
+ fireEvent.click(document.getElementById('root-button'))
- expect(getRootLocale()).toBe('en-GB')
- expect(getNestedLocale()).toBe('en-GB')
+ expect(getRootLocale()).toBe('en-GB')
+ expect(getNestedLocale()).toBe('en-GB')
- fireEvent.click(document.getElementById('nested-button'))
+ fireEvent.click(document.getElementById('nested-button'))
- expect(getRootLocale()).toBe('nb-NO')
- expect(getNestedLocale()).toBe('nb-NO')
- })
+ expect(getRootLocale()).toBe('nb-NO')
+ expect(getNestedLocale()).toBe('nb-NO')
+ })
- it('locale should be changed in local context', () => {
- const Consumer = ({ id }) => {
- const context = React.useContext(Context)
- const { locale, setCurrentLocale } = context
+ it('should change locale in local context', () => {
+ const Consumer = ({ id }) => {
+ const context = React.useContext(Context)
+ const { locale, setCurrentLocale } = context
- const handleOnChange = () => {
- setCurrentLocale(locale === 'nb-NO' ? 'en-GB' : 'nb-NO')
- }
+ const handleOnChange = () => {
+ setCurrentLocale(locale === 'nb-NO' ? 'en-GB' : 'nb-NO')
+ }
- return (
- <>
- {locale}
-
- change
-
- >
- )
- }
+ return (
+ <>
+ {locale}
+
+ change
+
+ >
+ )
+ }
- const RootConsumer = () => {
- return
- }
+ const RootConsumer = () => {
+ return
+ }
- const NestedConsumer = () => {
- return
- }
+ const NestedConsumer = () => {
+ return
+ }
- render(
-
-
-
-
+ render(
+
+
+
+
+
-
- )
+ )
- const getRootLocale = () =>
- document.getElementById('root-locale').textContent
- const getNestedLocale = () =>
- document.getElementById('nested-locale').textContent
+ const getRootLocale = () =>
+ document.getElementById('root-locale').textContent
+ const getNestedLocale = () =>
+ document.getElementById('nested-locale').textContent
- expect(getRootLocale()).toBe('nb-NO')
- expect(getNestedLocale()).toBe('nb-NO')
+ expect(getRootLocale()).toBe('nb-NO')
+ expect(getNestedLocale()).toBe('nb-NO')
- fireEvent.click(document.getElementById('nested-button'))
+ fireEvent.click(document.getElementById('nested-button'))
- expect(getRootLocale()).toBe('nb-NO')
- expect(getNestedLocale()).toBe('en-GB')
+ expect(getRootLocale()).toBe('nb-NO')
+ expect(getNestedLocale()).toBe('en-GB')
- fireEvent.click(document.getElementById('nested-button'))
+ fireEvent.click(document.getElementById('nested-button'))
- expect(getRootLocale()).toBe('nb-NO')
- expect(getNestedLocale()).toBe('nb-NO')
+ expect(getRootLocale()).toBe('nb-NO')
+ expect(getNestedLocale()).toBe('nb-NO')
- fireEvent.click(document.getElementById('root-button'))
+ fireEvent.click(document.getElementById('root-button'))
- expect(getRootLocale()).toBe('en-GB')
- expect(getNestedLocale()).toBe('nb-NO')
- })
+ expect(getRootLocale()).toBe('en-GB')
+ expect(getNestedLocale()).toBe('nb-NO')
+ })
- it('will support "value" prop in nested contexts', () => {
- type ConsumerContext = {
- myProperty: string
- }
+ it('will support "value" prop in nested contexts', () => {
+ type ConsumerContext = {
+ myProperty: string
+ }
- const Consumer = ({ id }) => {
- const context = React.useContext<
- ContextProps & Partial
- >(Context)
+ const Consumer = ({ id }) => {
+ const context = React.useContext<
+ ContextProps & Partial
+ >(Context)
- return {context.myProperty}
- }
+ return {context.myProperty}
+ }
- const RootConsumer = () => {
- return
- }
+ const RootConsumer = () => {
+ return
+ }
- const NestedConsumer = () => {
- return
- }
+ const NestedConsumer = () => {
+ return
+ }
- const { rerender } = render(
-
-
-
-
+ const { rerender } = render(
+
+
+
+
+
-
- )
+ )
- const getRootLocale = () =>
- document.getElementById('root-locale').textContent
- const getNestedLocale = () =>
- document.getElementById('nested-locale').textContent
+ const getRootLocale = () =>
+ document.getElementById('root-locale').textContent
+ const getNestedLocale = () =>
+ document.getElementById('nested-locale').textContent
- expect(getRootLocale()).toBe('bar')
- expect(getNestedLocale()).toBe('bar')
+ expect(getRootLocale()).toBe('bar')
+ expect(getNestedLocale()).toBe('bar')
- const value: ConsumerContext = { myProperty: 'changed' }
+ const value: ConsumerContext = { myProperty: 'changed' }
- rerender(
-
-
-
-
+ rerender(
+
+
+
+
+
-
- )
+ )
- expect(getRootLocale()).toBe('changed')
- expect(getNestedLocale()).toBe('changed')
- })
+ expect(getRootLocale()).toBe('changed')
+ expect(getNestedLocale()).toBe('changed')
+ })
- it('should support nested providers and update the root context', () => {
- render(
-
-
-
- )
-
- const getRootElement = () => document.querySelectorAll('p')[0]
- const getNestedElement = () => document.querySelectorAll('p')[1]
- const switchRootTo = (locale: string) => {
- fireEvent.click(document.querySelectorAll(`.${locale} button`)[0])
- }
- const switchNestedTo = (locale: string) => {
- fireEvent.click(document.querySelectorAll(`.${locale} button`)[1])
- }
+ it('should support nested providers and update the root context', () => {
+ render(
+
+
+
+ )
+
+ const getRootElement = () => document.querySelectorAll('p')[0]
+ const getNestedElement = () => document.querySelectorAll('p')[1]
+ const switchRootTo = (locale: string) => {
+ fireEvent.click(document.querySelectorAll(`.${locale} button`)[0])
+ }
+ const switchNestedTo = (locale: string) => {
+ fireEvent.click(document.querySelectorAll(`.${locale} button`)[1])
+ }
- expect(getRootElement().textContent).toBe(title_gb)
- expect(
- document
- .querySelectorAll('.en-GB button')[0]
- .getAttribute('aria-pressed')
- ).toBe('true')
- expect(getNestedElement().textContent).toBe(title_nb)
- expect(
- document
- .querySelectorAll('.nb-NO button')[1]
- .getAttribute('aria-pressed')
- ).toBe('true')
-
- // First, let's change the inner
- switchNestedTo('nb-NO')
-
- expect(getRootElement().textContent).toBe(title_gb)
- expect(
- document
- .querySelectorAll('.en-GB button')[0]
- .getAttribute('aria-pressed')
- ).toBe('true')
- expect(getNestedElement().textContent).toBe(title_nb)
- expect(
- document
- .querySelectorAll('.nb-NO button')[1]
- .getAttribute('aria-pressed')
- ).toBe('true')
-
- switchNestedTo('en-GB')
-
- expect(getRootElement().textContent).toBe(title_gb)
- expect(
- document
- .querySelectorAll('.en-GB button')[0]
- .getAttribute('aria-pressed')
- ).toBe('true')
- expect(getNestedElement().textContent).toBe(title_gb)
- expect(
- document
- .querySelectorAll('.en-GB button')[1]
- .getAttribute('aria-pressed')
- ).toBe('true')
-
- switchNestedTo('nb-NO')
-
- expect(getRootElement().textContent).toBe(title_nb)
- expect(
- document
- .querySelectorAll('.nb-NO button')[0]
- .getAttribute('aria-pressed')
- ).toBe('true')
- expect(getNestedElement().textContent).toBe(title_nb)
- expect(
- document
- .querySelectorAll('.nb-NO button')[1]
- .getAttribute('aria-pressed')
- ).toBe('true')
-
- // Now, let's change the outer
- switchRootTo('en-GB')
-
- expect(getRootElement().textContent).toBe(title_gb)
- expect(
- document
- .querySelectorAll('.en-GB button')[0]
- .getAttribute('aria-pressed')
- ).toBe('true')
- expect(getNestedElement().textContent).toBe(title_nb)
- expect(
- document
- .querySelectorAll('.nb-NO button')[1]
- .getAttribute('aria-pressed')
- ).toBe('true')
+ expect(getRootElement().textContent).toBe(title_gb)
+ expect(
+ document
+ .querySelectorAll('.en-GB button')[0]
+ .getAttribute('aria-pressed')
+ ).toBe('true')
+ expect(getNestedElement().textContent).toBe(title_nb)
+ expect(
+ document
+ .querySelectorAll('.nb-NO button')[1]
+ .getAttribute('aria-pressed')
+ ).toBe('true')
+
+ // First, let's change the inner
+ switchNestedTo('nb-NO')
+
+ expect(getRootElement().textContent).toBe(title_gb)
+ expect(
+ document
+ .querySelectorAll('.en-GB button')[0]
+ .getAttribute('aria-pressed')
+ ).toBe('true')
+ expect(getNestedElement().textContent).toBe(title_nb)
+ expect(
+ document
+ .querySelectorAll('.nb-NO button')[1]
+ .getAttribute('aria-pressed')
+ ).toBe('true')
+
+ switchNestedTo('en-GB')
+
+ expect(getRootElement().textContent).toBe(title_gb)
+ expect(
+ document
+ .querySelectorAll('.en-GB button')[0]
+ .getAttribute('aria-pressed')
+ ).toBe('true')
+ expect(getNestedElement().textContent).toBe(title_gb)
+ expect(
+ document
+ .querySelectorAll('.en-GB button')[1]
+ .getAttribute('aria-pressed')
+ ).toBe('true')
+
+ switchNestedTo('nb-NO')
+
+ expect(getRootElement().textContent).toBe(title_nb)
+ expect(
+ document
+ .querySelectorAll('.nb-NO button')[0]
+ .getAttribute('aria-pressed')
+ ).toBe('true')
+ expect(getNestedElement().textContent).toBe(title_nb)
+ expect(
+ document
+ .querySelectorAll('.nb-NO button')[1]
+ .getAttribute('aria-pressed')
+ ).toBe('true')
+
+ // Now, let's change the outer
+ switchRootTo('en-GB')
+
+ expect(getRootElement().textContent).toBe(title_gb)
+ expect(
+ document
+ .querySelectorAll('.en-GB button')[0]
+ .getAttribute('aria-pressed')
+ ).toBe('true')
+ expect(getNestedElement().textContent).toBe(title_nb)
+ expect(
+ document
+ .querySelectorAll('.nb-NO button')[1]
+ .getAttribute('aria-pressed')
+ ).toBe('true')
+ })
})
})
diff --git a/packages/dnb-eufemia/src/shared/__tests__/Translation.test.tsx b/packages/dnb-eufemia/src/shared/__tests__/Translation.test.tsx
index 6cc8af265fa..daab302659c 100644
--- a/packages/dnb-eufemia/src/shared/__tests__/Translation.test.tsx
+++ b/packages/dnb-eufemia/src/shared/__tests__/Translation.test.tsx
@@ -1,9 +1,6 @@
import React from 'react'
import { render } from '@testing-library/react'
-import Translation, {
- TranslationProps,
- getTranslation,
-} from '../Translation'
+import Translation, { TranslationProps } from '../Translation'
import Context from '../Context'
import Provider from '../Provider'
@@ -24,76 +21,61 @@ describe('flatten translations', () => {
'Modal.close_title': 'Close',
'other.string': given_enGB,
}
-
- const defaultLocales = {
+ const translations = {
'nb-NO': nbNO,
'en-GB': enGB,
}
- it('"getTranslation" should return requested string inside render', () => {
- render(
-
-
- {getTranslation('other.string', {
- foo: 'foo',
- bar: 'bar',
- max: 'max',
- })}
-
-
- )
-
- expect(document.querySelector('span.getTranslation').textContent).toBe(
- expected_nbNO
- )
- })
-
it('should return requested string inside render', () => {
render(
-
-
+
+
-
+
-
+
other.string
-
+
)
- expect(document.querySelector('span.Translation').textContent).toBe(
+ expect(document.querySelector('.Translation').textContent).toBe(
expected_nbNO
)
expect(
- document.querySelector('span.TranslationIdAsChildren').textContent
+ document.querySelector('.TranslationIdAsChildren').textContent
).toBe(expected_nbNO)
})
-})
-describe('Context.getTranslation', () => {
- nbNO['nb-NO'].HelpButton['other.string'] = given_nbNO
- enGB['en-GB'].HelpButton['other.string'] = given_enGB
+ it('should return given id if nothing found', () => {
+ const id = 'invalid.id'
+ render(
+
+
+
+
+
+ )
+ expect(document.querySelector('.invalid').textContent).toBe(id)
+ })
+})
+
+describe('context.getTranslation', () => {
const MagicContext = (props) => {
return (
{(context) => {
- // We may use that in future
- // if (props.translation) {
- // context.setTranslation(props.translation)
- // }
const title = context.getTranslation(props).HelpButton.title
const otherString =
- context.getTranslation(props).HelpButton['other.string']
+ context.getTranslation(props).HelpButton?.['other']?.string
return (
<>
{context.locale}
{title}
- {otherString && (
- {otherString}
- )}
+ {otherString}
>
)
}}
@@ -122,14 +104,35 @@ describe('Context.getTranslation', () => {
expect(document.querySelector('p.locale').textContent).toBe('nb-NO')
})
- it('should react on new lang prop and prepare other.string', () => {
- const { rerender } = render( )
+ it('should react on new lang prop and return other.string', () => {
+ const nbNO = {
+ 'Modal.close_title': 'Steng',
+ 'HelpButton.other.string': given_nbNO,
+ }
+ const enGB = {
+ 'Modal.close_title': 'Close',
+ 'HelpButton.other.string': given_enGB,
+ }
+ const translations = {
+ 'nb-NO': nbNO,
+ 'en-GB': enGB,
+ }
+
+ const { rerender } = render(
+
+
+
+ )
expect(document.querySelector('p.other-string').textContent).toBe(
given_nbNO
)
- rerender( )
+ rerender(
+
+
+
+ )
expect(document.querySelector('p.other-string').textContent).toBe(
given_enGB
diff --git a/packages/dnb-eufemia/src/shared/__tests__/useTranslation.test.tsx b/packages/dnb-eufemia/src/shared/__tests__/useTranslation.test.tsx
index 166f3e545e1..0681d552e9b 100644
--- a/packages/dnb-eufemia/src/shared/__tests__/useTranslation.test.tsx
+++ b/packages/dnb-eufemia/src/shared/__tests__/useTranslation.test.tsx
@@ -17,7 +17,7 @@ describe('useTranslation without an ID', () => {
})
expect(result.current).toEqual(
- Object.assign(nbNO[defaultLocale], {
+ Object.assign({}, nbNO[defaultLocale], {
formatMessage: expect.any(Function),
})
)
@@ -31,7 +31,9 @@ describe('useTranslation without an ID', () => {
})
expect(resultGB.current).toEqual(
- Object.assign(enGB['en-GB'], { formatMessage: expect.any(Function) })
+ Object.assign({}, enGB['en-GB'], {
+ formatMessage: expect.any(Function),
+ })
)
const { result: resultNO } = renderHook(() => useTranslation(), {
@@ -41,7 +43,9 @@ describe('useTranslation without an ID', () => {
})
expect(resultNO.current).toEqual(
- Object.assign(nbNO['nb-NO'], { formatMessage: expect.any(Function) })
+ Object.assign({}, nbNO['nb-NO'], {
+ formatMessage: expect.any(Function),
+ })
)
})
@@ -214,7 +218,7 @@ describe('useTranslation with an ID', () => {
'en-GB': enGB_nested,
}
- const RenderGetTranslation = () => {
+ const RenderUseTranslation = () => {
return useTranslation('other.string', {
foo: 'foo',
bar: 'bar',
@@ -251,7 +255,7 @@ describe('useTranslation with an ID', () => {
render(
-
+
)
@@ -265,7 +269,7 @@ describe('useTranslation with an ID', () => {
render(
-
+
@@ -291,7 +295,7 @@ describe('useTranslation with an ID', () => {
-
+
diff --git a/packages/dnb-eufemia/src/shared/index.tsx b/packages/dnb-eufemia/src/shared/index.tsx
index cd062e87a4d..e1256023a93 100644
--- a/packages/dnb-eufemia/src/shared/index.tsx
+++ b/packages/dnb-eufemia/src/shared/index.tsx
@@ -13,5 +13,6 @@ export { default as useMediaQuery } from './useMediaQuery'
export { default as useMedia } from './useMedia'
export { default as useTranslation } from './useTranslation'
export { default as Translation } from './Translation'
+export * from './Translation'
export type { ThemeNames } from './Theme'
diff --git a/packages/dnb-eufemia/src/shared/stories/Translation.stories.tsx b/packages/dnb-eufemia/src/shared/stories/Translation.stories.tsx
index fe6bd0242e6..4dab2926688 100644
--- a/packages/dnb-eufemia/src/shared/stories/Translation.stories.tsx
+++ b/packages/dnb-eufemia/src/shared/stories/Translation.stories.tsx
@@ -8,7 +8,7 @@ import { Wrapper, Box } from 'storybook-utils/helpers'
import { Dialog, Dropdown } from '../../components'
import { Provider, Context, useTranslation } from '../'
-import Translation, { getTranslation } from '../Translation'
+import Translation from '../Translation'
import { P } from '../..'
// import nbNO from '../locales/nb-NO'
@@ -86,12 +86,6 @@ export const TranslationSandbox = () => (
'en-GB': enGB,
}}
>
- {getTranslation('other.string', {
- foo: 'foo',
- bar: 'riskScore',
- max: 'max',
- })}
-
(
return useMemo(() => {
const id = typeof messages === 'string' ? messages : undefined
if (id) {
- return formatMessage(String(id), args, translation)
+ return formatMessage(id, args, translation)
}
- return combineTranslations({
+ return combineWithExternalTranslations({
translation,
messages,
locale,
@@ -38,22 +38,24 @@ export default function useTranslation(
}, [locale, messages, args, translation])
}
-export type CombineTranslationsArgs = {
+export type combineWithExternalTranslationsArgs = {
translation: Translation
messages?: TranslationCustomLocales
locale?: InternalLocale
}
-export type CombineTranslationsReturn = Translation &
+export type combineWithExternalTranslationsReturn = Translation &
TranslationCustomLocales & {
formatMessage: typeof formatMessage
}
-export function combineTranslations({
+export function combineWithExternalTranslations({
translation,
messages,
locale,
-}: CombineTranslationsArgs): CombineTranslationsReturn {
- let combined = { ...translation } as CombineTranslationsReturn
+}: combineWithExternalTranslationsArgs): combineWithExternalTranslationsReturn {
+ let combined = {
+ ...translation,
+ } as combineWithExternalTranslationsReturn
if (messages) {
if (Object.keys(defaultLocales).some((locale) => messages[locale])) {
@@ -71,7 +73,7 @@ export function combineTranslations({
id: TranslationId,
args: TranslationArguments
) => {
- return formatMessage(id, args, translation)
+ return formatMessage(id, args, combined)
}
return combined