Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Editor: Add wordcount and reading time info in post card #60672

Merged
merged 6 commits into from
Apr 15, 2024
Merged
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
146 changes: 113 additions & 33 deletions packages/editor/src/components/post-card-panel/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,45 +15,57 @@ import {
} from '@wordpress/components';
import { store as coreStore } from '@wordpress/core-data';
import { useSelect } from '@wordpress/data';
import { __, sprintf } from '@wordpress/i18n';
import { __, _x, _n, sprintf } from '@wordpress/i18n';
import { humanTimeDiff } from '@wordpress/date';
import { decodeEntities } from '@wordpress/html-entities';
import { count as wordCount } from '@wordpress/wordcount';

/**
* Internal dependencies
*/
import { store as editorStore } from '../../store';
import { TEMPLATE_POST_TYPE } from '../../store/constants';
import {
TEMPLATE_POST_TYPE,
TEMPLATE_PART_POST_TYPE,
} from '../../store/constants';
import { unlock } from '../../lock-unlock';
import TemplateAreas from '../template-areas';

export default function PostCardPanel( { className, actions } ) {
const { modified, title, templateInfo, icon, postType } = useSelect(
( select ) => {
const {
getEditedPostAttribute,
getCurrentPostType,
getCurrentPostId,
__experimentalGetTemplateInfo,
} = select( editorStore );
const { getEditedEntityRecord } = select( coreStore );
const _type = getCurrentPostType();
const _id = getCurrentPostId();
const _record = getEditedEntityRecord( 'postType', _type, _id );
const _templateInfo = __experimentalGetTemplateInfo( _record );
return {
title:
_templateInfo?.title || getEditedPostAttribute( 'title' ),
modified: getEditedPostAttribute( 'modified' ),
id: _id,
postType: _type,
templateInfo: _templateInfo,
icon: unlock( select( editorStore ) ).getPostIcon( _type, {
area: _record?.area,
} ),
};
}
);
const {
modified,
title,
templateInfo,
icon,
postType,
postContent,
isPostsPage,
} = useSelect( ( select ) => {
const {
getEditedPostAttribute,
getCurrentPostType,
getCurrentPostId,
__experimentalGetTemplateInfo,
} = select( editorStore );
const { getEditedEntityRecord, getEntityRecord } = select( coreStore );
const siteSettings = getEntityRecord( 'root', 'site' );
const _type = getCurrentPostType();
const _id = getCurrentPostId();
const _record = getEditedEntityRecord( 'postType', _type, _id );
const _templateInfo = __experimentalGetTemplateInfo( _record );
return {
title: _templateInfo?.title || getEditedPostAttribute( 'title' ),
modified: getEditedPostAttribute( 'modified' ),
id: _id,
postType: _type,
templateInfo: _templateInfo,
icon: unlock( select( editorStore ) ).getPostIcon( _type, {
area: _record?.area,
} ),
isPostsPage: +_id === siteSettings?.page_for_posts,
postContent: _record?.content?.rendered || _record?.content,
};
}, [] );
const description = templateInfo?.description;
const lastEditedText =
modified &&
Expand All @@ -62,7 +74,10 @@ export default function PostCardPanel( { className, actions } ) {
__( 'Last edited %s.' ),
humanTimeDiff( modified )
);

const showPostContentInfo =
! [ TEMPLATE_POST_TYPE, TEMPLATE_PART_POST_TYPE ].includes(
postType
) && ! isPostsPage;
ntsekouras marked this conversation as resolved.
Show resolved Hide resolved
return (
<PanelBody>
<div
Expand All @@ -89,15 +104,24 @@ export default function PostCardPanel( { className, actions } ) {
{ actions }
</HStack>
<VStack className="editor-post-card-panel__content">
{ ( description || lastEditedText ) && (
{ ( description ||
lastEditedText ||
showPostContentInfo ) && (
<VStack
className="editor-post-card-panel__description"
spacing={ 2 }
>
{ description && <Text>{ description }</Text> }
{ lastEditedText && (
<Text>{ lastEditedText }</Text>
) }
<span>
{ showPostContentInfo && (
<PostContentInfo
postContent={ postContent }
/>
) }{ ' ' }
{ lastEditedText && (
<Text>{ lastEditedText }</Text>
) }
</span>
</VStack>
) }
{ postType === TEMPLATE_POST_TYPE && <TemplateAreas /> }
Expand All @@ -106,3 +130,59 @@ export default function PostCardPanel( { className, actions } ) {
</PanelBody>
);
}

// Taken from packages/editor/src/components/time-to-read/index.js.
const AVERAGE_READING_RATE = 189;

// This component renders the wordcount and reading time for the post.
function PostContentInfo( { postContent } ) {
/*
* translators: If your word count is based on single characters (e.g. East Asian characters),
* enter 'characters_excluding_spaces' or 'characters_including_spaces'. Otherwise, enter 'words'.
* Do not translate into your own language.
*/
const wordCountType = _x( 'words', 'Word count type. Do not translate!' );
const wordsCounted = postContent
? wordCount( postContent, wordCountType )
ntsekouras marked this conversation as resolved.
Show resolved Hide resolved
: 0;
if ( ! wordsCounted ) {
return null;
}
const readingTime = Math.round( wordsCounted / AVERAGE_READING_RATE );
let wordsCountText;
if ( ! wordsCounted.toLocaleString() ) {
wordsCountText = __( 'Unknown' );
ntsekouras marked this conversation as resolved.
Show resolved Hide resolved
} else {
wordsCountText =
wordsCounted === 1
? __( '1 word' )
: sprintf(
// translators: %s: the number of words in the post.
_n( '%s word', '%s words', wordsCounted ),
wordsCounted.toLocaleString()
);
}
const readingTimeText =
readingTime <= 1
? __( '1 minute read time' )
: sprintf(
// translators: %s: the number of words in the post.
ntsekouras marked this conversation as resolved.
Show resolved Hide resolved
_n(
'%s minute read time',
'%s minutes read time',
readingTime
),
readingTime.toLocaleString()
);
return (
<Text>
{ sprintf(
/* translators: 1: How many words a post has(eg. 30 words). 2: the number of minutes of read time(eg. 2 minutes read time) */
ntsekouras marked this conversation as resolved.
Show resolved Hide resolved
/* translators: %1s: is the number of minutes. */
ntsekouras marked this conversation as resolved.
Show resolved Hide resolved
'%1$s, %2$s.',
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Usually such strings just containing placeholder are not really useful for translators.

Could we maybe move it around a little bit?

%1$s. %2$s read time.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Usually such strings just containing placeholder are not really useful for translators.

This was initially my suggestion, based on the idea that translators ought to decide which, if any, punctuation should best separate the two fragments. Of course, in a non-numerical setting this could all be a single string, e.g. __( '%1$s words, %2$s reading time.' ), but since we need to take care of number agreement foremost this seemed like a reasonable concatenation technique.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With something like %1$s. %2$s read time. we can still deal with the numbers in the individual strings, e.g. %1$s is _n( '%s word', '%s words' and %2$s here is _n( '%s minute', '%s minutes' )

Copy link
Contributor Author

@ntsekouras ntsekouras Apr 12, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I updated per @swissspidy's feedback.

wordsCountText,
readingTimeText
) }
</Text>
);
}
Loading