Fixed issue when user display names containing emoji are translated

Created a <Twemojify> component which can be passed through translation as a component, which then twemojify's the user name after translation
This commit is contained in:
Dylan 2022-08-07 12:06:28 +09:30
parent 4e0707953c
commit 7627d43e92
6 changed files with 158 additions and 95 deletions

View file

@ -262,10 +262,10 @@
"jump_unread": "Jump to unread messages", "jump_unread": "Jump to unread messages",
"mark_read": "Mark as read", "mark_read": "Mark as read",
"jump_latest": "Jump to latest", "jump_latest": "Jump to latest",
"user_typing_one": "<bold>{{user_one}}</bold> is typing...", "user_typing_1": "<bold><user_one/></bold> is typing...",
"user_typing_two": "<bold>{{user_one}}</bold> and <bold>{{user_two}}</bold> are typing...", "user_typing_2": "<bold><user_one/></bold> and <bold><user_two/></bold> are typing...",
"user_typing_three": "<bold>{{user_one}}</bold>, <bold>{{user_two}}</bold> and <bold>{{user_three}}</bold> are typing...", "user_typing_3": "<bold><user_one/></bold>, <bold><user_two/></bold> and <bold><user_three/></bold> are typing...",
"user_typing_four": "<bold>{{user_one}}</bold>, <bold>{{user_two}}</bold>, <bold>{{user_three}}</bold> and <bold>{{user_four}}</bold> are typing...", "user_typing_4": "<bold><user_one/></bold>, <bold><user_two/></bold>, <bold><user_three/></bold> and <bold><user_four/></bold> are typing...",
"user_typing_other": "<bold>Several people</bold> are typing..." "user_typing_other": "<bold>Several people</bold> are typing..."
}, },
"RoomViewContent": { "RoomViewContent": {
@ -287,20 +287,23 @@
"room_settings_subtitle": "room settings" "room_settings_subtitle": "room settings"
}, },
"RoomCommon": { "RoomCommon": {
"user_joined": "<bold>{{user_name}}</bold> joined the room", "user_joined": "<bold><user/></bold> joined the room",
"user_left": "<bold>{{user_name}}</bold> left the room", "user_left": "<bold><user/></bold> left the room",
"user_invited": "<bold>{{inviter_name}}</bold> invited <bold>{{user_name}}</bold>", "user_left_reason": "<bold><user/></bold> left the room: <reason/>",
"invite_cancelled": "<bold>{{inviter_name}}</bold> cancelled <bold>{{user_name}}'s</bold> invite", "user_invited": "<bold><inviter/></bold> invited <bold><user/></bold>",
"invite_rejected": "<bold>{{user_name}}</bold> rejected the invitation", "invite_cancelled": "<bold><inviter/></bold> cancelled <bold><user/>'s</bold> invite",
"user_kicked": "<bold>{{actor}}</bold> kicked <bold>{{user_name}}</bold>: {{reason}}", "invite_rejected": "<bold><user/></bold> rejected the invitation",
"user_banned": "<bold>{{actor}}</bold> banned <bold>{{user_name}}</bold>: {{reason}}", "user_kicked": "<bold><actor/></bold> kicked <bold><user/></bold>",
"user_unbanned": "<bold>{{actor}} unbanned <bold>{{user_name}}</bold>", "user_kicked_reason": "<bold><actor/></bold> kicked <bold><user/></bold>: <reason/>",
"avatar_set": "<bold>{{user_name}}</bold> set an avatar", "user_banned": "<bold><actor/></bold> banned <bold><user/></bold>",
"avatar_changed": "<bold>{{user_name}}</bold> changed their avatar", "user_banned_reason": "<bold><actor/></bold> banned <bold><user/></bold>: <reason/>",
"avatar_removed": "<bold>{{user_name}}</bold> removed their avatar", "user_unbanned": "<bold><actor/> unbanned <bold><user/></bold>",
"name_set": "<bold>{{user_name}}</bold> set their display name to <bold>{{new_name}}</bold>", "avatar_set": "<bold><user/></bold> set an avatar",
"name_changed": "<bold>{{user_name}}</bold> changed their display name to <bold>{{new_name}}</bold>", "avatar_changed": "<bold><user/></bold> changed their avatar",
"name_removed": "<bold>{{user_name}}</bold> removed their display name <bold>{{new_name}}</bold>" "avatar_removed": "<bold><user/></bold> removed their avatar",
"name_set": "<bold><user/></bold> set their display name to <bold><new_name/></bold>",
"name_changed": "<bold><user/></bold> changed their display name to <bold><new_name/></bold>",
"name_removed": "<bold><user/></bold> removed their display name <bold><last_name/></bold>"
}, },
"PublicRooms": { "PublicRooms": {
"could_not_join_alias": "Unable to join {{alias}}. Either the room is private or doesn't exist", "could_not_join_alias": "Unable to join {{alias}}. Either the room is private or doesn't exist",
@ -461,10 +464,10 @@
"close_tooltip": "Close" "close_tooltip": "Close"
}, },
"FollowingMembers": { "FollowingMembers": {
"users_following_one": "<bold>{{user_one}}</bold> is following the conversation", "users_following_1": "<bold><user_one/></bold> is following the conversation",
"users_following_two": "<bold>{{user_one}}</bold> and <bold>{{user_two}}</bold> are following the conversation", "users_following_2": "<bold><user_one/></bold> and <bold><user_two/></bold> are following the conversation",
"users_following_three": "<bold>{{user_one}}</bold>, <bold>{{user_two}}</bold>, and <bold>{{user_three}}</bold> are following the conversation", "users_following_3": "<bold><user_one/></bold>, <bold><user_two/></bold>, and <bold><user_three/></bold> are following the conversation",
"users_following_other": "<bold>{{user_one}}</bold>, <bold>{{user_two}}</bold>, <bold>{{user_three}}</bold> and {{other_count}} others are following the conversation" "users_following_other": "<bold><user_one/></bold>, <bold><user_two/></bold>, <bold><user_three/></bold> and {{other_count}} others are following the conversation"
}, },
"ImageUpload": { "ImageUpload": {
"prompt": "Upload", "prompt": "Upload",
@ -499,10 +502,10 @@
"unknown_user": "** Unknown user **", "unknown_user": "** Unknown user **",
"edited": "(edited)", "edited": "(edited)",
"edit_placeholder": "Edit message", "edit_placeholder": "Edit message",
"user_reacted_one": "<bold>{{user_one}}</bold> reacted with <emoji/>", "user_reacted_1": "<bold><user_one/></bold> reacted with <emoji/>",
"user_reacted_two": "<bold>{{user_one}}</bold> and <bold>{{user_two}}</bold> reacted with <emoji/>", "user_reacted_2": "<bold><user_one/></bold> and <bold><user_two/></bold> reacted with <emoji/>",
"user_reacted_three": "<bold>{{user_one}}</bold>, <bold>{{user_two}}</bold> and <bold>{{user_three}}</bold> reacted with <emoji/>", "user_reacted_3": "<bold><user_one/></bold>, <bold><user_two/></bold> and <bold><user_three/></bold> reacted with <emoji/>",
"user_reacted_other": "<bold>{{user_one}}</bold>, <bold>{{user_two}}</bold>, <bold>{{user_three}}</bold> and {{other_count}} others reacted with <emoji/>", "user_reacted_other": "<bold><user_one/></bold>, <bold><user_two/></bold>, <bold><user_three/></bold> and {{other_count}} others reacted with <emoji/>",
"add_reaction_tooltip": "Add reaction", "add_reaction_tooltip": "Add reaction",
"reply_tooltip": "Reply", "reply_tooltip": "Reply",
"edit_tooltip": "Edit", "edit_tooltip": "Edit",

View file

@ -12,7 +12,7 @@ import Text from '../../atoms/text/Text';
import RawIcon from '../../atoms/system-icons/RawIcon'; import RawIcon from '../../atoms/system-icons/RawIcon';
import TickMarkIC from '../../../../public/res/ic/outlined/tick-mark.svg'; import TickMarkIC from '../../../../public/res/ic/outlined/tick-mark.svg';
import { twemojify } from '../../../util/twemojify'; import { twemojify, Twemojify } from '../../../util/twemojify';
import '../../i18n'; import '../../i18n';
@ -43,6 +43,12 @@ function FollowingMembers({ roomTimeline }) {
const filteredM = followingMembers.filter((userId) => userId !== myUserId); const filteredM = followingMembers.filter((userId) => userId !== myUserId);
let i18nKey = 'Molecules.FollowingMembers.users_following';
if (filteredM.length <= 3) {
i18nKey += `_${filteredM.length}`;
}
return filteredM.length !== 0 && ( return filteredM.length !== 0 && (
<button <button
className="following-members" className="following-members"
@ -55,15 +61,17 @@ function FollowingMembers({ roomTimeline }) {
/> />
<Text variant="b2"> <Text variant="b2">
<Trans <Trans
i18nKey="Molecules.FollowingMembers.users_following" i18nKey={i18nKey}
values={{ values={{
count: filteredM.length, count: filteredM.length,
user_one: twemojify(getUserDisplayName(room, filteredM?.[0])),
user_two: twemojify(getUserDisplayName(room, filteredM?.[1])),
user_three: twemojify(getUserDisplayName(room, filteredM?.[2])),
other_count: filteredM.length - 3, other_count: filteredM.length - 3,
}} }}
components={{ bold: <b /> }} components={{
bold: <b />,
user_one: <Twemojify text={getUserDisplayName(room, filteredM?.[0])} />,
user_two: <Twemojify text={getUserDisplayName(room, filteredM?.[1])} />,
user_three: <Twemojify text={getUserDisplayName(room, filteredM?.[2])} />,
}}
/> />
</Text> </Text>
</button> </button>

View file

@ -6,7 +6,7 @@ import PropTypes from 'prop-types';
import './Message.scss'; import './Message.scss';
import { useTranslation, Trans } from 'react-i18next'; import { useTranslation, Trans } from 'react-i18next';
import { twemojify } from '../../../util/twemojify'; import { twemojify, Twemojify } from '../../../util/twemojify';
import initMatrix from '../../../client/initMatrix'; import initMatrix from '../../../client/initMatrix';
import { getUsername, getUsernameOfRoomMember, parseReply } from '../../../util/matrixUtil'; import { getUsername, getUsernameOfRoomMember, parseReply } from '../../../util/matrixUtil';
@ -353,21 +353,24 @@ function pickEmoji(e, roomId, eventId, roomTimeline) {
} }
function genReactionMsg(userIds, reaction, shortcode) { function genReactionMsg(userIds, reaction, shortcode) {
let i18nKey = 'Molecules.Message.user_reacted';
if (userIds.length <= 3) {
i18nKey += `_${userIds.length}`;
}
return ( return (
<> <Trans
{userIds.map((userId, index) => ( i18nKey={i18nKey}
<React.Fragment key={userId}> values={{ count: userIds.length, other_count: userIds.length - 3 }}
{twemojify(getUsername(userId))} components={{
{index < userIds.length - 1 && ( bold: <b />,
<span style={{ opacity: '.6' }}> user_one: <Twemojify text={getUsername(userIds?.[0])} />,
{index === userIds.length - 2 ? ' and ' : ', '} user_two: <Twemojify text={getUsername(userIds?.[1])} />,
</span> user_three: <Twemojify text={getUsername(userIds?.[2])} />,
)} emoji: <Twemojify text={shortcode ? `:${shortcode}:` : reaction} />,
</React.Fragment> }}
))} />
<span style={{ opacity: '.6' }}>{' reacted with '}</span>
{twemojify(shortcode ? `:${shortcode}:` : reaction, { className: 'react-emoji' })}
</>
); );
} }

View file

@ -4,7 +4,7 @@ import PropTypes from 'prop-types';
import './RoomViewFloating.scss'; import './RoomViewFloating.scss';
import { useTranslation, Trans } from 'react-i18next'; import { useTranslation, Trans } from 'react-i18next';
import { twemojify } from '../../../util/twemojify'; import { twemojify, Twemojify } from '../../../util/twemojify';
import initMatrix from '../../../client/initMatrix'; import initMatrix from '../../../client/initMatrix';
import cons from '../../../client/state/cons'; import cons from '../../../client/state/cons';
@ -109,11 +109,13 @@ function RoomViewFloating({
setIsAtBottom(true); setIsAtBottom(true);
}; };
console.log(typingMembers);
const typingMemberValues = [...typingMembers]; const typingMemberValues = [...typingMembers];
console.log(typingMemberValues); let i18nKey = 'Organisms.RoomViewFloating.user_typing';
if (typingMemberValues.length <= 4) {
i18nKey += `_${typingMemberValues.length}`;
}
return ( return (
<> <>
@ -129,15 +131,17 @@ function RoomViewFloating({
<div className="bouncing-loader"><div /></div> <div className="bouncing-loader"><div /></div>
<Text variant="b2"> <Text variant="b2">
<Trans <Trans
i18nKey="Organisms.RoomViewFloating.user_typing" i18nKey={i18nKey}
values={{ values={{
count: typingMembers.size, count: typingMembers.size,
user_one: twemojify(getUserDisplayName(typingMemberValues?.[0])),
user_two: twemojify(getUserDisplayName(typingMemberValues?.[1])),
user_three: twemojify(getUserDisplayName(typingMemberValues?.[2])),
user_four: twemojify(getUserDisplayName(typingMemberValues?.[3])),
}} }}
components={{ bold: <b /> }} components={{
bold: <b />,
user_one: <Twemojify text={getUsername(typingMemberValues?.[0])} />,
user_two: <Twemojify text={getUsername(typingMemberValues?.[1])} />,
user_three: <Twemojify text={getUsername(typingMemberValues?.[2])} />,
user_four: <Twemojify text={getUsername(typingMemberValues?.[3])} />,
}}
/> />
</Text> </Text>
</div> </div>

View file

@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import { Trans } from 'react-i18next'; import { Trans } from 'react-i18next';
import { twemojify } from '../../../util/twemojify'; import { twemojify, Twemojify } from '../../../util/twemojify';
import initMatrix from '../../../client/initMatrix'; import initMatrix from '../../../client/initMatrix';
import { getUsername, getUsernameOfRoomMember } from '../../../util/matrixUtil'; import { getUsername, getUsernameOfRoomMember } from '../../../util/matrixUtil';
@ -15,18 +15,20 @@ function getTimelineJSXMessages() {
return ( return (
<Trans <Trans
i18nKey="Organisms.RoomCommon.user_joined" i18nKey="Organisms.RoomCommon.user_joined"
values={{ user_name: twemojify(user) }} components={{ bold: <b />, user: <Twemojify text={user} /> }}
components={{ bold: <b /> }}
/> />
); );
}, },
leave(user, reason) { leave(user, reason) {
const reasonMsg = (typeof reason === 'string') ? `: ${reason}` : '';
return ( return (
<Trans <Trans
i18nKey="Organisms.RoomCommon.user_left" i18nKey="Organisms.RoomCommon.user_left"
values={{ user_name: twemojify(user) }} context={(typeof reason === 'string') ? 'reason' : null}
components={{ bold: <b /> }} components={{
bold: <b />,
user: <Twemojify text={user} />,
reason: <Twemojify text={reason} />,
}}
/> />
); );
}, },
@ -34,8 +36,11 @@ function getTimelineJSXMessages() {
return ( return (
<Trans <Trans
i18nKey="Organisms.RoomCommon.user_invited" i18nKey="Organisms.RoomCommon.user_invited"
values={{ user_name: twemojify(user), inviter_name: twemojify(inviter) }} components={{
components={{ bold: <b /> }} bold: <b />,
user: <Twemojify text={user} />,
inviter: <Twemojify text={inviter} />,
}}
/> />
); );
}, },
@ -43,8 +48,11 @@ function getTimelineJSXMessages() {
return ( return (
<Trans <Trans
i18nKey="Organisms.RoomCommon.invite_cancelled" i18nKey="Organisms.RoomCommon.invite_cancelled"
values={{ user_name: twemojify(user), inviter_name: twemojify(inviter) }} components={{
components={{ bold: <b /> }} bold: <b />,
user: <Twemojify text={user} />,
inviter: <Twemojify text={inviter} />,
}}
/> />
); );
}, },
@ -52,36 +60,38 @@ function getTimelineJSXMessages() {
return ( return (
<Trans <Trans
i18nKey="Organisms.RoomCommon.invite_rejected" i18nKey="Organisms.RoomCommon.invite_rejected"
values={{ user_name: twemojify(user) }} components={{
components={{ bold: <b /> }} bold: <b />,
user: <Twemojify text={user} />,
}}
/> />
); );
}, },
kick(actor, user, reason) { kick(actor, user, reason) {
const reasonMsg = (typeof reason === 'string') ? `${reason}` : '';
return ( return (
<Trans <Trans
i18nKey="Organisms.RoomCommon.user_kicked" i18nKey="Organisms.RoomCommon.user_kicked"
values={{ context={(typeof reason === 'string') ? 'reason' : null}
user_name: twemojify(user), components={{
actor: twemojify(actor), bold: <b />,
reason: twemojify(reasonMsg), user: <Twemojify text={user} />,
actor: <Twemojify text={actor} />,
reason: <Twemojify text={reason} />,
}} }}
components={{ bold: <b /> }}
/> />
); );
}, },
ban(actor, user, reason) { ban(actor, user, reason) {
const reasonMsg = (typeof reason === 'string') ? `${reason}` : '';
return ( return (
<Trans <Trans
i18nKey="Organisms.RoomCommon.user_banned" i18nKey="Organisms.RoomCommon.user_banned"
values={{ context={(typeof reason === 'string') ? 'reason' : null}
user_name: twemojify(user), components={{
actor: twemojify(actor), bold: <b />,
reason: twemojify(reasonMsg), user: <Twemojify text={user} />,
actor: <Twemojify text={actor} />,
reason: <Twemojify text={reason} />,
}} }}
components={{ bold: <b /> }}
/> />
); );
}, },
@ -89,8 +99,11 @@ function getTimelineJSXMessages() {
return ( return (
<Trans <Trans
i18nKey="Organisms.RoomCommon.user_unbanned" i18nKey="Organisms.RoomCommon.user_unbanned"
values={{ user_name: twemojify(user), actor: twemojify(actor) }} components={{
components={{ bold: <b /> }} bold: <b />,
user: <Twemojify text={user} />,
actor: <Twemojify text={actor} />,
}}
/> />
); );
}, },
@ -98,8 +111,10 @@ function getTimelineJSXMessages() {
return ( return (
<Trans <Trans
i18nKey="Organisms.RoomCommon.avatar_set" i18nKey="Organisms.RoomCommon.avatar_set"
values={{ user_name: twemojify(user) }} components={{
components={{ bold: <b /> }} bold: <b />,
user: <Twemojify text={user} />,
}}
/> />
); );
}, },
@ -107,8 +122,10 @@ function getTimelineJSXMessages() {
return ( return (
<Trans <Trans
i18nKey="Organisms.RoomCommon.avatar_changed" i18nKey="Organisms.RoomCommon.avatar_changed"
values={{ user_name: twemojify(user) }} components={{
components={{ bold: <b /> }} bold: <b />,
user: <Twemojify text={user} />,
}}
/> />
); );
}, },
@ -116,8 +133,10 @@ function getTimelineJSXMessages() {
return ( return (
<Trans <Trans
i18nKey="Organisms.RoomCommon.avatar_removed" i18nKey="Organisms.RoomCommon.avatar_removed"
values={{ user_name: twemojify(user) }} components={{
components={{ bold: <b /> }} bold: <b />,
user: <Twemojify text={user} />,
}}
/> />
); );
}, },
@ -125,8 +144,11 @@ function getTimelineJSXMessages() {
return ( return (
<Trans <Trans
i18nKey="Organisms.RoomCommon.name_set" i18nKey="Organisms.RoomCommon.name_set"
values={{ user_name: twemojify(user), new_name: twemojify(newName) }} components={{
components={{ bold: <b /> }} bold: <b />,
user: <Twemojify text={user} />,
new_name: <Twemojify text={newName} />,
}}
/> />
); );
}, },
@ -134,8 +156,11 @@ function getTimelineJSXMessages() {
return ( return (
<Trans <Trans
i18nKey="Organisms.RoomCommon.name_changed" i18nKey="Organisms.RoomCommon.name_changed"
values={{ user_name: twemojify(user), new_name: twemojify(newName) }} components={{
components={{ bold: <b /> }} bold: <b />,
user: <Twemojify text={user} />,
new_name: <Twemojify text={newName} />,
}}
/> />
); );
}, },
@ -143,8 +168,11 @@ function getTimelineJSXMessages() {
return ( return (
<Trans <Trans
i18nKey="Organisms.RoomCommon.name_removed" i18nKey="Organisms.RoomCommon.name_removed"
values={{ user_name: twemojify(user), new_name: twemojify(lastName) }} components={{
components={{ bold: <b /> }} bold: <b />,
user: <Twemojify text={user} />,
last_name: <Twemojify text={lastName} />,
}}
/> />
); );
}, },

View file

@ -5,6 +5,7 @@ import linkifyHtml from 'linkify-html';
import parse from 'html-react-parser'; import parse from 'html-react-parser';
import twemoji from 'twemoji'; import twemoji from 'twemoji';
import { sanitizeText } from './sanitize'; import { sanitizeText } from './sanitize';
import PropTypes from 'prop-types';
const Math = lazy(() => import('../app/atoms/math/Math')); const Math = lazy(() => import('../app/atoms/math/Math'));
@ -51,3 +52,19 @@ export function twemojify(text, opts, linkify = false, sanitize = true, maths =
} }
return parse(content, maths ? mathOptions : null); return parse(content, maths ? mathOptions : null);
} }
export function Twemojify({ text }) {
return (
<>
{twemojify(text)}
</>
);
}
Twemojify.defaultProps = {
text: null,
};
Twemojify.propTypes = {
text: PropTypes.string,
};