' . esc_html( $text ) . ' ' . esc_html__( 'Подробнее о Pro', 'yasno' ) . ' ';
}
private static function toggle( $name, $checked, $label, $desc = '' ) {
printf(
' %3$s%4$s ',
esc_attr( $name ),
checked( (bool) $checked, true, false ),
esc_html( $label ),
$desc ? ' ' . wp_kses_post( $desc ) . ' ' : ''
);
}
/** Кнопка запуска проверки с полосой прогресса. */
private static function scan_box( $label ) {
echo '';
echo '
' . esc_html( $label ) . '';
echo '
';
echo '
';
}
/** Слово в фрагменте текста — выделено. */
private static function context( $context, $word ) {
$html = esc_html( $context );
$re = '/' . preg_quote( esc_html( $word ), '/' ) . '/iu';
return (string) preg_replace( $re, '$0 ', $html, 1 );
}
/* ---------------------------------------------------------------------
* Обзор
* ------------------------------------------------------------------- */
public static function overview() {
$r = YSNO_Scan::results();
$words = YSNO_Scan::words();
if ( ! $r['finished'] ) {
YSNO_Admin::card_open( __( 'Сайт ещё не проверялся', 'yasno' ), __( 'Плагин прочитает опубликованные страницы, записи, меню и название сайта и найдёт слова латиницей и англицизмы, которые стоит заменить русскими. Проверка идёт по частям и не нагружает сайт.', 'yasno' ), array( 'icon' => 'dashicons-search' ) );
self::scan_box( __( 'Проверить сайт', 'yasno' ) );
YSNO_Admin::card_close();
} else {
$counts = array( 'word' => 0, 'latin' => 0, 'abbr' => 0 );
foreach ( $words as $w ) {
++$counts[ $w['type'] ];
}
$issues = count( $r['items'] );
echo '';
echo '
' . (int) $issues . ' ' . esc_html( _n( 'страница с замечаниями', 'страниц с замечаниями', $issues, 'yasno' ) ) . '
';
echo '
' . ( $issues ? esc_html__( 'Есть слова, которые стоит заменить', 'yasno' ) : esc_html__( 'Замечаний нет', 'yasno' ) ) . ' ';
/* translators: 1: дата, 2: число страниц */
echo '
' . esc_html( sprintf( __( 'Проверка %1$s, просмотрено %2$s.', 'yasno' ), wp_date( 'd.m.Y H:i', (int) $r['finished'] ), sprintf( _n( '%d страница', '%d страниц', (int) $r['checked'], 'yasno' ), (int) $r['checked'] ) ) ) . '
';
echo '
' . esc_html__( 'Открыть результаты', 'yasno' ) . '
';
echo '';
foreach ( array(
array( $counts['word'], __( 'англицизмов', 'yasno' ), __( 'есть привычная русская замена', 'yasno' ) ),
array( $counts['latin'], __( 'слов латиницей', 'yasno' ), __( 'перевести или внести в исключения как товарный знак', 'yasno' ) ),
array( $counts['abbr'], __( 'аббревиатур', 'yasno' ), __( 'расшифровать или оставить, если общеприняты', 'yasno' ) ),
) as $k ) {
echo '
' . esc_html( number_format_i18n( $k[0] ) ) . ' ' . esc_html( $k[1] ) . ' ' . esc_html( $k[2] ) . '
';
}
echo '
';
if ( $words ) {
YSNO_Admin::card_open( __( 'Чаще всего встречается', 'yasno' ), '', array( 'icon' => 'dashicons-editor-ul' ) );
echo '';
foreach ( array_slice( $words, 0, 10, true ) as $w ) {
echo '' . esc_html( $w['word'] ) . ' ' . esc_html( YSNO_Checker::type_label( $w['type'] ) ) . ' ';
if ( '' !== $w['suggest'] ) {
echo ' → ' . esc_html( $w['suggest'] );
}
/* translators: %d: число страниц */
echo ' ' . esc_html( sprintf( _n( 'на %d странице', 'на %d страницах', count( $w['items'] ), 'yasno' ), count( $w['items'] ) ) ) . ' ';
}
echo ' ';
YSNO_Admin::card_close();
}
}
/**
* Карточки дополнений на вкладке «Обзор».
*/
do_action( 'ysno/overview' );
YSNO_Admin::card_open( __( 'Что требует закон', 'yasno' ), '', array( 'icon' => 'dashicons-book' ) );
echo '';
echo '' . esc_html__( 'С 1 марта 2026 года информация для потребителей — на сайте, в карточках товаров, кнопках и меню — должна быть на русском языке (Федеральный закон от 24.06.2025 № 168-ФЗ).', 'yasno' ) . ' ';
echo '' . esc_html__( 'Иностранные слова допустимы, если они есть в нормативных словарях русского языка (перечень утверждён распоряжением Правительства РФ № 1102-р). Надпись на иностранном языке можно оставить рядом с русской — но не вместо неё.', 'yasno' ) . ' ';
echo '' . esc_html__( 'Товарные знаки, знаки обслуживания и фирменные наименования можно писать как зарегистрированы — внесите их в исключения.', 'yasno' ) . ' ';
echo '' . esc_html__( 'Штраф за нарушение в информации для потребителей — до 10 000 ₽ для организации, в рекламе — до 500 000 ₽.', 'yasno' ) . ' ';
echo ' ' . esc_html__( 'Плагин — помощник, а не юридическая консультация. Он не содержит нормативных словарей: спорные слова сверьте со словарями из перечня Правительства РФ.', 'yasno' ) . '
';
YSNO_Admin::card_close();
if ( ! YSNO_Plugin::has_pro_addon() ) {
YSNO_Admin::card_open( __( 'Рубрики, SEO-заголовки, Elementor — в Pro', 'yasno' ), __( 'Бесплатная версия проверяет все публичные типы записей — страницы, записи, товары WooCommerce и другие, — а также меню и название сайта. Pro добавляет атрибуты товаров, рубрики и метки, виджеты, заголовки и описания для поисковиков, подписи к картинкам и страницы Elementor, подсвечивает слова прямо в редакторе и заменяет их по всему сайту в один клик.', 'yasno' ), array( 'icon' => 'dashicons-star-filled', 'class' => 'is-muted' ) );
echo '' . esc_html__( 'Что даёт Pro', 'yasno' ) . '
';
YSNO_Admin::card_close();
}
}
/* ---------------------------------------------------------------------
* Проверка сайта
* ------------------------------------------------------------------- */
public static function check() {
$r = YSNO_Scan::results();
// phpcs:disable WordPress.Security.NonceVerification.Recommended
$view = isset( $_GET['view'] ) && 'pages' === $_GET['view'] ? 'pages' : 'words';
$only = isset( $_GET['item'] ) ? sanitize_text_field( wp_unslash( $_GET['item'] ) ) : '';
// phpcs:enable
YSNO_Admin::card_open( __( 'Проверка сайта', 'yasno' ), self::sources_line(), array( 'icon' => 'dashicons-search' ) );
self::scan_box( $r['finished'] ? __( 'Проверить заново', 'yasno' ) : __( 'Проверить сайт', 'yasno' ) );
/**
* Кнопки рядом с проверкой (Pro: отчёт CSV).
*
* @param array $r Результаты.
*/
do_action( 'ysno/check_actions', $r );
if ( $r['finished'] ) {
/* translators: 1: дата, 2: число страниц */
echo '' . esc_html( sprintf( __( 'Последняя проверка: %1$s, просмотрено страниц: %2$s. Записи перепроверяются сами при сохранении.', 'yasno' ), wp_date( 'd.m.Y H:i', (int) $r['finished'] ), number_format_i18n( (int) $r['checked'] ) ) ) . '
';
} elseif ( $r['started'] ) {
echo '' . esc_html__( 'Прошлая проверка не закончилась — запустите её заново.', 'yasno' ) . '
';
}
YSNO_Admin::card_close();
if ( ! $r['finished'] ) {
return;
}
if ( ! $r['items'] ) {
echo '' . esc_html__( 'Слов латиницей и англицизмов из словаря не найдено.', 'yasno' ) . '
';
return;
}
echo '';
foreach ( array( 'words' => __( 'По словам', 'yasno' ), 'pages' => __( 'По страницам', 'yasno' ) ) as $key => $label ) {
echo '
' . esc_html( $label ) . ' ';
}
echo '
';
if ( 'pages' === $view ) {
self::by_pages( $r['items'], $only );
} else {
self::by_words( YSNO_Scan::words(), $r['items'] );
}
}
const PER_PAGE = 40;
/** Номер страницы списка. */
private static function paged() {
return isset( $_GET['paged'] ) ? max( 1, absint( $_GET['paged'] ) ) : 1; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
}
/** Ссылки на страницы списка. */
private static function pager( $total, array $args ) {
$pages = (int) ceil( $total / self::PER_PAGE );
if ( $pages < 2 ) {
return;
}
$cur = min( self::paged(), $pages );
echo '';
}
private static function sources_line() {
$names = array();
foreach ( YSNO_Sources::all() as $src ) {
$names[] = $src['label'];
}
/* translators: %s: список источников */
return sprintf( __( 'Что проверяется: %s. Изменить — на вкладке «Настройки и исключения».', 'yasno' ), implode( ', ', $names ) );
}
private static function by_words( array $words, array $items ) {
$type = isset( $_GET['type'] ) ? sanitize_key( wp_unslash( $_GET['type'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
$counts = array( '' => count( $words ), 'word' => 0, 'latin' => 0, 'abbr' => 0 );
foreach ( $words as $w ) {
++$counts[ $w['type'] ];
}
echo '';
foreach ( array( '' => __( 'Все', 'yasno' ), 'word' => __( 'Англицизмы', 'yasno' ), 'latin' => __( 'Латиница', 'yasno' ), 'abbr' => __( 'Аббревиатуры', 'yasno' ) ) as $key => $label ) {
if ( '' !== $key && ! $counts[ $key ] ) {
continue;
}
echo '
' . esc_html( $label ) . ' ' . (int) $counts[ $key ] . ' ';
}
echo '
';
if ( '' !== $type ) {
$words = array_filter(
$words,
static function ( $w ) use ( $type ) {
return $w['type'] === $type;
}
);
}
$total = count( $words );
$words = array_slice( $words, ( self::paged() - 1 ) * self::PER_PAGE, self::PER_PAGE, true );
echo '' . esc_html__( 'Слово', 'yasno' ) . ' ' . esc_html__( 'Где встречается', 'yasno' ) . ' ' . esc_html__( 'Как заменить', 'yasno' ) . ' ';
foreach ( $words as $w ) {
$first = $items[ $w['items'][0] ] ?? null;
echo '' . esc_html( $w['word'] ) . ' ' . esc_html( YSNO_Checker::type_label( $w['type'] ) ) . ' ';
echo '';
$ctx = '';
foreach ( ( $first['found'] ?? array() ) as $f ) {
if ( $f['word'] === $w['word'] ) {
$ctx = $f['context'];
break;
}
}
if ( '' !== $ctx ) {
echo '' . self::context( $ctx, $w['word'] ) . '
'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- экранировано в context().
}
echo '';
/* translators: 1: число страниц, 2: число употреблений */
echo esc_html( sprintf( __( 'Страниц: %1$s, употреблений: %2$s', 'yasno' ), count( $w['items'] ), $w['count'] ) );
echo ' ';
echo '';
if ( '' !== $w['suggest'] ) {
echo '' . esc_html( $w['suggest'] ) . ' ';
if ( '' !== $w['note'] ) {
echo '' . esc_html( $w['note'] ) . ' ';
}
} elseif ( 'abbr' === $w['type'] ) {
echo '' . esc_html__( 'Расшифруйте по-русски при первом упоминании или оставьте, если это общепринятое обозначение.', 'yasno' ) . ' ';
} else {
echo '' . esc_html__( 'Переведите, напишите по-русски рядом или внесите в исключения, если это товарный знак.', 'yasno' ) . ' ';
}
echo ' ' . esc_html__( 'В исключения', 'yasno' ) . ' ';
/**
* Действия со словом в таблице результатов (Pro: «Заменить на всём сайте»).
*
* @param array $w Слово.
*/
do_action( 'ysno/word_actions', $w );
echo ' ';
}
echo '
';
self::pager( $total, array( 'type' => $type ) );
self::pro_note( __( 'Pro заменит слово на всех страницах сразу — с предпросмотром и сохранением прежних версий.', 'yasno' ) );
}
private static function by_pages( array $items, $only ) {
if ( '' !== $only && isset( $items[ $only ] ) ) {
$items = array( $only => $items[ $only ] );
echo '' . esc_html__( '← Все страницы', 'yasno' ) . '
';
}
uasort(
$items,
static function ( $a, $b ) {
return count( $b['found'] ) <=> count( $a['found'] );
}
);
$total = count( $items );
$items = array_slice( $items, ( self::paged() - 1 ) * self::PER_PAGE, self::PER_PAGE, true );
echo '' . esc_html__( 'Страница', 'yasno' ) . ' ' . esc_html__( 'Что стоит проверить', 'yasno' ) . ' ';
foreach ( $items as $it ) {
echo '' . esc_html( $it['title'] ) . ' ';
if ( ( $it['latin'] ?? 0 ) >= 0.5 ) {
echo '' . esc_html__( 'Почти весь текст на иностранном языке — нужен русский вариант страницы.', 'yasno' ) . '
';
}
echo '';
$shown = 0;
foreach ( $it['found'] as $f ) {
if ( ++$shown > 24 ) {
/* translators: %d: число слов */
echo '' . esc_html( sprintf( __( 'и ещё %d', 'yasno' ), count( $it['found'] ) - 24 ) ) . ' ';
break;
}
$title = '' !== $f['suggest'] ? '→ ' . $f['suggest'] : YSNO_Checker::type_label( $f['type'] );
echo '' . esc_html( $f['word'] ) . ( $f['count'] > 1 ? ' ×' . (int) $f['count'] . ' ' : '' ) . ' ';
}
echo '
';
}
echo '
';
self::pager( $total, array( 'view' => 'pages' ) );
}
/* ---------------------------------------------------------------------
* Настройки и исключения
* ------------------------------------------------------------------- */
public static function settings() {
$s = YSNO_Settings::all();
echo '';
}
}