diff --git a/package-lock.json b/package-lock.json index db75fd52..749a193d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,12 +17,14 @@ "classnames": "^2.3.2", "clsx": "^2.1.1", "date-fns": "^4.1.0", + "i18next": "^25.6.0", "libphonenumber-js": "^1.12.9", "lucide-react": "^0.475.0", "marked": "^15.0.6", "radix-ui": "^1.1.2", "react": "^18.3.0", "react-dom": "^18.3.0", + "react-i18next": "^16.2.1", "react-phone-number-input": "^3.4.12", "react-transition-group": "4.4.5", "validator": "^13.11.0" @@ -2031,13 +2033,9 @@ "license": "MIT" }, "node_modules/@babel/runtime": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.7.tgz", - "integrity": "sha512-UwgBRMjJP+xv857DCngvqXI3Iq6J4v0wXmwc6sapg+zyhbwmQX67LUEFrkK5tbyJ30jGuG3ZvWpBiB9LCy1kWw==", - "license": "MIT", - "dependencies": { - "regenerator-runtime": "^0.14.0" - }, + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", + "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", "engines": { "node": ">=6.9.0" } @@ -10335,6 +10333,14 @@ "dev": true, "license": "MIT" }, + "node_modules/html-parse-stringify": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz", + "integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==", + "dependencies": { + "void-elements": "3.1.0" + } + }, "node_modules/htmlparser2": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz", @@ -10404,6 +10410,36 @@ "node": ">=10.17.0" } }, + "node_modules/i18next": { + "version": "25.6.0", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-25.6.0.tgz", + "integrity": "sha512-tTn8fLrwBYtnclpL5aPXK/tAYBLWVvoHM1zdfXoRNLcI+RvtMsoZRV98ePlaW3khHYKuNh/Q65W/+NVFUeIwVw==", + "funding": [ + { + "type": "individual", + "url": "https://locize.com" + }, + { + "type": "individual", + "url": "https://locize.com/i18next.html" + }, + { + "type": "individual", + "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" + } + ], + "dependencies": { + "@babel/runtime": "^7.27.6" + }, + "peerDependencies": { + "typescript": "^5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/iconv-lite": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", @@ -15698,6 +15734,32 @@ "react": "^18.3.1" } }, + "node_modules/react-i18next": { + "version": "16.2.1", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-16.2.1.tgz", + "integrity": "sha512-z7TVwd8q4AjFo2n7oOwzNusY7xVL4uHykwX1zZRvasUQnmnXlp7Z1FZqXvhK/6hQaCvWTZmZW1bMaUWKowtvVw==", + "dependencies": { + "@babel/runtime": "^7.27.6", + "html-parse-stringify": "^3.0.1", + "use-sync-external-store": "^1.6.0" + }, + "peerDependencies": { + "i18next": ">= 25.5.2", + "react": ">= 16.8.0", + "typescript": "^5" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, "node_modules/react-is": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", @@ -15920,12 +15982,6 @@ "node": ">=4" } }, - "node_modules/regenerator-runtime": { - "version": "0.14.1", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", - "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", - "license": "MIT" - }, "node_modules/regenerator-transform": { "version": "0.15.2", "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.2.tgz", @@ -17768,7 +17824,7 @@ "version": "5.4.5", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -18203,6 +18259,14 @@ } } }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -18241,6 +18305,14 @@ "node": ">= 0.10" } }, + "node_modules/void-elements": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", + "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/w3c-xmlserializer": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz", diff --git a/package.json b/package.json index a7396969..3694b9a0 100644 --- a/package.json +++ b/package.json @@ -41,12 +41,14 @@ "classnames": "^2.3.2", "clsx": "^2.1.1", "date-fns": "^4.1.0", + "i18next": "^25.6.0", "libphonenumber-js": "^1.12.9", "lucide-react": "^0.475.0", "marked": "^15.0.6", "radix-ui": "^1.1.2", "react": "^18.3.0", "react-dom": "^18.3.0", + "react-i18next": "^16.2.1", "react-phone-number-input": "^3.4.12", "react-transition-group": "4.4.5", "validator": "^13.11.0" diff --git a/src/client.tsx b/src/client.tsx index d5f8d945..b075b56a 100644 --- a/src/client.tsx +++ b/src/client.tsx @@ -4,7 +4,7 @@ import { createRoot } from 'react-dom/client'; import { Config, Prettify } from './types'; -import { I18nMessages } from './core/i18n'; +import { type I18nMessages } from './contexts/i18n'; import { UserError } from './helpers/errors'; import { logError } from './helpers/logger'; diff --git a/src/components/form/fieldCreator.tsx b/src/components/form/fieldCreator.tsx index e18b8a60..874858ac 100644 --- a/src/components/form/fieldCreator.tsx +++ b/src/components/form/fieldCreator.tsx @@ -1,7 +1,7 @@ import React, { type ComponentType } from 'react'; +import { TFunction } from 'i18next'; import type { WithI18n } from '../../contexts/i18n'; -import type { I18nResolver } from '../../core/i18n'; import { DefaultPathMapping, type PathMapping } from '../../core/mapping'; import { empty as emptyRule, @@ -70,7 +70,7 @@ export type FieldComponentProps< rawProperty?: K; required?: boolean; readOnly?: boolean; - i18n: I18nResolver; + i18n: TFunction; showLabel?: boolean; value?: FormValue; validation?: ValidatorResult; @@ -107,7 +107,7 @@ export interface FieldProps< format?: Formatter; rawProperty?: K; component: ComponentType

; - extendedParams?: ExtraParams | ((i18n: I18nResolver) => ExtraParams); + extendedParams?: ExtraParams | ((i18n: TFunction) => ExtraParams); } export function createField< diff --git a/src/components/form/fields/passwordField.tsx b/src/components/form/fields/passwordField.tsx index bf82cc11..9a4382ee 100644 --- a/src/components/form/fields/passwordField.tsx +++ b/src/components/form/fields/passwordField.tsx @@ -11,10 +11,10 @@ import { FormError, FormGroupContainer, Input, Label } from '../formControlsComp import { PasswordPolicyRules, type PasswordRule } from './passwordPolicyRules'; import { useI18n } from '../../../contexts/i18n'; -import { I18nResolver } from '../../../core/i18n'; import { Validator, isValidatorError } from '../../../core/validation'; import { HidePasswordIcon, ShowPasswordIcon } from './simplePasswordField'; +import { TFunction } from 'i18next'; import { isRichFormValue } from '../../../helpers/utils'; import { createField } from '../fieldCreator'; import { FormContext } from '../formComponent'; @@ -72,7 +72,7 @@ const PasswordStrength = ({ score }: PasswordStrength) => { - {i18n('passwordStrength.score' + score)} + {i18n(`passwordStrength.score${score}`)} ); @@ -187,7 +187,7 @@ function PasswordField({ type RuleKeys = Exclude; export function listEnabledRules( - i18n: I18nResolver, + i18n: TFunction, passwordPolicy: Config['passwordPolicy'] ): Record { if (!passwordPolicy) return {} as Record; diff --git a/src/components/form/formComponent.tsx b/src/components/form/formComponent.tsx index 0d675798..70b91d61 100644 --- a/src/components/form/formComponent.tsx +++ b/src/components/form/formComponent.tsx @@ -294,7 +294,7 @@ export function createForm = {}, P = return i18n(err); } else if (isAppError(err)) { return err.errorMessageKey - ? i18n(err.errorMessageKey, {}, () => err.errorUserMsg ?? err.error) + ? i18n(err.errorMessageKey, { defaultValue: err.errorUserMsg ?? err.error }) : err.errorUserMsg; } }; diff --git a/src/components/form/socialButtonsComponent.tsx b/src/components/form/socialButtonsComponent.tsx index 7f49a091..91a0e91e 100644 --- a/src/components/form/socialButtonsComponent.tsx +++ b/src/components/form/socialButtonsComponent.tsx @@ -58,7 +58,7 @@ const SocialBtn = styled(Button).attrs(({ $provider, ...props }) => { $background: $provider.btnBackgroundColor ?? $provider.color, $border: $provider.btnBorderColor ?? $provider.color, className: classes(['r5-btn-social', `r5-btn-social-${$provider.key}`]), - title: i18n(`socialButton.${$provider.key}.title`, undefined, () => $provider.name), + title: i18n(`socialButton.${$provider.key}.title`, { defaultValue: $provider.name }), ...props, }; })` @@ -118,7 +118,7 @@ const SocialButton = ({ provider, onClick, count }: SocialButtonProps) => { {textVisible && ( - {i18n(`socialButton.${provider.key}.title`, undefined, () => provider.name)} + {i18n(`socialButton.${provider.key}.title`, { defaultValue: provider.name })} )} diff --git a/src/components/widget/widget.tsx b/src/components/widget/widget.tsx index f62e1956..b6394b8d 100644 --- a/src/components/widget/widget.tsx +++ b/src/components/widget/widget.tsx @@ -5,7 +5,7 @@ import styled, { StyleSheetManager, ThemeProvider, css } from 'styled-components import type { Client as CoreClient, SessionInfo } from '@reachfive/identity-core'; import { ConfigProvider } from '../../contexts/config'; -import { I18nProvider } from '../../contexts/i18n'; +import { I18nProvider, type I18nMessages } from '../../contexts/i18n'; import { ReachfiveProvider } from '../../contexts/reachfive'; import { RoutingProvider } from '../../contexts/routing'; import { SessionProvider } from '../../contexts/session'; @@ -13,10 +13,9 @@ import { buildTheme } from '../../core/theme'; import { Theme, ThemeOptions } from '../../types/styled'; import WidgetContainer, { WidgetContainerProps } from './widgetContainerComponent'; -import type { I18nMessages, I18nNestedMessages } from '../../core/i18n'; import type { Config, Prettify } from '../../types'; -export type I18nProps = { i18n?: I18nNestedMessages }; +export type I18nProps = { i18n?: I18nMessages }; export type ThemeProps = { theme?: ThemeOptions }; export type PropsWithI18n

