fix: fix up string unit tests

pull/3281/head
Audric Ackermann 4 months ago
parent 62a3d4e9be
commit 6616ac4aa7
No known key found for this signature in database

@ -1,8 +1,8 @@
import styled from 'styled-components'; import styled from 'styled-components';
import type { LocalizerComponentProps } from '../../types/localizer';
import { SessionHtmlRenderer } from './SessionHTMLRenderer'; import { SessionHtmlRenderer } from './SessionHTMLRenderer';
import { import {
GetMessageArgs, GetMessageArgs,
LocalizerComponentProps,
MergedLocalizerTokens, MergedLocalizerTokens,
sanitizeArgs, sanitizeArgs,
} from '../../localization/localeTools'; } from '../../localization/localeTools';

@ -1,10 +1,11 @@
import DOMPurify from 'dompurify'; import DOMPurify from 'dompurify';
import { createElement, type ElementType } from 'react'; import { createElement } from 'react';
import { supportedFormattingTags } from './Localizer'; import { supportedFormattingTags } from './Localizer';
import { LocalizerHtmlTag } from '../../localization/localeTools';
type ReceivedProps = { type ReceivedProps = {
html: string; html: string;
tag?: ElementType; tag?: LocalizerHtmlTag;
key?: any; key?: any;
className?: string; className?: string;
}; };

@ -1,7 +1,7 @@
import styled from 'styled-components'; import styled from 'styled-components';
import { forwardRef } from 'react'; import { forwardRef } from 'react';
import { Localizer } from './Localizer'; import { Localizer } from './Localizer';
import type { LocalizerComponentPropsObject } from '../../types/localizer'; import { LocalizerComponentPropsObject } from '../../localization/localeTools';
const StyledI18nSubTextContainer = styled('div')` const StyledI18nSubTextContainer = styled('div')`
font-size: var(--font-size-md); font-size: var(--font-size-md);

@ -17,7 +17,6 @@ import { ReleasedFeatures } from '../../util/releaseFeature';
import { Flex } from '../basic/Flex'; import { Flex } from '../basic/Flex';
import { SpacerMD, TextWithChildren } from '../basic/Text'; import { SpacerMD, TextWithChildren } from '../basic/Text';
import { ExpirableReadableMessage } from './message/message-item/ExpirableReadableMessage'; import { ExpirableReadableMessage } from './message/message-item/ExpirableReadableMessage';
import type { LocalizerComponentPropsObject } from '../../types/localizer';
// eslint-disable-next-line import/order // eslint-disable-next-line import/order
import { ConversationInteraction } from '../../interactions'; import { ConversationInteraction } from '../../interactions';
@ -27,6 +26,7 @@ import { Localizer } from '../basic/Localizer';
import { SessionButtonColor } from '../basic/SessionButton'; import { SessionButtonColor } from '../basic/SessionButton';
import { SessionIcon } from '../icon'; import { SessionIcon } from '../icon';
import { getTimerNotificationStr } from '../../models/timerNotifications'; import { getTimerNotificationStr } from '../../models/timerNotifications';
import { LocalizerComponentPropsObject } from '../../localization/localeTools';
const FollowSettingButton = styled.button` const FollowSettingButton = styled.button`
color: var(--primary-color); color: var(--primary-color);

@ -16,10 +16,10 @@ import {
useSelectedIsGroupV2, useSelectedIsGroupV2,
useSelectedNicknameOrProfileNameOrShortenedPubkey, useSelectedNicknameOrProfileNameOrShortenedPubkey,
} from '../../../../state/selectors/selectedConversation'; } from '../../../../state/selectors/selectedConversation';
import type { LocalizerComponentPropsObject } from '../../../../types/localizer';
import { Localizer } from '../../../basic/Localizer'; import { Localizer } from '../../../basic/Localizer';
import { ExpirableReadableMessage } from './ExpirableReadableMessage'; import { ExpirableReadableMessage } from './ExpirableReadableMessage';
import { NotificationBubble } from './notification-bubble/NotificationBubble'; import { NotificationBubble } from './notification-bubble/NotificationBubble';
import { LocalizerComponentPropsObject } from '../../../../localization/localeTools';
// This component is used to display group updates in the conversation view. // This component is used to display group updates in the conversation view.

@ -45,7 +45,11 @@ export const CallNotification = (props: PropsForCallNotification) => {
isControlMessage={true} isControlMessage={true}
> >
<NotificationBubble iconType={iconType} iconColor={iconColor}> <NotificationBubble iconType={iconType} iconColor={iconColor}>
<Localizer token={notificationTextKey} args={{ name }} /> {notificationTextKey === 'callsInProgress' ? (
<Localizer token={notificationTextKey} />
) : (
<Localizer token={notificationTextKey} args={{ name }} />
)}
</NotificationBubble> </NotificationBubble>
</ExpirableReadableMessage> </ExpirableReadableMessage>
); );

