Update the word count modal (#27068)

GitOrigin-RevId: c4d11bda020e435bcf8b6daec253cedb37df0252
This commit is contained in:
Alf Eaton
2025-08-26 15:47:50 +01:00
committed by Copybot
parent d6f6cbe189
commit cfcb9f32ab
5 changed files with 92 additions and 58 deletions

View File

@@ -248,6 +248,7 @@
"change_your_email": "",
"changing_the_position_of_your_figure": "",
"changing_the_position_of_your_table": "",
"characters": "",
"chat": "",
"chat_error": "",
"check_logs": "",
@@ -997,6 +998,7 @@
"main_bibliography_file_for_this_project": "",
"main_document": "",
"main_file_not_found": "",
"main_text": "",
"make_a_copy": "",
"make_email_primary_description": "",
"make_owner": "",
@@ -1767,7 +1769,6 @@
"test_configuration_successful": "",
"tex_live_version": "",
"texgpt": "",
"text": "",
"thank_you": "",
"thank_you_exclamation": "",
"thank_you_for_your_feedback": "",
@@ -1910,7 +1911,6 @@
"tooltip_show_filetree": "",
"tooltip_show_panel": "",
"tooltip_show_pdf": "",
"total": "",
"total_due_in_x_days": "",
"total_due_today": "",
"total_per_month": "",
@@ -2114,6 +2114,7 @@
"with_premium_subscription_you_also_get": "",
"word_count": "",
"word_count_lower": "",
"words": "",
"work_in_vim_or_emacs_emulation_mode": "",
"work_offline": "",
"work_offline_pull_to_overleaf": "",

View File

@@ -9,6 +9,7 @@ import OLButton from '@/shared/components/ol/ol-button'
import { WordCountServer } from './word-count-server'
import { WordCountClient } from './word-count-client'
import { isSplitTestEnabled } from '@/utils/splitTestUtils'
import SplitTestBadge from '@/shared/components/split-test-badge'
// NOTE: this component is only mounted when the modal is open
export default function WordCountModalContent({
@@ -21,7 +22,13 @@ export default function WordCountModalContent({
return (
<>
<OLModalHeader closeButton>
<OLModalTitle>{t('word_count')}</OLModalTitle>
<OLModalTitle>
{t('word_count')}{' '}
<SplitTestBadge
splitTestName="word-count-client"
displayOnVariants={['enabled']}
/>
</OLModalTitle>
</OLModalHeader>
<OLModalBody>

View File

@@ -5,6 +5,8 @@ import { Container, Row, Col, Form } from 'react-bootstrap'
import OLNotification from '@/shared/components/ol/ol-notification'
import usePersistedState from '@/shared/hooks/use-persisted-state'
const numberFormat = new Intl.NumberFormat()
export const WordCountsClient: FC<{ data: WordCountData }> = ({ data }) => {
const { t } = useTranslation()
@@ -17,7 +19,7 @@ export const WordCountsClient: FC<{ data: WordCountData }> = ({ data }) => {
return [
{
key: 'text',
label: t('text'),
label: t('main_text'),
words: data.textWords,
chars: data.textCharacters,
},
@@ -85,52 +87,73 @@ export const WordCountsClient: FC<{ data: WordCountData }> = ({ data }) => {
</Row>
)}
{items.map(item => (
<Row
key={item.key}
style={{
borderBottom: '1px solid #eee',
padding: 5,
marginBottom: 5,
}}
>
<Col
style={{
display: 'flex',
alignItems: 'top',
justifyContent: 'space-between',
}}
>
<Form.Check
type="checkbox"
id={`word-count-${item.key}`}
label={item.label}
checked={included.includes(item.key)}
onChange={event =>
setIncluded(prevValue => {
return event.target.checked
? prevValue.concat(item.key)
: prevValue.filter(key => key !== item.key)
})
}
aria-label={`Include ${item.label} in total`}
/>
</Col>
<Col>
{item.words} words
<br />
{item.chars} chars
</Col>
</Row>
))}
<Row className="mb-4">
<table style={{ width: 'auto' }}>
<thead>
<tr>
<th />
<th className="visually-hidden">{t('words')}</th>
<th className="visually-hidden">{t('characters')}</th>
</tr>
</thead>
<tbody>
<tr>
<th style={{ width: 100 }}>{t('total_words')}:</th>
<td style={{ width: 100, textAlign: 'right' }}>
{numberFormat.format(totals.words)}
</td>
<td style={{ width: 250, textAlign: 'right' }}>
<b style={{ marginRight: 10 }} aria-hidden="true">
{t('characters')}:
</b>{' '}
{numberFormat.format(totals.chars)}
</td>
</tr>
{items.map(item => (
<tr key={item.key}>
<th style={{ fontWeight: 'normal', paddingLeft: 20 }}>
<Form.Check
type="checkbox"
id={`word-count-${item.key}`}
label={`${item.label}:`}
checked={included.includes(item.key)}
onChange={event =>
setIncluded(prevValue => {
return event.target.checked
? prevValue.concat(item.key)
: prevValue.filter(key => key !== item.key)
})
}
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>
<Col style={{ textAlign: 'right' }}>
<span style={{ fontWeight: 'bold' }}>
{t('total')}: {totals.words} words
<br />
{totals.chars} chars
</span>
<Row className="border-top py-2">
<Col xs={12}>
<b>Headers:</b> {data.headers}
</Col>
</Row>
<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>
</Row>
</Container>

View File

@@ -5,7 +5,7 @@ import { debugConsole } from '@/utils/debugging'
import { findPreambleExtent } from '@/features/word-count-modal/utils/find-preamble-extent'
import { Segmenters } from './segmenters'
const whiteSpaceRe = /^\s$/
// const whiteSpaceRe = /^\s$/
type Context = 'text' | 'header' | 'abstract' | 'caption' | 'footnote' | 'other'
@@ -301,9 +301,8 @@ export const countWordsInFile = (
for (const [context, text] of Object.entries(texts)) {
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(
// replace - and _ with a word character, so that hyphenated words are counted as one word
text.replace(/\w[-_]\w/g, 'aaa')
)) {
if (value.isWordLike) {
@@ -311,13 +310,14 @@ export const countWordsInFile = (
}
}
// TODO: count hyphens as characters?
for (const value of segmenters.character.segment(text)) {
for (const _value of segmenters.character.segment(
// replace multiple spaces with a single space
text.replace(/\s+/, ' ').trim()
)) {
// TODO: option for whether to include whitespace?
if (!whiteSpaceRe.test(value.segment)) {
data[counter.character]++
}
// if (!whiteSpaceRe.test(value.segment)) {
data[counter.character]++
// }
}
}
}

View File

@@ -317,6 +317,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": "Characters",
"chat": "Chat",
"chat_error": "Could not load chat messages, please try again.",
"check_logs": "Check logs",
@@ -1307,6 +1308,7 @@
"main_bibliography_file_for_this_project": "Main bibliography file for this project",
"main_document": "Main document",
"main_file_not_found": "Unknown main document",
"main_text": "Main text",
"maintenance": "Maintenance",
"make_a_copy": "Make a copy",
"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",
"word_count": "Word Count",
"word_count_lower": "Word count",
"words": "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, then pull to __appName__",