= Prettify

; @@ -87,6 +86,7 @@ export function createWidget({ ; export interface Props { defaultMessages?: I18nMessages; - messages?: I18nNestedMessages; + messages?: I18nMessages; + locale: string; } -export const I18nContext = React.createContext(undefined); +export type WithI18n = T & { i18n: TFunction }; -export function useI18n(): I18nResolver { - const context = React.useContext(I18nContext); - if (!context) { - throw new Error('No I18nContext provided'); - } - - return context; -} - -export interface I18nProps { - i18n: I18nResolver; -} - -export type WithI18n

= P & I18nProps; - -export function withI18n(WrappedComponent: ComponentType) { - const displayName = WrappedComponent.displayName ?? WrappedComponent.name ?? 'Component'; - - const ComponentWithI18n = (props: Omit) => { - const i18n = useI18n(); - return ; - }; - - ComponentWithI18n.displayName = `withI18n(${displayName})`; - - return ComponentWithI18n; +export function useI18n(): TFunction { + const { t } = useTranslation(); + return t; } export function I18nProvider({ children, - defaultMessages, - messages, + defaultMessages = {}, + messages = {}, + locale, }: PropsWithChildren): JSX.Element | null { - const resolver = useMemo( - () => resolveI18n(defaultMessages, messages), - [defaultMessages, messages] - ); - return {children}; + i18n.use(initReactI18next).init({ + lng: locale, + interpolation: { + escapeValue: false, // react already safes from xss, + prefix: '{', + suffix: '}', + }, + fallbackLng: ['default', 'dev'], + resources: { + default: { + translation: defaultMessages, + }, + ...(locale in messages + ? Object.fromEntries( + Object.entries(messages).map(([language, translation]) => [ + language, + { translation }, + ]) + ) + : { [locale]: { translation: messages } }), + }, + }); + + return {children}; } diff --git a/src/core/i18n.ts b/src/core/i18n.ts deleted file mode 100644 index 2fe9e16d..00000000 --- a/src/core/i18n.ts +++ /dev/null @@ -1,53 +0,0 @@ -export type I18nMessages = Record; - -export type I18nNestedMessages = Record; - -export type I18nMessageParams = Record; - -export type I18nResolver = ( - key: string, - params?: I18nMessageParams, - fallback?: (params?: I18nMessageParams) => string -) => string; - -export function resolveI18n | string>( - defaultMessages: I18nMessages = {}, - messages: I18nNestedMessages = {} -): I18nResolver { - const mergedMessages: I18nMessages = { - ...defaultMessages, - ...flattenObject(messages), - }; - - return ( - key: string, - params?: I18nMessageParams, - fallback?: (params?: I18nMessageParams) => string - ) => { - const template = mergedMessages[key] ?? fallback?.(params) ?? key; - - return params - ? Object.keys(params).reduce( - (acc, param) => acc.replace(`{${param}}`, params[param] as string), - template - ) - : template; - }; -} - -export default resolveI18n; - -function flattenObject( - object: I18nNestedMessages | string, - prefix: string[] = [] -): Record { - return typeof object === 'object' && object !== null - ? Object.keys(object).reduce( - (acc, key) => ({ - ...acc, - ...flattenObject(object[key], [...prefix, key]), - }), - {} - ) - : { [prefix.join('.')]: object }; -} diff --git a/src/core/validation.ts b/src/core/validation.ts index 9ef4a4b2..a48dbcaf 100644 --- a/src/core/validation.ts +++ b/src/core/validation.ts @@ -2,8 +2,8 @@ import isEmail from 'validator/lib/isEmail'; import isFloat from 'validator/lib/isFloat'; import isInt from 'validator/lib/isInt'; +import { TFunction } from 'i18next'; import { isValued } from '../helpers/utils'; -import { I18nResolver } from './i18n'; export class CompoundValidator { current: Validator | CompoundValidator; @@ -17,7 +17,7 @@ export class CompoundValidator { this.next = next; } - create(i18n: I18nResolver): ValidatorInstance { + create(i18n: TFunction): ValidatorInstance { const current = this.current.create(i18n); const next = this.next.create(i18n); @@ -77,7 +77,7 @@ export class Validator { this.parameters = parameters; } - create(i18n: I18nResolver): ValidatorInstance { + create(i18n: TFunction): ValidatorInstance { const errorMessage = (value: T) => i18n(`validation.${this.hint(value)}`, this.parameters); return async (value: T, ctx: C) => { const res = this.rule(value, ctx); diff --git a/tests/components/form/WidgetContext.tsx b/tests/components/form/WidgetContext.tsx index dfca1b48..126d8895 100644 --- a/tests/components/form/WidgetContext.tsx +++ b/tests/components/form/WidgetContext.tsx @@ -7,9 +7,8 @@ import React from 'react'; import { ThemeProvider } from 'styled-components'; import { ConfigProvider } from '../../../src/contexts/config'; -import { I18nProvider } from '../../../src/contexts/i18n'; +import { I18nProvider, type I18nMessages } from '../../../src/contexts/i18n'; import { ReachfiveProvider } from '../../../src/contexts/reachfive'; -import { type I18nMessages } from '../../../src/core/i18n'; import { buildTheme } from '../../../src/core/theme'; import type { Config } from '../../../src/types'; import type { Theme } from '../../../src/types/styled'; @@ -40,7 +39,9 @@ export function WidgetContext({ - {children} + + {children} + diff --git a/tests/components/form/buildFormFields.test.tsx b/tests/components/form/buildFormFields.test.tsx index 39cb2c34..bdf35eb1 100644 --- a/tests/components/form/buildFormFields.test.tsx +++ b/tests/components/form/buildFormFields.test.tsx @@ -13,7 +13,7 @@ import type { Config } from '../../../src/types'; import { Client, PasswordStrengthScore } from '@reachfive/identity-core'; import { createForm } from '../../../src/components/form/formComponent'; import { buildFormFields } from '../../../src/components/form/formFieldFactory'; -import { I18nMessages } from '../../../src/core/i18n'; +import { type I18nMessages } from '../../../src/contexts/i18n'; import { WidgetContext } from './WidgetContext'; const defaultConfig: Config = { @@ -160,9 +160,9 @@ describe('DOM testing', () => { getPasswordStrength.mockImplementation((password: string) => { let score = 0; - if (password.match(/[a-z]+/)) score++; - if (password.match(/[0-9]+/)) score++; - if (password.match(/[^a-z0-9]+/)) score++; + if (/[a-z]+/.exec(password)) score++; + if (/[0-9]+/.exec(password)) score++; + if (/[^a-z0-9]+/.exec(password)) score++; if (password.length > 8) score++; return Promise.resolve({ score: score as PasswordStrengthScore }); }); diff --git a/tests/components/form/fields/birthdateField.test.tsx b/tests/components/form/fields/birthdateField.test.tsx index 0f979e97..5cc65dcc 100644 --- a/tests/components/form/fields/birthdateField.test.tsx +++ b/tests/components/form/fields/birthdateField.test.tsx @@ -14,7 +14,7 @@ import type { Config } from '../../../../src/types'; import birthdayField from '../../../../src/components/form/fields/birthdayField'; import { createForm } from '../../../../src/components/form/formComponent'; -import { I18nMessages } from '../../../../src/core/i18n'; +import { type I18nMessages } from '../../../../src/contexts/i18n'; import { WidgetContext } from '../WidgetContext'; const defaultConfig: Config = { diff --git a/tests/components/form/fields/checkboxField.test.tsx b/tests/components/form/fields/checkboxField.test.tsx index 0187a216..43be8528 100644 --- a/tests/components/form/fields/checkboxField.test.tsx +++ b/tests/components/form/fields/checkboxField.test.tsx @@ -13,7 +13,7 @@ import type { Config } from '../../../../src/types'; import checkboxField from '../../../../src/components/form/fields/checkboxField'; import { createForm } from '../../../../src/components/form/formComponent'; -import resolveI18n, { I18nMessages } from '../../../../src/core/i18n'; +import { type I18nMessages } from '../../../../src/contexts/i18n'; import { WidgetContext } from '../WidgetContext'; const defaultConfig: Config = { @@ -43,8 +43,6 @@ const defaultI18n: I18nMessages = { checkbox: 'Check?', }; -const i18nResolver = resolveI18n(defaultI18n); - type Model = { check: string }; describe('DOM testing', () => { @@ -73,7 +71,7 @@ describe('DOM testing', () => { ); }); - const checkbox = screen.getByLabelText(i18nResolver(label)); + const checkbox = screen.getByLabelText('Check?'); expect(checkbox).not.toBeChecked(); await user.click(checkbox); @@ -127,7 +125,7 @@ describe('DOM testing', () => { ); }); - const checkbox = screen.getByLabelText(i18nResolver(label)); + const checkbox = screen.getByLabelText('Check?'); expect(checkbox).toBeChecked(); await user.click(checkbox); diff --git a/tests/components/form/fields/consentField.test.tsx b/tests/components/form/fields/consentField.test.tsx index 12d37161..ea90474d 100644 --- a/tests/components/form/fields/consentField.test.tsx +++ b/tests/components/form/fields/consentField.test.tsx @@ -13,7 +13,7 @@ import type { Config } from '../../../../src/types'; import consentField from '../../../../src/components/form/fields/consentField'; import { createForm } from '../../../../src/components/form/formComponent'; -import resolveI18n, { I18nMessages } from '../../../../src/core/i18n'; +import { type I18nMessages } from '../../../../src/contexts/i18n'; import { WidgetContext } from '../WidgetContext'; const defaultConfig: Config = { @@ -43,8 +43,6 @@ const defaultI18n: I18nMessages = { checkbox: 'Check?', }; -const i18nResolver = resolveI18n(defaultI18n); - type Model = { 'consents.myconsent.1': string }; describe('DOM testing', () => { @@ -85,7 +83,7 @@ describe('DOM testing', () => { ); }); - const checkbox = screen.getByLabelText(i18nResolver(label)); + const checkbox = screen.getByLabelText('My Consent'); expect(checkbox).not.toBeChecked(); const description = screen.queryByTestId('consents.myconsent.1.description'); @@ -171,7 +169,7 @@ describe('DOM testing', () => { ); }); - const checkbox = screen.getByLabelText(i18nResolver(label)); + const checkbox = screen.getByLabelText('My Consent'); expect(checkbox).toBeChecked(); await user.click(checkbox); diff --git a/tests/components/form/fields/dateField.test.tsx b/tests/components/form/fields/dateField.test.tsx index e2c33a9f..bc690c42 100644 --- a/tests/components/form/fields/dateField.test.tsx +++ b/tests/components/form/fields/dateField.test.tsx @@ -23,7 +23,7 @@ import type { Config } from '../../../../src/types'; import dateField from '../../../../src/components/form/fields/dateField'; import { createForm } from '../../../../src/components/form/formComponent'; -import resolveI18n, { I18nMessages } from '../../../../src/core/i18n'; +import { type I18nMessages } from '../../../../src/contexts/i18n'; import { Validator } from '../../../../src/core/validation'; import { WidgetContext } from '../WidgetContext'; @@ -57,8 +57,6 @@ const defaultI18n: I18nMessages = { day: 'Jour', }; -const i18nResolver = resolveI18n(defaultI18n); - type Model = { date: string }; describe('DOM testing', () => { @@ -96,20 +94,20 @@ describe('DOM testing', () => { ); }); - const labelTag = screen.queryByText(i18nResolver(label)); + const labelTag = screen.queryByText('Date'); expect(labelTag).toBeInTheDocument(); const yearInput = screen.getByTestId('date.year'); expect(yearInput).toBeInTheDocument(); expect(yearInput).toHaveAttribute('type', 'number'); expect(yearInput).toHaveAttribute('inputMode', 'numeric'); - expect(yearInput).toHaveAttribute('aria-label', i18nResolver('year')); - expect(yearInput).toHaveAttribute('placeholder', i18nResolver('year')); + expect(yearInput).toHaveAttribute('aria-label', 'Année'); + expect(yearInput).toHaveAttribute('placeholder', 'Année'); expect(yearInput).not.toHaveValue(); const monthInput = screen.getByTestId('date.month'); expect(monthInput).toBeInTheDocument(); - expect(monthInput).toHaveAttribute('aria-label', i18nResolver('month')); + expect(monthInput).toHaveAttribute('aria-label', 'Mois'); expect(monthInput).not.toHaveValue(); const expectedMonthsOptions = ['', ...[...Array(12).keys()].map(value => String(value))]; const options = getAllByRole(monthInput, 'option'); @@ -117,7 +115,7 @@ describe('DOM testing', () => { expect.arrayContaining(expectedMonthsOptions) ); const expectedMonthsOptionsIntl = [ - i18nResolver('month'), + 'Mois', ...[...Array(12).keys()].map(value => new Intl.DateTimeFormat(defaultConfig.language, { month: 'long' }).format( new Date(2025, Number(value), 1) @@ -130,7 +128,7 @@ describe('DOM testing', () => { const dayInput = screen.getByTestId('date.day'); expect(dayInput).toBeInTheDocument(); - expect(dayInput).toHaveAttribute('aria-label', i18nResolver('day')); + expect(dayInput).toHaveAttribute('aria-label', 'Jour'); expect(dayInput).not.toHaveValue(); // default is based on current date const expectedDaysOptions = [ diff --git a/tests/components/form/fields/identifierField.test.tsx b/tests/components/form/fields/identifierField.test.tsx index c65f1853..87ea4217 100644 --- a/tests/components/form/fields/identifierField.test.tsx +++ b/tests/components/form/fields/identifierField.test.tsx @@ -14,7 +14,7 @@ import type { Config } from '../../../../src/types'; import { format } from 'libphonenumber-js'; import identifierField from '../../../../src/components/form/fields/identifierField'; import { createForm } from '../../../../src/components/form/formComponent'; -import resolveI18n, { I18nMessages } from '../../../../src/core/i18n'; +import { type I18nMessages } from '../../../../src/contexts/i18n'; import { WidgetContext } from '../WidgetContext'; const defaultConfig: Config = { @@ -44,8 +44,6 @@ const defaultI18n: I18nMessages = { identifier: 'Identifiant', }; -const i18nResolver = resolveI18n(defaultI18n); - type Model = { identifier: string }; describe('DOM testing', () => { @@ -73,7 +71,7 @@ describe('DOM testing', () => { ); }); - const input = screen.queryByLabelText(i18nResolver(label)); + const input = screen.queryByLabelText('Identifiant'); expect(input).toBeInTheDocument(); expect(input).toHaveAttribute('id', key); expect(input).toHaveValue(''); @@ -138,7 +136,7 @@ describe('DOM testing', () => { ); }); - const input = screen.queryByLabelText(i18nResolver(label)); + const input = screen.queryByLabelText('Identifiant'); expect(input).toBeInTheDocument(); expect(input).toHaveAttribute('id', key); expect(input).toHaveValue(''); @@ -208,7 +206,7 @@ describe('DOM testing', () => { ); }); - const input = screen.queryByLabelText(i18nResolver(label)); + const input = screen.queryByLabelText('Identifiant'); expect(input).toBeInTheDocument(); expect(input).toHaveAttribute('id', key); expect(input).toHaveValue(defaultValue); diff --git a/tests/components/form/fields/passwordField.test.tsx b/tests/components/form/fields/passwordField.test.tsx index 501f22ba..1368b92a 100644 --- a/tests/components/form/fields/passwordField.test.tsx +++ b/tests/components/form/fields/passwordField.test.tsx @@ -14,7 +14,7 @@ import type { Config } from '../../../../src/types'; import passwordField from '../../../../src/components/form/fields/passwordField'; import { createForm } from '../../../../src/components/form/formComponent'; -import resolveI18n, { I18nMessages } from '../../../../src/core/i18n'; +import { type I18nMessages } from '../../../../src/contexts/i18n'; import { Validator } from '../../../../src/core/validation'; import { WidgetContext } from '../WidgetContext'; @@ -45,8 +45,6 @@ const defaultI18n: I18nMessages = { password: 'Password', }; -const i18nResolver = resolveI18n(defaultI18n); - type Model = { password: string }; describe('DOM testing', () => { @@ -54,9 +52,9 @@ describe('DOM testing', () => { getPasswordStrength.mockImplementation((password: string) => { let score = 0; - if (password.match(/[a-z]+/)) score++; - if (password.match(/[0-9]+/)) score++; - if (password.match(/[^a-z0-9]+/)) score++; + if (/[a-z]+/.exec(password)) score++; + if (/[0-9]+/.exec(password)) score++; + if (/[^a-z0-9]+/.exec(password)) score++; if (password.length > 8) score++; return Promise.resolve({ score: score as PasswordStrengthScore }); }); @@ -105,7 +103,7 @@ describe('DOM testing', () => { ); }); - const input = screen.getByLabelText(i18nResolver(label)); + const input = screen.getByLabelText('Password'); expect(input).toBeInTheDocument(); expect(input).toHaveAttribute('id', key); expect(input).toHaveValue(''); @@ -185,7 +183,7 @@ describe('DOM testing', () => { ); }); - const input = screen.getByLabelText(i18nResolver(label)); + const input = screen.getByLabelText('Password'); expect(input).toBeInTheDocument(); expect(input).toHaveAttribute('type', 'password'); @@ -250,7 +248,7 @@ describe('DOM testing', () => { ); }); - const input = screen.getByLabelText(i18nResolver(label)); + const input = screen.getByLabelText('Password'); expect(input).toBeInTheDocument(); expect(screen.queryByTestId('password-strength')).not.toBeInTheDocument(); diff --git a/tests/components/form/fields/phoneNumberField.test.tsx b/tests/components/form/fields/phoneNumberField.test.tsx index 2af529cb..15ff067e 100644 --- a/tests/components/form/fields/phoneNumberField.test.tsx +++ b/tests/components/form/fields/phoneNumberField.test.tsx @@ -21,7 +21,7 @@ import type { Config } from '../../../../src/types'; import phoneNumberField from '../../../../src/components/form/fields/phoneNumberField'; import { createForm } from '../../../../src/components/form/formComponent'; -import resolveI18n, { I18nMessages } from '../../../../src/core/i18n'; +import { type I18nMessages } from '../../../../src/contexts/i18n'; import { WidgetContext } from '../WidgetContext'; const defaultConfig: Config = { @@ -51,8 +51,6 @@ const defaultI18n: I18nMessages = { phone: 'Phone number', }; -const i18nResolver = resolveI18n(defaultI18n); - const queryByName = (renderResult: RenderResult, name: Matcher) => { const query = queryHelpers.queryByAttribute.bind(null, 'name'); const element = query(renderResult.container, name); @@ -110,7 +108,7 @@ describe('DOM testing', () => { ); }); - const input = screen.queryByLabelText(i18nResolver(label)); + const input = screen.queryByLabelText('Phone number'); expect(input).toBeInTheDocument(); expect(input).toHaveAttribute('id', key); expect(input).toHaveValue(formatPhoneNumberIntl(initialValue)); diff --git a/tests/components/form/fields/radioboxField.test.tsx b/tests/components/form/fields/radioboxField.test.tsx index 98532cd7..630bc3d9 100644 --- a/tests/components/form/fields/radioboxField.test.tsx +++ b/tests/components/form/fields/radioboxField.test.tsx @@ -13,7 +13,7 @@ import type { Config } from '../../../../src/types'; import radioboxField from '../../../../src/components/form/fields/radioboxField'; import { createForm } from '../../../../src/components/form/formComponent'; -import resolveI18n, { I18nMessages } from '../../../../src/core/i18n'; +import { type I18nMessages } from '../../../../src/contexts/i18n'; import { WidgetContext } from '../WidgetContext'; const defaultConfig: Config = { @@ -43,8 +43,6 @@ const defaultI18n: I18nMessages = { radiobox: 'Pet', }; -const i18nResolver = resolveI18n(defaultI18n); - type Model = { check: string }; describe('DOM testing', () => { @@ -78,13 +76,13 @@ describe('DOM testing', () => { }); options.map(option => { - const input = screen.queryByLabelText(i18nResolver(option.label)); + const input = screen.queryByLabelText(option.label); expect(input).toBeInTheDocument(); expect(input).not.toBeChecked(); }); const choice = options[1]; - const choiceInput = screen.getByLabelText(i18nResolver(choice.label)); + const choiceInput = screen.getByLabelText('dog'); await user.click(choiceInput); expect(choiceInput).toBeChecked(); @@ -150,7 +148,7 @@ describe('DOM testing', () => { // }) const choice = options[0]; - const choiceInput = screen.getByLabelText(i18nResolver(choice.label)); + const choiceInput = screen.getByLabelText('cat'); await user.click(choiceInput); expect(choiceInput).toBeChecked(); diff --git a/tests/components/form/fields/selectField.test.tsx b/tests/components/form/fields/selectField.test.tsx index 34b7a496..b1e37999 100644 --- a/tests/components/form/fields/selectField.test.tsx +++ b/tests/components/form/fields/selectField.test.tsx @@ -13,7 +13,7 @@ import type { Config } from '../../../../src/types'; import selectField from '../../../../src/components/form/fields/selectField'; import { createForm } from '../../../../src/components/form/formComponent'; -import resolveI18n, { I18nMessages } from '../../../../src/core/i18n'; +import { type I18nMessages } from '../../../../src/contexts/i18n'; import { WidgetContext } from '../WidgetContext'; const defaultConfig: Config = { @@ -43,8 +43,6 @@ const defaultI18n: I18nMessages = { selectbox: 'Pet', }; -const i18nResolver = resolveI18n(defaultI18n); - type Model = { check: string }; describe('DOM testing', () => { @@ -77,7 +75,7 @@ describe('DOM testing', () => { ); }); - const selectbox = screen.getByLabelText(i18nResolver(label)); + const selectbox = screen.getByLabelText('Pet'); expect(selectbox).toHaveValue(''); options.forEach(option => { @@ -146,7 +144,7 @@ describe('DOM testing', () => { ); }); - const selectbox = screen.getByLabelText(i18nResolver(label)); + const selectbox = screen.getByLabelText('Pet'); expect(selectbox).toHaveValue(defaultOption.value); expect( diff --git a/tests/components/form/fields/simpleField.test.tsx b/tests/components/form/fields/simpleField.test.tsx index 364e4e65..f7cf0ae3 100644 --- a/tests/components/form/fields/simpleField.test.tsx +++ b/tests/components/form/fields/simpleField.test.tsx @@ -13,7 +13,7 @@ import type { Config } from '../../../../src/types'; import { simpleField } from '../../../../src/components/form/fields/simpleField'; import { createForm } from '../../../../src/components/form/formComponent'; -import resolveI18n, { I18nMessages } from '../../../../src/core/i18n'; +import { type I18nMessages } from '../../../../src/contexts/i18n'; import { Validator } from '../../../../src/core/validation'; import { WidgetContext } from '../WidgetContext'; @@ -44,8 +44,6 @@ const defaultI18n: I18nMessages = { simple: 'simple', }; -const i18nResolver = resolveI18n(defaultI18n); - type Model = { simple: string }; describe('DOM testing', () => { @@ -74,7 +72,7 @@ describe('DOM testing', () => { ); }); - const input = screen.getByLabelText(i18nResolver(label)); + const input = screen.getByLabelText('simple'); expect(input).toBeInTheDocument(); expect(input).toHaveAttribute('id', key); expect(input).toHaveValue(''); @@ -129,7 +127,7 @@ describe('DOM testing', () => { ); }); - const input = screen.queryByLabelText(i18nResolver(label)); + const input = screen.queryByLabelText('simple'); expect(input).toBeInTheDocument(); expect(input).toHaveAttribute('id', key); expect(input).toHaveAttribute('type', type); @@ -176,8 +174,8 @@ describe('DOM testing', () => { ); }); - const input = screen.getByLabelText(i18nResolver(label)); - expect(input).toHaveAttribute('placeholder', i18nResolver(label)); + const input = screen.getByLabelText('simple'); + expect(input).toHaveAttribute('placeholder', 'simple'); expect(input).toBeInTheDocument(); const invalidValue = 'ILoveApples'; diff --git a/tests/components/form/fields/simplePasswordField.test.tsx b/tests/components/form/fields/simplePasswordField.test.tsx index 9102b214..89de0712 100644 --- a/tests/components/form/fields/simplePasswordField.test.tsx +++ b/tests/components/form/fields/simplePasswordField.test.tsx @@ -13,7 +13,7 @@ import type { Config } from '../../../../src/types'; import simplePasswordField from '../../../../src/components/form/fields/simplePasswordField'; import { createForm } from '../../../../src/components/form/formComponent'; -import resolveI18n, { I18nMessages } from '../../../../src/core/i18n'; +import { type I18nMessages } from '../../../../src/contexts/i18n'; import { WidgetContext } from '../WidgetContext'; const defaultConfig: Config = { @@ -43,8 +43,6 @@ const defaultI18n: I18nMessages = { password: 'password', }; -const i18nResolver = resolveI18n(defaultI18n); - type Model = { password: string }; describe('DOM testing', () => { @@ -73,11 +71,11 @@ describe('DOM testing', () => { ); }); - const input = screen.getByLabelText(i18nResolver(label)); + const input = screen.getByLabelText('password'); expect(input).toBeInTheDocument(); expect(input).toHaveAttribute('id', key); expect(input).toHaveAttribute('type', 'password'); - expect(input).toHaveAttribute('placeholder', i18nResolver(label)); + expect(input).toHaveAttribute('placeholder', 'password'); expect(input).toHaveValue(''); expect(screen.queryByTestId('show-password-btn')).not.toBeInTheDocument(); @@ -135,7 +133,7 @@ describe('DOM testing', () => { ); }); - const input = screen.queryByLabelText(i18nResolver(label)); + const input = screen.queryByLabelText('password'); expect(input).toBeInTheDocument(); expect(input).toHaveAttribute('id', key); expect(input).toHaveAttribute('placeholder', placeholder); @@ -167,7 +165,7 @@ describe('DOM testing', () => { ); }); - const input = screen.getByLabelText(i18nResolver(label)); + const input = screen.getByLabelText('password'); expect(input).toBeInTheDocument(); expect(input).toHaveAttribute('type', 'password'); diff --git a/tests/contexts/i18n.test.tsx b/tests/contexts/i18n.test.tsx new file mode 100644 index 00000000..2cb19572 --- /dev/null +++ b/tests/contexts/i18n.test.tsx @@ -0,0 +1,176 @@ +/** + * @jest-environment jsdom + */ + +import { describe, expect, it } from '@jest/globals'; +import '@testing-library/jest-dom/jest-globals'; +import { render, screen } from '@testing-library/react'; +import React from 'react'; + +import type { I18nMessages } from '../../src/contexts/i18n'; +import { I18nProvider, useI18n } from '../../src/contexts/i18n'; + +// Test component that uses useI18n hook +function TestComponent({ + messageKey, + params, +}: { + messageKey: string; + params?: Record; +}) { + const i18n = useI18n(); + return

{i18n(messageKey, params)}
; +} + +describe('I18nProvider', () => { + const defaultMessages: I18nMessages = { + 'test.message': 'Hello {name}', + 'test.simple': 'Simple message', + 'test.nested': 'Nested message', + }; + + const customMessages: I18nMessages = { + 'test.message': 'Bonjour {name}', + 'test.new': 'Nouveau message', + }; + + it('should provide i18n context with default messages only', () => { + render( + + + + ); + + expect(screen.getByTestId('test-component')).toHaveTextContent('Hello World'); + }); + + it('should provide i18n context with custom messages overriding defaults', () => { + render( + + + + ); + + expect(screen.getByTestId('test-component')).toHaveTextContent('Bonjour Monde'); + }); + + it('should provide i18n context with custom messages only', () => { + render( + + + + ); + + expect(screen.getByTestId('test-component')).toHaveTextContent('Nouveau message'); + }); + + it('should provide i18n context with no messages', () => { + render( + + + + ); + + expect(screen.getByTestId('test-component')).toHaveTextContent('unknown.key'); + }); + + it('should handle nested messages correctly', () => { + const nestedMessages: I18nMessages = { + validation: { + minLength: 'La longueur minimale est de {min}', + maxLength: 'La longueur maximale est de {max}', + }, + }; + + render( + + + + ); + + expect(screen.getByTestId('test-component')).toHaveTextContent( + 'La longueur minimale est de 5' + ); + }); + + it('should handle complex nested messages', () => { + const complexMessages: I18nMessages = { + auth: { + 'login.success': 'Connexion réussie', + 'login.error': 'Erreur de connexion: {error}', + logout: 'Déconnexion réussie', + }, + }; + + render( + + + + ); + + expect(screen.getByTestId('test-component')).toHaveTextContent( + 'Erreur de connexion: Invalid credentials' + ); + }); + + it('should handle localized messages', () => { + const localizedMessages: I18nMessages = { + fr: { + 'test.message': 'Bonjour {name}', + }, + en: { + 'test.message': 'Hello {name}', + }, + }; + + render( + + + + ); + + expect(screen.getByTestId('test-component')).toHaveTextContent('Bonjour World'); + }); +}); + +describe('useI18n', () => { + it('should return i18n function when used inside I18nProvider', () => { + const defaultMessages: I18nMessages = { + 'test.message': 'Hello {name}', + }; + + render( + + + + ); + + expect(screen.getByTestId('test-component')).toHaveTextContent('Hello World'); + }); + + it('should handle fallback function', () => { + const defaultMessages: I18nMessages = { + 'test.known': 'Known message', + }; + + function TestComponentWithFallback() { + const i18n = useI18n(); + return ( +
+ {i18n('test.unknown', { defaultValue: 'Fallback message' })} +
+ ); + } + + render( + + + + ); + + expect(screen.getByTestId('test-component')).toHaveTextContent('Fallback message'); + }); +}); diff --git a/tests/core/validation.test.ts b/tests/core/validation.test.ts index 2bac4507..2af47465 100644 --- a/tests/core/validation.test.ts +++ b/tests/core/validation.test.ts @@ -1,6 +1,7 @@ -import { describe, expect, it } from '@jest/globals'; +import { beforeAll, describe, expect, it } from '@jest/globals'; +import i18n from 'i18next'; -import resolveI18n, { type I18nMessages } from '../../src/core/i18n'; +import { type I18nMessages } from '../../src/contexts/i18n'; import { checked, email, @@ -18,9 +19,23 @@ const defaultI18n: I18nMessages = { 'validation.maxLength': 'Max length is {max}', }; -const i18nResolver = resolveI18n(defaultI18n); - describe('Validator', () => { + beforeAll(() => { + i18n.init({ + lng: 'en', + resources: { + en: { + translation: defaultI18n, + }, + }, + interpolation: { + escapeValue: false, // react already safes from xss, + prefix: '{', + suffix: '}', + }, + }); + }); + it('should instanciate a custom Validator', async () => { const matchValidator = (matchText: string) => new Validator({ @@ -28,7 +43,7 @@ describe('Validator', () => { hint: 'match', }); - const validate = matchValidator('valid').create(i18nResolver); + const validate = matchValidator('valid').create(i18n.t); const valid = await validate('valid', {}); expect(valid).toMatchObject({ @@ -50,7 +65,7 @@ describe('Validator', () => { parameters: { max: MAX_LENGTH }, }); - const validate = maxLengthValidator.create(i18nResolver); + const validate = maxLengthValidator.create(i18n.t); const valid = await validate('valid', {}); expect(valid).toMatchObject({ @@ -70,7 +85,7 @@ describe('Validator', () => { hint: 'async', }); - const validate = asyncValidator.create(i18nResolver); + const validate = asyncValidator.create(i18n.t); const valid = await validate('valid', {}); expect(valid).toMatchObject({ @@ -89,7 +104,7 @@ describe('Validator', () => { hint: 'extra', }); - const validate = enrichedValidator.create(i18nResolver); + const validate = enrichedValidator.create(i18n.t); const valid = await validate(true, {}); expect(valid).toMatchObject({ @@ -121,7 +136,7 @@ describe('CompoundValidator', () => { }); const compoundValidator = minLengthValidator.and(maxLengthValidator); - const validate = compoundValidator.create(i18nResolver); + const validate = compoundValidator.create(i18n.t); const valid = await validate('valid', {}); expect(valid).toMatchObject({ @@ -161,21 +176,21 @@ describe('helpers', () => { describe('built-in validators', () => { describe('empty', () => { it('should be falsy if undefined', async () => { - const obtained = await empty.create(i18nResolver)(undefined, {}); + const obtained = await empty.create(i18n.t)(undefined, {}); expect(obtained).toMatchObject({ valid: true, }); }); it('should be falsy if null', async () => { - const obtained = await empty.create(i18nResolver)(null, {}); + const obtained = await empty.create(i18n.t)(null, {}); expect(obtained).toMatchObject({ valid: true, }); }); it('should be falsy if empty string', async () => { - const obtained = await empty.create(i18nResolver)('', {}); + const obtained = await empty.create(i18n.t)('', {}); expect(obtained).toMatchObject({ valid: true, }); @@ -184,7 +199,7 @@ describe('built-in validators', () => { describe('required', () => { it('should be invalid if undefined', async () => { - const obtained = await required.create(i18nResolver)(undefined, {}); + const obtained = await required.create(i18n.t)(undefined, {}); expect(obtained).toMatchObject({ valid: false, error: 'validation.required', @@ -192,7 +207,7 @@ describe('built-in validators', () => { }); it('should be invalid if null', async () => { - const obtained = await required.create(i18nResolver)(null, {}); + const obtained = await required.create(i18n.t)(null, {}); expect(obtained).toMatchObject({ valid: false, error: 'validation.required', @@ -200,7 +215,7 @@ describe('built-in validators', () => { }); it('should be invalid if empty string', async () => { - const obtained = await required.create(i18nResolver)('', {}); + const obtained = await required.create(i18n.t)('', {}); expect(obtained).toMatchObject({ valid: false, error: 'validation.required', @@ -208,14 +223,14 @@ describe('built-in validators', () => { }); it('should be falsy if non-empty string', async () => { - const obtained = await required.create(i18nResolver)('abc', {}); + const obtained = await required.create(i18n.t)('abc', {}); expect(obtained).toMatchObject({ valid: true, }); }); it('should be invalid if NaN', async () => { - const obtained = await required.create(i18nResolver)(NaN, {}); + const obtained = await required.create(i18n.t)(NaN, {}); expect(obtained).toMatchObject({ valid: false, error: 'validation.required', @@ -223,14 +238,14 @@ describe('built-in validators', () => { }); it('should be falsy if number', async () => { - const obtained = await required.create(i18nResolver)(42, {}); + const obtained = await required.create(i18n.t)(42, {}); expect(obtained).toMatchObject({ valid: true, }); }); it('should be invalid if empty array', async () => { - const obtained = await required.create(i18nResolver)([], {}); + const obtained = await required.create(i18n.t)([], {}); expect(obtained).toMatchObject({ valid: false, error: 'validation.required', @@ -238,21 +253,21 @@ describe('built-in validators', () => { }); it('should be falsy if non-empty array', async () => { - const obtained = await required.create(i18nResolver)(['foo'], {}); + const obtained = await required.create(i18n.t)(['foo'], {}); expect(obtained).toMatchObject({ valid: true, }); }); it('should be falsy if true', async () => { - const obtained = await required.create(i18nResolver)(true, {}); + const obtained = await required.create(i18n.t)(true, {}); expect(obtained).toMatchObject({ valid: true, }); }); it('should be falsy if false', async () => { - const obtained = await required.create(i18nResolver)(false, {}); + const obtained = await required.create(i18n.t)(false, {}); expect(obtained).toMatchObject({ valid: true, }); @@ -261,14 +276,14 @@ describe('built-in validators', () => { describe('checked', () => { it('should be falsy if true', async () => { - const obtained = await checked.create(i18nResolver)(true, {}); + const obtained = await checked.create(i18n.t)(true, {}); expect(obtained).toMatchObject({ valid: true, }); }); it('should be invalid if false', async () => { - const obtained = await checked.create(i18nResolver)(false, {}); + const obtained = await checked.create(i18n.t)(false, {}); expect(obtained).toMatchObject({ valid: false, error: 'validation.checked', @@ -276,14 +291,14 @@ describe('built-in validators', () => { }); it("should be falsy if 'true'", async () => { - const obtained = await checked.create(i18nResolver)('true', {}); + const obtained = await checked.create(i18n.t)('true', {}); expect(obtained).toMatchObject({ valid: true, }); }); it("should be invalid if 'false'", async () => { - const obtained = await checked.create(i18nResolver)('false', {}); + const obtained = await checked.create(i18n.t)('false', {}); expect(obtained).toMatchObject({ valid: false, error: 'validation.checked', @@ -293,14 +308,14 @@ describe('built-in validators', () => { describe('email', () => { it('should be falsy if valid email', async () => { - const obtained = await email.create(i18nResolver)('alice.do@reach5.co', {}); + const obtained = await email.create(i18n.t)('alice.do@reach5.co', {}); expect(obtained).toMatchObject({ valid: true, }); }); it('should be invalid if invalid email', async () => { - const obtained = await email.create(i18nResolver)('alicereach5.co', {}); + const obtained = await email.create(i18n.t)('alicereach5.co', {}); expect(obtained).toMatchObject({ valid: false, error: 'validation.email', @@ -310,21 +325,21 @@ describe('built-in validators', () => { describe('integer', () => { it("should be falsy if '0'", async () => { - const obtained = await integer.create(i18nResolver)('0', {}); + const obtained = await integer.create(i18n.t)('0', {}); expect(obtained).toMatchObject({ valid: true, }); }); it("should be falsy if '42'", async () => { - const obtained = await integer.create(i18nResolver)('42', {}); + const obtained = await integer.create(i18n.t)('42', {}); expect(obtained).toMatchObject({ valid: true, }); }); it("should be falsy if '12.3'", async () => { - const obtained = await integer.create(i18nResolver)('12.3', {}); + const obtained = await integer.create(i18n.t)('12.3', {}); expect(obtained).toMatchObject({ valid: false, error: 'validation.integer', @@ -332,7 +347,7 @@ describe('built-in validators', () => { }); it("should be invalid if 'invalid'", async () => { - const obtained = await integer.create(i18nResolver)('invalid', {}); + const obtained = await integer.create(i18n.t)('invalid', {}); expect(obtained).toMatchObject({ valid: false, error: 'validation.integer', @@ -342,28 +357,28 @@ describe('built-in validators', () => { describe('float', () => { it("should be falsy if '0'", async () => { - const obtained = await float.create(i18nResolver)('0', {}); + const obtained = await float.create(i18n.t)('0', {}); expect(obtained).toMatchObject({ valid: true, }); }); it("should be falsy if '42'", async () => { - const obtained = await float.create(i18nResolver)('42', {}); + const obtained = await float.create(i18n.t)('42', {}); expect(obtained).toMatchObject({ valid: true, }); }); it("should be falsy if '12.3'", async () => { - const obtained = await float.create(i18nResolver)('12.3', {}); + const obtained = await float.create(i18n.t)('12.3', {}); expect(obtained).toMatchObject({ valid: true, }); }); it("should be invalid if 'invalid'", async () => { - const obtained = await float.create(i18nResolver)('invalid', {}); + const obtained = await float.create(i18n.t)('invalid', {}); expect(obtained).toMatchObject({ valid: false, error: 'validation.float', diff --git a/tests/widgets/accountRecovery/accountRecovery.test.ts b/tests/widgets/accountRecovery/accountRecovery.test.ts index a25faf85..6829463a 100644 --- a/tests/widgets/accountRecovery/accountRecovery.test.ts +++ b/tests/widgets/accountRecovery/accountRecovery.test.ts @@ -11,7 +11,7 @@ import 'jest-styled-components'; import type { Client, PasswordStrengthScore } from '@reachfive/identity-core'; -import { type I18nMessages } from '../../../src/core/i18n'; +import { type I18nMessages } from '../../../src/contexts/i18n'; import type { Config, OnError, OnSuccess } from '../../../src/types'; import accountRecoveryWidget from '../../../src/widgets/accountRecovery/accountRecoveryWidget'; diff --git a/tests/widgets/auth/authWidget.test.ts b/tests/widgets/auth/authWidget.test.ts index 905f640e..c88a7916 100644 --- a/tests/widgets/auth/authWidget.test.ts +++ b/tests/widgets/auth/authWidget.test.ts @@ -10,7 +10,7 @@ import 'jest-styled-components'; import type { Client, PasswordStrengthScore } from '@reachfive/identity-core'; -import { type I18nMessages } from '../../../src/core/i18n'; +import { type I18nMessages } from '../../../src/contexts/i18n'; import { randomString } from '../../../src/helpers/random'; import { providers, type ProviderId } from '../../../src/providers/providers'; import type { Config } from '../../../src/types'; @@ -529,6 +529,32 @@ describe('DOM testing', () => { expect(screen.queryByText(title)).toBeInTheDocument(); }); + + test('overwrite title - internationalized', async () => { + expect.assertions(1); + await generateComponent( + { + i18n: { + fr: { + login: { + title: 'Connexion', + }, + }, + en: { + login: { + title: 'Login', + }, + }, + }, + }, + { + ...defaultConfig, + language: 'fr', + } + ); + + expect(screen.queryByText('Connexion')).toBeInTheDocument(); + }); }); }); diff --git a/tests/widgets/emailEditor/emailEditorWidget.test.ts b/tests/widgets/emailEditor/emailEditorWidget.test.ts index 8f89d380..56cd8993 100644 --- a/tests/widgets/emailEditor/emailEditorWidget.test.ts +++ b/tests/widgets/emailEditor/emailEditorWidget.test.ts @@ -10,7 +10,7 @@ import 'jest-styled-components'; import { type Client } from '@reachfive/identity-core'; -import { type I18nMessages } from '../../../src/core/i18n'; +import { type I18nMessages } from '../../../src/contexts/i18n'; import type { Config, OnError, OnSuccess } from '../../../src/types'; import emailEditorWidget from '../../../src/widgets/emailEditor/emailEditorWidget'; diff --git a/tests/widgets/mfa/MfaCredentialsWidget.test.ts b/tests/widgets/mfa/MfaCredentialsWidget.test.ts index b407b5f9..8426159b 100644 --- a/tests/widgets/mfa/MfaCredentialsWidget.test.ts +++ b/tests/widgets/mfa/MfaCredentialsWidget.test.ts @@ -9,7 +9,7 @@ import userEvent from '@testing-library/user-event'; import { Client, MFA } from '@reachfive/identity-core'; -import { I18nMessages } from '../../../src/core/i18n'; +import { type I18nMessages } from '../../../src/contexts/i18n'; import type { Config, OnError, OnSuccess } from '../../../src/types'; import mfaCredentialsWidget from '../../../src/widgets/mfa/MfaCredentialsWidget'; diff --git a/tests/widgets/mfa/mfaListWidget.test.ts b/tests/widgets/mfa/mfaListWidget.test.ts index 23337c3e..6cd6d9b1 100644 --- a/tests/widgets/mfa/mfaListWidget.test.ts +++ b/tests/widgets/mfa/mfaListWidget.test.ts @@ -9,7 +9,7 @@ import 'jest-styled-components'; import type { Client, MFA } from '@reachfive/identity-core'; -import type { I18nMessages } from '../../../src/core/i18n'; +import type { I18nMessages } from '../../../src/contexts/i18n'; import type { Config, OnError, OnSuccess } from '../../../src/types'; import { AppError } from '../../../src/helpers/errors'; diff --git a/tests/widgets/mfa/mfaStepUpWidget.test.ts b/tests/widgets/mfa/mfaStepUpWidget.test.ts index 24bca4ab..9ca6415e 100644 --- a/tests/widgets/mfa/mfaStepUpWidget.test.ts +++ b/tests/widgets/mfa/mfaStepUpWidget.test.ts @@ -9,7 +9,7 @@ import userEvent, { UserEvent } from '@testing-library/user-event'; import 'jest-styled-components'; import { Client } from '@reachfive/identity-core'; -import { I18nMessages } from '../../../src/core/i18n'; +import { type I18nMessages } from '../../../src/contexts/i18n'; import type { Config, OnError, OnSuccess } from '../../../src/types'; import mfaStepUpWidget from '../../../src/widgets/stepUp/mfaStepUpWidget'; diff --git a/tests/widgets/mfa/trustedDevicesWidget.test.ts b/tests/widgets/mfa/trustedDevicesWidget.test.ts index 2bbc94d2..3b95907e 100644 --- a/tests/widgets/mfa/trustedDevicesWidget.test.ts +++ b/tests/widgets/mfa/trustedDevicesWidget.test.ts @@ -2,7 +2,7 @@ import { beforeEach, describe, expect, jest, test } from '@jest/globals'; import { Client } from '@reachfive/identity-core'; import '@testing-library/jest-dom/jest-globals'; import { render, screen, waitFor } from '@testing-library/react'; -import { I18nMessages } from '../../../src/core/i18n'; +import { type I18nMessages } from '../../../src/contexts/i18n'; import { AppError } from '../../../src/helpers/errors'; import type { Config, OnError, OnSuccess } from '../../../src/types'; import trustedDevicesWidget from '../../../src/widgets/mfa/trustedDevicesWidget'; diff --git a/tests/widgets/passwordEditor/passwordEditorWidget.test.ts b/tests/widgets/passwordEditor/passwordEditorWidget.test.ts index 122b2737..29f5bf09 100644 --- a/tests/widgets/passwordEditor/passwordEditorWidget.test.ts +++ b/tests/widgets/passwordEditor/passwordEditorWidget.test.ts @@ -10,7 +10,7 @@ import 'jest-styled-components'; import type { Client, PasswordStrengthScore } from '@reachfive/identity-core'; -import { type I18nMessages } from '../../../src/core/i18n'; +import { type I18nMessages } from '../../../src/contexts/i18n'; import type { Config, OnError, OnSuccess } from '../../../src/types'; import passwordEditorWidget from '../../../src/widgets/passwordEditor/passwordEditorWidget'; diff --git a/tests/widgets/passwordReset/passwordResetWidget.test.ts b/tests/widgets/passwordReset/passwordResetWidget.test.ts index 60bbbcc1..87be3dcb 100644 --- a/tests/widgets/passwordReset/passwordResetWidget.test.ts +++ b/tests/widgets/passwordReset/passwordResetWidget.test.ts @@ -11,7 +11,7 @@ import 'jest-styled-components'; import { PasswordStrengthScore, type Client } from '@reachfive/identity-core'; -import { type I18nMessages } from '../../../src/core/i18n'; +import { type I18nMessages } from '../../../src/contexts/i18n'; import type { Config, OnError, OnSuccess } from '../../../src/types'; import passwordResetWidget from '../../../src/widgets/passwordReset/passwordResetWidget'; diff --git a/tests/widgets/passwordless/passwordlessWidget.test.ts b/tests/widgets/passwordless/passwordlessWidget.test.ts index 7fe81e57..24b46ca8 100644 --- a/tests/widgets/passwordless/passwordlessWidget.test.ts +++ b/tests/widgets/passwordless/passwordlessWidget.test.ts @@ -10,7 +10,7 @@ import 'jest-styled-components'; import { type Client } from '@reachfive/identity-core'; -import { type I18nMessages } from '../../../src/core/i18n'; +import { type I18nMessages } from '../../../src/contexts/i18n'; import type { Config, OnError, OnSuccess } from '../../../src/types'; import passwordlessWidget from '../../../src/widgets/passwordless/passwordlessWidget'; diff --git a/tests/widgets/phoneNumberEditor/phoneNumberEditorWidget.test.ts b/tests/widgets/phoneNumberEditor/phoneNumberEditorWidget.test.ts index 75fb2b4c..bd6433cc 100644 --- a/tests/widgets/phoneNumberEditor/phoneNumberEditorWidget.test.ts +++ b/tests/widgets/phoneNumberEditor/phoneNumberEditorWidget.test.ts @@ -10,7 +10,7 @@ import 'jest-styled-components'; import { type Client } from '@reachfive/identity-core'; -import { type I18nMessages } from '../../../src/core/i18n'; +import { type I18nMessages } from '../../../src/contexts/i18n'; import type { Config, OnError, OnSuccess } from '../../../src/types'; import phoneNumberEditorWidget from '../../../src/widgets/phoneNumberEditor/phoneNumberEditorWidget'; diff --git a/tests/widgets/profileEditor/profileEditorWidget.test.ts b/tests/widgets/profileEditor/profileEditorWidget.test.ts index 7c33edd7..c4072d6d 100644 --- a/tests/widgets/profileEditor/profileEditorWidget.test.ts +++ b/tests/widgets/profileEditor/profileEditorWidget.test.ts @@ -10,7 +10,7 @@ import 'jest-styled-components'; import { type Client, type Profile } from '@reachfive/identity-core'; -import { type I18nMessages } from '../../../src/core/i18n'; +import { type I18nMessages } from '../../../src/contexts/i18n'; import type { Config, OnError, OnSuccess } from '../../../src/types'; import profileEditorWidget from '../../../src/widgets/profileEditor/profileEditorWidget'; diff --git a/tests/widgets/socialAccounts/socialAccountsWidget.test.ts b/tests/widgets/socialAccounts/socialAccountsWidget.test.ts index 48d63c06..c91c53a1 100644 --- a/tests/widgets/socialAccounts/socialAccountsWidget.test.ts +++ b/tests/widgets/socialAccounts/socialAccountsWidget.test.ts @@ -10,7 +10,7 @@ import 'jest-styled-components'; import { type Client, type Profile } from '@reachfive/identity-core'; -import { type I18nMessages } from '../../../src/core/i18n'; +import { type I18nMessages } from '../../../src/contexts/i18n'; import type { Config, OnError, OnSuccess } from '../../../src/types'; import socialAccountsWidget from '../../../src/widgets/socialAccounts/socialAccountsWidget'; diff --git a/tests/widgets/socialLogin/socialLogin.test.ts b/tests/widgets/socialLogin/socialLogin.test.ts index 331411d0..983f5f63 100644 --- a/tests/widgets/socialLogin/socialLogin.test.ts +++ b/tests/widgets/socialLogin/socialLogin.test.ts @@ -10,7 +10,7 @@ import 'jest-styled-components'; import { type Client } from '@reachfive/identity-core'; -import { I18nMessages } from '../../../src/core/i18n'; +import { type I18nMessages } from '../../../src/contexts/i18n'; import { providers, type ProviderId } from '../../../src/providers/providers'; import type { Config, OnError, OnSuccess } from '../../../src/types'; diff --git a/tests/widgets/webAuthn/webAuthnDevicesWidget.test.ts b/tests/widgets/webAuthn/webAuthnDevicesWidget.test.ts index 209987cf..20153e93 100644 --- a/tests/widgets/webAuthn/webAuthnDevicesWidget.test.ts +++ b/tests/widgets/webAuthn/webAuthnDevicesWidget.test.ts @@ -10,7 +10,7 @@ import 'jest-styled-components'; import { type Client } from '@reachfive/identity-core'; -import { type I18nMessages } from '../../../src/core/i18n'; +import { type I18nMessages } from '../../../src/contexts/i18n'; import { UserError } from '../../../src/helpers/errors'; import type { Config, OnError, OnSuccess } from '../../../src/types'; diff --git a/tsconfig.json b/tsconfig.json index 2be61eb4..08078adc 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -28,5 +28,6 @@ "@/lib": ["./src/lib/*"] } }, - "include": ["src", "tests", "rollup.config.js", "tailwind.config.cjs", "eslint.config.mjs"] + "include": ["src", "tests", "rollup.config.js", "tailwind.config.cjs", "eslint.config.mjs"], + "exclude": ["node_modules", "cjs", "es", "umd", "eslint.config.mjs"] }