File: /home/ayxmplky/public_html/wp-content/themes/tactic/functions.php
<?php
/**
* TACTIC Industrial — functions.php
* WordPress 6.0+ | PHP 8.0+ | Polylang (двуязычность: 中文 / English)
*
* @package tactic
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
define( 'TACTIC_VERSION', '1.0.0' );
define( 'TACTIC_DIR', get_template_directory() );
define( 'TACTIC_URI', get_template_directory_uri() );
/* ═══════════════════════════════════════════
Базовая настройка темы
═══════════════════════════════════════════ */
function tactic_setup() {
load_theme_textdomain( 'tactic', TACTIC_DIR . '/languages' );
add_theme_support( 'title-tag' );
add_theme_support( 'post-thumbnails' );
add_theme_support( 'html5', [
'search-form', 'comment-form', 'comment-list',
'gallery', 'caption', 'script', 'style',
] );
add_theme_support( 'custom-logo', [
'height' => 60,
'width' => 200,
'flex-height' => true,
'flex-width' => true,
] );
add_theme_support( 'align-wide' );
add_theme_support( 'responsive-embeds' );
// Размеры изображений
add_image_size( 'tactic-hero', 1920, 900, true );
add_image_size( 'tactic-news-thumb', 800, 500, true );
add_image_size( 'tactic-news-list', 400, 250, true );
add_image_size( 'tactic-testimonial', 480, 360, true );
add_image_size( 'tactic-landscape', 1920, 650, true );
// Меню навигации
register_nav_menus( [
'primary' => __( '主导航 / Primary Navigation', 'tactic' ),
'footer' => __( '页脚导航 / Footer Navigation', 'tactic' ),
] );
}
add_action( 'after_setup_theme', 'tactic_setup' );
/* ═══════════════════════════════════════════
Подключение стилей и скриптов
═══════════════════════════════════════════ */
function tactic_enqueue() {
$main_css_path = TACTIC_DIR . '/assets/css/main.css';
$css_modules_glob = TACTIC_DIR . '/assets/css/modules/*.css';
$main_js_path = TACTIC_DIR . '/assets/js/main.js';
$css_version_seed = file_exists( $main_css_path ) ? (int) filemtime( $main_css_path ) : 0;
$css_module_files = glob( $css_modules_glob ) ?: [];
foreach ( $css_module_files as $css_module_file ) {
$css_module_mtime = file_exists( $css_module_file ) ? (int) filemtime( $css_module_file ) : 0;
if ( $css_module_mtime > $css_version_seed ) {
$css_version_seed = $css_module_mtime;
}
}
$main_css_ver = $css_version_seed > 0 ? (string) $css_version_seed : TACTIC_VERSION;
$main_js_ver = file_exists( $main_js_path ) ? (string) filemtime( $main_js_path ) : TACTIC_VERSION;
wp_enqueue_style(
'tactic-main',
TACTIC_URI . '/assets/css/main.css',
[],
$main_css_ver
);
wp_enqueue_script(
'tactic-main',
TACTIC_URI . '/assets/js/main.js',
[],
$main_js_ver,
true
);
wp_localize_script( 'tactic-main', 'tacticData', [
'ajaxUrl' => admin_url( 'admin-ajax.php' ),
'nonce' => wp_create_nonce( 'tactic_nonce' ),
'lang' => tactic_current_lang(),
'contactMessages' => [
'success' => tactic_s( 'contact_success' ),
'error' => tactic_s( 'contact_error' ),
'requiredName' => tactic_s( 'contact_required_name' ),
'requiredCompany' => tactic_s( 'contact_required_company' ),
'requiredMobile' => tactic_s( 'contact_required_mobile' ),
'requiredEmail' => tactic_s( 'contact_required_email' ),
'requiredPhone' => tactic_s( 'contact_required_phone' ),
'requiredAddress' => tactic_s( 'contact_required_address' ),
'requiredGender' => tactic_s( 'contact_required_gender' ),
'requiredMessage' => tactic_s( 'contact_required_message' ),
'requiredTerms' => tactic_s( 'contact_required_terms' ),
'invalidEmail' => tactic_s( 'contact_invalid_email' ),
'invalidPhone' => tactic_s( 'contact_invalid_phone' ),
],
] );
}
add_action( 'wp_enqueue_scripts', 'tactic_enqueue' );
/* ═══════════════════════════════════════════
Виджет-области
═══════════════════════════════════════════ */
function tactic_widgets_init() {
register_sidebar( [
'name' => __( '博客侧边栏 / Blog Sidebar', 'tactic' ),
'id' => 'sidebar-blog',
'before_widget' => '<div id="%1$s" class="widget %2$s">',
'after_widget' => '</div>',
'before_title' => '<h3 class="widget__title">',
'after_title' => '</h3>',
] );
}
add_action( 'widgets_init', 'tactic_widgets_init' );
/* ═══════════════════════════════════════════
Вспомогательные функции
═══════════════════════════════════════════ */
/**
* Возвращает строку на текущем языке (inline-вариант).
* Использование: tactic_t('中文', 'English')
*/
function tactic_t( string $zh, string $en ): string {
return tactic_current_lang() === 'en' ? $en : $zh;
}
/**
* Возвращает строку по ключу из strings.php с поддержкой
* переопределений через WP Admin → TACTIC → Переводы.
* Использование: tactic_s('hero_title')
*/
function tactic_s( string $key ): string {
static $strings = null;
// Сброс статического кэша (вызывается после сохранения в админке)
if ( $key === '__reset__' ) {
$strings = null;
return '';
}
if ( $strings === null ) {
$defaults = require TACTIC_DIR . '/inc/strings.php';
$overrides = (array) get_option( 'tactic_translations', [] );
$strings = array_replace_recursive( $defaults, $overrides );
}
$lang = tactic_current_lang();
return $strings[ $key ][ $lang ] ?? $strings[ $key ]['en'] ?? $key;
}
/**
* Текущий язык интерфейса: 'zh' или 'en'
*/
function tactic_current_lang(): string {
// Приоритет: GET-параметр → cookie → по умолчанию ZH
$allowed = [ 'zh', 'en' ];
if ( isset( $_GET['lang'] ) && in_array( $_GET['lang'], $allowed, true ) ) {
$lang = sanitize_key( $_GET['lang'] );
setcookie( 'tactic_lang', $lang, time() + 30 * DAY_IN_SECONDS, COOKIEPATH, COOKIE_DOMAIN );
return $lang;
}
if ( isset( $_COOKIE['tactic_lang'] ) && in_array( $_COOKIE['tactic_lang'], $allowed, true ) ) {
return sanitize_key( $_COOKIE['tactic_lang'] );
}
return 'zh';
}
/**
* Язык для новостей (zh/en) на основе текущего языка интерфейса.
*/
function tactic_news_lang(): string {
$lang = tactic_current_lang();
return in_array( $lang, [ 'zh', 'en' ], true ) ? $lang : 'zh';
}
/**
* Meta query для выборки новостей по текущему языку.
*
* @return array<int, array<string, string>>
*/
function tactic_news_lang_meta_query(): array {
return [
[
'key' => '_tactic_news_lang',
'value' => tactic_news_lang(),
'compare' => '=',
],
];
}
/**
* Добавляет фильтр языка к произвольным WP_Query/get_posts аргументам новостей.
*/
function tactic_apply_news_lang_to_query_args( array $args ): array {
$meta_query = $args['meta_query'] ?? [];
if ( ! is_array( $meta_query ) ) {
$meta_query = [];
}
$meta_query[] = [
'key' => '_tactic_news_lang',
'value' => tactic_news_lang(),
'compare' => '=',
];
if ( count( $meta_query ) > 1 && ! isset( $meta_query['relation'] ) ) {
$meta_query['relation'] = 'AND';
}
$args['meta_query'] = $meta_query;
return $args;
}
/**
* Язык для карточек продукции (zh/en) на основе текущего языка интерфейса.
*/
function tactic_product_lang(): string {
$lang = tactic_current_lang();
return in_array( $lang, [ 'zh', 'en' ], true ) ? $lang : 'en';
}
/**
* Meta query для выборки карточек продукции по текущему языку.
*
* @return array<int, array<string, string>>
*/
function tactic_product_lang_meta_query(): array {
return [
[
'key' => '_tactic_product_lang',
'value' => tactic_product_lang(),
'compare' => '=',
],
];
}
/**
* Добавляет фильтр языка к произвольным WP_Query/get_posts аргументам продукции.
*/
function tactic_apply_product_lang_to_query_args( array $args ): array {
$meta_query = $args['meta_query'] ?? [];
if ( ! is_array( $meta_query ) ) {
$meta_query = [];
}
$meta_query[] = [
'key' => '_tactic_product_lang',
'value' => tactic_product_lang(),
'compare' => '=',
];
if ( count( $meta_query ) > 1 && ! isset( $meta_query['relation'] ) ) {
$meta_query['relation'] = 'AND';
}
$args['meta_query'] = $meta_query;
return $args;
}
/**
* Определяем язык новости по заголовку/контенту для миграции старых записей.
*/
function tactic_detect_news_lang( WP_Post $post ): string {
$haystack = (string) $post->post_title . ' ' . (string) $post->post_content . ' ' . (string) $post->post_excerpt;
return preg_match( '/[\x{3400}-\x{9FFF}]/u', $haystack ) ? 'zh' : 'en';
}
/**
* One-time: проставляем язык для старых новостей без метки языка.
*/
function tactic_maybe_migrate_news_lang_meta(): void {
$version = '2026-04-26-news-lang-v1';
if ( (string) get_option( 'tactic_news_lang_migration', '' ) === $version ) {
return;
}
$ids = get_posts( [
'post_type' => 'tactic_news',
'post_status' => [ 'publish', 'draft', 'pending', 'future', 'private' ],
'posts_per_page' => -1,
'fields' => 'ids',
'no_found_rows' => true,
] );
foreach ( $ids as $news_id ) {
if ( get_post_meta( (int) $news_id, '_tactic_news_lang', true ) ) {
continue;
}
$post = get_post( (int) $news_id );
if ( ! $post instanceof WP_Post ) {
continue;
}
update_post_meta( (int) $news_id, '_tactic_news_lang', tactic_detect_news_lang( $post ) );
}
update_option( 'tactic_news_lang_migration', $version );
}
add_action( 'init', 'tactic_maybe_migrate_news_lang_meta', 40 );
/**
* One-time: проставляем EN всем существующим карточкам продукции.
*/
function tactic_maybe_migrate_product_lang_meta(): void {
$version = '2026-04-30-product-lang-v1';
if ( (string) get_option( 'tactic_product_lang_migration', '' ) === $version ) {
return;
}
$ids = get_posts( [
'post_type' => 'tactic_product',
'post_status' => [ 'publish', 'draft', 'pending', 'future', 'private' ],
'posts_per_page' => -1,
'fields' => 'ids',
'no_found_rows' => true,
] );
foreach ( $ids as $product_id ) {
update_post_meta( (int) $product_id, '_tactic_product_lang', 'en' );
}
update_option( 'tactic_product_lang_migration', $version );
}
add_action( 'init', 'tactic_maybe_migrate_product_lang_meta', 41 );
/**
* Фильтрация основного архива /news по языку.
*/
function tactic_filter_news_archive_by_lang( WP_Query $query ): void {
if ( is_admin() || ! $query->is_main_query() ) {
return;
}
if ( ! $query->is_post_type_archive( 'tactic_news' ) ) {
return;
}
// Реальная пагинация архива: не более 7 новостей на страницу.
$query->set( 'posts_per_page', 7 );
$query->set( 'meta_query', tactic_news_lang_meta_query() );
}
add_action( 'pre_get_posts', 'tactic_filter_news_archive_by_lang' );
/**
* Переключатель языков ZH / EN
*/
function tactic_language_switcher(): void {
$current = tactic_current_lang();
$other = ( $current === 'zh' ) ? 'en' : 'zh';
$label = ( $other === 'en' ) ? 'EN' : '中文';
$url = esc_url( add_query_arg( 'lang', $other ) );
echo '<ul class="lang-switcher" role="list">';
echo '<li><a href="' . $url . '" hreflang="' . esc_attr( $other ) . '">' . esc_html( $label ) . '</a></li>';
echo '</ul>';
}
/**
* Динамический перевод пунктов меню
*/
add_filter( 'nav_menu_item_title', function( string $title, WP_Post $item ): string {
$map = [
home_url( '/about/' ) => tactic_s( 'nav_about' ),
home_url( '/news/' ) => tactic_s( 'nav_blog' ),
home_url( '/awards/' ) => tactic_s( 'nav_awards' ),
home_url( '/contact/' ) => tactic_s( 'nav_contact' ),
];
return $map[ trailingslashit( $item->url ) ] ?? $title;
}, 10, 2 );
/**
* SVG-иконки для футера
*/
function tactic_icon_instagram(): string {
return '<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="2" y="2" width="20" height="20" rx="5" ry="5"/><path d="M16 11.37A4 4 0 1 1 12.63 8 4 4 0 0 1 16 11.37z"/><line x1="17.5" y1="6.5" x2="17.51" y2="6.5"/></svg>';
}
function tactic_icon_wechat(): string {
return '<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M9.5 4C5.36 4 2 6.91 2 10.5c0 2.02 1.06 3.82 2.72 5.02L4 18l2.5-1.25A8.2 8.2 0 0 0 9.5 17c.17 0 .34 0 .5-.01A5.44 5.44 0 0 1 9.5 15c0-3.04 2.87-5.5 6.5-5.5.17 0 .34 0 .5.01C15.77 6.64 12.94 4 9.5 4zm-2 4.5a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm4 0a1 1 0 1 1 0-2 1 1 0 0 1 0 2zM16 11c-3.04 0-5.5 2.02-5.5 4.5S12.96 20 16 20c.69 0 1.35-.1 1.96-.29L20 21l-.62-2.08A4.26 4.26 0 0 0 21.5 15.5C21.5 13.02 19.04 11 16 11zm-1.5 3a.75.75 0 1 1 0-1.5.75.75 0 0 1 0 1.5zm3 0a.75.75 0 1 1 0-1.5.75.75 0 0 1 0 1.5z"/></svg>';
}
function tactic_icon_linkedin(): string {
return '<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M16 8a6 6 0 0 1 6 6v7h-4v-7a2 2 0 0 0-2-2 2 2 0 0 0-2 2v7h-4v-7a6 6 0 0 1 6-6zM2 9h4v12H2z"/><circle cx="4" cy="4" r="2"/></svg>';
}
/* ═══════════════════════════════════════════
Длина анонса
═══════════════════════════════════════════ */
function tactic_excerpt_length(): int {
return 20;
}
add_filter( 'excerpt_length', 'tactic_excerpt_length' );
function tactic_excerpt_more(): string {
return '…';
}
add_filter( 'excerpt_more', 'tactic_excerpt_more' );
/**
* Проверка, можно ли показывать запись в карточках блога.
* Скрываем записи, где миниатюра совпадает с логотипом сайта.
*/
function tactic_is_valid_blog_card_post( int $post_id ): bool {
if ( ! has_post_thumbnail( $post_id ) ) {
return true;
}
$thumb_id = (int) get_post_thumbnail_id( $post_id );
$logo_id = (int) get_theme_mod( 'custom_logo' );
if ( $logo_id > 0 && $thumb_id === $logo_id ) {
return false;
}
return true;
}
/* ═══════════════════════════════════════════
Body class — язык
═══════════════════════════════════════════ */
function tactic_body_class_lang( array $classes ): array {
$classes[] = 'lang-' . tactic_current_lang();
return $classes;
}
add_filter( 'body_class', 'tactic_body_class_lang' );
/* ═══════════════════════════════════════════
Admin: медиа-загрузчик для метабоксов
═══════════════════════════════════════════ */
function tactic_admin_scripts(): void {
wp_enqueue_media();
wp_enqueue_script(
'tactic-admin',
TACTIC_URI . '/assets/js/admin.js',
[ 'jquery' ],
TACTIC_VERSION,
true
);
}
add_action( 'admin_enqueue_scripts', 'tactic_admin_scripts' );
/* ═══════════════════════════════════════════
Обработчик формы «Контакты»
═══════════════════════════════════════════ */
function tactic_handle_contact_form(): void {
// Проверка nonce
if ( ! isset( $_POST['contact_nonce'] ) || ! wp_verify_nonce( sanitize_key( $_POST['contact_nonce'] ), 'tactic_contact_form' ) ) {
wp_send_json_error( [ 'message' => tactic_s( 'contact_security_error' ) ], 403 );
}
$name = sanitize_text_field( wp_unslash( $_POST['cf_name'] ?? '' ) );
$company = sanitize_text_field( wp_unslash( $_POST['cf_company'] ?? '' ) );
$country = sanitize_text_field( wp_unslash( $_POST['cf_country'] ?? '' ) );
$mobile = sanitize_text_field( wp_unslash( $_POST['cf_mobile'] ?? '' ) );
$email = sanitize_email( wp_unslash( $_POST['cf_email'] ?? '' ) );
$phone = sanitize_text_field( wp_unslash( $_POST['cf_phone'] ?? '' ) );
$address = sanitize_text_field( wp_unslash( $_POST['cf_address'] ?? '' ) );
$gender = sanitize_text_field( wp_unslash( $_POST['cf_gender'] ?? '' ) );
$industry = sanitize_text_field( wp_unslash( $_POST['cf_industry'] ?? '' ) );
$message = sanitize_textarea_field( wp_unslash( $_POST['cf_message'] ?? '' ) );
$terms = isset( $_POST['cf_terms'] ) ? '1' : '0';
$errors = [];
if ( ! $name ) {
$errors['cf_name'] = tactic_s( 'contact_required_name' );
}
if ( ! $company ) {
$errors['cf_company'] = tactic_s( 'contact_required_company' );
}
if ( ! $mobile ) {
$errors['cf_mobile'] = tactic_s( 'contact_required_mobile' );
}
if ( ! $email ) {
$errors['cf_email'] = tactic_s( 'contact_required_email' );
} elseif ( ! is_email( $email ) ) {
$errors['cf_email'] = tactic_s( 'contact_invalid_email' );
}
if ( ! $phone ) {
$errors['cf_phone'] = tactic_s( 'contact_required_phone' );
} elseif ( ! preg_match( '/^[+]?[\d\s\-().]{7,25}$/', $phone ) ) {
$errors['cf_phone'] = tactic_s( 'contact_invalid_phone' );
}
if ( ! $address ) {
$errors['cf_address'] = tactic_s( 'contact_required_address' );
}
if ( ! $gender ) {
$errors['cf_gender'] = tactic_s( 'contact_required_gender' );
}
if ( ! $message ) {
$errors['cf_message'] = tactic_s( 'contact_required_message' );
}
if ( $terms !== '1' ) {
$errors['cf_terms'] = tactic_s( 'contact_required_terms' );
}
if ( ! empty( $errors ) ) {
wp_send_json_error(
[
'message' => reset( $errors ),
'errors' => $errors,
],
422
);
}
$admin_email = get_option( 'admin_email' );
$to = $admin_email;
$subject = sprintf( '[TACTIC Contact] %s — %s', $name, $company ?: $country );
$body = implode( "\n", array_filter( [
"Name: $name",
$company ? "Company: $company" : '',
$country ? "Country: $country" : '',
$gender ? "Gender: $gender" : '',
"Email: $email",
$mobile ? "Mobile: $mobile" : '',
$phone ? "Phone: $phone" : '',
$address ? "Address: $address" : '',
$industry ? "Industry: $industry" : '',
"Terms accepted: " . ( $terms === '1' ? 'yes' : 'no' ),
"",
"Message:",
$message,
] ) );
$headers = [
'Content-Type: text/plain; charset=UTF-8',
"Reply-To: $name <$email>",
];
// Сохраняем сообщение в базу данных (CPT tactic_message)
$post_id = wp_insert_post( [
'post_type' => 'tactic_message',
'post_status' => 'publish',
'post_title' => $name,
'post_author' => 1,
] );
if ( $post_id && ! is_wp_error( $post_id ) ) {
update_post_meta( $post_id, '_msg_email', $email );
update_post_meta( $post_id, '_msg_company', $company );
update_post_meta( $post_id, '_msg_country', $country );
update_post_meta( $post_id, '_msg_mobile', $mobile );
update_post_meta( $post_id, '_msg_phone', $phone );
update_post_meta( $post_id, '_msg_address', $address );
update_post_meta( $post_id, '_msg_gender', $gender );
update_post_meta( $post_id, '_msg_industry', $industry );
update_post_meta( $post_id, '_msg_terms', $terms );
update_post_meta( $post_id, '_msg_text', $message );
update_post_meta( $post_id, '_msg_read', '0' );
}
// Отправляем письмо на admin email (дополнительно)
wp_mail( $to, $subject, $body, $headers );
wp_send_json_success( [ 'message' => 'ok' ] );
}
add_action( 'wp_ajax_tactic_contact', 'tactic_handle_contact_form' );
add_action( 'wp_ajax_nopriv_tactic_contact', 'tactic_handle_contact_form' );
/* ═══════════════════════════════════════════
Дополнительные модули
═══════════════════════════════════════════ */
require_once TACTIC_DIR . '/inc/custom-post-types.php';
require_once TACTIC_DIR . '/inc/meta-boxes.php';
require_once TACTIC_DIR . '/inc/customizer.php';
require_once TACTIC_DIR . '/inc/admin-translations.php';
require_once TACTIC_DIR . '/inc/admin-contact-settings.php';