From cfcb9f32abfb017d82eb65ab555fd4e3fcaf2d24 Mon Sep 17 00:00:00 2001 From: Alf Eaton Date: Tue, 26 Aug 2025 15:47:50 +0100 Subject: [PATCH] Update the word count modal (#27068) GitOrigin-RevId: c4d11bda020e435bcf8b6daec253cedb37df0252 --- .../web/frontend/extracted-translations.json | 5 +- .../components/word-count-modal-content.tsx | 9 +- .../components/word-counts-client.tsx | 115 +++++++++++------- .../utils/count-words-in-file.ts | 18 +-- services/web/locales/en.json | 3 + 5 files changed, 92 insertions(+), 58 deletions(-) diff --git a/services/web/frontend/extracted-translations.json b/services/web/frontend/extracted-translations.json index 6674b7d332..7009bed5a6 100644 --- a/services/web/frontend/extracted-translations.json +++ b/services/web/frontend/extracted-translations.json @@ -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": "", diff --git a/services/web/frontend/js/features/word-count-modal/components/word-count-modal-content.tsx b/services/web/frontend/js/features/word-count-modal/components/word-count-modal-content.tsx index c7e44c0355..35851825bb 100644 --- a/services/web/frontend/js/features/word-count-modal/components/word-count-modal-content.tsx +++ b/services/web/frontend/js/features/word-count-modal/components/word-count-modal-content.tsx @@ -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 ( <> - {t('word_count')} + + {t('word_count')}{' '} + + diff --git a/services/web/frontend/js/features/word-count-modal/components/word-counts-client.tsx b/services/web/frontend/js/features/word-count-modal/components/word-counts-client.tsx index a5697aed78..9e3ea62c72 100644 --- a/services/web/frontend/js/features/word-count-modal/components/word-counts-client.tsx +++ b/services/web/frontend/js/features/word-count-modal/components/word-counts-client.tsx @@ -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 }) => { )} - {items.map(item => ( - - - - setIncluded(prevValue => { - return event.target.checked - ? prevValue.concat(item.key) - : prevValue.filter(key => key !== item.key) - }) - } - aria-label={`Include ${item.label} in total`} - /> - - - {item.words} words -
- {item.chars} chars - -
- ))} + + + + + + + + + + + + + + + {items.map(item => ( + + + + + + ))} + +
+ {t('words')}{t('characters')}
{t('total_words')}: + {numberFormat.format(totals.words)} + + {' '} + {numberFormat.format(totals.chars)} +
+ + setIncluded(prevValue => { + return event.target.checked + ? prevValue.concat(item.key) + : prevValue.filter(key => key !== item.key) + }) + } + aria-label={`Include ${item.label} in total`} + /> + + {numberFormat.format(item.words)} + + {numberFormat.format(item.chars)} +
+
- - - - {t('total')}: {totals.words} words -
- {totals.chars} chars -
+ + + Headers: {data.headers} + + + + + + Math Inline: {data.mathInline} + + + + + + Math Display: {data.mathDisplay} diff --git a/services/web/frontend/js/features/word-count-modal/utils/count-words-in-file.ts b/services/web/frontend/js/features/word-count-modal/utils/count-words-in-file.ts index a14120d8d5..8187bb39a2 100644 --- a/services/web/frontend/js/features/word-count-modal/utils/count-words-in-file.ts +++ b/services/web/frontend/js/features/word-count-modal/utils/count-words-in-file.ts @@ -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]++ + // } } } } diff --git a/services/web/locales/en.json b/services/web/locales/en.json index 54e9e0c6a3..9d70051c92 100644 --- a/services/web/locales/en.json +++ b/services/web/locales/en.json @@ -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__",