Make notification titles translatable

## Issue
Desktop 7022: i18n: Notification title localization lost

## Approach
The current code breaks up the string into an array of strings, so i18n is somewhat impossible.

Since 99%¹ of dynamic notifications come with a `dynamic` property for all the replaceable values, we can actually reconstruct the string however we want.

¹ <sub>_as far as I can find, only `fiat_tip` does not provide the value via `dynamic`, i.e. hardcoded in the string. Boo._</sub>

### Benefits of this approach:
- able to localize the string again
- able to customize the string (e.g. making claim titles italic, fix typos, use more concise strings, etc.)

### Problems with this approach:
- if the api overloads a particular notification type with several strings, then the approach is broken.
    - Ex. For the case of `comment` type, the overload is whether the comment is a normal comment or hyperchat. But I was able to replicate the logic to differentiate them, so all is good.
    - For the case of "livestream reminder in 30 minutes", we would have to inspect the string to differentiate against "is live streaming". I think this reminder is not being used, so I didn't do it yet.
- some work is needed to maintain the code when the server side updates something.  But we are already manually maintaining the i18n strings anyway, so it shouldn't be too much of a burden.
- With the exception of the overload problem, any new notification type will simply pass through as un-localized, so things should continue to work, i.e. no need to update the front-end immediately 🤞
This commit is contained in:
infinite-persistence 2022-02-14 17:04:12 +08:00 committed by Thomas Zarebczan
parent bab96276b6
commit fc50095b36
5 changed files with 203 additions and 56 deletions

View file

@ -1442,6 +1442,12 @@
"You missed out on the daily view reward, sign up for our rewards program!": "You missed out on the daily view reward, sign up for our rewards program!",
"A Welcome 🎁": "A Welcome 🎁",
"Receive free Credits just for doing what you're already doing": "Receive free Credits just for doing what you're already doing",
"You have a fan 🥇": "You have a fan 🥇",
"%commenter% sent a %amount% hyperchat on %title%": "%commenter% sent a %amount% hyperchat on %title%",
"%streamer% is live streaming!": "%streamer% is live streaming!",
"New content from %creator%": "New content from %creator%",
"%commenter% commented on %title%": "%commenter% commented on %title%",
"%commenter% replied to you on %title%": "%commenter% replied to you on %title%",
"Watch more content to get to the next level.": "Watch more content to get to the next level.",
"Follow more channels to reach the next level.": "Follow more channels to reach the next level.",
"You need to improve the cool-aid (gain more followers) to get to the next level.": "You need to improve the cool-aid (gain more followers) to get to the next level.",

View file

@ -0,0 +1,54 @@
// @flow
import React from 'react';
import LbcMessage from 'component/common/lbc-message';
import OptimizedImage from 'component/optimizedImage';
import { RULE } from 'constants/notifications';
import { parseSticker } from 'util/comments';
function replaceLbcWithCredits(str: string) {
return str.replace(/\sLBC/g, ' Credits');
}
export function generateNotificationText(rule: string, notificationParams: any) {
switch (rule) {
default:
console.log(`TEXT: Unhandled notification_rule:%c ${rule}`, 'color:yellow'); // eslint-disable-line
// eslint-disable-next-line no-fallthrough, (intended fallthrough)
case RULE.NEW_CONTENT:
case RULE.NEW_LIVESTREAM:
case RULE.CREATOR_SUBSCRIBER:
case RULE.DAILY_WATCH_AVAILABLE:
case RULE.DAILY_WATCH_REMIND:
case RULE.WEEKLY_WATCH_REMINDER:
case RULE.MISSED_OUT:
case RULE.REWARDS_APPROVAL_PROMPT:
case RULE.FIAT_TIP:
return (
<div className="notification__text" title={replaceLbcWithCredits(notificationParams.device.text)}>
<LbcMessage>{notificationParams.device.text}</LbcMessage>
</div>
);
case RULE.COMMENT:
case RULE.CREATOR_COMMENT:
case RULE.COMMENT_REPLY: {
const commentText = notificationParams.dynamic.comment;
const sticker = commentText && parseSticker(commentText);
return (
<div className="notification__text notification__text--replies" title={sticker ? undefined : commentText}>
{sticker && (
<div className="sticker__comment">
<OptimizedImage src={sticker.url} waitLoad loading="lazy" />
</div>
)}
{!sticker && (
<blockquote>
<LbcMessage>{commentText}</LbcMessage>
</blockquote>
)}
</div>
);
}
}
}

