Update the word count modal (#27068)

GitOrigin-RevId: c4d11bda020e435bcf8b6daec253cedb37df0252
This commit is contained in:
Alf Eaton
2025-08-27 08:06:27 +00:00
committed by Copybot
parent d6f6cbe189
commit cfcb9f32ab
5 changed files with 92 additions and 58 deletions
@@ -248,6 +248,7 @@
"change_your_email": "", "change_your_email": "",
"changing_the_position_of_your_figure": "", "changing_the_position_of_your_figure": "",
"changing_the_position_of_your_table": "", "changing_the_position_of_your_table": "",
"characters": "",
"chat": "", "chat": "",
"chat_error": "", "chat_error": "",
"check_logs": "", "check_logs": "",
@@ -997,6 +998,7 @@
"main_bibliography_file_for_this_project": "", "main_bibliography_file_for_this_project": "",
"main_document": "", "main_document": "",
"main_file_not_found": "", "main_file_not_found": "",
"main_text": "",
"make_a_copy": "", "make_a_copy": "",
"make_email_primary_description": "", "make_email_primary_description": "",
"make_owner": "", "make_owner": "",
@@ -1767,7 +1769,6 @@
"test_configuration_successful": "", "test_configuration_successful": "",
"tex_live_version": "", "tex_live_version": "",
"texgpt": "", "texgpt": "",
"text": "",
"thank_you": "", "thank_you": "",
"thank_you_exclamation": "", "thank_you_exclamation": "",
"thank_you_for_your_feedback": "", "thank_you_for_your_feedback": "",
@@ -1910,7 +1911,6 @@
"tooltip_show_filetree": "", "tooltip_show_filetree": "",
"tooltip_show_panel": "", "tooltip_show_panel": "",
"tooltip_show_pdf": "", "tooltip_show_pdf": "",
"total": "",
"total_due_in_x_days": "", "total_due_in_x_days": "",
"total_due_today": "", "total_due_today": "",
"total_per_month": "", "total_per_month": "",
@@ -2114,6 +2114,7 @@
"with_premium_subscription_you_also_get": "", "with_premium_subscription_you_also_get": "",
"word_count": "", "word_count": "",
"word_count_lower": "", "word_count_lower": "",
"words": "",
"work_in_vim_or_emacs_emulation_mode": "", "work_in_vim_or_emacs_emulation_mode": "",
"work_offline": "", "work_offline": "",
"work_offline_pull_to_overleaf": "", "work_offline_pull_to_overleaf": "",
@@ -9,6 +9,7 @@ import OLButton from '@/shared/components/ol/ol-button'
import { WordCountServer } from './word-count-server' import { WordCountServer } from './word-count-server'
import { WordCountClient } from './word-count-client' import { WordCountClient } from './word-count-client'
import { isSplitTestEnabled } from '@/utils/splitTestUtils' import { isSplitTestEnabled } from '@/utils/splitTestUtils'
import SplitTestBadge from '@/shared/components/split-test-badge'
// NOTE: this component is only mounted when the modal is open // NOTE: this component is only mounted when the modal is open
export default function WordCountModalContent({ export default function WordCountModalContent({
@@ -21,7 +22,13 @@ export default function WordCountModalContent({
return ( return (
<> <>
<OLModalHeader closeButton> <OLModalHeader closeButton>
<OLModalTitle>{t('word_count')}</OLModalTitle> <OLModalTitle>
{t('word_count')}{' '}
<SplitTestBadge
splitTestName="word-count-client"
displayOnVariants={['enabled']}
/>
</OLModalTitle>
</OLModalHeader> </OLModalHeader>
<OLModalBody> <OLModalBody>
@@ -5,6 +5,8 @@ import { Container, Row, Col, Form } from 'react-bootstrap'
import OLNotification from '@/shared/components/ol/ol-notification' import OLNotification from '@/shared/components/ol/ol-notification'
import usePersistedState from '@/shared/hooks/use-persisted-state' import usePersistedState from '@/shared/hooks/use-persisted-state'
const numberFormat = new Intl.NumberFormat()
export const WordCountsClient: FC<{ data: WordCountData }> = ({ data }) => { export const WordCountsClient: FC<{ data: WordCountData }> = ({ data }) => {
const { t } = useTranslation() const { t } = useTranslation()
@@ -17,7 +19,7 @@ export const WordCountsClient: FC<{ data: WordCountData }> = ({ data }) => {
return [ return [
{ {
key: 'text', key: 'text',
label: t('text'), label: t('main_text'),
words: data.textWords, words: data.textWords,
chars: data.textCharacters, chars: data.textCharacters,
}, },
@@ -85,52 +87,73 @@ export const WordCountsClient: FC<{ data: WordCountData }> = ({ data }) => {
</Row> </Row>
)} )}
{items.map(item => ( <Row className="mb-4">
<Row <table style={{ width: 'auto' }}>
key={item.key} <thead>
style={{ <tr>
borderBottom: '1px solid #eee', <th />
padding: 5, <th className="visually-hidden">{t('words')}</th>
marginBottom: 5, <th className="visually-hidden">{t('characters')}</th>
}} </tr>
> </thead>
<Col <tbody>
style={{ <tr>
display: 'flex', <th style={{ width: 100 }}>{t('total_words')}:</th>
alignItems: 'top', <td style={{ width: 100, textAlign: 'right' }}>
justifyContent: 'space-between', {numberFormat.format(totals.words)}
}} </td>
> <td style={{ width: 250, textAlign: 'right' }}>
<Form.Check <b style={{ marginRight: 10 }} aria-hidden="true">
type="checkbox" {t('characters')}:
id={`word-count-${item.key}`} </b>{' '}
label={item.label} {numberFormat.format(totals.chars)}
checked={included.includes(item.key)} </td>
onChange={event => </tr>
setIncluded(prevValue => { {items.map(item => (
return event.target.checked <tr key={item.key}>
? prevValue.concat(item.key) <th style={{ fontWeight: 'normal', paddingLeft: 20 }}>
: prevValue.filter(key => key !== item.key) <Form.Check
}) type="checkbox"
} id={`word-count-${item.key}`}
aria-label={`Include ${item.label} in total`} label={`${item.label}:`}
/> checked={included.includes(item.key)}
</Col> onChange={event =>
<Col> setIncluded(prevValue => {
{item.words} words return event.target.checked
<br /> ? prevValue.concat(item.key)
{item.chars} chars : prevValue.filter(key => key !== item.key)
</Col> })
</Row> }
))} aria-label={`Include ${item.label} in total`}
/>
</th>
<td style={{ textAlign: 'right' }}>
{numberFormat.format(item.words)}
</td>
<td style={{ textAlign: 'right' }}>
{numberFormat.format(item.chars)}
</td>
</tr>
))}
</tbody>
</table>
</Row>
<Row> <Row className="border-top py-2">
<Col style={{ textAlign: 'right' }}> <Col xs={12}>
<span style={{ fontWeight: 'bold' }}> <b>Headers:</b> {data.headers}
{t('total')}: {totals.words} words </Col>
<br /> </Row>
{totals.chars} chars
</span> <Row className="border-top py-2">
<Col xs={12}>
<b>Math Inline:</b> {data.mathInline}
</Col>
</Row>
<Row className="border-top py-2 pb-0">
<Col xs={12}>
<b>Math Display:</b> {data.mathDisplay}
</Col> </Col>
</Row> </Row>
</Container> </Container>
@@ -5,7 +5,7 @@ import { debugConsole } from '@/utils/debugging'
import { findPreambleExtent } from '@/features/word-count-modal/utils/find-preamble-extent' import { findPreambleExtent } from '@/features/word-count-modal/utils/find-preamble-extent'
import { Segmenters } from './segmenters' import { Segmenters } from './segmenters'
const whiteSpaceRe = /^\s$/ // const whiteSpaceRe = /^\s$/
type Context = 'text' | 'header' | 'abstract' | 'caption' | 'footnote' | 'other' type Context = 'text' | 'header' | 'abstract' | 'caption' | 'footnote' | 'other'
@@ -301,9 +301,8 @@ export const countWordsInFile = (
for (const [context, text] of Object.entries(texts)) { for (const [context, text] of Object.entries(texts)) {
const counter = counters[context as Context] const counter = counters[context as Context]
// TODO: replace - and _ with a word character if hyphenated words should be counted as one word?
for (const value of segmenters.word.segment( for (const value of segmenters.word.segment(
// replace - and _ with a word character, so that hyphenated words are counted as one word
text.replace(/\w[-_]\w/g, 'aaa') text.replace(/\w[-_]\w/g, 'aaa')
)) { )) {
if (value.isWordLike) { if (value.isWordLike) {
@@ -311,13 +310,14 @@ export const countWordsInFile = (
} }
} }
// TODO: count hyphens as characters? for (const _value of segmenters.character.segment(
// replace multiple spaces with a single space
for (const value of segmenters.character.segment(text)) { text.replace(/\s+/, ' ').trim()
)) {
// TODO: option for whether to include whitespace? // TODO: option for whether to include whitespace?
if (!whiteSpaceRe.test(value.segment)) { // if (!whiteSpaceRe.test(value.segment)) {
data[counter.character]++ data[counter.character]++
} // }
} }
} }
} }
+3
View File
@@ -317,6 +317,7 @@
"change_your_email": "Change your email", "change_your_email": "Change your email",
"changing_the_position_of_your_figure": "Changing the position of your figure", "changing_the_position_of_your_figure": "Changing the position of your figure",
"changing_the_position_of_your_table": "Changing the position of your table", "changing_the_position_of_your_table": "Changing the position of your table",
"characters": "Characters",
"chat": "Chat", "chat": "Chat",
"chat_error": "Could not load chat messages, please try again.", "chat_error": "Could not load chat messages, please try again.",
"check_logs": "Check logs", "check_logs": "Check logs",
@@ -1307,6 +1308,7 @@
"main_bibliography_file_for_this_project": "Main bibliography file for this project", "main_bibliography_file_for_this_project": "Main bibliography file for this project",
"main_document": "Main document", "main_document": "Main document",
"main_file_not_found": "Unknown main document", "main_file_not_found": "Unknown main document",
"main_text": "Main text",
"maintenance": "Maintenance", "maintenance": "Maintenance",
"make_a_copy": "Make a copy", "make_a_copy": "Make a copy",
"make_email_primary_description": "Make this the primary email, used to log in", "make_email_primary_description": "Make this the primary email, used to log in",
@@ -2670,6 +2672,7 @@
"with_premium_subscription_you_also_get": "With an Overleaf Premium subscription you also get", "with_premium_subscription_you_also_get": "With an Overleaf Premium subscription you also get",
"word_count": "Word Count", "word_count": "Word Count",
"word_count_lower": "Word count", "word_count_lower": "Word count",
"words": "Words",
"work_in_vim_or_emacs_emulation_mode": "Work in Vim or Emacs emulation mode", "work_in_vim_or_emacs_emulation_mode": "Work in Vim or Emacs emulation mode",
"work_offline": "Work offline", "work_offline": "Work offline",
"work_offline_pull_to_overleaf": "Work offline, then pull to __appName__", "work_offline_pull_to_overleaf": "Work offline, then pull to __appName__",