chore: rename I18n component to Localizer

pull/3206/head
Ryan Miller 7 months ago
parent 28cf1a3dd7
commit 70d83c1b7e

@ -2,7 +2,7 @@ import styled from 'styled-components';
import type {
ArgsRecord,
GetMessageArgs,
I18nProps,
LocalizerComponentProps,
LocalizerDictionary,
LocalizerToken,
} from '../../types/Localizer';
@ -85,19 +85,17 @@ const StyledHtmlRenderer = styled.span<{ isDarkTheme: boolean }>`
* @param props.token - The token identifying the message to retrieve and an optional record of substitution variables and their replacement values.
* @param props.args - An optional record of substitution variables and their replacement values. This is required if the string has dynamic parts.
* @param props.as - An optional HTML tag to render the component as. Defaults to a fragment, unless the string contains html tags. In that case, it will render as HTML in a div tag.
* @param props.startTagProps - An optional object of props to pass to the start tag.
* @param props.endTagProps - An optional object of props to pass to the end tag.
*
* @returns The localized message string with substitutions and formatting applied.
*
* @example
* ```tsx
* <I18n token="about" />
* <I18n token="about" as='h1' />
* <I18n token="disappearingMessagesFollowSettingOn" args={{ time: 10, type: 'mode' }} />
* <Localizer token="about" />
* <Localizer token="about" as='h1' />
* <Localizer token="disappearingMessagesFollowSettingOn" args={{ time: 10, type: 'mode' }} />
* ```
*/
export const I18n = <T extends LocalizerToken>(props: I18nProps<T>) => {
export const Localizer = <T extends LocalizerToken>(props: LocalizerComponentProps<T>) => {
const isDarkMode = useIsDarkTheme();
const args = 'args' in props ? props.args : undefined;

@ -1,6 +1,6 @@
import DOMPurify from 'dompurify';
import { createElement, type ElementType } from 'react';
import { supportedFormattingTags } from './I18n';
import { supportedFormattingTags } from './Localizer';
type ReceivedProps = {
html: string;

@ -1,7 +1,7 @@
import styled from 'styled-components';
import { forwardRef } from 'react';
import { I18n } from './I18n';
import { I18nProps, LocalizerToken } from '../../types/Localizer';
import { Localizer } from './Localizer';
import { LocalizerComponentProps, LocalizerToken } from '../../types/Localizer';
const StyledI18nSubTextContainer = styled('div')`
font-size: var(--font-size-md);
@ -15,12 +15,13 @@ const StyledI18nSubTextContainer = styled('div')`
padding-inline: var(--margins-lg);
`;
export const StyledI18nSubText = forwardRef<HTMLSpanElement, I18nProps<LocalizerToken>>(
({ className, ...props }) => {
return (
<StyledI18nSubTextContainer className={className}>
<I18n {...props} />
</StyledI18nSubTextContainer>
);
}
);
export const StyledI18nSubText = forwardRef<
HTMLSpanElement,
LocalizerComponentProps<LocalizerToken>
>(({ className, ...props }) => {
return (
<StyledI18nSubTextContainer className={className}>
<Localizer {...props} />
</StyledI18nSubTextContainer>
);
});