View file

@ -0,0 +1,117 @@
// @flow
import React from 'react';
import LbcMessage from 'component/common/lbc-message';
import I18nMessage from 'component/i18nMessage';
import UriIndicator from 'component/uriIndicator';
import { RULE } from 'constants/notifications';
function getChannelNameLink(channelUrl: string, channelName: ?string) {
return <UriIndicator link showAtSign uri={channelUrl} channelInfo={{ uri: channelUrl, name: channelName }} />;
}
export function generateNotificationTitle(rule: string, notificationParams: any, channelName: ?string) {
switch (rule) {
case RULE.COMMENT: {
const channelUrl = notificationParams?.dynamic?.comment_author;
if (notificationParams?.dynamic?.amount > 0) {
const amountStr = `${parseFloat(notificationParams.dynamic.amount)} ${
notificationParams.dynamic.currency || 'LBC'
}`;
return channelUrl ? (
<I18nMessage
tokens={{
commenter: getChannelNameLink(channelUrl, channelName),
amount: <LbcMessage>{amountStr}</LbcMessage>,
title: <span className="notification__claim-title">{notificationParams.dynamic?.claim_title}</span>,
}}
>
%commenter% sent a %amount% hyperchat on %title%
</I18nMessage>
) : (
notificationParams.device.title
);
} else {
return channelUrl ? (
<I18nMessage
tokens={{
commenter: getChannelNameLink(channelUrl, channelName),
title: <span className="notification__claim-title">{notificationParams.dynamic?.claim_title}</span>,
}}
>
%commenter% commented on %title%
</I18nMessage>
) : (
notificationParams.device.title
);
}
}
case RULE.CREATOR_COMMENT: {
const channelUrl = notificationParams?.dynamic?.comment_author;
return channelUrl ? (
<I18nMessage
tokens={{
commenter: getChannelNameLink(channelUrl, channelName),
title: <span className="notification__claim-title">{notificationParams.dynamic?.claim_title}</span>,
}}
>
%commenter% commented on %title%
</I18nMessage>
) : (
notificationParams.device.title
);
}
case RULE.COMMENT_REPLY: {
const channelUrl = notificationParams?.dynamic?.reply_author;
return channelUrl ? (
<I18nMessage
tokens={{
commenter: getChannelNameLink(channelUrl, channelName),
title: <span className="notification__claim-title">{notificationParams.dynamic?.claim_title}</span>,
}}
>
%commenter% replied to you on %title%
</I18nMessage>
) : (
notificationParams.device.title
);
}
case RULE.NEW_CONTENT: {
const channelUrl = notificationParams?.dynamic?.channel_url;
return channelUrl ? (
<I18nMessage tokens={{ creator: getChannelNameLink(channelUrl, channelName) }}>
New content from %creator%
</I18nMessage>
) : (
notificationParams.device.title
);
}
case RULE.NEW_LIVESTREAM: {
const channelUrl = notificationParams?.dynamic?.channel_url;
return channelUrl ? (
<I18nMessage tokens={{ streamer: getChannelNameLink(channelUrl, channelName) }}>
%streamer% is live streaming!
</I18nMessage>
) : (
notificationParams.device.title
);
}
case RULE.CREATOR_SUBSCRIBER:
case RULE.DAILY_WATCH_AVAILABLE:
case RULE.DAILY_WATCH_REMIND:
case RULE.WEEKLY_WATCH_REMINDER:
case RULE.MISSED_OUT:
case RULE.REWARDS_APPROVAL_PROMPT:
case RULE.FIAT_TIP:
// Use Commentron default
return __(notificationParams.device.title);
default:
console.log(`TITLE: Unhandled notification_rule:%c ${rule}`, 'color:yellow'); // eslint-disable-line
return __(notificationParams.device.title);
}
}

View file

