mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-28 09:33:46 +00:00
Merge PR #479: счётчик символов и предупреждение о разбивке длинных текстов
Парный к боту #3049. В /admin/legal-pages под полем политики/оферты/правил: постоянный счётчик символов + предупреждение с оценкой числа сообщений бота (estimateTelegramParts зеркалит split_telegram_text, порог 3500). Скрыто там, где документ в боте не показывается (recurrent-payments, web-only); счётчик остаётся. Переводы ru/en с плюральными формами, прочие локали — en fallback. Валидация: JSON-ключи, tsc, biome lint/format, build — чисто.
This commit is contained in:
@@ -4275,6 +4275,9 @@
|
||||
"language": "Content language",
|
||||
"content": "Content",
|
||||
"contentPlaceholder": "HTML page content...",
|
||||
"charCount": "Characters: {{count}}",
|
||||
"botSplitEstimate_one": "⚠️ The bot will show it split into ~{{count}} message",
|
||||
"botSplitEstimate_other": "⚠️ The bot will show it split into ~{{count}} messages",
|
||||
"save": "Save",
|
||||
"saving": "Saving...",
|
||||
"saveError": "Failed to save. Please try again.",
|
||||
|
||||
@@ -4820,6 +4820,10 @@
|
||||
"language": "Язык контента",
|
||||
"content": "Текст",
|
||||
"contentPlaceholder": "HTML-текст страницы...",
|
||||
"charCount": "Символов: {{count}}",
|
||||
"botSplitEstimate_one": "⚠️ В боте будет показано по частям: ~{{count}} сообщение",
|
||||
"botSplitEstimate_few": "⚠️ В боте будет показано по частям: ~{{count}} сообщения",
|
||||
"botSplitEstimate_many": "⚠️ В боте будет показано по частям: ~{{count}} сообщений",
|
||||
"save": "Сохранить",
|
||||
"saving": "Сохранение...",
|
||||
"saveError": "Не удалось сохранить. Попробуйте ещё раз.",
|
||||
|
||||
@@ -20,6 +20,62 @@ const DISPLAY_MODES: LegalDisplayMode[] = ['bot', 'web', 'both'];
|
||||
|
||||
type DocumentKind = 'privacy-policy' | 'public-offer' | 'recurrent-payments';
|
||||
|
||||
// Bot chunk/page size (split_telegram_text max_length): longer texts are
|
||||
// delivered by the bot in several messages / paginated pages
|
||||
const TELEGRAM_SPLIT_THRESHOLD = 3500;
|
||||
|
||||
// Mirrors the bot's split_telegram_text greedy paragraph packing to estimate
|
||||
// how many messages/pages the bot will produce for this text
|
||||
function estimateTelegramParts(text: string): number {
|
||||
const normalized = text.replace(/\r\n/g, '\n').trim();
|
||||
if (!normalized) return 0;
|
||||
if (normalized.length <= TELEGRAM_SPLIT_THRESHOLD) return 1;
|
||||
const paragraphs = normalized.split('\n\n').filter((p) => p.trim());
|
||||
let parts = 0;
|
||||
let current = '';
|
||||
for (const paragraph of paragraphs) {
|
||||
const candidate = current ? `${current}\n\n${paragraph}` : paragraph;
|
||||
if (candidate.length <= TELEGRAM_SPLIT_THRESHOLD) {
|
||||
current = candidate;
|
||||
continue;
|
||||
}
|
||||
if (current) {
|
||||
parts += 1;
|
||||
current = '';
|
||||
}
|
||||
if (paragraph.length <= TELEGRAM_SPLIT_THRESHOLD) {
|
||||
current = paragraph;
|
||||
} else {
|
||||
parts += Math.ceil(paragraph.length / TELEGRAM_SPLIT_THRESHOLD);
|
||||
}
|
||||
}
|
||||
if (current) parts += 1;
|
||||
return parts;
|
||||
}
|
||||
|
||||
function ContentLengthMeta({ text, botVisible }: { text: string; botVisible: boolean }) {
|
||||
const { t } = useTranslation();
|
||||
const parts = botVisible ? estimateTelegramParts(text) : 0;
|
||||
return (
|
||||
<div className="mt-2 flex flex-wrap items-center gap-x-3 gap-y-1 text-xs">
|
||||
<span className="text-dark-500">
|
||||
{t('admin.legalPages.charCount', {
|
||||
count: text.length,
|
||||
defaultValue: 'Characters: {{count}}',
|
||||
})}
|
||||
</span>
|
||||
{parts > 1 && (
|
||||
<span className="text-warning-400">
|
||||
{t('admin.legalPages.botSplitEstimate', {
|
||||
count: parts,
|
||||
defaultValue: '⚠️ The bot will show it split into ~{{count}} messages',
|
||||
})}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const DOCUMENT_API: Record<
|
||||
DocumentKind,
|
||||
{
|
||||
@@ -236,6 +292,10 @@ function DocumentEditor({
|
||||
className="input min-h-[320px] w-full font-mono text-sm"
|
||||
placeholder={t('admin.legalPages.contentPlaceholder')}
|
||||
/>
|
||||
<ContentLengthMeta
|
||||
text={contents[activeLang] ?? ''}
|
||||
botVisible={kind !== 'recurrent-payments' && displayMode !== 'web'}
|
||||
/>
|
||||
</div>
|
||||
{saveError && <p className="text-sm text-error-400">{saveError}</p>}
|
||||
<button
|
||||
@@ -336,6 +396,7 @@ function RulesEditor({ onDirtyChange }: { onDirtyChange: (dirty: boolean) => voi
|
||||
className="input min-h-[320px] w-full font-mono text-sm"
|
||||
placeholder={t('admin.legalPages.contentPlaceholder')}
|
||||
/>
|
||||
<ContentLengthMeta text={contents[activeLang] ?? ''} botVisible={displayMode !== 'web'} />
|
||||
</div>
|
||||
{saveError && <p className="text-sm text-error-400">{saveError}</p>}
|
||||
<button
|
||||
|
||||
Reference in New Issue
Block a user