@ -15,7 +15,7 @@ import {
useSelectedIsPrivate,
useSelectedNicknameOrProfileNameOrShortenedPubkey,
} from '../../state/selectors/selectedConversation';
import { I18n } from '../basic/I18n';
import { Localizer } from '../basic/Localizer';
const Container = styled.div`
display: flex;
@ -99,18 +99,18 @@ export const NoMessageInConversation = () => {
const content = useMemo(() => {
if (isMe) {
return <I18n token="noteToSelfEmpty" />;
return <Localizer token="noteToSelfEmpty" />;
}
if (canWrite) {
return <I18n token="groupNoMessages" args={{ group_name: name }} />;
return <Localizer token="groupNoMessages" args={{ group_name: name }} />;
}
if (privateBlindedAndBlockingMsgReqs) {
return <I18n token="messageRequestsTurnedOff" args={{ name }} />;
return <Localizer token="messageRequestsTurnedOff" args={{ name }} />;
}
return <I18n token="conversationsEmpty" args={{ conversation_name: name }} />;
return <Localizer token="conversationsEmpty" args={{ conversation_name: name }} />;
}, [isMe, canWrite, privateBlindedAndBlockingMsgReqs, name]);
if (!selectedConversation || hasMessages) {

@ -24,8 +24,8 @@ import { getConversationController } from '../../session/conversations';
import { updateConfirmModal } from '../../state/ducks/modalDialog';
import { SessionButtonColor } from '../basic/SessionButton';
import { SessionIcon } from '../icon';
import { I18n } from '../basic/I18n';
import { I18nProps, LocalizerToken } from '../../types/Localizer';
import { Localizer } from '../basic/Localizer';
import type { LocalizerComponentProps, LocalizerToken } from '../../types/Localizer';
const FollowSettingButton = styled.button`
color: var(--primary-color);
@ -50,14 +50,14 @@ function useFollowSettingsButtonClick(
const i18nMessage = props.disabled
? ({
token: 'disappearingMessagesFollowSettingOff',
} as I18nProps<'disappearingMessagesFollowSettingOff'>)
} as LocalizerComponentProps<'disappearingMessagesFollowSettingOff'>)
: ({
token: 'disappearingMessagesFollowSettingOn',
args: {
time: props.timespanText,
disappearing_messages_type: localizedMode,
},
} as I18nProps<'disappearingMessagesFollowSettingOn'>);
} as LocalizerComponentProps<'disappearingMessagesFollowSettingOn'>);
const okText = props.disabled ? window.i18n('yes') : window.i18n('set');
@ -193,7 +193,7 @@ function useTextToRenderI18nProps(props: PropsForExpirationTimer) {
export const TimerNotification = (props: PropsForExpirationTimer) => {
const { messageId } = props;
const i18nProps = useTextToRenderI18nProps(props) as I18nProps<LocalizerToken>;
const i18nProps = useTextToRenderI18nProps(props) as LocalizerComponentProps<LocalizerToken>;
const isGroupOrCommunity = useSelectedIsGroupOrCommunity();
const isGroupV2 = useSelectedIsGroupV2();
// renderOff is true when the update is put to off, or when we have a legacy group control message (as they are not expiring at all)
@ -228,7 +228,7 @@ export const TimerNotification = (props: PropsForExpirationTimer) => {
</>
)}
<TextWithChildren subtle={true}>
<I18n {...i18nProps} />
<Localizer {...i18nProps} />
</TextWithChildren>
<FollowSettingsButton {...props} />
</Flex>

@ -71,17 +71,16 @@ export const ConversationHeaderTitle = (props: ConversationHeaderTitleProps) =>
);
const memberCountSubtitle = useMemo(() => {
let memberCount = 0;
let count = 0;
if (isGroup) {
if (isPublic) {
memberCount = subscriberCount || 0;
count = subscriberCount || 0;
} else {
memberCount = members.length;
count = members.length;
}
}
if (isGroup && memberCount > 0 && !isKickedFromGroup) {
const count = String(memberCount);
if (isGroup && count > 0 && !isKickedFromGroup) {
return isPublic ? i18n('membersActive', { count }) : i18n('members', { count });
}

@ -2,7 +2,7 @@ import { PropsForDataExtractionNotification } from '../../../../models/messageTy
import { SignalService } from '../../../../protobuf';
import { ExpirableReadableMessage } from './ExpirableReadableMessage';
import { NotificationBubble } from './notification-bubble/NotificationBubble';
import { I18n } from '../../../basic/I18n';
import { Localizer } from '../../../basic/Localizer';
export const DataExtractionNotification = (props: PropsForDataExtractionNotification) => {
const { name, type, source, messageId } = props;
@ -15,7 +15,7 @@ export const DataExtractionNotification = (props: PropsForDataExtractionNotifica
isControlMessage={true}
>
<NotificationBubble iconType="save">
<I18n
<Localizer
token={
type === SignalService.DataExtractionNotification.Type.MEDIA_SAVED
? 'attachmentsMediaSaved'

@ -11,12 +11,12 @@ import {
import { useSelectedNicknameOrProfileNameOrShortenedPubkey } from '../../../../state/selectors/selectedConversation';
import { ExpirableReadableMessage } from './ExpirableReadableMessage';
import { NotificationBubble } from './notification-bubble/NotificationBubble';
import { I18n } from '../../../basic/I18n';
import { type I18nPropsObject } from '../../../../types/Localizer';
import { Localizer } from '../../../basic/Localizer';
import { type LocalizerComponentPropsObject } from '../../../../types/Localizer';
// This component is used to display group updates in the conversation view.
const ChangeItemJoined = (added: Array<string>): I18nPropsObject => {
const ChangeItemJoined = (added: Array<string>): LocalizerComponentPropsObject => {
const groupName = useSelectedNicknameOrProfileNameOrShortenedPubkey();
if (!added.length) {
@ -26,7 +26,7 @@ const ChangeItemJoined = (added: Array<string>): I18nPropsObject => {
return getJoinedGroupUpdateChangeStr(added, groupName);
};
const ChangeItemKicked = (kicked: Array<string>): I18nPropsObject => {
const ChangeItemKicked = (kicked: Array<string>): LocalizerComponentPropsObject => {
if (!kicked.length) {
throw new Error('Group update kicked is missing details');
}
@ -35,7 +35,7 @@ const ChangeItemKicked = (kicked: Array<string>): I18nPropsObject => {
return getKickedGroupUpdateStr(kicked, groupName);
};
const ChangeItemLeft = (left: Array<string>): I18nPropsObject => {
const ChangeItemLeft = (left: Array<string>): LocalizerComponentPropsObject => {
const groupName = useSelectedNicknameOrProfileNameOrShortenedPubkey();
if (!left.length) {
@ -45,7 +45,7 @@ const ChangeItemLeft = (left: Array<string>): I18nPropsObject => {
return getLeftGroupUpdateChangeStr(left, groupName);
};
const ChangeItem = (change: PropsForGroupUpdateType): I18nPropsObject => {
const ChangeItem = (change: PropsForGroupUpdateType): LocalizerComponentPropsObject => {
const { type } = change;
switch (type) {
case 'name':
@ -79,7 +79,7 @@ export const GroupUpdateMessage = (props: PropsForGroupUpdate) => {
isControlMessage={true}
>
<NotificationBubble iconType="users">
{!isNull(changeItem) ? <I18n {...changeItem} /> : null}
{!isNull(changeItem) ? <Localizer {...changeItem} /> : null}
</NotificationBubble>
</ExpirableReadableMessage>
);

@ -2,7 +2,7 @@ import { useNicknameOrProfileNameOrShortenedPubkey } from '../../../../hooks/use
import { PropsForMessageRequestResponse } from '../../../../models/messageType';
import { UserUtils } from '../../../../session/utils';
import { Flex } from '../../../basic/Flex';
import { I18n } from '../../../basic/I18n';
import { Localizer } from '../../../basic/Localizer';
import { SpacerSM, TextWithChildren } from '../../../basic/Text';
import { ReadableMessage } from './ReadableMessage';
@ -32,14 +32,14 @@ export const MessageRequestResponse = (props: PropsForMessageRequestResponse) =>
<SpacerSM />
<TextWithChildren subtle={true} ellipsisOverflow={false} textAlign="center">
{isFromSync ? (
<I18n
<Localizer
token="messageRequestYouHaveAccepted"
args={{
name: profileName || window.i18n('unknown'),
}}
/>
) : (
<I18n token="messageRequestsAccepted" />
<Localizer token="messageRequestsAccepted" />
)}
</TextWithChildren>
</Flex>

@ -5,7 +5,7 @@ import { LocalizerToken } from '../../../../../types/Localizer';
import { SessionIconType } from '../../../../icon';
import { ExpirableReadableMessage } from '../ExpirableReadableMessage';
import { NotificationBubble } from './NotificationBubble';
import { I18n } from '../../../../basic/I18n';
import { Localizer } from '../../../../basic/Localizer';
type StyleType = Record<
CallNotificationType,
@ -45,7 +45,7 @@ export const CallNotification = (props: PropsForCallNotification) => {
isControlMessage={true}
>
<NotificationBubble iconType={iconType} iconColor={iconColor}>
<I18n token={notificationTextKey} args={{ name }} />
<Localizer token={notificationTextKey} args={{ name }} />
</NotificationBubble>
</ExpirableReadableMessage>
);

@ -3,9 +3,9 @@ import styled from 'styled-components';
import { findAndFormatContact } from '../../../../models/message';
import { PubKey } from '../../../../session/types/PubKey';
import { I18n } from '../../../basic/I18n';
import { Localizer } from '../../../basic/Localizer';
import { nativeEmojiData } from '../../../../util/emoji';
import { type I18nPropsObject } from '../../../../types/Localizer';
import { type LocalizerComponentPropsObject } from '../../../../types/Localizer';
export type TipPosition = 'center' | 'left' | 'right';
@ -83,7 +83,7 @@ const getI18nComponentProps = (
numberOfReactors: number,
emoji: string,
emojiName?: string
): I18nPropsObject => {
): LocalizerComponentPropsObject => {
const name = contacts[0];
const other_name = contacts[1];
const emoji_name = emojiName ? `:${emojiName}:` : emoji;
@ -134,7 +134,7 @@ export const ReactionPopup = (props: Props) => {
return (
<StyledPopupContainer tooltipPosition={tooltipPosition} onClick={onClick}>
<I18n {...i18nProps} />
<Localizer {...i18nProps} />
<StyledEmoji role={'img'} aria-label={emojiAriaLabel}>
{emoji}
</StyledEmoji>

@ -8,7 +8,7 @@ import { SessionWrapperModal } from '../SessionWrapperModal';
import { Flex } from '../basic/Flex';
import { SessionButton, SessionButtonColor, SessionButtonType } from '../basic/SessionButton';
import { SpacerMD } from '../basic/Text';
import { I18n } from '../basic/I18n';
import { Localizer } from '../basic/Localizer';
const StyledDescriptionContainer = styled.div`
width: 280px;
@ -79,7 +79,7 @@ export function HideRecoveryPasswordDialog(props: HideRecoveryPasswordDialogProp
additionalClassName="no-body-padding"
>
<StyledDescriptionContainer>
<I18n
<Localizer
token={
state === 'firstWarning'
? 'recoveryPasswordHidePermanentlyDescription1'

@ -27,7 +27,7 @@ import { MessageReactions } from '../conversation/message/message-content/Messag
import { SessionIconButton } from '../icon';
import { SessionWrapperModal } from '../SessionWrapperModal';
import { findAndFormatContact } from '../../models/message';
import { I18n } from '../basic/I18n';
import { Localizer } from '../basic/Localizer';
const StyledReactListContainer = styled(Flex)`
width: 376px;
@ -187,7 +187,7 @@ const StyledCountText = styled.p`
const CountText = ({ count, emoji }: { count: number; emoji: string }) => {
return (
<StyledCountText>
<I18n
<Localizer
token="emojiReactsCountOthers"
args={{
count: count - Reactions.SOGSReactorsFetchCount,

@ -10,13 +10,13 @@ import { SessionButton, SessionButtonColor, SessionButtonType } from '../basic/S
import { SessionRadioGroup, SessionRadioItems } from '../basic/SessionRadioGroup';
import { SpacerLG } from '../basic/Text';
import { SessionSpinner } from '../loading';
import { type I18nPropsObject } from '../../types/Localizer';
import { type LocalizerComponentPropsObject } from '../../types/Localizer';
import { StyledI18nSubText } from '../basic/StyledI18nSubText';
export interface SessionConfirmDialogProps {
i18nMessage?: I18nPropsObject;
i18nMessageSub?: I18nPropsObject;
i18nMessage?: LocalizerComponentPropsObject;
i18nMessageSub?: LocalizerComponentPropsObject;
title?: string;
radioOptions?: SessionRadioItems;
onOk?: any;

@ -11,7 +11,7 @@ import { SessionButton, SessionButtonColor, SessionButtonType } from '../basic/S
import { SpacerLG } from '../basic/Text';
import { useConversationRealName } from '../../hooks/useParamSelector';
import { PubKey } from '../../session/types';
import { I18n } from '../basic/I18n';
import { Localizer } from '../basic/Localizer';
type Props = {
conversationId: string;
@ -66,12 +66,12 @@ export const SessionNicknameDialog = (props: Props) => {
showHeader={true}
>
<StyledMaxWidth className="session-modal__centered">
<I18n
<Localizer
token="nicknameDescription"
args={{
name: displayName || PubKey.shorten(conversationId),
}}
></I18n>
/>
<SpacerLG />
</StyledMaxWidth>

@ -10,15 +10,15 @@ import { updateBlockOrUnblockModal } from '../../../state/ducks/modalDialog';
import { BlockedNumberController } from '../../../util';
import { SessionWrapperModal } from '../../SessionWrapperModal';
import { Flex } from '../../basic/Flex';
import { I18n } from '../../basic/I18n';
import { Localizer } from '../../basic/Localizer';
import { SessionButton, SessionButtonColor, SessionButtonType } from '../../basic/SessionButton';
import { StyledModalDescriptionContainer } from '../shared/ModalDescriptionContainer';
import { BlockOrUnblockModalState } from './BlockOrUnblockModalState';
import type { I18nPropsObject } from '../../../types/Localizer';
import type { LocalizerComponentPropsObject } from '../../../types/Localizer';
type ModalState = NonNullable<BlockOrUnblockModalState>;
function getUnblockTokenAndArgs(names: Array<string>): I18nPropsObject {
function getUnblockTokenAndArgs(names: Array<string>): LocalizerComponentPropsObject {
// multiple unblock is supported
switch (names.length) {
case 1:
@ -37,7 +37,7 @@ function getUnblockTokenAndArgs(names: Array<string>): I18nPropsObject {
function useBlockUnblockI18nDescriptionArgs({
action,
pubkeys,
}: Pick<ModalState, 'action' | 'pubkeys'>): I18nPropsObject {
}: Pick<ModalState, 'action' | 'pubkeys'>): LocalizerComponentPropsObject {
const names = useConversationsNicknameRealNameOrShortenPubkey(pubkeys);
if (!pubkeys.length) {
throw new Error('useI18nDescriptionArgsForAction called with empty list of pubkeys');
@ -82,11 +82,11 @@ export const BlockOrUnblockDialog = ({ pubkeys, action, onConfirmed }: NonNullab
closeModal();
return null;
}
return (
<SessionWrapperModal showExitIcon={true} title={localizedAction} onClose={closeModal}>
<StyledModalDescriptionContainer>
<I18n {...args} />
<Localizer {...args} />
</StyledModalDescriptionContainer>
<Flex container={true} flexDirection="column" alignItems="center">
<Flex container={true}>

@ -1,7 +1,7 @@
import { useDispatch } from 'react-redux';
import styled from 'styled-components';
import { updateTermsOfServicePrivacyModal } from '../../state/onboarding/ducks/modals';
import { I18n } from '../basic/I18n';
import { Localizer } from '../basic/Localizer';
const StyledTermsAndConditions = styled.div`
text-align: center;
@ -24,7 +24,7 @@ export const TermsAndConditions = () => {
onClick={() => dispatch(updateTermsOfServicePrivacyModal({ show: true }))}
data-testid="open-url"
>
<I18n token="onboardingTosPrivacy" />
<Localizer token="onboardingTosPrivacy" />
</StyledTermsAndConditions>
);
};

@ -17,7 +17,7 @@ import { deleteDbLocally } from '../../../util/accountManager';
import { Flex } from '../../basic/Flex';
import { SessionButtonColor } from '../../basic/SessionButton';
import { SessionIconButton } from '../../icon';
import { I18nProps, LocalizerToken } from '../../../types/Localizer';
import { LocalizerComponentProps, LocalizerToken } from '../../../types/Localizer';
/** Min height should match the onboarding step with the largest height this prevents the loading spinner from jumping around while still keeping things centered */
const StyledBackButtonContainer = styled(Flex)`
@ -38,7 +38,7 @@ export const BackButtonWithinContainer = ({
callback?: () => void;
onQuitVisible?: () => void;
shouldQuitOnClick?: boolean;
quitI18nMessageArgs: I18nProps<LocalizerToken>;
quitI18nMessageArgs: LocalizerComponentProps<LocalizerToken>;
}) => {
return (
<StyledBackButtonContainer
@ -70,7 +70,7 @@ export const BackButton = ({
callback?: () => void;
onQuitVisible?: () => void;
shouldQuitOnClick?: boolean;
quitI18nMessageArgs: I18nProps<LocalizerToken>;
quitI18nMessageArgs: LocalizerComponentProps<LocalizerToken>;
}) => {
const step = useOnboardStep();
const restorationStep = useOnboardAccountRestorationStep();

@ -18,7 +18,7 @@ import { prepareQRCodeForLightBox } from '../../../util/qrCodes';
import { getCurrentRecoveryPhrase } from '../../../util/storage';
import { QRCodeLogoProps, SessionQRCode } from '../../SessionQRCode';
import { AnimatedFlex } from '../../basic/Flex';
import { I18n } from '../../basic/I18n';
import { Localizer } from '../../basic/Localizer';
import { SessionButtonColor } from '../../basic/SessionButton';
import { SpacerMD, SpacerSM } from '../../basic/Text';
import { CopyToClipboardIcon } from '../../buttons/CopyToClipboardButton';
@ -110,7 +110,7 @@ export const SettingsCategoryRecoveryPassword = () => {
iconSize: 18,
iconColor: 'var(--text-primary-color)',
}}
description={<I18n asTag="p" token="recoveryPasswordDescription" />}
description={<Localizer token="recoveryPasswordDescription" />}
inline={false}
>
<SpacerMD />

@ -1,6 +1,6 @@
import { getConversationController } from '../session/conversations';
import { UserUtils } from '../session/utils';
import type { I18nPropsObject } from '../types/Localizer';
import type { LocalizerComponentPropsObject } from '../types/Localizer';
// to remove after merge with groups
function usAndXOthers(arr: Array<string>) {
@ -12,7 +12,10 @@ function usAndXOthers(arr: Array<string>) {
return { us: false, others: arr };
}
export function getKickedGroupUpdateStr(kicked: Array<string>, groupName: string): I18nPropsObject {
export function getKickedGroupUpdateStr(
kicked: Array<string>,
groupName: string
): LocalizerComponentPropsObject {
const { others, us } = usAndXOthers(kicked);
const othersNames = others.map(
getConversationController().getContactProfileNameOrShortenedPubKey
@ -56,7 +59,7 @@ export function getKickedGroupUpdateStr(kicked: Array<string>, groupName: string
export function getLeftGroupUpdateChangeStr(
left: Array<string>,
_groupName: string
): I18nPropsObject {
): LocalizerComponentPropsObject {
const { others, us } = usAndXOthers(left);
if (left.length !== 1) {
@ -76,7 +79,7 @@ export function getLeftGroupUpdateChangeStr(
export function getJoinedGroupUpdateChangeStr(
joined: Array<string>,
_groupName: string
): I18nPropsObject {
): LocalizerComponentPropsObject {
const { others, us } = usAndXOthers(joined);
const othersNames = others.map(
getConversationController().getContactProfileNameOrShortenedPubKey

@ -46,23 +46,23 @@ export type GetMessageArgs<T extends LocalizerToken> = T extends LocalizerToken
: [T, ArgsRecordExcludingDefaults<T>]
: never;
/** Basic props for all calls of the I18n component */
type I18nBaseProps<T extends LocalizerToken> = {
/** Basic props for all calls of the Localizer component */
type LocalizerComponentBaseProps<T extends LocalizerToken> = {
token: T;
asTag?: ElementType;
className?: string;
};
/** The props for the localization component */
export type I18nProps<T extends LocalizerToken> = T extends LocalizerToken
export type LocalizerComponentProps<T extends LocalizerToken> = T extends LocalizerToken
? DynamicArgs<Dictionary[T]> extends never
? I18nBaseProps<T>
? LocalizerComponentBaseProps<T>
: ArgsRecordExcludingDefaults<T> extends Record<string, never>
? I18nBaseProps<T>
: I18nBaseProps<T> & { args: ArgsRecordExcludingDefaults<T> }
? LocalizerComponentBaseProps<T>
: LocalizerComponentBaseProps<T> & { args: ArgsRecordExcludingDefaults<T> }
: never;
export type I18nPropsObject = I18nProps<LocalizerToken>;
export type LocalizerComponentPropsObject = LocalizerComponentProps<LocalizerToken>;
export type I18nMethods = {
/** @see {@link window.i18n.stripped} */

@ -1,6 +1,6 @@
/** NOTE: Because of docstring limitations changes MUST be manually synced between {@link setupI18n.stripped } and {@link window.i18n.stripped } */
import { deSanitizeHtmlTags, sanitizeArgs } from '../../../components/basic/I18n';
import { deSanitizeHtmlTags, sanitizeArgs } from '../../../components/basic/Localizer';
import { GetMessageArgs, LocalizerDictionary, LocalizerToken } from '../../../types/Localizer';
import { getMessage } from './getMessage';

Loading…
Cancel
Save