@ -4,7 +4,6 @@ import { formatLbryUrlForWeb } from 'util/url';
import { Menu, MenuList, MenuButton, MenuItem } from '@reach/menu-button';
import { NavLink } from 'react-router-dom';
import { PAGE_VIEW_QUERY, DISCUSSION_PAGE } from 'page/channel/view';
import { parseSticker } from 'util/comments';
import { parseURI } from 'util/lbryURI';
import { RULE } from 'constants/notifications';
import { useHistory } from 'react-router';
@ -16,11 +15,11 @@ import classnames from 'classnames';
import DateTime from 'component/dateTime';
import FileThumbnail from 'component/fileThumbnail';
import Icon from 'component/common/icon';
import LbcMessage from 'component/common/lbc-message';
import NotificationContentChannelMenu from 'component/notificationContentChannelMenu';
import OptimizedImage from 'component/optimizedImage';
import React from 'react';
import UriIndicator from 'component/uriIndicator';
import { generateNotificationTitle } from './helpers/title';
import { generateNotificationText } from './helpers/text';
const CommentCreate = lazyImport(() => import('component/commentCreate' /* webpackChunkName: "comments" */));
const CommentReactions = lazyImport(() => import('component/commentReactions' /* webpackChunkName: "comments" */));
@ -46,8 +45,6 @@ export default function Notification(props: Props) {
notification_rule === RULE.COMMENT ||
notification_rule === RULE.COMMENT_REPLY ||
notification_rule === RULE.CREATOR_COMMENT;
const commentText = isCommentNotification && notification_parameters.dynamic.comment;
const stickerFromComment = isCommentNotification && commentText && parseSticker(commentText);
const notificationTarget = getNotificationTarget();
const creatorIcon = (channelUrl, channelThumbnail) => (
@ -106,36 +103,6 @@ export default function Notification(props: Props) {
} catch (e) {}
}
const notificationTitle = notification_parameters.device.title;
const titleSplit = notificationTitle.split(' ');
let fullTitle = [' '];
let uriIndicator;
const title = titleSplit.map((message, index) => {
if (channelName === message) {
uriIndicator = (
<UriIndicator
key={channelUrl}
uri={channelUrl}
link
showAtSign
channelInfo={{ uri: channelUrl, name: channelName }}
/>
);
fullTitle.push(' ');
const resultTitle = fullTitle;
fullTitle = [' '];
return [resultTitle.join(' '), uriIndicator];
} else {
fullTitle.push(message);
if (index === titleSplit.length - 1) {
const result = fullTitle.join(' ');
return <LbcMessage key={result}>{result}</LbcMessage>;
}
}
});
try {
const { isChannel } = parseURI(notificationTarget);
if (isChannel) urlParams.append(PAGE_VIEW_QUERY, DISCUSSION_PAGE);
@ -192,24 +159,10 @@ export default function Notification(props: Props) {
<div className="notificationContent__wrapper">
<div className="notification__content">
<div className="notificationText__wrapper">
<div className="notification__title">{title}</div>
{!commentText ? (
<div
title={notification_parameters.device.text.replace(/\sLBC/g, ' Credits')}
className="notification__text"
>
<LbcMessage>{notification_parameters.device.text}</LbcMessage>
</div>
) : stickerFromComment ? (
<div className="sticker__comment">
<OptimizedImage src={stickerFromComment.url} waitLoad loading="lazy" />
</div>
) : (
<div title={commentText} className="notification__text">
{commentText}
</div>
)}
<div className="notification__title">
{generateNotificationTitle(notification_rule, notification_parameters, channelName)}
</div>
{generateNotificationText(notification_rule, notification_parameters)}
</div>
{notification_rule === RULE.NEW_CONTENT && (

View file

@ -232,20 +232,28 @@ $contentMaxWidth: 60rem;
position: relative;
font-size: var(--font-small);
color: var(--color-text);
margin-bottom: var(--spacing-s);
margin-bottom: var(--spacing-xxs);
display: -webkit-box;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;
overflow: hidden;
.channel-name {
font-size: unset;
}
@media (max-width: $breakpoint-small) {
margin-bottom: 0;
}
}
.notification__claim-title {
font-weight: var(--font-weight-bold);
}
.notification__text {
font-size: var(--font-body);
color: var(--color-text);
font-size: var(--font-xsmall);
color: var(--color-text-subtitle);
display: -webkit-box;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;
@ -257,6 +265,15 @@ $contentMaxWidth: 60rem;
flex-shrink: 1;
}
.notification__text--replies {
font-style: italic;
blockquote {
padding-left: var(--spacing-xs);
border-left: 2px solid;
}
}
.notification__time {
font-size: var(--font-small);
color: var(--color-text);