Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 86 additions & 14 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
2 changes: 1 addition & 1 deletion src/client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down
6 changes: 3 additions & 3 deletions src/components/form/fieldCreator.tsx
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -70,7 +70,7 @@ export type FieldComponentProps<
rawProperty?: K;
required?: boolean;
readOnly?: boolean;
i18n: I18nResolver;
i18n: TFunction;
showLabel?: boolean;
value?: FormValue<T, K>;
validation?: ValidatorResult<E>;
Expand Down Expand Up @@ -107,7 +107,7 @@ export interface FieldProps<
format?: Formatter<T, F, K>;
rawProperty?: K;
component: ComponentType<P>;
extendedParams?: ExtraParams | ((i18n: I18nResolver) => ExtraParams);
extendedParams?: ExtraParams | ((i18n: TFunction) => ExtraParams);
}

export function createField<
Expand Down
6 changes: 3 additions & 3 deletions src/components/form/fields/passwordField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -72,7 +72,7 @@ const PasswordStrength = ({ score }: PasswordStrength) => {
<PasswordStrengthGauge score={score} />
</PasswordStrengthGaugeContainer>
<PasswordStrengthLabel score={score}>
{i18n('passwordStrength.score' + score)}
{i18n(`passwordStrength.score${score}`)}
</PasswordStrengthLabel>
</PasswordStrengthContainer>
);
Expand Down Expand Up @@ -187,7 +187,7 @@ function PasswordField({
type RuleKeys = Exclude<keyof PasswordPolicy, 'minStrength' | 'allowUpdateWithAccessTokenOnly'>;

export function listEnabledRules(
i18n: I18nResolver,
i18n: TFunction,
passwordPolicy: Config['passwordPolicy']
): Record<RuleKeys, PasswordRule> {
if (!passwordPolicy) return {} as Record<RuleKeys, PasswordRule>;
Expand Down
2 changes: 1 addition & 1 deletion src/components/form/formComponent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -294,7 +294,7 @@ export function createForm<Model extends Record<PropertyKey, unknown> = {}, 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;
}
};
Expand Down
4 changes: 2 additions & 2 deletions src/components/form/socialButtonsComponent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ const SocialBtn = styled(Button).attrs<SocialBtn>(({ $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,
};
})<SocialBtn>`
Expand Down Expand Up @@ -118,7 +118,7 @@ const SocialButton = ({ provider, onClick, count }: SocialButtonProps) => {
<SocialButtonIcon icon={provider.icon} textVisible={textVisible} />
{textVisible && (
<SocialButtonText>
{i18n(`socialButton.${provider.key}.title`, undefined, () => provider.name)}
{i18n(`socialButton.${provider.key}.title`, { defaultValue: provider.name })}
</SocialButtonText>
)}
</SocialBtn>
Expand Down
6 changes: 3 additions & 3 deletions src/components/widget/widget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,17 @@ 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';
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<P> = Prettify<P & I18nProps>;
Expand Down Expand Up @@ -87,6 +86,7 @@ export function createWidget<P, U = P>({
<I18nProvider
defaultMessages={context.defaultI18n}
messages={preparedOptions.i18n}
locale={context.config.language}
>
<WidgetContainerThemeVariables
{...widgetAttrs}
Expand Down
74 changes: 37 additions & 37 deletions src/contexts/i18n.tsx
Original file line number Diff line number Diff line change
@@ -1,50 +1,50 @@
import React, { ComponentType, PropsWithChildren, useMemo } from 'react';
import i18n, { ResourceKey, TFunction } from 'i18next';
import React, { PropsWithChildren } from 'react';
import { I18nextProvider, initReactI18next, useTranslation } from 'react-i18next';

import { I18nMessages, I18nNestedMessages, I18nResolver, resolveI18n } from '../core/i18n';
export type I18nMessages = Record<string, ResourceKey>;

export interface Props {
defaultMessages?: I18nMessages;
messages?: I18nNestedMessages;
messages?: I18nMessages;
locale: string;
}

export const I18nContext = React.createContext<I18nResolver | undefined>(undefined);
export type WithI18n<T> = 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> = P & I18nProps;

export function withI18n<T extends I18nProps = I18nProps>(WrappedComponent: ComponentType<T>) {
const displayName = WrappedComponent.displayName ?? WrappedComponent.name ?? 'Component';

const ComponentWithI18n = (props: Omit<T, keyof I18nProps>) => {
const i18n = useI18n();
return <WrappedComponent {...{ i18n }} {...(props as T)} />;
};

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<Props>): JSX.Element | null {
const resolver = useMemo(
() => resolveI18n(defaultMessages, messages),
[defaultMessages, messages]
);
return <I18nContext.Provider value={resolver}>{children}</I18nContext.Provider>;
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 <I18nextProvider i18n={i18n}>{children}</I18nextProvider>;
}
Loading