From 6616ac4aa7e1a7ef79bd2e4c2e420cb895b41788 Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Fri, 3 Jan 2025 16:51:16 +1100 Subject: [PATCH] fix: fix up string unit tests --- ts/components/basic/Localizer.tsx | 2 +- ts/components/basic/SessionHTMLRenderer.tsx | 5 +- ts/components/basic/StyledI18nSubText.tsx | 2 +- .../conversation/TimerNotification.tsx | 2 +- .../message-item/GroupUpdateMessage.tsx | 2 +- .../notification-bubble/CallNotification.tsx | 6 ++- .../message/reactions/ReactionPopup.tsx | 2 +- ts/components/dialog/SessionConfirm.tsx | 2 +- .../blockOrUnblock/BlockOrUnblockDialog.tsx | 2 +- .../registration/components/BackButton.tsx | 2 +- ts/interactions/conversationInteractions.ts | 2 +- ts/localization/localeTools.ts | 36 +++++++++++++- ts/models/groupUpdate.ts | 27 +++++----- ts/models/message.ts | 49 +++++-------------- ts/models/timerNotifications.ts | 2 +- .../unit/onboarding/Onboarding_test.ts | 2 - .../unit/selectors/conversations_test.ts | 2 - .../utils/i18n/formatMessageWithArgs_test.ts | 8 +-- .../unit/utils/i18n/getMessage_test.ts | 36 +++----------- .../unit/utils/i18n/getRawMessage_test.ts | 47 +++++++++--------- .../session/unit/utils/i18n/setupI18n_test.ts | 4 -- .../session/unit/utils/i18n/stripped_test.ts | 40 +++++---------- ts/test/session/unit/utils/i18n/util.ts | 12 +---- ts/types/localizer.d.ts | 42 +++++----------- ts/util/i18n/functions/getMessage.ts | 2 + ts/util/i18n/shared.ts | 9 +--- ts/window.d.ts | 2 + 27 files changed, 147 insertions(+), 202 deletions(-) diff --git a/ts/components/basic/Localizer.tsx b/ts/components/basic/Localizer.tsx index 249755834..ca28132ba 100644 --- a/ts/components/basic/Localizer.tsx +++ b/ts/components/basic/Localizer.tsx @@ -1,8 +1,8 @@ import styled from 'styled-components'; -import type { LocalizerComponentProps } from '../../types/localizer'; import { SessionHtmlRenderer } from './SessionHTMLRenderer'; import { GetMessageArgs, + LocalizerComponentProps, MergedLocalizerTokens, sanitizeArgs, } from '../../localization/localeTools'; diff --git a/ts/components/basic/SessionHTMLRenderer.tsx b/ts/components/basic/SessionHTMLRenderer.tsx index f6ad7334d..588782f49 100644 --- a/ts/components/basic/SessionHTMLRenderer.tsx +++ b/ts/components/basic/SessionHTMLRenderer.tsx @@ -1,10 +1,11 @@ import DOMPurify from 'dompurify'; -import { createElement, type ElementType } from 'react'; +import { createElement } from 'react'; import { supportedFormattingTags } from './Localizer'; +import { LocalizerHtmlTag } from '../../localization/localeTools'; type ReceivedProps = { html: string; - tag?: ElementType; + tag?: LocalizerHtmlTag; key?: any; className?: string; }; diff --git a/ts/components/basic/StyledI18nSubText.tsx b/ts/components/basic/StyledI18nSubText.tsx index 1bb6dcd52..70c1bb3fa 100644 --- a/ts/components/basic/StyledI18nSubText.tsx +++ b/ts/components/basic/StyledI18nSubText.tsx @@ -1,7 +1,7 @@ import styled from 'styled-components'; import { forwardRef } from 'react'; import { Localizer } from './Localizer'; -import type { LocalizerComponentPropsObject } from '../../types/localizer'; +import { LocalizerComponentPropsObject } from '../../localization/localeTools'; const StyledI18nSubTextContainer = styled('div')` font-size: var(--font-size-md); diff --git a/ts/components/conversation/TimerNotification.tsx b/ts/components/conversation/TimerNotification.tsx index 9ea5423b2..23fa18c50 100644 --- a/ts/components/conversation/TimerNotification.tsx +++ b/ts/components/conversation/TimerNotification.tsx @@ -17,7 +17,6 @@ import { ReleasedFeatures } from '../../util/releaseFeature'; import { Flex } from '../basic/Flex'; import { SpacerMD, TextWithChildren } from '../basic/Text'; import { ExpirableReadableMessage } from './message/message-item/ExpirableReadableMessage'; -import type { LocalizerComponentPropsObject } from '../../types/localizer'; // eslint-disable-next-line import/order import { ConversationInteraction } from '../../interactions'; @@ -27,6 +26,7 @@ import { Localizer } from '../basic/Localizer'; import { SessionButtonColor } from '../basic/SessionButton'; import { SessionIcon } from '../icon'; import { getTimerNotificationStr } from '../../models/timerNotifications'; +import { LocalizerComponentPropsObject } from '../../localization/localeTools'; const FollowSettingButton = styled.button` color: var(--primary-color); diff --git a/ts/components/conversation/message/message-item/GroupUpdateMessage.tsx b/ts/components/conversation/message/message-item/GroupUpdateMessage.tsx index 541565044..7187d02d6 100644 --- a/ts/components/conversation/message/message-item/GroupUpdateMessage.tsx +++ b/ts/components/conversation/message/message-item/GroupUpdateMessage.tsx @@ -16,10 +16,10 @@ import { useSelectedIsGroupV2, useSelectedNicknameOrProfileNameOrShortenedPubkey, } from '../../../../state/selectors/selectedConversation'; -import type { LocalizerComponentPropsObject } from '../../../../types/localizer'; import { Localizer } from '../../../basic/Localizer'; import { ExpirableReadableMessage } from './ExpirableReadableMessage'; import { NotificationBubble } from './notification-bubble/NotificationBubble'; +import { LocalizerComponentPropsObject } from '../../../../localization/localeTools'; // This component is used to display group updates in the conversation view. diff --git a/ts/components/conversation/message/message-item/notification-bubble/CallNotification.tsx b/ts/components/conversation/message/message-item/notification-bubble/CallNotification.tsx index dd89a98da..bc624d625 100644 --- a/ts/components/conversation/message/message-item/notification-bubble/CallNotification.tsx +++ b/ts/components/conversation/message/message-item/notification-bubble/CallNotification.tsx @@ -45,7 +45,11 @@ export const CallNotification = (props: PropsForCallNotification) => { isControlMessage={true} > - + {notificationTextKey === 'callsInProgress' ? ( + + ) : ( + + )} ); diff --git a/ts/components/conversation/message/reactions/ReactionPopup.tsx b/ts/components/conversation/message/reactions/ReactionPopup.tsx index 1ba3cbae1..85561f8fa 100644 --- a/ts/components/conversation/message/reactions/ReactionPopup.tsx +++ b/ts/components/conversation/message/reactions/ReactionPopup.tsx @@ -5,7 +5,7 @@ import { PubKey } from '../../../../session/types/PubKey'; import { Localizer } from '../../../basic/Localizer'; import { nativeEmojiData } from '../../../../util/emoji'; -import type { LocalizerComponentPropsObject } from '../../../../types/localizer'; +import { LocalizerComponentPropsObject } from '../../../../localization/localeTools'; export type TipPosition = 'center' | 'left' | 'right'; diff --git a/ts/components/dialog/SessionConfirm.tsx b/ts/components/dialog/SessionConfirm.tsx index 2657cfa2a..be6da4866 100644 --- a/ts/components/dialog/SessionConfirm.tsx +++ b/ts/components/dialog/SessionConfirm.tsx @@ -10,9 +10,9 @@ import { SessionButton, SessionButtonColor, SessionButtonType } from '../basic/S import { SessionRadioGroup, SessionRadioItems } from '../basic/SessionRadioGroup'; import { SpacerLG } from '../basic/Text'; import { SessionSpinner } from '../loading'; -import type { LocalizerComponentPropsObject } from '../../types/localizer'; import { StyledI18nSubText } from '../basic/StyledI18nSubText'; +import { LocalizerComponentPropsObject } from '../../localization/localeTools'; export interface SessionConfirmDialogProps { i18nMessage?: LocalizerComponentPropsObject; diff --git a/ts/components/dialog/blockOrUnblock/BlockOrUnblockDialog.tsx b/ts/components/dialog/blockOrUnblock/BlockOrUnblockDialog.tsx index 7069beebd..93c49ec08 100644 --- a/ts/components/dialog/blockOrUnblock/BlockOrUnblockDialog.tsx +++ b/ts/components/dialog/blockOrUnblock/BlockOrUnblockDialog.tsx @@ -13,7 +13,7 @@ import { Localizer } from '../../basic/Localizer'; import { SessionButton, SessionButtonColor, SessionButtonType } from '../../basic/SessionButton'; import { StyledModalDescriptionContainer } from '../shared/ModalDescriptionContainer'; import { BlockOrUnblockModalState } from './BlockOrUnblockModalState'; -import type { LocalizerComponentPropsObject } from '../../../types/localizer'; +import { LocalizerComponentPropsObject } from '../../../localization/localeTools'; type ModalState = NonNullable; diff --git a/ts/components/registration/components/BackButton.tsx b/ts/components/registration/components/BackButton.tsx index fcc36e6ac..4c8ec63b7 100644 --- a/ts/components/registration/components/BackButton.tsx +++ b/ts/components/registration/components/BackButton.tsx @@ -17,7 +17,7 @@ import { deleteDbLocally } from '../../../util/accountManager'; import { Flex } from '../../basic/Flex'; import { SessionButtonColor } from '../../basic/SessionButton'; import { SessionIconButton } from '../../icon'; -import type { LocalizerComponentPropsObject } from '../../../types/localizer'; +import { LocalizerComponentPropsObject } from '../../../localization/localeTools'; /** 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)` diff --git a/ts/interactions/conversationInteractions.ts b/ts/interactions/conversationInteractions.ts index a2fab258e..39fad4de8 100644 --- a/ts/interactions/conversationInteractions.ts +++ b/ts/interactions/conversationInteractions.ts @@ -50,7 +50,6 @@ import { Storage, setLastProfileUpdateTimestamp } from '../util/storage'; import { UserGroupsWrapperActions } from '../webworker/workers/browser/libsession_worker_interface'; import { ConversationInteractionStatus, ConversationInteractionType } from './types'; import { BlockedNumberController } from '../util'; -import type { LocalizerComponentPropsObject } from '../types/localizer'; import { sendInviteResponseToGroup } from '../session/sending/group/GroupInviteResponse'; import { NetworkTime } from '../util/NetworkTime'; import { ClosedGroup } from '../session'; @@ -59,6 +58,7 @@ import { GroupPromote } from '../session/utils/job_runners/jobs/GroupPromoteJob' import { MessageSender } from '../session/sending'; import { StoreGroupRequestFactory } from '../session/apis/snode_api/factories/StoreGroupRequestFactory'; import { DURATION } from '../session/constants'; +import { LocalizerComponentPropsObject } from '../localization/localeTools'; export async function copyPublicKeyByConvoId(convoId: string) { if (OpenGroupUtils.isOpenGroupV2(convoId)) { diff --git a/ts/localization/localeTools.ts b/ts/localization/localeTools.ts index ef64bddb4..23eca9079 100644 --- a/ts/localization/localeTools.ts +++ b/ts/localization/localeTools.ts @@ -109,6 +109,14 @@ type MappedToTsTypes> = { [K in keyof T]: ArgsTypeStrToTypes; }; +function propsToTuple( + opts: LocalizerComponentProps +): GetMessageArgs { + return ( + isTokenWithArgs(opts.token) ? [opts.token, opts.args] : [opts.token] + ) as GetMessageArgs; +} + /** NOTE: Because of docstring limitations changes MUST be manually synced between {@link setupI18n.inEnglish } and {@link window.i18n.inEnglish } */ /** * Retrieves a message string in the {@link en} locale, substituting variables where necessary. @@ -178,6 +186,12 @@ export function stripped( return deSanitizeHtmlTags(strippedString, '\u200B'); } +export function strippedWithObj( + opts: LocalizerComponentProps +): string { + return stripped(...propsToTuple(opts)); +} + /** * Sanitizes the args to be used in the i18n function * @param args The args to sanitize @@ -489,6 +503,26 @@ export function localize(token: T) { return new LocalizedStringBuilder(token, localeInUse); } -function localizeFromOld(token: T, args: ArgsFromToken) { +export function localizeFromOld(token: T, args: ArgsFromToken) { return localize(token).withArgs(args); } + +export type LocalizerHtmlTag = 'span' | 'div'; +/** Basic props for all calls of the Localizer component */ +type LocalizerComponentBaseProps = { + token: T; + asTag?: LocalizerHtmlTag; + className?: string; +}; + +/** The props for the localization component */ +export type LocalizerComponentProps = + T extends MergedLocalizerTokens + ? ArgsFromToken extends never + ? LocalizerComponentBaseProps & { args?: undefined } + : ArgsFromToken extends Record + ? LocalizerComponentBaseProps & { args?: undefined } + : LocalizerComponentBaseProps & { args: ArgsFromToken } + : never; + +export type LocalizerComponentPropsObject = LocalizerComponentProps; diff --git a/ts/models/groupUpdate.ts b/ts/models/groupUpdate.ts index 0ff177582..7c1e9766c 100644 --- a/ts/models/groupUpdate.ts +++ b/ts/models/groupUpdate.ts @@ -1,6 +1,6 @@ +import { LocalizerComponentPropsObject } from '../localization/localeTools'; import { ConvoHub } from '../session/conversations'; import { UserUtils } from '../session/utils'; -import type { LocalizerComponentPropsObject } from '../types/localizer'; function usAndXOthers(arr: Array) { const us = UserUtils.getOurPubKeyStrFromCache(); @@ -23,7 +23,7 @@ export function getKickedGroupUpdateStr( if (us) { switch (others.length) { case 0: - return { token: 'groupRemovedYouGeneral' }; + return { token: 'groupRemovedYouGeneral', args: undefined }; case 1: return { token: 'groupRemovedYouTwo', args: { other_name: othersNames[0] } }; default: @@ -33,7 +33,7 @@ export function getKickedGroupUpdateStr( switch (othersNames.length) { case 0: - return { token: 'groupUpdated' }; + return { token: 'groupUpdated', args: undefined }; case 1: return { token: 'groupRemoved', args: { name: othersNames[0] } }; case 2: @@ -63,7 +63,7 @@ export function getLeftGroupUpdateChangeStr(left: Array): LocalizerCompo } return us - ? { token: 'groupMemberYouLeft' } + ? { token: 'groupMemberYouLeft', args: undefined } : { token: 'groupMemberLeft', args: { @@ -85,7 +85,10 @@ export function getJoinedGroupUpdateChangeStr( if (us) { switch (othersNames.length) { case 0: - return { token: addedWithHistory ? 'groupInviteYouHistory' : 'groupInviteYou' }; + return { + token: addedWithHistory ? 'groupInviteYouHistory' : 'groupInviteYou', + args: undefined, + }; case 1: return addedWithHistory ? { token: 'groupMemberNewYouHistoryTwo', args: { other_name: othersNames[0] } } @@ -98,7 +101,7 @@ export function getJoinedGroupUpdateChangeStr( } switch (othersNames.length) { case 0: - return { token: 'groupUpdated' }; // this is an invalid case, but well. + return { token: 'groupUpdated', args: undefined }; // this is an invalid case, but well. case 1: return addedWithHistory ? { token: 'groupMemberNewHistory', args: { name: othersNames[0] } } @@ -130,7 +133,7 @@ export function getJoinedGroupUpdateChangeStr( if (us) { switch (othersNames.length) { case 0: - return { token: 'legacyGroupMemberYouNew' }; + return { token: 'legacyGroupMemberYouNew', args: undefined }; case 1: return { token: 'legacyGroupMemberNewYouOther', args: { other_name: othersNames[0] } }; default: @@ -139,7 +142,7 @@ export function getJoinedGroupUpdateChangeStr( } switch (othersNames.length) { case 0: - return { token: 'groupUpdated' }; + return { token: 'groupUpdated', args: undefined }; case 1: return { token: 'legacyGroupMemberNew', args: { name: othersNames[0] } }; case 2: @@ -170,7 +173,7 @@ export function getPromotedGroupUpdateChangeStr( if (us) { switch (othersNames.length) { case 0: - return { token: 'groupPromotedYou' }; + return { token: 'groupPromotedYou', args: undefined }; case 1: return { token: 'groupPromotedYouTwo', args: { name: othersNames[0] } }; default: @@ -179,7 +182,7 @@ export function getPromotedGroupUpdateChangeStr( } switch (othersNames.length) { case 0: - return { token: 'groupUpdated' }; + return { token: 'groupUpdated', args: undefined }; case 1: return { token: 'adminPromotedToAdmin', args: { name: othersNames[0] } }; case 2: @@ -204,9 +207,9 @@ export function getPromotedGroupUpdateChangeStr( export function getGroupNameChangeStr(newName: string | undefined): LocalizerComponentPropsObject { return newName ? { token: 'groupNameNew', args: { group_name: newName } } - : { token: 'groupNameUpdated' }; + : { token: 'groupNameUpdated', args: undefined }; } export function getGroupDisplayPictureChangeStr(): LocalizerComponentPropsObject { - return { token: 'groupDisplayPictureUpdated' }; + return { token: 'groupDisplayPictureUpdated', args: undefined }; } diff --git a/ts/models/message.ts b/ts/models/message.ts index 1eb481255..708823e73 100644 --- a/ts/models/message.ts +++ b/ts/models/message.ts @@ -269,61 +269,39 @@ export class MessageModel extends Backbone.Model { this.getConversation()?.getNicknameOrRealUsernameOrPlaceholder() || window.i18n('unknown'); if (groupUpdate.left) { - // @ts-expect-error -- TODO: Fix by using new i18n builder - const { token, args } = getLeftGroupUpdateChangeStr(groupUpdate.left, groupName); - // TODO: clean up this typing - return window.i18n.stripped(...[token, args]); + return window.i18n.strippedWithObj(getLeftGroupUpdateChangeStr(groupUpdate.left)); } if (groupUpdate.name) { - const result = getGroupNameChangeStr(groupUpdate.name); - - if ('args' in result) { - return window.i18n.stripped(...[result.token, result.args]); - } - return window.i18n.stripped(...[result.token]); + return window.i18n.strippedWithObj(getGroupNameChangeStr(groupUpdate.name)); } if (groupUpdate.avatarChange) { - const result = getGroupDisplayPictureChangeStr(); - return window.i18n.stripped(...[result.token]); + return window.i18n.strippedWithObj(getGroupDisplayPictureChangeStr()); } if (groupUpdate.joined?.length) { - // @ts-expect-error -- TODO: Fix by using new i18n builder - const { token, args } = getJoinedGroupUpdateChangeStr( - groupUpdate.joined, - isGroupV2, - false, - groupName - ); - // TODO: clean up this typing - return window.i18n.stripped(...[token, args]); + const opts = getJoinedGroupUpdateChangeStr(groupUpdate.joined, isGroupV2, false, groupName); + return window.i18n.strippedWithObj(opts); } if (groupUpdate.joinedWithHistory?.length) { - // @ts-expect-error -- TODO: Fix by using new i18n builder - const { token, args } = getJoinedGroupUpdateChangeStr( + const opts = getJoinedGroupUpdateChangeStr( groupUpdate.joinedWithHistory, true, true, groupName ); - // TODO: clean up this typing - return window.i18n.stripped(...[token, args]); + return window.i18n.strippedWithObj(opts); } if (groupUpdate.kicked?.length) { - // @ts-expect-error -- TODO: Fix by using new i18n builder - const { token, args } = getKickedGroupUpdateStr(groupUpdate.kicked, groupName); - // TODO: clean up this typing - return window.i18n.stripped(...[token, args]); + const opts = getKickedGroupUpdateStr(groupUpdate.kicked, groupName); + return window.i18n.strippedWithObj(opts); } if (groupUpdate.promoted?.length) { - // @ts-expect-error -- TODO: Fix by using new i18n builder - const { token, args } = getPromotedGroupUpdateChangeStr(groupUpdate.promoted, groupName); - // TODO: clean up this typing - return window.i18n.stripped(...[token, args]); + const opts = getPromotedGroupUpdateChangeStr(groupUpdate.promoted); + return window.i18n.strippedWithObj(opts); } window.log.warn('did not build a specific change for getDescription of ', groupUpdate); @@ -429,10 +407,7 @@ export class MessageModel extends Backbone.Model { timespanSeconds: expireTimer, }); - if ('args' in i18nProps) { - return window.i18n.stripped(...[i18nProps.token, i18nProps.args]); - } - return window.i18n.stripped(...[i18nProps.token]); + return window.i18n.strippedWithObj(i18nProps); } const body = this.get('body'); if (body) { diff --git a/ts/models/timerNotifications.ts b/ts/models/timerNotifications.ts index 77ee40628..43c7186be 100644 --- a/ts/models/timerNotifications.ts +++ b/ts/models/timerNotifications.ts @@ -5,7 +5,7 @@ import { PubKey } from '../session/types'; import { UserUtils } from '../session/utils'; import { TimerOptions } from '../session/disappearing_messages/timerOptions'; import { isLegacyDisappearingModeEnabled } from '../session/disappearing_messages/legacy'; -import type { LocalizerComponentPropsObject } from '../types/localizer'; +import { LocalizerComponentPropsObject } from '../localization/localeTools'; export function getTimerNotificationStr({ expirationMode, diff --git a/ts/test/session/unit/onboarding/Onboarding_test.ts b/ts/test/session/unit/onboarding/Onboarding_test.ts index bbe8deb1a..81653687c 100644 --- a/ts/test/session/unit/onboarding/Onboarding_test.ts +++ b/ts/test/session/unit/onboarding/Onboarding_test.ts @@ -10,7 +10,6 @@ import { } from '../../../../util/accountManager'; import { TestUtils } from '../../../test-utils'; import { stubWindow } from '../../../test-utils/utils'; -import { resetLocaleAndTranslationDict } from '../../../../util/i18n/shared'; describe('Onboarding', () => { const polledDisplayName = 'Hello World'; @@ -26,7 +25,6 @@ describe('Onboarding', () => { }); afterEach(() => { - resetLocaleAndTranslationDict(); Sinon.restore(); }); diff --git a/ts/test/session/unit/selectors/conversations_test.ts b/ts/test/session/unit/selectors/conversations_test.ts index 8bf08156b..f1739d493 100644 --- a/ts/test/session/unit/selectors/conversations_test.ts +++ b/ts/test/session/unit/selectors/conversations_test.ts @@ -8,7 +8,6 @@ import { _getSortedConversations, } from '../../../../state/selectors/conversations'; import { TestUtils } from '../../../test-utils'; -import { resetLocaleAndTranslationDict } from '../../../../util/i18n/shared'; describe('state/selectors/conversations', () => { beforeEach(() => { @@ -16,7 +15,6 @@ describe('state/selectors/conversations', () => { TestUtils.stubI18n(); }); afterEach(() => { - resetLocaleAndTranslationDict(); Sinon.restore(); }); describe('#getSortedConversationsList', () => { diff --git a/ts/test/session/unit/utils/i18n/formatMessageWithArgs_test.ts b/ts/test/session/unit/utils/i18n/formatMessageWithArgs_test.ts index e031077a8..0fade2304 100644 --- a/ts/test/session/unit/utils/i18n/formatMessageWithArgs_test.ts +++ b/ts/test/session/unit/utils/i18n/formatMessageWithArgs_test.ts @@ -2,16 +2,12 @@ // @ts-nocheck - TODO: add generic type to setupI18n to fix this import { expect } from 'chai'; -import { initI18n, testDictionary } from './util'; -import { resetLocaleAndTranslationDict } from '../../../../../util/i18n/shared'; +import { initI18n } from './util'; describe('formatMessageWithArgs', () => { let i18n; beforeEach(() => { - i18n = initI18n(testDictionary); - }); - afterEach(() => { - resetLocaleAndTranslationDict(); + i18n = initI18n(); }); it('returns the message with args for a message', () => { diff --git a/ts/test/session/unit/utils/i18n/getMessage_test.ts b/ts/test/session/unit/utils/i18n/getMessage_test.ts index 77dced000..9e485209c 100644 --- a/ts/test/session/unit/utils/i18n/getMessage_test.ts +++ b/ts/test/session/unit/utils/i18n/getMessage_test.ts @@ -2,46 +2,26 @@ // @ts-nocheck - TODO: add generic type to setupI18n to fix this import { expect } from 'chai'; -import { initI18n, testDictionary } from './util'; -import { resetLocaleAndTranslationDict } from '../../../../../util/i18n/shared'; +import { initI18n } from './util'; describe('getMessage', () => { - let i18n; - beforeEach(() => { - i18n = initI18n(testDictionary); - }); - - afterEach(() => { - resetLocaleAndTranslationDict(); - }); - it('returns the message for a token', () => { - const message = i18n('greeting', { name: 'Alice' }); - expect(message).to.equal('Hello, Alice!'); + const message = initI18n()('searchContacts'); + expect(message).to.equal('Search Contacts'); }); it('returns the message for a plural token', () => { - const message = i18n('search', { count: 1, found_count: 2 }); + const message = initI18n()('searchMatches', { count: 1, found_count: 2 }); expect(message).to.equal('2 of 1 match'); }); it('returns the message for a token with no args', () => { - const message = i18n('noArgs'); - expect(message).to.equal('No args'); - }); - - it('returns the message for a token with args', () => { - const message = i18n('args', { name: 'Alice' }); - expect(message).to.equal('Hello, Alice!'); - }); - - it('returns the message for a token with a tag', () => { - const message = i18n('tag', { name: 'Alice' }); - expect(message).to.equal('Hello, Alice! Welcome!'); + const message = initI18n()('adminPromote'); + expect(message).to.equal('Promote Admins'); }); it('returns the message for a token with a tag and args', () => { - const message = i18n('argInTag', { name: 'Alice', arg: 'Bob' }); - expect(message).to.equal('Hello, Alice! Welcome, Bob!'); + const message = initI18n()('adminPromotedToAdmin', { name: 'Alice' }); + expect(message).to.equal('Alice was promoted to Admin.'); }); }); diff --git a/ts/test/session/unit/utils/i18n/getRawMessage_test.ts b/ts/test/session/unit/utils/i18n/getRawMessage_test.ts index 42cb20742..22f5acc4f 100644 --- a/ts/test/session/unit/utils/i18n/getRawMessage_test.ts +++ b/ts/test/session/unit/utils/i18n/getRawMessage_test.ts @@ -2,46 +2,49 @@ // @ts-nocheck - TODO: add generic type to setupI18n to fix this import { expect } from 'chai'; -import { initI18n, testDictionary } from './util'; -import { resetLocaleAndTranslationDict } from '../../../../../util/i18n/shared'; +import { initI18n } from './util'; describe('getRawMessage', () => { - let i18n; - beforeEach(() => { - i18n = initI18n(testDictionary); - }); - - afterEach(() => { - resetLocaleAndTranslationDict(); - }); - it('returns the raw message for a token', () => { - const rawMessage = i18n.getRawMessage('greeting', { name: 'Alice' }); - expect(rawMessage).to.equal('Hello, {name}!'); + const rawMessage = initI18n().getRawMessage('en', 'adminPromoteDescription', { name: 'Alice' }); + expect(rawMessage).to.equal( + 'Are you sure you want to promote {name} to admin? Admins cannot be removed.' + ); }); it('returns the raw message for a plural token', () => { - const rawMessage = i18n.getRawMessage('search', { count: 1, found_count: 2 }); + const rawMessage = initI18n().getRawMessage('en', 'searchMatches', { + count: 1, + found_count: 2, + }); expect(rawMessage).to.equal('{found_count} of {count} match'); }); it('returns the raw message for a token with no args', () => { - const rawMessage = i18n.getRawMessage('noArgs'); - expect(rawMessage).to.equal('No args'); + const rawMessage = initI18n().getRawMessage('en', 'adminCannotBeRemoved'); + expect(rawMessage).to.equal('Admins cannot be removed.'); }); it('returns the raw message for a token with args', () => { - const rawMessage = i18n.getRawMessage('args', { name: 'Alice' }); - expect(rawMessage).to.equal('Hello, {name}!'); + const rawMessage = initI18n().getRawMessage('en', 'adminPromotionFailedDescription', { + name: 'Alice', + group_name: 'Group', + }); + expect(rawMessage).to.equal('Failed to promote {name} in {group_name}'); }); it('returns the raw message for a token with a tag', () => { - const rawMessage = i18n.getRawMessage('tag', { name: 'Alice' }); - expect(rawMessage).to.equal('Hello, {name}! Welcome!'); + const message = initI18n().getRawMessage('en', 'screenshotTaken', { name: 'Alice' }); + expect(message).to.equal('{name} took a screenshot.'); }); it('returns the raw message for a token with a tag and args', () => { - const rawMessage = i18n.getRawMessage('argInTag', { name: 'Alice', arg: 'Bob' }); - expect(rawMessage).to.equal('Hello, {name}! Welcome, {arg}!'); + const message = initI18n().getRawMessage('en', 'adminPromoteTwoDescription', { + name: 'Alice', + other_name: 'Bob', + }); + expect(message).to.equal( + 'Are you sure you want to promote {name} and {other_name} to admin? Admins cannot be removed.' + ); }); }); diff --git a/ts/test/session/unit/utils/i18n/setupI18n_test.ts b/ts/test/session/unit/utils/i18n/setupI18n_test.ts index dca67d37c..1e749fe95 100644 --- a/ts/test/session/unit/utils/i18n/setupI18n_test.ts +++ b/ts/test/session/unit/utils/i18n/setupI18n_test.ts @@ -1,11 +1,7 @@ import { expect } from 'chai'; import { initI18n } from './util'; -import { resetLocaleAndTranslationDict } from '../../../../../util/i18n/shared'; describe('setupI18n', () => { - afterEach(() => { - resetLocaleAndTranslationDict(); - }); it('returns setupI18n with all methods defined', () => { const setupI18nReturn = initI18n(); expect(setupI18nReturn).to.be.a('function'); diff --git a/ts/test/session/unit/utils/i18n/stripped_test.ts b/ts/test/session/unit/utils/i18n/stripped_test.ts index 70cff0091..80eb52ca8 100644 --- a/ts/test/session/unit/utils/i18n/stripped_test.ts +++ b/ts/test/session/unit/utils/i18n/stripped_test.ts @@ -2,45 +2,31 @@ // @ts-nocheck - TODO: add generic type to setupI18n to fix this import { expect } from 'chai'; -import { initI18n, testDictionary } from './util'; -import { resetLocaleAndTranslationDict } from '../../../../../util/i18n/shared'; +import { initI18n } from './util'; describe('stripped', () => { - let i18n; - beforeEach(() => { - i18n = initI18n(testDictionary); - }); - afterEach(() => { - resetLocaleAndTranslationDict(); - }); - it('returns the stripped message for a token', () => { - const message = i18n.stripped('greeting', { name: 'Alice' }); - expect(message).to.equal('Hello, Alice!'); + const message = initI18n().stripped('search'); + expect(message).to.equal('Search'); }); it('returns the stripped message for a plural token', () => { - const message = i18n.stripped('search', { count: 1, found_count: 2 }); + const message = initI18n().stripped('searchMatches', { count: 1, found_count: 2 }); expect(message).to.equal('2 of 1 match'); }); - it('returns the stripped message for a token with no args', () => { - const message = i18n.stripped('noArgs'); - expect(message).to.equal('No args'); - }); - - it('returns the stripped message for a token with args', () => { - const message = i18n.stripped('args', { name: 'Alice' }); - expect(message).to.equal('Hello, Alice!'); - }); - it('returns the stripped message for a token with the tags stripped', () => { - const message = i18n.stripped('tag', { name: 'Alice' }); - expect(message).to.equal('Hello, Alice! Welcome!'); + const message = initI18n().stripped('messageRequestYouHaveAccepted', { name: 'Alice' }); + expect(message).to.equal('You have accepted the message request from Alice.'); }); it('returns the stripped message for a token with the tags stripped', () => { - const message = i18n.stripped('argInTag', { name: 'Alice', arg: 'Bob' }); - expect(message).to.equal('Hello, Alice! Welcome, Bob!'); + const message = initI18n().stripped('adminPromoteTwoDescription', { + name: 'Alice', + other_name: 'Bob', + }); + expect(message).to.equal( + 'Are you sure you want to promote Alice and Bob to admin? Admins cannot be removed.' + ); }); }); diff --git a/ts/test/session/unit/utils/i18n/util.ts b/ts/test/session/unit/utils/i18n/util.ts index 2b5b13dd4..96388edb5 100644 --- a/ts/test/session/unit/utils/i18n/util.ts +++ b/ts/test/session/unit/utils/i18n/util.ts @@ -1,17 +1,7 @@ import { setupI18n } from '../../../../../util/i18n/i18n'; -export const testDictionary = { - greeting: 'Hello, {name}!', - search: '{found_count} of {count} match', - noArgs: 'No args', - args: 'Hello, {name}!', - tag: 'Hello, {name}! Welcome!', - argInTag: 'Hello, {name}! Welcome, {arg}!', -} as const; - export function initI18n() { return setupI18n({ - // testing - crowdinLocale: 'en', + crowdinLocale: 'en', // testing }); } diff --git a/ts/types/localizer.d.ts b/ts/types/localizer.d.ts index 6864c9502..0b537d0ea 100644 --- a/ts/types/localizer.d.ts +++ b/ts/types/localizer.d.ts @@ -1,45 +1,29 @@ -import type { ElementType } from 'react'; -import type { ArgsFromToken, MergedLocalizerTokens } from '../localization/localeTools'; +import type { + ArgsFromToken, + MergedLocalizerTokens, + GetMessageArgs, + LocalizerComponentProps, +} from '../localization/localeTools'; import { CrowdinLocale } from '../localization/constants'; -/** Basic props for all calls of the Localizer component */ -type LocalizerComponentBaseProps = { - token: T; - asTag?: ElementType; - className?: string; -}; - -/** The props for the localization component */ -export type LocalizerComponentProps = - T extends MergedLocalizerTokens - ? ArgsFromToken extends never - ? LocalizerComponentBaseProps - : ArgsFromToken extends Record - ? LocalizerComponentBaseProps - : LocalizerComponentBaseProps & { args: ArgsFromToken } - : never; - -export type LocalizerComponentPropsObject = LocalizerComponentProps; - export type I18nMethods = { /** @see {@link window.i18n.stripped} */ - stripped: ( - ...[token, args]: GetMessageArgs - ) => R | T; + stripped: (...[token, args]: GetMessageArgs) => string | T; + strippedWithObj: ( + opts: LocalizerComponentProps + ) => string | T; /** @see {@link window.i18n.inEnglish} */ - inEnglish: ( - ...[token, args]: GetMessageArgs - ) => R | T; + inEnglish: (...[token, args]: GetMessageArgs) => string | T; /** @see {@link window.i18n.formatMessageWithArgs */ getRawMessage: ( crowdinLocale: CrowdinLocale, ...[token, args]: GetMessageArgs - ) => string; + ) => string | T; /** @see {@link window.i18n.formatMessageWithArgs} */ formatMessageWithArgs: ( rawMessage: string, args?: ArgsFromToken - ) => string; + ) => string | T; }; export type SetupI18nReturnType = I18nMethods & diff --git a/ts/util/i18n/functions/getMessage.ts b/ts/util/i18n/functions/getMessage.ts index f08d3d188..d0bef4190 100644 --- a/ts/util/i18n/functions/getMessage.ts +++ b/ts/util/i18n/functions/getMessage.ts @@ -7,12 +7,14 @@ import { inEnglish, stripped, getMessageDefault, + strippedWithObj, } from '../../../localization/localeTools'; const getMessageDefaultCopy: any = getMessageDefault; getMessageDefaultCopy.inEnglish = inEnglish; getMessageDefaultCopy.stripped = stripped; +getMessageDefaultCopy.strippedWithObj = strippedWithObj; getMessageDefaultCopy.getRawMessage = getRawMessage; getMessageDefaultCopy.formatMessageWithArgs = formatMessageWithArgs; diff --git a/ts/util/i18n/shared.ts b/ts/util/i18n/shared.ts index 736809ba9..2b9491a72 100644 --- a/ts/util/i18n/shared.ts +++ b/ts/util/i18n/shared.ts @@ -5,13 +5,6 @@ import { timeLocaleMap } from './timeLocaleMap'; let mappedBrowserLocaleDisplayed = false; let crowdinLocale: CrowdinLocale | undefined; -/** - * Only exported for testing, reset the dictionary to use for translations and the locale set - */ -export function resetLocaleAndTranslationDict() { - crowdinLocale = undefined; -} - /** * Logs an i18n message to the console. * @param message - The message to log. @@ -88,7 +81,7 @@ export function getBrowserLocale() { export function setInitialLocale(crowdinLocaleArg: CrowdinLocale) { if (crowdinLocale) { - throw new Error('setInitialLocale: crowdinLocale is already init'); + i18nLog('setInitialLocale: crowdinLocale is already init'); } crowdinLocale = crowdinLocaleArg; } diff --git a/ts/window.d.ts b/ts/window.d.ts index 8abb44aba..705c72dd7 100644 --- a/ts/window.d.ts +++ b/ts/window.d.ts @@ -78,6 +78,8 @@ declare global { */ stripped: I18nMethods['stripped']; + strippedWithObj: I18nMethods['strippedWithObj']; + /** NOTE: Because of docstring limitations changes MUST be manually synced between {@link setupI18n.inEnglish } and {@link window.i18n.inEnglish } */ /** * Retrieves a message string in the {@link en} locale, substituting variables where necessary.