@ -5,7 +5,7 @@ import { PubKey } from '../../../../session/types/PubKey';
import { Localizer } from '../../../basic/Localizer'; import { Localizer } from '../../../basic/Localizer';
import { nativeEmojiData } from '../../../../util/emoji'; import { nativeEmojiData } from '../../../../util/emoji';
import type { LocalizerComponentPropsObject } from '../../../../types/localizer'; import { LocalizerComponentPropsObject } from '../../../../localization/localeTools';
export type TipPosition = 'center' | 'left' | 'right'; export type TipPosition = 'center' | 'left' | 'right';

@ -10,9 +10,9 @@ import { SessionButton, SessionButtonColor, SessionButtonType } from '../basic/S
import { SessionRadioGroup, SessionRadioItems } from '../basic/SessionRadioGroup'; import { SessionRadioGroup, SessionRadioItems } from '../basic/SessionRadioGroup';
import { SpacerLG } from '../basic/Text'; import { SpacerLG } from '../basic/Text';
import { SessionSpinner } from '../loading'; import { SessionSpinner } from '../loading';
import type { LocalizerComponentPropsObject } from '../../types/localizer';
import { StyledI18nSubText } from '../basic/StyledI18nSubText'; import { StyledI18nSubText } from '../basic/StyledI18nSubText';
import { LocalizerComponentPropsObject } from '../../localization/localeTools';
export interface SessionConfirmDialogProps { export interface SessionConfirmDialogProps {
i18nMessage?: LocalizerComponentPropsObject; i18nMessage?: LocalizerComponentPropsObject;

@ -13,7 +13,7 @@ import { Localizer } from '../../basic/Localizer';
import { SessionButton, SessionButtonColor, SessionButtonType } from '../../basic/SessionButton'; import { SessionButton, SessionButtonColor, SessionButtonType } from '../../basic/SessionButton';
import { StyledModalDescriptionContainer } from '../shared/ModalDescriptionContainer'; import { StyledModalDescriptionContainer } from '../shared/ModalDescriptionContainer';
import { BlockOrUnblockModalState } from './BlockOrUnblockModalState'; import { BlockOrUnblockModalState } from './BlockOrUnblockModalState';
import type { LocalizerComponentPropsObject } from '../../../types/localizer'; import { LocalizerComponentPropsObject } from '../../../localization/localeTools';
type ModalState = NonNullable<BlockOrUnblockModalState>; type ModalState = NonNullable<BlockOrUnblockModalState>;

@ -17,7 +17,7 @@ import { deleteDbLocally } from '../../../util/accountManager';
import { Flex } from '../../basic/Flex'; import { Flex } from '../../basic/Flex';
import { SessionButtonColor } from '../../basic/SessionButton'; import { SessionButtonColor } from '../../basic/SessionButton';
import { SessionIconButton } from '../../icon'; 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 */ /** 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)` const StyledBackButtonContainer = styled(Flex)`

@ -50,7 +50,6 @@ import { Storage, setLastProfileUpdateTimestamp } from '../util/storage';
import { UserGroupsWrapperActions } from '../webworker/workers/browser/libsession_worker_interface'; import { UserGroupsWrapperActions } from '../webworker/workers/browser/libsession_worker_interface';
import { ConversationInteractionStatus, ConversationInteractionType } from './types'; import { ConversationInteractionStatus, ConversationInteractionType } from './types';
import { BlockedNumberController } from '../util'; import { BlockedNumberController } from '../util';
import type { LocalizerComponentPropsObject } from '../types/localizer';
import { sendInviteResponseToGroup } from '../session/sending/group/GroupInviteResponse'; import { sendInviteResponseToGroup } from '../session/sending/group/GroupInviteResponse';
import { NetworkTime } from '../util/NetworkTime'; import { NetworkTime } from '../util/NetworkTime';
import { ClosedGroup } from '../session'; import { ClosedGroup } from '../session';
@ -59,6 +58,7 @@ import { GroupPromote } from '../session/utils/job_runners/jobs/GroupPromoteJob'
import { MessageSender } from '../session/sending'; import { MessageSender } from '../session/sending';
import { StoreGroupRequestFactory } from '../session/apis/snode_api/factories/StoreGroupRequestFactory'; import { StoreGroupRequestFactory } from '../session/apis/snode_api/factories/StoreGroupRequestFactory';
import { DURATION } from '../session/constants'; import { DURATION } from '../session/constants';
import { LocalizerComponentPropsObject } from '../localization/localeTools';
export async function copyPublicKeyByConvoId(convoId: string) { export async function copyPublicKeyByConvoId(convoId: string) {
if (OpenGroupUtils.isOpenGroupV2(convoId)) { if (OpenGroupUtils.isOpenGroupV2(convoId)) {

@ -109,6 +109,14 @@ type MappedToTsTypes<T extends Record<string, DynamicArgStr>> = {
[K in keyof T]: ArgsTypeStrToTypes<T[K]>; [K in keyof T]: ArgsTypeStrToTypes<T[K]>;
}; };
function propsToTuple<T extends MergedLocalizerTokens>(
opts: LocalizerComponentProps<T>
): GetMessageArgs<T> {
return (
isTokenWithArgs(opts.token) ? [opts.token, opts.args] : [opts.token]
) as GetMessageArgs<T>;
}
/** NOTE: Because of docstring limitations changes MUST be manually synced between {@link setupI18n.inEnglish } and {@link window.i18n.inEnglish } */ /** 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. * Retrieves a message string in the {@link en} locale, substituting variables where necessary.
@ -178,6 +186,12 @@ export function stripped<T extends MergedLocalizerTokens>(
return deSanitizeHtmlTags(strippedString, '\u200B'); return deSanitizeHtmlTags(strippedString, '\u200B');
} }
export function strippedWithObj<T extends MergedLocalizerTokens>(
opts: LocalizerComponentProps<T>
): string {
return stripped(...propsToTuple(opts));
}
/** /**
* Sanitizes the args to be used in the i18n function * Sanitizes the args to be used in the i18n function
* @param args The args to sanitize * @param args The args to sanitize
@ -489,6 +503,26 @@ export function localize<T extends MergedLocalizerTokens>(token: T) {
return new LocalizedStringBuilder<T>(token, localeInUse); return new LocalizedStringBuilder<T>(token, localeInUse);
} }
function localizeFromOld<T extends MergedLocalizerTokens>(token: T, args: ArgsFromToken<T>) { export function localizeFromOld<T extends MergedLocalizerTokens>(token: T, args: ArgsFromToken<T>) {
return localize(token).withArgs(args); return localize(token).withArgs(args);
} }
export type LocalizerHtmlTag = 'span' | 'div';
/** Basic props for all calls of the Localizer component */
type LocalizerComponentBaseProps<T extends MergedLocalizerTokens> = {
token: T;
asTag?: LocalizerHtmlTag;
className?: string;
};
/** The props for the localization component */
export type LocalizerComponentProps<T extends MergedLocalizerTokens> =
T extends MergedLocalizerTokens
? ArgsFromToken<T> extends never
? LocalizerComponentBaseProps<T> & { args?: undefined }
: ArgsFromToken<T> extends Record<string, never>
? LocalizerComponentBaseProps<T> & { args?: undefined }
: LocalizerComponentBaseProps<T> & { args: ArgsFromToken<T> }
: never;
export type LocalizerComponentPropsObject = LocalizerComponentProps<MergedLocalizerTokens>;

@ -1,6 +1,6 @@
import { LocalizerComponentPropsObject } from '../localization/localeTools';
import { ConvoHub } from '../session/conversations'; import { ConvoHub } from '../session/conversations';
import { UserUtils } from '../session/utils'; import { UserUtils } from '../session/utils';
import type { LocalizerComponentPropsObject } from '../types/localizer';
function usAndXOthers(arr: Array<string>) { function usAndXOthers(arr: Array<string>) {
const us = UserUtils.getOurPubKeyStrFromCache(); const us = UserUtils.getOurPubKeyStrFromCache();
@ -23,7 +23,7 @@ export function getKickedGroupUpdateStr(
if (us) { if (us) {
switch (others.length) { switch (others.length) {
case 0: case 0:
return { token: 'groupRemovedYouGeneral' }; return { token: 'groupRemovedYouGeneral', args: undefined };
case 1: case 1:
return { token: 'groupRemovedYouTwo', args: { other_name: othersNames[0] } }; return { token: 'groupRemovedYouTwo', args: { other_name: othersNames[0] } };
default: default:
@ -33,7 +33,7 @@ export function getKickedGroupUpdateStr(
switch (othersNames.length) { switch (othersNames.length) {
case 0: case 0:
return { token: 'groupUpdated' }; return { token: 'groupUpdated', args: undefined };
case 1: case 1:
return { token: 'groupRemoved', args: { name: othersNames[0] } }; return { token: 'groupRemoved', args: { name: othersNames[0] } };
case 2: case 2:
@ -63,7 +63,7 @@ export function getLeftGroupUpdateChangeStr(left: Array<string>): LocalizerCompo
} }
return us return us
? { token: 'groupMemberYouLeft' } ? { token: 'groupMemberYouLeft', args: undefined }
: { : {
token: 'groupMemberLeft', token: 'groupMemberLeft',
args: { args: {
@ -85,7 +85,10 @@ export function getJoinedGroupUpdateChangeStr(
if (us) { if (us) {
switch (othersNames.length) { switch (othersNames.length) {
case 0: case 0:
return { token: addedWithHistory ? 'groupInviteYouHistory' : 'groupInviteYou' }; return {
token: addedWithHistory ? 'groupInviteYouHistory' : 'groupInviteYou',
args: undefined,
};
case 1: case 1:
return addedWithHistory return addedWithHistory
? { token: 'groupMemberNewYouHistoryTwo', args: { other_name: othersNames[0] } } ? { token: 'groupMemberNewYouHistoryTwo', args: { other_name: othersNames[0] } }
@ -98,7 +101,7 @@ export function getJoinedGroupUpdateChangeStr(
} }
switch (othersNames.length) { switch (othersNames.length) {
case 0: 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: case 1:
return addedWithHistory return addedWithHistory
? { token: 'groupMemberNewHistory', args: { name: othersNames[0] } } ? { token: 'groupMemberNewHistory', args: { name: othersNames[0] } }
@ -130,7 +133,7 @@ export function getJoinedGroupUpdateChangeStr(
if (us) { if (us) {
switch (othersNames.length) { switch (othersNames.length) {
case 0: case 0:
return { token: 'legacyGroupMemberYouNew' }; return { token: 'legacyGroupMemberYouNew', args: undefined };
case 1: case 1:
return { token: 'legacyGroupMemberNewYouOther', args: { other_name: othersNames[0] } }; return { token: 'legacyGroupMemberNewYouOther', args: { other_name: othersNames[0] } };
default: default:
@ -139,7 +142,7 @@ export function getJoinedGroupUpdateChangeStr(
} }
switch (othersNames.length) { switch (othersNames.length) {
case 0: case 0:
return { token: 'groupUpdated' }; return { token: 'groupUpdated', args: undefined };
case 1: case 1:
return { token: 'legacyGroupMemberNew', args: { name: othersNames[0] } }; return { token: 'legacyGroupMemberNew', args: { name: othersNames[0] } };
case 2: case 2:
@ -170,7 +173,7 @@ export function getPromotedGroupUpdateChangeStr(
if (us) { if (us) {
switch (othersNames.length) { switch (othersNames.length) {
case 0: case 0:
return { token: 'groupPromotedYou' }; return { token: 'groupPromotedYou', args: undefined };
case 1: case 1:
return { token: 'groupPromotedYouTwo', args: { name: othersNames[0] } }; return { token: 'groupPromotedYouTwo', args: { name: othersNames[0] } };
default: default:
@ -179,7 +182,7 @@ export function getPromotedGroupUpdateChangeStr(
} }
switch (othersNames.length) { switch (othersNames.length) {
case 0: case 0:
return { token: 'groupUpdated' }; return { token: 'groupUpdated', args: undefined };
case 1: case 1:
return { token: 'adminPromotedToAdmin', args: { name: othersNames[0] } }; return { token: 'adminPromotedToAdmin', args: { name: othersNames[0] } };
case 2: case 2:
@ -204,9 +207,9 @@ export function getPromotedGroupUpdateChangeStr(
export function getGroupNameChangeStr(newName: string | undefined): LocalizerComponentPropsObject { export function getGroupNameChangeStr(newName: string | undefined): LocalizerComponentPropsObject {
return newName return newName
? { token: 'groupNameNew', args: { group_name: newName } } ? { token: 'groupNameNew', args: { group_name: newName } }
: { token: 'groupNameUpdated' }; : { token: 'groupNameUpdated', args: undefined };
} }
export function getGroupDisplayPictureChangeStr(): LocalizerComponentPropsObject { export function getGroupDisplayPictureChangeStr(): LocalizerComponentPropsObject {
return { token: 'groupDisplayPictureUpdated' }; return { token: 'groupDisplayPictureUpdated', args: undefined };
} }

@ -269,61 +269,39 @@ export class MessageModel extends Backbone.Model<MessageAttributes> {
this.getConversation()?.getNicknameOrRealUsernameOrPlaceholder() || window.i18n('unknown'); this.getConversation()?.getNicknameOrRealUsernameOrPlaceholder() || window.i18n('unknown');
if (groupUpdate.left) { if (groupUpdate.left) {
// @ts-expect-error -- TODO: Fix by using new i18n builder return window.i18n.strippedWithObj(getLeftGroupUpdateChangeStr(groupUpdate.left));
const { token, args } = getLeftGroupUpdateChangeStr(groupUpdate.left, groupName);
// TODO: clean up this typing
return window.i18n.stripped(...[token, args]);
} }
if (groupUpdate.name) { if (groupUpdate.name) {
const result = getGroupNameChangeStr(groupUpdate.name); return window.i18n.strippedWithObj(getGroupNameChangeStr(groupUpdate.name));
if ('args' in result) {
return window.i18n.stripped(...[result.token, result.args]);
}
return window.i18n.stripped(...[result.token]);
} }
if (groupUpdate.avatarChange) { if (groupUpdate.avatarChange) {
const result = getGroupDisplayPictureChangeStr(); return window.i18n.strippedWithObj(getGroupDisplayPictureChangeStr());
return window.i18n.stripped(...[result.token]);
} }
if (groupUpdate.joined?.length) { if (groupUpdate.joined?.length) {
// @ts-expect-error -- TODO: Fix by using new i18n builder const opts = getJoinedGroupUpdateChangeStr(groupUpdate.joined, isGroupV2, false, groupName);
const { token, args } = getJoinedGroupUpdateChangeStr( return window.i18n.strippedWithObj(opts);
groupUpdate.joined,
isGroupV2,
false,
groupName
);
// TODO: clean up this typing
return window.i18n.stripped(...[token, args]);
} }
if (groupUpdate.joinedWithHistory?.length) { if (groupUpdate.joinedWithHistory?.length) {
// @ts-expect-error -- TODO: Fix by using new i18n builder const opts = getJoinedGroupUpdateChangeStr(
const { token, args } = getJoinedGroupUpdateChangeStr(
groupUpdate.joinedWithHistory, groupUpdate.joinedWithHistory,
true, true,
true, true,
groupName groupName
); );
// TODO: clean up this typing return window.i18n.strippedWithObj(opts);
return window.i18n.stripped(...[token, args]);
} }
if (groupUpdate.kicked?.length) { if (groupUpdate.kicked?.length) {
// @ts-expect-error -- TODO: Fix by using new i18n builder const opts = getKickedGroupUpdateStr(groupUpdate.kicked, groupName);
const { token, args } = getKickedGroupUpdateStr(groupUpdate.kicked, groupName); return window.i18n.strippedWithObj(opts);
// TODO: clean up this typing
return window.i18n.stripped(...[token, args]);
} }
if (groupUpdate.promoted?.length) { if (groupUpdate.promoted?.length) {
// @ts-expect-error -- TODO: Fix by using new i18n builder const opts = getPromotedGroupUpdateChangeStr(groupUpdate.promoted);
const { token, args } = getPromotedGroupUpdateChangeStr(groupUpdate.promoted, groupName); return window.i18n.strippedWithObj(opts);
// TODO: clean up this typing
return window.i18n.stripped(...[token, args]);
} }
window.log.warn('did not build a specific change for getDescription of ', groupUpdate); window.log.warn('did not build a specific change for getDescription of ', groupUpdate);
@ -429,10 +407,7 @@ export class MessageModel extends Backbone.Model<MessageAttributes> {
timespanSeconds: expireTimer, timespanSeconds: expireTimer,
}); });
if ('args' in i18nProps) { return window.i18n.strippedWithObj(i18nProps);
return window.i18n.stripped(...[i18nProps.token, i18nProps.args]);
}
return window.i18n.stripped(...[i18nProps.token]);
} }
const body = this.get('body'); const body = this.get('body');
if (body) { if (body) {

@ -5,7 +5,7 @@ import { PubKey } from '../session/types';
import { UserUtils } from '../session/utils'; import { UserUtils } from '../session/utils';
import { TimerOptions } from '../session/disappearing_messages/timerOptions'; import { TimerOptions } from '../session/disappearing_messages/timerOptions';
import { isLegacyDisappearingModeEnabled } from '../session/disappearing_messages/legacy'; import { isLegacyDisappearingModeEnabled } from '../session/disappearing_messages/legacy';
import type { LocalizerComponentPropsObject } from '../types/localizer'; import { LocalizerComponentPropsObject } from '../localization/localeTools';
export function getTimerNotificationStr({ export function getTimerNotificationStr({
expirationMode, expirationMode,

@ -10,7 +10,6 @@ import {
} from '../../../../util/accountManager'; } from '../../../../util/accountManager';
import { TestUtils } from '../../../test-utils'; import { TestUtils } from '../../../test-utils';
import { stubWindow } from '../../../test-utils/utils'; import { stubWindow } from '../../../test-utils/utils';
import { resetLocaleAndTranslationDict } from '../../../../util/i18n/shared';
describe('Onboarding', () => { describe('Onboarding', () => {
const polledDisplayName = 'Hello World'; const polledDisplayName = 'Hello World';
@ -26,7 +25,6 @@ describe('Onboarding', () => {
}); });
afterEach(() => { afterEach(() => {
resetLocaleAndTranslationDict();
Sinon.restore(); Sinon.restore();
}); });

@ -8,7 +8,6 @@ import {
_getSortedConversations, _getSortedConversations,
} from '../../../../state/selectors/conversations'; } from '../../../../state/selectors/conversations';
import { TestUtils } from '../../../test-utils'; import { TestUtils } from '../../../test-utils';
import { resetLocaleAndTranslationDict } from '../../../../util/i18n/shared';
describe('state/selectors/conversations', () => { describe('state/selectors/conversations', () => {
beforeEach(() => { beforeEach(() => {
@ -16,7 +15,6 @@ describe('state/selectors/conversations', () => {
TestUtils.stubI18n(); TestUtils.stubI18n();
}); });
afterEach(() => { afterEach(() => {
resetLocaleAndTranslationDict();
Sinon.restore(); Sinon.restore();
}); });
describe('#getSortedConversationsList', () => { describe('#getSortedConversationsList', () => {

@ -2,16 +2,12 @@
// @ts-nocheck - TODO: add generic type to setupI18n to fix this // @ts-nocheck - TODO: add generic type to setupI18n to fix this
import { expect } from 'chai'; import { expect } from 'chai';
import { initI18n, testDictionary } from './util'; import { initI18n } from './util';
import { resetLocaleAndTranslationDict } from '../../../../../util/i18n/shared';
describe('formatMessageWithArgs', () => { describe('formatMessageWithArgs', () => {
let i18n; let i18n;
beforeEach(() => { beforeEach(() => {
i18n = initI18n(testDictionary); i18n = initI18n();
});
afterEach(() => {
resetLocaleAndTranslationDict();
}); });
it('returns the message with args for a message', () => { it('returns the message with args for a message', () => {

@ -2,46 +2,26 @@
// @ts-nocheck - TODO: add generic type to setupI18n to fix this // @ts-nocheck - TODO: add generic type to setupI18n to fix this
import { expect } from 'chai'; import { expect } from 'chai';
import { initI18n, testDictionary } from './util'; import { initI18n } from './util';
import { resetLocaleAndTranslationDict } from '../../../../../util/i18n/shared';
describe('getMessage', () => { describe('getMessage', () => {
let i18n;
beforeEach(() => {
i18n = initI18n(testDictionary);
});
afterEach(() => {
resetLocaleAndTranslationDict();
});
it('returns the message for a token', () => { it('returns the message for a token', () => {
const message = i18n('greeting', { name: 'Alice' }); const message = initI18n()('searchContacts');
expect(message).to.equal('Hello, Alice!'); expect(message).to.equal('Search Contacts');
}); });
it('returns the message for a plural token', () => { 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'); expect(message).to.equal('2 of 1 match');
}); });
it('returns the message for a token with no args', () => { it('returns the message for a token with no args', () => {
const message = i18n('noArgs'); const message = initI18n()('adminPromote');
expect(message).to.equal('No args'); expect(message).to.equal('Promote Admins');
});
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! <b>Welcome!</b>');
}); });
it('returns the message for a token with a tag and args', () => { it('returns the message for a token with a tag and args', () => {
const message = i18n('argInTag', { name: 'Alice', arg: 'Bob' }); const message = initI18n()('adminPromotedToAdmin', { name: 'Alice' });
expect(message).to.equal('Hello, Alice! <b>Welcome, Bob!</b>'); expect(message).to.equal('<b>Alice</b> was promoted to Admin.');
}); });
}); });

@ -2,46 +2,49 @@
// @ts-nocheck - TODO: add generic type to setupI18n to fix this // @ts-nocheck - TODO: add generic type to setupI18n to fix this
import { expect } from 'chai'; import { expect } from 'chai';
import { initI18n, testDictionary } from './util'; import { initI18n } from './util';
import { resetLocaleAndTranslationDict } from '../../../../../util/i18n/shared';
describe('getRawMessage', () => { describe('getRawMessage', () => {
let i18n;
beforeEach(() => {
i18n = initI18n(testDictionary);
});
afterEach(() => {
resetLocaleAndTranslationDict();
});
it('returns the raw message for a token', () => { it('returns the raw message for a token', () => {
const rawMessage = i18n.getRawMessage('greeting', { name: 'Alice' }); const rawMessage = initI18n().getRawMessage('en', 'adminPromoteDescription', { name: 'Alice' });
expect(rawMessage).to.equal('Hello, {name}!'); expect(rawMessage).to.equal(
'Are you sure you want to promote <b>{name}</b> to admin? Admins cannot be removed.'
);
}); });
it('returns the raw message for a plural token', () => { 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'); expect(rawMessage).to.equal('{found_count} of {count} match');
}); });
it('returns the raw message for a token with no args', () => { it('returns the raw message for a token with no args', () => {
const rawMessage = i18n.getRawMessage('noArgs'); const rawMessage = initI18n().getRawMessage('en', 'adminCannotBeRemoved');
expect(rawMessage).to.equal('No args'); expect(rawMessage).to.equal('Admins cannot be removed.');
}); });
it('returns the raw message for a token with args', () => { it('returns the raw message for a token with args', () => {
const rawMessage = i18n.getRawMessage('args', { name: 'Alice' }); const rawMessage = initI18n().getRawMessage('en', 'adminPromotionFailedDescription', {
expect(rawMessage).to.equal('Hello, {name}!'); 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', () => { it('returns the raw message for a token with a tag', () => {
const rawMessage = i18n.getRawMessage('tag', { name: 'Alice' }); const message = initI18n().getRawMessage('en', 'screenshotTaken', { name: 'Alice' });
expect(rawMessage).to.equal('Hello, {name}! <b>Welcome!</b>'); expect(message).to.equal('<b>{name}</b> took a screenshot.');
}); });
it('returns the raw message for a token with a tag and args', () => { it('returns the raw message for a token with a tag and args', () => {
const rawMessage = i18n.getRawMessage('argInTag', { name: 'Alice', arg: 'Bob' }); const message = initI18n().getRawMessage('en', 'adminPromoteTwoDescription', {
expect(rawMessage).to.equal('Hello, {name}! <b>Welcome, {arg}!</b>'); name: 'Alice',
other_name: 'Bob',
});
expect(message).to.equal(
'Are you sure you want to promote <b>{name}</b> and <b>{other_name}</b> to admin? Admins cannot be removed.'
);
}); });
}); });

@ -1,11 +1,7 @@
import { expect } from 'chai'; import { expect } from 'chai';
import { initI18n } from './util'; import { initI18n } from './util';
import { resetLocaleAndTranslationDict } from '../../../../../util/i18n/shared';
describe('setupI18n', () => { describe('setupI18n', () => {
afterEach(() => {
resetLocaleAndTranslationDict();
});
it('returns setupI18n with all methods defined', () => { it('returns setupI18n with all methods defined', () => {
const setupI18nReturn = initI18n(); const setupI18nReturn = initI18n();
expect(setupI18nReturn).to.be.a('function'); expect(setupI18nReturn).to.be.a('function');

@ -2,45 +2,31 @@
// @ts-nocheck - TODO: add generic type to setupI18n to fix this // @ts-nocheck - TODO: add generic type to setupI18n to fix this
import { expect } from 'chai'; import { expect } from 'chai';
import { initI18n, testDictionary } from './util'; import { initI18n } from './util';
import { resetLocaleAndTranslationDict } from '../../../../../util/i18n/shared';
describe('stripped', () => { describe('stripped', () => {
let i18n;
beforeEach(() => {
i18n = initI18n(testDictionary);
});
afterEach(() => {
resetLocaleAndTranslationDict();
});
it('returns the stripped message for a token', () => { it('returns the stripped message for a token', () => {
const message = i18n.stripped('greeting', { name: 'Alice' }); const message = initI18n().stripped('search');
expect(message).to.equal('Hello, Alice!'); expect(message).to.equal('Search');
}); });
it('returns the stripped message for a plural token', () => { 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'); 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', () => { it('returns the stripped message for a token with the tags stripped', () => {
const message = i18n.stripped('tag', { name: 'Alice' }); const message = initI18n().stripped('messageRequestYouHaveAccepted', { name: 'Alice' });
expect(message).to.equal('Hello, Alice! Welcome!'); expect(message).to.equal('You have accepted the message request from Alice.');
}); });
it('returns the stripped message for a token with the tags stripped', () => { it('returns the stripped message for a token with the tags stripped', () => {
const message = i18n.stripped('argInTag', { name: 'Alice', arg: 'Bob' }); const message = initI18n().stripped('adminPromoteTwoDescription', {
expect(message).to.equal('Hello, Alice! Welcome, Bob!'); 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.'
);
}); });
}); });

@ -1,17 +1,7 @@
import { setupI18n } from '../../../../../util/i18n/i18n'; 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}! <b>Welcome!</b>',
argInTag: 'Hello, {name}! <b>Welcome, {arg}!</b>',
} as const;
export function initI18n() { export function initI18n() {
return setupI18n({ return setupI18n({
// testing crowdinLocale: 'en', // testing
crowdinLocale: 'en',
}); });
} }

@ -1,45 +1,29 @@
import type { ElementType } from 'react'; import type {
import type { ArgsFromToken, MergedLocalizerTokens } from '../localization/localeTools'; ArgsFromToken,
MergedLocalizerTokens,
GetMessageArgs,
LocalizerComponentProps,
} from '../localization/localeTools';
import { CrowdinLocale } from '../localization/constants'; import { CrowdinLocale } from '../localization/constants';
/** Basic props for all calls of the Localizer component */
type LocalizerComponentBaseProps<T extends MergedLocalizerTokens> = {
token: T;
asTag?: ElementType;
className?: string;
};
/** The props for the localization component */
export type LocalizerComponentProps<T extends MergedLocalizerTokens> =
T extends MergedLocalizerTokens
? ArgsFromToken<T> extends never
? LocalizerComponentBaseProps<T>
: ArgsFromToken<T> extends Record<string, never>
? LocalizerComponentBaseProps<T>
: LocalizerComponentBaseProps<T> & { args: ArgsFromToken<T> }
: never;
export type LocalizerComponentPropsObject = LocalizerComponentProps<MergedLocalizerTokens>;
export type I18nMethods = { export type I18nMethods = {
/** @see {@link window.i18n.stripped} */ /** @see {@link window.i18n.stripped} */
stripped: <T extends MergedLocalizerTokens, R extends LocalizerDictionary[T]>( stripped: <T extends MergedLocalizerTokens>(...[token, args]: GetMessageArgs<T>) => string | T;
...[token, args]: GetMessageArgs<T> strippedWithObj: <T extends MergedLocalizerTokens>(
) => R | T; opts: LocalizerComponentProps<T>
) => string | T;
/** @see {@link window.i18n.inEnglish} */ /** @see {@link window.i18n.inEnglish} */
inEnglish: <T extends MergedLocalizerTokens, R extends LocalizerDictionary[T]>( inEnglish: <T extends MergedLocalizerTokens>(...[token, args]: GetMessageArgs<T>) => string | T;
...[token, args]: GetMessageArgs<T>
) => R | T;
/** @see {@link window.i18n.formatMessageWithArgs */ /** @see {@link window.i18n.formatMessageWithArgs */
getRawMessage: <T extends MergedLocalizerTokens>( getRawMessage: <T extends MergedLocalizerTokens>(
crowdinLocale: CrowdinLocale, crowdinLocale: CrowdinLocale,
...[token, args]: GetMessageArgs<T> ...[token, args]: GetMessageArgs<T>
) => string; ) => string | T;
/** @see {@link window.i18n.formatMessageWithArgs} */ /** @see {@link window.i18n.formatMessageWithArgs} */
formatMessageWithArgs: <T extends MergedLocalizerTokens>( formatMessageWithArgs: <T extends MergedLocalizerTokens>(
rawMessage: string, rawMessage: string,
args?: ArgsFromToken<T> args?: ArgsFromToken<T>
) => string; ) => string | T;
}; };
export type SetupI18nReturnType = I18nMethods & export type SetupI18nReturnType = I18nMethods &

@ -7,12 +7,14 @@ import {
inEnglish, inEnglish,
stripped, stripped,
getMessageDefault, getMessageDefault,
strippedWithObj,
} from '../../../localization/localeTools'; } from '../../../localization/localeTools';
const getMessageDefaultCopy: any = getMessageDefault; const getMessageDefaultCopy: any = getMessageDefault;
getMessageDefaultCopy.inEnglish = inEnglish; getMessageDefaultCopy.inEnglish = inEnglish;
getMessageDefaultCopy.stripped = stripped; getMessageDefaultCopy.stripped = stripped;
getMessageDefaultCopy.strippedWithObj = strippedWithObj;
getMessageDefaultCopy.getRawMessage = getRawMessage; getMessageDefaultCopy.getRawMessage = getRawMessage;
getMessageDefaultCopy.formatMessageWithArgs = formatMessageWithArgs; getMessageDefaultCopy.formatMessageWithArgs = formatMessageWithArgs;

@ -5,13 +5,6 @@ import { timeLocaleMap } from './timeLocaleMap';
let mappedBrowserLocaleDisplayed = false; let mappedBrowserLocaleDisplayed = false;
let crowdinLocale: CrowdinLocale | undefined; 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. * Logs an i18n message to the console.
* @param message - The message to log. * @param message - The message to log.
@ -88,7 +81,7 @@ export function getBrowserLocale() {
export function setInitialLocale(crowdinLocaleArg: CrowdinLocale) { export function setInitialLocale(crowdinLocaleArg: CrowdinLocale) {
if (crowdinLocale) { if (crowdinLocale) {
throw new Error('setInitialLocale: crowdinLocale is already init'); i18nLog('setInitialLocale: crowdinLocale is already init');
} }
crowdinLocale = crowdinLocaleArg; crowdinLocale = crowdinLocaleArg;
} }

2
ts/window.d.ts vendored

@ -78,6 +78,8 @@ declare global {
*/ */
stripped: I18nMethods['stripped']; 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 } */ /** 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. * Retrieves a message string in the {@link en} locale, substituting variables where necessary.

Loading…
Cancel
Save